init skelly
parent
e00284bc7e
commit
637f213d4a
@ -1,2 +1 @@
|
|||||||
# lza-quart-app
|
# web3-flipbook
|
||||||
Template project for Quart (Python/Flask) applications.
|
|
@ -1,7 +0,0 @@
|
|||||||
from myapp.factory import create_app
|
|
||||||
|
|
||||||
|
|
||||||
app = create_app()
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
app.run()
|
|
@ -1,16 +0,0 @@
|
|||||||
version: '3'
|
|
||||||
services:
|
|
||||||
db:
|
|
||||||
image: postgres:9.6.15-alpine
|
|
||||||
ports:
|
|
||||||
- 127.0.0.1:5432:5432
|
|
||||||
environment:
|
|
||||||
POSTGRES_PASSWORD: ${DB_PASS}
|
|
||||||
POSTGRES_USER: ${DB_USER:-myapp}
|
|
||||||
POSTGRES_DB: ${DB_NAME:-myapp}
|
|
||||||
volumes:
|
|
||||||
- ${DATA_DIR:-./data/postgresql}:/var/lib/postgresql/data
|
|
||||||
cache:
|
|
||||||
image: redis:latest
|
|
||||||
ports:
|
|
||||||
- 127.0.0.1:6379:6379
|
|
@ -1,16 +1,9 @@
|
|||||||
DB_PASS=xxxxxxxxxxxxxxxxxxx
|
DB_PASS=xxxxxxxxxxxxxxxxxxx
|
||||||
DB_USER=myapp
|
DB_USER=flipbook
|
||||||
DB_NAME=myapp
|
DB_NAME=flipbook
|
||||||
DB_HOST=localhost
|
DB_HOST=localhost
|
||||||
|
WEB3_PROVIDER_URI=wss://ropsten.infura.io/ws/v3/xxxx
|
||||||
XMR_WALLET_PATH=/data/xmr-wallet
|
SITE_NAME=flipbook
|
||||||
XMR_WALLET_PASS=xxxxxxxxxxxxxxxxxxx
|
|
||||||
XMR_WALLET_RPC_USER=xxxxxxxxxx
|
|
||||||
XMR_WALLET_RPC_PASS=xxxxxxxxxxxxxxxxxxx
|
|
||||||
XMR_WALLET_RPC_ENDPOINT=http://localhost:9090
|
|
||||||
XMR_DAEMON_URI=http://super.fast.node.xmr.pm:38089
|
|
||||||
|
|
||||||
SITE_NAME=myapp
|
|
||||||
SECRET_KEY=xxxxxxxxxxxxxxxxxxx
|
SECRET_KEY=xxxxxxxxxxxxxxxxxxx
|
||||||
STATS_TOKEN=xxxxxxxxxxxxxxxxxxxx
|
STATS_TOKEN=xxxxxxxxxxxxxxxxxxxx
|
||||||
SERVER_NAME=localhost:5000
|
SERVER_NAME=localhost:5000
|
||||||
|
@ -0,0 +1 @@
|
|||||||
|
from flipbook.factory import create_app
|
@ -0,0 +1,30 @@
|
|||||||
|
import click
|
||||||
|
from quart import Blueprint, current_app
|
||||||
|
|
||||||
|
# from flipbook.models import MyThing
|
||||||
|
from flipbook.factory import db
|
||||||
|
|
||||||
|
|
||||||
|
bp = Blueprint('filters', 'filters')
|
||||||
|
|
||||||
|
@bp.cli.command('init')
|
||||||
|
def init():
|
||||||
|
import app.models
|
||||||
|
db.create_all()
|
||||||
|
|
||||||
|
# @bp.cli.command('delete')
|
||||||
|
# @click.argument('thing_id')
|
||||||
|
# def delete(thing_id):
|
||||||
|
# thing = MyThing.query.get(thing_id)
|
||||||
|
# if thing:
|
||||||
|
# db.session.delete(thing)
|
||||||
|
# db.session.commit()
|
||||||
|
# click.echo(f'MyThing {thing.id} was deleted')
|
||||||
|
# else:
|
||||||
|
# click.echo('MyThing ID does not exist')
|
||||||
|
|
||||||
|
# @bp.cli.command('list')
|
||||||
|
# def list_things():
|
||||||
|
# thing = MyThing.query.all()
|
||||||
|
# for i in thing:
|
||||||
|
# click.echo(i.id)
|
@ -0,0 +1,48 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
from json import loads
|
||||||
|
from secrets import token_urlsafe
|
||||||
|
from os import getenv
|
||||||
|
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
# Site meta
|
||||||
|
SITE_NAME = getenv('SITE_NAME', 'Flipbook')
|
||||||
|
SECRET_KEY = getenv('SECRET_KEY', token_urlsafe(12))
|
||||||
|
SERVER_NAME = getenv('SERVER_NAME', '127.0.0.1:5000')
|
||||||
|
|
||||||
|
# Web3
|
||||||
|
WEB3_PROVIDER_URI = getenv('WEB3_PROVIDER_URI')
|
||||||
|
CONTRACT_ABI = loads(Path('flipbook/library/abi/flipbook.json').open().read())
|
||||||
|
CONTRACT_ADDRESS = getenv('CONTRACT_ADDRESS')
|
||||||
|
|
||||||
|
# Uploads
|
||||||
|
MAX_CONTENT_LENGTH = 50 * 1024 * 1024
|
||||||
|
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'svg'}
|
||||||
|
UPLOADS_PATH = getenv('UPLOADS_PATH', 'data/uploads')
|
||||||
|
SESSION_LIFETIME = getenv('SESSION_LIFETIME', 30)
|
||||||
|
|
||||||
|
# Database
|
||||||
|
DB_HOST = getenv('DB_HOST', 'localhost')
|
||||||
|
DB_PORT = getenv('DB_PORT', 5432)
|
||||||
|
DB_NAME = getenv('DB_NAME', 'flipbook')
|
||||||
|
DB_USER = getenv('DB_USER', 'flipbook')
|
||||||
|
DB_PASS = getenv('DB_PASS')
|
||||||
|
|
||||||
|
# Redis
|
||||||
|
REDIS_HOST = getenv('REDIS_HOST', 'localhost')
|
||||||
|
REDIS_PORT = getenv('REDIS_PORT', 6379)
|
||||||
|
|
||||||
|
# Development
|
||||||
|
TEMPLATES_AUTO_RELOAD = True
|
||||||
|
DEBUG = False
|
||||||
|
if SERVER_NAME == '127.0.0.1:5000':
|
||||||
|
DEBUG = True
|
||||||
|
|
||||||
|
# Twitter
|
||||||
|
TWITTER_CONSUMER_KEY = getenv('TWITTER_CONSUMER_KEY', None)
|
||||||
|
TWITTER_CONSUMER_SECRET = getenv('TWITTER_CONSUMER_SECRET', None)
|
||||||
|
TWITTER_ACCESS_TOKEN = getenv('TWITTER_ACCESS_TOKEN', None)
|
||||||
|
TWITTER_ACCESS_SECRET = getenv('TWITTER_ACCESS_SECRET', None)
|
@ -0,0 +1,37 @@
|
|||||||
|
import quart.flask_patch
|
||||||
|
from quart import Quart
|
||||||
|
from flask_login import LoginManager
|
||||||
|
|
||||||
|
from flipbook import config
|
||||||
|
|
||||||
|
|
||||||
|
async def setup_db(app: Quart):
|
||||||
|
import peewee
|
||||||
|
import flipbook.models
|
||||||
|
models = peewee.Model.__subclasses__()
|
||||||
|
for m in models:
|
||||||
|
m.create_table()
|
||||||
|
|
||||||
|
def create_app():
|
||||||
|
app = Quart(__name__)
|
||||||
|
app.config.from_envvar('FLASK_SECRETS')
|
||||||
|
login_manager = LoginManager(app)
|
||||||
|
login_manager.logout_view = 'meta.logout'
|
||||||
|
|
||||||
|
@login_manager.user_loader
|
||||||
|
def load_user(user_id):
|
||||||
|
from flipbook.models import Wallet
|
||||||
|
Wallet = Wallet.get(user_id)
|
||||||
|
return Wallet
|
||||||
|
|
||||||
|
@app.before_serving
|
||||||
|
async def startup():
|
||||||
|
from flipbook.routes import meta, api
|
||||||
|
from flipbook import filters
|
||||||
|
await setup_db(app)
|
||||||
|
app.register_blueprint(meta.bp)
|
||||||
|
app.register_blueprint(api.bp)
|
||||||
|
app.register_blueprint(filters.bp)
|
||||||
|
# app.register_blueprint(cli.bp)
|
||||||
|
|
||||||
|
return app
|
@ -1,6 +1,6 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from quart import Blueprint, current_app
|
from quart import Blueprint
|
||||||
|
|
||||||
|
|
||||||
bp = Blueprint('filters', 'filters')
|
bp = Blueprint('filters', 'filters')
|
@ -0,0 +1,11 @@
|
|||||||
|
from web3.auto import w3
|
||||||
|
from eth_account.messages import encode_defunct
|
||||||
|
|
||||||
|
|
||||||
|
def verify_signature(message, signature, public_address):
|
||||||
|
msg = encode_defunct(text=message)
|
||||||
|
recovered = w3.eth.account.recover_message(msg, signature=signature)
|
||||||
|
if recovered.lower() == public_address.lower():
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
@ -0,0 +1 @@
|
|||||||
|
{}
|
@ -0,0 +1,68 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
from peewee import *
|
||||||
|
from PIL import Image
|
||||||
|
from flask_login import login_user
|
||||||
|
|
||||||
|
from flipbook import config
|
||||||
|
|
||||||
|
|
||||||
|
db = SqliteDatabase(f"data/flipbook.sqlite")
|
||||||
|
|
||||||
|
|
||||||
|
def rand_id():
|
||||||
|
return uuid4().hex
|
||||||
|
|
||||||
|
|
||||||
|
class Wallet(Model):
|
||||||
|
id = AutoField()
|
||||||
|
address = CharField(null=False, unique=True)
|
||||||
|
register_date = DateTimeField(default=datetime.utcnow)
|
||||||
|
login_date = DateTimeField(null=True)
|
||||||
|
opensea_handle = CharField(null=True)
|
||||||
|
twitter_handle = CharField(null=True)
|
||||||
|
nonce = CharField(default=rand_id())
|
||||||
|
nonce_date = DateTimeField(default=datetime.utcnow)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_authenticated(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_active(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_anonymous(self):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def generate_nonce(self):
|
||||||
|
return rand_id()
|
||||||
|
|
||||||
|
def change_nonce(self):
|
||||||
|
self.nonce = rand_id()
|
||||||
|
self.nonce_date = datetime.utcnow()
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
def login(self):
|
||||||
|
self.change_nonce()
|
||||||
|
self.last_login_date = datetime.utcnow()
|
||||||
|
login_user(self)
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
database = db
|
||||||
|
|
||||||
|
|
||||||
|
class Upload(Model):
|
||||||
|
id = AutoField()
|
||||||
|
token_id = IntegerField()
|
||||||
|
title = CharField()
|
||||||
|
text = CharField(null=True)
|
||||||
|
wallet = ForeignKeyField(Wallet)
|
||||||
|
image_name = CharField()
|
||||||
|
upload_date = DateTimeField(default=datetime.utcnow)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
database = db
|
@ -0,0 +1,92 @@
|
|||||||
|
import json
|
||||||
|
from secrets import token_urlsafe
|
||||||
|
|
||||||
|
from quart import Blueprint, jsonify, request
|
||||||
|
from flask_login import current_user
|
||||||
|
|
||||||
|
from flipbook.helpers import verify_signature
|
||||||
|
from flipbook.models import Wallet, rand_id
|
||||||
|
|
||||||
|
|
||||||
|
bp = Blueprint('api', 'api', url_prefix='/api/v1')
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/user_authenticated')
|
||||||
|
async def user_authenticated():
|
||||||
|
"""
|
||||||
|
Check to see if sender is authenticated.
|
||||||
|
Useful for AJAX calls to "check" we're still authenticated
|
||||||
|
instead of assuming (especially with old, loaded forms/pages).
|
||||||
|
"""
|
||||||
|
return jsonify(current_user.is_authenticated)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/user_exists/<wallet_address>')
|
||||||
|
async def user_exists(wallet_address):
|
||||||
|
"""
|
||||||
|
Check to see if a given wallet exists in the database.
|
||||||
|
This logic will help the login/connect MetaMask flow.
|
||||||
|
"""
|
||||||
|
nonce = rand_id()
|
||||||
|
wallet = Wallet.select().where(
|
||||||
|
Wallet.address == wallet_address.lower()
|
||||||
|
).first()
|
||||||
|
if wallet:
|
||||||
|
nonce = wallet.nonce
|
||||||
|
return jsonify({
|
||||||
|
'user_exists': wallet is not None,
|
||||||
|
'nonce': nonce,
|
||||||
|
'success': True
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/authenticate/metamask', methods=['POST'])
|
||||||
|
async def authenticate_metamask():
|
||||||
|
"""
|
||||||
|
This is the login/authenticate route for this dApp.
|
||||||
|
Users POST a `signedData` blob, a message signed by the user with MetaMask
|
||||||
|
(`personal_sign` method).
|
||||||
|
This route will verify the signed data against the user's public ETH
|
||||||
|
address. If no user exists, they get an entry in the database.
|
||||||
|
If user does exist, they get logged in.
|
||||||
|
"""
|
||||||
|
data = await request.get_data()
|
||||||
|
data = json.loads(data)
|
||||||
|
if current_user.is_authenticated:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': 'Already registered and authenticated.'
|
||||||
|
})
|
||||||
|
|
||||||
|
_u = Wallet.select().where(
|
||||||
|
Wallet.address == data['public_address']
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if _u:
|
||||||
|
if data['message'].endswith(_u.nonce):
|
||||||
|
if verify_signature(data['message'], data['signed_data'], data['public_address']):
|
||||||
|
_u.login()
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'message': 'Logged in'
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': 'Invalid signature'
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': 'Invalid nonce in signed message'
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
w = Wallet(
|
||||||
|
address=data['public_address'].lower()
|
||||||
|
)
|
||||||
|
w.save()
|
||||||
|
w.login()
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'message': 'Registered'
|
||||||
|
})
|
@ -0,0 +1,30 @@
|
|||||||
|
from quart import Blueprint, render_template, request, redirect, url_for, flash
|
||||||
|
from flask_login import logout_user
|
||||||
|
|
||||||
|
|
||||||
|
bp = Blueprint('meta', 'meta')
|
||||||
|
|
||||||
|
@bp.route('/')
|
||||||
|
async def index():
|
||||||
|
return await render_template('index.html')
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/logout')
|
||||||
|
async def logout():
|
||||||
|
"""
|
||||||
|
Log the current user out and redirect someplace within app if needed.
|
||||||
|
If 'next' is in the request args and is valid route, redirect there,
|
||||||
|
otherwise, redirect to peel off args and go home.
|
||||||
|
"""
|
||||||
|
logout_user()
|
||||||
|
if 'type' in request.args:
|
||||||
|
if request.args['type'] == 'accountsChanged':
|
||||||
|
flash('Metamask accounts have been changed, logging you out.', 'info')
|
||||||
|
if 'next' in request.args:
|
||||||
|
next_url = request.args['next']
|
||||||
|
if next_url.startswith('/'):
|
||||||
|
return redirect(next_url)
|
||||||
|
else:
|
||||||
|
return redirect(url_for('meta.index'))
|
||||||
|
|
||||||
|
return redirect(url_for('meta.index'))
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
@ -0,0 +1,18 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
{% include 'includes/head.html' %}
|
||||||
|
<body>
|
||||||
|
{% include 'includes/header.html' %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="one-half column" style="margin-top: 25%">
|
||||||
|
<h4>Basic Page</h4>
|
||||||
|
<p>This index.html page is a placeholder with the CSS, font and favicon. It's just waiting for you to add some content! If you need some help hit up the <a href="http://www.getskeleton.com">Skeleton documentation</a>.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
{% include 'includes/footer.html' %}
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -0,0 +1,13 @@
|
|||||||
|
<header id="header">
|
||||||
|
<h1 id="logo"><a href="/">{{ config.SITE_NAME }}</a></h1>
|
||||||
|
<nav id="nav">
|
||||||
|
<ul>
|
||||||
|
<li><a href="/">Home</a></li>
|
||||||
|
{% if current_user.is_authenticated %}
|
||||||
|
<li><a href="{{ url_for('meta.logout') }}" class="button">Logout</a></li>
|
||||||
|
{% else %}
|
||||||
|
<li><a href="#" id="connectWallet" class="button">Connect</a></li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</header>
|
@ -0,0 +1,73 @@
|
|||||||
|
<script src="/static/js/main.js"></script>
|
||||||
|
|
||||||
|
{% with messages = get_flashed_messages() %}
|
||||||
|
{% if messages %}
|
||||||
|
{% for message in messages %}
|
||||||
|
<p>{{ message }}</p>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
<!-- <script src="/static/js/vendor/metamask-onboarding-1.0.1.bundle.js"></script> -->
|
||||||
|
<script>
|
||||||
|
window.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const connectButton = document.getElementById('connectWallet');
|
||||||
|
let nonce = 0;
|
||||||
|
|
||||||
|
async function getSignedData(publicAddress, jsonData) {
|
||||||
|
const signedData = await window.ethereum.request({
|
||||||
|
method: 'eth_signTypedData_v3',
|
||||||
|
params: [publicAddress, JSON.stringify(jsonData)]
|
||||||
|
});
|
||||||
|
console.log(signedData);
|
||||||
|
return signedData
|
||||||
|
}
|
||||||
|
|
||||||
|
async function connectWallet() {
|
||||||
|
let userExists;
|
||||||
|
const allAccounts = await window.ethereum.request({
|
||||||
|
method: 'eth_requestAccounts',
|
||||||
|
});
|
||||||
|
await fetch('/api/v1/user_exists/' + allAccounts[0])
|
||||||
|
.then((resp) => resp.json())
|
||||||
|
.then(function(data) {
|
||||||
|
if (!data['success']) {
|
||||||
|
console.log('error checking user_exists!')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
console.log(data);
|
||||||
|
nonce = data['nonce'];
|
||||||
|
})
|
||||||
|
|
||||||
|
const msg = 'Authentication request from Flipbook app! Verifying message with nonce ' + nonce
|
||||||
|
const signedData = await window.ethereum.request({
|
||||||
|
method: 'personal_sign',
|
||||||
|
params: [msg, allAccounts[0]]
|
||||||
|
});
|
||||||
|
console.log('Signing data with msg (' + msg + '), address (' + allAccounts[0] + '), and nonce (' + nonce + ')')
|
||||||
|
console.log(signedData);
|
||||||
|
|
||||||
|
await fetch('{{ url_for("api.authenticate_metamask" ) }}', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json;charset=utf-8'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
'signed_data': signedData,
|
||||||
|
'public_address': allAccounts[0],
|
||||||
|
'nonce': nonce,
|
||||||
|
'message': msg,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then((resp) => resp.json())
|
||||||
|
.then(function(data) {
|
||||||
|
console.log(data)
|
||||||
|
if (data['success']) {
|
||||||
|
window.location.href = '/'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
if (connectButton) connectButton.onclick = async () => connectWallet();
|
||||||
|
});
|
||||||
|
</script>
|
@ -0,0 +1,4 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% block content %}
|
||||||
|
<p>fuck this</p>
|
||||||
|
{% endblock %}
|
@ -1,30 +0,0 @@
|
|||||||
import click
|
|
||||||
from quart import Blueprint, current_app
|
|
||||||
|
|
||||||
from myapp.models import MyThing
|
|
||||||
from myapp.factory import db
|
|
||||||
|
|
||||||
|
|
||||||
bp = Blueprint('filters', 'filters')
|
|
||||||
|
|
||||||
@bp.cli.command('init')
|
|
||||||
def init():
|
|
||||||
import app.models
|
|
||||||
db.create_all()
|
|
||||||
|
|
||||||
@bp.cli.command('delete')
|
|
||||||
@click.argument('thing_id')
|
|
||||||
def delete(thing_id):
|
|
||||||
thing = MyThing.query.get(thing_id)
|
|
||||||
if thing:
|
|
||||||
db.session.delete(thing)
|
|
||||||
db.session.commit()
|
|
||||||
click.echo(f'MyThing {thing.id} was deleted')
|
|
||||||
else:
|
|
||||||
click.echo('MyThing ID does not exist')
|
|
||||||
|
|
||||||
@bp.cli.command('list')
|
|
||||||
def list_things():
|
|
||||||
thing = MyThing.query.all()
|
|
||||||
for i in thing:
|
|
||||||
click.echo(i.id)
|
|
@ -1,33 +0,0 @@
|
|||||||
from dotenv import load_dotenv
|
|
||||||
from secrets import token_urlsafe
|
|
||||||
from os import getenv
|
|
||||||
|
|
||||||
|
|
||||||
load_dotenv()
|
|
||||||
|
|
||||||
# Site meta
|
|
||||||
SITE_NAME = getenv('SITE_NAME', 'MyApp')
|
|
||||||
SECRET_KEY = getenv('SECRET_KEY')
|
|
||||||
STATS_TOKEN = getenv('STATS_TOKEN', token_urlsafe(8))
|
|
||||||
SERVER_NAME = getenv('SERVER_NAME', 'localhost:5000')
|
|
||||||
|
|
||||||
# Crypto RPC
|
|
||||||
XMR_WALLET_PASS = getenv('XMR_WALLET_PASS')
|
|
||||||
XMR_WALLET_RPC_USER = getenv('XMR_WALLET_RPC_USER')
|
|
||||||
XMR_WALLET_RPC_PASS = getenv('XMR_WALLET_RPC_PASS')
|
|
||||||
XMR_WALLET_RPC_ENDPOINT = getenv('XMR_WALLET_RPC_ENDPOINT')
|
|
||||||
XMR_DAEMON_URI = getenv('XMR_DAEMON_URI')
|
|
||||||
|
|
||||||
# Database
|
|
||||||
DB_HOST = getenv('DB_HOST', 'localhost')
|
|
||||||
DB_PORT = getenv('DB_PORT', 5432)
|
|
||||||
DB_NAME = getenv('DB_NAME', 'myapp')
|
|
||||||
DB_USER = getenv('DB_USER', 'myapp')
|
|
||||||
DB_PASS = getenv('DB_PASS')
|
|
||||||
|
|
||||||
# Redis
|
|
||||||
REDIS_HOST = getenv('REDIS_HOST', 'localhost')
|
|
||||||
REDIS_PORT = getenv('REDIS_PORT', 6379)
|
|
||||||
|
|
||||||
# Development
|
|
||||||
TEMPLATES_AUTO_RELOAD = True
|
|
@ -1,37 +0,0 @@
|
|||||||
import quart.flask_patch
|
|
||||||
from quart import Quart
|
|
||||||
from flask_sqlalchemy import SQLAlchemy
|
|
||||||
|
|
||||||
from myapp import config
|
|
||||||
|
|
||||||
|
|
||||||
db = SQLAlchemy()
|
|
||||||
|
|
||||||
async def _setup_db(app: Quart):
|
|
||||||
uri = 'postgresql+psycopg2://{user}:{pw}@{host}:{port}/{db}'.format(
|
|
||||||
user=config.DB_USER,
|
|
||||||
pw=config.DB_PASS,
|
|
||||||
host=config.DB_HOST,
|
|
||||||
port=config.DB_PORT,
|
|
||||||
db=config.DB_NAME
|
|
||||||
)
|
|
||||||
app.config['SQLALCHEMY_DATABASE_URI'] = uri
|
|
||||||
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
|
||||||
db = SQLAlchemy(app)
|
|
||||||
|
|
||||||
def create_app():
|
|
||||||
app = Quart(__name__)
|
|
||||||
app.config.from_envvar('QUART_SECRETS')
|
|
||||||
|
|
||||||
|
|
||||||
@app.before_serving
|
|
||||||
async def startup():
|
|
||||||
from myapp.routes import meta, api
|
|
||||||
from myapp import filters
|
|
||||||
await _setup_db(app)
|
|
||||||
app.register_blueprint(meta.bp)
|
|
||||||
app.register_blueprint(api.bp)
|
|
||||||
app.register_blueprint(filters.bp)
|
|
||||||
# app.register_blueprint(cli.bp)
|
|
||||||
|
|
||||||
return app
|
|
@ -1,8 +0,0 @@
|
|||||||
from flask_wtf import FlaskForm
|
|
||||||
from wtforms import StringField, BooleanField
|
|
||||||
from wtforms.validators import DataRequired
|
|
||||||
|
|
||||||
|
|
||||||
class Login(FlaskForm):
|
|
||||||
email = StringField('Email Address:', validators=[DataRequired()], render_kw={"placeholder": "Email", "class": "form-control", "type": "email"})
|
|
||||||
password = StringField('Password:', validators=[DataRequired()], render_kw={"placeholder": "Password", "class": "form-control", "type": "password"})
|
|
@ -1,38 +0,0 @@
|
|||||||
from datetime import datetime
|
|
||||||
from uuid import uuid4
|
|
||||||
|
|
||||||
from sqlalchemy.sql import func
|
|
||||||
|
|
||||||
from myapp.factory import db
|
|
||||||
from myapp import config
|
|
||||||
|
|
||||||
|
|
||||||
def rand_id():
|
|
||||||
return uuid4().hex
|
|
||||||
|
|
||||||
class MyThing(db.Model):
|
|
||||||
__tablename__ = 'swaps'
|
|
||||||
|
|
||||||
# Meta
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
|
||||||
# id = db.Column(db.String(80), primary_key=True, default=rand_id) # hex based id
|
|
||||||
date = db.Column(db.DateTime, server_default=func.now())
|
|
||||||
my_bool = db.Column(db.Boolean)
|
|
||||||
my_int = db.Column(db.Integer)
|
|
||||||
my_str = db.Column(db.String(150))
|
|
||||||
completed = db.Column(db.Boolean, default=False)
|
|
||||||
completed_date = db.Column(db.DateTime, nullable=True)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return self.id
|
|
||||||
|
|
||||||
def hours_elapsed(self):
|
|
||||||
now = datetime.utcnow()
|
|
||||||
if since_completed:
|
|
||||||
if self.completed_date:
|
|
||||||
diff = now - self.completed_date
|
|
||||||
else:
|
|
||||||
return 0
|
|
||||||
else:
|
|
||||||
diff = now - self.date
|
|
||||||
return diff.total_seconds() / 60 / 60
|
|
@ -1,11 +0,0 @@
|
|||||||
from quart import Blueprint, jsonify
|
|
||||||
|
|
||||||
|
|
||||||
bp = Blueprint('api', 'api')
|
|
||||||
|
|
||||||
@bp.route('/api/test')
|
|
||||||
async def get_prices():
|
|
||||||
return jsonify({
|
|
||||||
'test': True,
|
|
||||||
'message': 'This is only a test.'
|
|
||||||
})
|
|
@ -1,8 +0,0 @@
|
|||||||
from quart import Blueprint, render_template
|
|
||||||
|
|
||||||
|
|
||||||
bp = Blueprint('meta', 'meta')
|
|
||||||
|
|
||||||
@bp.route('/')
|
|
||||||
async def index():
|
|
||||||
return await render_template('index.html')
|
|
@ -1,9 +0,0 @@
|
|||||||
<header id="header">
|
|
||||||
<h1 id="logo"><a href="/">{{ config.SITE_NAME }}</a></h1>
|
|
||||||
<nav id="nav">
|
|
||||||
<ul>
|
|
||||||
<li><a href="/">Home</a></li>
|
|
||||||
<li><a href="/#search" class="button primary">Search</a></li>
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
</header>
|
|
@ -1,9 +0,0 @@
|
|||||||
<script src="/static/js/main.js"></script>
|
|
||||||
|
|
||||||
{% with messages = get_flashed_messages() %}
|
|
||||||
{% if messages %}
|
|
||||||
{% for message in messages %}
|
|
||||||
<p>{{ message }}</p>
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
{% endwith %}
|
|
@ -1,37 +0,0 @@
|
|||||||
<!DOCTYPE HTML>
|
|
||||||
<html>
|
|
||||||
|
|
||||||
{% include 'includes/head.html' %}
|
|
||||||
|
|
||||||
<body class="is-preload landing">
|
|
||||||
<div id="page-wrapper">
|
|
||||||
|
|
||||||
<header id="header">
|
|
||||||
<h1 id="logo"><a href="/">MyThing sample app</a></h1>
|
|
||||||
<nav id="nav">
|
|
||||||
<ul>
|
|
||||||
<li><a href="/">Home</a></li>
|
|
||||||
<li><a href="/#search" class="button primary">Search</a></li>
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<section id="banner">
|
|
||||||
<div class="content">
|
|
||||||
<header>
|
|
||||||
<h2>MyThing</h2>
|
|
||||||
<p>This is a sample app.</p>
|
|
||||||
</header>
|
|
||||||
<span class="image"><img src="/static/images/monero-logo.png" width=150px></span>
|
|
||||||
</div>
|
|
||||||
<a href="#swap" class="goto-next scrolly">Next</a>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{% include 'includes/footer.html' %}
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% include 'includes/scripts.html' %}
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,14 +1,12 @@
|
|||||||
Flask
|
Flask
|
||||||
Flask-SQLAlchemy
|
|
||||||
Flask-WTF
|
|
||||||
flask-login
|
flask-login
|
||||||
gunicorn
|
hypercorn
|
||||||
Pillow
|
Pillow
|
||||||
psycopg2-binary
|
|
||||||
python-dotenv
|
python-dotenv
|
||||||
qrcode
|
|
||||||
redis
|
redis
|
||||||
requests
|
requests
|
||||||
SQLAlchemy
|
|
||||||
WTForms
|
|
||||||
quart
|
quart
|
||||||
|
peewee
|
||||||
|
arrow
|
||||||
|
black
|
||||||
|
web3
|
||||||
|
Loading…
Reference in New Issue