Merge pull request #12 from wpi-acm/password-reset

Implement Password Reset
This commit is contained in:
William Ryan 2023-01-03 20:42:45 -05:00 committed by GitHub
commit de8277c970
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 263 additions and 6 deletions

View file

@ -4,7 +4,7 @@ from flask_migrate import Migrate
from flask_login import LoginManager
from flask_assets import Bundle, Environment
from flask_cors import CORS
from flask_mail import Mail
from flask_mail import Mail, email_dispatched
db = SQLAlchemy()
@ -75,6 +75,11 @@ def create_app():
def assets(path):
return send_from_directory('templates/home/assets', path)
def log_message(message, app):
app.logger.debug(message)
email_dispatched.connect(log_message)
return app

View file

@ -1,6 +1,6 @@
from flask import flash, redirect, url_for
from flask_login import UserMixin
from sqlalchemy import Boolean, Column, DateTime, Integer, String
from sqlalchemy import Boolean, Column, Date, DateTime, ForeignKey, Integer, String
from . import db
from . import login
@ -50,3 +50,9 @@ def user_loader(user_id):
def unauth():
flash("Please login first")
return redirect(url_for("registration.register"))
class PwResetRequest(db.Model):
id = Column(String, primary_key=True)
user_id = Column(Integer, ForeignKey('user.id'), nullable=False)
expires = Column(DateTime, nullable=False)

View file

@ -1,13 +1,14 @@
from datetime import datetime
from flask import Blueprint, config, current_app, flash, redirect, render_template, request, url_for
from datetime import datetime, timedelta
from flask import Blueprint, abort, config, current_app, flash, redirect, render_template, request, url_for
import flask_login
from flask_login import current_user
from goathacks.registration.forms import LoginForm, RegisterForm
from goathacks.registration.forms import LoginForm, PwResetForm, RegisterForm, ResetForm
from werkzeug.security import check_password_hash, generate_password_hash
from flask_mail import Message
import ulid
from goathacks import db, mail as app_mail
from goathacks.models import User
from goathacks.models import PwResetRequest, User
bp = Blueprint('registration', __name__, url_prefix="/registration")
@ -81,6 +82,9 @@ def login():
password = request.form.get('password')
user = User.query.filter_by(email=email).first()
if user == None:
flash("Email or password incorrect")
return render_template("login.html", form=form)
if check_password_hash(user.password, password):
flask_login.login_user(user)
@ -92,3 +96,69 @@ def login():
flash("Incorrect password")
return render_template("login.html", form=form)
@bp.route("/reset", methods=["GET", "POST"])
def reset():
form = ResetForm(request.form)
if request.method == 'POST':
email = request.form.get('email')
user = User.query.filter_by(email=email).first()
if user == None:
flash("If that email has an account here, we've just sent it a link to reset your password.")
return redirect(url_for("registration.login"))
else:
r = PwResetRequest(
id=str(ulid.ulid()),
user_id=user.id,
expires=datetime.now() + timedelta(minutes=30)
)
db.session.add(r)
db.session.commit()
msg = Message("GoatHacks - Password Reset Request")
msg.add_recipient(user.email)
msg.body = render_template("emails/password_reset.txt", code=r.id)
app_mail.send(msg)
flash("If that email has an account here, we've just sent it a link to reset your password.")
return redirect(url_for("registration.login"))
else:
return render_template("pw_reset.html", form=form)
@bp.route("/reset/complete/<string:id>", methods=["GET", "POST"])
def do_reset(id):
form = PwResetForm(request.form)
req = PwResetRequest.query.filter_by(id=id).first()
if req == None:
flash("Invalid request")
return redirect(url_for("registration.login"))
if req.expires < datetime.now():
db.session.delete(req)
db.session.commit()
flash("Invalid request")
return redirect(url_for("registration.login"))
if request.method == "POST":
password = request.form.get("password")
password_c = request.form.get("password_confirm")
if password == password_c:
user = User.query.filter_by(id=req.user_id).first()
if user == None:
flash("Invalid user")
return redirect(url_for("registration.login"))
user.password = generate_password_hash(password)
db.session.delete(req)
db.session.commit()
flash("Password successfully reset")
return redirect(url_for("registration.login"))
else:
flash("Passwords do not match!")
return render_template("password_reset.html", form=form)
else:
return render_template("password_reset.html", form=form)

