Skip to content
This repository was archived by the owner on May 20, 2018. It is now read-only.

Commit 89cc4aa

Browse files
committed
Merge pull request #8 from levlaz/feature-account-management
Account Management Features
2 parents 1c52fd5 + fad4704 commit 89cc4aa

File tree

13 files changed

+305
-13
lines changed

13 files changed

+305
-13
lines changed

app/auth/forms.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,39 @@ def validate_email(self, field):
3232
def validate_username(self, field):
3333
if User.query.filter_by(username=field.data).first():
3434
raise ValidationError('Username already in use.')
35+
36+
class ChangePasswordForm(Form):
37+
old_password = PasswordField('Old password', validators=[Required()])
38+
password = PasswordField('New password', validators=[
39+
Required(), EqualTo('password2', message='Passwords must match')])
40+
password2 = PasswordField('Confirm new password', validators=[Required()])
41+
submit = SubmitField('Update Password')
42+
43+
class PasswordResetRequestForm(Form):
44+
email = StringField('Email', validators=[Required(), Length(1, 64),
45+
Email()])
46+
submit = SubmitField('Reset Password')
47+
48+
49+
class PasswordResetForm(Form):
50+
email = StringField('Email', validators=[Required(), Length(1, 64),
51+
Email()])
52+
password = PasswordField('New Password', validators=[
53+
Required(), EqualTo('password2', message='Passwords must match')])
54+
password2 = PasswordField('Confirm password', validators=[Required()])
55+
submit = SubmitField('Reset Password')
56+
57+
def validate_email(self, field):
58+
if User.query.filter_by(email=field.data).first() is None:
59+
raise ValidationError('Unknown email address.')
60+
61+
62+
class ChangeEmailForm(Form):
63+
email = StringField('New Email', validators=[Required(), Length(1, 64),
64+
Email()])
65+
password = PasswordField('Password', validators=[Required()])
66+
submit = SubmitField('Update Email Address')
67+
68+
def validate_email(self, field):
69+
if User.query.filter_by(email=field.data).first():
70+
raise ValidationError('Email already registered.')

app/auth/views.py

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from app import db
66
from ..models import User
77
from ..email import send_email
8-
from .forms import LoginForm, RegistrationForm
8+
from .forms import *
99

1010
@auth.before_app_request
1111
def before_request():
@@ -74,3 +74,82 @@ def unconfirmed():
7474
if current_user.is_anonymous() or current_user.confirmed:
7575
return redirect(url_for('main.index'))
7676
return render_template('auth/unconfirmed.html')
77+
78+
@auth.route('/change-password', methods=['GET', 'POST'])
79+
@login_required
80+
def change_password():
81+
form = ChangePasswordForm()
82+
if form.validate_on_submit():
83+
if current_user.verify_password(form.old_password.data):
84+
current_user.password = form.password.data
85+
db.session.add(current_user)
86+
db.session.commit()
87+
flash('Your password has been updated.')
88+
return redirect(url_for('main.index'))
89+
else:
90+
flash('Invalid password.')
91+
return render_template("auth/change_password.html", form=form)
92+
93+
@auth.route('/reset', methods=['GET', 'POST'])
94+
def password_reset_request():
95+
if not current_user.is_anonymous:
96+
return redirect(url_for('main.index'))
97+
form = PasswordResetRequestForm()
98+
if form.validate_on_submit():
99+
user = User.query.filter_by(email=form.email.data).first()
100+
if user:
101+
token = user.generate_reset_token()
102+
send_email(user.email, 'Reset Your Password',
103+
'auth/email/reset_password',
104+
user=user, token=token,
105+
next=request.args.get('next'))
106+
flash('An email with instructions to reset your password has been '
107+
'sent to you.')
108+
return redirect(url_for('auth.login'))
109+
return render_template('auth/reset_password.html', form=form)
110+
111+
112+
@auth.route('/reset/<token>', methods=['GET', 'POST'])
113+
def password_reset(token):
114+
if not current_user.is_anonymous:
115+
return redirect(url_for('main.index'))
116+
form = PasswordResetForm()
117+
if form.validate_on_submit():
118+
user = User.query.filter_by(email=form.email.data).first()
119+
if user is None:
120+
return redirect(url_for('main.index'))
121+
if user.reset_password(token, form.password.data):
122+
flash('Your password has been updated.')
123+
return redirect(url_for('auth.login'))
124+
else:
125+
return redirect(url_for('main.index'))
126+
return render_template('auth/reset_password.html', form=form)
127+
128+
129+
@auth.route('/change-email', methods=['GET', 'POST'])
130+
@login_required
131+
def change_email_request():
132+
form = ChangeEmailForm()
133+
if form.validate_on_submit():
134+
if current_user.verify_password(form.password.data):
135+
new_email = form.email.data
136+
token = current_user.generate_email_change_token(new_email)
137+
send_email(new_email, 'Confirm your email address',
138+
'auth/email/change_email',
139+
user=current_user, token=token)
140+
flash('An email with instructions to confirm your new email '
141+
'address has been sent to you.')
142+
return redirect(url_for('main.index'))
143+
else:
144+
flash('Invalid email or password.')
145+
return render_template("auth/change_email.html", form=form)
146+
147+
148+
@auth.route('/change-email/<token>')
149+
@login_required
150+
def change_email(token):
151+
if current_user.change_email(token):
152+
flash('Your email address has been updated.')
153+
else:
154+
flash('Invalid request.')
155+
return redirect(url_for('main.index'))

