Compare commits

...

9 Commits
main ... revamp

@ -1,7 +1,7 @@
FROM ubuntu:21.10
FROM ubuntu:22.04
ENV MONERO_HASH 59e16c53b2aff8d9ab7a8ba3279ee826ac1f2480fbb98e79a149e6be23dd9086
ENV MONERO_DL_URL https://downloads.getmonero.org/cli/monero-linux-x64-v0.17.2.0.tar.bz2
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

@ -10,14 +10,20 @@ setup: ## Establish local environment with dependencies installed
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
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 xmrbackers
shell: ## Start Quart CLI shell
QUART_APP=app.py QUART_SECRETS=config.py QUART_DEBUG=0 QUART_ENV=production .venv/bin/quart shell
dev: ## Start Quart development web server
QUART_APP=app.py QUART_SECRETS=config.py QUART_DEBUG=1 QUART_ENV=development .venv/bin/quart run
QUART_APP=app.py QUART_SECRETS=config.py QUART_DEBUG=1 QUART_ENV=development QUART_RELOAD=true .venv/bin/quart run

@ -1,2 +1,6 @@
# lza-quart-app
Template project for Quart (Python/Flask) applications.
# xmrbackers
That's the name for now. The intention is to provide a secure, although centralized (for now) web service for hosting private content and allowing subscribers to view with subscriptions paid in Monero (XMR).
It's intended to provide any sort of digital content experience - text, images, videos, streams, and have all the robust crypto mechanics for authenticating and validating transactions.

@ -1,7 +1,16 @@
from xmrbackers.factory import create_app
#!/usr/bin/env python3
from os import environ
from xmrbackers import create_app
app = create_app()
if __name__ == '__main__':
app.run()
# print('running')
# environ['QUART_SECRETS'] = 'config.py'
# environ['QUART_DEBUG'] = '1'
# environ['QUART_ENV'] = 'development'
app.run(debug=True, use_reloader=True)

@ -1,7 +1,7 @@
#!/bin/sh
export RPC_CREDS="${2}"
export DAEMON_ADDRESS=singapore.node.xmr.pm
export DAEMON_ADDRESS="${3}"
# Define the network we plan to operate our wallet in
if [[ "${1}" == "stagenet" ]]; then
@ -19,7 +19,7 @@ fi
if [[ ! -d /data/wallet ]]; then
monero-wallet-cli ${NETWORK} \
--generate-new-wallet /data/wallet \
--daemon-address http://${DAEMON_ADDRESS}:${PORT} \
--daemon-address ${DAEMON_ADDRESS} \
--trusted-daemon \
--use-english-language-names \
--mnemonic-language English
@ -27,7 +27,7 @@ fi
# Run RPC wallet
monero-wallet-rpc ${NETWORK} \
--daemon-address http://${DAEMON_ADDRESS}:${PORT} \
--daemon-address ${DAEMON_ADDRESS} \
--wallet-file /data/wallet \
--password "" \
--rpc-login ${RPC_CREDS} \

@ -19,8 +19,8 @@ services:
context: .
dockerfile: Dockerfile-monero
ports:
- 127.0.0.1:8000:8000
- 127.0.0.1:${XMR_WALLET_RPC_PORT:-8000}:${XMR_WALLET_RPC_PORT:-8000}
volumes:
- ./data/wallet:/data
- ${DATA_DIR:-./data/wallet}:/data
command:
bash /run_wallet.sh "${XMR_WALLET_NETWORK}" "${XMR_WALLET_RPC_USER}:${XMR_WALLET_RPC_PASS}"
bash /run_wallet.sh "${XMR_WALLET_NETWORK}" "${XMR_WALLET_RPC_USER}:${XMR_WALLET_RPC_PASS}" "${XMR_DAEMON_URI}"

@ -3,11 +3,11 @@ DB_USER=xmrbackers
DB_NAME=xmrbackers
DB_HOST=localhost
XMR_WALLET_PATH=/data/xmr-wallet
XMR_WALLET_PASS=xxxxxxxxxxxxxxxxxxx
PLATFORM_WALLET=xxxxxx
XMR_WALLET_RPC_USER=xxxxxxxxxx
XMR_WALLET_RPC_PASS=xxxxxxxxxxxxxxxxxxx
XMR_WALLET_RPC_ENDPOINT=http://localhost:9090
XMR_WALLET_RPC_PORT=8000
XMR_DAEMON_URI=http://super.fast.node.xmr.pm:38089
XMR_WALLET_NETWORK=stagenet

@ -9,9 +9,8 @@ qrcode
redis
peewee
requests
SQLAlchemy
WTForms
quart
quart==0.17.0
monero
arrow
flake8

@ -0,0 +1,16 @@
# Platform Sub
### Good one
tx_id: c50b4c2c2ca26691ac6205c67d72052a882aa0d71d71b18bdfe0dd887d8b75aa
tx_key: aaf3d3b81e57b2c874f0428a50428cf38ba5369c2ad06df7d4d7e7e7bb43e308
### Bad one
tx_id: 7fd5111343a0cc9c44705a03cddb4115027a2671855806f0a0dffddc7cc889ab
tx_key: c7f1c8266a6109ea547c7c5d04d226f68faa5f5a4cf1eb7ad6077cdb2bbb830e
# Content Sub
tx_id: 1127feb964a2f35ea819454fc8dd17f23f374f829364704a7a51cbec676e09a5
tx_key: 12ca27135e6dfc1bb5f2b6dc6334a84e3e9c09003db4204061a553a49452f505
address: 75gXGUPpyqb1Hhxv6wSZ9218x1QBjBNo3dvYX6FnVMRPCWXoAX52DZnCkAe9LwjRjaGB1A6CGUr5c5hUTqdqywxN9jsutNP

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

