Browse Source

Rewrite to Quart web-framework, refactor code.

nav-improvements
dsc 9 months ago
parent
commit
67f4c34604
  1. 52
      .gitignore
  2. 19
      README.md
  3. 5
      TODO
  4. 8
      classes/constraints.py
  5. 0
      data/.gitkeep
  6. 108
      frontend/templates/about/api/index.html
  7. 125
      frontend/templates/about/index.html
  8. 100
      frontend/templates/dashboard/index.html
  9. 126
      frontend/templates/yellwow/index.html
  10. 132
      frontend/templates/yellwow/single_user/index.html
  11. 45
      main.py
  12. 33
      requirements.txt
  13. 0
      routers/__init__.py
  14. 32
      routers/api.py
  15. 68
      routers/auth.py
  16. 63
      routers/dashboard.py
  17. 54
      routers/db.py
  18. 2
      routers/errors.py
  19. 39
      routers/static.py
  20. 5
      run.py
  21. 23
      settings.py_example
  22. 13
      yellow/__init__.py
  23. 25
      yellow/api.py
  24. 28
      yellow/auth.py
  25. 80
      yellow/factory.py
  26. 37
      yellow/models.py
  27. 69
      yellow/routes.py
  28. 0
      yellow/static/colors.css
  29. 0
      yellow/static/icon.css
  30. 0
      yellow/static/wownero.png
  31. 49
      yellow/templates/about.html
  32. 36
      yellow/templates/api.html
  33. 55
      yellow/templates/base.html
  34. 40
      yellow/templates/dashboard.html
  35. 4
      yellow/templates/error.html
  36. 25
      yellow/templates/includes/nav.html
  37. 17
      yellow/templates/index.html
  38. 59
      yellow/templates/search.html
  39. 60
      yellow/templates/search_results.html

52
.gitignore vendored

@ -0,0 +1,52 @@ @@ -0,0 +1,52 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
# C extensions
*.so
# Distribution / packaging
.Python
env/
bin/
build/
develop-eggs/
dist/
eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg
.idea
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.cache
nosetests.xml
coverage.xml
# Translations
*.mo
.project
.pydevproject
# Rope
.ropeproject
# Django stuff:
*.log
*.pot
# Sphinx documentation
docs/_build/

19
README.md

@ -0,0 +1,19 @@ @@ -0,0 +1,19 @@
## YellWOWPages
Wownero yellow pages web-application - lookup the WOW address of users.
### Installation
```bash
git clone gitea@git.wownero.com:muchwowmining/YellWOWPages.git
cd YellWOWPages
pip install -r requirements.txt
cp settings.py_example settings.py
```
Change `settings.py` to your liking. Then run the application:
```bash
python3 run.py
```

5
TODO

@ -1,5 +0,0 @@ @@ -1,5 +0,0 @@
remove Constraints.client_secret before publishing and make a backup of it!!
Register new application new login.wownero.com
client_id = yellwowpages
client url = <domain>/authenticate

8
classes/constraints.py

@ -1,8 +0,0 @@ @@ -1,8 +0,0 @@
from starlette.templating import Jinja2Templates
class Constraints:
templates = Jinja2Templates(directory='frontend/templates')
client_id = ''
client_secret = ''
uri = 'sqlite:///users.db'

0
classes/__init__.py → data/.gitkeep

108
frontend/templates/about/api/index.html

