Skip to content

Commit a23656b

Browse files
authored
Merge pull request #11 from Kalgoc/develop
Develop
2 parents 90abcd3 + 562f6dc commit a23656b

32 files changed

+939
-80
lines changed

.flake8

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
[flake8]
2+
exclude = .git,__pycache__,old,build,dist,.venv, */migrations/*
3+
ignore = E226,E302,E41,F401,F403,F405
4+
max-line-length = 120
5+
max-complexity = 10

.github/pull_request_template.md

+13
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,19 @@
22

33
[Escribir acá que es lo que incluye esta PR]
44

5+
## Checklist
6+
7+
[Revisar y confirmar que se hayan realizado los cambios de la lista]
8+
9+
- [ ] Mi código se adhiere al formato de estilo del proyecto
10+
- [ ] Revisé mi código
11+
- [ ] Eliminé código legacy
12+
- [ ] Mis cambios no generan nuevos warnings
13+
- [ ] Hice cambios en la documentación
14+
- [ ] Probé correr los tests ya existentes
15+
- [ ] Cree nuevos tests que mantengan al menos un 60% de cobertura del proyecto
16+
17+
518
## Comentarios Adicionales
619

720
[Escribir acá cualquier comentario adicional, por ejemplo, puntos a considerar o solicitudes de feedback]

.github/workflows/pytest.yml

+1
Original file line numberDiff line numberDiff line change
@@ -33,5 +33,6 @@ jobs:
3333
- uses: pavelzw/pytest-action@v2
3434
env:
3535
SECRET_KEY: ${{ secrets.DJANGO_SECRET_KEY }}
36+
AWS_REGION: us-east-2
3637
with:
3738
emoji: false

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -161,3 +161,5 @@ cython_debug/
161161

162162
# Misc
163163
.DS_Store
164+
165+
bci-script.py

Pipfile

+6
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ name = "pypi"
77
django = "*"
88
psycopg2-binary = "*"
99
python-dotenv = "*"
10+
djangorestframework = "*"
11+
boto3 = "*"
12+
pyjwt = "*"
13+
requests = "*"
14+
cryptography = "*"
1015

1116
[dev-packages]
1217
flake8 = "*"
@@ -18,6 +23,7 @@ pre-commit = "*"
1823
coverage = "*"
1924
pytest-cov = "*"
2025
flake8-pyproject = "*"
26+
factory-boy = "*"
2127

2228
[requires]
2329
python_version = "3.11"

Pipfile.lock

+391-65
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

appspec.yml

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
version: 0.0
2+
os: linux
3+
4+
# In this case we copy all the files of the project to the EC2
5+
files:
6+
- source: /
7+
destination: /home/ubuntu/api
8+
9+
# If a file already exists in the destination, it will be OVERWRITTEN
10+
file_exists_behavior: OVERWRITE
11+
12+
#
13+
hooks:
14+
# First the CodeDeploy agent stops the application
15+
ApplicationStop:
16+
- location: scripts/application_stop.sh
17+
timeout: 100
18+
runas: ubuntu
19+
overwrite: true
20+
# The isntall the service (Copy the revision files to the EC2)
21+
# So we can run after install scripts for example for configuration
22+
AfterInstall:
23+
- location: scripts/after_install.sh
24+
timeout: 100
25+
runas: ubuntu
26+
overwrite: true
27+
# Then Start the service
28+
ApplicationStart:
29+
- location: scripts/application_start.sh
30+
timeout: 100
31+
runas: ubuntu
32+
overwrite: true
33+
# Finally we validate the deployment with a script
34+
ValidateService:
35+
- location: scripts/validate_service.sh
36+
timeout: 100
37+
runas: ubuntu
38+
overwrite: true
File renamed without changes.

authentication/admin.py

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
from django.contrib import admin
2+
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
3+
from django.utils.translation import gettext_lazy as _
4+
from .models import User
5+
6+
7+
class UserAdmin(BaseUserAdmin):
8+
fieldsets = (
9+
(None, {"fields": ("username", "password")}),
10+
(_("Personal info"), {"fields": ("first_name", "last_name", "email", "phone")}),
11+
(_("Important dates"), {"fields": ("last_login", "date_joined", "created_at", "updated_at")}),
12+
(_("Permissions"), {"fields": ("is_active", "is_staff", "is_superuser", "groups", "user_permissions")}),
13+
(_("Status"), {"fields": ("enabled",)}),
14+
)
15+
add_fieldsets = (
16+
(
17+
None,
18+
{
19+
"classes": ("wide",),
20+
"fields": ("username", "password1", "password2"),
21+
},
22+
),
23+
)
24+
list_display = (
25+
"username",
26+
"email",
27+
"first_name",
28+
"last_name",
29+
"is_staff",
30+
"phone",
31+
"user_id",
32+
"enabled",
33+
"created_at",
34+
"updated_at",
35+
)
36+
search_fields = ("username", "first_name", "last_name", "email", "phone", "user_id")
37+
ordering = ("username",)
38+
39+
40+
admin.site.register(User, UserAdmin)

authentication/apps.py

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from django.apps import AppConfig
2+
3+
4+
class AuthenticationConfig(AppConfig):
5+
default_auto_field = "django.db.models.BigAutoField"
6+
name = "authentication"

authentication/decorators.py

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from functools import wraps
2+
from rest_framework.response import Response
3+
from rest_framework import status
4+
from rest_framework.decorators import api_view, authentication_classes
5+
from .services.cognito_authentication import CognitoAuthentication
6+
7+
8+
def cognito_authenticated(func):
9+
@wraps(func)
10+
@api_view(["GET", "POST", "PUT", "DELETE"])
11+
@authentication_classes([CognitoAuthentication])
12+
def wrapper(request, *args, **kwargs):
13+
if not request.user or not hasattr(request.user, "get"):
14+
return Response(
15+
{"error": "Authentication credentials were not provided or are invalid."},
16+
status=status.HTTP_401_UNAUTHORIZED,
17+
)
18+
return func(request, *args, **kwargs)
19+
20+
return wrapper
+97
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
# Generated by Django 5.0.6 on 2024-06-06 00:20
2+
3+
import django.contrib.auth.models
4+
import django.contrib.auth.validators
5+
import django.utils.timezone
6+
import uuid
7+
from django.db import migrations, models
8+
9+
10+
class Migration(migrations.Migration):
11+
initial = True
12+
13+
dependencies = [
14+
("auth", "0012_alter_user_first_name_max_length"),
15+
]
16+
17+
operations = [
18+
migrations.CreateModel(
19+
name="User",
20+
fields=[
21+
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
22+
("password", models.CharField(max_length=128, verbose_name="password")),
23+
("last_login", models.DateTimeField(blank=True, null=True, verbose_name="last login")),
24+
(
25+
"is_superuser",
26+
models.BooleanField(
27+
default=False,
28+
help_text="Designates that this user has all permissions without explicitly assigning them.",
29+
verbose_name="superuser status",
30+
),
31+
),
32+
(
33+
"username",
34+
models.CharField(
35+
error_messages={"unique": "A user with that username already exists."},
36+
help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.",
37+
max_length=150,
38+
unique=True,
39+
validators=[django.contrib.auth.validators.UnicodeUsernameValidator()],
40+
verbose_name="username",
41+
),
42+
),
43+
("first_name", models.CharField(blank=True, max_length=150, verbose_name="first name")),
44+
("last_name", models.CharField(blank=True, max_length=150, verbose_name="last name")),
45+
("email", models.EmailField(blank=True, max_length=254, verbose_name="email address")),
46+
(
47+
"is_staff",
48+
models.BooleanField(
49+
default=False,
50+
help_text="Designates whether the user can log into this admin site.",
51+
verbose_name="staff status",
52+
),
53+
),
54+
(
55+
"is_active",
56+
models.BooleanField(
57+
default=True,
58+
help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.",
59+
verbose_name="active",
60+
),
61+
),
62+
("date_joined", models.DateTimeField(default=django.utils.timezone.now, verbose_name="date joined")),
63+
("uuid", models.UUIDField(default=uuid.uuid4, editable=False, unique=True)),
64+
("phone", models.CharField(blank=True, max_length=15)),
65+
(
66+
"groups",
67+
models.ManyToManyField(
68+
blank=True,
69+
help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.",
70+
related_name="user_set",
71+
related_query_name="user",
72+
to="auth.group",
73+
verbose_name="groups",
74+
),
75+
),
76+
(
77+
"user_permissions",
78+
models.ManyToManyField(
79+
blank=True,
80+
help_text="Specific permissions for this user.",
81+
related_name="user_set",
82+
related_query_name="user",
83+
to="auth.permission",
84+
verbose_name="user permissions",
85+
),
86+
),
87+
],
88+
options={
89+
"verbose_name": "user",
90+
"verbose_name_plural": "users",
91+
"abstract": False,
92+
},
93+
managers=[
94+
("objects", django.contrib.auth.models.UserManager()),
95+
],
96+
),
97+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Generated by Django 5.0.6 on 2024-06-06 04:03
2+
3+
import datetime
4+
from django.db import migrations, models
5+
6+
7+
class Migration(migrations.Migration):
8+
dependencies = [
9+
("authentication", "0001_initial"),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name="user",
15+
name="created_at",
16+
field=models.DateTimeField(auto_now_add=True, default=datetime.datetime(2024, 6, 6, 4, 3, 44, 102325)),
17+
preserve_default=False,
18+
),
19+
migrations.AddField(
20+
model_name="user",
21+
name="enabled",
22+
field=models.BooleanField(default=True),
23+
),
24+
migrations.AddField(
25+
model_name="user",
26+
name="updated_at",
27+
field=models.DateTimeField(auto_now=True),
28+
),
29+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Generated by Django 5.0.6 on 2024-06-06 04:07
2+
3+
from django.db import migrations
4+
5+
6+
class Migration(migrations.Migration):
7+
dependencies = [
8+
("authentication", "0002_user_created_at_user_enabled_user_updated_at"),
9+
]
10+
11+
operations = [
12+
migrations.RenameField(
13+
model_name="user",
14+
old_name="uuid",
15+
new_name="user_id",
16+
),
17+
]

authentication/migrations/__init__.py

Whitespace-only changes.

authentication/models.py

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from django.contrib.auth.models import AbstractUser
2+
from django.db import models
3+
import uuid
4+
5+
6+
class User(AbstractUser):
7+
user_id = models.UUIDField(default=uuid.uuid4, unique=True, editable=False)
8+
phone = models.CharField(max_length=15, blank=True)
9+
created_at = models.DateTimeField(auto_now_add=True)
10+
updated_at = models.DateTimeField(auto_now=True)
11+
enabled = models.BooleanField(default=True)

authentication/serializers.py

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from rest_framework import serializers
2+
3+
4+
class RegisterSerializer(serializers.Serializer):
5+
name = serializers.CharField(max_length=150)
6+
phone = serializers.CharField(max_length=15)
7+
email = serializers.EmailField()
8+
password = serializers.CharField(write_only=True)
9+
10+
11+
class LoginSerializer(serializers.Serializer):
12+
email = serializers.EmailField()
13+
password = serializers.CharField(write_only=True)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import jwt
2+
from jwt import PyJWKClient, PyJWKSetError
3+
from django.conf import settings
4+
from rest_framework import authentication, exceptions
5+
6+
7+
class CognitoAuthentication(authentication.BaseAuthentication):
8+
def __init__(self):
9+
self.cognito_jwks_url = (
10+
f"https://cognito-idp.{settings.AWS_REGION}.amazonaws.com/"
11+
f"{settings.COGNITO_USER_POOL_ID}/.well-known/jwks.json"
12+
)
13+
self.jwks_client = PyJWKClient(self.cognito_jwks_url)
14+
15+
def authenticate(self, request):
16+
auth_header = request.headers.get("Authorization")
17+
if not auth_header:
18+
return None
19+
20+
try:
21+
token = auth_header.split()[1]
22+
signing_key = self.jwks_client.get_signing_key_from_jwt(token)
23+
payload = jwt.decode(token, signing_key.key, algorithms=["RS256"], aud=settings.COGNITO_APP_CLIENT_ID)
24+
return (payload, None)
25+
except PyJWKSetError as e:
26+
raise exceptions.AuthenticationFailed(f"JWK Set did not contain any usable keys: {e}")
27+
except jwt.ExpiredSignatureError as e:
28+
raise exceptions.AuthenticationFailed(f"Token has expired: {e}")
29+
except jwt.DecodeError as e:
30+
raise exceptions.AuthenticationFailed(f"Error decoding token: {e}")
31+
except jwt.InvalidTokenError as e:
32+
raise exceptions.AuthenticationFailed(f"Invalid token: {e}")
33+
except Exception as e:
34+
raise exceptions.AuthenticationFailed(f"Unhandled exception: {e}")

0 commit comments

Comments
 (0)