master
lza_menace 1 year ago
commit 5e50fd1640

4
.gitignore vendored

@ -0,0 +1,4 @@
.venv
.env
data
__pycache__

@ -0,0 +1,38 @@
FROM ubuntu:22.04
ENV MONERO_HASH 937dfcc48d91748dd2e8f58714dfc45d17a0959dff33fc7385bbe06344ff2c16
ENV MONERO_DL_URL https://downloads.getmonero.org/cli/monero-linux-x64-v0.18.1.1.tar.bz2
ENV MONERO_DL_FILE monero.tar.bz2
ENV MONERO_SUMS_FILE sha256sums
WORKDIR /opt/monero
# Update system and install dependencies
# Download Monero binaries from Github
# Confirm hashes match
# Install binaries to system path
# Clean up
RUN apt-get update
RUN apt-get upgrade -y
RUN apt-get install -y tar wget bzip2
RUN wget -qO ${MONERO_DL_FILE} ${MONERO_DL_URL}
RUN echo "${MONERO_HASH} ${MONERO_DL_FILE}" > ${MONERO_SUMS_FILE} \
&& sha256sum -c ${MONERO_SUMS_FILE}; \
if [ "$?" -eq 0 ]; \
then \
echo -e "[+] Hashes match - proceeding with container build"; \
else \
echo -e "[!] Hashes do not match - exiting"; \
exit 5; \
fi \
&& mkdir ./tmp \
&& tar xvf ${MONERO_DL_FILE} -C ./tmp --strip 1 \
&& mv ./tmp/* /usr/local/bin/ \
&& rm -rf ./tmp ${MONERO_SUMS_FILE} ${MONERO_DL_FILE}
WORKDIR /tmp
COPY bin/run_wallet.sh /run_wallet.sh
WORKDIR /data

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 @lza_menace (https://lzahq.tech)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

@ -0,0 +1,33 @@
.PHONY: format help
# Help system from https://marmelab.com/blog/2016/02/29/auto-documented-makefile.html
.DEFAULT_GOAL := help
help:
@grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'
setup: ## Establish local environment with dependencies installed
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
mkdir -p data/uploads
build: ## Build containers
docker-compose build
up: ## Start containers
docker-compose up -d
down: ## Stop containers
docker-compose down
dbshell:
docker-compose exec db psql -U nerochan
shell: ## Start Flask CLI shell
./manage.sh shell
init: ## Initialize SQLite DB
./manage.sh init
dev: ## Start Flask development web server
./manage.sh run

@ -0,0 +1,7 @@
# nerochan
Post your best neroChans. Get tipped.
No middle-men. p2p tips.
No passwords. Wallet signing authentication.

@ -0,0 +1,38 @@
#!/bin/sh
export RPC_CREDS="${2}"
export DAEMON_ADDRESS="${3}"
# Define the network we plan to operate our wallet in
if [[ "${1}" == "stagenet" ]]; then
export NETWORK=--stagenet
export PORT=38081
elif [[ "${1}" == "testnet" ]]; then
export NETWORK=--testnet
export PORT=28081
else
export NETWORK=
export PORT=18089
fi
# Create new wallet if it doesn't exist
if [[ ! -d /data/wallet ]]; then
monero-wallet-cli ${NETWORK} \
--generate-new-wallet /data/wallet \
--daemon-address ${DAEMON_ADDRESS} \
--trusted-daemon \
--use-english-language-names \
--mnemonic-language English
fi
# Run RPC wallet
monero-wallet-rpc ${NETWORK} \
--daemon-address ${DAEMON_ADDRESS} \
--wallet-file /data/wallet \
--password "" \
--rpc-login ${RPC_CREDS} \
--rpc-bind-port 8000 \
--rpc-bind-ip 0.0.0.0 \
--confirm-external-bind \
--log-file /data/wallet-rpc.log \
--trusted-daemon

@ -0,0 +1,16 @@
version: '3'
services:
cache:
image: redis:latest
ports:
- 127.0.0.1:6379:6379
wallet:
build:
context: .
dockerfile: Dockerfile-monero
ports:
- 127.0.0.1:${XMR_WALLET_RPC_PORT:-8000}:${XMR_WALLET_RPC_PORT:-8000}
volumes:
- ${DATA_DIR:-./data/wallet}:/data
command:
bash /run_wallet.sh "${XMR_WALLET_NETWORK}" "${XMR_WALLET_RPC_USER}:${XMR_WALLET_RPC_PASS}" "${XMR_DAEMON_URI}"

@ -0,0 +1,12 @@
DB_PATH=./data/sqlite.db
XMR_WALLET_RPC_USER=xxxxxxxxxx
XMR_WALLET_RPC_PASS=xxxxxxxxxxxxxxxxxxx
XMR_WALLET_RPC_PORT=8000
XMR_DAEMON_URI=http://super.fast.node.xmr.pm:38089
XMR_WALLET_NETWORK=stagenet
SITE_NAME=nerochan
SECRET_KEY=xxxxxxxxxxxxxxxxxxx
STATS_TOKEN=xxxxxxxxxxxxxxxxxxxx
SERVER_NAME=127.0.0.1:5000

@ -0,0 +1,31 @@
#!/bin/bash
source .venv/bin/activate
export FLASK_APP=nerochan/app.py
export FLASK_SECRETS=config.py
export FLASK_DEBUG=1
export FLASK_ENV=development
# override
source .env
if [[ ${1} == "prod" ]];
then
export FLASK_DEBUG=0
export FLASK_ENV=production
export BASE=./data/gunicorn
mkdir -p $BASE
pgrep -F $BASE/gunicorn.pid
if [[ $? != 0 ]]; then
gunicorn \
--bind 127.0.0.1:4000 "nerochan.app:app" \
--daemon \
--log-file $BASE/gunicorn.log \
--pid $BASE/gunicorn.pid \
--reload
sleep 2
echo "Started gunicorn on 127.0.0.1:4000 with pid $(cat $BASE/gunicorn.pid)"
fi
else
flask $@
fi

@ -0,0 +1 @@
from nerochan.factory import create_app

@ -0,0 +1,10 @@
#!/usr/bin/env python3
from nerochan import create_app
app = create_app()
if __name__ == '__main__':
app.run(debug=True, use_reloader=True)

@ -0,0 +1,12 @@
import click
def cli(app):
@app.cli.command('init')
def init():
import peewee
from nerochan.models import db
model = peewee.Model.__subclasses__()
db.create_tables(model)
return app

@ -0,0 +1,35 @@
from secrets import token_urlsafe
from datetime import timedelta
from os import getenv
from dotenv import load_dotenv
load_dotenv()
# Site meta
SITE_NAME = getenv('SITE_NAME', 'nerochan')
SECRET_KEY = getenv('SECRET_KEY', token_urlsafe(12))
SERVER_NAME = getenv('SERVER_NAME', '127.0.0.1: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_PORT = getenv('XMR_WALLET_RPC_PORT', 8000)
XMR_WALLET_NETWORK = getenv('XMR_WALLET_NETWORK')
# Database
DB_PATH = getenv('DB_PATH', './data/sqlite.db')
# Redis
REDIS_HOST = getenv('REDIS_HOST', 'localhost')
REDIS_PORT = getenv('REDIS_PORT', 6379)
# Sessions
SESSION_LENGTH = int(getenv('SESSION_LENGTH', 300))
PERMANENT_SESSION_LIFETIME = timedelta(minutes=SESSION_LENGTH)
MAX_CONTENT_LENGTH = 50 * 1024 * 1024
# Development
TEMPLATES_AUTO_RELOAD = True

@ -0,0 +1,25 @@
from flask import session, redirect, url_for, flash
from flask_login import current_user
from functools import wraps
from nerochan.models import User, CreatorProfile, BackerProfile, Subscription
# def login_required(f):
# @wraps(f)
# def decorated_function(*args, **kwargs):
# if "auth" not in session or not session["auth"]:
# return redirect(url_for("auth.login"))
# return f(*args, **kwargs)
# return decorated_function
def subscription_required(f):
@wraps(f)
def decorated_function(*args, **kwargs):
print(current_user)
# m = Moderator.filter(username=session["auth"]["preferred_username"])
# if m:
# return f(*args, **kwargs)
# else:
# flash("You are not a moderator")
# return redirect(url_for("index"))
return decorated_function

@ -0,0 +1,38 @@
from flask import Flask
from flask_login import LoginManager
from nerochan.cli import cli
def setup_db(app: Flask):
import peewee
models = peewee.Model.__subclasses__()
for m in models:
m.create_table()
def create_app():
app = Flask(__name__)
app = cli(app)
app.config.from_envvar('FLASK_SECRETS')
# Login manager
login_manager = LoginManager(app)
login_manager.login_view = 'auth.login'
login_manager.logout_view = 'auth.logout'
@login_manager.user_loader
def load_user(user_id):
from nerochan.models import User
return User.get_or_none(user_id)
with app.app_context():
from nerochan.routes import api, auth, post, main
from nerochan import filters
app.register_blueprint(main.bp)
app.register_blueprint(api.bp)
app.register_blueprint(auth.bp)
app.register_blueprint(post.bp)
app.register_blueprint(filters.bp)
return app

@ -0,0 +1,25 @@
from datetime import datetime
import arrow
from monero import numbers
from flask import Blueprint, current_app
bp = Blueprint('filters', 'filters')
@bp.app_template_filter('humanize')
def humanize(d):
return arrow.get(d).humanize()
@bp.app_template_filter('ts')
def from_ts(v):
return datetime.fromtimestamp(v)
@bp.app_template_filter('xmr_block_explorer')
def xmr_block_explorer(v):
return f'https://www.exploremonero.com/transaction/{v}'
@bp.app_template_filter('from_atomic')
def from_atomic(amt):
return numbers.from_atomic(amt)

@ -0,0 +1,47 @@
from flask_wtf import FlaskForm
from wtforms import StringField, FloatField
from wtforms.validators import DataRequired, ValidationError
from monero.address import address
from nerochan import config
def is_valid_xmr_address(form, field):
try:
# Ensure the provided address is valid address/subaddress/integrated address
a = address(field.data)
print(config.XMR_WALLET_NETWORK)
# Ensure the provided address matches the network that the application's wallet is using
if not config.XMR_WALLET_NETWORK.startswith(a.net):
raise ValidationError('Provided Monero address does not match the configured network. Application: {}. Provided: {}'.format(
config.XMR_WALLET_NETWORK.replace('net', ''), a.net
))
except ValueError:
raise ValidationError('Invalid Monero address provided')
class UserRegistration(FlaskForm):
handle = StringField('Handle:', validators=[DataRequired()], render_kw={'placeholder': 'online handle', 'class': 'form-control', 'type': 'text'})
wallet_address = StringField('Wallet Address:', validators=[DataRequired(), is_valid_xmr_address], render_kw={'placeholder': 'monero wallet address', 'class': 'form-control', 'type': 'text'})
class UserLogin(FlaskForm):
handle = StringField('Handle:', validators=[DataRequired()], render_kw={'placeholder': 'online handle', 'class': 'form-control', 'type': 'text'})
class UserChallenge(FlaskForm):
signature = StringField('Signature:', validators=[DataRequired()], render_kw={'placeholder': 'signed data', 'class': 'form-control', 'type': 'text'})
class ConfirmPlatformSubscription(FlaskForm):
tx_id = StringField('TX ID:', validators=[DataRequired()], render_kw={'placeholder': 'TX ID', 'class': 'form-control', 'type': 'text'})
tx_key = StringField('TX Key:', validators=[DataRequired()], render_kw={'placeholder': 'TX Key', 'class': 'form-control', 'type': 'text'})
class ConfirmCreatorSubscription(ConfirmPlatformSubscription):
wallet_address = StringField('Wallet Address:', validators=[DataRequired(), is_valid_xmr_address], render_kw={'placeholder': 'monero wallet address', 'class': 'form-control', 'type': 'text'})
class CreateSubscription(FlaskForm):
price_xmr = FloatField('Price (XMR):', validators=[DataRequired()], render_kw={'placeholder': '.5', 'class': 'form-control', 'type': 'text'})
number_days = FloatField('Length (Days)', validators=[DataRequired()], render_kw={'placeholder': '30', 'class': 'form-control', 'type': 'text'})

@ -0,0 +1,19 @@
import peewee as pw
import playhouse.postgres_ext as pwpg
from monero.wallet import Wallet
from nerochan import config
def make_wallet_rpc(method, data={}):
try:
w = Wallet(
port=config.XMR_WALLET_RPC_PORT,
user=config.XMR_WALLET_RPC_USER,
password=config.XMR_WALLET_RPC_PASS,
timeout=3
)
res = w._backend.raw_request(method, data)
return res
except Exception as e:
raise e

@ -0,0 +1,35 @@
from json import loads as json_loads
from json import dumps as json_dumps
from datetime import timedelta
from redis import Redis
from nerochan.library.market import get_market_data
from nerochan import config
class Cache(object):
def __init__(self):
self.redis = Redis(host=config.REDIS_HOST, port=config.REDIS_PORT)
def store_data(self, item_name, expiration_minutes, data):
self.redis.setex(
item_name,
timedelta(minutes=expiration_minutes),
value=data
)
def get_coin_price(self, coin_name):
key_name = f'{coin_name}_price'
data = self.redis.get(key_name)
if data:
return json_loads(data)
else:
d = get_market_data(coin_name)
data = {
key_name: d['market_data']['current_price'],
}
self.store_data(key_name, 4, json_dumps(data))
return data
cache = Cache()

@ -0,0 +1,16 @@
from requests import get as r_get
def get_market_data(coin_name="monero"):
data = {
'localization': False,
'tickers': False,
'market_data': True,
'community_data': False,
'developer_data': False,
'sparkline': False
}
headers = {'accept': 'application/json'}
url = f'https://api.coingecko.com/api/v3/coins/{coin_name}'
r = r_get(url, headers=headers, data=data)
return r.json()

@ -0,0 +1,103 @@
from datetime import datetime
from secrets import token_urlsafe
import peewee as pw
from nerochan import config
db = pw.SqliteDatabase(
config.DB_PATH
)
def gen_challenge():
return token_urlsafe().replace('-', '').replace('_', '')
class User(pw.Model):
"""
User model is for base user management and reporting.
"""
id = pw.AutoField()
register_date = pw.DateTimeField(default=datetime.utcnow)
last_login_date = pw.DateTimeField(default=datetime.utcnow)
handle = pw.CharField(unique=True)
wallet_address = pw.CharField(unique=True)
challenge = pw.CharField(default=gen_challenge)
is_admin = pw.BooleanField(default=False)
is_mod = pw.BooleanField(default=False)
@property
def is_authenticated(self):
return True
@property
def is_active(self):
return True
@property
def is_anonymous(self):
return False
def get_id(self):
return self.id
def regenerate_challenge(self):
self.challenge = gen_challenge()
self.save()
class Meta:
database = db
class Profile(pw.Model):
"""
Profile model is for users to provide metadata about
themselves; Creators for their fans or even just the general public.
Links to social media, contact info, portfolio sites, etc
should go in here.
"""
id = pw.AutoField()
user = pw.ForeignKeyField(User)
create_date = pw.DateTimeField(default=datetime.utcnow)
website = pw.CharField(unique=True, null=True)
twitter_handle = pw.CharField(unique=True, null=True)
bio = pw.CharField(null=True)
email = pw.CharField(unique=True, null=True)
verified = pw.CharField(default=False)
class Meta:
database = db
class Content(pw.Model):
"""
Content model is any uploaded content from a creator.
"""
id = pw.AutoField()
creator = pw.ForeignKeyField(User)
path = pw.CharField()
upload_date = pw.DateTimeField(default=datetime.utcnow)
last_edit_date = pw.DateTimeField(default=datetime.utcnow)
approved = pw.BooleanField(default=False)
hidden = pw.BooleanField(default=False)
title = pw.CharField()
description = pw.TextField()
class Meta:
database = db
class Transaction(pw.Model):
"""
Transaction model is a simple reference to a Monero transaction so that we can track
which transactions have occurred on-chain and what content they correspond to.
"""
id = pw.AutoField()
tx_id = pw.CharField(unique=True)
atomic_xmr = pw.BigIntegerField()
to_address = pw.CharField()
content = pw.ForeignKeyField(Content)
class Meta:
database = db

@ -0,0 +1,11 @@
from flask import Blueprint, jsonify
bp = Blueprint('api', 'api')
@bp.route('/api/test')
def get_prices():
return jsonify({
'test': True,
'message': 'This is only a test.'
})

@ -0,0 +1,102 @@
from flask import Blueprint, render_template
from flask import flash, redirect, url_for
from flask_login import login_user, logout_user, current_user
from nerochan.forms import UserLogin, UserRegistration, UserChallenge
from nerochan.helpers import make_wallet_rpc
from nerochan.models import User
bp = Blueprint('auth', 'auth')
@bp.route("/register", methods=["GET", "POST"])
def register():
form = UserRegistration()
if current_user.is_authenticated:
flash('Already registered and authenticated.')
return redirect(url_for('main.index'))
if form.validate_on_submit():
# Check if handle already exists
user = User.select().where(
User.handle == form.handle.data
).first()
if user:
flash('This handle is already registered.')
return redirect(url_for('auth.login'))
# Save new user
user = User(
handle=form.handle.data,
wallet_address=form.wallet_address.data,
)
user.save()
login_user(user)
return redirect(url_for('main.index'))
return render_template("auth/register.html", form=form)
@bp.route("/login", methods=["GET", "POST"])
def login():
form = UserLogin()
if current_user.is_authenticated:
flash('Already logged in.')
return redirect(url_for('main.index'))
if form.validate_on_submit():
# Check if user doesn't exist
user = User.select().where(
User.handle == form.handle.data
).first()
if not user:
flash('That handle does not exist.')
return redirect(url_for('auth.login'))
return redirect(url_for('auth.challenge', handle=user.handle))
return render_template("auth/login.html", form=form)
@bp.route("/login/challenge/<handle>", methods=["GET", "POST"])
def challenge(handle):
form = UserChallenge()
user = User.select().where(User.handle == handle).first()
if not user:
flash('User does not exist.')
return redirect(url_for('main.index'))
if current_user.is_authenticated:
flash('Already logged in.')
return redirect(url_for('main.index'))
if form.validate_on_submit():
data = {
'data': user.challenge,
'address': user.wallet_address,
'signature': form.signature.data
}
try:
res = make_wallet_rpc('verify', data)
if res['good']:
user.regenerate_challenge()
login_user(user)
flash('Successful login!')
return redirect(url_for('main.index'))
else:
flash('Invalid signature. Try again.')
return redirect(url_for('auth.challenge', handle=handle))
except Exception as e:
flash(f'Issue with checking the signature provided: {e}')
return redirect(url_for('auth.challenge', handle=handle))
return render_template(
'auth/challenge.html',
user=user,
form=form
)
@bp.route("/logout")
def logout():
if current_user.is_authenticated:
logout_user()
else:
flash('Not authenticated!')
return redirect(url_for('main.index'))

@ -0,0 +1,30 @@
from flask import Blueprint, render_template
from flask_login import current_user
from nerochan.models import *
bp = Blueprint('main', 'main')
@bp.route('/')
def index():
feed = dict()
# new_creators = User.select().where(
# User.roles.contains_any(UserRole.creator)
# ).order_by(User.register_date.desc()).execute()
# feed['new_creators'] = new_creators
# if current_user.is_authenticated:
# active_subscriptions = Subscription.select().where(
# Subscription.is_active == True,
# Subscription.backer == current_user
# ).order_by(Subscription.subscribe_date.desc()).execute()
# feed['active_subscriptions'] = active_subscriptions
# new_posts = Post.select().where(
# Post.hidden == False,
# Post.creator in [c.creator for c in active_subscriptions]
# ).order_by(Post.post_date.desc()).execute()
# feed['new_posts'] = new_posts
return render_template(
'index.html',
feed=feed
)

@ -0,0 +1,27 @@
from flask import Blueprint, render_template, flash, redirect, url_for
from flask_login import current_user
# from nerochan.models import Content, User
bp = Blueprint('post', 'post')
# @bp.route('/post/<int:post_id>')
# def show(post_id):
# post = TextPost.get_or_none(post_id)
# if post:
# if current_user.is_anonymous:
# flash('You must login to view this post.')
# return redirect(url_for('creator.show', username=post.creator.user.username))
# user_subscriptions = Subscription.select().where(
# Subscription.active == True,
# Subscription.backer == current_user.backer_profile.first()
# )
# if user_subscriptions:
# return render_template('post/show.html', post=post)
# else:
# flash('Viewing posts requires a subscription.')
# return redirect(url_for('creator.subscription', username=post.creator.user.username))
# else:
# flash('That post does not exist.')
# return redirect(url_for('meta.index'))

@ -0,0 +1,46 @@
.noty_theme__relax.noty_bar {
margin: 4px 0;
overflow: hidden;
border-radius: 2px;
position: relative; }
.noty_theme__relax.noty_bar .noty_body {
padding: 10px; }
.noty_theme__relax.noty_bar .noty_buttons {
border-top: 1px solid #e7e7e7;
padding: 5px 10px; }
.noty_theme__relax.noty_type__alert,
.noty_theme__relax.noty_type__notification {
background-color: #fff;
border: 1px solid #dedede;
color: #444; }
.noty_theme__relax.noty_type__warning {
background-color: #FFEAA8;
border: 1px solid #FFC237;
color: #826200; }
.noty_theme__relax.noty_type__warning .noty_buttons {
border-color: #dfaa30; }
.noty_theme__relax.noty_type__error {
background-color: #FF8181;
border: 1px solid #e25353;
color: #FFF; }
.noty_theme__relax.noty_type__error .noty_buttons {
border-color: darkred; }
.noty_theme__relax.noty_type__info,
.noty_theme__relax.noty_type__information {
background-color: #78C5E7;
border: 1px solid #3badd6;
color: #FFF; }
.noty_theme__relax.noty_type__info .noty_buttons,
.noty_theme__relax.noty_type__information .noty_buttons {
border-color: #0B90C4; }
.noty_theme__relax.noty_type__success {
background-color: #BCF5BC;
border: 1px solid #7cdd77;
color: darkgreen; }
.noty_theme__relax.noty_type__success .noty_buttons {
border-color: #50C24E; }

@ -0,0 +1,222 @@
.noty_layout_mixin, #noty_layout__top, #noty_layout__topLeft, #noty_layout__topCenter, #noty_layout__topRight, #noty_layout__bottom, #noty_layout__bottomLeft, #noty_layout__bottomCenter, #noty_layout__bottomRight, #noty_layout__center, #noty_layout__centerLeft, #noty_layout__centerRight {
position: fixed;
margin: 0;
padding: 0;
z-index: 9999999;
-webkit-transform: translateZ(0) scale(1, 1);
transform: translateZ(0) scale(1, 1);
-webkit-backface-visibility: hidden;
backface-visibility: hidden;
-webkit-font-smoothing: subpixel-antialiased;
filter: blur(0);
-webkit-filter: blur(0);
max-width: 90%; }
#noty_layout__top {
top: 0;
left: 5%;
width: 90%; }
#noty_layout__topLeft {
top: 20px;
left: 20px;
width: 325px; }
#noty_layout__topCenter {
top: 5%;
left: 50%;
width: 325px;
-webkit-transform: translate(-webkit-calc(-50% - .5px)) translateZ(0) scale(1, 1);
transform: translate(calc(-50% - .5px)) translateZ(0) scale(1, 1); }
#noty_layout__topRight {
top: 20px;
right: 20px;
width: 325px; }
#noty_layout__bottom {
bottom: 0;
left: 5%;
width: 90%; }
#noty_layout__bottomLeft {
bottom: 20px;
left: 20px;
width: 325px; }
#noty_layout__bottomCenter {
bottom: 5%;
left: 50%;
width: 325px;
-webkit-transform: translate(-webkit-calc(-50% - .5px)) translateZ(0) scale(1, 1);
transform: translate(calc(-50% - .5px)) translateZ(0) scale(1, 1); }
#noty_layout__bottomRight {
bottom: 20px;
right: 20px;
width: 325px; }
#noty_layout__center {
top: 50%;
left: 50%;
width: 325px;
-webkit-transform: translate(-webkit-calc(-50% - .5px), -webkit-calc(-50% - .5px)) translateZ(0) scale(1, 1);
transform: translate(calc(-50% - .5px), calc(-50% - .5px)) translateZ(0) scale(1, 1); }
#noty_layout__centerLeft {
top: 50%;
left: 20px;
width: 325px;
-webkit-transform: translate(0, -webkit-calc(-50% - .5px)) translateZ(0) scale(1, 1);
transform: translate(0, calc(-50% - .5px)) translateZ(0) scale(1, 1); }
#noty_layout__centerRight {
top: 50%;
right: 20px;
width: 325px;
-webkit-transform: translate(0, -webkit-calc(-50% - .5px)) translateZ(0) scale(1, 1);
transform: translate(0, calc(-50% - .5px)) translateZ(0) scale(1, 1); }
.noty_progressbar {
display: none; }
.noty_has_timeout.noty_has_progressbar .noty_progressbar {
display: block;
position: absolute;
left: 0;
bottom: 0;
height: 3px;
width: 100%;
background-color: #646464;
opacity: 0.2;
filter: alpha(opacity=10); }
.noty_bar {
-webkit-backface-visibility: hidden;
-webkit-transform: translate(0, 0) translateZ(0) scale(1, 1);
-ms-transform: translate(0, 0) scale(1, 1);
transform: translate(0, 0) scale(1, 1);
-webkit-font-smoothing: subpixel-antialiased;
overflow: hidden; }
.noty_effects_open {
opacity: 0;
-webkit-transform: translate(50%);
-ms-transform: translate(50%);
transform: translate(50%);
-webkit-animation: noty_anim_in 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55);
animation: noty_anim_in 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55);
-webkit-animation-fill-mode: forwards;
animation-fill-mode: forwards; }
.noty_effects_close {
-webkit-animation: noty_anim_out 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55);
animation: noty_anim_out 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55);
-webkit-animation-fill-mode: forwards;
animation-fill-mode: forwards; }
.noty_fix_effects_height {
-webkit-animation: noty_anim_height 75ms ease-out;
animation: noty_anim_height 75ms ease-out; }
.noty_close_with_click {
cursor: pointer; }
.noty_close_button {
position: absolute;
top: 2px;
right: 2px;
font-weight: bold;
width: 20px;
height: 20px;
text-align: center;
line-height: 20px;
background-color: rgba(0, 0, 0, 0.05);
border-radius: 2px;
cursor: pointer;
-webkit-transition: all .2s ease-out;
transition: all .2s ease-out; }
.noty_close_button:hover {
background-color: rgba(0, 0, 0, 0.1); }
.noty_modal {
position: fixed;
width: 100%;
height: 100%;
background-color: #000;
z-index: 10000;
opacity: .3;
left: 0;
top: 0; }
.noty_modal.noty_modal_open {
opacity: 0;
-webkit-animation: noty_modal_in .3s ease-out;
animation: noty_modal_in .3s ease-out; }
.noty_modal.noty_modal_close {
-webkit-animation: noty_modal_out .3s ease-out;
animation: noty_modal_out .3s ease-out;
-webkit-animation-fill-mode: forwards;
animation-fill-mode: forwards; }
@-webkit-keyframes noty_modal_in {
100% {
opacity: .3; } }
@keyframes noty_modal_in {
100% {
opacity: .3; } }
@-webkit-keyframes noty_modal_out {
100% {
opacity: 0; } }
@keyframes noty_modal_out {
100% {
opacity: 0; } }
@keyframes noty_modal_out {
100% {
opacity: 0; } }
@-webkit-keyframes noty_anim_in {
100% {
-webkit-transform: translate(0);
transform: translate(0);
opacity: 1; } }
@keyframes noty_anim_in {
100% {
-webkit-transform: translate(0);
transform: translate(0);
opacity: 1; } }
@-webkit-keyframes noty_anim_out {
100% {
-webkit-transform: translate(50%);
transform: translate(50%);
opacity: 0; } }
@keyframes noty_anim_out {
100% {
-webkit-transform: translate(50%);
transform: translate(50%);
opacity: 0; } }
@-webkit-keyframes noty_anim_height {
100% {
height: 0; } }
@keyframes noty_anim_height {
100% {
height: 0; } }
/*# sourceMappingURL=noty.css.map*/
/* Custom */
.noty_body {
text-align: center;
}

