home » zplus/freepost.git
Author zPlus <zplus@peers.community> 2018-07-18 21:42:50
Committer zPlus <zplus@peers.community> 2018-07-18 21:42:50
Commit ee46645 (patch)
Tree c8508b0
Parent(s)

Add "account password reset". modified: freepost/__init__.py modified: freepost/database.py new file: freepost/mail.py new file: freepost/templates/email/password_changed.txt new file: freepost/templates/email/password_reset.txt modified: freepost/templates/login.html new file: freepost/templates/login_change_password.html new file: freepost/templates/login_reset.html modified: settings.yaml


commits diff: 0dd6d3d..ee46645
9 files changed, 310 insertions, 17 deletionsdownload


Diffstat
-rwxr-xr-x freepost/__init__.py 102
-rw-r--r-- freepost/database.py 84
-rw-r--r-- freepost/mail.py 15
-rw-r--r-- freepost/templates/email/password_changed.txt 1
-rw-r--r-- freepost/templates/email/password_reset.txt 6
-rwxr-xr-x freepost/templates/login.html 6
-rwxr-xr-x freepost/templates/login_change_password.html 45
-rwxr-xr-x freepost/templates/login_reset.html 43
-rw-r--r-- settings.yaml 25

Diff options
View
Side
Whitespace
Context lines
Inter-hunk lines
+101/-1 M   freepost/__init__.py
index 14ba73b..7958ca7
old size: 19K - new size: 22K
@@ -64,7 +64,7 @@ bleach.sanitizer.ALLOWED_ATTRIBUTES.update ({
64 64 'img': [ 'src' ]
65 65 })
66 66
67 - from freepost import database, session
67 + from freepost import database, mail, session
68 68
69 69 # Decorator.
70 70 # Make sure user is logged in
@@ -162,8 +162,13 @@ def login_check ():
162 162 'login.html',
163 163 flash = 'Bad login!')
164 164
165 + # Delete any existing "reset token"
166 + database.delete_password_reset_token (user['id'])
167 +
168 + # Start new session
165 169 session.start (user['id'], remember)
166 170
171 + # Go to user homepage
167 172 redirect (application.get_url ('homepage'))
168 173
169 174 @get ('/register', name='register')
@@ -216,6 +221,101 @@ def logout ():
216 221
217 222 redirect (application.get_url ('homepage'))
218 223
224 + @get ('/password_reset', name='password_reset')
225 + @requires_logout
226 + def password_reset ():
227 + return template ('login_reset.html')
228 +
229 + @post ('/password_reset', name='password_reset_send_code')
230 + @requires_logout
231 + def password_reset_send_code ():
232 + username = request.forms.get ('username')
233 + email = request.forms.get ('email')
234 +
235 + if not username or not email:
236 + redirect (application.get_url ('change_password'))
237 +
238 + user = database.get_user_by_username (username)
239 +
240 + if not user:
241 + redirect (application.get_url ('change_password'))
242 +
243 + # Make sure the given email matches the one that we have in the database
244 + if user['email'] != email:
245 + redirect (application.get_url ('change_password'))
246 +
247 + # Is there another valid token already (from a previous request)?
248 + # If yes, do not send another one (to prevent multiple requests or spam)
249 + if database.is_password_reset_token_valid (user['id']):
250 + redirect (application.get_url ('change_password'))
251 +
252 + # Generate secret token to send via email
253 + secret_token = random.ascii_string (32)
254 +
255 + # Add token to database
256 + database.set_password_reset_token (user['id'], secret_token)
257 +
258 + # Send token via email
259 + client_ip = request.environ.get ('HTTP_X_FORWARDED_FOR') or \
260 + request.environ.get ('REMOTE_ADDR')
261 + email_from = 'freepost <noreply@freepost.peers.community>'
262 + email_to = user['email']
263 + email_subject = 'freepost password reset'
264 + email_body = template (
265 + 'email/password_reset.txt',
266 + ip=client_ip,
267 + secret_token=secret_token)
268 +
269 + mail.send (email_from, email_to, email_subject, email_body)
270 +
271 + redirect (application.get_url ('change_password'))
272 +
273 + @get ('/change_password', name='change_password')
274 + @requires_logout
275 + def change_password ():
276 + return template ('login_change_password.html')
277 +
278 + @post ('/change_password', name='validate_new_password')
279 + @requires_logout
280 + def validate_new_password ():
281 + username = request.forms.get ('username')
282 + email = request.forms.get ('email')
283 + password = request.forms.get ('password')
284 + secret_token = request.forms.get ('token')
285 +
286 + # We must have all fields
287 + if not username or not email or not password or not secret_token:
288 + redirect (application.get_url ('login'))
289 +
290 + # Password too short?
291 + if len (password) < 8:
292 + return template (
293 + 'login_change_password.html',
294 + flash = 'Password must be at least 8 characters long')
295 +
296 + # OK, everything should be fine now. Reset user password.
297 + database.reset_password (username, email, password, secret_token)
298 +
299 + # Check if the password was successfully reset
300 + user = database.check_user_credentials (username, password)
301 +
302 + # Username/Password not working
303 + if not user:
304 + redirect (application.get_url ('login'))
305 +
306 + # Everything matched!
307 + # Notify user of password change.
308 + email_from = 'freepost <noreply@freepost.peers.community>'
309 + email_to = user['email']
310 + email_subject = 'freepost password changed'
311 + email_body = template ('email/password_changed.txt')
312 +
313 + mail.send (email_from, email_to, email_subject, email_body)
314 +
315 + # Start new session and redirect user
316 + session.start (user['id'])
317 + redirect (application.get_url ('user'))
318 +
219 319 @get ('/user', name='user')
220 320 @requires_login
221 321 def user_private_homepage ():

