Rewrite to Quart web-framework, refactor code.

This commit is contained in:
dsc 2022-03-12 14:46:31 +02:00
parent 6b300fd304
commit 67f4c34604
39 changed files with 656 additions and 980 deletions

13
yellow/__init__.py Normal file
View file

@ -0,0 +1,13 @@
from quart import session, abort
from functools import wraps
def login_required(func):
@wraps(func)
async def wrapper(*args, **kwargs):
user = session.get('user')
if not isinstance(user, dict):
abort(403)
return await func(*args, **kwargs)
return wrapper

25
yellow/api.py Normal file
View file

@ -0,0 +1,25 @@
from quart import render_template, request, redirect, url_for, jsonify, Blueprint, abort, flash, send_from_directory, current_app
import settings
from yellow.models import User
bp_api = Blueprint('bp_api', __name__, url_prefix='/api')
@bp_api.get("/")
async def api_root():
return await render_template('api.html')
@bp_api.get('/user/')
async def api_all():
return jsonify([u.to_json(ignore_key='id') for u in User.select()])
@bp_api.get('/user/<path:needle>')
async def api_search(needle: str):
try:
return jsonify([u.to_json(ignore_key='id') for u in await User.search(needle)])
except Exception as ex:
current_app.logger.error(ex)
return jsonify([])

28
yellow/auth.py Normal file
View file

@ -0,0 +1,28 @@
import peewee
from quart import session, redirect, url_for
from yellow.factory import openid
from yellow.models import User
@openid.after_token()
async def handle_user_login(resp: dict):
access_token = resp["access_token"]
openid.verify_token(access_token)
user = await openid.user_info(access_token)
username = user['preferred_username']
uid = user['sub']
try:
user = User.select().where(User.id == uid).get()
except peewee.DoesNotExist:
user = None
if not user:
# create new user if it does not exist yet
user = User.create(id=uid, username=username)
# user is now logged in
session['user'] = user.to_json()
return redirect(url_for('bp_routes.root'))

80
yellow/factory.py Normal file
View file

@ -0,0 +1,80 @@
import os
import logging
import asyncio
from quart import Quart, url_for, jsonify, render_template, session
from quart_session_openid import OpenID
from quart_session import Session
import settings
app: Quart = None
peewee = None
cache = None
openid: OpenID = None
async def _setup_database(app: Quart):
import peewee
import yellow.models
models = peewee.Model.__subclasses__()
for m in models:
m.create_table()
async def _setup_openid(app: Quart):
global openid
openid = OpenID(app, **settings.OPENID_CFG)
from yellow.auth import handle_user_login
async def _setup_cache(app: Quart):
global cache
app.config['SESSION_TYPE'] = 'redis'
app.config['SESSION_URI'] = settings.REDIS_URI
Session(app)
async def _setup_error_handlers(app: Quart):
@app.errorhandler(500)
async def page_error(e):
return await render_template('error.html', code=500, msg="Error occurred"), 500
@app.errorhandler(403)
async def page_forbidden(e):
return await render_template('error.html', code=403, msg="Forbidden"), 403
@app.errorhandler(404)
async def page_not_found(e):
return await render_template('error.html', code=404, msg="Page not found"), 404
def create_app():
global app
app = Quart(__name__)
app.logger.setLevel(logging.INFO)
app.secret_key = settings.APP_SECRET
@app.context_processor
def template_variables():
global openid
from yellow.models import User
current_user = session.get('user')
if current_user:
current_user = User(**current_user)
return dict(user=current_user, url_login=openid.endpoint_name_login)
@app.before_serving
async def startup():
await _setup_cache(app)
await _setup_openid(app)
await _setup_database(app)
await _setup_error_handlers(app)
from yellow.routes import bp_routes
from yellow.api import bp_api
app.register_blueprint(bp_routes)
app.register_blueprint(bp_api)
return app

37
yellow/models.py Normal file
View file

@ -0,0 +1,37 @@
import os, re, random
from typing import Optional, List
from datetime import datetime
from peewee import SqliteDatabase, SQL, ForeignKeyField
import peewee as pw
import settings
db = SqliteDatabase(settings.DB_PATH)
class User(pw.Model):
id = pw.UUIDField(primary_key=True)
created = pw.DateTimeField(default=datetime.now)
username = pw.CharField(unique=True, null=False)
address = pw.CharField(null=True)
@staticmethod
async def search(needle) -> List['User']:
needle = needle.replace("*", "")
if len(needle) <= 2:
raise Exception("need longer search term")
return User.select().where(User.username % f"*{needle}*")
def to_json(self, ignore_key=None):
data = {
"id": self.id,
"username": self.username,
"address": self.address
}
if isinstance(ignore_key, str):
data.pop(ignore_key)
return data
class Meta:
database = db

69
yellow/routes.py Normal file
View file