@ -0,0 +1 @@
{"version":3,"sources":[],"names":[],"mappings":"","file":"noty.css","sourceRoot":""}

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

@ -0,0 +1,11 @@
{% extends 'includes/base.html' %}
{% block content %}
<h2>Challenge</h2>
<p>Handle: {{ user.handle }}</p>
<p>Challenge: {{ user.challenge }}</p>
<p>Wallet Address: {{ user.wallet_address }}</p>
{% include 'includes/form.html' %}
{% endblock %}

@ -0,0 +1,8 @@
{% extends 'includes/base.html' %}
{% block content %}
<h2>Login</h2>
{% include 'includes/form.html' %}
{% endblock %}

@ -0,0 +1,8 @@
{% extends 'includes/base.html' %}
{% block content %}
<h2>Register</h2>
{% include 'includes/form.html' %}
{% endblock %}

@ -0,0 +1,18 @@
<!DOCTYPE HTML>
<html>
{% include 'includes/head.html' %}
<body style="background-color: black; color: white;">
{% include 'includes/header.html' %}
{% block content %}{% endblock %}
{% include 'includes/debug.html' %}
<style>
body {
background-color: black;
color: white;
}
a, a:visited {
color: white;
}
</style>
</body>
</html>

@ -0,0 +1,9 @@
{% if config.DEBUG and current_user.is_authenticated %}
<hr>
<h2>Debug</h2>
<p>
Username: {{ current_user.handle }} <br>
{% if current_user.email %}Email: {{ current_user.email }} <br>{% endif %}
Wallet Address: {{ current_user.wallet_address }} <br>
</p>
{% endif %}

