Make sure user isn't already verified before resending verification.
[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.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, 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
199 if request.user["email_verified"]:
200 messages.add_message(
201 request,
202 messages.ERROR,
203 _("You've already verified your email address!"))
204
205 return redirect(request, "mediagoblin.user_pages.user_home", user=request.user['username'])
206
207 if request.user is None:
208 messages.add_message(
209 request,
210 messages.ERROR,
211 _('You must be logged in so we know who to send the email to!'))
212
213 return redirect(request, "/auth/login")
214
215 request.user[u'verification_key'] = unicode(uuid.uuid4())
216 request.user.save()
217
218 email_debug_message(request)
219 send_verification_email(request.user, request)
220
221 messages.add_message(
222 request,
223 messages.INFO,
224 _('Resent your verification email.'))
225 return redirect(
226 request, 'mediagoblin.user_pages.user_home',
227 user=request.user['username'])
228
229
230 def forgot_password(request):
231 """
232 Forgot password view
233
234 Sends an email whit an url to renew forgoten password
235 """
236 fp_form = auth_forms.ForgotPassForm(request.POST)
237
238 if request.method == 'POST' and fp_form.validate():
239
240 # Here, so it doesn't depend on the actual mail being sent
241 # and thus doesn't reveal, wether mail was sent.
242 email_debug_message(request)
243
244 # '$or' not available till mongodb 1.5.3
245 user = request.db.User.find_one(
246 {'username': request.POST['username']})
247 if not user:
248 user = request.db.User.find_one(
249 {'email': request.POST['username']})
250
251 if user:
252 if user['email_verified'] and user['status'] == 'active':
253 user[u'fp_verification_key'] = unicode(uuid.uuid4())
254 user[u'fp_token_expire'] = datetime.datetime.now() + \
255 datetime.timedelta(days=10)
256 user.save()
257
258 send_fp_verification_email(user, request)
259 else:
260 # special case... we can't send the email because the
261 # username is inactive / hasn't verified their email
262 messages.add_message(
263 request,
264 messages.WARNING,
265 _("Could not send password recovery email as "
266 "your username is inactive or your account's "
267 "email address has not been verified."))
268
269 return redirect(
270 request, 'mediagoblin.user_pages.user_home',
271 user=user['username'])
272
273
274 # do not reveal whether or not there is a matching user, just move along
275 return redirect(request, 'mediagoblin.auth.fp_email_sent')
276
277 return render_to_response(
278 request,
279 'mediagoblin/auth/forgot_password.html',
280 {'fp_form': fp_form})
281
282
283 def verify_forgot_password(request):
284 """
285 Check the forgot-password verification and possibly let the user
286 change their password because of it.
287 """
288 # get form data variables, and specifically check for presence of token
289 formdata = _process_for_token(request)
290 if not formdata['has_userid_and_token']:
291 return render_404(request)
292
293 formdata_token = formdata['vars']['token']
294 formdata_userid = formdata['vars']['userid']
295 formdata_vars = formdata['vars']
296
297 # check if it's a valid Id
298 try:
299 user = request.db.User.find_one(
300 {'_id': ObjectId(unicode(formdata_userid))})
301 except InvalidId:
302 return render_404(request)
303
304 # check if we have a real user and correct token
305 if ((user and user['fp_verification_key'] and
306 user['fp_verification_key'] == unicode(formdata_token) and
307 datetime.datetime.now() < user['fp_token_expire']
308 and user['email_verified'] and user['status'] == 'active')):
309
310 cp_form = auth_forms.ChangePassForm(formdata_vars)
311
312 if request.method == 'POST' and cp_form.validate():
313 user[u'pw_hash'] = auth_lib.bcrypt_gen_password_hash(
314 request.POST['password'])
315 user[u'fp_verification_key'] = None
316 user[u'fp_token_expire'] = None
317 user.save()
318
319 return redirect(request, 'mediagoblin.auth.fp_changed_success')
320 else:
321 return render_to_response(
322 request,
323 'mediagoblin/auth/change_fp.html',
324 {'cp_form': cp_form})
325
326 # in case there is a valid id but no user whit that id in the db
327 # or the token expired
328 else:
329 return render_404(request)
330
331
332 def _process_for_token(request):
333 """
334 Checks for tokens in formdata without prior knowledge of request method
335
336 For now, returns whether the userid and token formdata variables exist, and
337 the formdata variables in a hash. Perhaps an object is warranted?
338 """
339 # retrieve the formdata variables
340 if request.method == 'GET':
341 formdata_vars = request.GET
342 else:
343 formdata_vars = request.POST
344
345 formdata = {
346 'vars': formdata_vars,
347 'has_userid_and_token':
348 formdata_vars.has_key('userid') and formdata_vars.has_key('token')}
349
350 return formdata