added unit tests for lost password code
[mediagoblin.git] / mediagoblin / auth / views.py
CommitLineData
8e1e744d 1# GNU MediaGoblin -- federated, autonomous media hosting
24181820
CAW
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
a77d952a 17import uuid
25ba955e 18import datetime
a77d952a 19
1c63ad5d 20from webob import exc
24181820 21
cfe46f3e 22from mediagoblin import messages
13677ef9 23from mediagoblin import mg_globals
de12b4e7 24from mediagoblin.util import render_to_response, redirect, render_404
4b1adc13 25from mediagoblin.util import pass_to_ugettext as _
25ba955e 26from mediagoblin.db.util import ObjectId, InvalidId
24181820
CAW
27from mediagoblin.auth import lib as auth_lib
28from mediagoblin.auth import forms as auth_forms
25ba955e
AV
29from mediagoblin.auth.lib import send_verification_email, \
30 send_fp_verification_email
24181820
CAW
31
32
33def register(request):
34 """
35 Your classic registration view!
36 """
13677ef9
RL
37 # Redirects to indexpage if registrations are disabled
38 if not mg_globals.app_config["allow_registration"]:
166dc91a
CAW
39 messages.add_message(
40 request,
41 messages.WARNING,
4b1adc13 42 _('Sorry, registration is disabled on this instance.'))
13677ef9
RL
43 return redirect(request, "index")
44
24181820
CAW
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
ce72a1bb 49
dc49cf60
CAW
50 users_with_username = request.db.User.find(
51 {'username': request.POST['username'].lower()}).count()
0bf099d7
AV
52 users_with_email = request.db.User.find(
53 {'email': request.POST['email'].lower()}).count()
24181820 54
9f6ea475
CAW
55 extra_validation_passes = True
56
24181820
CAW
57 if users_with_username:
58 register_form.username.errors.append(
4b1adc13 59 _(u'Sorry, a user with that name already exists.'))
9f6ea475
CAW
60 extra_validation_passes = False
61 if users_with_email:
0bf099d7
AV
62 register_form.email.errors.append(
63 _(u'Sorry, that email address has already been taken.'))
9f6ea475 64 extra_validation_passes = False
24181820 65
9f6ea475 66 if extra_validation_passes:
24181820 67 # Create the user
0bc03620
CAW
68 user = request.db.User()
69 user['username'] = request.POST['username'].lower()
873e4e9d 70 user['email'] = request.POST['email'].lower()
0bc03620 71 user['pw_hash'] = auth_lib.bcrypt_gen_password_hash(
24181820 72 request.POST['password'])
0bc03620
CAW
73 user.save(validate=True)
74
f73f4c4b
CAW
75 # log the user in
76 request.session['user_id'] = unicode(user['_id'])
77 request.session.save()
78
79 # send verification email
0bc03620
CAW
80 send_verification_email(user, request)
81
dce5c9cb
CAW
82 # redirect the user to their homepage... there will be a
83 # message waiting for them to verify their email
0bc03620
CAW
84 return redirect(
85 request, 'mediagoblin.user_pages.user_home',
86 user=user['username'])
24181820 87
9038c9f9
CAW
88 return render_to_response(
89 request,
c9c24934
E
90 'mediagoblin/auth/register.html',
91 {'register_form': register_form})
24181820
CAW
92
93
692fd1c9 94def login(request):
a3776717 95 """
8e1e744d 96 MediaGoblin login view.
a3776717
CAW
97
98 If you provide the POST with 'next', it'll redirect to that view.
99 """
692fd1c9
CAW
100 login_form = auth_forms.LoginForm(request.POST)
101
a3776717
CAW
102 login_failed = False
103
692fd1c9 104 if request.method == 'POST' and login_form.validate():
b058cf15 105 user = request.db.User.one(
ce72a1bb 106 {'username': request.POST['username'].lower()})
692fd1c9 107
d1938963 108 if user and user.check_login(request.POST['password']):
692fd1c9
CAW
109 # set up login in session
110 request.session['user_id'] = unicode(user['_id'])
a3776717 111 request.session.save()
692fd1c9 112
574d1511 113 if request.POST.get('next'):
a3776717
CAW
114 return exc.HTTPFound(location=request.POST['next'])
115 else:
9150244a 116 return redirect(request, "index")
692fd1c9
CAW
117
118 else:
119 # Prevent detecting who's on this system by testing login
120 # attempt timings
121 auth_lib.fake_login_attempt()
a3776717 122 login_failed = True
692fd1c9 123
9038c9f9
CAW
124 return render_to_response(
125 request,
c9c24934
E
126 'mediagoblin/auth/login.html',
127 {'login_form': login_form,
128 'next': request.GET.get('next') or request.POST.get('next'),
13bb1d67
RL
129 'login_failed': login_failed,
130 'allow_registration': mg_globals.app_config["allow_registration"]})
692fd1c9
CAW
131
132
133def logout(request):
b97232fa
CAW
134 # Maybe deleting the user_id parameter would be enough?
135 request.session.delete()
7b31a11c 136
9150244a 137 return redirect(request, "index")
db1a438f 138
5866d1a8 139
db1a438f 140def verify_email(request):
4c093e85
JW
141 """
142 Email verification view
143
144 validates GET parameters against database and unlocks the user account, if
145 you are lucky :)
146 """
155f24f9
CAW
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'):
de12b4e7 149 return render_404(request)
155f24f9 150
db1a438f 151 user = request.db.User.find_one(
e0f84870 152 {'_id': ObjectId(unicode(request.GET['userid']))})
db1a438f 153
155f24f9 154 if user and user['verification_key'] == unicode(request.GET['token']):
db1a438f
JW
155 user['status'] = u'active'
156 user['email_verified'] = True
db1a438f 157 user.save()
fe80cb06 158 messages.add_message(
7b31a11c
CAW
159 request,
160 messages.SUCCESS,
4b1adc13
CAW
161 _("Your email address has been verified. "
162 "You may now login, edit your profile, and submit images!"))
db1a438f 163 else:
4b1adc13
CAW
164 messages.add_message(
165 request,
166 messages.ERROR,
167 _('The verification key or user id is incorrect'))
7b31a11c 168
269943a6
CAW
169 return redirect(
170 request, 'mediagoblin.user_pages.user_home',
788272f3 171 user=user['username'])
28afb47c 172
5866d1a8 173
b93a6a22
AM
174def resend_activation(request):
175 """
176 The reactivation view
177
178 Resend the activation email.
179 """
a77d952a
CAW
180 request.user['verification_key'] = unicode(uuid.uuid4())
181 request.user.save()
b93a6a22 182
02d80437 183 send_verification_email(request.user, request)
b93a6a22 184
61927e6e
CAW
185 messages.add_message(
186 request,
187 messages.INFO,
4b1adc13 188 _('Resent your verification email.'))
61927e6e
CAW
189 return redirect(
190 request, 'mediagoblin.user_pages.user_home',
191 user=request.user['username'])
25ba955e
AV
192
193
194def forgot_password(request):
195 """
196 Forgot password view
197
198 Sends an email whit an url to renew forgoten password
199 """
200 fp_form = auth_forms.ForgotPassForm(request.POST)
201
202 if request.method == 'POST' and fp_form.validate():
203 user = request.db.User.one(
204 {'$or': [{'username': request.POST['username']},
205 {'email': request.POST['username']}]})
206
24966c43 207 if user:
25ba955e
AV
208 user['fp_verification_key'] = unicode(uuid.uuid4())
209 user['fp_token_expire'] = datetime.datetime.now() + \
210 datetime.timedelta(days=10)
211 user.save()
212
213 send_fp_verification_email(user, request)
214
24966c43
CFD
215 # do not reveal whether or not there is a matching user, just move along
216 return redirect(request, 'mediagoblin.auth.fp_email_sent')
25ba955e
AV
217
218 return render_to_response(
219 request,
220 'mediagoblin/auth/forgot_password.html',
221 {'fp_form': fp_form})
222
223
224def verify_forgot_password(request):
8d1c9863
CFD
225 # get session variables, and specifically check for presence of token
226 mysession = _process_for_token(request)
227 if not mysession['token_complete']:
228 return render_404(request)
229
230 session_token = mysession['vars']['token']
231 session_userid = mysession['vars']['userid']
232 session_vars = mysession['vars']
233
234 # check if it's a valid Id
235 try:
236 user = request.db.User.find_one(
237 {'_id': ObjectId(unicode(session_userid))})
238 except InvalidId:
239 return render_404(request)
240
241 # check if we have a real user and correct token
242 if (user and user['fp_verification_key'] == unicode(session_token) and
243 datetime.datetime.now() < user['fp_token_expire']):
244 cp_form = auth_forms.ChangePassForm(session_vars)
245
246 if request.method == 'POST' and cp_form.validate():
247 user['pw_hash'] = auth_lib.bcrypt_gen_password_hash(
248 request.POST['password'])
249 user['fp_verification_key'] = None
250 user['fp_token_expire'] = None
251 user.save()
252
253 return redirect(request, 'mediagoblin.auth.fp_changed_success')
25ba955e 254 else:
8d1c9863
CFD
255 return render_to_response(request,
256 'mediagoblin/auth/change_fp.html',
257 {'cp_form': cp_form})
258 # in case there is a valid id but no user whit that id in the db
259 # or the token expired
260 else:
261 return render_404(request)
262
263
264def _process_for_token(request):
265 """
266 Checks for tokens in session without prior knowledge of request method
267
268 For now, returns whether the userid and token session variables exist, and
269 the session variables in a hash. Perhaps an object is warranted?
270 """
271 # retrieve the session variables
272 if request.method == 'GET':
273 session_vars = request.GET
274 else:
275 session_vars = request.POST
276
277 mysession = {'vars': session_vars,
278 'token_complete': session_vars.has_key('userid') and
279 session_vars.has_key('token')}
280 return mysession