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