86d7eaef88104351ce6a01fdbd2d564dcba71254
[mediagoblin.git] / mediagoblin / plugins / basic_auth / views.py
1 # GNU MediaGoblin -- federated, autonomous media hosting
2 # Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
3 #
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License as published by
6 # the Free Software Foundation, either version 3 of the License, or
7 # (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU Affero General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 from itsdangerous import BadSignature
17
18 from mediagoblin import messages
19 from mediagoblin.db.models import LocalUser
20 from mediagoblin.decorators import require_active_login
21 from mediagoblin.plugins.basic_auth import forms, tools
22 from mediagoblin.tools.crypto import get_timed_signer_url
23 from mediagoblin.tools.mail import email_debug_message
24 from mediagoblin.tools.response import redirect, render_to_response, render_404
25 from mediagoblin.tools.translate import pass_to_ugettext as _
26
27
28 def forgot_password(request):
29 """
30 Forgot password view
31
32 Sends an email with an url to renew forgotten password.
33 Use GET querystring parameter 'username' to pre-populate the input field
34 """
35 fp_form = forms.ForgotPassForm(request.form,
36 username=request.args.get('username'))
37
38 if not (request.method == 'POST' and fp_form.validate()):
39 # Either GET request, or invalid form submitted. Display the template
40 return render_to_response(request,
41 'mediagoblin/plugins/basic_auth/forgot_password.html',
42 {'fp_form': fp_form})
43
44 # If we are here: method == POST and form is valid. username casing
45 # has been sanitized. Store if a user was found by email. We should
46 # not reveal if the operation was successful then as we don't want to
47 # leak if an email address exists in the system.
48 found_by_email = '@' in fp_form.username.data
49
50 if found_by_email:
51 user = LocalUser.query.filter_by(
52 email=fp_form.username.data).first()
53 # Don't reveal success in case the lookup happened by email address.
54 success_message = _("If that email address (case sensitive!) is "
55 "registered an email has been sent with "
56 "instructions on how to change your password.")
57
58 else: # found by username
59 user = LocalUser.query.filter_by(
60 username=fp_form.username.data).first()
61
62 if user is None:
63 messages.add_message(request,
64 messages.WARNING,
65 _("Couldn't find someone with that username."))
66 return redirect(request,
67 'mediagoblin.plugins.basic_auth.forgot_password')
68
69 success_message = _("An email has been sent with instructions "
70 "on how to change your password.")
71
72 if user and user.has_privilege(u'active') is False:
73 # Don't send reminder because user is inactive or has no verified email
74 messages.add_message(request,
75 messages.WARNING,
76 _("Could not send password recovery email as your username is in"
77 "active or your account's email address has not been verified."))
78
79 return redirect(request, 'mediagoblin.user_pages.user_home',
80 user=user.username)
81
82 # SUCCESS. Send reminder and return to login page
83 if user:
84 email_debug_message(request)
85 tools.send_fp_verification_email(user, request)
86
87 messages.add_message(request, messages.INFO, success_message)
88 return redirect(request, 'mediagoblin.auth.login')
89
90
91 def verify_forgot_password(request):
92 """
93 Check the forgot-password verification and possibly let the user
94 change their password because of it.
95 """
96 # get form data variables, and specifically check for presence of token
97 formdata = _process_for_token(request)
98 if not formdata['has_token']:
99 return render_404(request)
100
101 formdata_vars = formdata['vars']
102
103 # Catch error if token is faked or expired
104 try:
105 token = get_timed_signer_url("mail_verification_token") \
106 .loads(formdata_vars['token'], max_age=10*24*3600)
107 except BadSignature:
108 messages.add_message(
109 request,
110 messages.ERROR,
111 _('The verification key or user id is incorrect.'))
112
113 return redirect(
114 request,
115 'index')
116
117 # check if it's a valid user id
118 user = LocalUser.query.filter_by(id=int(token)).first()
119
120 # no user in db
121 if not user:
122 messages.add_message(
123 request, messages.ERROR,
124 _('The user id is incorrect.'))
125 return redirect(
126 request, 'index')
127
128 # check if user active and has email verified
129 if user.has_privilege(u'active'):
130 cp_form = forms.ChangeForgotPassForm(formdata_vars)
131
132 if request.method == 'POST' and cp_form.validate():
133 user.pw_hash = tools.bcrypt_gen_password_hash(
134 cp_form.password.data)
135 user.save()
136
137 messages.add_message(
138 request,
139 messages.INFO,
140 _("You can now log in using your new password."))
141 return redirect(request, 'mediagoblin.auth.login')
142 else:
143 return render_to_response(
144 request,
145 'mediagoblin/plugins/basic_auth/change_fp.html',
146 {'cp_form': cp_form})
147
148 ## Commenting this out temporarily because I'm checking into
149 ## what's going on with user.email_verified.
150 ##
151 ## ... if this commit lasts long enough for anyone but me (cwebber) to
152 ## notice it, they should pester me to remove this or remove it
153 ## themselves ;)
154 #
155 # if not user.email_verified:
156 # messages.add_message(
157 # request, messages.ERROR,
158 # _('You need to verify your email before you can reset your'
159 # ' password.'))
160
161 if not user.status == 'active':
162 messages.add_message(
163 request, messages.ERROR,
164 _('You are no longer an active user. Please contact the system'
165 ' admin to reactivate your account.'))
166
167 return redirect(
168 request, 'index')
169
170
171 def _process_for_token(request):
172 """
173 Checks for tokens in formdata without prior knowledge of request method
174
175 For now, returns whether the userid and token formdata variables exist, and
176 the formdata variables in a hash. Perhaps an object is warranted?
177 """
178 # retrieve the formdata variables
179 if request.method == 'GET':
180 formdata_vars = request.GET
181 else:
182 formdata_vars = request.form
183
184 formdata = {
185 'vars': formdata_vars,
186 'has_token': 'token' in formdata_vars}
187
188 return formdata
189
190
191 @require_active_login
192 def change_pass(request):
193 form = forms.ChangePassForm(request.form)
194 user = request.user
195
196 if request.method == 'POST' and form.validate():
197
198 if not tools.bcrypt_check_password(
199 form.old_password.data, user.pw_hash):
200 form.old_password.errors.append(
201 _('Wrong password'))
202
203 return render_to_response(
204 request,
205 'mediagoblin/plugins/basic_auth/change_pass.html',
206 {'form': form,
207 'user': user})
208
209 # Password matches
210 user.pw_hash = tools.bcrypt_gen_password_hash(
211 form.new_password.data)
212 user.save()
213
214 messages.add_message(
215 request, messages.SUCCESS,
216 _('Your password was changed successfully'))
217
218 return redirect(request, 'mediagoblin.edit.account')
219
220 return render_to_response(
221 request,
222 'mediagoblin/plugins/basic_auth/change_pass.html',
223 {'form': form,
224 'user': user})