Commit 07a9ebffbae02d0055aa182d75b2acd3b02156d2

Authored by Andrew Buss
Exists in master

merge

Showing 6 changed files Inline Diff

flashcards/serializers.py View file @ 07a9ebf
from flashcards.models import Section, LecturePeriod, User 1 1 from flashcards.models import Section, LecturePeriod, User, Flashcard
from rest_framework import serializers 2 2 from rest_framework import serializers
from rest_framework.fields import EmailField, BooleanField, CharField, IntegerField 3 3 from rest_framework.fields import EmailField, BooleanField, CharField, IntegerField
from rest_framework.relations import HyperlinkedRelatedField 4 4 from rest_framework.relations import HyperlinkedRelatedField
from rest_framework.serializers import HyperlinkedModelSerializer, ModelSerializer, Serializer 5 5 from rest_framework.serializers import HyperlinkedModelSerializer, ModelSerializer, Serializer
from rest_framework.validators import UniqueValidator 6 6 from rest_framework.validators import UniqueValidator
7 7
8 8
class EmailSerializer(Serializer): 9 9 class EmailSerializer(Serializer):
email = EmailField(required=True) 10 10 email = EmailField(required=True)
11 11
12 12
class EmailPasswordSerializer(EmailSerializer): 13 13 class EmailPasswordSerializer(EmailSerializer):
password = CharField(required=True) 14 14 password = CharField(required=True)
15 15
16 16
class RegistrationSerializer(EmailPasswordSerializer): 17 17 class RegistrationSerializer(EmailPasswordSerializer):
email = EmailField(required=True, validators=[UniqueValidator(queryset=User.objects.all())]) 18 18 email = EmailField(required=True, validators=[UniqueValidator(queryset=User.objects.all())])
19 19
20 20
class PasswordResetRequestSerializer(EmailSerializer): 21 21 class PasswordResetRequestSerializer(EmailSerializer):
def validate_email(self, value): 22 22 def validate_email(self, value):
try: 23 23 try:
User.objects.get(email=value) 24 24 User.objects.get(email=value)
return value 25 25 return value
except User.DoesNotExist: 26 26 except User.DoesNotExist:
raise serializers.ValidationError('No user exists with that email') 27 27 raise serializers.ValidationError('No user exists with that email')
28 28
29 29
class PasswordResetSerializer(Serializer): 30 30 class PasswordResetSerializer(Serializer):
new_password = CharField(required=True, allow_blank=False) 31 31 new_password = CharField(required=True, allow_blank=False)
uid = IntegerField(required=True) 32 32 uid = IntegerField(required=True)
token = CharField(required=True) 33 33 token = CharField(required=True)
34 34
def validate_uid(self, value): 35 35 def validate_uid(self, value):
try: 36 36 try:
User.objects.get(id=value) 37 37 User.objects.get(id=value)
return value 38 38 return value
except User.DoesNotExist: 39 39 except User.DoesNotExist:
raise serializers.ValidationError('Could not verify reset token') 40 40 raise serializers.ValidationError('Could not verify reset token')
41 41
class UserUpdateSerializer(Serializer): 42 42 class UserUpdateSerializer(Serializer):
old_password = CharField(required=False) 43 43 old_password = CharField(required=False)
new_password = CharField(required=False, allow_blank=False) 44 44 new_password = CharField(required=False, allow_blank=False)
confirmation_key = CharField(required=False) 45 45 confirmation_key = CharField(required=False)
# reset_token = CharField(required=False) 46 46 # reset_token = CharField(required=False)
47 47
def validate(self, data): 48 48 def validate(self, data):
if 'new_password' in data and 'old_password' not in data: 49 49 if 'new_password' in data and 'old_password' not in data:
raise serializers.ValidationError('old_password is required to set a new_password') 50 50 raise serializers.ValidationError('old_password is required to set a new_password')
return data 51 51 return data
52 52
53 53
class Password(Serializer): 54 54 class Password(Serializer):
email = EmailField(required=True) 55 55 email = EmailField(required=True)
password = CharField(required=True) 56 56 password = CharField(required=True)
57 57
58 58
class LecturePeriodSerializer(ModelSerializer): 59 59 class LecturePeriodSerializer(ModelSerializer):
class Meta: 60 60 class Meta:
model = LecturePeriod 61 61 model = LecturePeriod
exclude = 'id', 'section' 62 62 exclude = 'id', 'section'
63 63
64 64
flashcards/tests.py View file @ 07a9ebf
from django.test import TestCase 1 File was deleted
2
flashcards/tests/test_api.py View file @ 07a9ebf
from django.core import mail 1 1 from django.core import mail
2 <<<<<<< HEAD
from flashcards.models import User 2 3 from flashcards.models import User
from rest_framework.generics import RetrieveAPIView 3 4 from rest_framework.generics import RetrieveAPIView
5 =======
6 from flashcards.models import User, Section, Flashcard
7 >>>>>>> 2a9edd990f102b292ef4fb59c0688f6ed5ab56f5
from rest_framework.status import HTTP_204_NO_CONTENT, HTTP_201_CREATED, HTTP_200_OK, HTTP_401_UNAUTHORIZED 4 8 from rest_framework.status import HTTP_204_NO_CONTENT, HTTP_201_CREATED, HTTP_200_OK, HTTP_401_UNAUTHORIZED
from rest_framework.test import APITestCase 5 9 from rest_framework.test import APITestCase
from re import search 6 10 from re import search
11 from django.utils.timezone import now
7 12
8 13
class LoginTests(APITestCase): 9 14 class LoginTests(APITestCase):
def setUp(self): 10 15 def setUp(self):
email = "test@flashy.cards" 11 16 email = "test@flashy.cards"
User.objects.create_user(email=email, password="1234") 12 17 User.objects.create_user(email=email, password="1234")
13 18
def test_login(self): 14 19 def test_login(self):
url = '/api/login' 15 20 url = '/api/login'
data = {'email': 'test@flashy.cards', 'password': '1234'} 16 21 data = {'email': 'test@flashy.cards', 'password': '1234'}
response = self.client.post(url, data, format='json') 17 22 response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, HTTP_200_OK) 18 23 self.assertEqual(response.status_code, HTTP_200_OK)
19 24
data = {'email': 'test@flashy.cards', 'password': '54321'} 20 25 data = {'email': 'test@flashy.cards', 'password': '54321'}
response = self.client.post(url, data, format='json') 21 26 response = self.client.post(url, data, format='json')
self.assertContains(response, 'Invalid email or password', status_code=403) 22 27 self.assertContains(response, 'Invalid email or password', status_code=403)
23 28
data = {'email': 'none@flashy.cards', 'password': '54321'} 24 29 data = {'email': 'none@flashy.cards', 'password': '54321'}
response = self.client.post(url, data, format='json') 25 30 response = self.client.post(url, data, format='json')
self.assertContains(response, 'Invalid email or password', status_code=403) 26 31 self.assertContains(response, 'Invalid email or password', status_code=403)
27 32
data = {'password': '54321'} 28 33 data = {'password': '54321'}
response = self.client.post(url, data, format='json') 29 34 response = self.client.post(url, data, format='json')
self.assertContains(response, 'email', status_code=400) 30 35 self.assertContains(response, 'email', status_code=400)
31 36
data = {'email': 'none@flashy.cards'} 32 37 data = {'email': 'none@flashy.cards'}
response = self.client.post(url, data, format='json') 33 38 response = self.client.post(url, data, format='json')
self.assertContains(response, 'password', status_code=400) 34 39 self.assertContains(response, 'password', status_code=400)
35 40
user = User.objects.get(email="test@flashy.cards") 36 41 user = User.objects.get(email="test@flashy.cards")
user.is_active = False 37 42 user.is_active = False
user.save() 38 43 user.save()
39 44
data = {'email': 'test@flashy.cards', 'password': '1234'} 40 45 data = {'email': 'test@flashy.cards', 'password': '1234'}
response = self.client.post(url, data, format='json') 41 46 response = self.client.post(url, data, format='json')
self.assertContains(response, 'Account is disabled', status_code=403) 42 47 self.assertContains(response, 'Account is disabled', status_code=403)
43 48
def test_logout(self): 44 49 def test_logout(self):
url = '/api/login' 45 50 url = '/api/login'
data = {'email': 'test@flashy.cards', 'password': '1234'} 46 51 data = {'email': 'test@flashy.cards', 'password': '1234'}
response = self.client.post(url, data, format='json') 47 52 response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, HTTP_200_OK) 48 53 self.assertEqual(response.status_code, HTTP_200_OK)
49 54
p = self.client.post('/api/logout') 50 55 p = self.client.post('/api/logout')
self.assertEqual(p.status_code, HTTP_204_NO_CONTENT) 51 56 self.assertEqual(p.status_code, HTTP_204_NO_CONTENT)
response = self.client.get('/api/users/me', format='json') 52 57 response = self.client.get('/api/users/me', format='json')
53 58
# since we're not logged in, we shouldn't be able to see this 54 59 # since we're not logged in, we shouldn't be able to see this
self.assertEqual(response.status_code, HTTP_401_UNAUTHORIZED) 55 60 self.assertEqual(response.status_code, HTTP_401_UNAUTHORIZED)
56 61
57 62
class PasswordResetTest(APITestCase): 58 63 class PasswordResetTest(APITestCase):
def setUp(self): 59 64 def setUp(self):
# create a user to test things with 60 65 # create a user to test things with
email = "test@flashy.cards" 61 66 email = "test@flashy.cards"
User.objects.create_user(email=email, password="12345") 62 67 User.objects.create_user(email=email, password="12345")
63 68
def test_reset_password(self): 64 69 def test_reset_password(self):
# submit the request to reset the password 65 70 # submit the request to reset the password
url = '/api/reset_password' 66 71 url = '/api/reset_password'
post_data = {'email': 'test@flashy.cards'} 67 72 post_data = {'email': 'test@flashy.cards'}
self.client.post(url, post_data, format='json') 68 73 self.client.post(url, post_data, format='json')
self.assertEqual(len(mail.outbox), 1) 69 74 self.assertEqual(len(mail.outbox), 1)
self.assertIn('reset your password', mail.outbox[0].body) 70 75 self.assertIn('reset your password', mail.outbox[0].body)
71 76
# capture the reset token from the email 72 77 # capture the reset token from the email
capture = search('https://flashy.cards/app/reset_password/(\d+)/(.*)', 73 78 capture = search('https://flashy.cards/app/reset_password/(\d+)/(.*)',
mail.outbox[0].body) 74 79 mail.outbox[0].body)
patch_data = {'new_password': '54321'} 75 80 patch_data = {'new_password': '54321'}
patch_data['uid'] = capture.group(1) 76 81 patch_data['uid'] = capture.group(1)
reset_token = capture.group(2) 77 82 reset_token = capture.group(2)
78 83
# try to reset the password with the wrong reset token 79 84 # try to reset the password with the wrong reset token
patch_data['token'] = 'wrong_token' 80 85 patch_data['token'] = 'wrong_token'
response = self.client.patch(url, patch_data, format='json') 81 86 response = self.client.patch(url, patch_data, format='json')
self.assertContains(response, 'Could not verify reset token', status_code=400) 82 87 self.assertContains(response, 'Could not verify reset token', status_code=400)
83 88
# try to reset the password with the correct token 84 89 # try to reset the password with the correct token
patch_data['token'] = reset_token 85 90 patch_data['token'] = reset_token
response = self.client.patch(url, patch_data, format='json') 86 91 response = self.client.patch(url, patch_data, format='json')
self.assertEqual(response.status_code, HTTP_204_NO_CONTENT) 87 92 self.assertEqual(response.status_code, HTTP_204_NO_CONTENT)
user = User.objects.get(id=patch_data['uid']) 88 93 user = User.objects.get(id=patch_data['uid'])
assert user.check_password(patch_data['new_password']) 89 94 assert user.check_password(patch_data['new_password'])
90 95
91 96
class RegistrationTest(APITestCase): 92 97 class RegistrationTest(APITestCase):
def test_create_account(self): 93 98 def test_create_account(self):
url = '/api/users/me' 94 99 url = '/api/users/me'
95 100
# missing password 96 101 # missing password
data = {'email': 'none@none.com'} 97 102 data = {'email': 'none@none.com'}
response = self.client.post(url, data, format='json') 98 103 response = self.client.post(url, data, format='json')
self.assertContains(response, 'password', status_code=400) 99 104 self.assertContains(response, 'password', status_code=400)
100 105
# missing email 101 106 # missing email
data = {'password': '1234'} 102 107 data = {'password': '1234'}
response = self.client.post(url, data, format='json') 103 108 response = self.client.post(url, data, format='json')
self.assertContains(response, 'email', status_code=400) 104 109 self.assertContains(response, 'email', status_code=400)
105 110
# create a user 106 111 # create a user
data = {'email': 'none@none.com', 'password': '1234'} 107 112 data = {'email': 'none@none.com', 'password': '1234'}
response = self.client.post(url, data, format='json') 108 113 response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, HTTP_201_CREATED) 109 114 self.assertEqual(response.status_code, HTTP_201_CREATED)
110 115
# user should not be confirmed 111 116 # user should not be confirmed
user = User.objects.get(email="none@none.com") 112 117 user = User.objects.get(email="none@none.com")
self.assertFalse(user.is_confirmed) 113 118 self.assertFalse(user.is_confirmed)
114 119
# check that the confirmation key was sent 115 120 # check that the confirmation key was sent
self.assertEqual(len(mail.outbox), 1) 116 121 self.assertEqual(len(mail.outbox), 1)
self.assertIn(user.confirmation_key, mail.outbox[0].body) 117 122 self.assertIn(user.confirmation_key, mail.outbox[0].body)
118 123
# log the user out 119 124 # log the user out
self.client.logout() 120 125 self.client.logout()
121 126
# log the user in with their registered credentials 122 127 # log the user in with their registered credentials
self.client.login(email='none@none.com', password='1234') 123 128 self.client.login(email='none@none.com', password='1234')
124 129
# try activating with an invalid key 125 130 # try activating with an invalid key
response = self.client.patch(url, {'confirmation_key': 'NOT A KEY'}) 126 131 response = self.client.patch(url, {'confirmation_key': 'NOT A KEY'})
self.assertContains(response, 'confirmation_key is invalid', status_code=400) 127 132 self.assertContains(response, 'confirmation_key is invalid', status_code=400)
128 133
# try activating with the valid key 129 134 # try activating with the valid key
response = self.client.patch(url, {'confirmation_key': user.confirmation_key}) 130 135 response = self.client.patch(url, {'confirmation_key': user.confirmation_key})
self.assertTrue(response.data['is_confirmed']) 131 136 self.assertTrue(response.data['is_confirmed'])
132 137
133 138
class ProfileViewTest(APITestCase): 134 139 class ProfileViewTest(APITestCase):
def setUp(self): 135 140 def setUp(self):
email = "profileviewtest@flashy.cards" 136 141 email = "profileviewtest@flashy.cards"
User.objects.create_user(email=email, password="1234") 137 142 User.objects.create_user(email=email, password="1234")
138 143
def test_get_me(self): 139 144 def test_get_me(self):
url = '/api/users/me' 140 145 url = '/api/users/me'
response = self.client.get(url, format='json') 141 146 response = self.client.get(url, format='json')
# since we're not logged in, we shouldn't be able to see this 142 147 # since we're not logged in, we shouldn't be able to see this
self.assertEqual(response.status_code, HTTP_401_UNAUTHORIZED) 143 148 self.assertEqual(response.status_code, HTTP_401_UNAUTHORIZED)
144 149
self.client.login(email='profileviewtest@flashy.cards', password='1234') 145 150 self.client.login(email='profileviewtest@flashy.cards', password='1234')
response = self.client.get(url, format='json') 146 151 response = self.client.get(url, format='json')
self.assertEqual(response.status_code, HTTP_200_OK) 147 152 self.assertEqual(response.status_code, HTTP_200_OK)
148 153
149 154
class PasswordChangeTest(APITestCase): 150 155 class PasswordChangeTest(APITestCase):
def setUp(self): 151 156 def setUp(self):
email = "none@none.com" 152 157 email = "none@none.com"
User.objects.create_user(email=email, password="1234") 153 158 User.objects.create_user(email=email, password="1234")
154 159
def test_change_password(self): 155 160 def test_change_password(self):
url = '/api/users/me' 156 161 url = '/api/users/me'
user = User.objects.get(email='none@none.com') 157 162 user = User.objects.get(email='none@none.com')
self.assertTrue(user.check_password('1234')) 158 163 self.assertTrue(user.check_password('1234'))
159 164
response = self.client.patch(url, {'new_password': '4321', 'old_password': '1234'}, format='json') 160 165 response = self.client.patch(url, {'new_password': '4321', 'old_password': '1234'}, format='json')
self.assertContains(response, 'You must be logged in to change your password', status_code=403) 161 166 self.assertContains(response, 'You must be logged in to change your password', status_code=403)
162 167
self.client.login(email='none@none.com', password='1234') 163 168 self.client.login(email='none@none.com', password='1234')
response = self.client.patch(url, {'new_password': '4321'}, format='json') 164 169 response = self.client.patch(url, {'new_password': '4321'}, format='json')
self.assertContains(response, 'old_password is required', status_code=400) 165 170 self.assertContains(response, 'old_password is required', status_code=400)
flashcards/views.py View file @ 07a9ebf
from flashcards.api import StandardResultsSetPagination 1 1 from flashcards.api import StandardResultsSetPagination
from flashcards.models import Section, User 2 2 from flashcards.models import Section, User, Flashcard
from flashcards.serializers import SectionSerializer, UserUpdateSerializer, RegistrationSerializer, UserSerializer, \ 3 3 from flashcards.serializers import SectionSerializer, UserUpdateSerializer, RegistrationSerializer, UserSerializer, \
PasswordResetSerializer, PasswordResetRequestSerializer, EmailPasswordSerializer 4 4 PasswordResetSerializer, PasswordResetRequestSerializer, EmailPasswordSerializer, FlashcardSerializer
from rest_framework.permissions import IsAuthenticated 5 5 from rest_framework.permissions import IsAuthenticated
from rest_framework.viewsets import ReadOnlyModelViewSet, ModelViewSet 6 6 from rest_framework.viewsets import ReadOnlyModelViewSet, ModelViewSet
from django.core.mail import send_mail 7 7 from django.core.mail import send_mail
from django.contrib.auth import authenticate, login, logout 8 8 from django.contrib.auth import authenticate, login, logout
from django.contrib.auth.tokens import default_token_generator 9 9 from django.contrib.auth.tokens import default_token_generator
from rest_framework.status import HTTP_204_NO_CONTENT, HTTP_401_UNAUTHORIZED, HTTP_201_CREATED 10 10 from rest_framework.status import HTTP_204_NO_CONTENT, HTTP_401_UNAUTHORIZED, HTTP_201_CREATED
from rest_framework.views import APIView 11 11 from rest_framework.views import APIView
from rest_framework.response import Response 12 12 from rest_framework.response import Response
13 from rest_framework.generics import RetrieveAPIView
from rest_framework.exceptions import AuthenticationFailed, NotAuthenticated, ValidationError 13 14 from rest_framework.exceptions import AuthenticationFailed, NotAuthenticated, ValidationError
from simple_email_confirmation import EmailAddress 14 15 from simple_email_confirmation import EmailAddress
from rest_framework import filters 15 16 from rest_framework import filters
16 17
class SectionViewSet(ReadOnlyModelViewSet): 17 18 class SectionViewSet(ReadOnlyModelViewSet):
queryset = Section.objects.all() 18 19 queryset = Section.objects.all()
serializer_class = SectionSerializer 19 20 serializer_class = SectionSerializer
pagination_class = StandardResultsSetPagination 20 21 pagination_class = StandardResultsSetPagination
21 22
class UserSectionViewSet(ModelViewSet): 22 23 class UserSectionViewSet(ModelViewSet):
serializer_class = SectionSerializer 23 24 serializer_class = SectionSerializer
permission_classes = [IsAuthenticated] 24 25 permission_classes = [IsAuthenticated]
def get_queryset(self): 25 26 def get_queryset(self):
return self.request.user.sections.all() 26 27 return self.request.user.sections.all()
27 28
def paginate_queryset(self, queryset): return None 28 29 def paginate_queryset(self, queryset): return None
29 30
class UserDetail(APIView): 30 31 class UserDetail(APIView):
def patch(self, request, format=None): 31 32 def patch(self, request, format=None):
""" 32 33 """
Updates the user's password, or verifies their email address 33 34 Updates the user's password, or verifies their email address
--- 34 35 ---
request_serializer: UserUpdateSerializer 35 36 request_serializer: UserUpdateSerializer
response_serializer: UserSerializer 36 37 response_serializer: UserSerializer
""" 37 38 """
data = UserUpdateSerializer(data=request.data, context={'user': request.user}) 38 39 data = UserUpdateSerializer(data=request.data, context={'user': request.user})
data.is_valid(raise_exception=True) 39 40 data.is_valid(raise_exception=True)
data = data.validated_data 40 41 data = data.validated_data
41 42
if 'new_password' in data: 42 43 if 'new_password' in data:
if not request.user.is_authenticated(): 43 44 if not request.user.is_authenticated():
raise NotAuthenticated('You must be logged in to change your password') 44 45 raise NotAuthenticated('You must be logged in to change your password')
if not request.user.check_password(data['old_password']): 45 46 if not request.user.check_password(data['old_password']):
raise ValidationError('old_password is incorrect') 46 47 raise ValidationError('old_password is incorrect')
request.user.set_password(data['new_password']) 47 48 request.user.set_password(data['new_password'])
request.user.save() 48 49 request.user.save()
49 50
if 'confirmation_key' in data: 50 51 if 'confirmation_key' in data:
try: 51 52 try:
request.user.confirm_email(data['confirmation_key']) 52 53 request.user.confirm_email(data['confirmation_key'])
except EmailAddress.DoesNotExist: 53 54 except EmailAddress.DoesNotExist:
raise ValidationError('confirmation_key is invalid') 54 55 raise ValidationError('confirmation_key is invalid')
55 56
return Response(UserSerializer(request.user).data) 56 57 return Response(UserSerializer(request.user).data)
57 58
def get(self, request, format=None): 58 59 def get(self, request, format=None):
""" 59 60 """
Return data about the user 60 61 Return data about the user
--- 61 62 ---
response_serializer: UserSerializer 62 63 response_serializer: UserSerializer
""" 63 64 """
if not request.user.is_authenticated(): return Response(status=HTTP_401_UNAUTHORIZED) 64 65 if not request.user.is_authenticated(): return Response(status=HTTP_401_UNAUTHORIZED)
serializer = UserSerializer(request.user) 65 66 serializer = UserSerializer(request.user)
return Response(serializer.data) 66 67 return Response(serializer.data)
67 68
def post(self, request, format=None): 68 69 def post(self, request, format=None):
""" 69 70 """
Register a new user 70 71 Register a new user
--- 71 72 ---
request_serializer: EmailPasswordSerializer 72 73 request_serializer: EmailPasswordSerializer
response_serializer: UserSerializer 73 74 response_serializer: UserSerializer
""" 74 75 """
data = RegistrationSerializer(data=request.data) 75 76 data = RegistrationSerializer(data=request.data)
data.is_valid(raise_exception=True) 76 77 data.is_valid(raise_exception=True)
77 78
User.objects.create_user(**data.validated_data) 78 79 User.objects.create_user(**data.validated_data)
user = authenticate(**data.validated_data) 79 80 user = authenticate(**data.validated_data)
login(request, user) 80 81 login(request, user)
81 82
body = ''' 82 83 body = '''
Visit the following link to confirm your email address: 83 84 Visit the following link to confirm your email address:
https://flashy.cards/app/verify_email/%s 84 85 https://flashy.cards/app/verify_email/%s
85 86
If you did not register for Flashy, no action is required. 86 87 If you did not register for Flashy, no action is required.
''' 87 88 '''
88 89
assert send_mail("Flashy email verification", 89 90 assert send_mail("Flashy email verification",
body % user.confirmation_key, 90 91 body % user.confirmation_key,
"noreply@flashy.cards", 91 92 "noreply@flashy.cards",
[user.email]) 92 93 [user.email])
93 94
return Response(UserSerializer(request.user).data, status=HTTP_201_CREATED) 94 95 return Response(UserSerializer(request.user).data, status=HTTP_201_CREATED)
95 96
def delete(self, request): 96 97 def delete(self, request):
""" 97 98 """
Irrevocably delete the user and their data 98 99 Irrevocably delete the user and their data
99 100
Yes, really 100 101 Yes, really
""" 101 102 """
request.user.delete() 102 103 request.user.delete()
return Response(status=HTTP_204_NO_CONTENT) 103 104 return Response(status=HTTP_204_NO_CONTENT)
104 105
105 106
class UserLogin(APIView): 106 107 class UserLogin(APIView):
def post(self, request): 107 108 def post(self, request):
""" 108 109 """
Authenticates user and returns user data if valid. 109 110 Authenticates user and returns user data if valid.
--- 110 111 ---
request_serializer: EmailPasswordSerializer 111 112 request_serializer: EmailPasswordSerializer
response_serializer: UserSerializer 112 113 response_serializer: UserSerializer
""" 113 114 """
114 115
data = EmailPasswordSerializer(data=request.data) 115 116 data = EmailPasswordSerializer(data=request.data)
data.is_valid(raise_exception=True) 116 117 data.is_valid(raise_exception=True)
user = authenticate(**data.validated_data) 117 118 user = authenticate(**data.validated_data)
118 119
if user is None: 119 120 if user is None:
raise AuthenticationFailed('Invalid email or password') 120 121 raise AuthenticationFailed('Invalid email or password')
if not user.is_active: 121 122 if not user.is_active:
raise NotAuthenticated('Account is disabled') 122 123 raise NotAuthenticated('Account is disabled')
login(request, user) 123 124 login(request, user)
return Response(UserSerializer(request.user).data) 124 125 return Response(UserSerializer(request.user).data)
125 126
126 127
class UserLogout(APIView): 127 128 class UserLogout(APIView):
permission_classes = (IsAuthenticated,) 128 129 permission_classes = (IsAuthenticated,)
def post(self, request, format=None): 129 130 def post(self, request, format=None):
""" 130 131 """
Logs the authenticated user out. 131 132 Logs the authenticated user out.
""" 132 133 """
logout(request) 133 134 logout(request)
return Response(status=HTTP_204_NO_CONTENT) 134 135 return Response(status=HTTP_204_NO_CONTENT)
135 136
136 137
class PasswordReset(APIView): 137 138 class PasswordReset(APIView):
""" 138 139 """
Allows user to reset their password. 139 140 Allows user to reset their password.
""" 140 141 """
141 142
def post(self, request, format=None): 142 143 def post(self, request, format=None):
""" 143 144 """
Send a password reset token/link to the provided email. 144 145 Send a password reset token/link to the provided email.
--- 145 146 ---
request_serializer: PasswordResetRequestSerializer 146 147 request_serializer: PasswordResetRequestSerializer
""" 147 148 """
data = PasswordResetRequestSerializer(data=request.data) 148 149 data = PasswordResetRequestSerializer(data=request.data)
data.is_valid(raise_exception=True) 149 150 data.is_valid(raise_exception=True)
user = User.objects.get(email=data['email'].value) 150 151 user = User.objects.get(email=data['email'].value)
token = default_token_generator.make_token(user) 151 152 token = default_token_generator.make_token(user)
152 153
body = ''' 153 154 body = '''
Visit the following link to reset your password: 154 155 Visit the following link to reset your password:
https://flashy.cards/app/reset_password/%d/%s 155 156 https://flashy.cards/app/reset_password/%d/%s
156 157
If you did not request a password reset, no action is required. 157 158 If you did not request a password reset, no action is required.
''' 158 159 '''
159 160
send_mail("Flashy password reset", 160 161 send_mail("Flashy password reset",
body % (user.pk, token), 161 162 body % (user.pk, token),
"noreply@flashy.cards", 162 163 "noreply@flashy.cards",
flashy/urls.py View file @ 07a9ebf
from django.conf.urls import include, url 1 1 from django.conf.urls import include, url
from django.contrib import admin 2 2 from django.contrib import admin
from flashcards.views import SectionViewSet, UserDetail, UserLogin, UserLogout, PasswordReset, UserSectionViewSet 3 3 from flashcards.views import SectionViewSet, UserDetail, UserLogin, UserLogout, PasswordReset, UserSectionViewSet, FlashcardDetail
from rest_framework.routers import DefaultRouter 4 4 from rest_framework.routers import DefaultRouter
from flashcards.api import * 5 5 from flashcards.api import *
6 6
router = DefaultRouter() 7 7 router = DefaultRouter()
router.register(r'sections', SectionViewSet) 8 8 router.register(r'sections', SectionViewSet)
9 9
router.register(r'users/me/sections', UserSectionViewSet, base_name = 'usersection') 10 10 router.register(r'users/me/sections', UserSectionViewSet, base_name = 'usersection')
11 11
urlpatterns = [ 12 12 urlpatterns = [
url(r'^api/docs/', include('rest_framework_swagger.urls')), 13 13 url(r'^api/docs/', include('rest_framework_swagger.urls')),
url(r'^api/users/me$', UserDetail.as_view()), 14 14 url(r'^api/users/me$', UserDetail.as_view()),
url(r'^api/login$', UserLogin.as_view()), 15 15 url(r'^api/login$', UserLogin.as_view()),
url(r'^api/logout$', UserLogout.as_view()), 16 16 url(r'^api/logout$', UserLogout.as_view()),
url(r'^api/reset_password$', PasswordReset.as_view()), 17 17 url(r'^api/reset_password$', PasswordReset.as_view()),
url(r'^api/', include(router.urls)), 18 18 url(r'^api/', include(router.urls)),
url(r'^admin/doc/', include('django.contrib.admindocs.urls')), 19 19 url(r'^admin/doc/', include('django.contrib.admindocs.urls')),
url(r'^admin/', include(admin.site.urls)), 20 20 url(r'^admin/', include(admin.site.urls)),
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework' 21 21 url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework'
)) 22 22 )),
23 url(r'^api/flashcards/(?P<pk>[0-9]+)$', FlashcardDetail.as_view(), name \
24 ="flashcard-detail"),
] 23 25 ]
nginxconf/flashy.cards View file @ 07a9ebf
upstream backend_production { 1 1 upstream backend_production {
# server unix:/tmp/flashy.sock; 2 2 # server unix:/tmp/flashy.sock;
server localhost:7002; 3 3 server localhost:7002;
} 4 4 }
5 5
server { 6 6 server {
server_name dev.flashy.cards; 7 7 server_name dev.flashy.cards;
listen 80; 8 8 listen 80;
location ^~ /app/ { 9 9 location ^~ /app/ {
alias /srv/flashy-frontend/; 10 10 alias /srv/flashy-frontend/;
try_files $uri /app/home.html; 11 11 try_files $uri /app/home.html;
} 12 12 }
location ~ /(api|admin|api-auth)/ { 13 13 location ~ /(api|admin|api-auth)/ {
add_header 'Access-Control-Allow-Origin' '*'; 14 14 add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Credentials' 'true'; 15 15 add_header 'Access-Control-Allow-Credentials' 'true';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, PATCH, PUT'; 16 16 add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, PATCH, PUT';
add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,Origin,Authorization,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,X-CSRFToken'; 17 17 add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,Origin,Authorization,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,X-CSRFToken';
proxy_pass http://backend_production; 18 18 proxy_pass http://backend_production;
proxy_redirect http://backend_production $scheme://flashy.cards; 19 19 proxy_redirect http://backend_production $scheme://flashy.cards;
proxy_set_header Host $host; 20 20 proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; 21 21 proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 22 22 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme; 23 23 proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header REMOTE_ADDR $remote_addr; 24 24 proxy_set_header REMOTE_ADDR $remote_addr;
} 25 25 }
} 26 26 }
27 27
server { 28 28 server {
29 29
server_name flashy.cards; 30 30 server_name flashy.cards;
listen 443 ssl; 31 31 listen 443 ssl;
location ~ \.php$ { 32 32
33
34 location / {
35 root /srv/flashy.cards/;
36 index index.html /docs/_h5ai/server/php/index.php;
37 location ~ \.php$ {
try_files $uri =404; 33 38 try_files $uri =404;
fastcgi_pass unix:/var/run/php5-fpm.sock; 34 39 fastcgi_pass unix:/var/run/php5-fpm.sock;
fastcgi_index index.php; 35
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; 36
include fastcgi_params; 37 40 include fastcgi_params;
41 }
} 38 42 }
39 43
location / { 40
root /srv/flashy.cards/; 41
} 42
43
location ^~ /static { 44 44 location ^~ /static {
root /srv/; 45 45 root /srv/;
access_log off; 46 46 access_log off;
expires 30d; 47 47 expires 30d;
} 48 48 }
49 49
location ^~ /app/ { 50 50 location ^~ /app/ {
alias /srv/flashy-frontend/; 51 51 alias /srv/flashy-frontend/;
try_files $uri /app/home.html; 52 52 try_files $uri /app/home.html;
} 53 53 }
54 54
location ~ /(api|admin|api-auth)/ { 55 55 location ~ /(api|admin|api-auth)/ {
add_header 'Access-Control-Allow-Origin' 'http://localhost'; 56 56 add_header 'Access-Control-Allow-Origin' 'http://localhost';
add_header 'Access-Control-Allow-Credentials' 'true'; 57 57 add_header 'Access-Control-Allow-Credentials' 'true';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, PATCH, PUT'; 58 58 add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, PATCH, PUT';
add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,Origin,Authorization,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,X-CSRFToken'; 59 59 add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,Origin,Authorization,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,X-CSRFToken';
proxy_pass http://backend_production; 60 60 proxy_pass http://backend_production;
proxy_redirect http://backend_production $scheme://flashy.cards; 61 61 proxy_redirect http://backend_production $scheme://flashy.cards;
proxy_set_header Host $host; 62 62 proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; 63 63 proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 64 64 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme; 65 65 proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header REMOTE_ADDR $remote_addr; 66 66 proxy_set_header REMOTE_ADDR $remote_addr;
} 67 67 }
68 68
location ^~ /jenkins { 69 69 location ^~ /jenkins {
proxy_pass http://localhost:8080; 70 70 proxy_pass http://localhost:8080;
proxy_redirect http://localhost:8080 $scheme://flashy.cards; 71 71 proxy_redirect http://localhost:8080 $scheme://flashy.cards;
proxy_set_header Host $host; 72 72 proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; 73 73 proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 74 74 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme; 75 75 proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 90; 76 76 proxy_read_timeout 90;
} 77 77 }
ssl_certificate /etc/nginx/ssl/bundle.crt; 78 78 ssl_certificate /etc/nginx/ssl/bundle.crt;
ssl_certificate_key /etc/nginx/ssl/nginx.key; 79 79 ssl_certificate_key /etc/nginx/ssl/nginx.key;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2; 80 80 ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:ECDH+3DES:DH+3DES:RSA+AESGCM:RSA+AES:RSA+3DES:!aNULL:!MD5:!DSS; 81 81 ssl_ciphers ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:ECDH+3DES:DH+3DES:RSA+AESGCM:RSA+AES:RSA+3DES:!aNULL:!MD5:!DSS;
ssl_prefer_server_ciphers on; 82 82 ssl_prefer_server_ciphers on;
keepalive_timeout 70; 83 83 keepalive_timeout 70;
ssl_session_cache shared:SSL:10m; 84 84 ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m; 85 85 ssl_session_timeout 10m;
add_header Strict-Transport-Security "max-age=259200"; 86 86 add_header Strict-Transport-Security "max-age=259200";