revamping entire backend

revamp
lza_menace 2 years ago
parent b7a5d042f7
commit 6dc1b351b8

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

@ -3,5 +3,6 @@ from xmrbackers.factory import create_app
app = create_app() app = create_app()
if __name__ == '__main__': if __name__ == '__main__':
app.run() app.run(debug=True, use_reloader=True)

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

@ -19,8 +19,8 @@ services:
context: . context: .
dockerfile: Dockerfile-monero dockerfile: Dockerfile-monero
ports: ports:
- 127.0.0.1:8000:8000 - 127.0.0.1:${XMR_WALLET_RPC_PORT:-8000}:${XMR_WALLET_RPC_PORT:-8000}
volumes: volumes:
- ./data/wallet:/data - ${DATA_DIR:-./data/wallet}:/data
command: 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}"

@ -7,7 +7,7 @@ XMR_WALLET_PATH=/data/xmr-wallet
XMR_WALLET_PASS=xxxxxxxxxxxxxxxxxxx XMR_WALLET_PASS=xxxxxxxxxxxxxxxxxxx
XMR_WALLET_RPC_USER=xxxxxxxxxx XMR_WALLET_RPC_USER=xxxxxxxxxx
XMR_WALLET_RPC_PASS=xxxxxxxxxxxxxxxxxxx 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_DAEMON_URI=http://super.fast.node.xmr.pm:38089
XMR_WALLET_NETWORK=stagenet XMR_WALLET_NETWORK=stagenet

@ -11,7 +11,7 @@ peewee
requests requests
SQLAlchemy SQLAlchemy
WTForms WTForms
quart quart==0.17.0
monero monero
arrow arrow
flake8 flake8

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

@ -16,8 +16,10 @@ SERVER_NAME = getenv('SERVER_NAME', 'localhost:5000')
XMR_WALLET_PASS = getenv('XMR_WALLET_PASS') XMR_WALLET_PASS = getenv('XMR_WALLET_PASS')
XMR_WALLET_RPC_USER = getenv('XMR_WALLET_RPC_USER') XMR_WALLET_RPC_USER = getenv('XMR_WALLET_RPC_USER')
XMR_WALLET_RPC_PASS = getenv('XMR_WALLET_RPC_PASS') XMR_WALLET_RPC_PASS = getenv('XMR_WALLET_RPC_PASS')
XMR_WALLET_RPC_ENDPOINT = getenv('XMR_WALLET_RPC_ENDPOINT') XMR_WALLET_RPC_PORT = getenv('XMR_WALLET_RPC_PORT', 8000)
XMR_DAEMON_URI = getenv('XMR_DAEMON_URI') XMR_DAEMON_URI = getenv('XMR_DAEMON_URI')
XMR_WALLET_NETWORK = getenv('XMR_WALLET_NETWORK')
# Database # Database
DB_HOST = getenv('DB_HOST', 'localhost') DB_HOST = getenv('DB_HOST', 'localhost')
@ -31,7 +33,7 @@ REDIS_HOST = getenv('REDIS_HOST', 'localhost')
REDIS_PORT = getenv('REDIS_PORT', 6379) REDIS_PORT = getenv('REDIS_PORT', 6379)
# Sessions # Sessions
SESSION_LENGTH = int(getenv('SESSION_LENGTH', 30)) SESSION_LENGTH = int(getenv('SESSION_LENGTH', 300))
PERMANENT_SESSION_LIFETIME = timedelta(minutes=SESSION_LENGTH) PERMANENT_SESSION_LIFETIME = timedelta(minutes=SESSION_LENGTH)
MAX_CONTENT_LENGTH = 50 * 1024 * 1024 MAX_CONTENT_LENGTH = 50 * 1024 * 1024

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

@ -1,12 +1,36 @@
# import quart.flask_patch
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from wtforms import StringField from wtforms import StringField
from wtforms.validators import DataRequired from wtforms.validators import DataRequired, ValidationError
from monero.address import address
from xmrbackers import config
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 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): class ConfirmSubscription(FlaskForm):
tx_id = StringField('TX ID:', validators=[DataRequired()], render_kw={"placeholder": "TX ID", "class": "form-control", "type": "text"}) tx_id = StringField('TX ID:', validators=[DataRequired()], render_kw={"placeholder": "TX ID", "class": "form-control", "type": "text"})

