Merge pull request #12 from wpi-acm/password-reset
Implement Password Reset
This commit is contained in:
commit
de8277c970
12 changed files with 263 additions and 6 deletions
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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")
|
||||
|
|
15
goathacks/templates/emails/password_reset.txt
Normal file
15
goathacks/templates/emails/password_reset.txt
Normal 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
|
|
@ -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 %}
|
||||
|
|
27
goathacks/templates/password_reset.html
Normal file
27
goathacks/templates/password_reset.html
Normal 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 %}
|
24
goathacks/templates/pw_reset.html
Normal file
24
goathacks/templates/pw_reset.html
Normal 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 %}
|
32
migrations/versions/261c004968a4_.py
Normal file
32
migrations/versions/261c004968a4_.py
Normal 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 ###
|
34
migrations/versions/8a0c9c00f04c_.py
Normal file
34
migrations/versions/8a0c9c00f04c_.py
Normal 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 ###
|
31
migrations/versions/db38c3deb0b9_.py
Normal file
31
migrations/versions/db38c3deb0b9_.py
Normal 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 ###
|
|
@ -21,3 +21,4 @@ SQLAlchemy==1.4.44
|
|||
uWSGI==2.0.21
|
||||
Werkzeug==2.2.2
|
||||
WTForms==3.0.1
|
||||
ulid
|
||||
|
|
Loading…
Add table
Reference in a new issue