@ -0,0 +1,18 @@
<hr>
<section id="banner">
<div class="content">
<header>
<p>This is a simple prototype and is under heavy development.</p>
</header>
<span class="image"><img src="/static/images/monero-logo.png" width=150px></span>
</div>
</section>
<footer id="footer">
<ul class="icons">
</ul>
<ul class="copyright">
<li>&copy; {{ config.SITE_NAME }}. All rights reserved.</li>
</ul>
</footer>

@ -0,0 +1,18 @@
<form method="POST" action="">
{% for f in form %}
{% if f.name == 'csrf_token' %}
{{ f }}
{% else %}
<div class="form-group">
{{ f.label }}
{{ f }}
</div>
{% endif %}
{% endfor %}
<ul>
{% for field, errors in form.errors.items() %}
<li>{{ form[field].label }}: {{ ', '.join(errors) }}</li>
{% endfor %}
</ul>
<input type="submit" value="Confirm" class="btn btn-link btn-outline btn-xl">
</form>

@ -0,0 +1,22 @@
<head>
<title>{{ config.SITE_NAME }}</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" />
<link rel="shortcut icon" href="/static/images/favicon.ico" type="image/png">
<meta itemprop="name" content="MyThing app">
<meta itemprop="description" content="This is a sample application making using of Flask, a Flask replacement for Python">
<meta itemprop="image" content="">
<meta property="og:url" content="">
<meta property="og:type" content="website">
<meta property="og:title" content="">
<meta property="og:description" content="This is a sample application making using of Flask, a Flask replacement for Python">
<meta property="og:image" content="">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="">
<meta name="twitter:description" content="This is a sample application making using of Flask, a Flask replacement for Python">
<meta name="twitter:image" content="">
<meta name="keywords" content="Wownero, Monero, crypto, swap">
<link rel="stylesheet" href="/static/css/main.css" />
<link rel="stylesheet" href="/static/css/noty.css">
<link rel="stylesheet" href="/static/css/noty-relax.css">
</head>