+81/-3 M   freepost/database.py
index 747b490..0923433
old size: 16K - new size: 18K
@@ -117,6 +117,9 @@ def count_unread_messages (user_id):
117 117
118 118 # Retrieve a user
119 119 def get_user_by_username (username):
120 + if not username:
121 + return None
122 +
120 123 cursor = db.cursor (MySQLdb.cursors.DictCursor)
121 124
122 125 cursor.execute (
@@ -621,12 +624,87 @@ def search (query, page = 0, order = 'newest'):
621 624
622 625 return cursor.fetchall ()
623 626
627 + # Set reset token for user email
628 + def set_password_reset_token (user_id = None, token = None):
629 + if not user_id or not token:
630 + return
631 +
632 + cursor = db.cursor (MySQLdb.cursors.DictCursor)
633 +
634 + cursor.execute (
635 + """
636 + UPDATE user
637 + SET passwordResetToken = SHA2(%(token)s, 512),
638 + passwordResetTokenExpire = NOW() + INTERVAL 1 HOUR
639 + WHERE id = %(user)s
640 + """,
641 + {
642 + 'user': user_id,
643 + 'token': token
644 + }
645 + )
624 646
647 + # Delete the password reset token for a user
648 + def delete_password_reset_token (user_id = None):
649 + cursor = db.cursor (MySQLdb.cursors.DictCursor)
650 +
651 + cursor.execute (
652 + """
653 + UPDATE user
654 + SET passwordResetToken = NULL,
655 + passwordResetTokenExpire = NULL
656 + WHERE id = %(user)s
657 + """,
658 + {
659 + 'user': user_id
660 + }
661 + )
625 662
663 + # Check if a reset token has expired.
664 + def is_password_reset_token_valid (user_id = None):
665 + cursor = db.cursor (MySQLdb.cursors.DictCursor)
666 +
667 + cursor.execute (
668 + """
669 + SELECT COUNT(1) AS valid
670 + FROM user
671 + WHERE id = %(user)s AND
672 + passwordResetToken IS NOT NULL AND
673 + passwordResetTokenExpire IS NOT NULL AND
674 + passwordResetTokenExpire > NOW()
675 + """,
676 + {
677 + 'user': user_id
678 + }
679 + )
680 +
681 + return cursor.fetchone ()['valid'] == 1
626 682
627 -
628 -
629 -
683 + # Reset user password
684 + def reset_password (username = None, email = None, new_password = None, secret_token = None):
685 + if not new_password:
686 + return
687 +
688 + cursor = db.cursor (MySQLdb.cursors.DictCursor)
689 +
690 + cursor.execute (
691 + """
692 + UPDATE user
693 + SET password = SHA2(CONCAT(%(password)s, `salt`), 512),
694 + passwordResetToken = NULL,
695 + passwordResetTokenExpire = NULL
696 + WHERE username = %(user)s AND
697 + email = %(email)s AND
698 + passwordResetToken = SHA2(%(token)s, 512) AND
699 + passwordResetTokenExpire > NOW()
700 + """,
701 + {
702 + 'password': new_password,
703 + 'user': username,
704 + 'email': email,
705 + 'token': secret_token
706 + }
707 + )
630 708
631 709
632 710

+15/-0 A   freepost/mail.py
index 0000000..e40e46f
old size: 0B - new size: 575B
new file mode: -rw-r--r--
@@ -0,0 +1,15 @@
1 + from email.mime.multipart import MIMEMultipart
2 + from email.mime.text import MIMEText
3 + from freepost import settings
4 + from subprocess import Popen, PIPE
5 +
6 + def send (from_address, to_address, subject, body):
7 + email_message = MIMEMultipart ()
8 + email_message['From'] = from_address
9 + email_message['To'] = to_address
10 + email_message['Subject'] = subject
11 + email_message.attach (MIMEText (body, 'plain'))
12 +
13 + # Open pipe to sendmail
14 + Popen ([ settings['sendmail']['path'] , "-t" ], stdin=PIPE) \
15 + .communicate (email_message.as_bytes ())

+1/-0 A   freepost/templates/email/password_changed.txt
index 0000000..ef26307
old size: 0B - new size: 32B
new file mode: -rw-r--r--
@@ -0,0 +1 @@
1 + Your password has been changed.

+6/-0 A   freepost/templates/email/password_reset.txt
index 0000000..2aa4f15
old size: 0B - new size: 313B
new file mode: -rw-r--r--
@@ -0,0 +1,6 @@
1 + Somebody from IP:{{ ip }} has requested to reset your freepost password.
2 + The secret code to reset your password is {{ secret_token|safe }}
3 + This code can only be used one time, and will automatically expire in 1 hour.
4 +
5 + If you did not request to change your password, please ignore this message
6 + or contact support.

+3/-3 M   freepost/templates/login.html
index 5bf5db2..19fcb2d
old size: 1K - new size: 1K
@@ -19,14 +19,14 @@
19 19 Screen name
20 20 </div>
21 21 <div>
22 - <input type="text" name="username" class="form-control" />
22 + <input type="text" name="username" class="form-control" required />
23 23 </div>
24 24
25 25 <div class="title">
26 26 Password
27 27 </div>
28 28 <div>
29 - <input type="password" name="password" class="form-control" />
29 + <input type="password" name="password" class="form-control" required />
30 30 </div>
31 31
32 32 <div>
@@ -39,7 +39,7 @@
39 39 </form>
40 40
41 41 <div>
42 - <a href="login_reset">Reset password</a>
42 + <a href="{{ url ('password_reset') }}">Reset password</a>
43 43 </div>
44 44 <div>
45 45 <a href="{{ url ('register') }}">Create new account</a>

+45/-0 A   freepost/templates/login_change_password.html
index 0000000..272e7a4
old size: 0B - new size: 1K
new file mode: -rwxr-xr-x
@@ -0,0 +1,45 @@
1 + {% extends 'layout.html' %}
2 +
3 + {% block content %}
4 +
5 + <div class="login">
6 + {% if flash %}
7 + <div class="alert bg-red">
8 + {{ flash }}
9 + </div>
10 + {% endif %}
11 +
12 + <h3>Change password</h3>
13 +
14 + <p style="margin: 1em 0 2em 0;">
15 + If the username and email are valid, you should receive a new message soon.
16 + Insert the secret code and your new password below.
17 + </p>
18 +
19 + <form action="{{ url ('validate_new_password') }}" method="post">
20 + <input type="hidden" name="token" value="" />
21 + <p>
22 + <input type="text" name="username" placeholder="Username" class="form-control" required />
23 + </p>
24 + <p>
25 + <input type="text" name="email" placeholder="Email" class="form-control" required />
26 + </p>
27 + <p>
28 + <input type="password" name="password" placeholder="New password" class="form-control" required />
29 + </p>
30 + <p>
31 + <input type="password" name="token" placeholder="Secret code" class="form-control" required />
32 + </p>
33 +
34 + <div>
35 + <input type="submit" class="button button_info" value="Change password" />
36 + </div>
37 + </form>
38 +
39 + </div>
40 +
41 + {% endblock %}
42 +
43 +
44 +
45 +

+43/-0 A   freepost/templates/login_reset.html
index 0000000..250331f
old size: 0B - new size: 996B
new file mode: -rwxr-xr-x
@@ -0,0 +1,43 @@
1 + {% extends 'layout.html' %}
2 +
3 + {% block content %}
4 +
5 + <div class="login">
6 + {% if flash %}
7 + <div class="alert bg-red">
8 + {{ flash }}
9 + </div>
10 + {% endif %}
11 +
12 + <h3>Reset password</h3>
13 +
14 + <p>
15 + Please insert your username and email. You will receive a secret
16 + code for resetting your password.
17 + </p>
18 + <p style="margin: 1em 0 2em 0;">
19 + You can only reset your password once per hour.
20 + </p>
21 +
22 + <form action="{{ url ('password_reset_send_code') }}" method="post">
23 + <div>
24 + <p>
25 + <input type="text" name="username" placeholder="Username" class="form-control" required />
26 + </p>
27 + <p>
28 + <input type="text" name="email" placeholder="Email" class="form-control" required />
29 + </p>
30 + </div>
31 +
32 + <div>
33 + <input type="submit" value="Send email" class="button button_info" />
34 + </div>
35 + </form>
36 +
37 + </div>
38 +
39 + {% endblock %}
40 +
41 +
42 +
43 +

+15/-10 M   settings.yaml
index 9115290..b46a8ab
old size: 815B - new size: 941B
@@ -1,21 +1,12 @@
1 1 # This is a bunch of settings useful for the app
2 2
3 3 defaults:
4 + # How many posts to show per page
4 5 items_per_page: 50
5 6 search_results_per_page: 50
6 7 # How many topics to allow for a post
7 8 topics_per_post: 10
8 9
9 - session:
10 - # Name to use for the session cookie
11 - name: freepost
12 -
13 - # Timeout in seconds for the "remember me" option.
14 - # By default, if the user doesn't click "remember me" during login the
15 - # session will end when the browser is closed.
16 - # 2592000 = 30 days
17 - remember_me: 2592000
18 -
19 10 cookies:
20 11 # A secret key for signing cookies. Must be kept private.
21 12 # Used to verify that cookies haven't been tampered with.
@@ -29,3 +20,17 @@ mysql:
29 20 charset: utf8
30 21 username: freepost
31 22 password: freepost
23 +
24 + # Emails are sent using the local sendmail MTA.
25 + sendmail:
26 + path: /usr/sbin/sendmail
27 +
28 + session:
29 + # Name to use for the session cookie
30 + name: freepost
31 +
32 + # Timeout in seconds for the "remember me" option.
33 + # By default, if the user doesn't click "remember me" during login the
34 + # session will end when the browser is closed.
35 + # 2592000 = 30 days
36 + remember_me: 2592000