@ -0,0 +1,69 @@
from quart import render_template, request, redirect, url_for, jsonify, Blueprint, abort, flash, send_from_directory, session
from yellow import login_required
from yellow.factory import openid
from yellow.models import User
bp_routes = Blueprint('bp_routes', __name__)
@bp_routes.get("/")
async def root():
return await render_template('index.html')
@bp_routes.route("/login")
async def login():
return redirect(url_for(openid.endpoint_name_login))
@bp_routes.route("/logout")
@login_required
async def logout():
session['user'] = None
return redirect(url_for('bp_routes.root'))
@bp_routes.route("/dashboard")
@login_required
async def dashboard():
return await render_template('dashboard.html')
@bp_routes.post("/dashboard/address")
@login_required
async def dashboard_address_post():
# get FORM POST value 'address'
form = await request.form
address = form.get('address')
if len(address) != 97:
raise Exception("Please submit a WOW address")
# update user
from yellow.models import User
user = User.select().filter(User.id == session['user']['id']).get()
user.address = address
user.save()
session['user'] = user.to_json()
return await render_template('dashboard.html')
@bp_routes.route("/search")
async def search():
needle = request.args.get('username')
if needle:
if len(needle) <= 2:
raise Exception("Search term needs to be longer")
users = [u for u in await User.search(needle)]
if users:
return await render_template('search_results.html', users=users)
users = [u for u in User.select()]
return await render_template('search.html', users=users)
@bp_routes.route("/about")
async def about():
return await render_template('about.html')

4
yellow/static/colors.css Normal file
View file

@ -0,0 +1,4 @@
:root{
--yellow: #ffcc00;
--purple: #ff2ad4;
}

1
yellow/static/icon.css Normal file

File diff suppressed because one or more lines are too long

BIN
yellow/static/wownero.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View file

@ -0,0 +1,49 @@
{% extends "base.html" %}
{% block content %}
<div style="display:none">
{% block title %}YellWOWPages - About{% endblock %}
</div>
<div id="main">
<h1>About</h1>
<p>
Search for any Wownero <em>address</em> you want by username and pay
the world!
<br>
This application uses <u>Wownero's Centralized Authentication Service.</u>
</p>
<p>
Other Wownero related stuff:
<br>
<a href="https://wownero.org/">WebSite</a>
<br>
<a href="https://suchwow.xyz">SuchWow</a>
<br>
<a href="https://git.wownero.com">Official Git</a>
<br>
<a href="https://discord.com/invite/ykZyAzJhDK">Discord server</a>
</p>
<p>
Made by <a href="https://notmtth.xyz">NotMtth</a> and <code>dsc</code>
</p>
</div>
<style>
#main{
width: 100%;
height: 80vh;
display: grid;
place-content: center;
text-align: center;
}
form{
height: 50px;
}
@media (max-width: 800px) {
kbd{
width: 100vw;
}
}
</style>
{% endblock %}

36
yellow/templates/api.html Normal file
View file

@ -0,0 +1,36 @@
{% extends "base.html" %}
{% block content %}
<div style="display:none">
{% block title %}YellWOWPages - API{% endblock %}
</div>
<div id="main">
<h1>API</h1>
<p>
Search user: <code><a href="/api/user/dsc" data-tooltip="partial search supported">/api/user/{username}</a></code>
<br><br>
Get all users: <code><a href="/api/user/">/api/user/</a></code>
</p>
</div>
<style>
#main {
width: 100%;
height: 80vh;
display: grid;
place-content: center;
text-align: center;
}
form {
height: 50px;
}
@media (max-width: 800px) {
kbd {
width: 100vw;
}
}
</style>
{% endblock %}

View file

@ -0,0 +1,83 @@
<!DOCTYPE html>
<html lang="en" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}YellWOWPages - Sex and Drugs in the metaverse{% endblock %}</title>
<link rel="stylesheet" href="https://unpkg.com/@picocss/pico@latest/css/pico.min.css">
<link rel="stylesheet" href="{{ url_for('static', filename='colors.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='icon.css') }}">
</head>
<style>
html, body{
font-family: 'Courier New', Courier, monospace;
overflow-x: hidden;
}
a{
color: var(--yellow);
}
span{
color: var(--purple);
}
strong{
color: var(--yellow);
}
#dropdown{
position: relative;
display: inline-block;
}
#dropdowncontent{
display: none;
top: 0;
position: absolute;
background-color: var(--table-border-color);
min-width: 160px;
box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
padding: 30px 50px;
color: var(--yellow);
text-align: center;
z-index: 1;
}
#dropdown:hover #dropdowncontent {
display: block;
}
#main{
width: 100%;
height: 80vh;
display: grid;
place-content: center;
}
main{
font-size: bold;
font-size: 10rem;
}
img{
width: 50px;
height: 50px;
object-fit: contain;
}
#footer{
height: 12vh;
width: 100%;
text-align: center;
}
@media (max-width: 800px) {
main{
font-size: 4rem;
}
}
</style>
<body>
<div class="container-fluid">
{% include 'includes/nav.html' %}
{% block content %} {% endblock %}
</div>
<div id="footer">
2022 - ... [the future is w0w]
</div>
</body>
</html>

View file

