fa84bbb178f36520849efd689491c8f59b280df4
[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 mediagoblin import messages, mg_globals
21 from mediagoblin.db.models import User
22 from mediagoblin.tools.response import render_to_response, redirect, render_404
23 from mediagoblin.tools.translate import pass_to_ugettext as _
24 from mediagoblin.tools.mail import email_debug_message
25 from mediagoblin.auth import forms as auth_forms
26 from mediagoblin.auth.tools import (send_verification_email, register_user,
27 send_fp_verification_email,
28 check_login_simple)
29 from mediagoblin import auth
30
31
32 def register(request):
33 """The registration view.
34
35 Note that usernames will always be lowercased. Email domains are lowercased while
36 the first part remains case-sensitive.
37 """
38 # Redirects to indexpage if registrations are disabled or no authentication
39 # is enabled
40 if not mg_globals.app_config["allow_registration"] or not mg_globals.app.auth:
41 messages.add_message(
42 request,
43 messages.WARNING,
44 _('Sorry, registration is disabled on this instance.'))
45 return redirect(request, "index")
46
47 if 'pass_auth' not in request.template_env.globals:
48 if 'openid' in request.template_env.globals:
49 return redirect(request, 'mediagoblin.plugins.openid.register')
50
51 register_form = auth.get_registration_form(request)
52
53 if request.method == 'POST' and register_form.validate():
54 # TODO: Make sure the user doesn't exist already
55 user = register_user(request, register_form)
56
57 if user:
58 # redirect the user to their homepage... there will be a
59 # message waiting for them to verify their email
60 return redirect(
61 request, 'mediagoblin.user_pages.user_home',
62 user=user.username)
63
64 return render_to_response(
65 request,
66 'mediagoblin/auth/register.html',
67 {'register_form': register_form,
68 'focus': 'username',
69 'post_url': request.urlgen('mediagoblin.auth.register')})
70
71
72 def login(request):
73 """
74 MediaGoblin login view.
75
76 If you provide the POST with 'next', it'll redirect to that view.
77 """
78 # Redirects to index page if no authentication is enabled
79 if not mg_globals.app.auth:
80 messages.add_message(
81 request,
82 messages.WARNING,
83 _('Sorry, authentication is disabled on this instance.'))
84 return redirect(request, 'index')
85
86 if 'pass_auth' not in request.template_env.globals:
87 if 'openid' in request.template_env.globals:
88 return redirect(request, 'mediagoblin.plugins.openid.login')
89
90 login_form = auth.get_login_form(request)
91
92 login_failed = False
93
94 if request.method == 'POST':
95 username = login_form.username.data
96
97 if login_form.validate():
98 user = check_login_simple(username, login_form.password.data)
99
100 username = login_form.data['username']
101
102 if login_form.validate():
103 user = check_login_simple(username, login_form.password.data, True)
104
105 if user:
106 # set up login in session
107 request.session['user_id'] = unicode(user.id)
108 request.session.save()
109
110 if request.form.get('next'):
111 return redirect(request, location=request.form['next'])
112 else:
113 return redirect(request, "index")
114
115 login_failed = True
116
117 return render_to_response(
118 request,
119 'mediagoblin/auth/login.html',
120 {'login_form': login_form,
121 'next': request.GET.get('next') or request.form.get('next'),
122 'login_failed': login_failed,
123 'focus': 'username',
124 'post_url': request.urlgen('mediagoblin.auth.login'),
125 'allow_registration': mg_globals.app_config["allow_registration"]})
126
127
128 def logout(request):
129 # Maybe deleting the user_id parameter would be enough?
130 request.session.delete()
131
132 return redirect(request, "index")
133
134
135 def verify_email(request):
136 """
137 Email verification view
138
139 validates GET parameters against database and unlocks the user account, if
140 you are lucky :)
141 """
142 # If we don't have userid and token parameters, we can't do anything; 404
143 if not 'userid' in request.GET or not 'token' in request.GET:
144 return render_404(request)
145
146 user = User.query.filter_by(id=request.args['userid']).first()
147
148 if user and user.verification_key == unicode(request.GET['token']):
149 user.status = u'active'
150 user.email_verified = True
151 user.verification_key = None
152
153 user.save()
154
155 messages.add_message(
156 request,
157 messages.SUCCESS,
158 _("Your email address has been verified. "
159 "You may now login, edit your profile, and submit images!"))
160 else:
161 messages.add_message(
162 request,
163 messages.ERROR,
164 _('The verification key or user id is incorrect'))
165
166 return redirect(
167 request, 'mediagoblin.user_pages.user_home',
168 user=user.username)
169
170
171 def resend_activation(request):
172 """
173 The reactivation view
174
175 Resend the activation email.
176 """
177
178 if request.user is None:
179 messages.add_message(
180 request,
181 messages.ERROR,
182 _('You must be logged in so we know who to send the email to!'))
183
184 return redirect(request, 'mediagoblin.auth.login')
185
186 if request.user.email_verified:
187 messages.add_message(
188 request,
189 messages.ERROR,
190 _("You've already verified your email address!"))
191
192 return redirect(request, "mediagoblin.user_pages.user_home", user=request.user['username'])
193
194 request.user.verification_key = unicode(uuid.uuid4())
195 request.user.save()
196
197 email_debug_message(request)
198 send_verification_email(request.user, request)
199
200 messages.add_message(
201 request,
202 messages.INFO,
203 _('Resent your verification email.'))
204 return redirect(
205 request, 'mediagoblin.user_pages.user_home',
206 user=request.user.username)
207
208
209 def forgot_password(request):
210 """
211 Forgot password view
212
213 Sends an email with an url to renew forgotten password.
214 Use GET querystring parameter 'username' to pre-populate the input field
215 """
216 if not 'pass_auth' in request.template_env.globals:
217 return redirect(request, 'index')
218
219 fp_form = auth_forms.ForgotPassForm(request.form,
220 username=request.args.get('username'))
221
222 if not (request.method == 'POST' and fp_form.validate()):
223 # Either GET request, or invalid form submitted. Display the template
224 return render_to_response(request,
225 'mediagoblin/auth/forgot_password.html', {'fp_form': fp_form,
226 'focus': 'username'})
227
228 # If we are here: method == POST and form is valid. username casing
229 # has been sanitized. Store if a user was found by email. We should
230 # not reveal if the operation was successful then as we don't want to
231 # leak if an email address exists in the system.
232 found_by_email = '@' in fp_form.username.data
233
234 if found_by_email:
235 user = User.query.filter_by(
236 email = fp_form.username.data).first()
237 # Don't reveal success in case the lookup happened by email address.
238 success_message=_("If that email address (case sensitive!) is "
239 "registered an email has been sent with instructions "
240 "on how to change your password.")
241
242 else: # found by username
243 user = User.query.filter_by(
244 username = fp_form.username.data).first()
245
246 if user is None:
247 messages.add_message(request,
248 messages.WARNING,
249 _("Couldn't find someone with that username."))
250 return redirect(request, 'mediagoblin.auth.forgot_password')
251
252 success_message=_("An email has been sent with instructions "
253 "on how to change your password.")
254
255 if user and not(user.email_verified and user.status == 'active'):
256 # Don't send reminder because user is inactive or has no verified email
257 messages.add_message(request,
258 messages.WARNING,
259 _("Could not send password recovery email as your username is in"
260 "active or your account's email address has not been verified."))
261
262 return redirect(request, 'mediagoblin.user_pages.user_home',
263 user=user.username)
264
265 # SUCCESS. Send reminder and return to login page
266 if user:
267 user.fp_verification_key = unicode(uuid.uuid4())
268 user.fp_token_expire = datetime.datetime.now() + \
269 datetime.timedelta(days=10)
270 user.save()
271
272 email_debug_message(request)
273 send_fp_verification_email(user, request)
274
275 messages.add_message(request, messages.INFO, success_message)
276 return redirect(request, 'mediagoblin.auth.login')
277
278
279 def verify_forgot_password(request):
280 """
281 Check the forgot-password verification and possibly let the user
282 change their password because of it.
283 """
284 # get form data variables, and specifically check for presence of token
285 formdata = _process_for_token(request)
286 if not formdata['has_userid_and_token']:
287 return render_404(request)
288
289 formdata_token = formdata['vars']['token']
290 formdata_userid = formdata['vars']['userid']
291 formdata_vars = formdata['vars']
292
293 # check if it's a valid user id
294 user = User.query.filter_by(id=formdata_userid).first()
295 if not user:
296 return render_404(request)
297
298 # check if we have a real user and correct token
299 if ((user and user.fp_verification_key and
300 user.fp_verification_key == unicode(formdata_token) and
301 datetime.datetime.now() < user.fp_token_expire
302 and user.email_verified and user.status == 'active')):
303
304 cp_form = auth_forms.ChangePassForm(formdata_vars)
305
306 if request.method == 'POST' and cp_form.validate():
307 user.pw_hash = auth.gen_password_hash(
308 cp_form.password.data)
309 user.fp_verification_key = None
310 user.fp_token_expire = None
311 user.save()
312
313 messages.add_message(
314 request,
315 messages.INFO,
316 _("You can now log in using your new password."))
317 return redirect(request, 'mediagoblin.auth.login')
318 else:
319 return render_to_response(
320 request,
321 'mediagoblin/auth/change_fp.html',
322 {'cp_form': cp_form,
323 'focus': 'password'})
324
325 # in case there is a valid id but no user with that id in the db
326 # or the token expired
327 else:
328 return render_404(request)
329
330
331 def _process_for_token(request):
332 """
333 Checks for tokens in formdata without prior knowledge of request method
334
335 For now, returns whether the userid and token formdata variables exist, and
336 the formdata variables in a hash. Perhaps an object is warranted?
337 """
338 # retrieve the formdata variables
339 if request.method == 'GET':
340 formdata_vars = request.GET
341 else:
342 formdata_vars = request.form
343
344 formdata = {
345 'vars': formdata_vars,
346 'has_userid_and_token':
347 'userid' in formdata_vars and 'token' in formdata_vars}
348
349 return formdata