Merge remote branch 'remotes/nyergler/569-application-middleware'
[mediagoblin.git] / mediagoblin / auth / views.py
1 # GNU MediaGoblin -- federated, autonomous media hosting
2 # Copyright (C) 2011 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.util import render_to_response, redirect, render_404
25 from mediagoblin.util 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, that email address has already been taken.'))
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.save(validate=True)
88
89 # log the user in
90 request.session['user_id'] = unicode(user['_id'])
91 request.session.save()
92
93 # send verification email
94 email_debug_message(request)
95 send_verification_email(user, request)
96
97 # redirect the user to their homepage... there will be a
98 # message waiting for them to verify their email
99 return redirect(
100 request, 'mediagoblin.user_pages.user_home',
101 user=user['username'])
102
103 return render_to_response(
104 request,
105 'mediagoblin/auth/register.html',
106 {'register_form': register_form})
107
108
109 def login(request):
110 """
111 MediaGoblin login view.
112
113 If you provide the POST with 'next', it'll redirect to that view.
114 """
115 login_form = auth_forms.LoginForm(request.POST)
116
117 login_failed = False
118
119 if request.method == 'POST' and login_form.validate():
120 user = request.db.User.one(
121 {'username': request.POST['username'].lower()})
122
123 if user and user.check_login(request.POST['password']):
124 # set up login in session
125 request.session['user_id'] = unicode(user['_id'])
126 request.session.save()
127
128 if request.POST.get('next'):
129 return exc.HTTPFound(location=request.POST['next'])
130 else:
131 return redirect(request, "index")
132
133 else:
134 # Prevent detecting who's on this system by testing login
135 # attempt timings
136 auth_lib.fake_login_attempt()
137 login_failed = True
138
139 return render_to_response(
140 request,
141 'mediagoblin/auth/login.html',
142 {'login_form': login_form,
143 'next': request.GET.get('next') or request.POST.get('next'),
144 'login_failed': login_failed,
145 'allow_registration': mg_globals.app_config["allow_registration"]})
146
147
148 def logout(request):
149 # Maybe deleting the user_id parameter would be enough?
150 request.session.delete()
151
152 return redirect(request, "index")
153
154
155 def verify_email(request):
156 """
157 Email verification view
158
159 validates GET parameters against database and unlocks the user account, if
160 you are lucky :)
161 """
162 # If we don't have userid and token parameters, we can't do anything; 404
163 if not request.GET.has_key('userid') or not request.GET.has_key('token'):
164 return render_404(request)
165
166 user = request.db.User.find_one(
167 {'_id': ObjectId(unicode(request.GET['userid']))})
168
169 if user and user['verification_key'] == unicode(request.GET['token']):
170 user[u'status'] = u'active'
171 user[u'email_verified'] = True
172 user[u'verification_key'] = None
173
174 user.save()
175
176 messages.add_message(
177 request,
178 messages.SUCCESS,
179 _("Your email address has been verified. "
180 "You may now login, edit your profile, and submit images!"))
181 else:
182 messages.add_message(
183 request,
184 messages.ERROR,
185 _('The verification key or user id is incorrect'))
186
187 return redirect(
188 request, 'mediagoblin.user_pages.user_home',
189 user=user['username'])
190
191
192 def resend_activation(request):
193 """
194 The reactivation view
195
196 Resend the activation email.
197 """
198 request.user[u'verification_key'] = unicode(uuid.uuid4())
199 request.user.save()
200
201 email_debug_message(request)
202 send_verification_email(request.user, request)
203
204 messages.add_message(
205 request,
206 messages.INFO,
207 _('Resent your verification email.'))
208 return redirect(
209 request, 'mediagoblin.user_pages.user_home',
210 user=request.user['username'])
211
212
213 def forgot_password(request):
214 """
215 Forgot password view
216
217 Sends an email whit an url to renew forgoten password
218 """
219 fp_form = auth_forms.ForgotPassForm(request.POST)
220
221 if request.method == 'POST' and fp_form.validate():
222
223 # Here, so it doesn't depend on the actual mail being sent
224 # and thus doesn't reveal, wether mail was sent.
225 email_debug_message(request)
226
227 # '$or' not available till mongodb 1.5.3
228 user = request.db.User.find_one(
229 {'username': request.POST['username']})
230 if not user:
231 user = request.db.User.find_one(
232 {'email': request.POST['username']})
233
234 if user:
235 if user['email_verified'] and user['status'] == 'active':
236 user[u'fp_verification_key'] = unicode(uuid.uuid4())
237 user[u'fp_token_expire'] = datetime.datetime.now() + \
238 datetime.timedelta(days=10)
239 user.save()
240
241 send_fp_verification_email(user, request)
242 else:
243 # special case... we can't send the email because the
244 # username is inactive / hasn't verified their email
245 messages.add_message(
246 request,
247 messages.WARNING,
248 _("Could not send password recovery email as "
249 "your username is inactive or your account's "
250 "email address has not been verified."))
251
252 return redirect(
253 request, 'mediagoblin.user_pages.user_home',
254 user=user['username'])
255
256
257 # do not reveal whether or not there is a matching user, just move along
258 return redirect(request, 'mediagoblin.auth.fp_email_sent')
259
260 return render_to_response(
261 request,
262 'mediagoblin/auth/forgot_password.html',
263 {'fp_form': fp_form})
264
265
266 def verify_forgot_password(request):
267 """
268 Check the forgot-password verification and possibly let the user
269 change their password because of it.
270 """
271 # get form data variables, and specifically check for presence of token
272 formdata = _process_for_token(request)
273 if not formdata['has_userid_and_token']:
274 return render_404(request)
275
276 formdata_token = formdata['vars']['token']
277 formdata_userid = formdata['vars']['userid']
278 formdata_vars = formdata['vars']
279
280 # check if it's a valid Id
281 try:
282 user = request.db.User.find_one(
283 {'_id': ObjectId(unicode(formdata_userid))})
284 except InvalidId:
285 return render_404(request)
286
287 # check if we have a real user and correct token
288 if ((user and user['fp_verification_key'] and
289 user['fp_verification_key'] == unicode(formdata_token) and
290 datetime.datetime.now() < user['fp_token_expire']
291 and user['email_verified'] and user['status'] == 'active')):
292
293 cp_form = auth_forms.ChangePassForm(formdata_vars)
294
295 if request.method == 'POST' and cp_form.validate():
296 user[u'pw_hash'] = auth_lib.bcrypt_gen_password_hash(
297 request.POST['password'])
298 user[u'fp_verification_key'] = None
299 user[u'fp_token_expire'] = None
300 user.save()
301
302 return redirect(request, 'mediagoblin.auth.fp_changed_success')
303 else:
304 return render_to_response(
305 request,
306 'mediagoblin/auth/change_fp.html',
307 {'cp_form': cp_form})
308
309 # in case there is a valid id but no user whit that id in the db
310 # or the token expired
311 else:
312 return render_404(request)
313
314
315 def _process_for_token(request):
316 """
317 Checks for tokens in formdata without prior knowledge of request method
318
319 For now, returns whether the userid and token formdata variables exist, and
320 the formdata variables in a hash. Perhaps an object is warranted?
321 """
322 # retrieve the formdata variables
323 if request.method == 'GET':
324 formdata_vars = request.GET
325 else:
326 formdata_vars = request.POST
327
328 formdata = {
329 'vars': formdata_vars,
330 'has_userid_and_token':
331 formdata_vars.has_key('userid') and formdata_vars.has_key('token')}
332
333 return formdata