@ -1,108 +0,0 @@ @@ -1,108 +0,0 @@
<!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>YellWOWPages - Sex and Drugs in the metaverse</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;
overflow: hidden;
}
a{
color: var(--yellow);
}
span{
color: var(--purple);
}
.nav{
border-color: var(--yellow);
border: 2px;
}
#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;
text-align: center;
}
form{
height: 50px;
}
#footer{
height: 12vh;
width: 100%;
text-align: center;
}
@media (max-width: 800px) {
kbd{
width: 100vw;
}
}
</style>
<body>
<div class="container-fluid">
<nav>
<ul>
<li>
<div id="dropdown">
<i class="icon icon-menu"></i>
<div id="dropdowncontent">
<p>
<a href="/login">Login</a>
<a href="/dashboard">Dashboard</a>
<a href="/yellwowpage">Yell<span>WOW</span>Page</a>
<a href="/about">About</a>
<a href="/about/api">Api</a>
</p>
</div>
</div>
</li>
</ul>
<ul>
<li><strong><a href="/">Yell<span>WOW</span>Pages</a></strong></li>
</ul>
<ul>
<li><a href="https://git.wownero.com/muchwowmining/YellWOWPages"><i class="icon icon-edit"></i></a></li>
</ul>
</nav>
</div>
<div class="container-fluid">
<div id="main">
<h1>About - Api</h1>
<p>
Search user: <code><a href="/api/user/{username}" data-tooltip="no partial search yet">/api/user/{username}</a></code>
<br><br>
Get all users: <code><a href="/api/all">/api/all</a></code>
</p>
</div>
</div>
<div id="footer">
2022 - ... [the future is w0w]
</div>
</body>
</html>

125
frontend/templates/about/index.html

@ -1,125 +0,0 @@ @@ -1,125 +0,0 @@
<!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>YellWOWPages - Sex and Drugs in the metaverse</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;
overflow: hidden;
}
a{
color: var(--yellow);
}
span{
color: var(--purple);
}
.nav{
border-color: var(--yellow);
border: 2px;
}
#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;
text-align: center;
}
form{
height: 50px;
}
#footer{
height: 12vh;
width: 100%;
text-align: center;
}
@media (max-width: 800px) {
kbd{
width: 100vw;
}
}
</style>
<body>
<div class="container-fluid">
<nav>
<ul>
<li>
<div id="dropdown">
<i class="icon icon-menu"></i>
<div id="dropdowncontent">
<p>
<a href="/login">Login</a>
<a href="/dashboard">Dashboard</a>
<a href="/yellwowpage">Yell<span>WOW</span>Page</a>
<a href="/about">About</a>
<a href="/about/api">Api</a>
</p>
</div>
</div>
</li>
</ul>
<ul>
<li><strong><a href="/">Yell<span>WOW</span>Pages</a></strong></li>
</ul>
<ul>
<li><a href="https://git.wownero.com/muchwowmining/YellWOWPages"><i class="icon icon-edit"></i></a></li>
</ul>
</nav>
</div>
<div class="container-fluid">
<div id="main">
<h1>About</h1>
<p>
Search for any Wownero <em>sub-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>
Idea of: dsc_
<br>
Made by <a href="https://notmtth.xyz">NotMtth</a>
</p>
</div>
</div>
<div id="footer">
2022 - ... [the future is w0w]
</div>
</body>
</html>

100
frontend/templates/dashboard/index.html

@ -1,100 +0,0 @@ @@ -1,100 +0,0 @@
<!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>Such dashboard</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;
overflow: hidden;
}
a{
color: var(--yellow);
}
span{
color: var(--purple);
}
.nav{
border-color: var(--yellow);
border: 2px;
}
#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: 85vh;
display: grid;
place-content: center;
}
@media (max-width: 800px) {}
</style>
<body>
<div class="container-fluid">
<nav>
<ul>
<li>
<div id="dropdown">
<i class="icon icon-menu"></i>
<div id="dropdowncontent">
<p>
<a href="/login">Login</a>
<a href="/dashboard">Dashboard</a>
<a href="/yellwowpage">Yell<span>WOW</span>Page</a>
<a href="/about">About</a>
<a href="/about/api">Api</a>
</p>
</div>
</div>
</li>
</ul>
<ul>
<li><strong><a href="/">Yell<span>WOW</span>Pages</a></strong></li>
</ul>
<ul>
<li><a href="https://git.wownero.com/muchwowmining/YellWOWPages"><i class="icon icon-edit"></i></a></li>
</ul>
</nav>
</div>
<div class="container-fluid">
<div id="main">
<article>
{% for username, address in user_data.items() %}
<Header>Welcome back <em>{{username}}</em>!</Header>
Current <u>sub-address</u>: <label><mark>{{address}}</mark></label>
<footer>
Change <u>sub-address</u>:
<form action="/submit_address" method="POST">
<input type="text" name="address">
<button data-tooltip="Be sure it's correct">Submit</button>
</form>
</footer>
{% endfor %}
</article>
</div>
</div>
</body>
</html>

