Merge remote-tracking branch 'JDShu/649_use_form_data_field'
[mediagoblin.git] / mediagoblin / 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
17 import uuid
18 import datetime
19
20 from mediagoblin import messages, mg_globals
21 from mediagoblin.db.models import User
22 from mediagoblin.tools.response import render_to_response, redirect, render_404
23 from mediagoblin.tools.translate import pass_to_ugettext as _
24 from mediagoblin.auth import lib as auth_lib
25 from mediagoblin.auth import forms as auth_forms
26 from mediagoblin.auth.lib import send_verification_email, \
27 send_fp_verification_email
28
29
30 def email_debug_message(request):
31 """
32 If the server is running in email debug mode (which is
33 the current default), give a debug message to the user
34 so that they have an idea where to find their email.
35 """
36 if mg_globals.app_config['email_debug_mode']:
37 # DEBUG message, no need to translate
38 messages.add_message(request, messages.DEBUG,
39 u"This instance is running in email debug mode. "
40 u"The email will be on the console of the server process.")
41
42
43 def register(request):
44 """The registration view.
45
46 Note that usernames will always be lowercased. Email domains are lowercased while
47 the first part remains case-sensitive.
48 """
49 # Redirects to indexpage if registrations are disabled
50 if not mg_globals.app_config["allow_registration"]:
51 messages.add_message(
52 request,
53 messages.WARNING,
54 _('Sorry, registration is disabled on this instance.'))
55 return redirect(request, "index")
56
57 register_form = auth_forms.RegistrationForm(request.form)
58
59 if request.method == 'POST' and register_form.validate():
60 # TODO: Make sure the user doesn't exist already
61 users_with_username = User.query.filter_by(username=register_form.data['username']).count()
62 users_with_email = User.query.filter_by(email=register_form.data['email']).count()
63
64 extra_validation_passes = True
65
66 if users_with_username:
67 register_form.username.errors.append(
68 _(u'Sorry, a user with that name already exists.'))
69 extra_validation_passes = False
70 if users_with_email:
71 register_form.email.errors.append(
72 _(u'Sorry, a user with that email address already exists.'))
73 extra_validation_passes = False
74
75 if extra_validation_passes:
76 # Create the user
77 user = User()
78 user.username = register_form.data['username']
79 user.email = register_form.data['email']
80 user.pw_hash = auth_lib.bcrypt_gen_password_hash(
81 register_form.password.data)
82 user.verification_key = unicode(uuid.uuid4())
83 user.save()
84
85 # log the user in
86 request.session['user_id'] = unicode(user.id)
87 request.session.save()
88
89 # send verification email
90 email_debug_message(request)
91 send_verification_email(user, request)
92
93 # redirect the user to their homepage... there will be a
94 # message waiting for them to verify their email
95 return redirect(
96 request, 'mediagoblin.user_pages.user_home',
97 user=user.username)
98
99 return render_to_response(
100 request,
101 'mediagoblin/auth/register.html',
102 {'register_form': register_form})
103
104
105 def login(request):
106 """
107 MediaGoblin login view.
108
109 If you provide the POST with 'next', it'll redirect to that view.
110 """
111 login_form = auth_forms.LoginForm(request.form)
112
113 login_failed = False
114
115 if request.method == 'POST':
116 if login_form.validate():
117 user = User.query.filter_by(username=login_form.data['username']).first()
118
119 if user and user.check_login(login_form.password.data):
120 # set up login in session
121 request.session['user_id'] = unicode(user.id)
122 request.session.save()
123
124 if request.form.get('next'):
125 return redirect(request, location=request.form['next'])
126 else:
127 return redirect(request, "index")
128
129 # Some failure during login occured if we are here!
130 # Prevent detecting who's on this system by testing login
131 # attempt timings
132 auth_lib.fake_login_attempt()
133 login_failed = True
134
135 return render_to_response(
136 request,
137 'mediagoblin/auth/login.html',
138 {'login_form': login_form,
139 'next': request.GET.get('next') or request.form.get('next'),
140 'login_failed': login_failed,
141 'allow_registration': mg_globals.app_config["allow_registration"]})
142
143
144 def logout(request):
145 # Maybe deleting the user_id parameter would be enough?
146 request.session.delete()
147
148 return redirect(request, "index")
149
150
151 def verify_email(request):
152 """
153 Email verification view
154
155 validates GET parameters against database and unlocks the user account, if
156 you are lucky :)
157 """
158 # If we don't have userid and token parameters, we can't do anything; 404
159 if not 'userid' in request.GET or not 'token' in request.GET:
160 return render_404(request)
161
162 user = User.query.filter_by(id=request.args['userid']).first()
163
164 if user and user.verification_key == unicode(request.GET['token']):
165 user.status = u'active'
166 user.email_verified = True
167 user.verification_key = None
168
169 user.save()
170
171 messages.add_message(
172 request,
173 messages.SUCCESS,
174 _("Your email address has been verified. "
175 "You may now login, edit your profile, and submit images!"))
176 else:
177 messages.add_message(
178 request,
179 messages.ERROR,
180 _('The verification key or user id is incorrect'))
181
182 return redirect(
183 request, 'mediagoblin.user_pages.user_home',
184 user=user.username)
185
186
187 def resend_activation(request):
188 """
189 The reactivation view
190
191 Resend the activation email.
192 """
193
194 if request.user is None:
195 messages.add_message(
196 request,
197 messages.ERROR,
198 _('You must be logged in so we know who to send the email to!'))
199
200 return redirect(request, 'mediagoblin.auth.login')
201
202 if request.user.email_verified:
203 messages.add_message(
204 request,
205 messages.ERROR,
206 _("You've already verified your email address!"))
207
208 return redirect(request, "mediagoblin.user_pages.user_home", user=request.user['username'])
209
210 request.user.verification_key = unicode(uuid.uuid4())
211 request.user.save()
212
213 email_debug_message(request)
214 send_verification_email(request.user, request)
215
216 messages.add_message(
217 request,
218 messages.INFO,
219 _('Resent your verification email.'))
220 return redirect(
221 request, 'mediagoblin.user_pages.user_home',
222 user=request.user.username)
223
224
225 def forgot_password(request):
226 """
227 Forgot password view
228
229 Sends an email with an url to renew forgotten password.
230 Use GET querystring parameter 'username' to pre-populate the input field
231 """
232 fp_form = auth_forms.ForgotPassForm(request.form,
233 username=request.args.get('username'))
234
235 if not (request.method == 'POST' and fp_form.validate()):
236 # Either GET request, or invalid form submitted. Display the template
237 return render_to_response(request,
238 'mediagoblin/auth/forgot_password.html', {'fp_form': fp_form})
239
240 # If we are here: method == POST and form is valid. username casing
241 # has been sanitized. Store if a user was found by email. We should
242 # not reveal if the operation was successful then as we don't want to
243 # leak if an email address exists in the system.
244 found_by_email = '@' in fp_form.username.data
245
246 if found_by_email:
247 user = User.query.filter_by(
248 email = fp_form.username.data).first()
249 # Don't reveal success in case the lookup happened by email address.
250 success_message=_("If that email address (case sensitive!) is "
251 "registered an email has been sent with instructions "
252 "on how to change your password.")
253
254 else: # found by username
255 user = User.query.filter_by(
256 username = fp_form.username.data).first()
257
258 if user is None:
259 messages.add_message(request,
260 messages.WARNING,
261 _("Couldn't find someone with that username."))
262 return redirect(request, 'mediagoblin.auth.forgot_password')
263
264 success_message=_("An email has been sent with instructions "
265 "on how to change your password.")
266
267 if user and not(user.email_verified and user.status == 'active'):
268 # Don't send reminder because user is inactive or has no verified email
269 messages.add_message(request,
270 messages.WARNING,
271 _("Could not send password recovery email as your username is in"
272 "active or your account's email address has not been verified."))
273
274 return redirect(request, 'mediagoblin.user_pages.user_home',
275 user=user.username)
276
277 # SUCCESS. Send reminder and return to login page
278 if user:
279 user.fp_verification_key = unicode(uuid.uuid4())
280 user.fp_token_expire = datetime.datetime.now() + \
281 datetime.timedelta(days=10)
282 user.save()
283
284 email_debug_message(request)
285 send_fp_verification_email(user, request)
286
287 messages.add_message(request, messages.INFO, success_message)
288 return redirect(request, 'mediagoblin.auth.login')
289
290
291 def verify_forgot_password(request):
292 """
293 Check the forgot-password verification and possibly let the user
294 change their password because of it.
295 """
296 # get form data variables, and specifically check for presence of token
297 formdata = _process_for_token(request)
298 if not formdata['has_userid_and_token']:
299 return render_404(request)
300
301 formdata_token = formdata['vars']['token']
302 formdata_userid = formdata['vars']['userid']
303 formdata_vars = formdata['vars']
304
305 # check if it's a valid user id
306 user = User.query.filter_by(id=formdata_userid).first()
307 if not user:
308 return render_404(request)
309
310 # check if we have a real user and correct token
311 if ((user and user.fp_verification_key and
312 user.fp_verification_key == unicode(formdata_token) and
313 datetime.datetime.now() < user.fp_token_expire
314 and user.email_verified and user.status == 'active')):
315
316 cp_form = auth_forms.ChangePassForm(formdata_vars)
317
318 if request.method == 'POST' and cp_form.validate():
319 user.pw_hash = auth_lib.bcrypt_gen_password_hash(
320 cp_form.password.data)
321 user.fp_verification_key = None
322 user.fp_token_expire = None
323 user.save()
324
325 messages.add_message(
326 request,
327 messages.INFO,
328 _("You can now log in using your new password."))
329 return redirect(request, 'mediagoblin.auth.login')
330 else:
331 return render_to_response(
332 request,
333 'mediagoblin/auth/change_fp.html',
334 {'cp_form': cp_form})
335
336 # in case there is a valid id but no user with that id in the db
337 # or the token expired
338 else:
339 return render_404(request)
340
341
342 def _process_for_token(request):
343 """
344 Checks for tokens in formdata without prior knowledge of request method
345
346 For now, returns whether the userid and token formdata variables exist, and
347 the formdata variables in a hash. Perhaps an object is warranted?
348 """
349 # retrieve the formdata variables
350 if request.method == 'GET':
351 formdata_vars = request.GET
352 else:
353 formdata_vars = request.form
354
355 formdata = {
356 'vars': formdata_vars,
357 'has_userid_and_token':
358 'userid' in formdata_vars and 'token' in formdata_vars}
359
360 return formdata