@ -1,8 +1,11 @@
import peewee as pw
import playhouse.postgres_ext as pwpg
from monero.wallet import Wallet from monero.wallet import Wallet
from monero.exceptions import WrongAddress # from monero.exceptions import WrongAddress
from xmrbackers import config from xmrbackers import config
def check_tx_key(tx_id, tx_key, wallet_address): def check_tx_key(tx_id, tx_key, wallet_address):
try: try:
check_data = { check_data = {
@ -10,8 +13,84 @@ def check_tx_key(tx_id, tx_key, wallet_address):
'tx_key': tx_key, 'tx_key': tx_key,
'address': wallet_address 'address': wallet_address
} }
w = Wallet(port=8000, user=config.XMR_WALLET_RPC_USER, password=config.XMR_WALLET_RPC_PASS) w = Wallet(port=config.XMR_WALLET_RPC_PORT, user=config.XMR_WALLET_RPC_USER, password=config.XMR_WALLET_RPC_PASS)
res = w._backend.raw_request('check_tx_key', check_data) res = w._backend.raw_request('check_tx_key', check_data)
return res return res
except: except:
raise Exception('there was a problem i dont feel like writing good code for right now') raise Exception('there was a problem i dont feel like writing good code for right now')
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)
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')
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")
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,12 @@
from datetime import datetime from datetime import datetime
from enum import IntEnum, unique
from typing import List
from secrets import token_urlsafe
import peewee as pw import peewee as pw
from xmrbackers import config from xmrbackers import config
from xmrbackers.helpers import EnumArrayField, EnumIntField
db = pw.PostgresqlDatabase( db = pw.PostgresqlDatabase(
@ -12,16 +16,33 @@ db = pw.PostgresqlDatabase(
host=config.DB_HOST, host=config.DB_HOST,
) )
@unique
class UserRole(IntEnum):
admin = 0
backer = 1
creator = 2
@unique
class ContentType(IntEnum):
text = 0
gallery = 1
stream = 2
class User(pw.Model): class User(pw.Model):
""" """
User model is for pure user authentication management User model is for base user management and reporting.
and reporting.
""" """
id = pw.AutoField() id = pw.AutoField()
register_date = pw.DateTimeField(default=datetime.now) register_date = pw.DateTimeField(default=datetime.utcnow)
last_login_date = pw.DateTimeField(default=datetime.now) last_login_date = pw.DateTimeField(default=datetime.utcnow)
username = pw.CharField(unique=True) handle = pw.CharField(unique=True)
password = pw.CharField() wallet_address = pw.CharField(unique=True)
email = pw.CharField(unique=True, null=True)
challenge = pw.CharField(default=token_urlsafe)
roles: List[UserRole] = EnumArrayField(enum_class=UserRole, field_class=pw.IntegerField, default=[UserRole.backer])
@property @property
def is_authenticated(self): def is_authenticated(self):
@ -37,51 +58,38 @@ class User(pw.Model):
@property @property
def is_admin(self): def is_admin(self):
return self.admin return UserRole.admin in self.roles
def get_id(self): def get_id(self):
return self.id return self.id
def generate_challenge(self):
self.challenge = token_urlsafe(24)
self.save()
return self.challenge
class Meta: class Meta:
database = db database = db
class CreatorProfile(pw.Model): class Profile(pw.Model):
""" """
CreatorProfile model is for creators to provide metadata about Profile model is for users to provide metadata about
themselves for their fans or even just the general public. themselves; Creators for their fans or even just the general public.
Links to social media, contact info, portfolio sites, etc Links to social media, contact info, portfolio sites, etc
should go in here. should go in here.
""" """
id = pw.AutoField() id = pw.AutoField()
user = pw.ForeignKeyField(User) user = pw.ForeignKeyField(User)
create_date = pw.DateTimeField(default=datetime.now) create_date = pw.DateTimeField(default=datetime.utcnow)
wallet_address = pw.CharField(null=True)
website = pw.CharField(null=True) website = pw.CharField(null=True)
twitter_handle = pw.CharField(null=True) twitter_handle = pw.CharField(null=True)
email = pw.CharField(unique=True, null=True) bio = pw.CharField(null=True)
bio = pw.CharField()
verified = pw.CharField(default=False) verified = pw.CharField(default=False)
class Meta: class Meta:
database = db database = db
class BackerProfile(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.
"""
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)
class Meta:
database = db
class SubscriptionMeta(pw.Model): class SubscriptionMeta(pw.Model):
""" """
SubscriptionMeta model is for the Creator to define details about SubscriptionMeta model is for the Creator to define details about
@ -90,8 +98,8 @@ class SubscriptionMeta(pw.Model):
existing subscription (by loading it on screen) will be grandfathered in. existing subscription (by loading it on screen) will be grandfathered in.
""" """
id = pw.AutoField() id = pw.AutoField()
create_date = pw.DateTimeField(default=datetime.now) user = pw.ForeignKeyField(User)
creator = pw.ForeignKeyField(CreatorProfile) create_date = pw.DateTimeField(default=datetime.utcnow)
atomic_xmr = pw.BigIntegerField() atomic_xmr = pw.BigIntegerField()
number_hours = pw.IntegerField() number_hours = pw.IntegerField()
wallet_address = pw.CharField() wallet_address = pw.CharField()
@ -106,33 +114,36 @@ class SubscriptionMeta(pw.Model):
class Subscription(pw.Model): class Subscription(pw.Model):
""" """
Subscription model gets created when backers can confirm payment via Subscription model gets created when users/backers can confirm payment via
the `check_tx_key` RPC method. Once a subscription is in place and is the `check_tx_key` RPC method to the creator's wallet. Once a subscription
associated with a user, that user is then elligible to view that is in place and is associated with a user, that user is then elligible to
creator's content. view that creator's content until the subscription expires (number_hours).
""" """
id = pw.AutoField() id = pw.AutoField()
subscribe_date = pw.DateTimeField(default=datetime.now) subscribe_date = pw.DateTimeField(default=datetime.utcnow)
active = pw.BooleanField(default=True) active = pw.BooleanField(default=True)
creator = pw.ForeignKeyField(CreatorProfile) creator = pw.ForeignKeyField(User)
backer = pw.ForeignKeyField(BackerProfile) backer = pw.ForeignKeyField(User)
meta = pw.ForeignKeyField(SubscriptionMeta) meta = pw.ForeignKeyField(SubscriptionMeta)
class Meta: class Meta:
database = db database = db
class TextPost(pw.Model):
class Content(pw.Model):
""" """
TextPost model is the first content type available to post. Metadata Content model represents any uploaded content from a creator which is only
here is basic for now, let's proof out the other components first. viewable by backers with an active subscription.
""" """
id = pw.AutoField() id = pw.AutoField()
post_date = pw.DateTimeField(default=datetime.now) post_date = pw.DateTimeField(default=datetime.utcnow)
hidden = pw.BooleanField(default=False) hidden = pw.BooleanField(default=False)
content = pw.TextField() content = pw.TextField()
title = pw.CharField() title = pw.CharField()
last_edit_date = pw.DateTimeField(default=datetime.now) last_edit_date = pw.DateTimeField(default=datetime.utcnow)
creator = pw.ForeignKeyField(CreatorProfile) creator = pw.ForeignKeyField(User)
type: List[ContentType] = EnumIntField(enum_class=ContentType, default=ContentType.text)
class Meta: class Meta:
database = db database = db

@ -2,8 +2,8 @@ from quart import Blueprint, render_template
from quart import flash, redirect, url_for from quart import flash, redirect, url_for
from flask_login import login_user, logout_user, current_user from flask_login import login_user, logout_user, current_user
from xmrbackers.factory import bcrypt from xmrbackers.forms import UserLogin, UserRegistration, UserChallenge
from xmrbackers.forms import UserAuth from xmrbackers.helpers import make_wallet_rpc
from xmrbackers.models import User from xmrbackers.models import User
@ -11,60 +11,86 @@ bp = Blueprint('auth', 'auth')
@bp.route("/register", methods=["GET", "POST"]) @bp.route("/register", methods=["GET", "POST"])
async def register(): async def register():
form = UserAuth() form = UserRegistration()
if current_user.is_authenticated: if current_user.is_authenticated:
await flash('Already registered and authenticated.') await flash('Already registered and authenticated.')
return redirect(url_for('meta.index')) return redirect(url_for('main.index'))
if form.validate_on_submit(): if form.validate_on_submit():
# Check if username already exists # Check if handle already exists
user = User.select().where( user = User.select().where(
User.username == form.username.data User.handle == form.handle.data
).first() ).first()
if user: if user:
await flash('This username is already registered.') await flash('This handle is already registered.')
return redirect(url_for('auth.login')) return redirect(url_for('auth.login'))
# Save new user # Save new user
user = User( user = User(
username=form.username.data, handle=form.handle.data,
password=bcrypt.generate_password_hash(form.password.data).decode('utf8'), wallet_address=form.wallet_address.data,
) )
user.save() user.save()
login_user(user) login_user(user)
return redirect(url_for('meta.index')) return redirect(url_for('main.index'))
return await render_template("auth/register.html", form=form) return await render_template("auth/register.html", form=form)
@bp.route("/login", methods=["GET", "POST"]) @bp.route("/login", methods=["GET", "POST"])
async def login(): async def login():
form = UserAuth() form = UserLogin()
if current_user.is_authenticated: if current_user.is_authenticated:
await flash('Already logged in.') await flash('Already logged in.')
return redirect(url_for('meta.index')) return redirect(url_for('main.index'))
if form.validate_on_submit(): if form.validate_on_submit():
# Check if user doesn't exist # Check if user doesn't exist
user = User.select().where( user = User.select().where(
User.username == form.username.data User.handle == form.handle.data
).first() ).first()
if not user: 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.login'))
return redirect(url_for('auth.challenge', handle=user.handle))
# Check if password is correct return await render_template("auth/login.html", form=form)
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'))
login_user(user) @bp.route("/login/challenge/<handle>", methods=["GET", "POST"])
return redirect(url_for('meta.index')) 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
}
res = make_wallet_rpc('verify', data)
print(res)
from quart import jsonify
return jsonify(res)
# # Check if user doesn't exist
# user = User.select().where(
# User.handle == form.handle.data
# ).first()
# if not user:
# await flash('That handle does not exist.')
# return redirect(url_for('auth.login'))
return redirect(url_for('main.index'))
return await render_template(
'auth/challenge.html',
user=user,
form=form
)
@bp.route("/logout") @bp.route("/logout")
async def logout(): async def logout():
@ -72,7 +98,7 @@ async def logout():
logout_user() logout_user()
else: else:
await flash('Not authenticated!') await flash('Not authenticated!')
return redirect(url_for('meta.index')) return redirect(url_for('main.index'))
# @auth_bp.route("/reset/<string:hash>", methods=["GET", "POST"]) # @auth_bp.route("/reset/<string:hash>", methods=["GET", "POST"])
# def reset(hash): # def reset(hash):

@ -3,8 +3,8 @@ from flask_login import current_user, login_required
from monero.wallet import Wallet from monero.wallet import Wallet
from xmrbackers.forms import ConfirmSubscription from xmrbackers.forms import ConfirmSubscription
from xmrbackers.models import User, CreatorProfile, BackerProfile, TextPost from xmrbackers.models import User, Profile, Content
from xmrbackers.models import Subscription, SubscriptionMeta from xmrbackers.models import Subscription, SubscriptionMeta, UserRole
from xmrbackers.helpers import check_tx_key from xmrbackers.helpers import check_tx_key
from xmrbackers import config from xmrbackers import config
@ -13,22 +13,21 @@ bp = Blueprint('creator', 'creator')
@bp.route('/creators') @bp.route('/creators')
async def all(): async def all():
creators = CreatorProfile.select().order_by( creators = User.select().join(Profile).where(
CreatorProfile.create_date.desc() User.roles.contains_any(UserRole.creator)
).order_by(
Profile.create_date.desc()
) )
return await render_template('creator/creators.html', creators=creators) return await render_template('creator/creators.html', creators=creators)
@bp.route('/creator/<username>') @bp.route('/creator/<handle>')
async def show(username): async def show(handle):
user = User.select().where(User.username == username) creator = User.select().where(User.handle == handle, User.roles.contains_any(UserRole.creator))
creator = CreatorProfile.select().where(
CreatorProfile.user == user
).first()
if creator: if creator:
posts = TextPost.select().where( posts = Content.select().where(
TextPost.creator == creator, Content.creator == creator,
TextPost.hidden == False Content.hidden == False
).order_by(TextPost.post_date.desc()) ).order_by(Content.post_date.desc())
return await render_template( return await render_template(
'creator/creator.html', 'creator/creator.html',
creator=creator, creator=creator,
@ -38,63 +37,63 @@ async def show(username):
await flash('That creator does not exist.') await flash('That creator does not exist.')
return redirect(url_for('meta.index')) return redirect(url_for('meta.index'))
@bp.route('/creator/<username>/subscription') # @bp.route('/creator/<username>/subscription')
async def subscription(username): # async def subscription(username):
user = User.select().where(User.username == username) # user = User.select().where(User.username == username)
creator = CreatorProfile.select().where( # creator = CreatorProfile.select().where(
CreatorProfile.user == user # CreatorProfile.user == user
) # )
if creator: # if creator:
subscription_meta = SubscriptionMeta.select().where( # subscription_meta = SubscriptionMeta.select().where(
SubscriptionMeta.creator == creator # SubscriptionMeta.creator == creator
).order_by(SubscriptionMeta.create_date.desc()).first() # ).order_by(SubscriptionMeta.create_date.desc()).first()
form = ConfirmSubscription() # form = ConfirmSubscription()
return await render_template( # return await render_template(
'creator/subscription.html', # 'creator/subscription.html',
subscription_meta=subscription_meta, # subscription_meta=subscription_meta,
form=form # form=form
) # )
else: # else:
await flash('That creator does not exist.') # await flash('That creator does not exist.')
return redirect(url_for('meta.index')) # return redirect(url_for('meta.index'))
@bp.route('/subscription/<int:subscription_id>/confirm', methods=['POST']) # @bp.route('/subscription/<int:subscription_id>/confirm', methods=['POST'])
async def confirm_subscription(subscription_id): # async def confirm_subscription(subscription_id):
# do checks here for SubscriptionMeta assumption # # do checks here for SubscriptionMeta assumption
sm = SubscriptionMeta.get_or_none(subscription_id) # sm = SubscriptionMeta.get_or_none(subscription_id)
form = ConfirmSubscription() # form = ConfirmSubscription()
if form.validate_on_submit(): # if form.validate_on_submit():
w = Wallet( # w = Wallet(
port=8000, # port=8000,
user=config.XMR_WALLET_RPC_USER, # user=config.XMR_WALLET_RPC_USER,
password=config.XMR_WALLET_RPC_PASS # password=config.XMR_WALLET_RPC_PASS
) # )
check_data = { # check_data = {
'txid': form.tx_id.data, # 'txid': form.tx_id.data,
'tx_key': form.tx_key.data, # 'tx_key': form.tx_key.data,
'address': form.wallet_address.data # 'address': form.wallet_address.data
} # }
try: # try:
res = w._backend.raw_request('check_tx_key', check_data) # res = w._backend.raw_request('check_tx_key', check_data)
except: # except:
await flash(f'Invalid transaction! No subscription for you!') # await flash(f'Invalid transaction! No subscription for you!')
return redirect(url_for('creator.show', username=sm.creator.user.username)) # return redirect(url_for('creator.show', username=sm.creator.user.username))
if res['received'] >= sm.atomic_xmr: # if res['received'] >= sm.atomic_xmr:
backer_profile = BackerProfile.select().where( # backer_profile = BackerProfile.select().where(
BackerProfile.user == current_user # BackerProfile.user == current_user
).first() # ).first()
s = Subscription( # s = Subscription(
creator=sm.creator.id, # creator=sm.creator.id,
backer=backer_profile.id, # backer=backer_profile.id,
meta=sm.id, # meta=sm.id,
) # )
s.save() # s.save()
await flash(f'Found valid transaction! You are now subscribed to {sm.creator.user.username}!') # 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)) # return redirect(url_for('creator.show', username=sm.creator.user.username))
else: # else:
await flash('Not enough XMR sent! No subscription for you!') # await flash('Not enough XMR sent! No subscription for you!')
return redirect(url_for('creator.show', username=sm.creator.user.username)) # return redirect(url_for('creator.show', username=sm.creator.user.username))
else: # else:
await flash('Unable to accept form POST.') # await flash('Unable to accept form POST.')
return redirect(url_for('meta.index')) # return redirect(url_for('meta.index'))

@ -0,0 +1,33 @@
from quart import Blueprint, render_template
from flask_login import current_user
from xmrbackers.models import User, Profile, Subscription, Content, UserRole
bp = Blueprint('main', 'main')
@bp.route('/')
async def index():
feed = None
if current_user.is_authenticated:
new_creators = User.select().where(
User.roles.contains_any(UserRole.creator)
).order_by(User.register_date.desc()).execute()
active_subscriptions = Subscription.select().where(
Subscription.active == True,
Subscription.backer == current_user
).order_by(Subscription.subscribe_date.desc()).execute()
new_posts = Content.select().where(
Content.hidden == False,
Content.creator in [c.creator for c in active_subscriptions]
).order_by(Content.post_date.desc()).execute()
feed = {
'new_creators': new_creators,
'new_posts': new_posts,
'active_subscriptions': active_subscriptions
}
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 quart import Blueprint, render_template, flash, redirect, url_for
from flask_login import current_user from flask_login import current_user
from xmrbackers.models import TextPost, Subscription from xmrbackers.models import Content, Subscription
bp = Blueprint('post', 'post') bp = Blueprint('post', 'post')
@bp.route('/post/<int:post_id>') # @bp.route('/post/<int:post_id>')
async def show(post_id): # async def show(post_id):
post = TextPost.get_or_none(post_id) # post = TextPost.get_or_none(post_id)
if post: # if post:
if current_user.is_anonymous: # if current_user.is_anonymous:
await flash('You must login to view this post.') # await flash('You must login to view this post.')
return redirect(url_for('creator.show', username=post.creator.user.username)) # return redirect(url_for('creator.show', username=post.creator.user.username))
user_subscriptions = Subscription.select().where( # user_subscriptions = Subscription.select().where(
Subscription.active == True, # Subscription.active == True,
Subscription.backer == current_user.backer_profile.first() # Subscription.backer == current_user.backer_profile.first()
) # )
if user_subscriptions: # if user_subscriptions:
return await render_template('post/show.html', post=post) # return await render_template('post/show.html', post=post)
else: # else:
await flash('Viewing posts requires a subscription.') # await flash('Viewing posts requires a subscription.')
return redirect(url_for('creator.subscription', username=post.creator.user.username)) # return redirect(url_for('creator.subscription', username=post.creator.user.username))
else: # else:
flash('That post does not exist.') # flash('That post does not exist.')
return redirect(url_for('meta.index')) # return redirect(url_for('meta.index'))

@ -0,0 +1,49 @@
<!DOCTYPE HTML>
<html>
{% include 'includes/head.html' %}
<body class="is-preload landing">
<div id="page-wrapper">
{% include 'includes/header.html' %}
<section id="banner">
<div class="content">
<header>
<h2>Challenge</h2>
<p>Handle: {{ user.handle }}</p>
<p>Challenge: {{ user.challenge }}</p>
<p>Wallet Address: {{ user.wallet_address }}</p>
<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="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>

@ -9,9 +9,9 @@
</nav> </nav>
<p>Authenticated: {{ current_user.is_authenticated }}</p> <p>Authenticated: {{ current_user.is_authenticated }}</p>
{% if current_user.is_authenticated %} {% if current_user.is_authenticated %}
<p>Username: {{ current_user.username }}</p> <p>Username: {{ current_user.handle }}</p>
<p>Email: {{ current_user.email }}</p> <p>Email: {{ current_user.email }}</p>
<p>Password: {{ current_user.password }}</p> <p>Wallet Address: {{ current_user.wallet_address }}</p>
{% endif %} {% endif %}
</header> </header>

@ -9,14 +9,24 @@
{% include 'includes/header.html' %} {% include 'includes/header.html' %}
{% if feed %} {% if feed %}
{% for k in feed %} {% if feed['new_creators'] %}
<h2>{{ k }}</h2> <h2>New Creators</h2>
{% if k == 'new_creators' %} {% for c in feed['new_creators'] %}
<p><a href="{{ url_for('creator.show', username=feed[k]) }}">{{ feed[k] }}</a></p> <p><a href="{{ url_for('creator.show', handle=c.handle) }}">{{ c.handle }}</a></p>
{% else %} {% endfor %}
<p>{{ feed[k]}}</p> {% endif %}
{% endif %} {% if feed['new_posts'] %}
{% endfor %} <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 %}
{% else %} {% else %}
<section id="banner"> <section id="banner">

Loading…
Cancel
Save