@ -0,0 +1,40 @@
{% extends "base.html" %}
{% block content %}
<div style="display:none">
{% block title %}YellWOWPages - Dashboard{% endblock %}
</div>
<div id="main">
<article>
<Header>Welcome back <em>{{user.username}}</em>!</Header>
Current <u>WOW address</u>: <label>
{% if user.address %}
<mark>{{user.address}}</mark>
{% else %}
<mark>empty</mark>
{% endif %}
</label>
<footer>
Change <u>WOW address</u>:
<form action="{{ url_for('bp_routes.dashboard_address_post') }}" method="POST">
<input type="text" name="address">
<button data-tooltip="Be sure it's correct">Submit</button>
</form>
</footer>
</article>
</div>
<style>
#main {
width: 100%;
height: 85vh;
display: grid;
place-content: center;
}
@media (max-width: 800px) {
}
</style>
{% endblock %}

View file

@ -0,0 +1,30 @@
<!DOCTYPE html>
<html lang="en" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="refresh" content="3; URL={{url}}">
<title>Such {{code}} error :(</title>
<link rel="stylesheet" href="https://unpkg.com/@picocss/pico@latest/css/pico.min.css">
<link rel="stylesheet" href="../../static/colors.css">
<link rel="stylesheet" href="../../static/icon.css">
</head>
<style>
html, body{
font-family: 'Courier New', Courier, monospace;
}
#main{
height: 100vh;
display: grid;
place-content: center;
}
</style>
<body>
<div class="container-fluid">
<div id="main">
<p>Error {{code}}: {{msg}}</p>
</div>
</div>
</body>
</html>

View file

@ -0,0 +1,25 @@
<nav>
<ul>
<li>
<div id="dropdown">
<i class="icon icon-menu"></i>
<div id="dropdowncontent">
<p>
{% if not user %}
<a href="{{ url_for('bp_routes.login') }}">Login</a>
{% else %}
<a href="{{ url_for('bp_routes.logout') }}">Logout</a>
{% endif %}
<a href="{{ url_for('bp_routes.dashboard') }}">Dashboard</a>
<a href="{{ url_for('bp_routes.search') }}">Yell<span>WOW</span>Page search</a>
<a href="{{ url_for('bp_routes.about') }}">About</a>
<a href="{{ url_for('bp_api.api_root') }}">Api</a>
</p>
</div>
</div>
</li>
</ul>
<ul>
<li><a href="https://git.wownero.com/muchwowmining/YellWOWPages"><i class="icon icon-edit"></i></a></li>
</ul>
</nav>

View file

@ -0,0 +1,17 @@
{% extends "base.html" %}
{% block content %}
<div style="display:none">
{% block title %}YellWOWPages - Sex and Drugs in the metaverse{% endblock %}
</div>
<div id="main">
<main>
<strong>Yell<span>WOW</span>Pages</strong>
</main>
<div>
The first <img src="../../static/wownero.png" alt=""> addresses library -
from the community to the community
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,59 @@
{% extends "base.html" %}
{% block content %}
<div style="display:none">
{% block title %}YellWOWPages - Yellwow{% endblock %}
</div>
<div id="main">
<form action="{{ url_for('bp_routes.search') }}" method="GET">
<input type="text" name="username" placeholder="Search for an username...">
</form>
<div id="addresses">
{% for user in users %}
<article>
<header>
<em>{{user.username}}</em>
</header>
<kbd>{{user.address}}</kbd>
</article>
{% endfor %}
</div>
</div>
<style>
#main {
width: 100%;
height: 80vh;
display: grid;
place-content: center;
}
form {
height: 80px;
}
#addresses {
width: 100%;
height: 54vh;
overflow-y: auto;
}
#addresses::-webkit-scrollbar {
display: none;
}
#footer {
height: 12vh;
width: 100%;
text-align: center;
}
@media (max-width: 800px) {
kbd {
width: 100vw;
}
}
</style>
{% endblock %}

View file

@ -0,0 +1,60 @@
{% extends "base.html" %}
{% block content %}
<div style="display:none">
{% block title %}YellWOWPages - User{% endblock %}
</div>
<div id="main">
<form action="{{ url_for('bp_routes.search') }}" method="GET">
<input type="text" name="username" placeholder="Username to search">
</form>
<br>
Result(s): {{users|length}}
{% if not users %}
Nothing found...
{% else %}
<div id="addresses">
{% for user in users %}
<article>
<header>
<em>{{user.username}}</em>
</header>
<kbd>{{user.address}}</kbd>
</article>
{% endfor %}
</div>
{% endif %}
</div>
<style>
#main{
width: 100%;
height: 80vh;
display: grid;
place-content: center;
}
form{
height: 80px;
}
#addresses{
width: 100%;
height: 50vh;
overflow-y: auto;
}
#addresses::-webkit-scrollbar{
display: none;
}
#footer{
height: 12vh;
width: 100%;
text-align: center;
}
@media (max-width: 800px) {
kbd{
width: 100vw;
}
}
</style>
{% endblock %}