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_login import LoginManager
|
||||||
from flask_assets import Bundle, Environment
|
from flask_assets import Bundle, Environment
|
||||||
from flask_cors import CORS
|
from flask_cors import CORS
|
||||||
from flask_mail import Mail
|
from flask_mail import Mail, email_dispatched
|
||||||
|
|
||||||
|
|
||||||
db = SQLAlchemy()
|
db = SQLAlchemy()
|
||||||
|
@ -75,6 +75,11 @@ def create_app():
|
||||||
def assets(path):
|
def assets(path):
|
||||||
return send_from_directory('templates/home/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
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
from flask import flash, redirect, url_for
|
from flask import flash, redirect, url_for
|
||||||
from flask_login import UserMixin
|
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 db
|
||||||
from . import login
|
from . import login
|
||||||
|
|
||||||
|
@ -50,3 +50,9 @@ def user_loader(user_id):
|
||||||
def unauth():
|
def unauth():
|
||||||
flash("Please login first")
|
flash("Please login first")
|
||||||
return redirect(url_for("registration.register"))
|
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 datetime import datetime, timedelta
|
||||||
from flask import Blueprint, config, current_app, flash, redirect, render_template, request, url_for
|
from flask import Blueprint, abort, config, current_app, flash, redirect, render_template, request, url_for
|
||||||
import flask_login
|
import flask_login
|
||||||
from flask_login import current_user
|
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 werkzeug.security import check_password_hash, generate_password_hash
|
||||||
from flask_mail import Message
|
from flask_mail import Message
|
||||||
|
import ulid
|
||||||
|
|
||||||
from goathacks import db, mail as app_mail
|
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")
|
bp = Blueprint('registration', __name__, url_prefix="/registration")
|
||||||
|
|
||||||
|
@ -81,6 +82,9 @@ def login():
|
||||||
password = request.form.get('password')
|
password = request.form.get('password')
|
||||||
|
|
||||||
user = User.query.filter_by(email=email).first()
|
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):
|
if check_password_hash(user.password, password):
|
||||||
flask_login.login_user(user)
|
flask_login.login_user(user)
|
||||||
|
@ -92,3 +96,69 @@ def login():
|
||||||
flash("Incorrect password")
|
flash("Incorrect password")
|
||||||
|
|
||||||
return render_template("login.html", form=form)
|
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()])
|
password = PasswordField("Password", validators=[DataRequired()])
|
||||||
submit = SubmitField("Sign in")
|
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
|
<span><p><em>Don't have an account? <a
|
||||||
href="{{url_for('registration.register')}}">Register
|
href="{{url_for('registration.register')}}">Register
|
||||||
here</a>.</em></p></span>
|
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>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% 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
|
uWSGI==2.0.21
|
||||||
Werkzeug==2.2.2
|
Werkzeug==2.2.2
|
||||||
WTForms==3.0.1
|
WTForms==3.0.1
|
||||||
|
ulid
|
||||||
|
|
Loading…
Add table
Reference in a new issue