Skip to content

Commit b7375ee

Browse files
feat: Create a user app for registration and authentication.
- Implement proper separation of 400/401/409 status codes for registration errors - Implement proper separation of 400/401 status codes for authentication errors - Add some tests for registration and authentication
1 parent e968dd9 commit b7375ee

File tree

16 files changed

+635
-5
lines changed

16 files changed

+635
-5
lines changed

.flake8

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[flake8]
22
max-line-length = 79
3-
application_import_names = promo_code
3+
application_import_names = promo_code, user
44
import-order-style = google
55
exclude = */migrations/, venv/, verdict.py, .venv/, env/, venv, .git, __pycache__
66
max-complexity = 10

.github/workflows/test.yaml

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
name: Run tests
2+
3+
on: deployment
4+
5+
permissions:
6+
contents: read
7+
packages: write
8+
attestations: write
9+
id-token: write
10+
11+
12+
jobs:
13+
build:
14+
name: Build image
15+
runs-on: ubuntu-latest
16+
timeout-minutes: 10
17+
if: github.action != 'github-classroom[bot]'
18+
steps:
19+
- uses: actions/checkout@v4
20+
21+
- name: Log in to GitHub Container Registry
22+
uses: docker/login-action@v2
23+
with:
24+
registry: ghcr.io
25+
username: ${{ github.actor }}
26+
password: ${{ secrets.GITHUB_TOKEN }}
27+
28+
- name: Set up Docker Buildx
29+
uses: docker/setup-buildx-action@v2
30+
31+
- name: Save image name (lowercased)
32+
run: echo "IMAGE_NAME=$(echo 'ghcr.io/${{ github.repository }}:run-${{ github.run_id }}' | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV
33+
34+
- name: Build and push image
35+
uses: docker/build-push-action@v4
36+
with:
37+
context: ./solution
38+
file: ./solution/Dockerfile
39+
tags: ${{ env.IMAGE_NAME }}
40+
push: true
41+
42+
tests:
43+
needs: build
44+
name: Run tests
45+
runs-on: ubuntu-latest
46+
timeout-minutes: 20
47+
if: github.action != 'github-classroom[bot]'
48+
steps:
49+
- name: Setup checker environment
50+
uses: Central-University-IT/setup-test-2025-backend@v1
51+
52+
- name: Log in to GitHub Container Registry
53+
uses: docker/login-action@v2
54+
with:
55+
registry: ghcr.io
56+
username: ${{ github.actor }}
57+
password: ${{ secrets.GITHUB_TOKEN }}
58+
59+
- name: Run tests
60+
run: |
61+
export IMAGE_SOLUTION=$(echo 'ghcr.io/${{ github.repository }}:run-${{ github.run_id }}' | tr '[:upper:]' '[:lower:]')
62+
export IMAGE_ANTIFRAUD=docker.io/lodthe/prod-backend-antifraud
63+
/usr/local/bin/checker
64+
continue-on-error: true
65+
66+
- uses: actions/[email protected]
67+
with:
68+
name: result
69+
path: ./result.json
70+
if-no-files-found: error
71+
compression-level: 0
72+
73+
- uses: bots-house/[email protected]
74+
continue-on-error: true
75+
with:
76+
owner: ${{ github.repository_owner }}
77+
name: ${{ github.event.repository.name }}
78+
token: ${{ secrets.GITHUB_TOKEN }}
79+
tag: run-${{ github.run_id }}

.isort.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[settings]
22
profile = black
33
skip = migrations, venv/, venv
4-
known_first_party = promo_code
4+
known_first_party = promo_code, user
55
default_section = THIRDPARTY
66
force_sort_within_sections = true
77
line_length = 79

promo_code/promo_code/settings.py

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import datetime
12
import os
23
import pathlib
34

@@ -26,19 +27,70 @@ def load_bool(name, default):
2627

2728
DEBUG = load_bool('DJANGO_DEBUG', False)
2829

29-
ALLOWED_HOSTS = []
30-
30+
ALLOWED_HOSTS = ['*']
3131

3232
INSTALLED_APPS = [
33-
'core.apps.CoreConfig',
3433
'django.contrib.admin',
3534
'django.contrib.auth',
3635
'django.contrib.contenttypes',
3736
'django.contrib.sessions',
3837
'django.contrib.messages',
3938
'django.contrib.staticfiles',
39+
#
40+
'rest_framework',
41+
'rest_framework_simplejwt',
42+
'rest_framework_simplejwt.token_blacklist',
43+
#
44+
'core.apps.CoreConfig',
45+
'user.apps.UserConfig',
4046
]
4147

