flask site buildout #2
@ -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))
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -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)])
|
||||
|
@ -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__))
|
||||
|
@ -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'))
|
||||
|
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
|
||||
|
||||
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
|
||||
|
13
compose.yaml
13
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:
|
||||
|
Loading…
Reference in New Issue
Block a user