Allow email login
[wolnelektury.git] / src / api / views.py
1 # This file is part of Wolne Lektury, licensed under GNU Affero GPLv3 or later.
2 # Copyright © Fundacja Wolne Lektury. See NOTICE for more information.
3 #
4 from time import time
5 from allauth.account.forms import ResetPasswordForm
6 from allauth.account.utils import filter_users_by_email
7 from django.conf import settings
8 from django.contrib.auth import authenticate, login
9 from django.contrib.auth.decorators import login_required
10 from django.contrib.auth.models import User
11 from django import forms
12 from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseForbidden
13 from django.http import Http404
14 from django.shortcuts import redirect, render
15 from django.views.generic.base import View
16 from oauthlib.common import urlencode, generate_token
17 from oauthlib.oauth1 import RequestTokenEndpoint, AccessTokenEndpoint
18 from oauthlib.oauth1 import AuthorizationEndpoint, OAuth1Error
19 from rest_framework.permissions import IsAuthenticated
20 from rest_framework.response import Response
21 from rest_framework.views import APIView
22 from rest_framework.generics import GenericAPIView, RetrieveAPIView, get_object_or_404
23 from catalogue.models import Book
24 from .models import BookUserData, KEY_SIZE, SECRET_SIZE, Token, SessionTransferToken
25 from social.models import UserConfirmation
26 from . import serializers
27 from .request_validator import PistonRequestValidator
28 from .utils import oauthlib_request, oauthlib_response, vary_on_auth
29
30
31 class OAuth1RequestTokenEndpoint(RequestTokenEndpoint):
32     def _create_request(self, *args, **kwargs):
33         r = super(OAuth1RequestTokenEndpoint, self)._create_request(*args, **kwargs)
34         r.redirect_uri = 'oob'
35         return r
36
37     def create_request_token(self, request, credentials):
38         token = {
39             'oauth_token': self.token_generator()[:KEY_SIZE],
40             'oauth_token_secret': self.token_generator()[:SECRET_SIZE],
41         }
42         token.update(credentials)
43         self.request_validator.save_request_token(token, request)
44         return urlencode(token.items())
45
46
47 # Never Cache
48 class OAuth1RequestTokenView(View):
49     def __init__(self):
50         self.endpoint = OAuth1RequestTokenEndpoint(PistonRequestValidator())
51
52     def dispatch(self, request):
53         return oauthlib_response(
54             self.endpoint.create_request_token_response(
55                 **oauthlib_request(request)
56             )
57         )
58
59
60 class OAuthAuthenticationForm(forms.Form):
61     oauth_token = forms.CharField(widget=forms.HiddenInput)
62     oauth_callback = forms.CharField(widget=forms.HiddenInput)  # changed from URLField - too strict
63     # removed authorize_access - redundant
64
65
66 class OAuth1AuthorizationEndpoint(AuthorizationEndpoint):
67     def create_verifier(self, request, credentials):
68         verifier = super(OAuth1AuthorizationEndpoint, self).create_verifier(request, credentials)
69         return {
70             'oauth_token': verifier['oauth_token'],
71         }
72
73
74 @login_required
75 def oauth_user_auth(request):
76     endpoint = OAuth1AuthorizationEndpoint(PistonRequestValidator())
77
78     if request.method == "GET":
79         # Why not just get oauth_token here?
80         # This is fairly straightforward, in't?
81         try:
82             realms, credentials = endpoint.get_realms_and_credentials(
83                 **oauthlib_request(request))
84         except OAuth1Error as e:
85             return HttpResponse(str(e), status=400)
86         callback = request.GET.get('oauth_callback')
87
88         form = OAuthAuthenticationForm(initial={
89             'oauth_token': credentials['resource_owner_key'],
90             'oauth_callback': callback,
91         })
92
93         return render(request, 'oauth/authorize_token.html', {'form': form})
94
95     if request.method == "POST":
96         try:
97             response = oauthlib_response(
98                 endpoint.create_authorization_response(
99                     credentials={"user": request.user},
100                     **oauthlib_request(request)
101                 )
102             )
103         except OAuth1Error as e:
104             return HttpResponse(e.message, status=400)
105         else:
106             return response
107
108
109 class OAuth1AccessTokenEndpoint(AccessTokenEndpoint):
110     def _create_request(self, *args, **kwargs):
111         r = super(OAuth1AccessTokenEndpoint, self)._create_request(*args, **kwargs)
112         r.verifier = 'x' * 20
113         return r
114
115     def create_access_token(self, request, credentials):
116         request.realms = self.request_validator.get_realms(
117             request.resource_owner_key, request)
118         token = {
119             'oauth_token': self.token_generator()[:KEY_SIZE],
120             'oauth_token_secret': self.token_generator()[:SECRET_SIZE],
121             'oauth_authorized_realms': ' '.join(request.realms)
122         }
123         token.update(credentials)
124         self.request_validator.save_access_token(token, request)
125         return urlencode(token.items())
126
127
128 # Never cache
129 class OAuth1AccessTokenView(View):
130     def __init__(self):
131         self.endpoint = OAuth1AccessTokenEndpoint(PistonRequestValidator())
132
133     def dispatch(self, request):
134         return oauthlib_response(
135             self.endpoint.create_access_token_response(
136                 **oauthlib_request(request)
137             )
138         )
139
140
141 class LoginView(GenericAPIView):
142     serializer_class = serializers.LoginSerializer
143
144     def post(self, request):
145         serializer = self.get_serializer(data=request.data)
146         serializer.is_valid(raise_exception=True)
147         d = serializer.validated_data
148         user = authenticate(username=d['username'], password=d['password'])
149         if user is None:
150             return Response({"detail": "Invalid credentials."})
151
152         key = generate_token()[:KEY_SIZE]
153         Token.objects.create(
154             key=key,
155             token_type=Token.ACCESS,
156             timestamp=time(),
157             user=user,
158         )
159         return Response({"access_token": key})
160
161
162 class Login2View(GenericAPIView):
163     serializer_class = serializers.LoginSerializer
164
165     def post(self, request):
166         serializer = self.get_serializer(data=request.data)
167         serializer.is_valid(raise_exception=True)
168         d = serializer.validated_data
169         user = authenticate(username=d['username'], password=d['password'])
170         if user is None:
171             return Response({"detail": "Invalid credentials."})
172
173         access_token = generate_token()[:KEY_SIZE]
174         Token.objects.create(
175             key=access_token,
176             token_type=Token.ACCESS,
177             timestamp=time(),
178             user=user,
179         )
180         refresh_token = generate_token()[:KEY_SIZE]
181         Token.objects.create(
182             key=refresh_token,
183             token_type=Token.REFRESH,
184             timestamp=time(),
185             user=user,
186         )
187         return Response({
188             "access_token": access_token,
189             "refresh_token": refresh_token,
190             "expires": 3600,
191         })
192
193
194 @vary_on_auth
195 class UserView(RetrieveAPIView):
196     permission_classes = [IsAuthenticated]
197     serializer_class = serializers.UserSerializer
198
199     def get_object(self):
200         return self.request.user
201
202
203 @vary_on_auth
204 class BookUserDataView(RetrieveAPIView):
205     permission_classes = [IsAuthenticated]
206     serializer_class = serializers.BookUserDataSerializer
207     lookup_field = 'book__slug'
208     lookup_url_kwarg = 'slug'
209
210     def get_queryset(self):
211         return BookUserData.objects.filter(user=self.request.user)
212
213     def get(self, *args, **kwargs):
214         try:
215             return super(BookUserDataView, self).get(*args, **kwargs)
216         except Http404:
217             return Response({"state": "not_started"})
218
219     def post(self, request, slug, state):
220         if state not in ('reading', 'complete'):
221             raise Http404
222
223         book = get_object_or_404(Book, slug=slug)
224         instance = BookUserData.update(book, request.user, state)
225         serializer = self.get_serializer(instance)
226         return Response(serializer.data)
227
228
229 class BlogView(APIView):
230     def get(self, request):
231         return Response([])
232
233
234
235 class RegisterView(GenericAPIView):
236     serializer_class = serializers.RegisterSerializer
237
238     def get(self, request):
239         return Response({
240             "options": [
241                 {
242                     "id": 1,
243                     "html": "Chcę otrzymywać newsletter Wolnych Lektur",
244                     "required": False
245                 }
246             ],
247             "info": [
248                 'Administratorem danych osobowych jest Fundacja Wolne Lektury (ul. Marszałkowska 84/92 lok. 125, 00-514 Warszawa). Podanie danych osobowych jest dobrowolne. Dane są przetwarzane w zakresie niezbędnym do prowadzenia serwisu, a także w celach prowadzenia statystyk, ewaluacji i sprawozdawczości. W przypadku wyrażenia dodatkowej zgody adres e-mail zostanie wykorzystany także w celu przesyłania newslettera Wolnych Lektur. Osobom, których dane są zbierane, przysługuje prawo dostępu do treści swoich danych oraz ich poprawiania. Więcej informacji w <a href="https://fundacja.wolnelektury.pl/prywatnosc/">polityce prywatności</a>.'
249             ]            
250         })
251
252     def post(self, request):
253         if not settings.FEATURE_API_REGISTER:
254             return Response(
255                 {
256                     "detail": "Rejestracja aktualnie niedostępna."
257                 },
258                 status=400
259             )
260         serializer = self.get_serializer(data=request.data)
261         serializer.is_valid(raise_exception=True)
262         d = serializer.validated_data
263
264         email = d['email']
265
266         user = User(
267             username=email,
268             email=email,
269             is_active=True
270         )
271         user.set_password(d['password'])
272
273         if settings.FEATURE_CONFIRM_USER:
274             user.is_active = False
275
276         try:
277             assert not filter_users_by_email(email)
278             user.save()
279         except:
280             return Response(
281                 {
282                     "detail": "Nie można utworzyć konta.",
283                 },
284                 status=400
285             )
286
287         if settings.FEATURE_CONFIRM_USER:
288             UserConfirmation.request(user)
289         return Response({})
290
291
292 class RefreshTokenView(APIView):
293     serializer_class = serializers.RefreshTokenSerializer
294
295     def post(self, request):
296         serializer = self.get_serializer(data=request.data)
297         serializer.is_valid(raise_exception=True)
298         d = serializer.validated_data
299         
300         t = Token.objects.get(
301             key=d['refresh_token'],
302             token_type=Token.REFRESH
303         )
304         user = t.user
305
306         access_token = generate_token()[:KEY_SIZE]
307         Token.objects.create(
308             key=access_token,
309             token_type=Token.ACCESS,
310             timestamp=time(),
311             user=user,
312         )
313         refresh_token = generate_token()[:KEY_SIZE]
314         Token.objects.create(
315             key=refresh_token,
316             token_type=Token.REFRESH,
317             timestamp=time(),
318             user=user,
319         )
320         return Response({
321             "access_token": access_token,
322             "refresh_token": refresh_token,
323             "expires": 3600,
324         })
325
326
327 class RequestConfirmView(APIView):
328     serializer_class = serializers.RequestConfirmSerializer
329
330     def post(self, request):
331         serializer = self.get_serializer(data=request.data)
332         serializer.is_valid(raise_exception=True)
333         d = serializer.validated_data
334
335         try:
336             user = User.objects.get(
337                 username=d['email'],
338                 is_active=False
339             )
340         except User.DoesNotExist:
341             raise Http404
342
343         UserConfirmation.request(user)
344         return Response({})
345
346
347 class DeleteAccountView(GenericAPIView):
348     permission_classes = [IsAuthenticated]
349     serializer_class = serializers.DeleteAccountSerializer
350
351     def post(self, request):
352         u = request.user
353         serializer = self.get_serializer(
354             data=request.data,
355             context={'user': u}
356         )
357         serializer.is_valid(raise_exception=True)
358         d = serializer.validated_data
359         u.is_active = False
360         u.save()
361         return Response({})
362
363
364 class PasswordView(GenericAPIView):
365     permission_classes = [IsAuthenticated]
366     serializer_class = serializers.PasswordSerializer
367
368     def post(self, request):
369         u = request.user
370         serializer = self.get_serializer(
371             data=request.data,
372             context={'user': u}
373         )
374         serializer.is_valid(raise_exception=True)
375         d = serializer.validated_data
376         u.set_password(d['new_password'])
377         u.save()
378         return Response({})
379
380
381 class ResetPasswordView(GenericAPIView):
382     serializer_class = serializers.ResetPasswordSerializer
383
384     def post(self, request):
385         serializer = serializers.ResetPasswordSerializer(data=request.data)
386         serializer.is_valid(raise_exception=True)
387         form = ResetPasswordForm({"email": serializer.validated_data['email']})
388         form.is_valid()
389         form.save(request)
390         return Response({})
391
392
393 class SessionTransferTokenView(APIView):
394     permission_classes = [IsAuthenticated]
395
396     def post(self, request):
397         ott = SessionTransferToken.create_for_user(request.user)
398         return Response({
399             "token": str(ott.token)
400         })
401
402
403 class ConsumeSessionTransferTokenView(View):
404     def get(self, request):
405         token_str = request.GET.get("token")
406         next_url = request.GET.get("next", "/") #TODO: validate
407
408         if not token_str:
409             return HttpResponseBadRequest("Missing token")
410
411         try:
412             ott = SessionTransferToken.objects.get(token=token_str)
413         except SessionTransferToken.DoesNotExist:
414             return HttpResponseBadRequest("Invalid token")
415
416         if not ott.is_valid():
417             return HttpResponseForbidden("Token expired or already used")
418
419         # Mark token as used
420         ott.used = True
421         ott.save(update_fields=["used"])
422
423         # Log in the user via Django session
424         login(request, ott.user)
425
426         return redirect(next_url)