registration: Basic password reset
I think expiration should be pending on a proper way to do recurring tasks -- I'm personally in favor of a CLI command that can be run from a cronjob or systemd timer that will do things like auto-expire password reset requests and send the daily registration reports. Now that I'm thinking about it, this does need at least a rudimentary system to make sure that it actually expires. If the expiration is invalid at the time of reset, then the request can just be invalidated and deleted. There's no pressing need for automatic removal until it's implemented. Thoughts @willhockey20?
This commit is contained in:
parent
952df13136
commit
60953074e7
9 changed files with 184 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, DateTime, ForeignKey, Integer, String
|
||||
from . import db
|
||||
from . import login
|
||||
|
||||
|
@ -54,4 +54,4 @@ def unauth():
|
|||
|
||||
class PwResetRequest(db.Model):
|
||||
id = Column(String, primary_key=True)
|
||||
user_id = db.relationship("User")
|
||||
user_id = Column(Integer, ForeignKey('user.id'), 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 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,62 @@ 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
|
||||
)
|
||||
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 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")
|
||||
|
|
13
goathacks/templates/emails/password_reset.txt
Normal file
13
goathacks/templates/emails/password_reset.txt
Normal file
|
@ -0,0 +1,13 @@
|
|||
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)}}
|
||||
|
||||
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 %}
|
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 ###
|
Loading…
Add table
Reference in a new issue