fixes
[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             'emailConfirmationRequired': settings.FEATURE_CONFIRM_USER,
291         })
292
293
294 class RefreshTokenView(APIView):
295     serializer_class = serializers.RefreshTokenSerializer
296
297     def post(self, request):
298         serializer = self.get_serializer(data=request.data)
299         serializer.is_valid(raise_exception=True)
300         d = serializer.validated_data
301         
302         t = Token.objects.get(
303             key=d['refresh_token'],
304             token_type=Token.REFRESH
305         )
306         user = t.user
307
308         access_token = generate_token()[:KEY_SIZE]
309         Token.objects.create(
310             key=access_token,
311             token_type=Token.ACCESS,
312             timestamp=time(),
313             user=user,
314         )
315         refresh_token = generate_token()[:KEY_SIZE]
316         Token.objects.create(
317             key=refresh_token,
318             token_type=Token.REFRESH,
319             timestamp=time(),
320             user=user,
321         )
322         return Response({
323             "access_token": access_token,
324             "refresh_token": refresh_token,
325             "expires": 3600,
326         })
327
328
329 class RequestConfirmView(APIView):
330     serializer_class = serializers.RequestConfirmSerializer
331
332     def post(self, request):
333         serializer = self.get_serializer(data=request.data)
334         serializer.is_valid(raise_exception=True)
335         d = serializer.validated_data
336
337         try:
338             user = User.objects.get(
339                 username=d['email'],
340                 is_active=False
341             )
342         except User.DoesNotExist:
343             raise Http404
344
345         UserConfirmation.request(user)
346         return Response({})
347
348
349 class DeleteAccountView(GenericAPIView):
350     permission_classes = [IsAuthenticated]
351     serializer_class = serializers.DeleteAccountSerializer
352
353     def post(self, request):
354         u = request.user
355         serializer = self.get_serializer(
356             data=request.data,
357             context={'user': u}
358         )
359         serializer.is_valid(raise_exception=True)
360         d = serializer.validated_data
361         u.is_active = False
362         u.save()
363         return Response({})
364
365
366 class PasswordView(GenericAPIView):
367     permission_classes = [IsAuthenticated]
368     serializer_class = serializers.PasswordSerializer
369
370     def post(self, request):
371         u = request.user
372         serializer = self.get_serializer(
373             data=request.data,
374             context={'user': u}
375         )
376         serializer.is_valid(raise_exception=True)
377         d = serializer.validated_data
378         u.set_password(d['new_password'])
379         u.save()
380         return Response({})
381
382
383 class ResetPasswordView(GenericAPIView):
384     serializer_class = serializers.ResetPasswordSerializer
385
386     def post(self, request):
387         serializer = serializers.ResetPasswordSerializer(data=request.data)
388         serializer.is_valid(raise_exception=True)
389         form = ResetPasswordForm({"email": serializer.validated_data['email']})
390         form.is_valid()
391         form.save(request)
392         return Response({})
393
394
395 class SessionTransferTokenView(APIView):
396     permission_classes = [IsAuthenticated]
397
398     def post(self, request):
399         ott = SessionTransferToken.create_for_user(request.user)
400         return Response({
401             "token": str(ott.token)
402         })
403
404
405 class ConsumeSessionTransferTokenView(View):
406     def get(self, request):
407         token_str = request.GET.get("token")
408         next_url = request.GET.get("next", "/") #TODO: validate
409
410         if not token_str:
411             return HttpResponseBadRequest("Missing token")
412
413         try:
414             ott = SessionTransferToken.objects.get(token=token_str)
415         except SessionTransferToken.DoesNotExist:
416             return HttpResponseBadRequest("Invalid token")
417
418         if not ott.is_valid():
419             return HttpResponseForbidden("Token expired or already used")
420
421         # Mark token as used
422         ott.used = True
423         ott.save(update_fields=["used"])
424
425         # Log in the user via Django session
426         login(request, ott.user)
427
428         return redirect(next_url)