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
52
.gitignore
vendored
Normal file
52
.gitignore
vendored
Normal file
|
@ -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
Normal file
19
README.md
Normal file
|
@ -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
5
TODO
|
@ -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
|
|
|
@ -1,8 +0,0 @@
|
||||||
from starlette.templating import Jinja2Templates
|
|
||||||
|
|
||||||
|
|
||||||
class Constraints:
|
|
||||||
templates = Jinja2Templates(directory='frontend/templates')
|
|
||||||
client_id = ''
|
|
||||||
client_secret = ''
|
|
||||||
uri = 'sqlite:///users.db'
|
|
|
@ -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>
|
|
|
@ -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>
|
|
|
@ -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>
|
|
|
@ -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>
|
|
|
@ -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
45
main.py
|
@ -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)
|
|
|
@ -1,30 +1,3 @@
|
||||||
anyio==3.5.0
|
peewee
|
||||||
asgiref==3.5.0
|
quart
|
||||||
asttokens==2.0.5
|
quart_session_openid
|
||||||
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
|
|
||||||
|
|
|
@ -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}')
|
|
|
@ -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
|
|
|
@ -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)
|
|
||||||
|
|
||||||
|
|
|
@ -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')
|
|
|
@ -1,2 +0,0 @@
|
||||||
# stupid fastapi error handling drove me crazy for some hours, updating this asap
|
|
||||||
# the handler is in the main file
|
|
|
@ -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
Normal file
5
run.py
Normal file
|
@ -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
Normal file
23
settings.py_example
Normal file
|
@ -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
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')
|
Before Width: | Height: | Size: 12 KiB 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 %}
|
|
@ -4,10 +4,11 @@
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>YellWOWPages - Sex and Drugs in the metaverse</title>
|
<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="https://unpkg.com/@picocss/pico@latest/css/pico.min.css">
|
||||||
<link rel="stylesheet" href="../../static/colors.css">
|
<link rel="stylesheet" href="{{ url_for('static', filename='colors.css') }}">
|
||||||
<link rel="stylesheet" href="../../static/icon.css">
|
<link rel="stylesheet" href="{{ url_for('static', filename='icon.css') }}">
|
||||||
</head>
|
</head>
|
||||||
<style>
|
<style>
|
||||||
html, body{
|
html, body{
|
||||||
|
@ -69,42 +70,14 @@
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<body>
|
<body>
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<nav>
|
{% include 'includes/nav.html' %}
|
||||||
<ul>
|
|
||||||
<li>
|
{% block content %} {% endblock %}
|
||||||
<div id="dropdown">
|
</div>
|
||||||
<i class="icon icon-menu"></i>
|
|
||||||
<div id="dropdowncontent">
|
<div id="footer">
|
||||||
<p>
|
2022 - ... [the future is w0w]
|
||||||
<a href="/login">Login</a>
|
</div>
|
||||||
<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><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">
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
<div id="footer">
|
|
||||||
2022 - ... [the future is w0w]
|
|
||||||
</div>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</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 %}
|
|
@ -5,7 +5,7 @@
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<meta http-equiv="refresh" content="3; URL={{url}}">
|
<meta http-equiv="refresh" content="3; URL={{url}}">
|
||||||
<title>Such error :(</title>
|
<title>Such {{code}} error :(</title>
|
||||||
<link rel="stylesheet" href="https://unpkg.com/@picocss/pico@latest/css/pico.min.css">
|
<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/colors.css">
|
||||||
<link rel="stylesheet" href="../../static/icon.css">
|
<link rel="stylesheet" href="../../static/icon.css">
|
||||||
|
@ -23,7 +23,7 @@
|
||||||
<body>
|
<body>
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<div id="main">
|
<div id="main">
|
||||||
<p>Error: {{error}}</p>
|
<p>Error {{code}}: {{msg}}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
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…
Reference in a new issue