Keys in mongodb should be unicode, here...
[mediagoblin.git] / mediagoblin / auth / views.py
1 # GNU MediaGoblin -- federated, autonomous media hosting
2 # Copyright (C) 2011 Free Software Foundation, Inc
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 register(request):
34 """
35 Your classic registration view!
36 """
37 # Redirects to indexpage if registrations are disabled
38 if not mg_globals.app_config["allow_registration"]:
39 messages.add_message(
40 request,
41 messages.WARNING,
42 _('Sorry, registration is disabled on this instance.'))
43 return redirect(request, "index")
44
45 register_form = auth_forms.RegistrationForm(request.POST)
46
47 if request.method == 'POST' and register_form.validate():
48 # TODO: Make sure the user doesn't exist already
49
50 users_with_username = request.db.User.find(
51 {'username': request.POST['username'].lower()}).count()
52 users_with_email = request.db.User.find(
53 {'email': request.POST['email'].lower()}).count()
54
55 extra_validation_passes = True
56
57 if users_with_username:
58 register_form.username.errors.append(
59 _(u'Sorry, a user with that name already exists.'))
60 extra_validation_passes = False
61 if users_with_email:
62 register_form.email.errors.append(
63 _(u'Sorry, that email address has already been taken.'))
64 extra_validation_passes = False
65
66 if extra_validation_passes:
67 # Create the user
68 user = request.db.User()
69 user['username'] = request.POST['username'].lower()
70 user['email'] = request.POST['email'].lower()
71 user['pw_hash'] = auth_lib.bcrypt_gen_password_hash(
72 request.POST['password'])
73 user.save(validate=True)
74
75 # log the user in
76 request.session['user_id'] = unicode(user['_id'])
77 request.session.save()
78
79 # send verification email
80 send_verification_email(user, request)
81
82 # redirect the user to their homepage... there will be a
83 # message waiting for them to verify their email
84 return redirect(
85 request, 'mediagoblin.user_pages.user_home',
86 user=user['username'])
87
88 return render_to_response(
89 request,
90 'mediagoblin/auth/register.html',
91 {'register_form': register_form})
92
93
94 def login(request):
95 """
96 MediaGoblin login view.
97
98 If you provide the POST with 'next', it'll redirect to that view.
99 """
100 login_form = auth_forms.LoginForm(request.POST)
101
102 login_failed = False
103
104 if request.method == 'POST' and login_form.validate():
105 user = request.db.User.one(
106 {'username': request.POST['username'].lower()})
107
108 if user and user.check_login(request.POST['password']):
109 # set up login in session
110 request.session['user_id'] = unicode(user['_id'])
111 request.session.save()
112
113 if request.POST.get('next'):
114 return exc.HTTPFound(location=request.POST['next'])
115 else:
116 return redirect(request, "index")
117
118 else:
119 # Prevent detecting who's on this system by testing login
120 # attempt timings
121 auth_lib.fake_login_attempt()
122 login_failed = True
123
124 return render_to_response(
125 request,
126 'mediagoblin/auth/login.html',
127 {'login_form': login_form,
128 'next': request.GET.get('next') or request.POST.get('next'),
129 'login_failed': login_failed,
130 'allow_registration': mg_globals.app_config["allow_registration"]})
131
132
133 def logout(request):
134 # Maybe deleting the user_id parameter would be enough?
135 request.session.delete()
136
137 return redirect(request, "index")
138
139
140 def verify_email(request):
141 """
142 Email verification view
143
144 validates GET parameters against database and unlocks the user account, if
145 you are lucky :)
146 """
147 # If we don't have userid and token parameters, we can't do anything; 404
148 if not request.GET.has_key('userid') or not request.GET.has_key('token'):
149 return render_404(request)
150
151 user = request.db.User.find_one(
152 {'_id': ObjectId(unicode(request.GET['userid']))})
153
154 if user and user['verification_key'] == unicode(request.GET['token']):
155 user[u'status'] = u'active'
156 user[u'email_verified'] = True
157 user[u'verification_key'] = None
158
159 user.save()
160
161 messages.add_message(
162 request,
163 messages.SUCCESS,
164 _("Your email address has been verified. "
165 "You may now login, edit your profile, and submit images!"))
166 else:
167 messages.add_message(
168 request,
169 messages.ERROR,
170 _('The verification key or user id is incorrect'))
171
172 return redirect(
173 request, 'mediagoblin.user_pages.user_home',
174 user=user['username'])
175
176
177 def resend_activation(request):
178 """
179 The reactivation view
180
181 Resend the activation email.
182 """
183 request.user[u'verification_key'] = unicode(uuid.uuid4())
184 request.user.save()
185
186 send_verification_email(request.user, request)
187
188 messages.add_message(
189 request,
190 messages.INFO,
191 _('Resent your verification email.'))
192 return redirect(
193 request, 'mediagoblin.user_pages.user_home',
194 user=request.user['username'])
195
196
197 def forgot_password(request):
198 """
199 Forgot password view
200
201 Sends an email whit an url to renew forgoten password
202 """
203 fp_form = auth_forms.ForgotPassForm(request.POST)
204
205 if request.method == 'POST' and fp_form.validate():
206 # '$or' not available till mongodb 1.5.3
207 user = request.db.User.find_one(
208 {'username': request.POST['username']})
209 if not user:
210 user = request.db.User.find_one(
211 {'email': request.POST['username']})
212
213 if user:
214 if user['email_verified'] and user['status'] == 'active':
215 user[u'fp_verification_key'] = unicode(uuid.uuid4())
216 user[u'fp_token_expire'] = datetime.datetime.now() + \
217 datetime.timedelta(days=10)
218 user.save()
219
220 send_fp_verification_email(user, request)
221 else:
222 # special case... we can't send the email because the
223 # username is inactive / hasn't verified their email
224 messages.add_message(
225 request,
226 messages.WARNING,
227 _("Could not send password recovery email as "
228 "your username is inactive or your account's "
229 "email address has not been verified."))
230
231 return redirect(
232 request, 'mediagoblin.user_pages.user_home',
233 user=user['username'])
234
235
236 # do not reveal whether or not there is a matching user, just move along
237 return redirect(request, 'mediagoblin.auth.fp_email_sent')
238
239 return render_to_response(
240 request,
241 'mediagoblin/auth/forgot_password.html',
242 {'fp_form': fp_form})
243
244
245 def verify_forgot_password(request):
246 # get session variables, and specifically check for presence of token
247 mysession = _process_for_token(request)
248 if not mysession['has_userid_and_token']:
249 return render_404(request)
250
251 session_token = mysession['vars']['token']
252 session_userid = mysession['vars']['userid']
253 session_vars = mysession['vars']
254
255 # check if it's a valid Id
256 try:
257 user = request.db.User.find_one(
258 {'_id': ObjectId(unicode(session_userid))})
259 except InvalidId:
260 return render_404(request)
261
262 # check if we have a real user and correct token
263 if ((user and user['fp_verification_key'] and
264 user['fp_verification_key'] == unicode(session_token) and
265 datetime.datetime.now() < user['fp_token_expire']
266 and user['email_verified'] and user['status'] == 'active')):
267
268 cp_form = auth_forms.ChangePassForm(session_vars)
269
270 if request.method == 'POST' and cp_form.validate():
271 user[u'pw_hash'] = auth_lib.bcrypt_gen_password_hash(
272 request.POST['password'])
273 user[u'fp_verification_key'] = None
274 user[u'fp_token_expire'] = None
275 user.save()
276
277 return redirect(request, 'mediagoblin.auth.fp_changed_success')
278 else:
279 return render_to_response(
280 request,
281 'mediagoblin/auth/change_fp.html',
282 {'cp_form': cp_form})
283
284 # in case there is a valid id but no user whit that id in the db
285 # or the token expired
286 else:
287 return render_404(request)
288
289
290 def _process_for_token(request):
291 """
292 Checks for tokens in session without prior knowledge of request method
293
294 For now, returns whether the userid and token session variables exist, and
295 the session variables in a hash. Perhaps an object is warranted?
296 """
297 # retrieve the session variables
298 if request.method == 'GET':
299 session_vars = request.GET
300 else:
301 session_vars = request.POST
302
303 mysession = {
304 'vars': session_vars,
305 'has_userid_and_token':
306 session_vars.has_key('userid') and session_vars.has_key('token')}
307
308 return mysession