diff --git a/Dockerfile-monero b/Dockerfile-monero index 1d5a7d0..d61ada7 100644 --- a/Dockerfile-monero +++ b/Dockerfile-monero @@ -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 diff --git a/app.py b/app.py index 57897b8..ead8c71 100644 --- a/app.py +++ b/app.py @@ -3,5 +3,6 @@ from xmrbackers.factory import create_app app = create_app() + if __name__ == '__main__': - app.run() + app.run(debug=True, use_reloader=True) diff --git a/bin/run_wallet.sh b/bin/run_wallet.sh index fa6b6dc..8fbb371 100644 --- a/bin/run_wallet.sh +++ b/bin/run_wallet.sh @@ -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} \ diff --git a/docker-compose.yaml b/docker-compose.yaml index 2e6b6eb..97a3afe 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -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}" diff --git a/env-example b/env-example index 5f9282d..f63caf8 100644 --- a/env-example +++ b/env-example @@ -7,7 +7,7 @@ XMR_WALLET_PATH=/data/xmr-wallet XMR_WALLET_PASS=xxxxxxxxxxxxxxxxxxx XMR_WALLET_RPC_USER=xxxxxxxxxx XMR_WALLET_RPC_PASS=xxxxxxxxxxxxxxxxxxx -XMR_WALLET_RPC_ENDPOINT=http://localhost:9090 +XMR_WALLET_RPC_PORT=8000 XMR_DAEMON_URI=http://super.fast.node.xmr.pm:38089 XMR_WALLET_NETWORK=stagenet diff --git a/requirements.txt b/requirements.txt index d462646..b84c968 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,7 +11,7 @@ peewee requests SQLAlchemy WTForms -quart +quart==0.17.0 monero arrow flake8 diff --git a/xmrbackers/__init__.py b/xmrbackers/__init__.py index e69de29..6dcecf6 100644 --- a/xmrbackers/__init__.py +++ b/xmrbackers/__init__.py @@ -0,0 +1 @@ +from xmrbackers.factory import create_app \ No newline at end of file diff --git a/xmrbackers/config.py b/xmrbackers/config.py index 1cb9df0..2cfb37f 100644 --- a/xmrbackers/config.py +++ b/xmrbackers/config.py @@ -16,8 +16,10 @@ SERVER_NAME = getenv('SERVER_NAME', 'localhost:5000') 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_WALLET_RPC_PORT = getenv('XMR_WALLET_RPC_PORT', 8000) XMR_DAEMON_URI = getenv('XMR_DAEMON_URI') +XMR_WALLET_NETWORK = getenv('XMR_WALLET_NETWORK') + # Database DB_HOST = getenv('DB_HOST', 'localhost') @@ -31,7 +33,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 diff --git a/xmrbackers/factory.py b/xmrbackers/factory.py index ef9c35e..c0bbbbf 100644 --- a/xmrbackers/factory.py +++ b/xmrbackers/factory.py @@ -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()) diff --git a/xmrbackers/forms.py b/xmrbackers/forms.py index 7db15cb..01e51a4 100644 --- a/xmrbackers/forms.py +++ b/xmrbackers/forms.py @@ -1,12 +1,36 @@ -# import quart.flask_patch from flask_wtf import FlaskForm 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): tx_id = StringField('TX ID:', validators=[DataRequired()], render_kw={"placeholder": "TX ID", "class": "form-control", "type": "text"}) diff --git a/xmrbackers/helpers.py b/xmrbackers/helpers.py index 8047b55..6b4cbb6 100644 --- a/xmrbackers/helpers.py +++ b/xmrbackers/helpers.py @@ -1,8 +1,11 @@ +import peewee as pw +import playhouse.postgres_ext as pwpg from monero.wallet import Wallet -from monero.exceptions import WrongAddress +# from monero.exceptions import WrongAddress from xmrbackers import config + def check_tx_key(tx_id, tx_key, wallet_address): try: check_data = { @@ -10,8 +13,84 @@ def check_tx_key(tx_id, tx_key, wallet_address): 'tx_key': tx_key, '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) return res except: 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) \ No newline at end of file diff --git a/xmrbackers/models.py b/xmrbackers/models.py index f782b01..4af50e0 100644 --- a/xmrbackers/models.py +++ b/xmrbackers/models.py @@ -1,8 +1,12 @@ from datetime import datetime +from enum import IntEnum, unique +from typing import List +from secrets import token_urlsafe import peewee as pw from xmrbackers import config +from xmrbackers.helpers import EnumArrayField, EnumIntField db = pw.PostgresqlDatabase( @@ -12,16 +16,33 @@ db = pw.PostgresqlDatabase( 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): """ - 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=token_urlsafe) + roles: List[UserRole] = EnumArrayField(enum_class=UserRole, field_class=pw.IntegerField, default=[UserRole.backer]) @property def is_authenticated(self): @@ -37,51 +58,38 @@ 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 generate_challenge(self): + self.challenge = token_urlsafe(24) + self.save() + return self.challenge + 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): - """ - 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): """ 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. """ 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() @@ -106,33 +114,36 @@ 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) + subscribe_date = pw.DateTimeField(default=datetime.utcnow) active = pw.BooleanField(default=True) - creator = pw.ForeignKeyField(CreatorProfile) - backer = pw.ForeignKeyField(BackerProfile) + creator = pw.ForeignKeyField(User) + backer = pw.ForeignKeyField(User) meta = pw.ForeignKeyField(SubscriptionMeta) class Meta: database = db -class TextPost(pw.Model): + +class Content(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. + Content model represents any uploaded content from a creator 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[ContentType] = EnumIntField(enum_class=ContentType, default=ContentType.text) class Meta: database = db diff --git a/xmrbackers/routes/auth.py b/xmrbackers/routes/auth.py index 1729cdc..06ec32c 100644 --- a/xmrbackers/routes/auth.py +++ b/xmrbackers/routes/auth.py @@ -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,86 @@ 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/", 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 + } + 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") async def logout(): @@ -72,7 +98,7 @@ async def logout(): logout_user() else: await flash('Not authenticated!') - return redirect(url_for('meta.index')) + return redirect(url_for('main.index')) # @auth_bp.route("/reset/", methods=["GET", "POST"]) # def reset(hash): diff --git a/xmrbackers/routes/creator.py b/xmrbackers/routes/creator.py index e550363..92f328f 100644 --- a/xmrbackers/routes/creator.py +++ b/xmrbackers/routes/creator.py @@ -3,8 +3,8 @@ from flask_login import current_user, login_required from monero.wallet import Wallet from xmrbackers.forms import ConfirmSubscription -from xmrbackers.models import User, CreatorProfile, BackerProfile, TextPost -from xmrbackers.models import Subscription, SubscriptionMeta +from xmrbackers.models import User, Profile, Content +from xmrbackers.models import Subscription, SubscriptionMeta, UserRole from xmrbackers.helpers import check_tx_key from xmrbackers import config @@ -13,22 +13,21 @@ 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/') -async def show(username): - user = User.select().where(User.username == username) - creator = CreatorProfile.select().where( - CreatorProfile.user == user - ).first() +@bp.route('/creator/') +async def show(handle): + creator = User.select().where(User.handle == handle, User.roles.contains_any(UserRole.creator)) if creator: - posts = TextPost.select().where( - TextPost.creator == creator, - TextPost.hidden == False - ).order_by(TextPost.post_date.desc()) + posts = Content.select().where( + Content.creator == creator, + Content.hidden == False + ).order_by(Content.post_date.desc()) return await render_template( 'creator/creator.html', creator=creator, @@ -38,63 +37,63 @@ async def show(username): await flash('That creator does not exist.') return redirect(url_for('meta.index')) -@bp.route('/creator//subscription') -async def subscription(username): - user = User.select().where(User.username == username) - creator = CreatorProfile.select().where( - CreatorProfile.user == user - ) - 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('/creator//subscription') +# async def subscription(username): +# user = User.select().where(User.username == username) +# creator = CreatorProfile.select().where( +# CreatorProfile.user == user +# ) +# 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//confirm', methods=['POST']) -async def confirm_subscription(subscription_id): - # do checks here for SubscriptionMeta assumption - sm = SubscriptionMeta.get_or_none(subscription_id) - form = ConfirmSubscription() - 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)) +# @bp.route('/subscription//confirm', methods=['POST']) +# async def confirm_subscription(subscription_id): +# # do checks here for SubscriptionMeta assumption +# sm = SubscriptionMeta.get_or_none(subscription_id) +# form = ConfirmSubscription() +# 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')) +# 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')) diff --git a/xmrbackers/routes/main.py b/xmrbackers/routes/main.py new file mode 100644 index 0000000..8030998 --- /dev/null +++ b/xmrbackers/routes/main.py @@ -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 + ) + \ No newline at end of file diff --git a/xmrbackers/routes/meta.py b/xmrbackers/routes/meta.py deleted file mode 100644 index 2234251..0000000 --- a/xmrbackers/routes/meta.py +++ /dev/null @@ -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) diff --git a/xmrbackers/routes/post.py b/xmrbackers/routes/post.py index f0cd656..7531326 100644 --- a/xmrbackers/routes/post.py +++ b/xmrbackers/routes/post.py @@ -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/') -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/') +# 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')) diff --git a/xmrbackers/templates/auth/challenge.html b/xmrbackers/templates/auth/challenge.html new file mode 100644 index 0000000..aa1a11c --- /dev/null +++ b/xmrbackers/templates/auth/challenge.html @@ -0,0 +1,49 @@ + + + + {% include 'includes/head.html' %} + + +
+ + {% include 'includes/header.html' %} + + + + {% include 'includes/footer.html' %} + +
+ + {% include 'includes/scripts.html' %} + + + diff --git a/xmrbackers/templates/includes/header.html b/xmrbackers/templates/includes/header.html index b454fd6..975d980 100644 --- a/xmrbackers/templates/includes/header.html +++ b/xmrbackers/templates/includes/header.html @@ -9,9 +9,9 @@

Authenticated: {{ current_user.is_authenticated }}

{% if current_user.is_authenticated %} -

Username: {{ current_user.username }}

+

Username: {{ current_user.handle }}

Email: {{ current_user.email }}

-

Password: {{ current_user.password }}

+

Wallet Address: {{ current_user.wallet_address }}

{% endif %} diff --git a/xmrbackers/templates/index.html b/xmrbackers/templates/index.html index 9205c7c..083e0a8 100644 --- a/xmrbackers/templates/index.html +++ b/xmrbackers/templates/index.html @@ -9,14 +9,24 @@ {% include 'includes/header.html' %} {% if feed %} - {% for k in feed %} -

{{ k }}

- {% if k == 'new_creators' %} -

{{ feed[k] }}

- {% else %} -

{{ feed[k]}}

- {% endif %} - {% endfor %} + {% if feed['new_creators'] %} +

New Creators

+ {% for c in feed['new_creators'] %} +

{{ c.handle }}

+ {% endfor %} + {% endif %} + {% if feed['new_posts'] %} +

New Posts

+ {% for p in feed['new_posts'] %} +

{{ p.id }}

+ {% endfor %} + {% endif %} + {% if feed['active_subscriptions'] %} +

Active Subscriptions

+ {% for s in feed['active_subscriptions'] %} +

{{ s.id }}

+ {% endfor %} + {% endif %} {% else %}