From 582dfde90c78a792d01c361ac237abe0030ba5ee Mon Sep 17 00:00:00 2001 From: Ethan Strominger Date: Wed, 16 Oct 2024 08:10:46 -0400 Subject: [PATCH 01/25] Enable CORS for React application --- app/.env.docker-example | 1 + app/peopledepot/settings.py | 35 +++++++++++++++++++++++++++++------ app/requirements.txt | 1 + scripts/createsuperuser.sh | 1 - 4 files changed, 31 insertions(+), 7 deletions(-) diff --git a/app/.env.docker-example b/app/.env.docker-example index 01528a9f..d3da5667 100644 --- a/app/.env.docker-example +++ b/app/.env.docker-example @@ -1,5 +1,6 @@ DEBUG=1 SECRET_KEY=foo +CORS_ALLOWED_ORIGINS="localhost" DJANGO_PORT=8000 DJANGO_ALLOWED_HOSTS="localhost 127.0.0.1 [::1]" DJANGO_SUPERUSER_USERNAME=admin1111 diff --git a/app/peopledepot/settings.py b/app/peopledepot/settings.py index db019b76..07160cb8 100644 --- a/app/peopledepot/settings.py +++ b/app/peopledepot/settings.py @@ -51,16 +51,22 @@ # On django init we download jwks public keys which are used to validate jwt tokens. # For now there is no rotation of keys (seems like in Cognito decided not to implement it) if COGNITO_AWS_REGION and COGNITO_USER_POOL: - COGNITO_POOL_URL = ( - f"https://cognito-idp.{COGNITO_AWS_REGION}.amazonaws.com/{COGNITO_USER_POOL}" - ) - pool_jwks_url = COGNITO_POOL_URL + "/.well-known/jwks.json" - jwks = json.loads(request.urlopen(pool_jwks_url).read()) # nosec B310 - rsa_keys = {key["kid"]: json.dumps(key) for key in jwks["keys"]} + try: + COGNITO_POOL_URL = ( + f"https://cognito-idp.{COGNITO_AWS_REGION}.amazonaws.com/{COGNITO_USER_POOL}" + ) + pool_jwks_url = COGNITO_POOL_URL + "/.well-known/jwks.json" + jwks = json.loads(request.urlopen(pool_jwks_url).read()) # nosec B310 + rsa_keys = {key["kid"]: json.dumps(key) for key in jwks["keys"]} + except Exception as e: + print(f"Error loading JWKS: {e}", COGNITO_POOL_URL) + raise e + # return {} # Application definition INSTALLED_APPS = [ + "corsheaders", "django.contrib.admin", "django.contrib.auth", "django.contrib.contenttypes", @@ -79,7 +85,24 @@ "data", ] +# Allow specific origins (like your React dev and production URLs) +CORS_ALLOWED_ORIGINS = [ + "http://localhost:3000", # React Dev Server + "https://your-frontend.com", # React Production App +] + +# Optional: Allow credentials (for cookies or tokens) +CORS_ALLOW_CREDENTIALS = True + +# Optional: Control which headers are allowed +CORS_ALLOW_HEADERS = [ + "Authorization", + "Content-Type", +] + + MIDDLEWARE = [ + "corsheaders.middleware.CorsMiddleware", "django.middleware.security.SecurityMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.common.CommonMiddleware", diff --git a/app/requirements.txt b/app/requirements.txt index a3d5c9fc..360a31f3 100644 --- a/app/requirements.txt +++ b/app/requirements.txt @@ -22,6 +22,7 @@ django==4.2.11 # djangorestframework # drf-jwt # drf-spectacular +django-cors-headers==4.5.0 django-extensions==3.2.3 django-linear-migrations==2.12.0 django-phonenumber-field==7.3.0 diff --git a/scripts/createsuperuser.sh b/scripts/createsuperuser.sh index 9f7ffdb4..01a88642 100755 --- a/scripts/createsuperuser.sh +++ b/scripts/createsuperuser.sh @@ -8,5 +8,4 @@ set -x # This command requires the DJANGO_SUPERUSER_USERNAME and # DJANGO_SUPERUSER_PASSWORD environmental variables to be set when django starts -echo "DJANGO_SUPERUSER_USERNAME: $DJANGO_SUPERUSER_USERNAME" docker-compose exec web python manage.py createsuperuser --no-input From dd3fc1c3a8f3252cef2fbe117f82dbbf06b6926c Mon Sep 17 00:00:00 2001 From: Ethan Strominger Date: Tue, 26 Nov 2024 06:57:38 -0500 Subject: [PATCH 02/25] Partially working, some code commented out --- app/core/api/views.py | 32 +++++++++++++++++++++++++++++--- app/core/utils/jwt.py | 38 +++++++++++++++++++++++++++++++++++++- 2 files changed, 66 insertions(+), 4 deletions(-) diff --git a/app/core/api/views.py b/app/core/api/views.py index 8b55ca3a..e3b249c8 100644 --- a/app/core/api/views.py +++ b/app/core/api/views.py @@ -10,7 +10,8 @@ from rest_framework.mixins import RetrieveModelMixin from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticatedOrReadOnly - +from rest_framework.exceptions import AuthenticationFailed +from core.utils.jwt import cognito_jwt_decode_handler from ..models import Affiliate from ..models import Affiliation from ..models import CheckType @@ -46,6 +47,16 @@ from .serializers import StackElementTypeSerializer from .serializers import UserPermissionSerializer from .serializers import UserSerializer +from rest_framework.permissions import BasePermission + +class IsAuthenticated2(BasePermission): + """ + Allows access only to authenticated users. + """ + + def has_permission(self, request, view): + print("debug", request.user, request.user.is_authenticated) + return bool(request.user and request.user.is_authenticated) class UserProfileAPIView(RetrieveModelMixin, GenericAPIView): @@ -132,10 +143,25 @@ def get_queryset(self): partial_update=extend_schema(description="Patch a project"), ) class ProjectViewSet(viewsets.ModelViewSet): - permission_classes = [IsAuthenticated] + permission_classes = [IsAuthenticatedOrReadOnly] queryset = Project.objects.all() serializer_class = ProjectSerializer - + # def get_queryset(self, request, *args, **kwargs): + # token = request.headers.get('Authorization') + # print("Received Token:", token) + # super.get() + def perform_authentication(self, request): + print("Here 1") + auth = request.headers.get('Authorization') + if not auth: + raise AuthenticationFailed('Authorization header missing') + try: + prefix, token = auth.split(' ') + print("debug auth", auth) + # Here, call your cognito_jwt_decode_handler with the token + cognito_jwt_decode_handler(token) + except ValueError: + raise AuthenticationFailed('Invalid token format') @extend_schema_view( list=extend_schema(description="Return a list of all the events"), diff --git a/app/core/utils/jwt.py b/app/core/utils/jwt.py index 1b2a9bcc..950b3eb6 100644 --- a/app/core/utils/jwt.py +++ b/app/core/utils/jwt.py @@ -18,19 +18,55 @@ def cognito_jwt_decode_handler(token): https://aws.amazon.com/premiumsupport/knowledge-center/decode-verify-cognito-json-token/ Almost the same as default 'rest_framework_jwt.utils.jwt_decode_handler', but 'secret_key' feature is skipped """ + print("jwt handler", token) options = {"verify_exp": api_settings.JWT_VERIFY_EXPIRATION} - unverified_header = jwt.get_unverified_header(token) + unverified_header = {} + try: + unverified_header = jwt.get_unverified_header(token) + except Exception as e: + print("Debug exception 1", e) if "kid" not in unverified_header: + print("kid") + return raise DecodeError("Incorrect authentication credentials.") kid = unverified_header["kid"] + print("Try and try") try: # pick a proper public key according to `kid` from token header public_key = RSAAlgorithm.from_jwk(api_settings.JWT_PUBLIC_KEY[kid]) except KeyError: + print("KeyError") # in this place we could refresh cached jwks and try again raise DecodeError("Can't find proper public key in jwks") else: + print( + "else", + public_key, + api_settings.JWT_ALGORITHM, + # api_settings.JWT_VERIFY, + "options",options, + "audience", api_settings.JWT_AUDIENCE, + "issuer", api_settings.JWT_ISSUER, + "leeway",api_settings.JWT_LEEWAY, + ) + decode_text="About" + print("Token", token) + try: + decode_text = jwt.decode( + token, + public_key, + algorithms=[api_settings.JWT_ALGORITHM], + # api_settings.JWT_VERIFY, + options=options, + audience=api_settings.JWT_AUDIENCE, + issuer=api_settings.JWT_ISSUER, + leeway=api_settings.JWT_LEEWAY, + ) + except Exception as e: + print("Debug exception 2", e) + + print("Decode text", decode_text) return jwt.decode( token, public_key, From 4627e3f86a3ddd10f807e3573bc1b114d1794a4a Mon Sep 17 00:00:00 2001 From: Ethan Strominger Date: Tue, 26 Nov 2024 11:31:07 -0500 Subject: [PATCH 03/25] Handle jwt decode error, --- app/core/api/views.py | 20 +--------------- app/core/utils/jwt.py | 55 +++++++------------------------------------ scripts/buildrun.sh | 2 +- 3 files changed, 11 insertions(+), 66 deletions(-) diff --git a/app/core/api/views.py b/app/core/api/views.py index e3b249c8..212f48e6 100644 --- a/app/core/api/views.py +++ b/app/core/api/views.py @@ -10,8 +10,6 @@ from rest_framework.mixins import RetrieveModelMixin from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticatedOrReadOnly -from rest_framework.exceptions import AuthenticationFailed -from core.utils.jwt import cognito_jwt_decode_handler from ..models import Affiliate from ..models import Affiliation from ..models import CheckType @@ -55,7 +53,6 @@ class IsAuthenticated2(BasePermission): """ def has_permission(self, request, view): - print("debug", request.user, request.user.is_authenticated) return bool(request.user and request.user.is_authenticated) @@ -146,22 +143,7 @@ class ProjectViewSet(viewsets.ModelViewSet): permission_classes = [IsAuthenticatedOrReadOnly] queryset = Project.objects.all() serializer_class = ProjectSerializer - # def get_queryset(self, request, *args, **kwargs): - # token = request.headers.get('Authorization') - # print("Received Token:", token) - # super.get() - def perform_authentication(self, request): - print("Here 1") - auth = request.headers.get('Authorization') - if not auth: - raise AuthenticationFailed('Authorization header missing') - try: - prefix, token = auth.split(' ') - print("debug auth", auth) - # Here, call your cognito_jwt_decode_handler with the token - cognito_jwt_decode_handler(token) - except ValueError: - raise AuthenticationFailed('Invalid token format') + @extend_schema_view( list=extend_schema(description="Return a list of all the events"), diff --git a/app/core/utils/jwt.py b/app/core/utils/jwt.py index 950b3eb6..28a7d875 100644 --- a/app/core/utils/jwt.py +++ b/app/core/utils/jwt.py @@ -1,6 +1,8 @@ +# todo: fix +# get expired to work +import traceback import jwt from django.contrib.auth import authenticate -from jwt import DecodeError from jwt.algorithms import RSAAlgorithm from rest_framework_jwt.settings import api_settings @@ -18,55 +20,12 @@ def cognito_jwt_decode_handler(token): https://aws.amazon.com/premiumsupport/knowledge-center/decode-verify-cognito-json-token/ Almost the same as default 'rest_framework_jwt.utils.jwt_decode_handler', but 'secret_key' feature is skipped """ - print("jwt handler", token) - options = {"verify_exp": api_settings.JWT_VERIFY_EXPIRATION} - unverified_header = {} try: + options = {"verify_exp": api_settings.JWT_VERIFY_EXPIRATION} unverified_header = jwt.get_unverified_header(token) - except Exception as e: - print("Debug exception 1", e) - if "kid" not in unverified_header: - print("kid") - return - raise DecodeError("Incorrect authentication credentials.") - - kid = unverified_header["kid"] - print("Try and try") - try: + kid = unverified_header["kid"] # pick a proper public key according to `kid` from token header public_key = RSAAlgorithm.from_jwk(api_settings.JWT_PUBLIC_KEY[kid]) - except KeyError: - print("KeyError") - # in this place we could refresh cached jwks and try again - raise DecodeError("Can't find proper public key in jwks") - else: - print( - "else", - public_key, - api_settings.JWT_ALGORITHM, - # api_settings.JWT_VERIFY, - "options",options, - "audience", api_settings.JWT_AUDIENCE, - "issuer", api_settings.JWT_ISSUER, - "leeway",api_settings.JWT_LEEWAY, - ) - decode_text="About" - print("Token", token) - try: - decode_text = jwt.decode( - token, - public_key, - algorithms=[api_settings.JWT_ALGORITHM], - # api_settings.JWT_VERIFY, - options=options, - audience=api_settings.JWT_AUDIENCE, - issuer=api_settings.JWT_ISSUER, - leeway=api_settings.JWT_LEEWAY, - ) - except Exception as e: - print("Debug exception 2", e) - - print("Decode text", decode_text) return jwt.decode( token, public_key, @@ -77,3 +36,7 @@ def cognito_jwt_decode_handler(token): issuer=api_settings.JWT_ISSUER, leeway=api_settings.JWT_LEEWAY, ) + except Exception as e: + print(traceback.format_exc()) + print("Authentication failed", repr(e)) + raise e diff --git a/scripts/buildrun.sh b/scripts/buildrun.sh index e1346d00..0394a162 100755 --- a/scripts/buildrun.sh +++ b/scripts/buildrun.sh @@ -15,4 +15,4 @@ SCRIPT_DIR="$(dirname "$0")" # m Run migrations # s Create superuser # l Tail logs after run -"$SCRIPT_DIR"/run.sh -c -o -d -b -m "$@" +"$SCRIPT_DIR"/run.sh -c -o -b -m "$@" From 2553707e22c646c0b1639689ba16891258c94daf Mon Sep 17 00:00:00 2001 From: Ethan Strominger Date: Wed, 27 Nov 2024 19:25:15 -0500 Subject: [PATCH 04/25] WIP --- app/core/api/permissions.py | 11 +++++++++++ app/core/api/special_api.py | 35 +++++++++++++++++++++++++++++++++++ app/core/api/urls.py | 2 ++ app/core/api/views.py | 5 ++++- app/peopledepot/settings.py | 25 ++++++++++++++++--------- docker-compose.yml | 3 +++ 6 files changed, 71 insertions(+), 10 deletions(-) create mode 100644 app/core/api/special_api.py diff --git a/app/core/api/permissions.py b/app/core/api/permissions.py index d0036045..a3af6510 100644 --- a/app/core/api/permissions.py +++ b/app/core/api/permissions.py @@ -1,4 +1,5 @@ from rest_framework.permissions import BasePermission +import json class DenyAny(BasePermission): @@ -7,3 +8,13 @@ def has_permission(self, request, view): def has_object_permission(self, request, view, obj): return False + + +class IsAuthenticated2(BasePermission): + """ + Allows access only to authenticated users. + """ + + def has_permission(self, request, view): + print("debug", json.stringify(request.user)) + return bool(request.user and request.user.is_authenticated) diff --git a/app/core/api/special_api.py b/app/core/api/special_api.py new file mode 100644 index 00000000..dca68c0e --- /dev/null +++ b/app/core/api/special_api.py @@ -0,0 +1,35 @@ +from rest_framework.views import APIView +from rest_framework.response import Response +from rest_framework.authtoken.models import Token +from rest_framework import status +from django.contrib.auth import authenticate + + +class LoginAPIView(APIView): + permission_classes = [] + def post(self, request): + username = request.data.get("username") + password = request.data.get("password") + + # Authenticate the user + print("Authenticating") + user = authenticate(username=username, password=password) + print("user", user) + if user: + # Generate JWT token + jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER + jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER + + payload = jwt_payload_handler(user) + token = jwt_encode_handler(payload) + + return Response( + { + "token": token, + } + ) + else: + return Response( + {"error": "Invalid username or password"}, + status=status.HTTP_401_UNAUTHORIZED, + ) diff --git a/app/core/api/urls.py b/app/core/api/urls.py index 37adb4ea..675c1be4 100644 --- a/app/core/api/urls.py +++ b/app/core/api/urls.py @@ -20,6 +20,7 @@ from .views import UserPermissionViewSet from .views import UserProfileAPIView from .views import UserViewSet +from .special_api import LoginAPIView router = routers.SimpleRouter() router.register(r"user-permissions", UserPermissionViewSet, basename="user-permission") @@ -48,6 +49,7 @@ router.register(r"soc-majors", SocMajorViewSet, basename="soc-major") urlpatterns = [ path("me/", UserProfileAPIView.as_view(), name="my_profile"), + path("login/", LoginAPIView.as_view(), name="login") ] urlpatterns += router.urls diff --git a/app/core/api/views.py b/app/core/api/views.py index 212f48e6..c9cfd020 100644 --- a/app/core/api/views.py +++ b/app/core/api/views.py @@ -10,6 +10,7 @@ from rest_framework.mixins import RetrieveModelMixin from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticatedOrReadOnly +from core.api.permissions import IsAuthenticated2 from ..models import Affiliate from ..models import Affiliation from ..models import CheckType @@ -58,9 +59,10 @@ def has_permission(self, request, view): class UserProfileAPIView(RetrieveModelMixin, GenericAPIView): serializer_class = UserSerializer - permission_classes = [IsAuthenticated] + permission_classes = [IsAuthenticated2] def get_object(self): + print("Get object") return self.request.user def get(self, request, *args, **kwargs): @@ -69,6 +71,7 @@ def get(self, request, *args, **kwargs): Get profile of current logged in user. """ + print("Get") return self.retrieve(request, *args, **kwargs) diff --git a/app/peopledepot/settings.py b/app/peopledepot/settings.py index 07160cb8..a58a6247 100644 --- a/app/peopledepot/settings.py +++ b/app/peopledepot/settings.py @@ -39,6 +39,7 @@ # Cognito stuff COGNITO_AWS_REGION = os.environ.get("COGNITO_AWS_REGION", default=None) COGNITO_USER_POOL = os.environ.get("COGNITO_USER_POOL", default=None) +COGNITO_CLIENT_ID = os.environ.get("COGNITO_CLIENT_ID", default=None) # Provide this value if `id_token` is used for authentication (it contains 'aud' claim). # `access_token` doesn't have it, in this case keep the COGNITO_AUDIENCE empty COGNITO_AUDIENCE = None @@ -76,6 +77,7 @@ # 3rd party "django_extensions", "rest_framework", + "rest_framework.authtoken", "drf_spectacular", "phonenumber_field", "timezone_field", @@ -205,15 +207,20 @@ "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema", } -JWT_AUTH = { - "JWT_PAYLOAD_GET_USERNAME_HANDLER": "core.utils.jwt.get_username_from_payload_handler", - "JWT_DECODE_HANDLER": "core.utils.jwt.cognito_jwt_decode_handler", - "JWT_PUBLIC_KEY": rsa_keys, - "JWT_ALGORITHM": "RS256", - "JWT_AUDIENCE": COGNITO_AUDIENCE, - "JWT_ISSUER": COGNITO_POOL_URL, - "JWT_AUTH_HEADER_PREFIX": "Bearer", -} + +print("Cognito", COGNITO_CLIENT_ID) + +if (COGNITO_CLIENT_ID): + print("Setting JWT") + JWT_AUTH = { + "JWT_PAYLOAD_GET_USERNAME_HANDLER": "core.utils.jwt.get_username_from_payload_handler", + "JWT_DECODE_HANDLER": "core.utils.jwt.cognito_jwt_decode_handler", + "JWT_PUBLIC_KEY": rsa_keys, + "JWT_ALGORITHM": "RS256", + "JWT_AUDIENCE": COGNITO_AUDIENCE, + "JWT_ISSUER": COGNITO_POOL_URL, + "JWT_AUTH_HEADER_PREFIX": "Bearer", + } GRAPH_MODELS = {"all_applications": True, "group_models": True} diff --git a/docker-compose.yml b/docker-compose.yml index 69e78df8..ba53f808 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -30,6 +30,9 @@ services: - 8005:8000 volumes: - .:/app + deploy: + replicas: ${ENABLE_MKDOCS:-1} + stop_grace_period: 1s volumes: postgres_data: From 0c5280573f886257ae46b014d31f406ab82a69a7 Mon Sep 17 00:00:00 2001 From: Ethan Strominger Date: Sat, 30 Nov 2024 16:18:10 -0500 Subject: [PATCH 05/25] Small changes --- app/core/api/permissions.py | 10 --------- app/core/api/special_api.py | 4 +++- app/core/api/views.py | 45 ++++++++++++++++++++++++++++--------- 3 files changed, 38 insertions(+), 21 deletions(-) diff --git a/app/core/api/permissions.py b/app/core/api/permissions.py index a3af6510..2636f1a8 100644 --- a/app/core/api/permissions.py +++ b/app/core/api/permissions.py @@ -8,13 +8,3 @@ def has_permission(self, request, view): def has_object_permission(self, request, view, obj): return False - - -class IsAuthenticated2(BasePermission): - """ - Allows access only to authenticated users. - """ - - def has_permission(self, request, view): - print("debug", json.stringify(request.user)) - return bool(request.user and request.user.is_authenticated) diff --git a/app/core/api/special_api.py b/app/core/api/special_api.py index dca68c0e..8b7db35c 100644 --- a/app/core/api/special_api.py +++ b/app/core/api/special_api.py @@ -1,7 +1,8 @@ from rest_framework.views import APIView from rest_framework.response import Response -from rest_framework.authtoken.models import Token from rest_framework import status +from rest_framework_jwt.settings import api_settings + from django.contrib.auth import authenticate @@ -23,6 +24,7 @@ def post(self, request): payload = jwt_payload_handler(user) token = jwt_encode_handler(payload) + return Response( { "token": token, diff --git a/app/core/api/views.py b/app/core/api/views.py index c9cfd020..e2d6a3a6 100644 --- a/app/core/api/views.py +++ b/app/core/api/views.py @@ -1,4 +1,6 @@ from django.contrib.auth import get_user_model +from rest_framework.response import Response + from drf_spectacular.types import OpenApiTypes from drf_spectacular.utils import OpenApiExample from drf_spectacular.utils import OpenApiParameter @@ -10,7 +12,6 @@ from rest_framework.mixins import RetrieveModelMixin from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticatedOrReadOnly -from core.api.permissions import IsAuthenticated2 from ..models import Affiliate from ..models import Affiliation from ..models import CheckType @@ -48,18 +49,10 @@ from .serializers import UserSerializer from rest_framework.permissions import BasePermission -class IsAuthenticated2(BasePermission): - """ - Allows access only to authenticated users. - """ - - def has_permission(self, request, view): - return bool(request.user and request.user.is_authenticated) - class UserProfileAPIView(RetrieveModelMixin, GenericAPIView): serializer_class = UserSerializer - permission_classes = [IsAuthenticated2] + permission_classes = [IsAuthenticated] def get_object(self): print("Get object") @@ -74,6 +67,38 @@ def get(self, request, *args, **kwargs): print("Get") return self.retrieve(request, *args, **kwargs) +class UserProfileAPIView(RetrieveModelMixin, GenericAPIView): + serializer_class = UserSerializer + permission_classes = [IsAuthenticated] + + def get_object(self): + return self.request.user + + def get(self, request, *args, **kwargs): + """ + # User Profile + + Get profile of current logged in user. + """ + return self.retrieve(request, *args, **kwargs) + + + def post(self, request, *args, **kwargs): + """ + Update the profile of the current logged-in user. + """ + user = self.get_object() # Get the logged-in user + serializer = self.serializer_class(user, data=request.data, partial=True) + + if serializer.is_valid(): + # Save the updated user data + serializer.save() + return Response({ "data": serializer.data}) # Return the updated user data + + return Response( + serializer.errors, status=400 + ) # Return validation errors if invalid data + @extend_schema_view( list=extend_schema( From e94bfd6da65dfe97229d14d35ad2d8f0e6290c9e Mon Sep 17 00:00:00 2001 From: Ethan Strominger Date: Sat, 30 Nov 2024 16:45:25 -0500 Subject: [PATCH 06/25] Resolve views.py conflict --- app/core/api/views.py | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/app/core/api/views.py b/app/core/api/views.py index e2d6a3a6..117cf1c2 100644 --- a/app/core/api/views.py +++ b/app/core/api/views.py @@ -50,23 +50,6 @@ from rest_framework.permissions import BasePermission -class UserProfileAPIView(RetrieveModelMixin, GenericAPIView): - serializer_class = UserSerializer - permission_classes = [IsAuthenticated] - - def get_object(self): - print("Get object") - return self.request.user - - def get(self, request, *args, **kwargs): - """ - # User Profile - - Get profile of current logged in user. - """ - print("Get") - return self.retrieve(request, *args, **kwargs) - class UserProfileAPIView(RetrieveModelMixin, GenericAPIView): serializer_class = UserSerializer permission_classes = [IsAuthenticated] @@ -83,7 +66,7 @@ def get(self, request, *args, **kwargs): return self.retrieve(request, *args, **kwargs) - def post(self, request, *args, **kwargs): + def patch(self, request, *args, **kwargs): """ Update the profile of the current logged-in user. """ From 884e8d47ee6997529f6a1f0da387fd9112698bd9 Mon Sep 17 00:00:00 2001 From: Ethan Strominger Date: Sat, 30 Nov 2024 18:07:36 -0500 Subject: [PATCH 07/25] Change post to patch --- app/core/api/special_api.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/core/api/special_api.py b/app/core/api/special_api.py index 8b7db35c..7f9fd347 100644 --- a/app/core/api/special_api.py +++ b/app/core/api/special_api.py @@ -15,7 +15,7 @@ def post(self, request): # Authenticate the user print("Authenticating") user = authenticate(username=username, password=password) - print("user", user) + print("debug user", user) if user: # Generate JWT token jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER @@ -25,6 +25,7 @@ def post(self, request): token = jwt_encode_handler(payload) + return Response( { "token": token, From 319ae56aa7bc1edd5cf984cffe165d97a5ffd741 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 30 Nov 2024 23:09:20 +0000 Subject: [PATCH 08/25] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .github/ISSUE_TEMPLATE/update-table.md | 4 ++-- app/core/api/permissions.py | 3 ++- app/core/api/special_api.py | 12 +++++------- app/core/api/urls.py | 4 ++-- app/core/api/views.py | 9 ++++----- app/core/utils/jwt.py | 1 + app/peopledepot/settings.py | 20 +++++++++----------- 7 files changed, 25 insertions(+), 28 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/update-table.md b/.github/ISSUE_TEMPLATE/update-table.md index 2cdab4c5..ecb3f44a 100644 --- a/.github/ISSUE_TEMPLATE/update-table.md +++ b/.github/ISSUE_TEMPLATE/update-table.md @@ -25,7 +25,7 @@ Current name in code | Updated Name | Updated Type (may already be this type) - [ ] Add the following items in the code Name | Type --- | -- +-- | -- [Replace with ADD TABLE] - [ ] Write a test for the new relationships this model will have with other models (e.g., creating a user and assigning them a set of permissions on a project) if any. - [ ] Update API end point @@ -41,7 +41,7 @@ Name | Type - 1.01.01 [/app/core/models.py](https://github.com/hackforla/peopledepot/blob/main/app/core/models.py) - 1.01.02 [/app/core/admin.py](https://github.com/hackforla/peopledepot/blob/main/app/core/admin.py) - 1.01.03 [/app/core/api/serializers.py](https://github.com/hackforla/peopledepot/blob/main/app/core/api/serializers.py) - - 1.01.04 + - 1.01.04 - 1.02 [People Depot Resources wiki page](https://github.com/hackforla/peopledepot/wiki/Resources-and-Links) for links - ERD - Table and Field Definitions diff --git a/app/core/api/permissions.py b/app/core/api/permissions.py index 2636f1a8..8d42ebfb 100644 --- a/app/core/api/permissions.py +++ b/app/core/api/permissions.py @@ -1,6 +1,7 @@ -from rest_framework.permissions import BasePermission import json +from rest_framework.permissions import BasePermission + class DenyAny(BasePermission): def has_permission(self, request, view): diff --git a/app/core/api/special_api.py b/app/core/api/special_api.py index 7f9fd347..37c555f0 100644 --- a/app/core/api/special_api.py +++ b/app/core/api/special_api.py @@ -1,13 +1,13 @@ -from rest_framework.views import APIView -from rest_framework.response import Response +from django.contrib.auth import authenticate from rest_framework import status +from rest_framework.response import Response +from rest_framework.views import APIView from rest_framework_jwt.settings import api_settings -from django.contrib.auth import authenticate - class LoginAPIView(APIView): permission_classes = [] + def post(self, request): username = request.data.get("username") password = request.data.get("password") @@ -24,13 +24,11 @@ def post(self, request): payload = jwt_payload_handler(user) token = jwt_encode_handler(payload) - - return Response( { "token": token, } - ) + ) else: return Response( {"error": "Invalid username or password"}, diff --git a/app/core/api/urls.py b/app/core/api/urls.py index 7666ff8c..b86978fe 100644 --- a/app/core/api/urls.py +++ b/app/core/api/urls.py @@ -1,6 +1,7 @@ from django.urls import path from rest_framework import routers +from .special_api import LoginAPIView from .views import AffiliateViewSet from .views import AffiliationViewSet from .views import CheckTypeViewSet @@ -22,7 +23,6 @@ from .views import UserProfileAPIView from .views import UserStatusTypeViewSet from .views import UserViewSet -from .special_api import LoginAPIView router = routers.SimpleRouter() router.register(r"user-permissions", UserPermissionViewSet, basename="user-permission") @@ -55,7 +55,7 @@ ) urlpatterns = [ path("me/", UserProfileAPIView.as_view(), name="my_profile"), - path("login/", LoginAPIView.as_view(), name="login") + path("login/", LoginAPIView.as_view(), name="login"), ] urlpatterns += router.urls diff --git a/app/core/api/views.py b/app/core/api/views.py index 8b4443bd..27bb6e76 100644 --- a/app/core/api/views.py +++ b/app/core/api/views.py @@ -1,6 +1,4 @@ from django.contrib.auth import get_user_model -from rest_framework.response import Response - from drf_spectacular.types import OpenApiTypes from drf_spectacular.utils import OpenApiExample from drf_spectacular.utils import OpenApiParameter @@ -10,8 +8,11 @@ from rest_framework import viewsets from rest_framework.generics import GenericAPIView from rest_framework.mixins import RetrieveModelMixin +from rest_framework.permissions import BasePermission from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticatedOrReadOnly +from rest_framework.response import Response + from ..models import Affiliate from ..models import Affiliation from ..models import CheckType @@ -51,7 +52,6 @@ from .serializers import UserPermissionSerializer from .serializers import UserSerializer from .serializers import UserStatusTypeSerializer -from rest_framework.permissions import BasePermission class UserProfileAPIView(RetrieveModelMixin, GenericAPIView): @@ -69,7 +69,6 @@ def get(self, request, *args, **kwargs): """ return self.retrieve(request, *args, **kwargs) - def patch(self, request, *args, **kwargs): """ Update the profile of the current logged-in user. @@ -80,7 +79,7 @@ def patch(self, request, *args, **kwargs): if serializer.is_valid(): # Save the updated user data serializer.save() - return Response({ "data": serializer.data}) # Return the updated user data + return Response({"data": serializer.data}) # Return the updated user data return Response( serializer.errors, status=400 diff --git a/app/core/utils/jwt.py b/app/core/utils/jwt.py index 28a7d875..12d829e6 100644 --- a/app/core/utils/jwt.py +++ b/app/core/utils/jwt.py @@ -1,6 +1,7 @@ # todo: fix # get expired to work import traceback + import jwt from django.contrib.auth import authenticate from jwt.algorithms import RSAAlgorithm diff --git a/app/peopledepot/settings.py b/app/peopledepot/settings.py index a58a6247..41c3a7c9 100644 --- a/app/peopledepot/settings.py +++ b/app/peopledepot/settings.py @@ -53,9 +53,7 @@ # For now there is no rotation of keys (seems like in Cognito decided not to implement it) if COGNITO_AWS_REGION and COGNITO_USER_POOL: try: - COGNITO_POOL_URL = ( - f"https://cognito-idp.{COGNITO_AWS_REGION}.amazonaws.com/{COGNITO_USER_POOL}" - ) + COGNITO_POOL_URL = f"https://cognito-idp.{COGNITO_AWS_REGION}.amazonaws.com/{COGNITO_USER_POOL}" pool_jwks_url = COGNITO_POOL_URL + "/.well-known/jwks.json" jwks = json.loads(request.urlopen(pool_jwks_url).read()) # nosec B310 rsa_keys = {key["kid"]: json.dumps(key) for key in jwks["keys"]} @@ -210,16 +208,16 @@ print("Cognito", COGNITO_CLIENT_ID) -if (COGNITO_CLIENT_ID): +if COGNITO_CLIENT_ID: print("Setting JWT") JWT_AUTH = { - "JWT_PAYLOAD_GET_USERNAME_HANDLER": "core.utils.jwt.get_username_from_payload_handler", - "JWT_DECODE_HANDLER": "core.utils.jwt.cognito_jwt_decode_handler", - "JWT_PUBLIC_KEY": rsa_keys, - "JWT_ALGORITHM": "RS256", - "JWT_AUDIENCE": COGNITO_AUDIENCE, - "JWT_ISSUER": COGNITO_POOL_URL, - "JWT_AUTH_HEADER_PREFIX": "Bearer", + "JWT_PAYLOAD_GET_USERNAME_HANDLER": "core.utils.jwt.get_username_from_payload_handler", + "JWT_DECODE_HANDLER": "core.utils.jwt.cognito_jwt_decode_handler", + "JWT_PUBLIC_KEY": rsa_keys, + "JWT_ALGORITHM": "RS256", + "JWT_AUDIENCE": COGNITO_AUDIENCE, + "JWT_ISSUER": COGNITO_POOL_URL, + "JWT_AUTH_HEADER_PREFIX": "Bearer", } GRAPH_MODELS = {"all_applications": True, "group_models": True} From 72f72dcd3fd6a56eda75be755d533d238443cb74 Mon Sep 17 00:00:00 2001 From: Ethan Strominger Date: Sat, 30 Nov 2024 18:20:34 -0500 Subject: [PATCH 09/25] Changes to support React --- app/core/api/{special_api.py => custom_api.py} | 2 -- app/core/api/permissions.py | 2 -- app/core/api/urls.py | 2 +- app/core/api/views.py | 1 - app/peopledepot/settings.py | 3 ++- 5 files changed, 3 insertions(+), 7 deletions(-) rename app/core/api/{special_api.py => custom_api.py} (94%) diff --git a/app/core/api/special_api.py b/app/core/api/custom_api.py similarity index 94% rename from app/core/api/special_api.py rename to app/core/api/custom_api.py index 7f9fd347..41b176e2 100644 --- a/app/core/api/special_api.py +++ b/app/core/api/custom_api.py @@ -13,9 +13,7 @@ def post(self, request): password = request.data.get("password") # Authenticate the user - print("Authenticating") user = authenticate(username=username, password=password) - print("debug user", user) if user: # Generate JWT token jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER diff --git a/app/core/api/permissions.py b/app/core/api/permissions.py index 2636f1a8..da210630 100644 --- a/app/core/api/permissions.py +++ b/app/core/api/permissions.py @@ -1,6 +1,4 @@ from rest_framework.permissions import BasePermission -import json - class DenyAny(BasePermission): def has_permission(self, request, view): diff --git a/app/core/api/urls.py b/app/core/api/urls.py index 7666ff8c..e6ff024f 100644 --- a/app/core/api/urls.py +++ b/app/core/api/urls.py @@ -22,7 +22,7 @@ from .views import UserProfileAPIView from .views import UserStatusTypeViewSet from .views import UserViewSet -from .special_api import LoginAPIView +from .custom_api import LoginAPIView router = routers.SimpleRouter() router.register(r"user-permissions", UserPermissionViewSet, basename="user-permission") diff --git a/app/core/api/views.py b/app/core/api/views.py index 8b4443bd..3e6eb08a 100644 --- a/app/core/api/views.py +++ b/app/core/api/views.py @@ -51,7 +51,6 @@ from .serializers import UserPermissionSerializer from .serializers import UserSerializer from .serializers import UserStatusTypeSerializer -from rest_framework.permissions import BasePermission class UserProfileAPIView(RetrieveModelMixin, GenericAPIView): diff --git a/app/peopledepot/settings.py b/app/peopledepot/settings.py index a58a6247..73335285 100644 --- a/app/peopledepot/settings.py +++ b/app/peopledepot/settings.py @@ -52,6 +52,8 @@ # On django init we download jwks public keys which are used to validate jwt tokens. # For now there is no rotation of keys (seems like in Cognito decided not to implement it) if COGNITO_AWS_REGION and COGNITO_USER_POOL: + if not COGNITO_CLIENT_ID: + raise Exception("COGNITO_CLIENT_ID not defined. Either define COGNITO_CLIENT_ID or unset COGNIto_AWS_REGION and COGNITO_USER_POOL") try: COGNITO_POOL_URL = ( f"https://cognito-idp.{COGNITO_AWS_REGION}.amazonaws.com/{COGNITO_USER_POOL}" @@ -208,7 +210,6 @@ } -print("Cognito", COGNITO_CLIENT_ID) if (COGNITO_CLIENT_ID): print("Setting JWT") From 3e96aaf35ccf63ef29baa79b94e9424d22858191 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 30 Nov 2024 23:24:42 +0000 Subject: [PATCH 10/25] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- app/core/api/permissions.py | 1 + app/core/api/urls.py | 2 +- app/peopledepot/settings.py | 5 +++-- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/app/core/api/permissions.py b/app/core/api/permissions.py index da210630..d0036045 100644 --- a/app/core/api/permissions.py +++ b/app/core/api/permissions.py @@ -1,5 +1,6 @@ from rest_framework.permissions import BasePermission + class DenyAny(BasePermission): def has_permission(self, request, view): return False diff --git a/app/core/api/urls.py b/app/core/api/urls.py index f99a20ee..890a0b36 100644 --- a/app/core/api/urls.py +++ b/app/core/api/urls.py @@ -1,6 +1,7 @@ from django.urls import path from rest_framework import routers +from .custom_api import LoginAPIView from .special_api import LoginAPIView from .views import AffiliateViewSet from .views import AffiliationViewSet @@ -23,7 +24,6 @@ from .views import UserProfileAPIView from .views import UserStatusTypeViewSet from .views import UserViewSet -from .custom_api import LoginAPIView router = routers.SimpleRouter() router.register(r"user-permissions", UserPermissionViewSet, basename="user-permission") diff --git a/app/peopledepot/settings.py b/app/peopledepot/settings.py index 013d0e15..51e4d5d2 100644 --- a/app/peopledepot/settings.py +++ b/app/peopledepot/settings.py @@ -53,7 +53,9 @@ # For now there is no rotation of keys (seems like in Cognito decided not to implement it) if COGNITO_AWS_REGION and COGNITO_USER_POOL: if not COGNITO_CLIENT_ID: - raise Exception("COGNITO_CLIENT_ID not defined. Either define COGNITO_CLIENT_ID or unset COGNIto_AWS_REGION and COGNITO_USER_POOL") + raise Exception( + "COGNITO_CLIENT_ID not defined. Either define COGNITO_CLIENT_ID or unset COGNIto_AWS_REGION and COGNITO_USER_POOL" + ) try: COGNITO_POOL_URL = f"https://cognito-idp.{COGNITO_AWS_REGION}.amazonaws.com/{COGNITO_USER_POOL}" pool_jwks_url = COGNITO_POOL_URL + "/.well-known/jwks.json" @@ -208,7 +210,6 @@ } - if COGNITO_CLIENT_ID: print("Setting JWT") JWT_AUTH = { From 1c31653cbe017e1b52bb66337f71de5804411e33 Mon Sep 17 00:00:00 2001 From: Ethan Strominger Date: Sat, 30 Nov 2024 18:27:06 -0500 Subject: [PATCH 11/25] Add blank line --- app/core/api/permissions.py | 1 + 1 file changed, 1 insertion(+) diff --git a/app/core/api/permissions.py b/app/core/api/permissions.py index da210630..d0036045 100644 --- a/app/core/api/permissions.py +++ b/app/core/api/permissions.py @@ -1,5 +1,6 @@ from rest_framework.permissions import BasePermission + class DenyAny(BasePermission): def has_permission(self, request, view): return False From 6b233c9462fd2d06dc96db758fc973e6cc1920ed Mon Sep 17 00:00:00 2001 From: Ethan Strominger Date: Sat, 30 Nov 2024 22:52:57 -0500 Subject: [PATCH 12/25] Revert changes to files related to /login local --- app/.env.docker-example | 20 ++------------------ app/core/api/views.py | 17 +++++++++++++++++ app/core/utils/{jwt.py => jwt_handler.py} | 22 +++++++++++----------- app/peopledepot/settings.py | 20 ++++++++++++++++++-- app/requirements.txt | 1 + 5 files changed, 49 insertions(+), 31 deletions(-) rename app/core/utils/{jwt.py => jwt_handler.py} (72%) diff --git a/app/.env.docker-example b/app/.env.docker-example index 01528a9f..2ac1fdf6 100644 --- a/app/.env.docker-example +++ b/app/.env.docker-example @@ -6,6 +6,8 @@ DJANGO_SUPERUSER_USERNAME=admin1111 DJANGO_SUPERUSER_EMAIL=admin@admin.com DJANGO_SUPERUSER_PASSWORD=admin +CORS_ALLOWED_ORIGINS="http://localhost:3000 https://your-production-server.com" + # postgres settings for docker SQL_ENGINE=django.db.backends.postgresql SQL_DATABASE=people_depot_dev @@ -15,24 +17,6 @@ SQL_HOST=db SQL_PORT=5432 DATABASE=postgres -# postgres settings for local development -# SQL_ENGINE=django.db.backends.postgresql -# SQL_DATABASE=postgres -# SQL_USER= -# SQL_PASSWORD= -# SQL_HOST=localhost -# SQL_PORT=5432 -# DATABASE=postgres - -# sqlite settings for local development -# SQL_ENGINE= -# SQL_DATABASE= -# SQL_USER= -# SQL_PASSWORD= -# SQL_HOST= -# SQL_PORT= -# DATABASE= - COGNITO_DOMAIN=peopledepot COGNITO_AWS_REGION=us-west-2 COGNITO_USER_POOL=us-west-2_Fn4rkZpuB diff --git a/app/core/api/views.py b/app/core/api/views.py index f537e7b8..2adba959 100644 --- a/app/core/api/views.py +++ b/app/core/api/views.py @@ -10,6 +10,7 @@ from rest_framework.mixins import RetrieveModelMixin from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticatedOrReadOnly +from rest_framework.response import Response from ..models import Affiliate from ..models import Affiliation @@ -67,6 +68,22 @@ def get(self, request, *args, **kwargs): """ return self.retrieve(request, *args, **kwargs) + def patch(self, request, *args, **kwargs): + """ + Update the profile of the current logged-in user. + """ + user = self.get_object() # Get the logged-in user + serializer = self.serializer_class(user, data=request.data, partial=True) + + if serializer.is_valid(): + # Save the updated user data + serializer.save() + return Response({"data": serializer.data}) # Return the updated user data + + return Response( + serializer.errors, status=400 + ) # Return validation errors if invalid data + @extend_schema_view( list=extend_schema( diff --git a/app/core/utils/jwt.py b/app/core/utils/jwt_handler.py similarity index 72% rename from app/core/utils/jwt.py rename to app/core/utils/jwt_handler.py index 1b2a9bcc..12d829e6 100644 --- a/app/core/utils/jwt.py +++ b/app/core/utils/jwt_handler.py @@ -1,6 +1,9 @@ +# todo: fix +# get expired to work +import traceback + import jwt from django.contrib.auth import authenticate -from jwt import DecodeError from jwt.algorithms import RSAAlgorithm from rest_framework_jwt.settings import api_settings @@ -18,19 +21,12 @@ def cognito_jwt_decode_handler(token): https://aws.amazon.com/premiumsupport/knowledge-center/decode-verify-cognito-json-token/ Almost the same as default 'rest_framework_jwt.utils.jwt_decode_handler', but 'secret_key' feature is skipped """ - options = {"verify_exp": api_settings.JWT_VERIFY_EXPIRATION} - unverified_header = jwt.get_unverified_header(token) - if "kid" not in unverified_header: - raise DecodeError("Incorrect authentication credentials.") - - kid = unverified_header["kid"] try: + options = {"verify_exp": api_settings.JWT_VERIFY_EXPIRATION} + unverified_header = jwt.get_unverified_header(token) + kid = unverified_header["kid"] # pick a proper public key according to `kid` from token header public_key = RSAAlgorithm.from_jwk(api_settings.JWT_PUBLIC_KEY[kid]) - except KeyError: - # in this place we could refresh cached jwks and try again - raise DecodeError("Can't find proper public key in jwks") - else: return jwt.decode( token, public_key, @@ -41,3 +37,7 @@ def cognito_jwt_decode_handler(token): issuer=api_settings.JWT_ISSUER, leeway=api_settings.JWT_LEEWAY, ) + except Exception as e: + print(traceback.format_exc()) + print("Authentication failed", repr(e)) + raise e diff --git a/app/peopledepot/settings.py b/app/peopledepot/settings.py index db019b76..4532b018 100644 --- a/app/peopledepot/settings.py +++ b/app/peopledepot/settings.py @@ -61,6 +61,7 @@ # Application definition INSTALLED_APPS = [ + "corsheaders", "django.contrib.admin", "django.contrib.auth", "django.contrib.contenttypes", @@ -70,6 +71,7 @@ # 3rd party "django_extensions", "rest_framework", + "rest_framework.authtoken", "drf_spectacular", "phonenumber_field", "timezone_field", @@ -79,7 +81,21 @@ "data", ] +# Allow specific origins (like your React dev and production URLs) +CORS_ALLOWED_ORIGINS = os.getenv("CORS_ALLOWED_ORIGINS").split(" ") + +# Optional: Allow credentials (for cookies or tokens) +CORS_ALLOW_CREDENTIALS = True + +# Optional: Control which headers are allowed +CORS_ALLOW_HEADERS = [ + "Authorization", + "Content-Type", +] + + MIDDLEWARE = [ + "corsheaders.middleware.CorsMiddleware", "django.middleware.security.SecurityMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.common.CommonMiddleware", @@ -183,8 +199,8 @@ } JWT_AUTH = { - "JWT_PAYLOAD_GET_USERNAME_HANDLER": "core.utils.jwt.get_username_from_payload_handler", - "JWT_DECODE_HANDLER": "core.utils.jwt.cognito_jwt_decode_handler", + "JWT_PAYLOAD_GET_USERNAME_HANDLER": "core.utils.jwt_handler.get_username_from_payload_handler", + "JWT_DECODE_HANDLER": "core.utils.jwt_handler.cognito_jwt_decode_handler", "JWT_PUBLIC_KEY": rsa_keys, "JWT_ALGORITHM": "RS256", "JWT_AUDIENCE": COGNITO_AUDIENCE, diff --git a/app/requirements.txt b/app/requirements.txt index 7d084f69..30314063 100644 --- a/app/requirements.txt +++ b/app/requirements.txt @@ -22,6 +22,7 @@ django==4.2.11 # djangorestframework # drf-jwt # drf-spectacular +django-cors-headers==4.5.0 django-extensions==3.2.3 django-linear-migrations==2.12.0 django-phonenumber-field==7.3.0 From dc5b878122635f132a84b189c81153ae71d87797 Mon Sep 17 00:00:00 2001 From: Ethan Strominger Date: Sun, 1 Dec 2024 15:45:01 -0500 Subject: [PATCH 13/25] Remove /login API implementation --- app/core/api/custom_api.py | 34 ---- app/core/api/urls.py | 3 - app/core/api/views.py | 2 +- app/core/utils/{jwt_handler.py => jwt.py} | 2 +- app/peopledepot/settings.py | 218 ++++++++++++++++++++++ 5 files changed, 220 insertions(+), 39 deletions(-) delete mode 100644 app/core/api/custom_api.py rename app/core/utils/{jwt_handler.py => jwt.py} (92%) diff --git a/app/core/api/custom_api.py b/app/core/api/custom_api.py deleted file mode 100644 index 0996d79f..00000000 --- a/app/core/api/custom_api.py +++ /dev/null @@ -1,34 +0,0 @@ -from django.contrib.auth import authenticate -from rest_framework import status -from rest_framework.response import Response -from rest_framework.views import APIView -from rest_framework_jwt.settings import api_settings - - -class LoginAPIView(APIView): - permission_classes = [] - - def post(self, request): - username = request.data.get("username") - password = request.data.get("password") - - # Authenticate the user - user = authenticate(username=username, password=password) - if user: - # Generate JWT token - jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER - jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER - - payload = jwt_payload_handler(user) - token = jwt_encode_handler(payload) - - return Response( - { - "token": token, - } - ) - else: - return Response( - {"error": "Invalid username or password"}, - status=status.HTTP_401_UNAUTHORIZED, - ) diff --git a/app/core/api/urls.py b/app/core/api/urls.py index 890a0b36..7c6c2ef5 100644 --- a/app/core/api/urls.py +++ b/app/core/api/urls.py @@ -1,8 +1,6 @@ from django.urls import path from rest_framework import routers -from .custom_api import LoginAPIView -from .special_api import LoginAPIView from .views import AffiliateViewSet from .views import AffiliationViewSet from .views import CheckTypeViewSet @@ -56,7 +54,6 @@ ) urlpatterns = [ path("me/", UserProfileAPIView.as_view(), name="my_profile"), - path("login/", LoginAPIView.as_view(), name="login"), ] urlpatterns += router.urls diff --git a/app/core/api/views.py b/app/core/api/views.py index 27bb6e76..7b10824d 100644 --- a/app/core/api/views.py +++ b/app/core/api/views.py @@ -154,7 +154,7 @@ def get_queryset(self): partial_update=extend_schema(description="Patch a project"), ) class ProjectViewSet(viewsets.ModelViewSet): - permission_classes = [IsAuthenticatedOrReadOnly] + permission_classes = [IsAuthenticated] queryset = Project.objects.all() serializer_class = ProjectSerializer diff --git a/app/core/utils/jwt_handler.py b/app/core/utils/jwt.py similarity index 92% rename from app/core/utils/jwt_handler.py rename to app/core/utils/jwt.py index 12d829e6..ff8815b5 100644 --- a/app/core/utils/jwt_handler.py +++ b/app/core/utils/jwt.py @@ -19,7 +19,7 @@ def cognito_jwt_decode_handler(token): To verify the signature of an Amazon Cognito JWT, first search for the public key with a key ID that matches the key ID in the header of the token. (c) https://aws.amazon.com/premiumsupport/knowledge-center/decode-verify-cognito-json-token/ - Almost the same as default 'rest_framework_jwt.utils.jwt_decode_handler', but 'secret_key' feature is skipped + Almost the same as default 'rest_framework_jwt.utils.jwt', but 'secret_key' feature is skipped """ try: options = {"verify_exp": api_settings.JWT_VERIFY_EXPIRATION} diff --git a/app/peopledepot/settings.py b/app/peopledepot/settings.py index e69de29b..4532b018 100644 --- a/app/peopledepot/settings.py +++ b/app/peopledepot/settings.py @@ -0,0 +1,218 @@ +""" +Django settings for peopledepot project. + +Generated by 'django-admin startproject' using Django 4.0.1. + +For more information on this file, see +https://docs.djangoproject.com/en/4.0/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/4.0/ref/settings/ +""" + +import json +import os +from pathlib import Path +from urllib import request + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = os.environ.get("SECRET_KEY") + +DJANGO_SUPERUSER_USERNAME = os.environ.get("DJANGO_SUPERUSER_USERNAME") +DJANGO_SUPERUSER_EMAIL = os.environ.get("DJANGO_SUPERUSER_EMAIL") +DJANGO_SUPERUSER_PASSWORD = os.environ.get("DJANGO_SUPERUSER_PASSWORD") + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = os.environ.get("DEBUG", default=0) + +# 'DJANGO_ALLOWED_HOSTS' should be a single string of hosts with a space between each. +# For example: 'DJANGO_ALLOWED_HOSTS=localhost 127.0.0.1 [::1]' +ALLOWED_HOSTS = os.environ.get("DJANGO_ALLOWED_HOSTS").split(" ") + +# Cognito stuff +COGNITO_AWS_REGION = os.environ.get("COGNITO_AWS_REGION", default=None) +COGNITO_USER_POOL = os.environ.get("COGNITO_USER_POOL", default=None) +# Provide this value if `id_token` is used for authentication (it contains 'aud' claim). +# `access_token` doesn't have it, in this case keep the COGNITO_AUDIENCE empty +COGNITO_AUDIENCE = None +COGNITO_POOL_URL = ( + None # will be set few lines of code later, if configuration provided +) + +rsa_keys = {} +# To avoid circular imports, we keep this logic here. +# On django init we download jwks public keys which are used to validate jwt tokens. +# For now there is no rotation of keys (seems like in Cognito decided not to implement it) +if COGNITO_AWS_REGION and COGNITO_USER_POOL: + COGNITO_POOL_URL = ( + f"https://cognito-idp.{COGNITO_AWS_REGION}.amazonaws.com/{COGNITO_USER_POOL}" + ) + pool_jwks_url = COGNITO_POOL_URL + "/.well-known/jwks.json" + jwks = json.loads(request.urlopen(pool_jwks_url).read()) # nosec B310 + rsa_keys = {key["kid"]: json.dumps(key) for key in jwks["keys"]} + +# Application definition + +INSTALLED_APPS = [ + "corsheaders", + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + # 3rd party + "django_extensions", + "rest_framework", + "rest_framework.authtoken", + "drf_spectacular", + "phonenumber_field", + "timezone_field", + "django_linear_migrations", + # Local + "core", + "data", +] + +# Allow specific origins (like your React dev and production URLs) +CORS_ALLOWED_ORIGINS = os.getenv("CORS_ALLOWED_ORIGINS").split(" ") + +# Optional: Allow credentials (for cookies or tokens) +CORS_ALLOW_CREDENTIALS = True + +# Optional: Control which headers are allowed +CORS_ALLOW_HEADERS = [ + "Authorization", + "Content-Type", +] + + +MIDDLEWARE = [ + "corsheaders.middleware.CorsMiddleware", + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.auth.middleware.RemoteUserMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", +] + +ROOT_URLCONF = "peopledepot.urls" + +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + ], + }, + }, +] + +WSGI_APPLICATION = "peopledepot.wsgi.application" + + +# Database +# https://docs.djangoproject.com/en/4.0/ref/settings/#databases + +DATABASES = { + "default": { + "ENGINE": os.environ.get("SQL_ENGINE", "django.db.backends.sqlite3"), + "NAME": os.environ.get("SQL_DATABASE", BASE_DIR / "db.sqlite3"), + "USER": os.environ.get("SQL_USER", "user"), + "PASSWORD": os.environ.get("SQL_PASSWORD", "password"), + "HOST": os.environ.get("SQL_HOST", "localhost"), + "PORT": os.environ.get("SQL_PORT", "5432"), + } +} + + +# Password validation +# https://docs.djangoproject.com/en/4.0/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/4.0/topics/i18n/ + +LANGUAGE_CODE = "en-us" + +TIME_ZONE = "UTC" + +USE_I18N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/4.0/howto/static-files/ + +STATIC_URL = "static/" + +# Default primary key field type +# https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" + +AUTH_USER_MODEL = "core.User" + +AUTHENTICATION_BACKENDS = [ + "django.contrib.auth.backends.RemoteUserBackend", + "django.contrib.auth.backends.ModelBackend", +] + +REST_FRAMEWORK = { + "DEFAULT_PERMISSION_CLASSES": ("core.api.permissions.DenyAny",), + "DEFAULT_AUTHENTICATION_CLASSES": ( + "rest_framework_jwt.authentication.JSONWebTokenAuthentication", + ), + "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema", +} + +JWT_AUTH = { + "JWT_PAYLOAD_GET_USERNAME_HANDLER": "core.utils.jwt_handler.get_username_from_payload_handler", + "JWT_DECODE_HANDLER": "core.utils.jwt_handler.cognito_jwt_decode_handler", + "JWT_PUBLIC_KEY": rsa_keys, + "JWT_ALGORITHM": "RS256", + "JWT_AUDIENCE": COGNITO_AUDIENCE, + "JWT_ISSUER": COGNITO_POOL_URL, + "JWT_AUTH_HEADER_PREFIX": "Bearer", +} + +GRAPH_MODELS = {"all_applications": True, "group_models": True} + +SPECTACULAR_SETTINGS = { + "TITLE": "PeopleDepot API", + "DESCRIPTION": "", + "VERSION": "1.0.0", + "SERVE_INCLUDE_SCHEMA": False, +} From dddcb7c9302847a526c6af73a0ddef5054c46e31 Mon Sep 17 00:00:00 2001 From: Ethan Strominger Date: Sun, 1 Dec 2024 15:51:03 -0500 Subject: [PATCH 14/25] Restore jwt.py --- app/core/utils/jwt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/core/utils/jwt.py b/app/core/utils/jwt.py index ff8815b5..12d829e6 100644 --- a/app/core/utils/jwt.py +++ b/app/core/utils/jwt.py @@ -19,7 +19,7 @@ def cognito_jwt_decode_handler(token): To verify the signature of an Amazon Cognito JWT, first search for the public key with a key ID that matches the key ID in the header of the token. (c) https://aws.amazon.com/premiumsupport/knowledge-center/decode-verify-cognito-json-token/ - Almost the same as default 'rest_framework_jwt.utils.jwt', but 'secret_key' feature is skipped + Almost the same as default 'rest_framework_jwt.utils.jwt_decode_handler', but 'secret_key' feature is skipped """ try: options = {"verify_exp": api_settings.JWT_VERIFY_EXPIRATION} From ace35798d2ccc59195f0452575c0dcfb0becba62 Mon Sep 17 00:00:00 2001 From: Ethan Strominger Date: Sun, 1 Dec 2024 15:53:50 -0500 Subject: [PATCH 15/25] Restore update-table.md --- .github/ISSUE_TEMPLATE/update-table.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/update-table.md b/.github/ISSUE_TEMPLATE/update-table.md index ecb3f44a..2cdab4c5 100644 --- a/.github/ISSUE_TEMPLATE/update-table.md +++ b/.github/ISSUE_TEMPLATE/update-table.md @@ -25,7 +25,7 @@ Current name in code | Updated Name | Updated Type (may already be this type) - [ ] Add the following items in the code Name | Type --- | -- +-- | -- [Replace with ADD TABLE] - [ ] Write a test for the new relationships this model will have with other models (e.g., creating a user and assigning them a set of permissions on a project) if any. - [ ] Update API end point @@ -41,7 +41,7 @@ Name | Type - 1.01.01 [/app/core/models.py](https://github.com/hackforla/peopledepot/blob/main/app/core/models.py) - 1.01.02 [/app/core/admin.py](https://github.com/hackforla/peopledepot/blob/main/app/core/admin.py) - 1.01.03 [/app/core/api/serializers.py](https://github.com/hackforla/peopledepot/blob/main/app/core/api/serializers.py) - - 1.01.04 + - 1.01.04 - 1.02 [People Depot Resources wiki page](https://github.com/hackforla/peopledepot/wiki/Resources-and-Links) for links - ERD - Table and Field Definitions From 368e92a80ea6b25d045c44c4bb7cd2d78b55c9af Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 1 Dec 2024 20:54:15 +0000 Subject: [PATCH 16/25] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .github/ISSUE_TEMPLATE/update-table.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/update-table.md b/.github/ISSUE_TEMPLATE/update-table.md index 2cdab4c5..ecb3f44a 100644 --- a/.github/ISSUE_TEMPLATE/update-table.md +++ b/.github/ISSUE_TEMPLATE/update-table.md @@ -25,7 +25,7 @@ Current name in code | Updated Name | Updated Type (may already be this type) - [ ] Add the following items in the code Name | Type --- | -- +-- | -- [Replace with ADD TABLE] - [ ] Write a test for the new relationships this model will have with other models (e.g., creating a user and assigning them a set of permissions on a project) if any. - [ ] Update API end point @@ -41,7 +41,7 @@ Name | Type - 1.01.01 [/app/core/models.py](https://github.com/hackforla/peopledepot/blob/main/app/core/models.py) - 1.01.02 [/app/core/admin.py](https://github.com/hackforla/peopledepot/blob/main/app/core/admin.py) - 1.01.03 [/app/core/api/serializers.py](https://github.com/hackforla/peopledepot/blob/main/app/core/api/serializers.py) - - 1.01.04 + - 1.01.04 - 1.02 [People Depot Resources wiki page](https://github.com/hackforla/peopledepot/wiki/Resources-and-Links) for links - ERD - Table and Field Definitions From 6d69f404a7d098514c5209ee9ef9d5a380b39202 Mon Sep 17 00:00:00 2001 From: Ethan Strominger Date: Sun, 1 Dec 2024 15:58:20 -0500 Subject: [PATCH 17/25] Restore jwt.py --- app/core/utils/jwt.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/app/core/utils/jwt.py b/app/core/utils/jwt.py index 12d829e6..1b2a9bcc 100644 --- a/app/core/utils/jwt.py +++ b/app/core/utils/jwt.py @@ -1,9 +1,6 @@ -# todo: fix -# get expired to work -import traceback - import jwt from django.contrib.auth import authenticate +from jwt import DecodeError from jwt.algorithms import RSAAlgorithm from rest_framework_jwt.settings import api_settings @@ -21,12 +18,19 @@ def cognito_jwt_decode_handler(token): https://aws.amazon.com/premiumsupport/knowledge-center/decode-verify-cognito-json-token/ Almost the same as default 'rest_framework_jwt.utils.jwt_decode_handler', but 'secret_key' feature is skipped """ + options = {"verify_exp": api_settings.JWT_VERIFY_EXPIRATION} + unverified_header = jwt.get_unverified_header(token) + if "kid" not in unverified_header: + raise DecodeError("Incorrect authentication credentials.") + + kid = unverified_header["kid"] try: - options = {"verify_exp": api_settings.JWT_VERIFY_EXPIRATION} - unverified_header = jwt.get_unverified_header(token) - kid = unverified_header["kid"] # pick a proper public key according to `kid` from token header public_key = RSAAlgorithm.from_jwk(api_settings.JWT_PUBLIC_KEY[kid]) + except KeyError: + # in this place we could refresh cached jwks and try again + raise DecodeError("Can't find proper public key in jwks") + else: return jwt.decode( token, public_key, @@ -37,7 +41,3 @@ def cognito_jwt_decode_handler(token): issuer=api_settings.JWT_ISSUER, leeway=api_settings.JWT_LEEWAY, ) - except Exception as e: - print(traceback.format_exc()) - print("Authentication failed", repr(e)) - raise e From ac7bb0864536c8b7a87770618e63f6eae3fec6a6 Mon Sep 17 00:00:00 2001 From: Ethan Strominger Date: Sun, 1 Dec 2024 16:00:33 -0500 Subject: [PATCH 18/25] Remove duplicate env variable in .env.docker-example --- app/.env.docker-example | 1 - 1 file changed, 1 deletion(-) diff --git a/app/.env.docker-example b/app/.env.docker-example index d8f41a33..2ac1fdf6 100644 --- a/app/.env.docker-example +++ b/app/.env.docker-example @@ -1,6 +1,5 @@ DEBUG=1 SECRET_KEY=foo -CORS_ALLOWED_ORIGINS="localhost" DJANGO_PORT=8000 DJANGO_ALLOWED_HOSTS="localhost 127.0.0.1 [::1]" DJANGO_SUPERUSER_USERNAME=admin1111 From 9cc11eb95b408b4d2ed49c94d022aab3e93223be Mon Sep 17 00:00:00 2001 From: Ethan Strominger Date: Sun, 1 Dec 2024 16:03:39 -0500 Subject: [PATCH 19/25] Restore docker-compose.yml --- docker-compose.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index c2f4ecbe..824ad038 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -28,9 +28,6 @@ services: - 8005:8000 volumes: - .:/app - deploy: - replicas: ${ENABLE_MKDOCS:-1} - stop_grace_period: 1s volumes: postgres_data: From ddfe984f9613feb146b4b08b91ccfa2165d91a1f Mon Sep 17 00:00:00 2001 From: Ethan Strominger Date: Sun, 1 Dec 2024 16:06:11 -0500 Subject: [PATCH 20/25] Remove unused import --- app/core/api/views.py | 1 - 1 file changed, 1 deletion(-) diff --git a/app/core/api/views.py b/app/core/api/views.py index 7b10824d..2adba959 100644 --- a/app/core/api/views.py +++ b/app/core/api/views.py @@ -8,7 +8,6 @@ from rest_framework import viewsets from rest_framework.generics import GenericAPIView from rest_framework.mixins import RetrieveModelMixin -from rest_framework.permissions import BasePermission from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticatedOrReadOnly from rest_framework.response import Response From ed177de8589a2dfa1b7a0d20069c7b54256f3dd3 Mon Sep 17 00:00:00 2001 From: Ethan Strominger Date: Sun, 1 Dec 2024 16:47:46 -0500 Subject: [PATCH 21/25] pre-commit and update-table.md --- .github/ISSUE_TEMPLATE/update-table.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/update-table.md b/.github/ISSUE_TEMPLATE/update-table.md index ecb3f44a..a3ecf034 100644 --- a/.github/ISSUE_TEMPLATE/update-table.md +++ b/.github/ISSUE_TEMPLATE/update-table.md @@ -41,7 +41,7 @@ Name | Type - 1.01.01 [/app/core/models.py](https://github.com/hackforla/peopledepot/blob/main/app/core/models.py) - 1.01.02 [/app/core/admin.py](https://github.com/hackforla/peopledepot/blob/main/app/core/admin.py) - 1.01.03 [/app/core/api/serializers.py](https://github.com/hackforla/peopledepot/blob/main/app/core/api/serializers.py) - - 1.01.04 + - 1.01.04 - 1.02 [People Depot Resources wiki page](https://github.com/hackforla/peopledepot/wiki/Resources-and-Links) for links - ERD - Table and Field Definitions From 190919497614a3527c2ba1eecd511343c1a37e58 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 1 Dec 2024 21:49:00 +0000 Subject: [PATCH 22/25] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .github/ISSUE_TEMPLATE/update-table.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/update-table.md b/.github/ISSUE_TEMPLATE/update-table.md index a3ecf034..ecb3f44a 100644 --- a/.github/ISSUE_TEMPLATE/update-table.md +++ b/.github/ISSUE_TEMPLATE/update-table.md @@ -41,7 +41,7 @@ Name | Type - 1.01.01 [/app/core/models.py](https://github.com/hackforla/peopledepot/blob/main/app/core/models.py) - 1.01.02 [/app/core/admin.py](https://github.com/hackforla/peopledepot/blob/main/app/core/admin.py) - 1.01.03 [/app/core/api/serializers.py](https://github.com/hackforla/peopledepot/blob/main/app/core/api/serializers.py) - - 1.01.04 + - 1.01.04 - 1.02 [People Depot Resources wiki page](https://github.com/hackforla/peopledepot/wiki/Resources-and-Links) for links - ERD - Table and Field Definitions From 8e1f7dba7bf79ccfb1c18fce8d451c239d685672 Mon Sep 17 00:00:00 2001 From: Ethan Strominger Date: Sun, 1 Dec 2024 16:51:42 -0500 Subject: [PATCH 23/25] Try to fix update-table.md hanging space --- .github/ISSUE_TEMPLATE/update-table.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/update-table.md b/.github/ISSUE_TEMPLATE/update-table.md index a3ecf034..b3cc834c 100644 --- a/.github/ISSUE_TEMPLATE/update-table.md +++ b/.github/ISSUE_TEMPLATE/update-table.md @@ -1,6 +1,6 @@ --- name: Update Table -about: Describe this issue template's purpose here. +about: Describe the purpose of the issue template here. title: 'Update Table: [TABLE NAME]' labels: 'feature: update table, good first issue, milestone: missing, role: back end, size: 0.25pt, stakeholder: missing' From 8d9bd82316c6fc6d0362fb013d54baee59ecbf20 Mon Sep 17 00:00:00 2001 From: Ethan Strominger Date: Wed, 18 Dec 2024 23:59:26 -0500 Subject: [PATCH 24/25] Squashed commit of the following: commit 0d70d8f887b9294fcc9949194aebcf49e10f87cf Author: Ethan Strominger Date: Wed Dec 18 11:42:21 2024 -0500 Update graphviz commit 775d753164598661bbf74ee5ebc0ec71d729e64d Author: Ethan Strominger Date: Sun Dec 1 17:25:56 2024 -0500 Modify to tricker pre-commit commit c75db1684e04183cc0770dc85478b026f4da3ea8 Author: Ethan Strominger Date: Sun Dec 1 17:23:16 2024 -0500 Fix trailing whitespace --- .github/ISSUE_TEMPLATE/update-table.md | 2 +- app/Dockerfile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/update-table.md b/.github/ISSUE_TEMPLATE/update-table.md index b3cc834c..30345771 100644 --- a/.github/ISSUE_TEMPLATE/update-table.md +++ b/.github/ISSUE_TEMPLATE/update-table.md @@ -41,7 +41,7 @@ Name | Type - 1.01.01 [/app/core/models.py](https://github.com/hackforla/peopledepot/blob/main/app/core/models.py) - 1.01.02 [/app/core/admin.py](https://github.com/hackforla/peopledepot/blob/main/app/core/admin.py) - 1.01.03 [/app/core/api/serializers.py](https://github.com/hackforla/peopledepot/blob/main/app/core/api/serializers.py) - - 1.01.04 + - 1.01.04 - 1.02 [People Depot Resources wiki page](https://github.com/hackforla/peopledepot/wiki/Resources-and-Links) for links - ERD - Table and Field Definitions diff --git a/app/Dockerfile b/app/Dockerfile index 52f9df7d..c1e74074 100644 --- a/app/Dockerfile +++ b/app/Dockerfile @@ -14,7 +14,7 @@ RUN \ --mount=type=cache,target=/var/cache/apk \ --mount=type=cache,target=/etc/apk/cache \ apk add \ - 'graphviz=~9.0' + 'graphviz=~12.2' # install font for graphviz COPY Roboto-Regular.ttf /root/.fonts/ From 2924fb90930b5b41e88ec4a073040c3530425052 Mon Sep 17 00:00:00 2001 From: Ethan Strominger Date: Thu, 19 Dec 2024 17:58:04 -0500 Subject: [PATCH 25/25] Squashed commit of the following: commit 0d70d8f887b9294fcc9949194aebcf49e10f87cf Author: Ethan Strominger Date: Wed Dec 18 11:42:21 2024 -0500 Update graphviz commit 775d753164598661bbf74ee5ebc0ec71d729e64d Author: Ethan Strominger Date: Sun Dec 1 17:25:56 2024 -0500 Modify to tricker pre-commit commit c75db1684e04183cc0770dc85478b026f4da3ea8 Author: Ethan Strominger Date: Sun Dec 1 17:23:16 2024 -0500 Fix trailing whitespace --- .github/ISSUE_TEMPLATE/update-table.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/update-table.md b/.github/ISSUE_TEMPLATE/update-table.md index 30345771..68291efb 100644 --- a/.github/ISSUE_TEMPLATE/update-table.md +++ b/.github/ISSUE_TEMPLATE/update-table.md @@ -1,6 +1,6 @@ --- name: Update Table -about: Describe the purpose of the issue template here. +about: Describe the purpose of the template here. title: 'Update Table: [TABLE NAME]' labels: 'feature: update table, good first issue, milestone: missing, role: back end, size: 0.25pt, stakeholder: missing'