@ -0,0 +1,20 @@
<header id="header">
<h1 id="logo"><a href="/">{{ config.SITE_NAME }}</a></h1>
<nav id="nav">
<ul>
<li><a href="{{ url_for('auth.login') }}">Login</a></li>
<li><a href="{{ url_for('auth.register') }}">Register</a></li>
<li><a href="{{ url_for('auth.logout') }}">Logout</a></li>
</ul>
</nav>
</header>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<p>
{% for category, message in messages %}
<p>{{ message }} - {{ category }}</p>
{% endfor %}
</p>
{% endif %}
{% endwith %}

@ -0,0 +1,9 @@
{% extends 'includes/base.html' %}
{% block content %}
{% if current_user.is_authenticated %}
<p>logged in: {{ current_user.handle }}</p>
{% endif %}
{% endblock %}

@ -0,0 +1,25 @@
<!DOCTYPE HTML>
<html>
{% include 'includes/head.html' %}
<body class="is-preload landing">
<div id="page-wrapper">
{% include 'includes/header.html' %}
{% if post %}
<h1>{{ post.title }}</h1>
<p>Posted: {{ post.post_date }}</p>
<p>Edited: {{ post.last_edit_date }}</p>
<p>{{ post.content }}</p>
{% endif %}
{% include 'includes/footer.html' %}
</div>
{% include 'includes/scripts.html' %}
</body>
</html>

@ -0,0 +1,17 @@
Flask-WTF
flask-login
flask-bcrypt
hypercorn
Pillow
psycopg2-binary
python-dotenv
qrcode
redis
peewee
requests
WTForms
flask-session
flask
monero
arrow
flake8
Loading…
Cancel
Save