diff --git a/goathacks/__init__.py b/goathacks/__init__.py index 6530201..d11608c 100644 --- a/goathacks/__init__.py +++ b/goathacks/__init__.py @@ -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 diff --git a/goathacks/models.py b/goathacks/models.py index fb6d7ac..2eb9b50 100644 --- a/goathacks/models.py +++ b/goathacks/models.py @@ -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) diff --git a/goathacks/registration/__init__.py b/goathacks/registration/__init__.py index 671cdd3..c48a04e 100644 --- a/goathacks/registration/__init__.py +++ b/goathacks/registration/__init__.py @@ -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/", 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) diff --git a/goathacks/registration/forms.py b/goathacks/registration/forms.py index 3fe74c5..7898b00 100644 --- a/goathacks/registration/forms.py +++ b/goathacks/registration/forms.py @@ -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") diff --git a/goathacks/templates/emails/password_reset.txt b/goathacks/templates/emails/password_reset.txt new file mode 100644 index 0000000..bfc2ae5 --- /dev/null +++ b/goathacks/templates/emails/password_reset.txt @@ -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 diff --git a/goathacks/templates/login.html b/goathacks/templates/login.html index 3366ab9..57a3abe 100644 --- a/goathacks/templates/login.html +++ b/goathacks/templates/login.html @@ -25,6 +25,10 @@

Don't have an account? Register here.

+ +

Forgot your password? Head over here + to reset it.

{% endblock %} diff --git a/goathacks/templates/password_reset.html b/goathacks/templates/password_reset.html new file mode 100644 index 0000000..582fb82 --- /dev/null +++ b/goathacks/templates/password_reset.html @@ -0,0 +1,27 @@ +{% extends 'base.html' %} + +{% block content %} +
+
+
+

Reset Password

+
+
+
+
+
+
+ {{ form.csrf_token }} +
+ {{form.password}}
{{form.password.label}} +
+
+ {{form.password_confirm}}
{{form.password_confirm.label}} +
+
+ {{form.submit}} +
+
+
+
+{% endblock %} diff --git a/goathacks/templates/pw_reset.html b/goathacks/templates/pw_reset.html new file mode 100644 index 0000000..15722ab --- /dev/null +++ b/goathacks/templates/pw_reset.html @@ -0,0 +1,24 @@ +{% extends 'base.html' %} + +{% block content %} +
+
+
+

Reset Password

+
+
+
+
+
+
+ {{ form.csrf_token }} +
+ {{ form.email }}
{{ form.email.label}} +
+
+ {{form.submit}} +
+
+
+
+{% endblock %} diff --git a/migrations/versions/8a0c9c00f04c_.py b/migrations/versions/8a0c9c00f04c_.py new file mode 100644 index 0000000..dd7524f --- /dev/null +++ b/migrations/versions/8a0c9c00f04c_.py @@ -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 ###