From 3d1f21ffcb879c34b3fc554c79c32b8d07b61ee5 Mon Sep 17 00:00:00 2001 From: finn Date: Mon, 5 Aug 2024 00:59:01 -0700 Subject: [PATCH] mgt c10 checkpoint with debug --- backend/app/email.py | 15 ++++++- backend/app/forms.py | 7 +++- backend/app/models.py | 41 ++++++++++++++----- backend/app/routes.py | 29 ++++++++++++- .../app/templates/email/reset_password.html | 10 +++++ .../app/templates/email/reset_password.txt | 7 ++++ backend/app/templates/reset_password.html | 26 ++++++++++++ backend/config.py | 10 +++-- compose.yaml | 13 +++--- 9 files changed, 133 insertions(+), 25 deletions(-) create mode 100644 backend/app/templates/email/reset_password.html create mode 100644 backend/app/templates/email/reset_password.txt create mode 100644 backend/app/templates/reset_password.html diff --git a/backend/app/email.py b/backend/app/email.py index 2adc7a8..cbf8f9e 100644 --- a/backend/app/email.py +++ b/backend/app/email.py @@ -1,5 +1,6 @@ +from flask import render_template from flask_mail import Message -from app import mail +from app import mail, app def send_email(subject, sender, recipients, text_body, html_body): msg = Message(subject, sender=sender, recipients=recipients) @@ -7,3 +8,15 @@ def send_email(subject, sender, recipients, text_body, html_body): msg.html = html_body mail.send(msg) +def send_password_reset_email(user): + token = user.get_reset_password_token() + hostname = app.config['REAL_HOSTNAME'] + send_email('[Blog] Reset Password', + sender=app.config['ADMINS'][0], + recipients=[user.email], + text_body=render_template('email/reset_password.txt', hostname=hostname, user=user, token=token), + html_body=render_template('email/reset_password.html', hostname=hostname, user=user, token=token)) + + + + diff --git a/backend/app/forms.py b/backend/app/forms.py index 25249ca..d9a5c14 100644 --- a/backend/app/forms.py +++ b/backend/app/forms.py @@ -17,17 +17,20 @@ class RegistrationForm(FlaskForm): password = PasswordField('Password', validators=[DataRequired()]) password2 = PasswordField('Repeat Password', validators=[DataRequired()]) submit = SubmitField('Register') - def validate_username(self, username): user = db.session.scalar(sa.select(User).where(User.username == username.data)) if user is not None: raise ValidationError('Please use a different username.') - def validate_email(self, email): user = db.session.scalar(sa.select(User).where(User.email == email.data)) if user is not None: raise ValidationError('Please use a different email address.') +class ResetPasswordForm(FlaskForm): + password = PasswordField('Password', validators=[DataRequired()]) + password2 = PasswordField('Repeat Password', validators=[DataRequired()]) + submit = SubmitField('Request Reset') + class EditProfileForm(FlaskForm): username = StringField('Username', validators=[DataRequired()]) about_me = TextAreaField('About me', validators=[Length(min=0, max=140)]) diff --git a/backend/app/models.py b/backend/app/models.py index 7131df2..dc71998 100644 --- a/backend/app/models.py +++ b/backend/app/models.py @@ -1,14 +1,17 @@ -import os from datetime import datetime, timezone from typing import Optional import sqlalchemy as sa import sqlalchemy.orm as so from werkzeug.security import generate_password_hash, check_password_hash -import pydenticon, hashlib, base64 +import os, pydenticon, hashlib, base64, jwt +from time import time -from app import db, login +from app import db, login, app from flask_login import UserMixin +#debug +import sys + followers = sa.Table( 'followers', db.metadata, @@ -39,15 +42,31 @@ class User(UserMixin, db.Model): self.password_hash = generate_password_hash(password) def check_password(self, password): return check_password_hash(self.password_hash, password) + def get_reset_password_token(self, expires_in=600): + token = jwt.encode({'reset_password': self.id, 'exp': time() + expires_in}, + app.config['SECRET_KEY'], algorithm='HS256') + return token + @staticmethod + def verify_reset_password_token(token): + try: + id = jwt.decode(token, + app.config['SECRET_KEY'], algorithms='HS256')['reset_password'] + + except: + return + return db.session.get(User, id) + def gen_avatar(self, write_png=True): - foreground = [ "rgb(45,79,255)", - "rgb(254,180,44)", - "rgb(226,121,234)", - "rgb(30,179,253)", - "rgb(232,77,65)", - "rgb(49,203,115)", - "rgb(141,69,170)" ] - background = "rgb(22,22,22)" + foreground = ['#ACE1AF', + '#ACC4E1', + '#E1ACDE', + '#E1CAAC', + '#AFFF00', + '#00FFCF', + '#5000FF', + '#FF0030'] + + background = '#151515' digest = hashlib.md5(self.email.lower().encode('utf-8')).hexdigest() basedir = os.path.abspath(os.path.dirname(__file__)) diff --git a/backend/app/routes.py b/backend/app/routes.py index 099877a..0b3f724 100644 --- a/backend/app/routes.py +++ b/backend/app/routes.py @@ -5,9 +5,12 @@ from datetime import datetime, timezone import sqlalchemy as sa from app import app, db -from app.forms import LoginForm, RegistrationForm, EditProfileForm, EmptyForm, PostForm, ResetPasswordRequestForm +from app.forms import LoginForm, RegistrationForm, EditProfileForm, EmptyForm, PostForm, ResetPasswordRequestForm, ResetPasswordForm from app.models import User, Post -#from app.email import send_password_reset_email +from app.email import send_password_reset_email + +#debug: +import sys @app.before_request def before_request(): @@ -84,6 +87,25 @@ def register(): return redirect(url_for('login')) return render_template('register.html', title='Register', form=form) +@app.route('/reset_password/', methods=['GET', 'POST']) +def reset_password(token): + if current_user.is_authenticated: + print('rp user is authed', file=sys.stderr) + return redirect(url_for('index')) + user = User.verify_reset_password_token(token) + if not user: + print('rp not user', file=sys.stderr) + return redirect(url_for('index')) + form = ResetPasswordForm() + if form.validate_on_submit(): + print('rp validated', file=sys.stderr) + user.set_password(form.password.data) + db.session.commit() + flash('Your password has been reset.') + return redirect(url_for('login')) + return render_template('reset_password.html', form=form) + + @app.route('/user/') @login_required def user(username): @@ -153,11 +175,14 @@ def unfollow(username): @app.route('/reset_password_request', methods=['GET', 'POST']) def reset_password_request(): if current_user.is_authenticated: + print('rpr user is authed', file=sys.stderr) return redirect(url_for('index')) form = ResetPasswordRequestForm() if form.validate_on_submit(): + print('rpr form validated', file=sys.stderr) user = db.session.scalar(sa.select(User).where(User.email == form.email.data)) if user: + print('rpr if user', file=sys.stderr) send_password_reset_email(user) flash('Password reset sent.') return redirect(url_for('login')) diff --git a/backend/app/templates/email/reset_password.html b/backend/app/templates/email/reset_password.html new file mode 100644 index 0000000..3f14636 --- /dev/null +++ b/backend/app/templates/email/reset_password.html @@ -0,0 +1,10 @@ + + + +

