Skip to content

Commit 72d7dc1

Browse files
authored
Merge pull request #9 from Kalgoc/budget
Budget
2 parents 562f6dc + 7b37d07 commit 72d7dc1

14 files changed

+259
-9
lines changed

authentication/decorators.py

+20-7
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,27 @@
77

88
def cognito_authenticated(func):
99
@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"):
10+
def wrapper(self, request, *args, **kwargs):
11+
auth = CognitoAuthentication()
12+
try:
13+
result = auth.authenticate(request)
14+
if result is None:
15+
return Response(
16+
{"error": "Authentication credentials were not provided or are invalid."},
17+
status=status.HTTP_401_UNAUTHORIZED,
18+
)
19+
user, auth_error = result
20+
if auth_error or not user:
21+
return Response(
22+
{"error": "Authentication credentials were not provided or are invalid."},
23+
status=status.HTTP_401_UNAUTHORIZED,
24+
)
25+
request.user = user
26+
return func(self, request, *args, **kwargs)
27+
except Exception as e:
1428
return Response(
15-
{"error": "Authentication credentials were not provided or are invalid."},
16-
status=status.HTTP_401_UNAUTHORIZED,
29+
{"error": f"Authentication failed: {str(e)}"},
30+
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
1731
)
18-
return func(request, *args, **kwargs)
1932

2033
return wrapper

budget/__init__.py

Whitespace-only changes.

budget/admin.py

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# budget/admin.py
2+
from django.contrib import admin
3+
from .models import Budget
4+
5+
6+
@admin.register(Budget)
7+
class BudgetAdmin(admin.ModelAdmin):
8+
list_display = ("username", "amount", "created_at", "updated_at")
9+
search_fields = ("username", "amount")

budget/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 BudgetConfig(AppConfig):
5+
default_auto_field = "django.db.models.BigAutoField"
6+
name = "budget"

budget/migrations/0001_initial.py

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Generated by Django 5.0.6 on 2024-06-10 16:41
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
initial = True
9+
10+
dependencies = []
11+
12+
operations = [
13+
migrations.CreateModel(
14+
name="Budget",
15+
fields=[
16+
("id", models.AutoField(primary_key=True, serialize=False)),
17+
("username", models.UUIDField()),
18+
("amount", models.IntegerField()),
19+
("created_at", models.DateTimeField(auto_now_add=True)),
20+
("updated_at", models.DateTimeField(auto_now=True)),
21+
],
22+
),
23+
]

budget/migrations/__init__.py

Whitespace-only changes.

budget/models.py

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# budget/models.py
2+
from django.db import models
3+
from django.conf import settings
4+
import uuid
5+
6+
7+
class Budget(models.Model):
8+
id = models.AutoField(primary_key=True)
9+
username = models.UUIDField()
10+
amount = models.IntegerField()
11+
created_at = models.DateTimeField(auto_now_add=True)
12+
updated_at = models.DateTimeField(auto_now=True)
13+
14+
def __str__(self):
15+
return f"{self.user}'s budget: {self.amount}"

budget/serializers.py

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
from rest_framework import serializers
2+
from .models import Budget
3+
4+
5+
class BudgetSerializer(serializers.Serializer):
6+
username = serializers.UUIDField()
7+
amount = serializers.IntegerField()
8+
9+
def create(self, validated_data):
10+
return Budget.objects.create(**validated_data)
11+
12+
def update(self, instance, validated_data):
13+
instance.amount = validated_data.get("amount", instance.amount)
14+
instance.save()
15+
return instance

budget/tests.py

+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
from django.test import TestCase
2+
from unittest.mock import patch
3+
from rest_framework.test import APIClient
4+
from rest_framework import status
5+
from authentication.services.cognito_service import CognitoService
6+
from budget.views import BudgetViewSet
7+
from rest_framework.response import Response
8+
9+
10+
class BudgetViewSetTests(TestCase):
11+
def setUp(self):
12+
self.client = APIClient()
13+
self.view = BudgetViewSet()
14+
15+
@patch.object(BudgetViewSet, "create")
16+
@patch.object(CognitoService, "login_user")
17+
def test_create(self, mock_login_user, mock_create):
18+
mock_login_user.return_value = {
19+
"AuthenticationResult": {"AccessToken": "mock_access_token", "IdToken": "mock_id_token"}
20+
}
21+
mock_create.return_value = Response(status=status.HTTP_201_CREATED, data={"amount": 100})
22+
23+
request = self.client.post("/budget/", {"amount": 100})
24+
request.headers["Authorization"] = "Bearer mock_access_token"
25+
26+
response = self.client.post("/budget/", {"amount": 100})
27+
28+
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
29+
self.assertEqual(response.data["amount"], 100)
30+
31+
@patch.object(BudgetViewSet, "list")
32+
@patch.object(CognitoService, "login_user")
33+
def test_list(self, mock_login_user, mock_list):
34+
mock_login_user.return_value = {
35+
"AuthenticationResult": {"AccessToken": "mock_access_token", "IdToken": "mock_id_token"}
36+
}
37+
mock_list.return_value = Response(status=status.HTTP_200_OK, data={"amount": 100})
38+
39+
request = self.client.get("/budget/")
40+
request.headers["Authorization"] = "Bearer mock_access_token"
41+
42+
response = self.client.get("/budget/")
43+
44+
self.assertEqual(response.status_code, status.HTTP_200_OK)
45+
self.assertEqual(response.data["amount"], 100)
46+
47+
@patch.object(BudgetViewSet, "destroy")
48+
@patch.object(CognitoService, "login_user")
49+
def test_destroy(self, mock_login_user, mock_destroy):
50+
mock_login_user.return_value = {
51+
"AuthenticationResult": {"AccessToken": "mock_access_token", "IdToken": "mock_id_token"}
52+
}
53+
mock_destroy.return_value = Response(status=status.HTTP_204_NO_CONTENT)
54+
55+
request = self.client.delete("/budget/")
56+
request.headers["Authorization"] = "Bearer mock_access_token"
57+
58+
response = self.client.delete("/budget/")
59+
60+
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
61+
62+
@patch.object(BudgetViewSet, "partial_update")
63+
@patch.object(CognitoService, "login_user")
64+
def test_partial_update(self, mock_login_user, mock_partial_update):
65+
mock_login_user.return_value = {
66+
"AuthenticationResult": {"AccessToken": "mock_access_token", "IdToken": "mock_id_token"}
67+
}
68+
mock_partial_update.return_value = Response(status=status.HTTP_200_OK, data={"amount": 200})
69+
70+
request = self.client.put("/budget/", {"amount": 200})
71+
request.headers["Authorization"] = "Bearer mock_access_token"
72+
73+
response = self.client.put("/budget/", {"amount": 200})
74+
75+
self.assertEqual(response.status_code, status.HTTP_200_OK)
76+
self.assertEqual(response.data["amount"], 200)

