init skelly

main
lza_menace 1 year ago
parent e00284bc7e
commit 637f213d4a

1
.gitignore vendored

@ -127,3 +127,4 @@ dmypy.json
# Pyre type checker
.pyre/
data/

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2021 lalanza808
Copyright (c) 2023 lalanza808
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

@ -10,8 +10,11 @@ setup: ## Establish local environment with dependencies installed
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
up: ## Build and run the required containers by fetching binaries
docker-compose -f docker-compose.yaml up -d
shell:
FLASK_SECRETS=config.py QUART_APP="flipbook:create_app()" .venv/bin/quart shell
shell: ## Start Flask CLI shell
FLASK_APP=app/app.py FLASK_SECRETS=config.py FLASK_DEBUG=0 FLASK_ENV=production .venv/bin/flask shell
dev:
FLASK_SECRETS=config.py QUART_APP="flipbook:create_app()" QUART_ENV=development .venv/bin/python3 run.py
prod:
FLASK_SECRETS=config.py QUART_APP="flipbook:create_app()" QUART_ENV=production .venv/bin/hypercorn run

@ -1,2 +1 @@
# lza-quart-app
Template project for Quart (Python/Flask) applications.
# web3-flipbook

@ -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_USER=myapp
DB_NAME=myapp
DB_USER=flipbook
DB_NAME=flipbook
DB_HOST=localhost
XMR_WALLET_PATH=/data/xmr-wallet
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
WEB3_PROVIDER_URI=wss://ropsten.infura.io/ws/v3/xxxx
SITE_NAME=flipbook
SECRET_KEY=xxxxxxxxxxxxxxxxxxx
STATS_TOKEN=xxxxxxxxxxxxxxxxxxxx
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 quart import Blueprint, current_app
from quart import Blueprint
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

@ -4,8 +4,8 @@ from datetime import timedelta
from redis import Redis
from app.library.market import get_market_data
from app import config
from flipbook.library.market import get_market_data
from flipbook import config
class Cache(object):

@ -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>

@ -5,3 +5,5 @@
<li>&copy; {{ config.SITE_NAME }}. All rights reserved.</li>
</ul>
</footer>
{% include 'includes/scripts.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-SQLAlchemy
Flask-WTF
flask-login
gunicorn
hypercorn
Pillow
psycopg2-binary
python-dotenv
qrcode
redis
requests
SQLAlchemy
WTForms
quart
peewee
arrow
black
web3

@ -0,0 +1,7 @@
from flipbook.factory import create_app
app = create_app()
if __name__ == '__main__':
app.run(use_reloader=True)
Loading…
Cancel
Save