@ -1,8 +1,9 @@
from dotenv import load_dotenv
from secrets import token_urlsafe
from datetime import timedelta
from os import getenv
from dotenv import load_dotenv
load_dotenv()
@ -11,13 +12,21 @@ SITE_NAME = getenv('SITE_NAME', 'xmrbackers')
SECRET_KEY = getenv('SECRET_KEY')
STATS_TOKEN = getenv('STATS_TOKEN', token_urlsafe(8))
SERVER_NAME = getenv('SERVER_NAME', 'localhost:5000')
PLATFORM_WALLET = getenv('PLATFORM_WALLET')
# Constants - here for easy template references
CREATOR_SUBSCRIPTION_TERM = 30 # days term of how long creator subscriptions are valid for until content is hidden
CREATOR_SUBSCRIPTION_GRACE = 21 # days grace period after expiration of creator subscriptions until content is archived
CREATOR_SUBSCRIPTION_FEE_XMR = .15 # XMR flat rate fee for creator subscriptions
CREATOR_CONTENT_FEE_PERCENT = 5 # percentage of base fee to charge for content/posts
CREATOR_ACTIVE_SUBSCRIBER_FEE_PERCENT = 10 # percentage of received subscriber fees to charge creator for platform reup
# 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')
XMR_WALLET_RPC_PORT = getenv('XMR_WALLET_RPC_PORT', 8000)
XMR_WALLET_NETWORK = getenv('XMR_WALLET_NETWORK')
# Database
DB_HOST = getenv('DB_HOST', 'localhost')
@ -31,7 +40,7 @@ REDIS_HOST = getenv('REDIS_HOST', 'localhost')
REDIS_PORT = getenv('REDIS_PORT', 6379)
# Sessions
SESSION_LENGTH = int(getenv('SESSION_LENGTH', 30))
SESSION_LENGTH = int(getenv('SESSION_LENGTH', 300))
PERMANENT_SESSION_LIFETIME = timedelta(minutes=SESSION_LENGTH)
MAX_CONTENT_LENGTH = 50 * 1024 * 1024

@ -1,10 +1,8 @@
import quart.flask_patch
from quart import Quart
from flask_bcrypt import Bcrypt
from flask_login import LoginManager
from xmrbackers.cli import cli
from xmrbackers import config
async def _setup_db(app: Quart):
@ -20,10 +18,10 @@ def create_app():
app.config.from_envvar('QUART_SECRETS')
@app.before_serving
async def startup():
from xmrbackers.routes import meta, api, auth, creator, post
from xmrbackers.routes import api, auth, creator, post, main
from xmrbackers import filters
await _setup_db(app)
app.register_blueprint(meta.bp)
app.register_blueprint(main.bp)
app.register_blueprint(api.bp)
app.register_blueprint(auth.bp)
app.register_blueprint(creator.bp)
@ -41,5 +39,3 @@ def create_app():
user = User.get_or_none(user_id)
return user
return app
bcrypt = Bcrypt(create_app())

@ -1,7 +1,7 @@
from datetime import datetime
import arrow
import monero
from monero import numbers
from quart import Blueprint, current_app
@ -22,4 +22,4 @@ def xmr_block_explorer(v):
@bp.app_template_filter('from_atomic')
def from_atomic(amt):
return monero.numbers.from_atomic(amt)
return numbers.from_atomic(amt)

