init
commit
5e50fd1640
@ -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>© {{ 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…
Reference in New Issue