diff --git a/app/__init__.py b/app/__init__.py index c717972..95f8c6c 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -5,6 +5,7 @@ from flask import Flask from flask_sqlalchemy import SQLAlchemy from flask_migrate import Migrate from flask_login import LoginManager +from flask_mail import Mail from config import Config app = Flask(__name__) @@ -13,6 +14,7 @@ db = SQLAlchemy(app) migrate = Migrate(app, db) login = LoginManager(app) login.login_view = 'login' +mail = Mail(app) if not app.debug: if app.config['MAIL_SERVER']: diff --git a/app/email.py b/app/email.py new file mode 100644 index 0000000..fc7f929 --- /dev/null +++ b/app/email.py @@ -0,0 +1,27 @@ +from threading import Thread +from flask import render_template +from flask_mail import Message +from app import app, mail + + +def send_async_email(app, msg): + with app.app_context(): + mail.send(msg) + + +def send_email(subject, sender, recipients, text_body, html_body): + msg = Message(subject, sender=sender, recipients=recipients) + msg.body = text_body + msg.html = html_body + Thread(target=send_async_email, args=(app, msg)).start() + + +def send_password_reset_email(user): + token = user.get_reset_password_token() + send_email('[Microblog] Reset Your Password', + sender=app.config['ADMINS'][0], + recipients=[user.email], + text_body=render_template('email/reset_password.txt', + user=user, token=token), + html_body=render_template('email/reset_password.html', + user=user, token=token)) diff --git a/app/forms.py b/app/forms.py index 5003ce2..85b10af 100644 --- a/app/forms.py +++ b/app/forms.py @@ -32,6 +32,18 @@ class RegistrationForm(FlaskForm): raise ValidationError('Please use a different email address.') +class ResetPasswordRequestForm(FlaskForm): + email = StringField('Email', validators=[DataRequired(), Email()]) + submit = SubmitField('Request Password Reset') + + +class ResetPasswordForm(FlaskForm): + password = PasswordField('Password', validators=[DataRequired()]) + password2 = PasswordField( + 'Repeat Password', validators=[DataRequired(), EqualTo('password')]) + submit = SubmitField('Request Password Reset') + + class EditProfileForm(FlaskForm): username = StringField('Username', validators=[DataRequired()]) about_me = TextAreaField('About me', validators=[Length(min=0, max=140)]) diff --git a/app/models.py b/app/models.py index b160ccb..a0f68ae 100644 --- a/app/models.py +++ b/app/models.py @@ -1,8 +1,10 @@ from datetime import datetime from hashlib import md5 -from app import db, login +from time import time from flask_login import UserMixin from werkzeug.security import generate_password_hash, check_password_hash +import jwt +from app import app, db, login followers = db.Table( @@ -59,6 +61,20 @@ class User(UserMixin, db.Model): own = Post.query.filter_by(user_id=self.id) return followed.union(own).order_by(Post.timestamp.desc()) + def get_reset_password_token(self, expires_in=600): + return jwt.encode( + {'reset_password': self.id, 'exp': time() + expires_in}, + app.config['SECRET_KEY'], algorithm='HS256') + + @staticmethod + def verify_reset_password_token(token): + try: + id = jwt.decode(token, app.config['SECRET_KEY'], + algorithms=['HS256'])['reset_password'] + except: + return + return User.query.get(id) + @login.user_loader def load_user(id): diff --git a/app/routes.py b/app/routes.py index 804ae25..4d26806 100644 --- a/app/routes.py +++ b/app/routes.py @@ -4,8 +4,9 @@ from flask_login import login_user, logout_user, current_user, login_required from werkzeug.urls import url_parse from app import app, db from app.forms import LoginForm, RegistrationForm, EditProfileForm, \ - EmptyForm, PostForm + EmptyForm, PostForm, ResetPasswordRequestForm, ResetPasswordForm from app.models import User, Post +from app.email import send_password_reset_email @app.before_request @@ -91,6 +92,37 @@ def register(): return render_template('register.html', title='Register', form=form) +@app.route('/reset_password_request', methods=['GET', 'POST']) +def reset_password_request(): + if current_user.is_authenticated: + return redirect(url_for('index')) + form = ResetPasswordRequestForm() + if form.validate_on_submit(): + user = User.query.filter_by(email=form.email.data).first() + if user: + send_password_reset_email(user) + flash('Check your email for the instructions to reset your password') + return redirect(url_for('login')) + return render_template('reset_password_request.html', + title='Reset Password', form=form) + + +@app.route('/reset_password/', methods=['GET', 'POST']) +def reset_password(token): + if current_user.is_authenticated: + return redirect(url_for('index')) + user = User.verify_reset_password_token(token) + if not user: + return redirect(url_for('index')) + form = ResetPasswordForm() + if form.validate_on_submit(): + user.set_password(form.password.data) + db.session.commit() + flash('Your password has been reset.') + return redirect(url_for('login')) + return render_template('reset_password.html', form=form) + + @app.route('/user/') @login_required def user(username): diff --git a/app/templates/email/reset_password.html b/app/templates/email/reset_password.html new file mode 100644 index 0000000..a39a9c4 --- /dev/null +++ b/app/templates/email/reset_password.html @@ -0,0 +1,12 @@ +

Dear {{ user.username }},

+

+ To reset your password + + click here + . +

+

Alternatively, you can paste the following link in your browser's address bar:

+

{{ url_for('reset_password', token=token, _external=True) }}

+

If you have not requested a password reset simply ignore this message.

+

Sincerely,

+

The Microblog Team

diff --git a/app/templates/email/reset_password.txt b/app/templates/email/reset_password.txt new file mode 100644 index 0000000..5387347 --- /dev/null +++ b/app/templates/email/reset_password.txt @@ -0,0 +1,11 @@ +Dear {{ user.username }}, + +To reset your password click on the following link: + +{{ url_for('reset_password', token=token, _external=True) }} + +If you have not requested a password reset simply ignore this message. + +Sincerely, + +The Microblog Team diff --git a/app/templates/login.html b/app/templates/login.html index 2875293..2a54187 100644 --- a/app/templates/login.html +++ b/app/templates/login.html @@ -22,4 +22,8 @@

{{ form.submit() }}

New User? Click to Register!

+

+ Forgot Your Password? + Click to Reset It +

{% endblock %} diff --git a/app/templates/reset_password.html b/app/templates/reset_password.html new file mode 100644 index 0000000..92f0c8e --- /dev/null +++ b/app/templates/reset_password.html @@ -0,0 +1,23 @@ +{% extends "base.html" %} + +{% block content %} +

Reset Your Password

+
+ {{ form.hidden_tag() }} +

+ {{ form.password.label }}
+ {{ form.password(size=32) }}
+ {% for error in form.password.errors %} + [{{ error }}] + {% endfor %} +

+

+ {{ form.password2.label }}
+ {{ form.password2(size=32) }}
+ {% for error in form.password2.errors %} + [{{ error }}] + {% endfor %} +

+

{{ form.submit() }}

+
+{% endblock %} diff --git a/app/templates/reset_password_request.html b/app/templates/reset_password_request.html new file mode 100644 index 0000000..18bb0ae --- /dev/null +++ b/app/templates/reset_password_request.html @@ -0,0 +1,16 @@ +{% extends "base.html" %} + +{% block content %} +

Reset Password

+
+ {{ form.hidden_tag() }} +

+ {{ form.email.label }}
+ {{ form.email(size=64) }}
+ {% for error in form.email.errors %} + [{{ error }}] + {% endfor %} +

+

{{ form.submit() }}

+
+{% endblock %}