@ -1,14 +1,46 @@
# import quart.flask_patch
from flask_wtf import FlaskForm
from wtforms import StringField
from wtforms.validators import DataRequired
from wtforms import StringField, FloatField
from wtforms.validators import DataRequired, ValidationError
from monero.address import address
from xmrbackers import config
class UserAuth(FlaskForm):
username = StringField('Username:', validators=[DataRequired()], render_kw={"placeholder": "Username", "class": "form-control", "type": "text"})
password = StringField('Password:', validators=[DataRequired()], render_kw={"placeholder": "Password", "class": "form-control", "type": "password"})
class ConfirmSubscription(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"})
wallet_address = StringField('XMR Address:', validators=[DataRequired()], render_kw={"placeholder": "XMR Address", "class": "form-control", "type": "text"})
def is_valid_xmr_address(form, field):
try:
# Ensure the provided address is valid address/subaddress/integrated address
a = address(field.data)
# 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'})

@ -1,17 +1,90 @@
import peewee as pw
import playhouse.postgres_ext as pwpg
from monero.wallet import Wallet
from monero.exceptions import WrongAddress
from xmrbackers import config
def check_tx_key(tx_id, tx_key, wallet_address):
def make_wallet_rpc(method, data={}):
try:
check_data = {
'txid': tx_id,
'tx_key': tx_key,
'address': wallet_address
}
w = Wallet(port=8000, user=config.XMR_WALLET_RPC_USER, password=config.XMR_WALLET_RPC_PASS)
res = w._backend.raw_request('check_tx_key', check_data)
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:
raise Exception('there was a problem i dont feel like writing good code for right now')
except Exception as e:
raise e
class EnumArrayField(pwpg.ArrayField):
def __init__(self, enum_class, *args, **kwargs):
"""
Usage:
from enum import IntEnum, unique
@unique
class UserRoles(IntEnum):
admin = 0
moderator = 1
guest = 2
[...]
roles = EnumArrayField(enum_class=UserRoles, field_class=IntegerField, default=[UserRoles.guest])
# Fetch results with `admin` OR `moderator` role set:
Model.select().where(
Model.roles.contains_any(UserRoles.admin, UserRoles.moderator)).get()
"""
super(EnumArrayField, self).__init__(*args, **kwargs)
self.enum_class = enum_class
def db_value(self, value):
"""python -> database"""
if not isinstance(value, (tuple, list)):
raise TypeError("Wrong type, must be a list of enums")
# if isinstance(value, tuple):
# value = value[0]
data = []
for enum in value:
if not isinstance(enum, self.enum_class):
raise TypeError("Wrong type, must be a list of enums")
data.append(enum.value)
return super().adapt(data)
def python_value(self, value):
"""database -> python"""
data = []
for val in value:
data.append(self.enum_class(val))
return data
class EnumIntField(pw.IntegerField):
def __init__(self, enum_class, *args, **kwargs):
"""
Usage:
from enum import IntEnum, unique
@unique
class UserStatus(IntEnum):
disabled = 0
enable = 1
banned = 2
[...]
status = EnumIntField(enum_class=UserStatus, default=UserStatus.active)
[...]
Model.select().where(Model.status != UserStatus.banned)
"""
super(pw.IntegerField, self).__init__(*args, **kwargs)
self.enum_class = enum_class
def db_value(self, value):
return value.value
def python_value(self, value):
return self.enum_class(value)

@ -1,8 +1,13 @@
from datetime import datetime
from datetime import datetime, timedelta
from enum import IntEnum, unique
from typing import List
from secrets import token_urlsafe
import peewee as pw
from monero.numbers import to_atomic, from_atomic
from xmrbackers import config
from xmrbackers.helpers import EnumArrayField, EnumIntField
db = pw.PostgresqlDatabase(
@ -12,16 +17,44 @@ db = pw.PostgresqlDatabase(
host=config.DB_HOST,
)
def gen_challenge():
return token_urlsafe().replace('-', '').replace('_', '')
@unique
class UserRole(IntEnum):
admin = 0
backer = 1
creator = 2
@unique
class PostType(IntEnum):
text = 0
gallery = 1
stream = 2
@unique
class ContentType(IntEnum):
text = 0
image = 1
video = 2
stream = 3
class User(pw.Model):
"""
User model is for pure user authentication management
and reporting.
User model is for base user management and reporting.
"""
id = pw.AutoField()
register_date = pw.DateTimeField(default=datetime.now)
last_login_date = pw.DateTimeField(default=datetime.now)
username = pw.CharField(unique=True)
password = pw.CharField()
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)
email = pw.CharField(unique=True, null=True)
challenge = pw.CharField(default=gen_challenge)
roles: List[UserRole] = EnumArrayField(enum_class=UserRole, field_class=pw.IntegerField, default=[UserRole.backer])
@property
def is_authenticated(self):
@ -37,46 +70,117 @@ class User(pw.Model):
@property
def is_admin(self):
return self.admin
return UserRole.admin in self.roles
def get_id(self):
return self.id
def regenerate_challenge(self):
self.challenge = gen_challenge()
self.save()
def is_subscribed(self, subscription):
s = Subscription.select().where(
Subscription.backer == self
).first()
if s:
return True
return False
def derive_subscription_fees(self):
base_fee_atomic = to_atomic(config.CREATOR_SUBSCRIPTION_FEE_XMR)
posts = Post.select().where(Post.creator == self)
content = Content.select().join(Post).where(Post.creator == self)
content_base_fee_atomic = base_fee_atomic * config.CREATOR_CONTENT_FEE_PERCENT
content_fee_atomic = (posts.count() + content.count()) * content_base_fee_atomic
active_subs = Subscription.select().where(
Subscription.creator == self,
Subscription.is_active == True
)
received_sub_xmr_atomic = 0
for sub in active_subs:
received_sub_xmr_atomic += sub.meta.atomic_xmr
print('User can expect to pay the following fees on their next reup:\nBase Fee: {} XMR\nContent Fees: {} ({} posts, {} uploads)\nSubscriber Fees: {}'.format(
config.CREATOR_SUBSCRIPTION_FEE_XMR,
from_atomic(content_fee_atomic),
posts.count(),
content.count(),
from_atomic(received_sub_xmr_atomic)
))
class Meta:
database = db
class CreatorProfile(pw.Model):
class Profile(pw.Model):
"""
CreatorProfile model is for creators to provide metadata about
themselves for their fans or even just the general public.
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.now)
wallet_address = pw.CharField(null=True)
create_date = pw.DateTimeField(default=datetime.utcnow)
website = pw.CharField(null=True)
twitter_handle = pw.CharField(null=True)
email = pw.CharField(unique=True, null=True)
bio = pw.CharField()
bio = pw.CharField(null=True)
verified = pw.CharField(default=False)
class Meta:
database = db
class BackerProfile(pw.Model):
class Transaction(pw.Model):
"""
BackerProfile model is for backers to give contact info
if they wanted to retain communications in some way...ie
recurring emails and/or notifications. For now.
Transaction model is a simple reference to a Monero transaction so that we can track
which transactions have occurred on-chain and which subscription they correspond to.
"""
id = pw.AutoField()
user = pw.ForeignKeyField(User, backref='backer_profile')
register_date = pw.DateTimeField(default=datetime.now)
last_login_date = pw.DateTimeField(default=datetime.now)
email = pw.CharField(unique=True, null=True)
tx_id = pw.CharField(unique=True)
atomic_xmr = pw.BigIntegerField()
to_address = pw.CharField()
class Meta:
database = db
class CreatorSubscription(pw.Model):
"""
CreatorSubscription model is for tracking subscriptions of creators to the platform.
Subscription is a flat fee with a modifier based on usage/consumption
and will be re-negotiated every N days.
"""
id = pw.AutoField()
user = pw.ForeignKeyField(User)
tx = pw.ForeignKeyField(Transaction)
create_date = pw.DateTimeField(default=datetime.utcnow)
atomic_xmr = pw.BigIntegerField()
term_hours = pw.IntegerField(default=config.CREATOR_SUBSCRIPTION_TERM * 24)
grace_hours = pw.IntegerField(default=config.CREATOR_SUBSCRIPTION_GRACE * 24)
@property
def grace_start_date(self):
# Hide content after this date if no new valid platform subscription
return self.create_date + timedelta(hours=self.term_hours)
@property
def delete_start_date(self):
# Archive content after this date if no new valid platform subscription
return self.grace_start_date + timedelta(hours=self.grace_hours)
@property
def hours_until_content_hidden(self):
return self.grace_start_date - datetime.utcnow()
@property
def hours_until_content_archived(self):
return self.delete_start_date - datetime.utcnow()
@property
def is_active(self):
return self.grace_start_date > datetime.utcnow()
class Meta:
database = db
@ -85,20 +189,21 @@ class BackerProfile(pw.Model):
class SubscriptionMeta(pw.Model):
"""
SubscriptionMeta model is for the Creator to define details about
their subscription plan to release for subscribers. There is no
their subscription plan to release for subscribers/backers. There is no
editing in place, only creating new plans; anyone utilizing an
existing subscription (by loading it on screen) will be grandfathered in.
"""
id = pw.AutoField()
create_date = pw.DateTimeField(default=datetime.now)
creator = pw.ForeignKeyField(CreatorProfile)
user = pw.ForeignKeyField(User)
create_date = pw.DateTimeField(default=datetime.utcnow)
atomic_xmr = pw.BigIntegerField()
number_hours = pw.IntegerField()
wallet_address = pw.CharField()
def get_end_date(self) -> datetime:
# some timedelta shiz
pass
def get_active_subscriptions(self):
return Subscription.select().where(
Subscription.meta == self
)
class Meta:
database = db
@ -106,33 +211,56 @@ class SubscriptionMeta(pw.Model):
class Subscription(pw.Model):
"""
Subscription model gets created when backers can confirm payment via
the `check_tx_key` RPC method. Once a subscription is in place and is
associated with a user, that user is then elligible to view that
creator's content.
Subscription model gets created when users/backers can confirm payment via
the `check_tx_key` RPC method to the creator's wallet. Once a subscription
is in place and is associated with a user, that user is then elligible to
view that creator's content until the subscription expires (number_hours).
"""
id = pw.AutoField()
subscribe_date = pw.DateTimeField(default=datetime.now)
active = pw.BooleanField(default=True)
creator = pw.ForeignKeyField(CreatorProfile)
backer = pw.ForeignKeyField(BackerProfile)
subscribe_date = pw.DateTimeField(default=datetime.utcnow)
tx = pw.ForeignKeyField(Transaction)
creator = pw.ForeignKeyField(User)
backer = pw.ForeignKeyField(User)
meta = pw.ForeignKeyField(SubscriptionMeta)
@property
def is_active(self):
end_date = self.subscribe_date + timedelta(hours=self.meta.number_hours)
return end_date > datetime.utcnow()
class Meta:
database = db
class TextPost(pw.Model):
class Post(pw.Model):
"""
TextPost model is the first content type available to post. Metadata
here is basic for now, let's proof out the other components first.
Post model represents a post from a creator consisting of Content objects
which is only viewable by backers with an active subscription.
"""
id = pw.AutoField()
post_date = pw.DateTimeField(default=datetime.now)
post_date = pw.DateTimeField(default=datetime.utcnow)
hidden = pw.BooleanField(default=False)
content = pw.TextField()
title = pw.CharField()
last_edit_date = pw.DateTimeField(default=datetime.now)
creator = pw.ForeignKeyField(CreatorProfile)
last_edit_date = pw.DateTimeField(default=datetime.utcnow)
creator = pw.ForeignKeyField(User)
type: List[PostType] = EnumIntField(enum_class=PostType, default=PostType.text)
class Meta:
database = db
class Content(pw.Model):
"""
Content model is any uploaded content from a creator.
"""
id = pw.AutoField()
location = pw.CharField()
upload_date = pw.DateTimeField(default=datetime.utcnow)
post = pw.ForeignKeyField(Post)
type: List[ContentType] = EnumIntField(enum_class=ContentType, default=ContentType.text)
class Meta:
database = db

@ -2,8 +2,8 @@ from quart import Blueprint, render_template
from quart import flash, redirect, url_for
from flask_login import login_user, logout_user, current_user
from xmrbackers.factory import bcrypt
from xmrbackers.forms import UserAuth
from xmrbackers.forms import UserLogin, UserRegistration, UserChallenge
from xmrbackers.helpers import make_wallet_rpc
from xmrbackers.models import User
@ -11,60 +11,87 @@ bp = Blueprint('auth', 'auth')
@bp.route("/register", methods=["GET", "POST"])
async def register():
form = UserAuth()
form = UserRegistration()
if current_user.is_authenticated:
await flash('Already registered and authenticated.')
return redirect(url_for('meta.index'))
return redirect(url_for('main.index'))
if form.validate_on_submit():
# Check if username already exists
# Check if handle already exists
user = User.select().where(
User.username == form.username.data
User.handle == form.handle.data
).first()
if user:
await flash('This username is already registered.')
await flash('This handle is already registered.')
return redirect(url_for('auth.login'))
# Save new user
user = User(
username=form.username.data,
password=bcrypt.generate_password_hash(form.password.data).decode('utf8'),
handle=form.handle.data,
wallet_address=form.wallet_address.data,
)
user.save()
login_user(user)
return redirect(url_for('meta.index'))
return redirect(url_for('main.index'))
return await render_template("auth/register.html", form=form)
@bp.route("/login", methods=["GET", "POST"])
async def login():
form = UserAuth()
form = UserLogin()
if current_user.is_authenticated:
await flash('Already logged in.')
return redirect(url_for('meta.index'))
return redirect(url_for('main.index'))
if form.validate_on_submit():
# Check if user doesn't exist
user = User.select().where(
User.username == form.username.data
User.handle == form.handle.data
).first()
if not user:
await flash('Invalid username or password.')
await flash('That handle does not exist.')
return redirect(url_for('auth.login'))
return redirect(url_for('auth.challenge', handle=user.handle))
# Check if password is correct
password_matches = bcrypt.check_password_hash(
user.password,
form.password.data
)
if not password_matches:
await flash('Invalid username or password.')
return redirect(url_for('auth.login'))
return await render_template("auth/login.html", form=form)
login_user(user)
return redirect(url_for('meta.index'))
@bp.route("/login/challenge/<handle>", methods=["GET", "POST"])
async def challenge(handle):
form = UserChallenge()
user = User.select().where(User.handle == handle).first()
if not user:
await flash('User does not exist.')
return redirect(url_for('main.index'))
return await render_template("auth/login.html", form=form)
if current_user.is_authenticated:
await 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)
await flash('Successful login!')
return redirect(url_for('main.index'))
else:
await flash('Invalid signature. Try again.')
return redirect(url_for('auth.challenge', handle=handle))
except Exception as e:
await flash(f'Issue with checking the signature provided: {e}')
return redirect(url_for('auth.challenge', handle=handle))
return await render_template(
'auth/challenge.html',
user=user,
form=form
)
@bp.route("/logout")
async def logout():
@ -72,30 +99,4 @@ async def logout():
logout_user()
else:
await flash('Not authenticated!')
return redirect(url_for('meta.index'))
# @auth_bp.route("/reset/<string:hash>", methods=["GET", "POST"])
# def reset(hash):
# hash = PasswordReset.query.filter(PasswordReset.hash==hash).first()
# if not hash:
# flash('Invalid password reset hash')
# return redirect(url_for('auth.login'))
#
# if hash.hours_elapsed() > hash.expiration_hours or hash.expired:
# flash('Reset hash has expired')
# return redirect(url_for('auth.login'))
#
# form = ResetPassword()
# if form.validate_on_submit():
# try:
# user = User.query.get(hash.user)
# user.password = bcrypt.generate_password_hash(form.password.data).decode('utf8')
# hash.expired = True
# db.session.commit()
# flash('Password reset successfully')
# return redirect(url_for('auth.login'))
# except:
# flash('Error resetting password')
# return redirect(url_for('auth.login'))
#
# return render_template('auth/reset.html', form=form)
return redirect(url_for('main.index'))

@ -1,100 +1,230 @@
from quart import Blueprint, render_template, flash, redirect, url_for
from quart import Blueprint, render_template, flash, redirect, url_for, request
from flask_login import current_user, login_required
from monero.wallet import Wallet
from monero.address import address
from monero.numbers import to_atomic, from_atomic
from monero.backends.jsonrpc.exceptions import RPCError
from xmrbackers.forms import ConfirmSubscription
from xmrbackers.models import User, CreatorProfile, BackerProfile, TextPost
from xmrbackers.models import Subscription, SubscriptionMeta
from xmrbackers.helpers import check_tx_key
from xmrbackers import config
from xmrbackers.models import *
from xmrbackers.helpers import make_wallet_rpc
from xmrbackers import config, forms
bp = Blueprint('creator', 'creator')
@bp.route('/creators')
async def all():
creators = CreatorProfile.select().order_by(
CreatorProfile.create_date.desc()
creators = User.select().join(Profile).where(
User.roles.contains_any(UserRole.creator)
).order_by(
Profile.create_date.desc()
)
return await render_template('creator/creators.html', creators=creators)
@bp.route('/creator/<username>')
async def show(username):
user = User.select().where(User.username == username)
creator = CreatorProfile.select().where(
CreatorProfile.user == user
).first()
if creator:
posts = TextPost.select().where(
TextPost.creator == creator,
TextPost.hidden == False
).order_by(TextPost.post_date.desc())
return await render_template(
'creator/creator.html',
creator=creator,
posts=posts
)
else:
await flash('That creator does not exist.')
return redirect(url_for('meta.index'))
@bp.route('/creator/<username>/subscription')
async def subscription(username):
user = User.select().where(User.username == username)
creator = CreatorProfile.select().where(
CreatorProfile.user == user
@bp.route('/creators/join', methods=['GET', 'POST'])
@login_required
async def join():
form = forms.ConfirmPlatformSubscription()
valid_address = False
try:
address(config.PLATFORM_WALLET)
valid_address = True
except:
pass
if not config.PLATFORM_WALLET or valid_address is False:
await flash('Platform operator has not setup wallet yet. Try later.', 'warning')
return redirect(url_for('main.index'))
if UserRole.creator in current_user.roles:
await flash('You already are a creator!', 'warning')
return redirect(url_for('main.index'))
if form.validate_on_submit():
try:
data = {
'txid': form.tx_id.data,
'tx_key': form.tx_key.data,
'address': config.PLATFORM_WALLET
}
res = make_wallet_rpc('check_tx_key', data)
existing_tx = Transaction.select().where(
Transaction.tx_id == form.tx_id.data.lower()
).first()
existing_sub = CreatorSubscription.select().where(
CreatorSubscription.tx == existing_tx
).first()
for i in ['confirmations', 'in_pool', 'received']:
assert i in res
if res['in_pool']:
await flash('That transaction is still in the mempool. You need to wait a few more minutes for it to clear. Try again in a bit.', 'warning')
elif res['received'] < to_atomic(config.CREATOR_SUBSCRIPTION_FEE_XMR):
await flash(f'Not enought XMR sent. {from_atomic(res["received"])} XMR sent, expected {config.CREATOR_SUBSCRIPTION_FEE_XMR} XMR.', 'error')
elif existing_tx:
if existing_sub:
await flash('This transaction was already used for another subscription.', 'warning')
else:
await flash('Adding creator subscription.', 'success')
c = CreatorSubscription(
user=current_user,
tx=existing_tx,
atomic_xmr=res['received']
)
c.save()
current_user.roles.append(UserRole.creator)
current_user.save()
elif res['received'] >= to_atomic(config.CREATOR_SUBSCRIPTION_FEE_XMR):
await flash('Success! Welcome to the creator club!', 'success')
t = Transaction(
tx_id=form.tx_id.data.lower(),
atomic_xmr=res['received'],
to_address=config.PLATFORM_WALLET
)
t.save()
c = CreatorSubscription(
user=current_user,
tx=t,
atomic_xmr=res['received']
)
c.save()
current_user.roles.append(UserRole.creator)
current_user.save()
return redirect(url_for('creator.show', handle=current_user.handle))
else:
await flash('Something went wrong. No idea what, though. Check with admin.', 'error')
except Exception as e:
await flash(f'seems bad: {e}', 'error')
return await render_template(
'creator/join.html',
form=form
)
if creator:
subscription_meta = SubscriptionMeta.select().where(
SubscriptionMeta.creator == creator
).order_by(SubscriptionMeta.create_date.desc()).first()
form = ConfirmSubscription()
return await render_template(
'creator/subscription.html',
subscription_meta=subscription_meta,
form=form
)
else:
await flash('That creator does not exist.')
return redirect(url_for('meta.index'))
@bp.route('/subscription/<int:subscription_id>/confirm', methods=['POST'])
async def confirm_subscription(subscription_id):
# do checks here for SubscriptionMeta assumption
sm = SubscriptionMeta.get_or_none(subscription_id)
form = ConfirmSubscription()
@bp.route('/creator/<handle>', methods=['GET', 'POST'])
async def show(handle):
form = forms.ConfirmCreatorSubscription()
creator = User.select().join(Profile, pw.JOIN.LEFT_OUTER).where(
User.handle == handle,
User.roles.contains_any(UserRole.creator)
).first()
if not creator:
await flash('That creator does not exist.', 'warning')
return redirect(url_for('main.index'))
creator.derive_subscription_fees()
posts = Post.select().where(
Post.creator == creator,
Post.hidden == False
).order_by(Post.post_date.desc())
subscriptions = SubscriptionMeta.select().where(
SubscriptionMeta.user == creator
).order_by(SubscriptionMeta.create_date.desc())
if form.validate_on_submit():
w = Wallet(
port=8000,
user=config.XMR_WALLET_RPC_USER,
password=config.XMR_WALLET_RPC_PASS
)
check_data = {
data = {
'txid': form.tx_id.data,
'tx_key': form.tx_key.data,
'address': form.wallet_address.data
}
try:
res = w._backend.raw_request('check_tx_key', check_data)
except:
await flash(f'Invalid transaction! No subscription for you!')
return redirect(url_for('creator.show', username=sm.creator.user.username))
if res['received'] >= sm.atomic_xmr:
backer_profile = BackerProfile.select().where(
BackerProfile.user == current_user
).first()
s = Subscription(
creator=sm.creator.id,
backer=backer_profile.id,
meta=sm.id,
res = make_wallet_rpc('check_tx_key', data)
subs = subscriptions.where(
SubscriptionMeta.wallet_address == data['address']
)
s.save()
await flash(f'Found valid transaction! You are now subscribed to {sm.creator.user.username}!')
return redirect(url_for('creator.show', username=sm.creator.user.username))
else:
await flash('Not enough XMR sent! No subscription for you!')
return redirect(url_for('creator.show', username=sm.creator.user.username))
else:
await flash('Unable to accept form POST.')
return redirect(url_for('meta.index'))
for sub in subs:
if res['in_pool'] is False and res['received'] >= sub.atomic_xmr:
await flash('Found a valid subscription.', 'success')
print(request)
return redirect(url_for('creator.show', handle=handle))
except RPCError:
await flash('Failed to check TX Key. Problem with provided info.', 'error')
return await render_template(
'creator/show.html',
creator=creator,
subscription=subscriptions.first(),
posts=posts,
form=form
)
@bp.route('/creator/subscriptions/manage', methods=['GET', 'POST'])
@login_required
async def manage_subscriptions():
form = forms.CreateSubscription()
if UserRole.creator not in current_user.roles:
await flash('You are not a creator!', 'warning')
return redirect(url_for('main.index'))
platform_subs = CreatorSubscription.select().where(
CreatorSubscription.user == current_user
).order_by(CreatorSubscription.create_date.desc())
content_subs = SubscriptionMeta.select().where(
SubscriptionMeta.user == current_user
).order_by(SubscriptionMeta.create_date.desc())
subscribers = Subscription.select().where(
Subscription.meta.in_(content_subs)
).order_by(Subscription.subscribe_date.desc())
if form.validate_on_submit():
s = SubscriptionMeta(
user=current_user,
atomic_xmr=to_atomic(form.price_xmr.data),
number_hours=form.number_days.data * 24.0,
wallet_address=current_user.wallet_address
)
s.save()
await flash('posting form data', 'success')
return await render_template(
'creator/manage_subscriptions.html',
platform_subs=platform_subs,
content_subs=content_subs,
subscribers=subscribers,
form=form
)
# @bp.route('/subscription/<int:subscription_id>/confirm', methods=['POST'])
# async def confirm_subscription(subscription_id):
# # do checks here for SubscriptionMeta assumption
# sm = SubscriptionMeta.get_or_none(subscription_id)
# form = ConfirmPlatformSubscription()
# if form.validate_on_submit():
# w = Wallet(
# port=8000,
# user=config.XMR_WALLET_RPC_USER,
# password=config.XMR_WALLET_RPC_PASS
# )
# check_data = {
# 'txid': form.tx_id.data,
# 'tx_key': form.tx_key.data,
# 'address': form.wallet_address.data
# }
# try:
# res = w._backend.raw_request('check_tx_key', check_data)
# except:
# await flash(f'Invalid transaction! No subscription for you!')
# return redirect(url_for('creator.show', username=sm.creator.user.username))
# if res['received'] >= sm.atomic_xmr:
# backer_profile = BackerProfile.select().where(
# BackerProfile.user == current_user
# ).first()
# s = Subscription(
# creator=sm.creator.id,
# backer=backer_profile.id,
# meta=sm.id,
# )
# s.save()
# await flash(f'Found valid transaction! You are now subscribed to {sm.creator.user.username}!')
# return redirect(url_for('creator.show', username=sm.creator.user.username))
# else:
# await flash('Not enough XMR sent! No subscription for you!')
# return redirect(url_for('creator.show', username=sm.creator.user.username))
# else:
# await flash('Unable to accept form POST.')
# return redirect(url_for('meta.index'))

@ -0,0 +1,30 @@
from quart import Blueprint, render_template
from flask_login import current_user
from xmrbackers.models import *
bp = Blueprint('main', 'main')
@bp.route('/')
async 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 await render_template(
'index.html',
feed=feed
)

@ -1,30 +0,0 @@
from quart import Blueprint, render_template
from flask_login import current_user
from xmrbackers.models import CreatorProfile, Subscription, TextPost
bp = Blueprint('meta', 'meta')
@bp.route('/')
async def index():
feed = None
if current_user.is_authenticated:
backer = current_user.backer_profile.first()
new_creators = CreatorProfile.select().where(
CreatorProfile.verified == True
).order_by(CreatorProfile.create_date.desc()).execute()
active_subscriptions = Subscription.select().where(
Subscription.active == True,
Subscription.backer == backer
).order_by(Subscription.subscribe_date.desc()).execute()
new_posts = TextPost.select().where(
TextPost.hidden == False,
TextPost.creator in [c.creator for c in active_subscriptions]
).order_by(TextPost.post_date.desc()).execute()
feed = {
'new_creators': [i.user.username for i in new_creators],
'new_posts': [i.title for i in new_posts],
'active_subscriptions': [i.id for i in active_subscriptions]
}
return await render_template('index.html', feed=feed)

@ -1,27 +1,27 @@
from quart import Blueprint, render_template, flash, redirect, url_for
from flask_login import current_user
from xmrbackers.models import TextPost, Subscription
from xmrbackers.models import Content, Subscription
bp = Blueprint('post', 'post')
@bp.route('/post/<int:post_id>')
async def show(post_id):
post = TextPost.get_or_none(post_id)
if post:
if current_user.is_anonymous:
await 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 await render_template('post/show.html', post=post)
else:
await 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'))
# @bp.route('/post/<int:post_id>')
# async def show(post_id):
# post = TextPost.get_or_none(post_id)
# if post:
# if current_user.is_anonymous:
# await 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 await render_template('post/show.html', post=post)
# else:
# await 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,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 %}

@ -1,46 +1,8 @@
<!DOCTYPE HTML>
<html>
{% extends 'includes/base.html' %}
{% include 'includes/head.html' %}
{% block content %}
<body class="is-preload landing">
<div id="page-wrapper">
<h2>Login</h2>
{% include 'includes/form.html' %}
{% include 'includes/header.html' %}
<section id="banner">
<div class="content">
<header>
<h2>Login</h2>
<form method="POST" action="{{ url_for('auth.login') }}">
{% 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="Login" class="btn btn-link btn-outline btn-xl">
</form>
</header>
<span class="image"><img src="/static/images/monero-logo.png" width=150px></span>
</div>
</section>
{% include 'includes/footer.html' %}
</div>
{% include 'includes/scripts.html' %}
</body>
</html>
{% endblock %}

@ -1,46 +1,8 @@
<!DOCTYPE HTML>
<html>
{% extends 'includes/base.html' %}
{% include 'includes/head.html' %}
{% block content %}
<body class="is-preload landing">
<div id="page-wrapper">
<h2>Register</h2>
{% include 'includes/form.html' %}
{% include 'includes/header.html' %}
<section id="banner">
<div class="content">
<header>
<h2>Register</h2>
<form method="POST" action="{{ url_for('auth.register') }}">
{% 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="Register" class="btn btn-link btn-outline btn-xl">
</form>
</header>
<span class="image"><img src="/static/images/monero-logo.png" width=150px></span>
</div>
</section>
{% include 'includes/footer.html' %}
</div>
{% include 'includes/scripts.html' %}
</body>
</html>
{% endblock %}

@ -1,30 +0,0 @@
<!DOCTYPE HTML>
<html>
{% include 'includes/head.html' %}
<body class="is-preload landing">
<div id="page-wrapper">
{% include 'includes/header.html' %}
{% if creator %}
<h1>{{ creator.user.username }}</h1>
<p>Bio: {{ creator.bio }}</p>
{% endif %}
{% if posts %}
<h1>Posts</h1>
{% for post in posts %}
<p><a href="{{ url_for('post.show', post_id=post.id) }}">{{ post.id }} - {{ post.title }} - {{ post.post_date | humanize }}</a></p>
{% endfor %}
{% endif %}
{% include 'includes/footer.html' %}
</div>
{% include 'includes/scripts.html' %}
</body>
</html>

@ -0,0 +1,17 @@
{% extends 'includes/base.html' %}
{% block content %}
<h1>Become a Creator</h1>
<p>
Creator subscriptions have a flat rate of {{ config.CREATOR_SUBSCRIPTION_FEE_XMR }} XMR with a modifier for how much content is currently stored. <br>
Subscriptions are a {{ config.CREATOR_SUBSCRIPTION_TERM }} day term. <br>
Please send XMR to the platform wallet below and provide your <code>tx_hash</code> and <code>tx_key</code> in the form below to initiate your platform subscription.
</p>
<p>
Subscription Price: {{ config.CREATOR_SUBSCRIPTION_FEE_XMR }} XMR <br>
Platform Wallet: {{ config.PLATFORM_WALLET }}
</p>
{% include 'includes/form.html' %}
{% endblock %}

@ -0,0 +1,49 @@
{% extends 'includes/base.html' %}
{% block content %}
<h2>Platform Subscriptions</h2>
<ul>
{% for s in platform_subs %}
<li>
Platform Subscription ID: {{ s.id }} <br>
Created: {{ s.create_date }} <br>
TX ID: {{ s.tx.tx_id }} <br>
Paid: {{ s.tx.atomic_xmr | from_atomic }} XMR <br>
Term Length: {{ s.term_hours }} hours ({{ s.hours_until_content_hidden }} hours) <br>
Hide Content: {{ s.hours_until_content_hidden }} hours <br>
Archive Content: {{ s.hours_until_content_archived }} hours <br>
Active Subscription: <strong>{{ s.is_active }}</strong> <br>
<br>
</li>
{% endfor %}
</ul>
<h2>Content Subscriptions</h2>
{% if not content_subs %}
<p>No subscriptions to your content yet.</p>
{% else %}
<ul>
{% for s in content_subs %}
<li>
Content Subscription ID: {{ s.id }} <br>
Created: {{ s.create_date }} <br>
Price: {{ s.atomic_xmr | from_atomic }} XMR <br>
Term Length: {{ s.number_hours }} hours <br>
Active Subscriptions: {{ s.get_active_subscriptions().count() }} <br>
</li>
{% endfor %}
</ul>
{% endif %}
{% include 'includes/form.html' %}
<h2>Subscribers</h2>
{% if not subscribers %}
<p>No subscribers yet.</p>
{% else %}
{% for subscriber in subscribers %}
{{ subscriber }}
{% endfor %}
{% endif %}
{% endblock %}

@ -0,0 +1,38 @@
{% extends 'includes/base.html' %}
{% block content %}
<h1>{{ creator.handle }}</h1>
{% if creator.bio %}<p>Bio: {{ creator.bio }}</p>{% endif %}
<h2>Subscribe</h2>
{% if subscription %}
<p>
Send <code>{{ subscription.atomic_xmr | from_atomic | round(2) }}</code> XMR to
<code>{{ subscription.wallet_address }}</code>
and provide your TX ID and TX KEY to subscribe to this user
and view their content for the next {{ subscription.number_hours / 24 }} days
({{ subscription.number_hours }} hours).
</p>
{% else %}
<p>This creator has not setup a subscription plan yet.</p>
{% endif %}
{% if current_user.is_authenticated %}
{% if not current_user.is_subscribed(subscription) %}
{% include 'includes/form.html' %}
{% else %}
subscribed
{% endif %}
{% else %}
<p><a href="{{ url_for('auth.login') }}">Login to subscribe.</a></p>
{% endif %}
<h2>Content</h2>
{% if not posts %}<p>This creator has not posted any content yet.</p>{% endif %}
{% for post in posts %}
<p><a href="{{ url_for('post.show', post_id=post.id) }}">{{ post.id }} - {{ post.title }} - {{ post.post_date | humanize }}</a></p>
{% endfor %}
{% endblock %}

@ -0,0 +1,19 @@
<!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' %}
{% include 'includes/scripts.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 %}

@ -1,3 +1,14 @@
<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>

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

@ -7,23 +7,20 @@
<li><a href="{{ url_for('auth.logout') }}">Logout</a></li>
</ul>
</nav>
<p>Authenticated: {{ current_user.is_authenticated }}</p>
{% if current_user.is_authenticated %}
<p>Username: {{ current_user.username }}</p>
<p>Email: {{ current_user.email }}</p>
<p>Password: {{ current_user.password }}</p>
{% endif %}
</header>
<hr>
<script src="/static/js/noty.js"></script>
{% with messages = get_flashed_messages() %}
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<script type="text/javascript">
{% for message in messages %}
{% for category, message in messages %}
{% if category == "message" %}
{% set _c = "info" %}
{% else %}
{% set _c = category %}
{% endif %}
new Noty({
type: 'error',
type: '{{ _c }}',
theme: 'relax',
layout: 'topCenter',
text: '{{ message }}',

@ -1,40 +1,35 @@
<!DOCTYPE HTML>
<html>
{% include 'includes/head.html' %}
<body class="is-preload landing">
<div id="page-wrapper">
{% include 'includes/header.html' %}
{% if feed %}
{% for k in feed %}
<h2>{{ k }}</h2>
{% if k == 'new_creators' %}
<p><a href="{{ url_for('creator.show', username=feed[k]) }}">{{ feed[k] }}</a></p>
{% else %}
<p>{{ feed[k]}}</p>
{% endif %}
{% endfor %}
{% else %}
<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>
{% endif %}
{% include 'includes/footer.html' %}
</div>
{% include 'includes/scripts.html' %}
</body>
</html>
{% extends 'includes/base.html' %}
{% block content %}
{% if current_user.is_authenticated %}
{% if 2 not in current_user.roles %}
<a href="{{ url_for('creator.join') }}">Become a Creator</a>
{% else %}
<a href="{{ url_for('creator.manage_subscriptions') }}">Manage Subscriptions</a>
{% endif %}
{% endif %}
{% if feed %}
<h1>Feed</h1>
{% if feed['new_creators'] %}
<h2>New Creators</h2>
{% for c in feed['new_creators'] %}
<p><a href="{{ url_for('creator.show', handle=c.handle) }}">{{ c.handle }}</a></p>
{% endfor %}
{% endif %}
{% if feed['new_posts'] %}
<h2>New Posts</h2>
{% for p in feed['new_posts'] %}
<p>{{ p.id }}</p>
{% endfor %}
{% endif %}
{% if feed['active_subscriptions'] %}
<h2>Active Subscriptions</h2>
{% for s in feed['active_subscriptions'] %}
<p>{{ s.id }}</p>
{% endfor %}
{% endif %}
{% endif %}
{% endblock %}

Loading…
Cancel
Save