48+
AUTH_USER_MODEL = 'user.User'
49+
50+
REST_FRAMEWORK = {
51+
'DEFAULT_RENDERER_CLASSES': ('rest_framework.renderers.JSONRenderer',),
52+
'DEFAULT_AUTHENTICATION_CLASSES': [
53+
'user.authentication.CustomJWTAuthentication',
54+
],
55+
}
56+
57+
SIMPLE_JWT = {
58+
'ACCESS_TOKEN_LIFETIME': datetime.timedelta(hours=1),
59+
'REFRESH_TOKEN_LIFETIME': datetime.timedelta(days=1),
60+
'ROTATE_REFRESH_TOKENS': True,
61+
'BLACKLIST_AFTER_ROTATION': True,
62+
'UPDATE_LAST_LOGIN': False,
63+
#
64+
'ALGORITHM': 'HS256',
65+
'SIGNING_KEY': SECRET_KEY,
66+
'VERIFYING_KEY': None,
67+
'AUDIENCE': None,
68+
'ISSUER': None,
69+
'JSON_ENCODER': None,
70+
'JWK_URL': None,
71+
'LEEWAY': 0,
72+
#
73+
'AUTH_HEADER_TYPES': ('Bearer',),
74+
'AUTH_HEADER_NAME': 'HTTP_AUTHORIZATION',
75+
'USER_ID_FIELD': 'id',
76+
'USER_ID_CLAIM': 'user_id',
77+
'USER_AUTHENTICATION_RULE': (
78+
'rest_framework_simplejwt.authentication'
79+
'.default_user_authentication_rule',
80+
),
81+
#
82+
'TOKEN_TYPE_CLAIM': 'token_type',
83+
'TOKEN_USER_CLASS': 'rest_framework_simplejwt.models.TokenUser',
84+
#
85+
'JTI_CLAIM': 'jti',
86+
#
87+
'SLIDING_TOKEN_REFRESH_EXP_CLAIM': 'refresh_exp',
88+
'SLIDING_TOKEN_LIFETIME': datetime.timedelta(minutes=5),
89+
'SLIDING_TOKEN_REFRESH_LIFETIME': datetime.timedelta(days=1),
90+
#
91+
'ACCESS_TOKEN_CLASS': 'user.tokens.CustomAccessToken',
92+
}
93+
4294
MIDDLEWARE = [
4395
'django.middleware.security.SecurityMiddleware',
4496
'django.contrib.sessions.middleware.SessionMiddleware',

promo_code/promo_code/urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@
33

44
urlpatterns = [
55
django.urls.path('api/ping/', django.urls.include('core.urls')),
6+
django.urls.path('api/user/', django.urls.include('user.urls')),
67
django.urls.path('admin/', django.contrib.admin.site.urls),
78
]

promo_code/user/__init__.py

Whitespace-only changes.

promo_code/user/apps.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import django.apps
2+
3+
4+
class UserConfig(django.apps.AppConfig):
5+
default_auto_field = 'django.db.models.BigAutoField'
6+
name = 'user'

promo_code/user/authentication.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import datetime
2+
3+
import rest_framework.exceptions
4+
import rest_framework_simplejwt.authentication
5+
6+
7+
class CustomJWTAuthentication(
8+
rest_framework_simplejwt.authentication.JWTAuthentication,
9+
):
10+
def get_user(self, validated_token):
11+
user = super().get_user(validated_token)
12+
last_login_str = validated_token.get('last_login')
13+
if last_login_str:
14+
last_login = datetime.datetime.fromisoformat(last_login_str)
15+
if user.last_login and user.last_login > last_login:
16+
raise rest_framework.exceptions.AuthenticationFailed(
17+
'Token has been invalidated',
18+
)
19+
20+
return user
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# Generated by Django 5.2b1 on 2025-02-28 17:01
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
initial = True
9+
10+
dependencies = [
11+
('auth', '0012_alter_user_first_name_max_length'),
12+
]
13+
14+
operations = [
15+
migrations.CreateModel(
16+
name='User',
17+
fields=[
18+
(
19+
'id',
20+
models.BigAutoField(
21+
auto_created=True,
22+
primary_key=True,
23+
serialize=False,
24+
verbose_name='ID',
25+
),
26+
),
27+
(
28+
'password',
29+
models.CharField(max_length=128, verbose_name='password'),
30+
),
31+
(
32+
'is_superuser',
33+
models.BooleanField(
34+
default=False,
35+
help_text='Designates that this user has all permissions without explicitly assigning them.',
36+
verbose_name='superuser status',
37+
),
38+
),
39+
('email', models.EmailField(max_length=120, unique=True)),
40+
('name', models.CharField(max_length=100)),
41+
('surname', models.CharField(max_length=120)),
42+
(
43+
'avatar_url',
44+
models.URLField(blank=True, max_length=350, null=True),
45+
),
46+
('other', models.JSONField(default=dict)),
47+
('is_active', models.BooleanField(default=True)),
48+
('is_staff', models.BooleanField(default=False)),
49+
('last_login', models.DateTimeField(blank=True, null=True)),
50+
(
51+
'groups',
52+
models.ManyToManyField(
53+
blank=True,
54+
help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.',
55+
related_name='user_set',
56+
related_query_name='user',
57+
to='auth.group',
58+
verbose_name='groups',
59+
),
60+
),
61+
(
62+
'user_permissions',
63+
models.ManyToManyField(
64+
blank=True,
65+
help_text='Specific permissions for this user.',
66+
related_name='user_set',
67+
related_query_name='user',
68+
to='auth.permission',
69+
verbose_name='user permissions',
70+
),
71+
),
72+
],
73+
options={
74+
'abstract': False,
75+
},
76+
),
77+
]

promo_code/user/migrations/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)