Commit 07a9ebffbae02d0055aa182d75b2acd3b02156d2
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"; |