app/models.py

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
from . import db, login_manager
22
from werkzeug.security import generate_password_hash, check_password_hash
3+
from flask import current_app, request, url_for
34
from flask.ext.login import UserMixin
45
from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
56

67
from flask import current_app
78
from datetime import datetime
89

910
import bleach
11+
import hashlib
1012

1113
@login_manager.user_loader
1214
def load_user(user_id):
@@ -30,10 +32,15 @@ class User(UserMixin, db.Model):
3032
role_id = db.Column(db.Integer, db.ForeignKey('roles.id'))
3133
password_hash = db.Column(db.String(128))
3234
confirmed = db.Column(db.Boolean, default=False)
33-
35+
avatar_hash = db.Column(db.String(32))
36+
created_date = db.Column(db.DateTime(), default=datetime.utcnow)
37+
updated_date = db.Column(db.DateTime(), default=datetime.utcnow)
3438
notes = db.relationship('Note', backref='author', lazy='dynamic')
35-
def __repr__(self):
36-
return '<User {0}>'.format(self.username)
39+
40+
def __init__(self, **kwargs):
41+
if self.email is not None and self.avatar_hash is None:
42+
self.avatar_hash = hashlib.md5(
43+
self.email.encode('utf-8')).hexdigest()
3744

3845
@property
3946
def password(self):
@@ -50,6 +57,47 @@ def generate_confirmation_token(self, expiration=3600):
5057
s = Serializer(current_app.config['SECRET_KEY'], expiration)
5158
return s.dumps({'confirm': self.id})
5259

60+
def generate_reset_token(self, expiration=3600):
61+
s = Serializer(current_app.config['SECRET_KEY'], expiration)
62+
return s.dumps({'reset': self.id})
63+
64+
def reset_password(self, token, new_password):
65+
s = Serializer(current_app.config['SECRET_KEY'])
66+
try:
67+
data = s.loads(token)
68+
except:
69+
return False
70+
if data.get('reset') != self.id:
71+
return False
72+
self.password = new_password
73+
db.session.add(self)
74+
db.session.commit()
75+
return True
76+
77+
def generate_email_change_token(self, new_email, expiration=3600):
78+
s = Serializer(current_app.config['SECRET_KEY'], expiration)
79+
return s.dumps({'change_email': self.id, 'new_email': new_email})
80+
81+
def change_email(self, token):
82+
s = Serializer(current_app.config['SECRET_KEY'])
83+
try:
84+
data = s.loads(token)
85+
except:
86+
return False
87+
if data.get('change_email') != self.id:
88+
return False
89+
new_email = data.get('new_email')
90+
if new_email is None:
91+
return False
92+
if self.query.filter_by(email=new_email).first() is not None:
93+
return False
94+
self.email = new_email
95+
self.avatar_hash = hashlib.md5(
96+
self.email.encode('utf-8')).hexdigest()
97+
db.session.add(self)
98+
db.session.commit()
99+
return True
100+
53101
def confirm(self, token):
54102
s = Serializer(current_app.config['SECRET_KEY'])
55103
try:
@@ -63,6 +111,19 @@ def confirm(self, token):
63111
db.session.commit()
64112
return True
65113

