Commit 8fa22f31e3ed3a9f389dd5045ecf3f68999eb021
Exists in
master
Merge branch 'master' of https://git.ucsd.edu/110swag/flashy-backend
Showing 15 changed files Side-by-side Diff
README.md
View file @
8fa22f3
1 | -Set up the environment by running setup.sh | |
1 | +Set up the environment by running: | |
2 | 2 | |
3 | + scripts/setup.sh | |
4 | + | |
3 | 5 | If you get errors about a module not being found, make sure you are working in the virtualenv. To enter the venv: |
4 | 6 | |
5 | 7 | . venv/bin/activate |
6 | 8 | |
9 | +If you still get errors about a module not being found, make sure your virtualenv is up to date. Re-run: | |
10 | + | |
11 | + scripts/setup.sh | |
12 | + | |
7 | 13 | Run tests: |
8 | 14 | |
9 | - ./run_tests.sh | |
15 | + scripts/run_tests.sh | |
10 | 16 | |
11 | 17 | Run development server (local): |
12 | 18 |
flashcards/api.py
View file @
8fa22f3
flashcards/tests/test_api.py
View file @
8fa22f3
1 | +from django.core import mail | |
1 | 2 | from flashcards.models import User |
2 | -from rest_framework.status import HTTP_201_CREATED, HTTP_200_OK, HTTP_403_FORBIDDEN, HTTP_401_UNAUTHORIZED | |
3 | +from rest_framework.status import HTTP_201_CREATED, HTTP_200_OK, HTTP_401_UNAUTHORIZED | |
3 | 4 | from rest_framework.test import APITestCase |
5 | +from re import search | |
4 | 6 | |
5 | 7 | |
6 | 8 | class LoginTests(APITestCase): |
7 | 9 | |
8 | 10 | |
9 | 11 | |
10 | 12 | |
11 | 13 | |
12 | 14 | |
... | ... | @@ -26,27 +28,91 @@ |
26 | 28 | response = self.client.post(url, data, format='json') |
27 | 29 | self.assertContains(response, 'email', status_code=400) |
28 | 30 | |
31 | + data = {'email': 'none@flashy.cards'} | |
32 | + response = self.client.post(url, data, format='json') | |
33 | + self.assertContains(response, 'password', status_code=400) | |
29 | 34 | |
35 | + user = User.objects.get(email="test@flashy.cards") | |
36 | + user.is_active = False | |
37 | + user.save() | |
38 | + | |
39 | + data = {'email': 'test@flashy.cards', 'password': '1234'} | |
40 | + response = self.client.post(url, data, format='json') | |
41 | + self.assertContains(response, 'Account is disabled', status_code=403) | |
42 | + | |
43 | + def test_logout(self): | |
44 | + self.client.login(email='none@none.com', password='1234') | |
45 | + self.client.post('/api/logout') | |
46 | + | |
47 | + response = self.client.get('/api/users/me', format='json') | |
48 | + # since we're not logged in, we shouldn't be able to see this | |
49 | + self.assertEqual(response.status_code, HTTP_401_UNAUTHORIZED) | |
50 | + | |
51 | +class PasswordResetTest(APITestCase): | |
52 | + def setUp(self): | |
53 | + email = "test@flashy.cards" | |
54 | + User.objects.create_user(email=email, password="12345") | |
55 | + | |
56 | + def test_reset_password(self): | |
57 | + url = '/api/reset_password' | |
58 | + post_data = {'email': 'test@flashy.cards'} | |
59 | + patch_data = {'new_password': '54321', | |
60 | + 'uid': '', 'token': ''} | |
61 | + self.client.post(url, post_data, format='json') | |
62 | + self.assertEqual(len(mail.outbox), 1) | |
63 | + self.assertIn('reset your password', mail.outbox[0].body) | |
64 | + | |
65 | + capture = search('https://flashy.cards/app/reset_password/(\d+)/(.*)', | |
66 | + mail.outbox[0].body) | |
67 | + patch_data['uid'] = capture.group(1) | |
68 | + patch_data['token'] = capture.group(2) | |
69 | + self.client.patch(url, patch_data, format='json') | |
70 | + user = User.objects.get(id=patch_data['uid']) | |
71 | + assert user.check_password(patch_data['new_password']) | |
72 | + | |
73 | + | |
30 | 74 | class RegistrationTest(APITestCase): |
31 | 75 | def test_create_account(self): |
32 | 76 | url = '/api/users/me' |
33 | - data = {'email': 'none@none.com', 'password': '1234'} | |
34 | - response = self.client.post(url, data, format='json') | |
35 | - self.assertEqual(response.status_code, HTTP_201_CREATED) | |
36 | 77 | |
37 | - data = {'email': 'none@none.com', 'password': '1234'} | |
38 | - response = self.client.post('/api/login', data, format='json') | |
39 | - self.assertEqual(response.status_code, HTTP_200_OK) | |
40 | - | |
78 | + # missing password | |
41 | 79 | data = {'email': 'none@none.com'} |
42 | 80 | response = self.client.post(url, data, format='json') |
43 | 81 | self.assertContains(response, 'password', status_code=400) |
44 | 82 | |
83 | + # missing email | |
45 | 84 | data = {'password': '1234'} |
46 | 85 | response = self.client.post(url, data, format='json') |
47 | 86 | self.assertContains(response, 'email', status_code=400) |
48 | 87 | |
88 | + # create a user | |
89 | + data = {'email': 'none@none.com', 'password': '1234'} | |
90 | + response = self.client.post(url, data, format='json') | |
91 | + self.assertEqual(response.status_code, HTTP_201_CREATED) | |
49 | 92 | |
93 | + # user should not be confirmed | |
94 | + user = User.objects.get(email="none@none.com") | |
95 | + self.assertFalse(user.is_confirmed) | |
96 | + | |
97 | + # check that the confirmation key was sent | |
98 | + self.assertEqual(len(mail.outbox), 1) | |
99 | + self.assertIn(user.confirmation_key, mail.outbox[0].body) | |
100 | + | |
101 | + # log the user out | |
102 | + self.client.logout() | |
103 | + | |
104 | + # log the user in with their registered credentials | |
105 | + self.client.login(email='none@none.com', password='1234') | |
106 | + | |
107 | + # try activating with an invalid key | |
108 | + response = self.client.patch(url, {'confirmation_key': 'NOT A KEY'}) | |
109 | + self.assertContains(response, 'confirmation_key is invalid', status_code=400) | |
110 | + | |
111 | + # try activating with the valid key | |
112 | + response = self.client.patch(url, {'confirmation_key': user.confirmation_key}) | |
113 | + self.assertTrue(response.data['is_confirmed']) | |
114 | + | |
115 | + | |
50 | 116 | class ProfileViewTest(APITestCase): |
51 | 117 | def setUp(self): |
52 | 118 | email = "profileviewtest@flashy.cards" |
... | ... | @@ -61,4 +127,47 @@ |
61 | 127 | self.client.login(email='profileviewtest@flashy.cards', password='1234') |
62 | 128 | response = self.client.get(url, format='json') |
63 | 129 | self.assertEqual(response.status_code, HTTP_200_OK) |
130 | + | |
131 | + | |
132 | +class PasswordChangeTest(APITestCase): | |
133 | + def setUp(self): | |
134 | + email = "none@none.com" | |
135 | + User.objects.create_user(email=email, password="1234") | |
136 | + | |
137 | + def test_change_password(self): | |
138 | + url = '/api/users/me' | |
139 | + user = User.objects.get(email='none@none.com') | |
140 | + self.assertTrue(user.check_password('1234')) | |
141 | + | |
142 | + response = self.client.patch(url, {'new_password': '4321', 'old_password': '1234'}, format='json') | |
143 | + self.assertContains(response, 'You must be logged in to change your password', status_code=403) | |
144 | + | |
145 | + self.client.login(email='none@none.com', password='1234') | |
146 | + response = self.client.patch(url, {'new_password': '4321'}, format='json') | |
147 | + self.assertContains(response, 'old_password is required', status_code=400) | |
148 | + | |
149 | + response = self.client.patch(url, {'new_password': '4321', 'old_password': '4321'}, format='json') | |
150 | + self.assertContains(response, 'old_password is incorrect', status_code=400) | |
151 | + | |
152 | + response = self.client.patch(url, {'new_password': '4321', 'old_password': '1234'}, format='json') | |
153 | + self.assertEqual(response.status_code, 200) | |
154 | + user = User.objects.get(email='none@none.com') | |
155 | + | |
156 | + self.assertFalse(user.check_password('1234')) | |
157 | + self.assertTrue(user.check_password('4321')) | |
158 | + | |
159 | + | |
160 | +class DeleteUserTest(APITestCase): | |
161 | + def setUp(self): | |
162 | + email = "none@none.com" | |
163 | + User.objects.create_user(email=email, password="1234") | |
164 | + | |
165 | + def test_delete_user(self): | |
166 | + url = '/api/users/me' | |
167 | + user = User.objects.get(email='none@none.com') | |
168 | + | |
169 | + self.client.login(email='none@none.com', password='1234') | |
170 | + self.client.delete(url) | |
171 | + | |
172 | + self.assertFalse(User.objects.filter(email='none@none.com').exists()) |
flashcards/tests/test_models.py
View file @
8fa22f3
1 | 1 | from django.test import TestCase |
2 | 2 | from flashcards.models import User, Section |
3 | +from simple_email_confirmation import EmailAddress | |
3 | 4 | |
4 | 5 | |
6 | +class RegistrationTests(TestCase): | |
7 | + def setUp(self): | |
8 | + User.objects.create_user(email="none@none.com", password="1234") | |
9 | + | |
10 | + def test_email_confirmation(self): | |
11 | + user = User.objects.get(email="none@none.com") | |
12 | + self.assertFalse(user.is_confirmed) | |
13 | + user.confirm_email(user.confirmation_key) | |
14 | + self.assertTrue(user.is_confirmed) | |
15 | + | |
16 | + | |
5 | 17 | class UserTests(TestCase): |
6 | 18 | def setUp(self): |
7 | - User.objects.create(email="none@none.com", password="1234") | |
19 | + User.objects.create_user(email="none@none.com", password="1234") | |
8 | 20 | Section.objects.create(department='dept', |
9 | 21 | course_num='101a', |
10 | 22 | course_title='how 2 test', |
... | ... | @@ -19,4 +31,6 @@ |
19 | 31 | self.assertIn(section, user.sections.all()) |
20 | 32 | user.sections.add(section) |
21 | 33 | self.assertEqual(user.sections.count(), 1) |
34 | + user.sections.remove(section) | |
35 | + self.assertEqual(user.sections.count(), 0) |
flashcards/views.py
View file @
8fa22f3
... | ... | @@ -2,6 +2,7 @@ |
2 | 2 | from flashcards.models import Section, User |
3 | 3 | from flashcards.serializers import SectionSerializer, UserUpdateSerializer, RegistrationSerializer, UserSerializer, \ |
4 | 4 | PasswordResetSerializer, PasswordResetRequestSerializer, EmailPasswordSerializer |
5 | +from rest_framework.permissions import IsAuthenticated | |
5 | 6 | from rest_framework.viewsets import ReadOnlyModelViewSet |
6 | 7 | from django.core.mail import send_mail |
7 | 8 | from django.contrib.auth import authenticate, login, logout |
... | ... | @@ -10,6 +11,7 @@ |
10 | 11 | from rest_framework.views import APIView |
11 | 12 | from rest_framework.response import Response |
12 | 13 | from rest_framework.exceptions import AuthenticationFailed, NotAuthenticated, ValidationError |
14 | +from simple_email_confirmation import EmailAddress | |
13 | 15 | |
14 | 16 | |
15 | 17 | class SectionViewSet(ReadOnlyModelViewSet): |
16 | 18 | |
17 | 19 | |
18 | 20 | |
... | ... | @@ -34,15 +36,21 @@ |
34 | 36 | """ |
35 | 37 | data = UserUpdateSerializer(data=request.data, context={'user': request.user}) |
36 | 38 | data.is_valid(raise_exception=True) |
39 | + data = data.validated_data | |
37 | 40 | |
38 | 41 | if 'new_password' in data: |
42 | + if not request.user.is_authenticated(): | |
43 | + raise NotAuthenticated('You must be logged in to change your password') | |
39 | 44 | if not request.user.check_password(data['old_password']): |
40 | 45 | raise ValidationError('old_password is incorrect') |
41 | - request.user.set_password(request.data['new_password']) | |
46 | + request.user.set_password(data['new_password']) | |
42 | 47 | request.user.save() |
43 | 48 | |
44 | - if 'confirmation_key' in data and not request.user.confirm_email(data['confirmation_key']): | |
45 | - raise ValidationError('confirmation_key is invalid') | |
49 | + if 'confirmation_key' in data: | |
50 | + try: | |
51 | + request.user.confirm_email(data['confirmation_key']) | |
52 | + except EmailAddress.DoesNotExist: | |
53 | + raise ValidationError('confirmation_key is invalid') | |
46 | 54 | |
47 | 55 | return Response(UserSerializer(request.user).data) |
48 | 56 | |
... | ... | @@ -52,7 +60,7 @@ |
52 | 60 | --- |
53 | 61 | response_serializer: UserSerializer |
54 | 62 | """ |
55 | - if not request.user.is_active: return Response(status=HTTP_401_UNAUTHORIZED) | |
63 | + if not request.user.is_authenticated(): return Response(status=HTTP_401_UNAUTHORIZED) | |
56 | 64 | serializer = UserSerializer(request.user) |
57 | 65 | return Response(serializer.data) |
58 | 66 | |
... | ... | @@ -72,7 +80,7 @@ |
72 | 80 | |
73 | 81 | body = ''' |
74 | 82 | Visit the following link to confirm your email address: |
75 | - http://flashy.cards/app/verify_email/%s | |
83 | + https://flashy.cards/app/verify_email/%s | |
76 | 84 | |
77 | 85 | If you did not register for Flashy, no action is required. |
78 | 86 | ''' |
... | ... | @@ -116,6 +124,7 @@ |
116 | 124 | |
117 | 125 | |
118 | 126 | class UserLogout(APIView): |
127 | + permission_classes = (IsAuthenticated,) | |
119 | 128 | def post(self, request, format=None): |
120 | 129 | """ |
121 | 130 | Logs the authenticated user out. |
... | ... | @@ -142,7 +151,7 @@ |
142 | 151 | |
143 | 152 | body = ''' |
144 | 153 | Visit the following link to reset your password: |
145 | - http://flashy.cards/app/reset_password/%d/%s | |
154 | + https://flashy.cards/app/reset_password/%d/%s | |
146 | 155 | |
147 | 156 | If you did not request a password reset, no action is required. |
148 | 157 | ''' |
flashy.ini
View file @
8fa22f3
1 | 1 | # mysite_uwsgi.ini file |
2 | 2 | [uwsgi] |
3 | 3 | |
4 | +# set the uid and gid for user flashy | |
4 | 5 | uid = 1007 |
5 | 6 | gid = 1007 |
6 | 7 | # Django-related settings |
7 | 8 | |
... | ... | @@ -12,12 +13,12 @@ |
12 | 13 | home = /srv/flashy-backend/venv/ |
13 | 14 | logger = file:/var/log/uwsgi |
14 | 15 | # process-related settings |
15 | -# master | |
16 | -# master = true | |
17 | 16 | # maximum number of worker processes |
18 | 17 | # processes = 1 |
19 | 18 | http = :7001 |
20 | -# ... with appropriate permissions - may be needed | |
21 | -# chmod-socket = 664 | |
22 | -# touch-reload = '/tmp/reload_uwsgi' | |
19 | + | |
20 | +# load the secrets file | |
21 | +for-readline = file:/srv/flashy-backend/secrets | |
22 | + env = %(_) | |
23 | +endfor = |
flashy/settings.py
View file @
8fa22f3
nginxconf/flashy.cards
View file @
8fa22f3
1 | 1 | upstream backend_production { |
2 | 2 | # server unix:/tmp/flashy.sock; |
3 | - server localhost:7001; | |
3 | + server localhost:7002; | |
4 | 4 | } |
5 | 5 | |
6 | 6 | server { |
7 | 7 | |
8 | 8 | |
... | ... | @@ -17,14 +17,16 @@ |
17 | 17 | expires 30d; |
18 | 18 | } |
19 | 19 | |
20 | - location ^~ /app { | |
21 | - alias /srv/flashy-frontend; | |
20 | + location ^~ /app/ { | |
21 | + alias /srv/flashy-frontend/; | |
22 | + try_files $uri /app/home.html; | |
22 | 23 | } |
23 | 24 | |
24 | 25 | location ~ /(api|admin|api-auth)/ { |
25 | - add_header 'Access-Control-Allow-Origin' 'http://localhost/'; | |
26 | + add_header 'Access-Control-Allow-Origin' 'http://localhost'; | |
26 | 27 | add_header 'Access-Control-Allow-Credentials' 'true'; |
27 | 28 | add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, PATCH, PUT'; |
29 | + 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'; | |
28 | 30 | proxy_pass http://backend_production; |
29 | 31 | proxy_redirect http://backend_production $scheme://flashy.cards; |
30 | 32 | proxy_set_header Host $host; |
requirements.txt
View file @
8fa22f3
1 | -beautifulsoup4 | |
1 | +#beautifulsoup4 | |
2 | 2 | Django>=1.8 |
3 | 3 | #django-websocket-redis==0.4.3 |
4 | 4 | #gevent==1.0.1 |
5 | 5 | |
... | ... | @@ -7,10 +7,8 @@ |
7 | 7 | six==1.9.0 |
8 | 8 | djangorestframework |
9 | 9 | docutils |
10 | -gunicorn | |
11 | 10 | django-simple-email-confirmation |
12 | 11 | django-ses |
13 | 12 | coverage |
14 | 13 | django-rest-swagger |
15 | -psycopg2 |
run_tests.sh
View file @
8fa22f3
scripts/run_production.sh
View file @
8fa22f3
scripts/run_tests.sh
View file @
8fa22f3
scripts/setup.sh
View file @
8fa22f3
scripts/setup_production.sh
View file @
8fa22f3