Merge pull request #1 from Lunaresk/feature/S2K-11-passwort-recovery-erstellen

This commit is contained in:
Lunaresk 2022-11-14 15:47:36 +01:00 committed by GitHub
commit 8964a48011
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 164 additions and 3 deletions

View File

@ -3,6 +3,7 @@ from flask import Flask
from flask_bootstrap import Bootstrap
from flask_login import LoginManager
from flask_sqlalchemy import SQLAlchemy
from flask_mail import Mail
from flask_migrate import Migrate
from logging import getLogger
from logging.config import fileConfig
@ -28,6 +29,7 @@ bootstrap = Bootstrap()
db = SQLAlchemy()
login = LoginManager()
login.login_view = 'auth.web_login'
mail = Mail()
migrate = Migrate()
@ -37,6 +39,7 @@ def create_app(config_class=Config):
bootstrap.init_app(app)
db.init_app(app)
login.init_app(app)
mail.init_app(app)
migrate.init_app(app, db, render_as_batch=True)
from app.auth import bp as auth_bp

14
app/auth/email.py Normal file
View File

@ -0,0 +1,14 @@
from flask import current_app
from app.utils.email import send_email
from app.utils.routes_utils import render_custom_template as render_template
def send_password_reset_email(user):
token = user.get_reset_password_token()
send_email('[Scan2Kasse] Reset Your Password',
sender=current_app.config['ADMINS'][0],
recipients=[user.email],
text_body=render_template('email/reset_password.txt',
user=user, token=token),
html_body=render_template('email/reset_password.html',
user=user, token=token))

View File

@ -24,4 +24,14 @@ class RegistrationForm(FlaskForm):
def validate_email(self, email):
user = User.query.filter_by(email=email.data).first()
if user is not None:
raise ValidationError('Please use a different email address.')
raise ValidationError('Please use a different email address.')
class ResetPasswordRequestForm(FlaskForm):
email = StringField('Email', validators=[DataRequired(), Email()])
submit = SubmitField('Request Password Reset', render_kw={"class": "btn btn-primary mt-3"})
class ResetPasswordForm(FlaskForm):
password = PasswordField('Password', validators=[DataRequired()])
password2 = PasswordField(
'Repeat Password', validators=[DataRequired(), EqualTo('password')])
submit = SubmitField('Request Password Reset', render_kw={"class": "btn btn-primary mt-3"})

View File

@ -1,6 +1,7 @@
from app import db
from app.auth import bp
from app.auth.forms import LoginForm, RegistrationForm
from app.auth.email import send_password_reset_email
from app.auth.forms import LoginForm, RegistrationForm, ResetPasswordForm, ResetPasswordRequestForm
from app.models import User
from app.utils.routes_utils import render_custom_template as render_template
from flask import flash, redirect, request, url_for
@ -41,4 +42,33 @@ def web_login():
@bp.route(f'/logout')
def web_logout():
logout_user()
return redirect(url_for('main.index'))
return redirect(url_for('main.index'))
@bp.route('/reset_password_request', methods=['GET', 'POST'])
def reset_password_request():
if current_user.is_authenticated:
return redirect(url_for('index'))
form = ResetPasswordRequestForm()
if form.validate_on_submit():
user = User.query.filter_by(email=form.email.data).first()
if user:
send_password_reset_email(user)
flash('Check your email for the instructions to reset your password')
return redirect(url_for('auth.web_login'))
return render_template('auth/reset_password_request.html',
title='Reset Password', form=form)
@bp.route('/reset_password/<token>', methods=['GET', 'POST'])
def reset_password(token):
if current_user.is_authenticated:
return redirect(url_for('index'))
user = User.verify_reset_password_token(token)
if not user:
return redirect(url_for('index'))
form = ResetPasswordForm()
if form.validate_on_submit():
user.set_password(form.password.data)
db.session.commit()
flash('Your password has been reset.')
return redirect(url_for('auth.web_login'))
return render_template('auth/reset_password.html', form=form)

View File

@ -1,6 +1,9 @@
import jwt
from app import db, login
from datetime import date
from flask import current_app
from flask_login import UserMixin
from time import time
from werkzeug.security import generate_password_hash, check_password_hash
item_category = db.Table("item_category",
@ -23,6 +26,20 @@ class User(UserMixin, db.Model):
def check_password(self, password):
return check_password_hash(self.password_hash, password)
def get_reset_password_token(self, expires_in=600):
return jwt.encode(
{'reset_password': self.id, 'exp': time() + expires_in},
current_app.config['SECRET_KEY'], algorithm='HS256')
@staticmethod
def verify_reset_password_token(token):
try:
id = jwt.decode(token, current_app.config['SECRET_KEY'],
algorithms=['HS256'])['reset_password']
except:
return
return User.query.get(id)
def __repr__(self) -> str:
return f"<User {self.id} ({self.email})>"

View File

@ -16,4 +16,8 @@
</p>
{{ wtf.form_field(form.submit, class=form.submit.render_kw["class"]) }}
</form>
<p>
Forgot Your Password?
<a href="{{ url_for('auth.reset_password_request') }}">Click to Reset It</a>
</p>
{% endblock %}

View File

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

View File

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

View File

@ -0,0 +1,12 @@
<p>Dear User,</p>
<p>
To reset your password
<a href="{{ url_for('auth.reset_password', token=token, _external=True) }}">
click here
</a>.
</p>
<p>Alternatively, you can paste the following link in your browser's address bar:</p>
<p>{{ url_for('auth.reset_password', token=token, _external=True) }}</p>
<p>If you have not requested a password reset simply ignore this message.</p>
<p>Sincerely,</p>
<p>The Admins</p>

View File

@ -0,0 +1,11 @@
Dear User,
To reset your password click on the following link:
{{ url_for('auth.reset_password', token=token, _external=True) }}
If you have not requested a password reset simply ignore this message.
Sincerely,
The Admins

15
app/utils/email.py Normal file
View File

@ -0,0 +1,15 @@
from app import mail
from flask import current_app
from flask_mail import Message
from threading import Thread
def send_async_email(app, msg):
with app.app_context():
mail.send(msg)
def send_email(subject, sender, recipients, text_body, html_body):
msg = Message(subject, sender=sender, recipients=recipients)
msg.body = text_body
msg.html = html_body
Thread(target=send_async_email,
args=(current_app._get_current_object(), msg)).start()

View File

@ -13,3 +13,9 @@ class Config(object):
f"@{os.environ.get('DATABASE_HOST', 'localhost')}:{os.environ.get('DATABASE_PORT', '5432')}"
f"/{os.environ.get('DATABASE_DB', '') or os.environ.get('DATABASE_USER', 'scan2kasse')}")
SQLALCHEMY_TRACK_MODIFICATIONS = False
MAIL_SERVER = os.environ.get('MAIL_SERVER')
MAIL_PORT = int(os.environ.get('MAIL_PORT'))
MAIL_USE_TLS = os.environ.get('MAIL_USE_TLS') is not None
MAIL_USERNAME = os.environ.get('MAIL_USERNAME')
MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD')
ADMINS = ['postmaster@wpgcommunity.net']