126
frontend/templates/yellwow/index.html

@ -1,126 +0,0 @@ @@ -1,126 +0,0 @@
<!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>YellWOWPages - Sex and Drugs in the metaverse</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;
overflow-x: hidden;
}
a{
color: var(--yellow);
}
span{
color: var(--purple);
}
.nav{
border-color: var(--yellow);
border: 2px;
}
#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;
}
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>
<body>
<div class="container-fluid">
<nav>
<ul>
<li>
<div id="dropdown">
<i class="icon icon-menu"></i>
<div id="dropdowncontent">
<p>
<a href="/login">Login</a>
<a href="/dashboard">Dashboard</a>
<a href="/yellwowpage">Yell<span>WOW</span>Page</a>
<a href="/about">About</a>
<a href="/about/api">Api</a>
</p>
</div>
</div>
</li>
</ul>
<ul>
<li><strong><a href="/">Yell<span>WOW</span>Pages</a></strong></li>
</ul>
<ul>
<li><a href="https://git.wownero.com/muchwowmining/YellWOWPages"><i class="icon icon-edit"></i></a></li>
</ul>
</nav>
</div>
<div class="container-fluid">
<div id="main">
<form action="/search" method="GET">
<input type="text" name="username" placeholder="Username to search">
<label for="switch">
<input type="checkbox" name="switch" role="switch">
<em data-tooltip="Search address with a part of username; get 3 results">Partial</em>
</label>
</form>
<div id="addresses">
{% for username, address in user_data.items() %}
<article>
<header>
<em>{{username}}</em>
</header>
<kbd>{{address}}</kbd>
</article>
{% endfor %}
</div>
</div>
</div>
<div id="footer">
2022 - ... [the future is w0w]
</div>
</body>
</html>

132
frontend/templates/yellwow/single_user/index.html

@ -1,132 +0,0 @@ @@ -1,132 +0,0 @@
<!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>YellWOWPages - Sex and Drugs in the metaverse</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;
overflow: hidden;
}
a{
color: var(--yellow);
}
span{
color: var(--purple);
}
.nav{
border-color: var(--yellow);
border: 2px;
}
#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;
}
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>
<body>
<div class="container-fluid">
<nav>
<ul>
<li>
<div id="dropdown">
<i class="icon icon-menu"></i>
<div id="dropdowncontent">
<p>
<a href="/login">Login</a>
<a href="/dashboard">Dashboard</a>
<a href="/yellwowpage">Yell<span>WOW</span>Page</a>
<a href="/about">About</a>
<a href="/about/api">Api</a>
</p>
</div>
</div>
</li>
</ul>
<ul>
<li><strong><a href="/">Yell<span>WOW</span>Pages</a></strong></li>
</ul>
<ul>
<li><a href="https://git.wownero.com/muchwowmining/YellWOWPages"><i class="icon icon-edit"></i></a></li>
</ul>
</nav>
</div>
<div class="container-fluid">
<div id="main">
<form action="/search" method="GET">
<input type="text" name="username" placeholder="Username to search">
<label for="switch">
<input type="checkbox" name="switch" role="switch">
<em data-tooltip="Search address with a part of username; get 3 results">Partial</em>
</label>
</form>
<br>
Result: {{user_data|length}}
{% if not user_data|length %}
Nothing found...
{% else %}
<div id="addresses">
{% for username, address in user_data.items() %}
<article>
<header>
<em>{{username}}</em>
</header>
<kbd>{{address}}</kbd>
</article>
{% endfor %}
</div>
{% endif %}
</div>
</div>
<div id="footer">
2022 - ... [the future is w0w]
</div>
</body>
</html>

45
main.py