114+
def gravatar(self, size=100, default='identicon', rating='g'):
115+
if request.is_secure:
116+
url = 'https://secure.gravatar.com/avatar'
117+
else:
118+
url = 'http://www.gravatar.com/avatar'
119+
hash = self.avatar_hash or hashlib.md5(
120+
self.email.encode('utf-8')).hexdigest()
121+
return '{url}/{hash}?s={size}&d={default}&r={rating}'.format(
122+
url=url, hash=hash, size=size, default=default, rating=rating)
123+
124+
def __repr__(self):
125+
return '<User {0}>'.format(self.username)
126+
66127
class Note(db.Model):
67128
__tablename__ = 'notes'
68129
id = db.Column(db.Integer, primary_key=True)
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{% extends "base.html" %}
2+
{% import "bootstrap/wtf.html" as wtf %}
3+
4+
{% block title %}Flasky - Change Email Address{% endblock %}
5+
6+
{% block page_content %}
7+
<div class="page-header">
8+
<h1>Change Your Email Address</h1>
9+
</div>
10+
<div class="col-md-4">
11+
{{ wtf.quick_form(form) }}
12+
</div>
13+
{% endblock %}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{% extends "base.html" %}
2+
{% import "bootstrap/wtf.html" as wtf %}
3+
4+
{% block title %}Flasky - Change Password{% endblock %}
5+
6+
{% block page_content %}
7+
<div class="page-header">
8+
<h1>Change Your Password</h1>
9+
</div>
10+
<div class="col-md-4">
11+
{{ wtf.quick_form(form) }}
12+
</div>
13+
{% endblock %}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<p>Dear {{ user.username }},</p>
2+
<p>To confirm your new email address <a href="{{ url_for('auth.change_email', token=token, _external=True) }}">click here</a>.</p>
3+
<p>Alternatively, you can paste the following link in your browser's address bar:</p>
4+
<p>{{ url_for('auth.change_email', token=token, _external=True) }}</p>
5+
<p>Sincerely,</p>
6+
<p>The BrainDump Team</p>
7+
<p><small>Note: replies to this email address are not monitored.</small></p>
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
Dear {{ user.username }},
2+
3+
To confirm your new email address click on the following link:
4+
5+
{{ url_for('auth.change_email', token=token, _external=True) }}
6+
7+
Sincerely,
8+
9+
The BrainDump Team
10+
11+
Note: replies to this email address are not monitored.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<p>Dear {{ user.username }},</p>
2+
<p>To reset your password <a href="{{ url_for('auth.password_reset', token=token, _external=True) }}">click here</a>.</p>
3+
<p>Alternatively, you can paste the following link in your browser's address bar:</p>
4+
<p>{{ url_for('auth.password_reset', token=token, _external=True) }}</p>
5+
<p>If you have not requested a password reset simply ignore this message.</p>
6+
<p>Sincerely,</p>
7+
<p>The BrainDump Team</p>
8+
<p><small>Note: replies to this email address are not monitored.</small></p>
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
Dear {{ user.username }},
2+
3+
To reset your password click on the following link:
4+
5+
{{ url_for('auth.password_reset', token=token, _external=True) }}
6+
7+
If you have not requested a password reset simply ignore this message.
8+
9+
Sincerely,
10+
11+
The BrainDump Team
12+
13+
Note: replies to this email address are not monitored.

app/templates/auth/login.html

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,12 @@ <h1>Login</h1>
1010

1111
<div class="col-md-4">
1212
{{ wtf.quick_form(form) }}
13-
</div>
1413

15-
<p>
16-
New User?
17-
<a href="{{ url_for('auth.register') }}">
18-
Click here to register
19-
</a>
20-
</p>
14+
<br />
15+
<p>Forgot your password? <a href="{{ url_for('auth.password_reset_request') }}">Click here to reset it</a>.</p>
16+
<p> New User? <a href="{{ url_for('auth.register') }}"> Click here to register.</a></p>
17+
18+
</div>
2119

2220
{% endblock %}
2321

0 commit comments

Comments
 (0)