flask site buildout #2
@ -1,5 +1,6 @@
|
|||||||
|
from flask import render_template
|
||||||
from flask_mail import Message
|
from flask_mail import Message
|
||||||
from app import mail
|
from app import mail, app
|
||||||
|
|
||||||
def send_email(subject, sender, recipients, text_body, html_body):
|
def send_email(subject, sender, recipients, text_body, html_body):
|
||||||
msg = Message(subject, sender=sender, recipients=recipients)
|
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
|
msg.html = html_body
|
||||||
mail.send(msg)
|
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))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -17,17 +17,20 @@ class RegistrationForm(FlaskForm):
|
|||||||
password = PasswordField('Password', validators=[DataRequired()])
|
password = PasswordField('Password', validators=[DataRequired()])
|
||||||
password2 = PasswordField('Repeat Password', validators=[DataRequired()])
|
password2 = PasswordField('Repeat Password', validators=[DataRequired()])
|
||||||
submit = SubmitField('Register')
|
submit = SubmitField('Register')
|
||||||
|
|
||||||
def validate_username(self, username):
|
def validate_username(self, username):
|
||||||
user = db.session.scalar(sa.select(User).where(User.username == username.data))
|
user = db.session.scalar(sa.select(User).where(User.username == username.data))
|
||||||
if user is not None:
|
if user is not None:
|
||||||
raise ValidationError('Please use a different username.')
|
raise ValidationError('Please use a different username.')
|
||||||
|
|
||||||
def validate_email(self, email):
|
def validate_email(self, email):
|
||||||
user = db.session.scalar(sa.select(User).where(User.email == email.data))
|
user = db.session.scalar(sa.select(User).where(User.email == email.data))
|
||||||
if user is not None:
|
if user is not None:
|
||||||
raise ValidationError('Please use a different email address.')
|
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):
|
class EditProfileForm(FlaskForm):
|
||||||
username = StringField('Username', validators=[DataRequired()])
|
username = StringField('Username', validators=[DataRequired()])
|
||||||
about_me = TextAreaField('About me', validators=[Length(min=0, max=140)])
|
about_me = TextAreaField('About me', validators=[Length(min=0, max=140)])
|
||||||
|
@ -1,14 +1,17 @@
|
|||||||
import os
|
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
import sqlalchemy.orm as so
|
import sqlalchemy.orm as so
|
||||||
from werkzeug.security import generate_password_hash, check_password_hash
|
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
|
from flask_login import UserMixin
|
||||||
|
|
||||||
|
#debug
|
||||||
|
import sys
|
||||||
|
|
||||||
followers = sa.Table(
|
followers = sa.Table(
|
||||||
'followers',
|
'followers',
|
||||||
db.metadata,
|
db.metadata,
|
||||||
@ -39,15 +42,31 @@ class User(UserMixin, db.Model):
|
|||||||
self.password_hash = generate_password_hash(password)
|
self.password_hash = generate_password_hash(password)
|
||||||
def check_password(self, password):
|
def check_password(self, password):
|
||||||
return check_password_hash(self.password_hash, 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):
|
def gen_avatar(self, write_png=True):
|
||||||
foreground = [ "rgb(45,79,255)",
|
foreground = ['#ACE1AF',
|
||||||
"rgb(254,180,44)",
|
'#ACC4E1',
|
||||||
"rgb(226,121,234)",
|
'#E1ACDE',
|
||||||
"rgb(30,179,253)",
|
'#E1CAAC',
|
||||||
"rgb(232,77,65)",
|
'#AFFF00',
|
||||||
"rgb(49,203,115)",
|
'#00FFCF',
|
||||||
"rgb(141,69,170)" ]
|
'#5000FF',
|
||||||
background = "rgb(22,22,22)"
|
'#FF0030']
|
||||||
|
|
||||||
|
background = '#151515'
|
||||||
|
|
||||||
digest = hashlib.md5(self.email.lower().encode('utf-8')).hexdigest()
|
digest = hashlib.md5(self.email.lower().encode('utf-8')).hexdigest()
|
||||||
basedir = os.path.abspath(os.path.dirname(__file__))
|
basedir = os.path.abspath(os.path.dirname(__file__))
|
||||||
|
@ -5,9 +5,12 @@ from datetime import datetime, timezone
|
|||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
|
||||||
from app import app, db
|
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.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
|
@app.before_request
|
||||||
def before_request():
|
def before_request():
|
||||||
@ -84,6 +87,25 @@ def register():
|
|||||||
return redirect(url_for('login'))
|
return redirect(url_for('login'))
|
||||||
return render_template('register.html', title='Register', form=form)
|
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>')
|
@app.route('/user/<username>')
|
||||||
@login_required
|
@login_required
|
||||||
def user(username):
|
def user(username):
|
||||||
@ -153,11 +175,14 @@ def unfollow(username):
|
|||||||
@app.route('/reset_password_request', methods=['GET', 'POST'])
|
@app.route('/reset_password_request', methods=['GET', 'POST'])
|
||||||
def reset_password_request():
|
def reset_password_request():
|
||||||
if current_user.is_authenticated:
|
if current_user.is_authenticated:
|
||||||
|
print('rpr user is authed', file=sys.stderr)
|
||||||
return redirect(url_for('index'))
|
return redirect(url_for('index'))
|
||||||
form = ResetPasswordRequestForm()
|
form = ResetPasswordRequestForm()
|
||||||
if form.validate_on_submit():
|
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))
|
user = db.session.scalar(sa.select(User).where(User.email == form.email.data))
|
||||||
if user:
|
if user:
|
||||||
|
print('rpr if user', file=sys.stderr)
|
||||||
send_password_reset_email(user)
|
send_password_reset_email(user)
|
||||||
flash('Password reset sent.')
|
flash('Password reset sent.')
|
||||||
return redirect(url_for('login'))
|
return redirect(url_for('login'))
|
||||||
|
10
backend/app/templates/email/reset_password.html
Normal file
10
backend/app/templates/email/reset_password.html
Normal 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>
|
||||||
|
|
7
backend/app/templates/email/reset_password.txt
Normal file
7
backend/app/templates/email/reset_password.txt
Normal 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.
|
26
backend/app/templates/reset_password.html
Normal file
26
backend/app/templates/reset_password.html
Normal 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 %}
|
@ -4,9 +4,9 @@ basedir = os.path.abspath(os.path.dirname(__file__))
|
|||||||
# Remove or fallbacks for prod
|
# Remove or fallbacks for prod
|
||||||
|
|
||||||
class Config:
|
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 = '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 = 'pmb'
|
||||||
#MAIL_SERVER = ''
|
#MAIL_SERVER = ''
|
||||||
@ -14,8 +14,10 @@ class Config:
|
|||||||
MAIL_USE_TLS = False
|
MAIL_USE_TLS = False
|
||||||
MAIL_USERNAME = ''
|
MAIL_USERNAME = ''
|
||||||
MAIL_PASSWORD = ''
|
MAIL_PASSWORD = ''
|
||||||
ADMINS = [os.environ.get('ADMIN_EMAIL')]
|
ADMINS = [os.environ.get('DOTENV_ADMIN_EMAIL')]
|
||||||
FROM_ADDRESS = os.environ.get('FROM_ADDRESS')
|
FROM_ADDRESS = os.environ.get('DOTENV_FROM_ADDRESS')
|
||||||
|
REAL_HOSTNAME = os.environ.get('DOTENV_REAL_HOSTNAME')
|
||||||
|
|
||||||
|
|
||||||
DC_LOGGING = True
|
DC_LOGGING = True
|
||||||
POSTS_PER_PAGE=5
|
POSTS_PER_PAGE=5
|
||||||
|
13
compose.yaml
13
compose.yaml
@ -37,11 +37,14 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
- MYSQL_USER=flasku
|
- MYSQL_USER=flasku
|
||||||
#- MYSQL_PASSWORD=flaskp
|
#- MYSQL_PASSWORD=flaskp
|
||||||
- MYSQL_PASSWORD=${DOTENV_MYSQL_FLASK_PASSWORD}
|
- DOTENV_MYSQL_PASSWORD=${DOTENV_MYSQL_FLASK_PASSWORD}
|
||||||
- TOKEN_I=${DOTENV_TOKEN_I}
|
- DOTENV_FLASK_SECRET_KEY=${FLASK_SECRET_KEY}
|
||||||
- TOKEN_C=${DOTENV_TOKEN_C}
|
- DOTENV_TOKEN_I=${FLASK_TOKEN_I}
|
||||||
- ADMIN_EMAIL=${ADMIN_EMAIL}
|
- DOTENV_TOKEN_C=${FLASK_TOKEN_C}
|
||||||
- FROM_ADDRESS=${GITEA_MAIL_FROM}
|
- 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:
|
#ports:
|
||||||
# - 8000:8000
|
# - 8000:8000
|
||||||
expose:
|
expose:
|
||||||
|
Loading…
Reference in New Issue
Block a user