@ -1,45 +0,0 @@ @@ -1,45 +0,0 @@
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
import uvicorn
from classes.constraints import Constraints
from starlette.exceptions import HTTPException as StarletteHTTPException
app = FastAPI(docs_url=None, redoc_url=None)
app.mount('/static', StaticFiles(directory='frontend/static'), name='static')
from routers import auth, static, dashboard, db, api
app.include_router(auth.router)
app.include_router(dashboard.router)
app.include_router(static.router)
app.include_router(api.router)
@app.get("/")
@app.get('/root', response_class=HTMLResponse)
async def root(request: Request):
return Constraints.templates.TemplateResponse('/root/index.html', {'request': request})
# shitty error handling
@app.exception_handler(StarletteHTTPException)
async def http_exception_handler(request, exc):
if exc.status_code == 404:
return Constraints.templates.TemplateResponse('/errors/index.html', {'request': request,
'error': 'not found...',
'url': '/root'})
elif exc.status_code == 500:
return Constraints.templates.TemplateResponse('/errors/index.html', {'request': request,
'error': 'internal server error',
'url': '/root'})
return Constraints.templates.TemplateResponse('/errors/index.html', {'request': request,
'error': exc.detail,
'url': '/root'})
if __name__ == '__main__':
uvicorn.run(app, host='127.0.0.1', port=8080)

33
requirements.txt

@ -1,30 +1,3 @@ @@ -1,30 +1,3 @@
anyio==3.5.0
asgiref==3.5.0
asttokens==2.0.5
certifi==2021.10.8
charset-normalizer==2.0.12
click==8.0.4
colorama==0.4.4
executing==0.8.3
fastapi==0.75.0
Flask==2.0.3
Flask-SQLAlchemy==2.5.1
greenlet==1.1.2
h11==0.13.0
icecream==2.1.2
idna==3.3
itsdangerous==2.1.1
Jinja2==3.0.3
MarkupSafe==2.1.0
pydantic==1.9.0
Pygments==2.11.2
python-multipart==0.0.5
requests==2.27.1
six==1.16.0
sniffio==1.2.0
SQLAlchemy==1.4.32
starlette==0.17.1
typing-extensions==4.1.1
urllib3==1.26.8
uvicorn==0.17.5
Werkzeug==2.0.3
peewee
quart
quart_session_openid

0
routers/__init__.py

32
routers/api.py

@ -1,32 +0,0 @@ @@ -1,32 +0,0 @@
from fastapi import APIRouter, Request, Form
from fastapi.responses import RedirectResponse, HTMLResponse
from icecream import ic
from classes.constraints import Constraints
from .db import Database
router = APIRouter()
# external api
@router.get('/api/user/{username}')
async def get_api(request: Request, username: str):
if not Database.Users.get_address(username):
return {'error': 'invalid user'}
return Database.Users.get_address(username)
@router.get('/api/all')
async def get_api_all(request: Request):
return Database.Users.get_all()
# site search redirect
@router.get('/search')
async def search_api(request: Request):
username = request.query_params['username']
ic(request.query_params.get('switch', None) == 'on')
if request.query_params.get('switch', None) == 'on':
return RedirectResponse(f'/yellwowpage/matches/{username}')
else:
return RedirectResponse(f'/yellwowpage/user/{username}')

68
routers/auth.py

