Merge remote-tracking branch 'JDShu/649_use_form_data_field'
[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
24181820
CAW
28
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
SS
115 if request.method == 'POST':
116 if login_form.validate():
117 user = User.query.filter_by(username=login_form.data['username']).first()
692fd1c9 118
9d140cb8 119 if user and user.check_login(login_form.password.data):
69b56235
SS
120 # set up login in session
121 request.session['user_id'] = unicode(user.id)
122 request.session.save()
692fd1c9 123
69b56235
SS
124 if request.form.get('next'):
125 return redirect(request, location=request.form['next'])
126 else:
127 return redirect(request, "index")
692fd1c9 128
69b56235 129 # Some failure during login occured if we are here!
692fd1c9
CAW
130 # Prevent detecting who's on this system by testing login
131 # attempt timings
132 auth_lib.fake_login_attempt()
a3776717 133 login_failed = True
692fd1c9 134
9038c9f9
CAW
135 return render_to_response(
136 request,
c9c24934
E
137 'mediagoblin/auth/login.html',
138 {'login_form': login_form,
111a609d 139 'next': request.GET.get('next') or request.form.get('next'),
13bb1d67
RL
140 'login_failed': login_failed,
141 'allow_registration': mg_globals.app_config["allow_registration"]})
692fd1c9
CAW
142
143
144def logout(request):
b97232fa
CAW
145 # Maybe deleting the user_id parameter would be enough?
146 request.session.delete()
7b31a11c 147
9150244a 148 return redirect(request, "index")
db1a438f 149
5866d1a8 150
db1a438f 151def verify_email(request):
4c093e85
JW
152 """
153 Email verification view
154
155 validates GET parameters against database and unlocks the user account, if
156 you are lucky :)
157 """
155f24f9 158 # If we don't have userid and token parameters, we can't do anything; 404
285ffedd 159 if not 'userid' in request.GET or not 'token' in request.GET:
de12b4e7 160 return render_404(request)
155f24f9 161
70f8b2d0 162 user = User.query.filter_by(id=request.args['userid']).first()
db1a438f 163
00bb9550 164 if user and user.verification_key == unicode(request.GET['token']):
7a3d00ec 165 user.status = u'active'
4facc7a0 166 user.email_verified = True
00bb9550 167 user.verification_key = None
daf02964 168
db1a438f 169 user.save()
daf02964 170
fe80cb06 171 messages.add_message(
7b31a11c
CAW
172 request,
173 messages.SUCCESS,
4b1adc13
CAW
174 _("Your email address has been verified. "
175 "You may now login, edit your profile, and submit images!"))
db1a438f 176 else:
4b1adc13
CAW
177 messages.add_message(
178 request,
179 messages.ERROR,
180 _('The verification key or user id is incorrect'))
7b31a11c 181
269943a6
CAW
182 return redirect(
183 request, 'mediagoblin.user_pages.user_home',
5a4e3ff1 184 user=user.username)
28afb47c 185
5866d1a8 186
b93a6a22
AM
187def resend_activation(request):
188 """
189 The reactivation view
190
191 Resend the activation email.
192 """
84a7e770 193
2fe69916 194 if request.user is None:
7903a14f
AW
195 messages.add_message(
196 request,
197 messages.ERROR,
2fe69916 198 _('You must be logged in so we know who to send the email to!'))
dfa6994d 199
5dbeda8a 200 return redirect(request, 'mediagoblin.auth.login')
7903a14f 201
0ab21f98 202 if request.user.email_verified:
84a7e770
AW
203 messages.add_message(
204 request,
205 messages.ERROR,
2fe69916 206 _("You've already verified your email address!"))
dfa6994d 207
2fe69916 208 return redirect(request, "mediagoblin.user_pages.user_home", user=request.user['username'])
84a7e770 209
00bb9550 210 request.user.verification_key = unicode(uuid.uuid4())
a77d952a 211 request.user.save()
dfa6994d 212
bf33272f 213 email_debug_message(request)
02d80437 214 send_verification_email(request.user, request)
b93a6a22 215
61927e6e
CAW
216 messages.add_message(
217 request,
218 messages.INFO,
4b1adc13 219 _('Resent your verification email.'))
61927e6e
CAW
220 return redirect(
221 request, 'mediagoblin.user_pages.user_home',
5a4e3ff1 222 user=request.user.username)
25ba955e
AV
223
224
225def forgot_password(request):
226 """
227 Forgot password view
228
a89df961
SS
229 Sends an email with an url to renew forgotten password.
230 Use GET querystring parameter 'username' to pre-populate the input field
25ba955e 231 """
111a609d 232 fp_form = auth_forms.ForgotPassForm(request.form,
a89df961
SS
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.
9d140cb8 244 found_by_email = '@' in fp_form.username.data
a89df961
SS
245
246 if found_by_email:
247 user = User.query.filter_by(
9d140cb8 248 email = fp_form.username.data).first()
a89df961
SS
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(
9d140cb8 256 username = fp_form.username.data).first()
a89df961
SS
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')
25ba955e 263
a89df961
SS
264 success_message=_("An email has been sent with instructions "
265 "on how to change your password.")
bf33272f 266
a89df961
SS
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."))
25ba955e 273
a89df961
SS
274 return redirect(request, 'mediagoblin.user_pages.user_home',
275 user=user.username)
a85a2110 276
a89df961
SS
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()
992e4f80 283
a89df961
SS
284 email_debug_message(request)
285 send_fp_verification_email(user, request)
992e4f80 286
a89df961
SS
287 messages.add_message(request, messages.INFO, success_message)
288 return redirect(request, 'mediagoblin.auth.login')
25ba955e
AV
289
290
291def verify_forgot_password(request):
961fe381
CAW
292 """
293 Check the forgot-password verification and possibly let the user
294 change their password because of it.
295 """
f7ab6670
CAW
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']:
8d1c9863
CFD
299 return render_404(request)
300
f7ab6670
CAW
301 formdata_token = formdata['vars']['token']
302 formdata_userid = formdata['vars']['userid']
303 formdata_vars = formdata['vars']
8d1c9863 304
70f8b2d0
SS
305 # check if it's a valid user id
306 user = User.query.filter_by(id=formdata_userid).first()
307 if not user:
8d1c9863
CFD
308 return render_404(request)
309
310 # check if we have a real user and correct token
dc39e455
E
311 if ((user and user.fp_verification_key and
312 user.fp_verification_key == unicode(formdata_token) and
2d540fed 313 datetime.datetime.now() < user.fp_token_expire
7a3d00ec 314 and user.email_verified and user.status == 'active')):
73fffbb8 315
f7ab6670 316 cp_form = auth_forms.ChangePassForm(formdata_vars)
8d1c9863
CFD
317
318 if request.method == 'POST' and cp_form.validate():
9047b254 319 user.pw_hash = auth_lib.bcrypt_gen_password_hash(
9d140cb8 320 cp_form.password.data)
dc39e455 321 user.fp_verification_key = None
2d540fed 322 user.fp_token_expire = None
8d1c9863
CFD
323 user.save()
324
35149b11
JS
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')
25ba955e 330 else:
73fffbb8
CAW
331 return render_to_response(
332 request,
333 'mediagoblin/auth/change_fp.html',
334 {'cp_form': cp_form})
335
70f8b2d0 336 # in case there is a valid id but no user with that id in the db
8d1c9863
CFD
337 # or the token expired
338 else:
339 return render_404(request)
340
341
342def _process_for_token(request):
343 """
f7ab6670 344 Checks for tokens in formdata without prior knowledge of request method
8d1c9863 345
f7ab6670
CAW
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?
8d1c9863 348 """
f7ab6670 349 # retrieve the formdata variables
8d1c9863 350 if request.method == 'GET':
f7ab6670 351 formdata_vars = request.GET
8d1c9863 352 else:
111a609d 353 formdata_vars = request.form
8d1c9863 354
f7ab6670
CAW
355 formdata = {
356 'vars': formdata_vars,
2c9e8184 357 'has_userid_and_token':
285ffedd 358 'userid' in formdata_vars and 'token' in formdata_vars}
2c9e8184 359
f7ab6670 360 return formdata