Use the full URI at invitation targets.
[pithos] / pithos / im / views.py
1 # Copyright 2011 GRNET S.A. All rights reserved.
2
3 # Redistribution and use in source and binary forms, with or
4 # without modification, are permitted provided that the following
5 # conditions are met:
6
7 #   1. Redistributions of source code must retain the above
8 #      copyright notice, this list of conditions and the following
9 #      disclaimer.
10
11 #   2. Redistributions in binary form must reproduce the above
12 #      copyright notice, this list of conditions and the following
13 #      disclaimer in the documentation and/or other materials
14 #      provided with the distribution.
15
16 # THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17 # OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18 # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19 # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20 # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23 # USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24 # AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26 # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27 # POSSIBILITY OF SUCH DAMAGE.
28
29 # The views and conclusions contained in the software and
30 # documentation are those of the authors and should not be
31 # interpreted as representing official policies, either expressed
32 # or implied, of GRNET S.A.
33
34 import json
35 import logging
36 import socket
37
38 from datetime import datetime
39 from functools import wraps
40 from math import ceil
41 from random import randint
42 from smtplib import SMTPException
43
44 from django.conf import settings
45 from django.core.mail import send_mail
46 from django.http import HttpResponse, HttpResponseRedirect
47 from django.shortcuts import redirect
48 from django.template.loader import render_to_string
49 from django.utils.http import urlencode
50 from django.utils.translation import ugettext as _
51 from django.core.urlresolvers import reverse
52
53 from urllib import quote
54
55 from pithos.im.models import User, Invitation
56 from pithos.im.util import isoformat
57
58
59 def render_response(template, tab=None, status=200, **kwargs):
60     if tab is None:
61         tab = template.partition('_')[0]
62     kwargs.setdefault('tab', tab)
63     html = render_to_string(template, kwargs)
64     return HttpResponse(html, status=status)
65
66
67 def requires_login(func):
68     @wraps(func)
69     def wrapper(request, *args):
70         if not settings.BYPASS_ADMIN_AUTH:
71             if not request.user:
72                 next = urlencode({'next': request.build_absolute_uri()})
73                 login_uri = reverse(index) + '?' + next
74                 return HttpResponseRedirect(login_uri)
75         return func(request, *args)
76     return wrapper
77
78
79 def requires_admin(func):
80     @wraps(func)
81     def wrapper(request, *args):
82         if not settings.BYPASS_ADMIN_AUTH:
83             if not request.user:
84                 next = urlencode({'next': request.build_absolute_uri()})
85                 login_uri = reverse(index) + '?' + next
86                 return HttpResponseRedirect(login_uri)
87             if not request.user.is_admin:
88                 return HttpResponse('Forbidden', status=403)
89         return func(request, *args)
90     return wrapper
91
92
93 def index(request):
94     return render_response('index.html', next=request.GET.get('next', ''))
95
96
97 @requires_admin
98 def admin(request):
99     stats = {}
100     stats['users'] = User.objects.count()
101     
102     invitations = Invitation.objects.all()
103     stats['invitations'] = invitations.count()
104     stats['invitations_accepted'] = invitations.filter(is_accepted=True).count()
105     
106     return render_response('admin.html', tab='home', stats=stats)
107
108
109 @requires_admin
110 def users_list(request):
111     users = User.objects.order_by('id')
112     
113     filter = request.GET.get('filter', '')
114     if filter:
115         if filter.startswith('-'):
116             users = users.exclude(uniq__icontains=filter[1:])
117         else:
118             users = users.filter(uniq__icontains=filter)
119     
120     try:
121         page = int(request.GET.get('page', 1))
122     except ValueError:
123         page = 1
124     offset = max(0, page - 1) * settings.ADMIN_PAGE_LIMIT
125     limit = offset + settings.ADMIN_PAGE_LIMIT
126     
127     npages = int(ceil(1.0 * users.count() / settings.ADMIN_PAGE_LIMIT))
128     prev = page - 1 if page > 1 else None
129     next = page + 1 if page < npages else None
130     return render_response('users_list.html',
131                             users=users[offset:limit],
132                             filter=filter,
133                             pages=range(1, npages + 1),
134                             page=page,
135                             prev=prev,
136                             next=next)
137     
138 @requires_admin
139 def users_create(request):
140     if request.method == 'GET':
141         return render_response('users_create.html')
142     if request.method == 'POST':
143         user = User()
144         user.uniq = request.POST.get('uniq')
145         user.realname = request.POST.get('realname')
146         user.is_admin = True if request.POST.get('admin') else False
147         user.affiliation = request.POST.get('affiliation')
148         user.quota = int(request.POST.get('quota') or 0) * (1024 ** 3)  # In GiB
149         user.renew_token()
150         user.save()
151         return redirect(users_info, user.id)
152
153 @requires_admin
154 def users_info(request, user_id):
155     user = User.objects.get(id=user_id)
156     states = [x[0] for x in User.ACCOUNT_STATE]
157     return render_response('users_info.html',
158                             user=user,
159                             states=states)
160
161
162 @requires_admin
163 def users_modify(request, user_id):
164     user = User.objects.get(id=user_id)
165     user.uniq = request.POST.get('uniq')
166     user.realname = request.POST.get('realname')
167     user.is_admin = True if request.POST.get('admin') else False
168     user.affiliation = request.POST.get('affiliation')
169     user.state = request.POST.get('state')
170     user.invitations = int(request.POST.get('invitations') or 0)
171     user.quota = int(request.POST.get('quota') or 0) * (1024 ** 3)  # In GiB
172     user.auth_token = request.POST.get('auth_token')
173     try:
174         auth_token_expires = request.POST.get('auth_token_expires')
175         d = datetime.strptime(auth_token_expires, '%Y-%m-%dT%H:%MZ')
176         user.auth_token_expires = d
177     except ValueError:
178         pass
179     user.save()
180     return redirect(users_info, user.id)
181
182
183 @requires_admin
184 def users_delete(request, user_id):
185     user = User.objects.get(id=user_id)
186     user.delete()
187     return redirect(users_list)
188
189
190 def generate_invitation_code():
191     while True:
192         code = randint(1, 2L**63 - 1)
193         try:
194             Invitation.objects.get(code=code)
195             # An invitation with this code already exists, try again
196         except Invitation.DoesNotExist:
197             return code
198
199
200 def send_invitation(baseurl, inv):
201     url = settings.INVITATION_LOGIN_TARGET % (baseurl, inv.code, quote(baseurl))
202     subject = _('Invitation to Pithos')
203     message = render_to_string('invitation.txt', {
204                 'invitation': inv,
205                 'url': url,
206                 'baseurl': baseurl,
207                 'service': settings.SERVICE_NAME,
208                 'support': settings.DEFAULT_CONTACT_EMAIL})
209     sender = settings.DEFAULT_FROM_EMAIL
210     send_mail(subject, message, sender, [inv.uniq])
211     logging.info('Sent invitation %s', inv)
212
213
214 @requires_login
215 def invite(request):
216     status = None
217     message = None
218     inviter = request.user
219
220     if request.method == 'POST':
221         uniq = request.POST.get('uniq')
222         realname = request.POST.get('realname')
223         
224         if inviter.invitations > 0:
225             code = generate_invitation_code()
226             invitation, created = Invitation.objects.get_or_create(
227                 inviter=inviter,
228                 uniq=uniq,
229                 defaults={'code': code, 'realname': realname})
230             
231             try:
232                 send_invitation(request.build_absolute_uri('/').rstrip('/'), invitation)
233                 if created:
234                     inviter.invitations = max(0, inviter.invitations - 1)
235                     inviter.save()
236                 status = 'success'
237                 message = _('Invitation sent to %s' % uniq)
238             except (SMTPException, socket.error) as e:
239                 status = 'error'
240                 message = getattr(e, 'strerror', '')
241         else:
242             status = 'error'
243             message = _('No invitations left')
244
245     if request.GET.get('format') == 'json':
246         sent = [{'email': inv.uniq,
247                  'realname': inv.realname,
248                  'is_accepted': inv.is_accepted}
249                     for inv in inviter.invitations_sent.all()]
250         rep = {'invitations': inviter.invitations, 'sent': sent}
251         return HttpResponse(json.dumps(rep))
252     
253     html = render_to_string('invitations.html', {
254             'user': inviter,
255             'status': status,
256             'message': message})
257     return HttpResponse(html)
258
259 def send_verification(baseurl, user):
260     url = settings.ACTIVATION_LOGIN_TARGET % (baseurl,
261                                               quote(user.auth_token),
262                                               quote(baseurl))
263     message = render_to_string('activation.txt', {
264             'user': user,
265             'url': url,
266             'baseurl': baseurl,
267             'service': settings.SERVICE_NAME,
268             'support': settings.DEFAULT_CONTACT_EMAIL})
269     sender = settings.DEFAULT_FROM_EMAIL
270     send_mail('Pithos account activation', message, sender, [user.email])
271     logging.info('Sent activation %s', user)
272
273 def local_create(request):
274     if request.method == 'GET':
275         return render_response('local_create.html')
276     elif request.method == 'POST':
277         username = request.POST.get('uniq')
278         realname = request.POST.get('realname')
279         email = request.POST.get('email')
280         password = request.POST.get('password')
281         status = 'success'
282         cookie_value = None
283         if not username:
284             status = 'error'
285             message = 'No username provided'
286         elif not password:
287             status = 'error'
288             message = 'No password provided'
289         elif not email:
290             status = 'error'
291             message = 'No email provided'
292         
293         if status == 'success':
294             username = '%s@local' % username
295             try:
296                 user = User.objects.get(uniq=username)
297                 status = 'error'
298                 message = 'Username is not available'
299             except User.DoesNotExist:
300                 user = User()
301                 user.uniq = username 
302                 user.realname = realname
303                 user.email = request.POST.get('email')
304                 user.password = request.POST.get('password')
305                 user.is_admin = False
306                 user.quota = 0
307                 user.state = 'UNVERIFIED'
308                 user.level = 1
309                 user.renew_token()
310                 try:
311                     send_verification(request.build_absolute_uri('/').rstrip('/'), user)
312                     message = _('Verification sent to %s' % user.email)
313                     user.save()
314                 except (SMTPException, socket.error) as e:
315                     status = 'error'
316                     name = 'strerror'
317                     message = getattr(e, name) if hasattr(e, name) else e
318         
319         html = render_to_string('local_create.html', {
320                 'status': status,
321                 'message': message})
322         response = HttpResponse(html)
323         return response
324
325 def send_password(baseurl, user):
326     url = settings.PASSWORD_RESET_TARGET % (baseurl,
327                                             quote(user.uniq),
328                                             quote(baseurl))
329     message = render_to_string('password.txt', {
330             'user': user,
331             'url': url,
332             'baseurl': baseurl,
333             'service': settings.SERVICE_NAME,
334             'support': settings.DEFAULT_CONTACT_EMAIL})
335     sender = settings.DEFAULT_FROM_EMAIL
336     send_mail('Pithos password recovering', message, sender, [user.email])
337     logging.info('Sent password %s', user)
338
339 def reclaim_password(request):
340     if request.method == 'GET':
341         return render_response('reclaim.html')
342     elif request.method == 'POST':
343         username = request.POST.get('uniq')
344         username = '%s@local' % username
345         try:
346             user = User.objects.get(uniq=username)
347             try:
348                 send_password(request.build_absolute_uri('/').rstrip('/'), user)
349                 status = 'success'
350                 message = _('Password reset sent to %s' % user.email)
351                 user.status = 'UNVERIFIED'
352                 user.save()
353             except (SMTPException, socket.error) as e:
354                 status = 'error'
355                 name = 'strerror'
356                 message = getattr(e, name) if hasattr(e, name) else e
357         except User.DoesNotExist:
358             status = 'error'
359             message = 'Username does not exist'
360         
361         html = render_to_string('reclaim.html', {
362                 'status': status,
363                 'message': message})
364         return HttpResponse(html)