@ -1,68 +0,0 @@ @@ -1,68 +0,0 @@
from fastapi import APIRouter, Request, Cookie
from fastapi.responses import RedirectResponse, HTMLResponse
import requests
from icecream import ic
import secrets
from classes.constraints import Constraints
router = APIRouter()
@router.get('/login')
async def get_login(request: Request):
state = secrets.token_hex(10)
response = RedirectResponse(
'https://login.wownero.com/auth/realms/master/protocol/openid-connect/auth?'f'client_id='
f'{Constraints.client_id}&redirect_uri=https://yellow.wownero.com/authenticate&'
f'response_type=code&state={state}')
response.set_cookie(key='state', value=state)
return response
@router.get('/authenticate')
async def get_auth(request: Request, state: str = Cookie(None)):
params = request.query_params
if state is None:
return Constraints.templates.TemplateResponse('/errors/index.html',
{'request': request,
'error': '`state` security code not found...',
'url': '/login'})
if params['state'] != state:
return Constraints.templates.TemplateResponse('/errors/index.html',
{'request': request,
'error': '`state` security code is wrong',
'url': '/login'})
url = "https://login.wownero.com/auth/realms/master/protocol/openid-connect/token"
data = {
"grant_type": "authorization_code",
"code": params["code"],
"redirect_uri": "http://127.0.0.1:8080/authenticate",
"client_id": f'{Constraints.client_id}',
"client_secret": f'{Constraints.client_secret}',
"state": params['state']
}
r = requests.post(url=url, data=data)
response = r.json()
if response.get('error', None) is not None:
return Constraints.templates.TemplateResponse('/errors/index.html',
{'request': request, 'error': r.json()['error_description'],
'url': '/login'})
auth_code = response.get('access_token', None)
if auth_code is None:
return Constraints.templates.TemplateResponse('/errors/index.html',
{'request': request, 'error': 'invalid auth code',
'url': '/login'})
response = RedirectResponse('/dashboard')
response.set_cookie(key='auth_code', value=auth_code)
response.delete_cookie(key='state')
return response
@router.get('/logout')
async def get_logout():
response = RedirectResponse('/root')
response.delete_cookie('auth_code')
return response

63
routers/dashboard.py

@ -1,63 +0,0 @@ @@ -1,63 +0,0 @@
from fastapi import APIRouter, Request, Cookie, Form
from fastapi.responses import RedirectResponse, HTMLResponse
import requests
from icecream import ic
from classes.constraints import Constraints
from .db import Database
router = APIRouter()
@router.get('/dashboard', response_class=HTMLResponse)
async def get_dashboard(request: Request, auth_code: str = Cookie(None)):
if auth_code is None:
return Constraints.templates.TemplateResponse('/errors/index.html', {'request': request,
'error': 'not logged in',
'url': '/login'})
url = "https://login.wownero.com/auth/realms/master/protocol/openid-connect/userinfo"
response = requests.post(url, headers={"Authorization": f"Bearer {auth_code}"})
user_profile = response.json()
if user_profile.get('preferred_username', None) is None:
return Constraints.templates.TemplateResponse('/errors/index.html', {'request': request,
'error': 'account not found...',
'url': '/login'})
user_name = user_profile.get('preferred_username', None)
return Constraints.templates.TemplateResponse('/dashboard/index.html', {'request': request,
'user_data': Database.Users.get_address(user_name)})
@router.post('/submit_address')
async def post_submit_address(request: Request, auth_code: str = Cookie(None), address: str = Form(None)):
if auth_code is None:
return Constraints.templates.TemplateResponse('/errors/index.html', {'request': request,
'error': 'not logged in',
'url': '/login'})
if address is None:
return Constraints.templates.TemplateResponse('/errors/index.html', {'request': request,
'error': 'invalid address',
'url': '/dashboard'})
if len(address) != 97:
return Constraints.templates.TemplateResponse('/errors/index.html', {'request': request,
'error': 'invalid address length',
'url': '/dashboard'})
url = "https://login.wownero.com/auth/realms/master/protocol/openid-connect/userinfo"
response = requests.post(url, headers={"Authorization": f"Bearer {auth_code}"})
user_name = response.json().get('preferred_username', None)
if not Database.Users.get_address(user_name):
new_user = Database.Users(username=user_name, address=address)
Database.sqla.session.add(new_user)
Database.sqla.session.commit()
return RedirectResponse('/dashboard', status_code=303)
update_address = Database.Users.query.filter_by(username=user_name).first()
update_address.address = address
Database.sqla.session.commit()
return RedirectResponse('/dashboard', status_code=303)

54
routers/db.py

