flask site buildout #2

Merged
finn merged 25 commits from mgtut1 into master 2024-08-05 08:41:03 +00:00
9 changed files with 133 additions and 25 deletions
Showing only changes of commit 3d1f21ffcb - Show all commits

View File

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

View File

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

View File

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

View File

@ -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/<token>', 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/<username>')
@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'))

View File

@ -0,0 +1,10 @@
<!doctype html>
<html>
<body>
<p>User {{ user.username }} requested password reset.</p>
<p>Reset link:</p>
<p><a href="{{ hostname }}{{ url_for('reset_password', token=token) }}">click here</a>
<p>If you did not request this, ignore this message.</p>
</body>
</html>

View File

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

View File

@ -0,0 +1,26 @@
{% extends "base.html" %}
{% block content %}
<h1>Reset Your Password</h1>
<form action="" method="post">
{{ form.hidden_tag() }}
<p>
{{ form.password.label }}
{{ form.password(size=32) }}
{% for error in form.password.errors %}
<span style="color: red;">[{{ error }}]</span>
{% endfor %}
</p>
<p>
{{ form.password2.label }}
{{ form.password2(size=32) }}
{% for error in form.password2.errors %}
<span style="color: red;">[{{ error }}]</span>
{% endfor %}
</p>
<p>{{ form.submit() }}</p>
</form>
{% endblock %}

View File

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

View File

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