View file

@ -23,3 +23,11 @@ class LoginForm(FlaskForm):
password = PasswordField("Password", validators=[DataRequired()])
submit = SubmitField("Sign in")
class ResetForm(FlaskForm):
email = StringField("Email", validators=[DataRequired()])
submit = SubmitField("Request reset")
class PwResetForm(FlaskForm):
password = PasswordField("Password")
password_confirm = PasswordField("Confirm Password")
submit = SubmitField("Submit")

View file

@ -0,0 +1,15 @@
Hello!
Someone just requested a password reset for your GoatHacks account. If this
wasn't you, don't worry. Just ignore this request, and nothing will happen.
If this was you, please follow this link: {{url_for("registration.do_reset", id=code, _external=True)}}
This link will expire in 30 minutes.
Happy Hacking!
GoatHacks Team
This is an automated message. Please email hack@wpi.edu with any questions or
concerns

View file

@ -25,6 +25,10 @@
<span><p><em>Don't have an account? <a
href="{{url_for('registration.register')}}">Register
here</a>.</em></p></span>
<span><p><em>Forgot your password? Head over <a
href="{{url_for('registration.reset')}}">here</a>
to reset it.</em></p></span>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,27 @@
{% extends 'base.html' %}
{% block content %}
<div style="height: 100%;">
<div id="registration-banner" class="parallax-container valign-wrapper">
<div class="section">
<h3 class="header-center text-darken-2">Reset Password</h3>
</div>
</div>
</div>
<div class="container">
<div class="section" style="background-color: #974355; padding: 20px;">
<form method="post">
{{ form.csrf_token }}
<div>
{{form.password}}<br/>{{form.password.label}}
</div>
<div>
{{form.password_confirm}}<br/>{{form.password_confirm.label}}
</div>
<div>
{{form.submit}}
</div>
</form>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,24 @@
{% extends 'base.html' %}
{% block content %}
<div style="height: 100%">
<div id="registration-banner" class="parallax-container valign-wrapper">
<div class="section">
<h3 class="header-center text-darken-2">Reset Password</h3>
</div>
</div>
</div>
<div class="container">
<div class="section" style="background-color: #974355; padding: 20px;">
<form method="post">
{{ form.csrf_token }}
<div>
{{ form.email }}<br/>{{ form.email.label}}
</div>
<div>
{{form.submit}}
</div>
</form>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,32 @@
"""empty message
Revision ID: 261c004968a4
Revises: 8a0c9c00f04c
Create Date: 2023-01-03 17:58:35.801660
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '261c004968a4'
down_revision = '8a0c9c00f04c'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('pw_reset_request', schema=None) as batch_op:
batch_op.add_column(sa.Column('expires', sa.DateTime(), nullable=False))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('pw_reset_request', schema=None) as batch_op:
batch_op.drop_column('expires')
# ### end Alembic commands ###

View file

@ -0,0 +1,34 @@
"""empty message
Revision ID: 8a0c9c00f04c
Revises: db38c3deb0b9
Create Date: 2023-01-03 16:59:23.201953
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '8a0c9c00f04c'
down_revision = 'db38c3deb0b9'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('pw_reset_request', schema=None) as batch_op:
batch_op.add_column(sa.Column('user_id', sa.Integer(), nullable=False))
batch_op.create_foreign_key(None, 'user', ['user_id'], ['id'])
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('pw_reset_request', schema=None) as batch_op:
batch_op.drop_constraint(None, type_='foreignkey')
batch_op.drop_column('user_id')
# ### end Alembic commands ###

View file

@ -0,0 +1,31 @@
"""empty message
Revision ID: db38c3deb0b9
Revises: a14a95ec57b0
Create Date: 2022-12-30 14:35:27.652423
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'db38c3deb0b9'
down_revision = 'a14a95ec57b0'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('pw_reset_request',
sa.Column('id', sa.String(), nullable=False),
sa.PrimaryKeyConstraint('id')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('pw_reset_request')
# ### end Alembic commands ###

View file

@ -21,3 +21,4 @@ SQLAlchemy==1.4.44
uWSGI==2.0.21
Werkzeug==2.2.2
WTForms==3.0.1
ulid