@ -1,54 +0,0 @@ @@ -1,54 +0,0 @@
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from icecream import install, ic
from classes.constraints import Constraints
app = Flask(__name__)
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['SQLALCHEMY_DATABASE_URI'] = Constraints.uri
db = SQLAlchemy(app, session_options={'autocommit': False})
class Users(db.Model):
__tablename__ = 'wowusers'
username = db.Column(db.VARCHAR(32), primary_key=True)
address = db.Column(db.CHAR(97), nullable=False)
@staticmethod
def get_address(username):
user_data = {}
try:
user_data.update({Users.query.filter_by(username=username).first().username:
Users.query.filter_by(username=username).first().address})
except AttributeError:
return user_data
return user_data
@staticmethod
def get_all():
users_data = {}
for user in Users.query.all():
users_data.update({user.username: user.address})
return users_data
@staticmethod
def get_matches(username):
user_data = {}
try:
for i in range(3):
user = Users.query.filter(Database.Users.username.like(f'%{username}%'))[i]
user_data.update({user.username: user.address})
except IndexError:
return user_data
return user_data
class Database:
Users = Users
sqla = db
db.create_all()
ic('db done')

2
routers/errors.py

@ -1,2 +0,0 @@ @@ -1,2 +0,0 @@
# stupid fastapi error handling drove me crazy for some hours, updating this asap
# the handler is in the main file

39
routers/static.py

@ -1,39 +0,0 @@ @@ -1,39 +0,0 @@
from fastapi import APIRouter, Request
from fastapi.responses import RedirectResponse, HTMLResponse
from icecream import ic
from classes.constraints import Constraints
from .db import Database
router = APIRouter()
@router.get('/yellwowpage')
async def get_yellwowpage(request: Request):
return Constraints.templates.TemplateResponse('/yellwow/index.html',
{'request': request, 'user_data': Database.Users.get_all()})
@router.get('/yellwowpage/user/{username}')
async def get_yellwowpage(request: Request, username: str):
return Constraints.templates.TemplateResponse('/yellwow/single_user/index.html',
{'request': request,
'user_data': Database.Users.get_address(username)})
@router.get('/yellwowpage/matches/{username}')
async def get_yellwowpage_matches(request: Request, username: str):
ic(Database.Users.get_matches(username))
return Constraints.templates.TemplateResponse('/yellwow/single_user/index.html',
{'request': request,
'user_data': Database.Users.get_matches(username)})
@router.get('/about')
async def get_about(request: Request):
return Constraints.templates.TemplateResponse('/about/index.html', {'request': request})
@router.get('/about/api')
async def get_api_about(request: Request):
return Constraints.templates.TemplateResponse('/about/api/index.html', {'request': request})

5
run.py

@ -0,0 +1,5 @@ @@ -0,0 +1,5 @@
from yellow.factory import create_app
import settings
app = create_app()
app.run(settings.HOST, port=settings.PORT, debug=settings.DEBUG, use_reloader=False)

23
settings.py_example

@ -0,0 +1,23 @@ @@ -0,0 +1,23 @@
import os
cwd = os.path.dirname(os.path.realpath(__file__))
def bool_env(val):
return val is True or (isinstance(val, str) and (val.lower() == 'true' or val == '1'))
DEBUG = bool_env(os.environ.get("DEBUG", False))
HOST = os.environ.get("HOST", "127.0.0.1")
PORT = int(os.environ.get("PORT", 8080))
APP_SECRET = os.environ.get("APP_SECRET")
REDIS_URI = os.environ.get("REDIS_URI", "redis://localhost:6379")
DB_PATH = os.path.join(cwd, "data", "db.sqlite3")
OPENID_CFG = {
"client_id": "",
"client_secret": "",
"configuration": "https://login.wownero.com/auth/realms/master/.well-known/openid-configuration"
}

13
yellow/__init__.py

@ -0,0 +1,13 @@ @@ -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

@ -0,0 +1,25 @@ @@ -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

@ -0,0 +1,28 @@ @@ -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

@ -0,0 +1,80 @@ @@ -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

@ -0,0 +1,37 @@ @@ -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

@ -0,0 +1,69 @@ @@ -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')

0
frontend/static/colors.css → yellow/static/colors.css

0
frontend/static/icon.css → yellow/static/icon.css

0
frontend/static/wownero.png → yellow/static/wownero.png

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

49
yellow/templates/about.html

@ -0,0 +1,49 @@ @@ -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>