budget/urls.py

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# budget/urls.py
2+
from django.urls import path
3+
from .views import BudgetViewSet
4+
5+
urlpatterns = [
6+
path(
7+
"",
8+
BudgetViewSet.as_view({"get": "list", "post": "create", "delete": "destroy", "put": "partial_update"}),
9+
name="budget",
10+
),
11+
]

budget/views.py

+82
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
from rest_framework import viewsets, status
2+
from rest_framework.response import Response
3+
from .models import Budget
4+
from .serializers import BudgetSerializer
5+
from authentication.decorators import cognito_authenticated
6+
import jwt
7+
8+
9+
class BudgetViewSet(viewsets.ViewSet):
10+
def get_user_id_from_token(self, request):
11+
try:
12+
authorization_header = request.headers.get("Authorization")
13+
if not authorization_header:
14+
raise Exception("Authorization header not found")
15+
16+
token = authorization_header.split()[1]
17+
decoded_token = jwt.decode(token, options={"verify_signature": False})
18+
user_id = decoded_token.get("username")
19+
if not user_id:
20+
raise Exception("User ID not found in token")
21+
return user_id
22+
except jwt.DecodeError:
23+
raise Exception("Invalid token")
24+
except jwt.ExpiredSignatureError:
25+
raise Exception("Expired token")
26+
except Exception as e:
27+
raise Exception(f"Error decoding token: {e}")
28+
29+
@cognito_authenticated
30+
def list(self, request):
31+
try:
32+
username = self.get_user_id_from_token(request)
33+
budget = Budget.objects.filter(username=username).order_by("-created_at").first()
34+
serializer = BudgetSerializer(budget)
35+
return Response(data={"amount": serializer.data["amount"]})
36+
except Budget.DoesNotExist:
37+
return Response({"error": "Budget not found"}, status=status.HTTP_404_NOT_FOUND)
38+
except Exception as e:
39+
return Response({"error": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
40+
41+
@cognito_authenticated
42+
def create(self, request):
43+
try:
44+
user_id = self.get_user_id_from_token(request)
45+
data = request.data.copy()
46+
data["username"] = user_id
47+
48+
serializer = BudgetSerializer(data=data)
49+
print(serializer)
50+
if serializer.is_valid():
51+
serializer.save()
52+
return Response(data={"amount": serializer.data["amount"]}, status=status.HTTP_201_CREATED)
53+
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
54+
except Exception as e:
55+
return Response({"error": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
56+
57+
@cognito_authenticated
58+
def destroy(self, request):
59+
try:
60+
username = self.get_user_id_from_token(request)
61+
budget = Budget.objects.filter(username=username).order_by("-created_at").first()
62+
budget.delete()
63+
return Response(status=status.HTTP_204_NO_CONTENT)
64+
except Budget.DoesNotExist:
65+
return Response({"error": "Budget not found"}, status=status.HTTP_404_NOT_FOUND)
66+
except Exception as e:
67+
return Response({"error": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
68+
69+
@cognito_authenticated
70+
def partial_update(self, request):
71+
try:
72+
username = self.get_user_id_from_token(request)
73+
budget = Budget.objects.filter(username=username).order_by("-created_at").first()
74+
serializer = BudgetSerializer(budget, data=request.data, partial=True)
75+
if serializer.is_valid():
76+
serializer.save()
77+
return Response(data={"amount": serializer.data["amount"]})
78+
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
79+
except Budget.DoesNotExist:
80+
return Response({"error": "Budget not found"}, status=status.HTTP_404_NOT_FOUND)
81+
except Exception as e:
82+
return Response({"error": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

docker-compose.yml

-2
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,6 @@ services:
1212
- "5432:5432"
1313
networks:
1414
- app_network
15-
environment:
16-
POSTGRES_HOST_AUTH_METHOD: trust
1715

1816
web:
1917
build:

piggywallet/settings/base.py

+1
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
"rest_framework",
4646
"playground",
4747
"authentication",
48+
"budget",
4849
]
4950

5051
REST_FRAMEWORK = {

piggywallet/urls.py

+1
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,5 @@
2222
path("admin/", admin.site.urls),
2323
path("auth/", include("authentication.urls")),
2424
path("playground/", include("playground.urls")),
25+
path("budget/", include("budget.urls")),
2526
]

0 commit comments

Comments
 (0)