Merge remote-tracking branch 'refs/remotes/tsyesika/599-allow-email-login'
[mediagoblin.git] / mediagoblin / auth / views.py
... / ...
CommitLineData
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
17import uuid
18import datetime
19
20from mediagoblin import messages, mg_globals
21from mediagoblin.db.models import User
22from mediagoblin.tools.response import render_to_response, redirect, render_404
23from mediagoblin.tools.translate import pass_to_ugettext as _
24from mediagoblin.auth import lib as auth_lib
25from mediagoblin.auth import forms as auth_forms
26from mediagoblin.auth.lib import send_verification_email, \
27 send_fp_verification_email
28from sqlalchemy import or_
29
30def 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
43def 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
105def 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
117 username = login_form.data['username']
118
119 if login_form.validate():
120 user = User.query.filter(
121 or_(
122 User.username == username,
123 User.email == username,
124
125 )).first()
126
127 if user and user.check_login(login_form.password.data):
128 # set up login in session
129 request.session['user_id'] = unicode(user.id)
130 request.session.save()
131
132 if request.form.get('next'):
133 return redirect(request, location=request.form['next'])
134 else:
135 return redirect(request, "index")
136
137 # Some failure during login occured if we are here!
138 # Prevent detecting who's on this system by testing login
139 # attempt timings
140 auth_lib.fake_login_attempt()
141 login_failed = True
142
143 return render_to_response(
144 request,
145 'mediagoblin/auth/login.html',
146 {'login_form': login_form,
147 'next': request.GET.get('next') or request.form.get('next'),
148 'login_failed': login_failed,
149 'allow_registration': mg_globals.app_config["allow_registration"]})
150
151
152def logout(request):
153 # Maybe deleting the user_id parameter would be enough?
154 request.session.delete()
155
156 return redirect(request, "index")
157
158
159def verify_email(request):
160 """
161 Email verification view
162
163 validates GET parameters against database and unlocks the user account, if
164 you are lucky :)
165 """
166 # If we don't have userid and token parameters, we can't do anything; 404
167 if not 'userid' in request.GET or not 'token' in request.GET:
168 return render_404(request)
169
170 user = User.query.filter_by(id=request.args['userid']).first()
171
172 if user and user.verification_key == unicode(request.GET['token']):
173 user.status = u'active'
174 user.email_verified = True
175 user.verification_key = None
176
177 user.save()
178
179 messages.add_message(
180 request,
181 messages.SUCCESS,
182 _("Your email address has been verified. "
183 "You may now login, edit your profile, and submit images!"))
184 else:
185 messages.add_message(
186 request,
187 messages.ERROR,
188 _('The verification key or user id is incorrect'))
189
190 return redirect(
191 request, 'mediagoblin.user_pages.user_home',
192 user=user.username)
193
194
195def resend_activation(request):
196 """
197 The reactivation view
198
199 Resend the activation email.
200 """
201
202 if request.user is None:
203 messages.add_message(
204 request,
205 messages.ERROR,
206 _('You must be logged in so we know who to send the email to!'))
207
208 return redirect(request, 'mediagoblin.auth.login')
209
210 if request.user.email_verified:
211 messages.add_message(
212 request,
213 messages.ERROR,
214 _("You've already verified your email address!"))
215
216 return redirect(request, "mediagoblin.user_pages.user_home", user=request.user['username'])
217
218 request.user.verification_key = unicode(uuid.uuid4())
219 request.user.save()
220
221 email_debug_message(request)
222 send_verification_email(request.user, request)
223
224 messages.add_message(
225 request,
226 messages.INFO,
227 _('Resent your verification email.'))
228 return redirect(
229 request, 'mediagoblin.user_pages.user_home',
230 user=request.user.username)
231
232
233def forgot_password(request):
234 """
235 Forgot password view
236
237 Sends an email with an url to renew forgotten password.
238 Use GET querystring parameter 'username' to pre-populate the input field
239 """
240 fp_form = auth_forms.ForgotPassForm(request.form,
241 username=request.args.get('username'))
242
243 if not (request.method == 'POST' and fp_form.validate()):
244 # Either GET request, or invalid form submitted. Display the template
245 return render_to_response(request,
246 'mediagoblin/auth/forgot_password.html', {'fp_form': fp_form})
247
248 # If we are here: method == POST and form is valid. username casing
249 # has been sanitized. Store if a user was found by email. We should
250 # not reveal if the operation was successful then as we don't want to
251 # leak if an email address exists in the system.
252 found_by_email = '@' in fp_form.username.data
253
254 if found_by_email:
255 user = User.query.filter_by(
256 email = fp_form.username.data).first()
257 # Don't reveal success in case the lookup happened by email address.
258 success_message=_("If that email address (case sensitive!) is "
259 "registered an email has been sent with instructions "
260 "on how to change your password.")
261
262 else: # found by username
263 user = User.query.filter_by(
264 username = fp_form.username.data).first()
265
266 if user is None:
267 messages.add_message(request,
268 messages.WARNING,
269 _("Couldn't find someone with that username."))
270 return redirect(request, 'mediagoblin.auth.forgot_password')
271
272 success_message=_("An email has been sent with instructions "
273 "on how to change your password.")
274
275 if user and not(user.email_verified and user.status == 'active'):
276 # Don't send reminder because user is inactive or has no verified email
277 messages.add_message(request,
278 messages.WARNING,
279 _("Could not send password recovery email as your username is in"
280 "active or your account's email address has not been verified."))
281
282 return redirect(request, 'mediagoblin.user_pages.user_home',
283 user=user.username)
284
285 # SUCCESS. Send reminder and return to login page
286 if user:
287 user.fp_verification_key = unicode(uuid.uuid4())
288 user.fp_token_expire = datetime.datetime.now() + \
289 datetime.timedelta(days=10)
290 user.save()
291
292 email_debug_message(request)
293 send_fp_verification_email(user, request)
294
295 messages.add_message(request, messages.INFO, success_message)
296 return redirect(request, 'mediagoblin.auth.login')
297
298
299def verify_forgot_password(request):
300 """
301 Check the forgot-password verification and possibly let the user
302 change their password because of it.
303 """
304 # get form data variables, and specifically check for presence of token
305 formdata = _process_for_token(request)
306 if not formdata['has_userid_and_token']:
307 return render_404(request)
308
309 formdata_token = formdata['vars']['token']
310 formdata_userid = formdata['vars']['userid']
311 formdata_vars = formdata['vars']
312
313 # check if it's a valid user id
314 user = User.query.filter_by(id=formdata_userid).first()
315 if not user:
316 return render_404(request)
317
318 # check if we have a real user and correct token
319 if ((user and user.fp_verification_key and
320 user.fp_verification_key == unicode(formdata_token) and
321 datetime.datetime.now() < user.fp_token_expire
322 and user.email_verified and user.status == 'active')):
323
324 cp_form = auth_forms.ChangePassForm(formdata_vars)
325
326 if request.method == 'POST' and cp_form.validate():
327 user.pw_hash = auth_lib.bcrypt_gen_password_hash(
328 cp_form.password.data)
329 user.fp_verification_key = None
330 user.fp_token_expire = None
331 user.save()
332
333 messages.add_message(
334 request,
335 messages.INFO,
336 _("You can now log in using your new password."))
337 return redirect(request, 'mediagoblin.auth.login')
338 else:
339 return render_to_response(
340 request,
341 'mediagoblin/auth/change_fp.html',
342 {'cp_form': cp_form})
343
344 # in case there is a valid id but no user with that id in the db
345 # or the token expired
346 else:
347 return render_404(request)
348
349
350def _process_for_token(request):
351 """
352 Checks for tokens in formdata without prior knowledge of request method
353
354 For now, returns whether the userid and token formdata variables exist, and
355 the formdata variables in a hash. Perhaps an object is warranted?
356 """
357 # retrieve the formdata variables
358 if request.method == 'GET':
359 formdata_vars = request.GET
360 else:
361 formdata_vars = request.form
362
363 formdata = {
364 'vars': formdata_vars,
365 'has_userid_and_token':
366 'userid' in formdata_vars and 'token' in formdata_vars}
367
368 return formdata