User {{ user.username }} requested password reset.

+

Reset link:

+

click here +

If you did not request this, ignore this message.

+ + + diff --git a/backend/app/templates/email/reset_password.txt b/backend/app/templates/email/reset_password.txt new file mode 100644 index 0000000..f6ec353 --- /dev/null +++ b/backend/app/templates/email/reset_password.txt @@ -0,0 +1,7 @@ +User {{ user.username }} requested password reset. + +Reset link follows. + +{{ hostname }}{{ url_for('reset_password', token=token) }} + +If you did not request this, ignore this message. diff --git a/backend/app/templates/reset_password.html b/backend/app/templates/reset_password.html new file mode 100644 index 0000000..50f8dba --- /dev/null +++ b/backend/app/templates/reset_password.html @@ -0,0 +1,26 @@ +{% extends "base.html" %} + +{% block content %} + +

Reset Your Password

+
+ {{ form.hidden_tag() }} +

+ {{ form.password.label }} + {{ form.password(size=32) }} + {% for error in form.password.errors %} + [{{ error }}] + {% endfor %} +

+

+ {{ form.password2.label }} + {{ form.password2(size=32) }} + {% for error in form.password2.errors %} + [{{ error }}] + {% endfor %} +

+

{{ form.submit() }}

+
+ + +{% endblock %} diff --git a/backend/config.py b/backend/config.py index cf211e7..ac82c1e 100644 --- a/backend/config.py +++ b/backend/config.py @@ -4,9 +4,9 @@ basedir = os.path.abspath(os.path.dirname(__file__)) # Remove or fallbacks for prod class Config: - SECRET_KEY = os.environ.get('FLASK_SECRET_KEY') or 'flasksk' + SECRET_KEY = os.environ.get('DOTENV_FLASK_SECRET_KEY') #SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'zapp.db') - SQLALCHEMY_DATABASE_URI = 'mariadb+mariadbconnector://flasku:' + os.environ.get('MYSQL_PASSWORD') + '@db:3306/flask' + SQLALCHEMY_DATABASE_URI = 'mariadb+mariadbconnector://flasku:' + os.environ.get('DOTENV_MYSQL_PASSWORD') + '@db:3306/flask' MAIL_SERVER = 'pmb' #MAIL_SERVER = '' @@ -14,8 +14,10 @@ class Config: MAIL_USE_TLS = False MAIL_USERNAME = '' MAIL_PASSWORD = '' - ADMINS = [os.environ.get('ADMIN_EMAIL')] - FROM_ADDRESS = os.environ.get('FROM_ADDRESS') + ADMINS = [os.environ.get('DOTENV_ADMIN_EMAIL')] + FROM_ADDRESS = os.environ.get('DOTENV_FROM_ADDRESS') + REAL_HOSTNAME = os.environ.get('DOTENV_REAL_HOSTNAME') + DC_LOGGING = True POSTS_PER_PAGE=5 diff --git a/compose.yaml b/compose.yaml index a4ffbc3..88cff10 100644 --- a/compose.yaml +++ b/compose.yaml @@ -37,11 +37,14 @@ services: environment: - MYSQL_USER=flasku #- MYSQL_PASSWORD=flaskp - - MYSQL_PASSWORD=${DOTENV_MYSQL_FLASK_PASSWORD} - - TOKEN_I=${DOTENV_TOKEN_I} - - TOKEN_C=${DOTENV_TOKEN_C} - - ADMIN_EMAIL=${ADMIN_EMAIL} - - FROM_ADDRESS=${GITEA_MAIL_FROM} + - DOTENV_MYSQL_PASSWORD=${DOTENV_MYSQL_FLASK_PASSWORD} + - DOTENV_FLASK_SECRET_KEY=${FLASK_SECRET_KEY} + - DOTENV_TOKEN_I=${FLASK_TOKEN_I} + - DOTENV_TOKEN_C=${FLASK_TOKEN_C} + - DOTENV_ADMIN_EMAIL=${FLASK_ADMIN_EMAIL} + - DOTENV_FROM_ADDRESS=${FLASK_MAIL_FROM} + - DOTENV_JWT_PHRASE=${FLASK_JWT_PHRASE} + - DOTENV_REAL_HOSTNAME=${FLASK_REAL_HOSTNAME} #ports: # - 8000:8000 expose: