diff --git a/requirements.txt b/requirements.txt index 6f3758e..c6b34de 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,6 @@ Flask-WTF flask-login +flask-bcrypt hypercorn Pillow psycopg2-binary diff --git a/xmrbackers/factory.py b/xmrbackers/factory.py index 8e9660a..9898119 100644 --- a/xmrbackers/factory.py +++ b/xmrbackers/factory.py @@ -1,5 +1,7 @@ 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 @@ -15,12 +17,27 @@ async def _setup_db(app: Quart): def create_app(): app = Quart(__name__) app.config.from_envvar('QUART_SECRETS') + app = cli(app) @app.before_serving async def startup(): - from xmrbackers.routes import meta, api + from xmrbackers.routes import meta, api, auth from xmrbackers import filters await _setup_db(app) app.register_blueprint(meta.bp) app.register_blueprint(api.bp) + app.register_blueprint(auth.bp) app.register_blueprint(filters.bp) - return cli(app) + login_manager = LoginManager(app) + + # Login manager + login_manager.login_view = 'auth.login' + login_manager.logout_view = 'auth.logout' + + @login_manager.user_loader + def load_user(user_id): + from xmrbackers.models import User + user = User.query.get(user_id) + return user.id + return app + +bcrypt = Bcrypt(create_app()) diff --git a/xmrbackers/forms.py b/xmrbackers/forms.py index 8849566..657e331 100644 --- a/xmrbackers/forms.py +++ b/xmrbackers/forms.py @@ -1,8 +1,15 @@ +import quart.flask_patch from flask_wtf import FlaskForm from wtforms import StringField, BooleanField from wtforms.validators import DataRequired class Login(FlaskForm): + username = StringField('Username:', validators=[DataRequired()], render_kw={"placeholder": "Username", "class": "form-control", "type": "text"}) + email = StringField('Email Address:', validators=[DataRequired()], render_kw={"placeholder": "Email", "class": "form-control", "type": "email"}) + password = StringField('Password:', validators=[DataRequired()], render_kw={"placeholder": "Password", "class": "form-control", "type": "password"}) + +class Register(FlaskForm): + username = StringField('Username:', validators=[DataRequired()], render_kw={"placeholder": "Username", "class": "form-control", "type": "text"}) email = StringField('Email Address:', validators=[DataRequired()], render_kw={"placeholder": "Email", "class": "form-control", "type": "email"}) password = StringField('Password:', validators=[DataRequired()], render_kw={"placeholder": "Password", "class": "form-control", "type": "password"}) diff --git a/xmrbackers/models.py b/xmrbackers/models.py index 05913e7..43b52f9 100644 --- a/xmrbackers/models.py +++ b/xmrbackers/models.py @@ -12,29 +12,52 @@ db = pw.PostgresqlDatabase( host=config.DB_HOST, ) - -class Creator(pw.Model): +class User(pw.Model): id = pw.AutoField() register_date = pw.DateTimeField(default=datetime.now) last_login_date = pw.DateTimeField(default=datetime.now) - wallet_address = pw.CharField() username = pw.CharField(unique=True) email = pw.CharField(unique=True) password = pw.CharField(unique=True) + + @property + def is_authenticated(self): + return True + + @property + def is_active(self): + return True + + @property + def is_anonymous(self): + return False + + @property + def is_admin(self): + return self.admin + + def get_id(self): + return self.id + + class Meta: + database = db + +class CreatorProfile(pw.Model): + id = pw.AutoField() + user = pw.ForeignKeyField(User) + create_date = pw.DateTimeField(default=datetime.now) + last_login_date = pw.DateTimeField(default=datetime.now) + wallet_address = pw.CharField(null=True) bio = pw.CharField() class Meta: database = db -class Backer(pw.Model): +class BackerProfile(pw.Model): id = pw.AutoField() register_date = pw.DateTimeField(default=datetime.now) last_login_date = pw.DateTimeField(default=datetime.now) - wallet_address = pw.CharField() - username = pw.CharField(unique=True) - email = pw.CharField(unique=True) - password = pw.CharField(unique=True) class Meta: database = db @@ -43,7 +66,7 @@ class Backer(pw.Model): class SubscriptionMeta(pw.Model): id = pw.AutoField() create_date = pw.DateTimeField(default=datetime.now) - creator = pw.ForeignKeyField(Creator) + creator = pw.ForeignKeyField(CreatorProfile) atomic_xmr = pw.BigIntegerField() number_hours = pw.IntegerField() @@ -59,8 +82,8 @@ class Subscription(pw.Model): id = pw.AutoField() subscribe_date = pw.DateTimeField(default=datetime.now) active = pw.BooleanField(default=True) - creator = pw.ForeignKeyField(Creator) - backer = pw.ForeignKeyField(Backer) + creator = pw.ForeignKeyField(CreatorProfile) + backer = pw.ForeignKeyField(BackerProfile) meta = pw.ForeignKeyField(SubscriptionMeta) xmr_address = pw.CharField(unique=True) xmr_acct_idx = pw.BigIntegerField(unique=True) # in case it gets many subscribers @@ -76,7 +99,7 @@ class TextPost(pw.Model): content = pw.TextField() title = pw.CharField() last_edit_date = pw.DateTimeField(default=datetime.now) - creator = pw.ForeignKeyField(Creator) + creator = pw.ForeignKeyField(CreatorProfile) class Meta: database = db diff --git a/xmrbackers/routes/auth.py b/xmrbackers/routes/auth.py new file mode 100644 index 0000000..e071ab9 --- /dev/null +++ b/xmrbackers/routes/auth.py @@ -0,0 +1,120 @@ +import quart.flask_patch +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 Register +from xmrbackers.models import User + + +bp = Blueprint('auth', 'auth') + +@bp.route("/register", methods=["GET", "POST"]) +async def register(): + form = Register() + # if current_user.is_authenticated: + # flash('Already registered and authenticated.') + # return redirect(url_for('meta.index')) + # return 'gotem' + if form.validate_on_submit(): + # Check if email already exists + user = User.query.filter_by(email=form.email.data).first() + if user: + flash('This email is already registered.') + # return redirect(url_for('auth.login')) + return 'gotem' + + # Save new user + user = User( + email=form.email.data, + username=form.username.data, + password=bcrypt.generate_password_hash(form.password.data).decode('utf8'), + ) + user.save() + login_user(user) + return redirect(url_for('meta.index')) + return await render_template("auth/register.html", form=form) +# +# @auth_bp.route("/login", methods=["GET", "POST"]) +# def login(): +# form = Login() +# if current_user.is_authenticated: +# flash('Already registered and authenticated.') +# return redirect(url_for('wallet.dashboard')) +# +# if form.validate_on_submit(): +# # Check if user doesn't exist +# user = User.query.filter_by(email=form.email.data).first() +# if not user: +# flash('Invalid username or password.') +# return redirect(url_for('auth.login')) +# +# # Check if password is correct +# password_matches = bcrypt.check_password_hash( +# user.password, +# form.password.data +# ) +# if not password_matches: +# flash('Invalid username or password.') +# return redirect(url_for('auth.login')) +# +# # Capture event, login user, and redirect to wallet page +# capture_event(user.id, 'login') +# login_user(user) +# return redirect(url_for('wallet.dashboard')) +# +# return render_template("auth/login.html", form=form) +# +# @auth_bp.route("/logout") +# def logout(): +# if current_user.is_authenticated: +# docker.stop_container(current_user.wallet_container) +# capture_event(current_user.id, 'stop_container') +# current_user.clear_wallet_data() +# capture_event(current_user.id, 'logout') +# logout_user() +# return redirect(url_for('meta.index')) +# +# @auth_bp.route("/delete", methods=["GET", "POST"]) +# @login_required +# def delete(): +# form = Delete() +# if form.validate_on_submit(): +# docker.stop_container(current_user.wallet_container) +# capture_event(current_user.id, 'stop_container') +# sleep(1) +# docker.delete_wallet_data(current_user.id) +# capture_event(current_user.id, 'delete_wallet') +# current_user.clear_wallet_data(reset_password=True, reset_wallet=True) +# flash('Successfully deleted wallet data') +# return redirect(url_for('wallet.setup')) +# else: +# flash('Please confirm deletion of the account') +# return redirect(url_for('wallet.dashboard')) +# +# @auth_bp.route("/reset/", 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) diff --git a/xmrbackers/templates/auth/register.html b/xmrbackers/templates/auth/register.html new file mode 100644 index 0000000..7a4495d --- /dev/null +++ b/xmrbackers/templates/auth/register.html @@ -0,0 +1,32 @@ + + + + {% include 'includes/head.html' %} + + +
+ + + + {% include 'includes/footer.html' %} + +
+ + {% include 'includes/scripts.html' %} + + + diff --git a/xmrbackers/templates/index.html b/xmrbackers/templates/index.html index a5fcffb..c5ebb4b 100644 --- a/xmrbackers/templates/index.html +++ b/xmrbackers/templates/index.html @@ -10,8 +10,7 @@

MyThing sample app