Merge remote-tracking branch 'refs/remotes/tsyesika/599-allow-email-login'
[mediagoblin.git] / mediagoblin / auth / views.py
CommitLineData
8e1e744d 1# GNU MediaGoblin -- federated, autonomous media hosting
cf29e8a8 2# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
24181820
CAW
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
a77d952a 17import uuid
25ba955e 18import datetime
a77d952a 19
70f8b2d0 20from mediagoblin import messages, mg_globals
b0c8328e 21from mediagoblin.db.models import User
152a3bfa 22from mediagoblin.tools.response import render_to_response, redirect, render_404
ae3bc7fa 23from mediagoblin.tools.translate import pass_to_ugettext as _
24181820
CAW
24from mediagoblin.auth import lib as auth_lib
25from mediagoblin.auth import forms as auth_forms
25ba955e
AV
26from mediagoblin.auth.lib import send_verification_email, \
27 send_fp_verification_email
b2c8dbcf 28from sqlalchemy import or_
24181820 29
bf33272f
E
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
24181820 43def register(request):
a89df961
SS
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.
24181820 48 """
13677ef9
RL
49 # Redirects to indexpage if registrations are disabled
50 if not mg_globals.app_config["allow_registration"]:
166dc91a
CAW
51 messages.add_message(
52 request,
53 messages.WARNING,
4b1adc13 54 _('Sorry, registration is disabled on this instance.'))
13677ef9
RL
55 return redirect(request, "index")
56
111a609d 57 register_form = auth_forms.RegistrationForm(request.form)
24181820
CAW
58
59 if request.method == 'POST' and register_form.validate():
60 # TODO: Make sure the user doesn't exist already
a89df961
SS
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()
24181820 63
9f6ea475
CAW
64 extra_validation_passes = True
65
24181820
CAW
66 if users_with_username:
67 register_form.username.errors.append(
4b1adc13 68 _(u'Sorry, a user with that name already exists.'))
9f6ea475
CAW
69 extra_validation_passes = False
70 if users_with_email:
0bf099d7 71 register_form.email.errors.append(
5ab3855e 72 _(u'Sorry, a user with that email address already exists.'))
9f6ea475 73 extra_validation_passes = False
24181820 74
9f6ea475 75 if extra_validation_passes:
24181820 76 # Create the user
70f8b2d0 77 user = User()
a89df961
SS
78 user.username = register_form.data['username']
79 user.email = register_form.data['email']
9047b254 80 user.pw_hash = auth_lib.bcrypt_gen_password_hash(
9d140cb8 81 register_form.password.data)
479e8a83 82 user.verification_key = unicode(uuid.uuid4())
b39d1f23 83 user.save()
0bc03620 84
f73f4c4b 85 # log the user in
5c2b8486 86 request.session['user_id'] = unicode(user.id)
f73f4c4b
CAW
87 request.session.save()
88
89 # send verification email
bf33272f 90 email_debug_message(request)
0bc03620
CAW
91 send_verification_email(user, request)
92
dce5c9cb
CAW
93 # redirect the user to their homepage... there will be a
94 # message waiting for them to verify their email
0bc03620
CAW
95 return redirect(
96 request, 'mediagoblin.user_pages.user_home',
5a4e3ff1 97 user=user.username)
24181820 98
9038c9f9
CAW
99 return render_to_response(
100 request,
c9c24934
E
101 'mediagoblin/auth/register.html',
102 {'register_form': register_form})
24181820
CAW
103
104
692fd1c9 105def login(request):
a3776717 106 """
8e1e744d 107 MediaGoblin login view.
a3776717
CAW
108
109 If you provide the POST with 'next', it'll redirect to that view.
110 """
111a609d 111 login_form = auth_forms.LoginForm(request.form)
692fd1c9 112
a3776717
CAW
113 login_failed = False
114
69b56235 115 if request.method == 'POST':
b2c8dbcf
J
116
117 username = login_form.data['username']
118
69b56235 119 if login_form.validate():
b2c8dbcf
J
120 user = User.query.filter(
121 or_(
122 User.username == username,
123 User.email == username,
124
125 )).first()
692fd1c9 126
9d140cb8 127 if user and user.check_login(login_form.password.data):
69b56235
SS
128 # set up login in session
129 request.session['user_id'] = unicode(user.id)
130 request.session.save()
692fd1c9 131
69b56235
SS
132 if request.form.get('next'):
133 return redirect(request, location=request.form['next'])
134 else:
135 return redirect(request, "index")
692fd1c9 136
69b56235 137 # Some failure during login occured if we are here!
692fd1c9
CAW
138 # Prevent detecting who's on this system by testing login
139 # attempt timings
140 auth_lib.fake_login_attempt()
a3776717 141 login_failed = True
692fd1c9 142
9038c9f9
CAW
143 return render_to_response(
144 request,
c9c24934
E
145 'mediagoblin/auth/login.html',
146 {'login_form': login_form,
111a609d 147 'next': request.GET.get('next') or request.form.get('next'),
13bb1d67
RL
148 'login_failed': login_failed,
149 'allow_registration': mg_globals.app_config["allow_registration"]})
692fd1c9
CAW
150
151
152def logout(request):
b97232fa
CAW
153 # Maybe deleting the user_id parameter would be enough?
154 request.session.delete()
7b31a11c 155
9150244a 156 return redirect(request, "index")
db1a438f 157
5866d1a8 158
db1a438f 159def verify_email(request):
4c093e85
JW
160 """
161 Email verification view
162
163 validates GET parameters against database and unlocks the user account, if
164 you are lucky :)
165 """
155f24f9 166 # If we don't have userid and token parameters, we can't do anything; 404
285ffedd 167 if not 'userid' in request.GET or not 'token' in request.GET:
de12b4e7 168 return render_404(request)
155f24f9 169
70f8b2d0 170 user = User.query.filter_by(id=request.args['userid']).first()
db1a438f 171
00bb9550 172 if user and user.verification_key == unicode(request.GET['token']):
7a3d00ec 173 user.status = u'active'
4facc7a0 174 user.email_verified = True
00bb9550 175 user.verification_key = None
daf02964 176
db1a438f 177 user.save()
daf02964 178
fe80cb06 179 messages.add_message(
7b31a11c
CAW
180 request,
181 messages.SUCCESS,
4b1adc13
CAW
182 _("Your email address has been verified. "
183 "You may now login, edit your profile, and submit images!"))
db1a438f 184 else:
4b1adc13
CAW
185 messages.add_message(
186 request,
187 messages.ERROR,
188 _('The verification key or user id is incorrect'))
7b31a11c 189
269943a6
CAW
190 return redirect(
191 request, 'mediagoblin.user_pages.user_home',
5a4e3ff1 192 user=user.username)
28afb47c 193
5866d1a8 194
b93a6a22
AM
195def resend_activation(request):
196 """
197 The reactivation view
198
199 Resend the activation email.
200 """
84a7e770 201
2fe69916 202 if request.user is None:
7903a14f
AW
203 messages.add_message(
204 request,
205 messages.ERROR,
2fe69916 206 _('You must be logged in so we know who to send the email to!'))
dfa6994d 207
5dbeda8a 208 return redirect(request, 'mediagoblin.auth.login')
7903a14f 209
0ab21f98 210 if request.user.email_verified:
84a7e770
AW
211 messages.add_message(
212 request,
213 messages.ERROR,
2fe69916 214 _("You've already verified your email address!"))
dfa6994d 215
2fe69916 216 return redirect(request, "mediagoblin.user_pages.user_home", user=request.user['username'])
84a7e770 217
00bb9550 218 request.user.verification_key = unicode(uuid.uuid4())
a77d952a 219 request.user.save()
dfa6994d 220
bf33272f 221 email_debug_message(request)
02d80437 222 send_verification_email(request.user, request)
b93a6a22 223
61927e6e
CAW
224 messages.add_message(
225 request,
226 messages.INFO,
4b1adc13 227 _('Resent your verification email.'))
61927e6e
CAW
228 return redirect(
229 request, 'mediagoblin.user_pages.user_home',
5a4e3ff1 230 user=request.user.username)
25ba955e
AV
231
232
233def forgot_password(request):
234 """
235 Forgot password view
236
a89df961
SS
237 Sends an email with an url to renew forgotten password.
238 Use GET querystring parameter 'username' to pre-populate the input field
25ba955e 239 """
111a609d 240 fp_form = auth_forms.ForgotPassForm(request.form,
a89df961
SS
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.
9d140cb8 252 found_by_email = '@' in fp_form.username.data
a89df961
SS
253
254 if found_by_email:
255 user = User.query.filter_by(
9d140cb8 256 email = fp_form.username.data).first()
a89df961
SS
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(
9d140cb8 264 username = fp_form.username.data).first()
a89df961
SS
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')
25ba955e 271
a89df961
SS
272 success_message=_("An email has been sent with instructions "
273 "on how to change your password.")
bf33272f 274
a89df961
SS
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."))
25ba955e 281
a89df961
SS
282 return redirect(request, 'mediagoblin.user_pages.user_home',
283 user=user.username)
a85a2110 284
a89df961
SS
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()
992e4f80 291
a89df961
SS
292 email_debug_message(request)
293 send_fp_verification_email(user, request)
992e4f80 294
a89df961
SS
295 messages.add_message(request, messages.INFO, success_message)
296 return redirect(request, 'mediagoblin.auth.login')
25ba955e
AV
297
298
299def verify_forgot_password(request):
961fe381
CAW
300 """
301 Check the forgot-password verification and possibly let the user
302 change their password because of it.
303 """
f7ab6670
CAW
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']:
8d1c9863
CFD
307 return render_404(request)
308
f7ab6670
CAW
309 formdata_token = formdata['vars']['token']
310 formdata_userid = formdata['vars']['userid']
311 formdata_vars = formdata['vars']
8d1c9863 312
70f8b2d0
SS
313 # check if it's a valid user id
314 user = User.query.filter_by(id=formdata_userid).first()
315 if not user:
8d1c9863
CFD
316 return render_404(request)
317
318 # check if we have a real user and correct token
dc39e455
E
319 if ((user and user.fp_verification_key and
320 user.fp_verification_key == unicode(formdata_token) and
2d540fed 321 datetime.datetime.now() < user.fp_token_expire
7a3d00ec 322 and user.email_verified and user.status == 'active')):
73fffbb8 323
f7ab6670 324 cp_form = auth_forms.ChangePassForm(formdata_vars)
8d1c9863
CFD
325
326 if request.method == 'POST' and cp_form.validate():
9047b254 327 user.pw_hash = auth_lib.bcrypt_gen_password_hash(
9d140cb8 328 cp_form.password.data)
dc39e455 329 user.fp_verification_key = None
2d540fed 330 user.fp_token_expire = None
8d1c9863
CFD
331 user.save()
332
35149b11
JS
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')
25ba955e 338 else:
73fffbb8
CAW
339 return render_to_response(
340 request,
341 'mediagoblin/auth/change_fp.html',
342 {'cp_form': cp_form})
343
70f8b2d0 344 # in case there is a valid id but no user with that id in the db
8d1c9863
CFD
345 # or the token expired
346 else:
347 return render_404(request)
348
349
350def _process_for_token(request):
351 """
f7ab6670 352 Checks for tokens in formdata without prior knowledge of request method
8d1c9863 353
f7ab6670
CAW
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?
8d1c9863 356 """
f7ab6670 357 # retrieve the formdata variables
8d1c9863 358 if request.method == 'GET':
f7ab6670 359 formdata_vars = request.GET
8d1c9863 360 else:
111a609d 361 formdata_vars = request.form
8d1c9863 362
f7ab6670
CAW
363 formdata = {
364 'vars': formdata_vars,
2c9e8184 365 'has_userid_and_token':
285ffedd 366 'userid' in formdata_vars and 'token' in formdata_vars}
2c9e8184 367
f7ab6670 368 return formdata