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