Merge branch 'master' of git://gitorious.org/mediagoblin/mediagoblin
[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 'userid' in request.GET or not 'token' in request.GET:
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 # do not reveal whether or not there is a matching user
257 return redirect(request, 'mediagoblin.auth.fp_email_sent')
258
259 return render_to_response(
260 request,
261 'mediagoblin/auth/forgot_password.html',
262 {'fp_form': fp_form})
263
264
265 def verify_forgot_password(request):
266 """
267 Check the forgot-password verification and possibly let the user
268 change their password because of it.
269 """
270 # get form data variables, and specifically check for presence of token
271 formdata = _process_for_token(request)
272 if not formdata['has_userid_and_token']:
273 return render_404(request)
274
275 formdata_token = formdata['vars']['token']
276 formdata_userid = formdata['vars']['userid']
277 formdata_vars = formdata['vars']
278
279 # check if it's a valid Id
280 try:
281 user = request.db.User.find_one(
282 {'_id': ObjectId(unicode(formdata_userid))})
283 except InvalidId:
284 return render_404(request)
285
286 # check if we have a real user and correct token
287 if ((user and user['fp_verification_key'] and
288 user['fp_verification_key'] == unicode(formdata_token) and
289 datetime.datetime.now() < user['fp_token_expire']
290 and user['email_verified'] and user['status'] == 'active')):
291
292 cp_form = auth_forms.ChangePassForm(formdata_vars)
293
294 if request.method == 'POST' and cp_form.validate():
295 user[u'pw_hash'] = auth_lib.bcrypt_gen_password_hash(
296 request.POST['password'])
297 user[u'fp_verification_key'] = None
298 user[u'fp_token_expire'] = None
299 user.save()
300
301 return redirect(request, 'mediagoblin.auth.fp_changed_success')
302 else:
303 return render_to_response(
304 request,
305 'mediagoblin/auth/change_fp.html',
306 {'cp_form': cp_form})
307
308 # in case there is a valid id but no user whit that id in the db
309 # or the token expired
310 else:
311 return render_404(request)
312
313
314 def _process_for_token(request):
315 """
316 Checks for tokens in formdata without prior knowledge of request method
317
318 For now, returns whether the userid and token formdata variables exist, and
319 the formdata variables in a hash. Perhaps an object is warranted?
320 """
321 # retrieve the formdata variables
322 if request.method == 'GET':
323 formdata_vars = request.GET
324 else:
325 formdata_vars = request.POST
326
327 formdata = {
328 'vars': formdata_vars,
329 'has_userid_and_token':
330 'userid' in formdata_vars and 'token' in formdata_vars}
331
332 return formdata