Merge pull request #1 from Lunaresk/feature/S2K-11-passwort-recovery-erstellen
This commit is contained in:
commit
b997e25a08
@ -3,6 +3,7 @@ from flask import Flask
|
|||||||
from flask_bootstrap import Bootstrap
|
from flask_bootstrap import Bootstrap
|
||||||
from flask_login import LoginManager
|
from flask_login import LoginManager
|
||||||
from flask_sqlalchemy import SQLAlchemy
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
|
from flask_mail import Mail
|
||||||
from flask_migrate import Migrate
|
from flask_migrate import Migrate
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
from logging.config import fileConfig
|
from logging.config import fileConfig
|
||||||
@ -28,6 +29,7 @@ bootstrap = Bootstrap()
|
|||||||
db = SQLAlchemy()
|
db = SQLAlchemy()
|
||||||
login = LoginManager()
|
login = LoginManager()
|
||||||
login.login_view = 'auth.web_login'
|
login.login_view = 'auth.web_login'
|
||||||
|
mail = Mail()
|
||||||
migrate = Migrate()
|
migrate = Migrate()
|
||||||
|
|
||||||
|
|
||||||
@ -37,6 +39,7 @@ def create_app(config_class=Config):
|
|||||||
bootstrap.init_app(app)
|
bootstrap.init_app(app)
|
||||||
db.init_app(app)
|
db.init_app(app)
|
||||||
login.init_app(app)
|
login.init_app(app)
|
||||||
|
mail.init_app(app)
|
||||||
migrate.init_app(app, db, render_as_batch=True)
|
migrate.init_app(app, db, render_as_batch=True)
|
||||||
|
|
||||||
from app.auth import bp as auth_bp
|
from app.auth import bp as auth_bp
|
||||||
|
|||||||
14
app/auth/email.py
Normal file
14
app/auth/email.py
Normal 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))
|
||||||
@ -24,4 +24,14 @@ class RegistrationForm(FlaskForm):
|
|||||||
def validate_email(self, email):
|
def validate_email(self, email):
|
||||||
user = User.query.filter_by(email=email.data).first()
|
user = User.query.filter_by(email=email.data).first()
|
||||||
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 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"})
|
||||||
@ -1,6 +1,7 @@
|
|||||||
from app import db
|
from app import db
|
||||||
from app.auth import bp
|
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.models import User
|
||||||
from app.utils.routes_utils import render_custom_template as render_template
|
from app.utils.routes_utils import render_custom_template as render_template
|
||||||
from flask import flash, redirect, request, url_for
|
from flask import flash, redirect, request, url_for
|
||||||
@ -41,4 +42,33 @@ def web_login():
|
|||||||
@bp.route(f'/logout')
|
@bp.route(f'/logout')
|
||||||
def web_logout():
|
def web_logout():
|
||||||
logout_user()
|
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)
|
||||||
@ -1,6 +1,9 @@
|
|||||||
|
import jwt
|
||||||
from app import db, login
|
from app import db, login
|
||||||
from datetime import date
|
from datetime import date
|
||||||
|
from flask import current_app
|
||||||
from flask_login import UserMixin
|
from flask_login import UserMixin
|
||||||
|
from time import time
|
||||||
from werkzeug.security import generate_password_hash, check_password_hash
|
from werkzeug.security import generate_password_hash, check_password_hash
|
||||||
|
|
||||||
item_category = db.Table("item_category",
|
item_category = db.Table("item_category",
|
||||||
@ -23,6 +26,20 @@ class User(UserMixin, db.Model):
|
|||||||
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):
|
||||||
|
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:
|
def __repr__(self) -> str:
|
||||||
return f"<User {self.id} ({self.email})>"
|
return f"<User {self.id} ({self.email})>"
|
||||||
|
|
||||||
|
|||||||
@ -16,4 +16,8 @@
|
|||||||
</p>
|
</p>
|
||||||
{{ wtf.form_field(form.submit, class=form.submit.render_kw["class"]) }}
|
{{ wtf.form_field(form.submit, class=form.submit.render_kw["class"]) }}
|
||||||
</form>
|
</form>
|
||||||
|
<p>
|
||||||
|
Forgot Your Password?
|
||||||
|
<a href="{{ url_for('auth.reset_password_request') }}">Click to Reset It</a>
|
||||||
|
</p>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
23
app/templates/auth/reset_password.html
Normal file
23
app/templates/auth/reset_password.html
Normal 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 %}
|
||||||
16
app/templates/auth/reset_password_request.html
Normal file
16
app/templates/auth/reset_password_request.html
Normal 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 %}
|
||||||
12
app/templates/email/reset_password.html
Normal file
12
app/templates/email/reset_password.html
Normal 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>
|
||||||
11
app/templates/email/reset_password.txt
Normal file
11
app/templates/email/reset_password.txt
Normal 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
15
app/utils/email.py
Normal 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()
|
||||||
@ -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_HOST', 'localhost')}:{os.environ.get('DATABASE_PORT', '5432')}"
|
||||||
f"/{os.environ.get('DATABASE_DB', '') or os.environ.get('DATABASE_USER', 'scan2kasse')}")
|
f"/{os.environ.get('DATABASE_DB', '') or os.environ.get('DATABASE_USER', 'scan2kasse')}")
|
||||||
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
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']
|
||||||
Loading…
x
Reference in New Issue
Block a user