mirror of
https://git.wownero.com/wownero/YellWOWPages.git
synced 2024-08-15 01:03:25 +00:00
Rewrite to Quart web-framework, refactor code.
This commit is contained in:
parent
6b300fd304
commit
67f4c34604
39 changed files with 656 additions and 980 deletions
13
yellow/__init__.py
Normal file
13
yellow/__init__.py
Normal 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
25
yellow/api.py
Normal 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
28
yellow/auth.py
Normal 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
80
yellow/factory.py
Normal 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
37
yellow/models.py
Normal 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
69
yellow/routes.py
Normal 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
4
yellow/static/colors.css
Normal file
|
@ -0,0 +1,4 @@
|
|||
:root{
|
||||
--yellow: #ffcc00;
|
||||
--purple: #ff2ad4;
|
||||
}
|
1
yellow/static/icon.css
Normal file
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
BIN
yellow/static/wownero.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
49
yellow/templates/about.html
Normal file
49
yellow/templates/about.html
Normal 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
36
yellow/templates/api.html
Normal 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 %}
|
83
yellow/templates/base.html
Normal file
83
yellow/templates/base.html
Normal 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>
|
40
yellow/templates/dashboard.html
Normal file
40
yellow/templates/dashboard.html
Normal 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 %}
|
30
yellow/templates/error.html
Normal file
30
yellow/templates/error.html
Normal 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>
|
25
yellow/templates/includes/nav.html
Normal file
25
yellow/templates/includes/nav.html
Normal 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>
|
17
yellow/templates/index.html
Normal file
17
yellow/templates/index.html
Normal 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 %}
|
59
yellow/templates/search.html
Normal file
59
yellow/templates/search.html
Normal 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 %}
|
60
yellow/templates/search_results.html
Normal file
60
yellow/templates/search_results.html
Normal 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 %}
|
Loading…
Add table
Add a link
Reference in a new issue