From 784959479803cbcb707f53145c78922500afbe60 Mon Sep 17 00:00:00 2001 From: CaptainAril Date: Tue, 22 Apr 2025 00:42:02 +0100 Subject: [PATCH 1/4] user registration and email verification with queue --- pyproject.toml | 2 + .../payload/requests/CreateUserRequest.py | 10 ++- src/api/services/UserService.py | 62 +++++++++++++ src/api/services/external/RabbitMQRoutes.py | 4 + src/config/apps.py | 1 + uv.lock | 86 ++++++++++++++++++- 6 files changed, 161 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ba4f079..bbb367b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,6 +21,8 @@ dependencies = [ "pyjwt>=2.10.1", "faststream[rabbit]>=0.5.39", "starlette>=0.46.2", + "django-extensions>=4.1", + "requests>=2.32.3", "uvicorn>=0.34.2", ] diff --git a/src/api/models/payload/requests/CreateUserRequest.py b/src/api/models/payload/requests/CreateUserRequest.py index 1a4e086..d8b7be2 100644 --- a/src/api/models/payload/requests/CreateUserRequest.py +++ b/src/api/models/payload/requests/CreateUserRequest.py @@ -1,8 +1,12 @@ from pydantic import EmailStr, BaseModel -from src.api.typing.PasswordValidator import IsStrongPassword - class CreateUserRequest(BaseModel): + id: str email: EmailStr - password: IsStrongPassword + first_name: str + last_name: str + address: str + phone_number: str + profile_picture: str + pin: str diff --git a/src/api/services/UserService.py b/src/api/services/UserService.py index c3d32e5..0f2d4dc 100644 --- a/src/api/services/UserService.py +++ b/src/api/services/UserService.py @@ -1,5 +1,7 @@ from typing import Annotated +from faststream.rabbit import RabbitRouter + from src.utils.svcs import Service from src.utils.logger import Logger from src.api.models.postgres import User @@ -7,10 +9,13 @@ from src.api.typing.UserSuccess import UserSuccess from src.api.constants.activity_types import ACTIVITY_TYPES from src.api.repositories.UserRepository import UserRepository +from src.api.models.payload.requests.CreateUserRequest import CreateUserRequest from src.api.models.payload.requests.UpdateUserRequest import UpdateUserRequest from .UtilityService import UtilityService +UserRouter = RabbitRouter() + @Service() class UserService: @@ -112,3 +117,60 @@ async def update(self, id: str, req: UpdateUserRequest) -> UserSuccess: user = self.utility_service.sanitize_user_object(updated_user) return {"is_success": True, "user": user} + + + @staticmethod + @UserRouter.subscriber("create-user") + async def register(message: dict) -> None: + logger = Logger('UserService') + user_data = CreateUserRequest( + id=message["id"], + email=message["email"], + first_name='', + last_name='', + address='', + phone_number='', + profile_picture='', + pin='', + ) + new_user = await UserRepository.add(user_data) + + logger.info( + { + "activity_type": ACTIVITY_TYPES["USER_REGISTRATION"], + "message": MESSAGES["REGISTRATION"]["USER_REGISTERED"], + "metadata": { + "user": {"id": new_user.id, "email": new_user.email} + }, + } + ) + + @staticmethod + @UserRouter.subscriber("validate-user") + async def validate_user(message: dict) -> None: + logger = Logger('UserService') + user = await UserRepository.find_by_id(message["id"]) + + if not user: + logger.warn( + { + "activity_type": ACTIVITY_TYPES["EMAIL_VALIDATION"], + "message": DYNAMIC_MESSAGES["COMMON"]["FETCHED_FAILED"]("User"), + "metadata": {"user": {"id": message["id"]}}, + } + ) + return + + await UserRepository.update_by_user( + user, {"is_active": True, "is_enabled": True, "is_validated": True} + ) + + logger.info( + { + "activity_type": ACTIVITY_TYPES["EMAIL_VALIDATION"], + "message": MESSAGES["REGISTRATION"]["VERIFICATION_SUCCESS"], + "metadata": { + "user": {"id": user.id, "email": user.email} + }, + } + ) diff --git a/src/api/services/external/RabbitMQRoutes.py b/src/api/services/external/RabbitMQRoutes.py index 8b13789..51aa83c 100644 --- a/src/api/services/external/RabbitMQRoutes.py +++ b/src/api/services/external/RabbitMQRoutes.py @@ -1 +1,5 @@ +from src.config.asgi import broker +from ..UserService import UserRouter + +broker.include_router(UserRouter) \ No newline at end of file diff --git a/src/config/apps.py b/src/config/apps.py index 963d990..be8962e 100644 --- a/src/config/apps.py +++ b/src/config/apps.py @@ -13,6 +13,7 @@ THIRD_PARTY_APPS = [ "corsheaders", + "django_extensions", ] INSTALLED_APPS = [ diff --git a/uv.lock b/uv.lock index ef4b6f7..4c87947 100644 --- a/uv.lock +++ b/uv.lock @@ -1,4 +1,5 @@ version = 1 +revision = 1 requires-python = ">=3.12" [[package]] @@ -119,6 +120,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a9/cf/45fb5261ece3e6b9817d3d82b2f343a505fd58674a92577923bc500bd1aa/bcrypt-4.3.0-cp39-abi3-win_amd64.whl", hash = "sha256:e53e074b120f2877a35cc6c736b8eb161377caae8925c17688bd46ba56daaa5b", size = 152799 }, ] +[[package]] +name = "certifi" +version = "2025.1.31" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 }, +] + [[package]] name = "cfgv" version = "3.4.0" @@ -128,6 +138,41 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249 }, ] +[[package]] +name = "charset-normalizer" +version = "3.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/9a/dd1e1cdceb841925b7798369a09279bd1cf183cef0f9ddf15a3a6502ee45/charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545", size = 196105 }, + { url = "https://files.pythonhosted.org/packages/d3/8c/90bfabf8c4809ecb648f39794cf2a84ff2e7d2a6cf159fe68d9a26160467/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7", size = 140404 }, + { url = "https://files.pythonhosted.org/packages/ad/8f/e410d57c721945ea3b4f1a04b74f70ce8fa800d393d72899f0a40526401f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757", size = 150423 }, + { url = "https://files.pythonhosted.org/packages/f0/b8/e6825e25deb691ff98cf5c9072ee0605dc2acfca98af70c2d1b1bc75190d/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa", size = 143184 }, + { url = "https://files.pythonhosted.org/packages/3e/a2/513f6cbe752421f16d969e32f3583762bfd583848b763913ddab8d9bfd4f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d", size = 145268 }, + { url = "https://files.pythonhosted.org/packages/74/94/8a5277664f27c3c438546f3eb53b33f5b19568eb7424736bdc440a88a31f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616", size = 147601 }, + { url = "https://files.pythonhosted.org/packages/7c/5f/6d352c51ee763623a98e31194823518e09bfa48be2a7e8383cf691bbb3d0/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b", size = 141098 }, + { url = "https://files.pythonhosted.org/packages/78/d4/f5704cb629ba5ab16d1d3d741396aec6dc3ca2b67757c45b0599bb010478/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d", size = 149520 }, + { url = "https://files.pythonhosted.org/packages/c5/96/64120b1d02b81785f222b976c0fb79a35875457fa9bb40827678e54d1bc8/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a", size = 152852 }, + { url = "https://files.pythonhosted.org/packages/84/c9/98e3732278a99f47d487fd3468bc60b882920cef29d1fa6ca460a1fdf4e6/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9", size = 150488 }, + { url = "https://files.pythonhosted.org/packages/13/0e/9c8d4cb99c98c1007cc11eda969ebfe837bbbd0acdb4736d228ccaabcd22/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1", size = 146192 }, + { url = "https://files.pythonhosted.org/packages/b2/21/2b6b5b860781a0b49427309cb8670785aa543fb2178de875b87b9cc97746/charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35", size = 95550 }, + { url = "https://files.pythonhosted.org/packages/21/5b/1b390b03b1d16c7e382b561c5329f83cc06623916aab983e8ab9239c7d5c/charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f", size = 102785 }, + { url = "https://files.pythonhosted.org/packages/38/94/ce8e6f63d18049672c76d07d119304e1e2d7c6098f0841b51c666e9f44a0/charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", size = 195698 }, + { url = "https://files.pythonhosted.org/packages/24/2e/dfdd9770664aae179a96561cc6952ff08f9a8cd09a908f259a9dfa063568/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", size = 140162 }, + { url = "https://files.pythonhosted.org/packages/24/4e/f646b9093cff8fc86f2d60af2de4dc17c759de9d554f130b140ea4738ca6/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", size = 150263 }, + { url = "https://files.pythonhosted.org/packages/5e/67/2937f8d548c3ef6e2f9aab0f6e21001056f692d43282b165e7c56023e6dd/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", size = 142966 }, + { url = "https://files.pythonhosted.org/packages/52/ed/b7f4f07de100bdb95c1756d3a4d17b90c1a3c53715c1a476f8738058e0fa/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", size = 144992 }, + { url = "https://files.pythonhosted.org/packages/96/2c/d49710a6dbcd3776265f4c923bb73ebe83933dfbaa841c5da850fe0fd20b/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", size = 147162 }, + { url = "https://files.pythonhosted.org/packages/b4/41/35ff1f9a6bd380303dea55e44c4933b4cc3c4850988927d4082ada230273/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", size = 140972 }, + { url = "https://files.pythonhosted.org/packages/fb/43/c6a0b685fe6910d08ba971f62cd9c3e862a85770395ba5d9cad4fede33ab/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", size = 149095 }, + { url = "https://files.pythonhosted.org/packages/4c/ff/a9a504662452e2d2878512115638966e75633519ec11f25fca3d2049a94a/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", size = 152668 }, + { url = "https://files.pythonhosted.org/packages/6c/71/189996b6d9a4b932564701628af5cee6716733e9165af1d5e1b285c530ed/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", size = 150073 }, + { url = "https://files.pythonhosted.org/packages/e4/93/946a86ce20790e11312c87c75ba68d5f6ad2208cfb52b2d6a2c32840d922/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", size = 145732 }, + { url = "https://files.pythonhosted.org/packages/cd/e5/131d2fb1b0dddafc37be4f3a2fa79aa4c037368be9423061dccadfd90091/charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", size = 95391 }, + { url = "https://files.pythonhosted.org/packages/27/f2/4f9a69cc7712b9b5ad8fdb87039fd89abba997ad5cbe690d1835d40405b0/charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", size = 102702 }, + { url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767 }, +] + [[package]] name = "click" version = "8.1.8" @@ -151,12 +196,12 @@ wheels = [ [[package]] name = "df-wallet-user-service" -version = "1.0.0" source = { virtual = "." } dependencies = [ { name = "bcrypt" }, { name = "django" }, { name = "django-cors-headers" }, + { name = "django-extensions" }, { name = "django-mongodb-backend" }, { name = "django-ninja" }, { name = "faker" }, @@ -168,6 +213,7 @@ dependencies = [ { name = "pymongo" }, { name = "python-decouple" }, { name = "redis" }, + { name = "requests" }, { name = "starlette" }, { name = "svcs" }, { name = "uvicorn" }, @@ -187,6 +233,7 @@ requires-dist = [ { name = "bcrypt", specifier = ">=4.3.0" }, { name = "django", specifier = ">=5.1.6" }, { name = "django-cors-headers", specifier = ">=4.7.0" }, + { name = "django-extensions", specifier = ">=4.1" }, { name = "django-mongodb-backend", specifier = ">=5.1.0b0" }, { name = "django-ninja", specifier = ">=1.3.0" }, { name = "faker", specifier = ">=36.1.1" }, @@ -198,6 +245,7 @@ requires-dist = [ { name = "pymongo", specifier = ">=4.11.1" }, { name = "python-decouple", specifier = ">=3.8" }, { name = "redis", specifier = ">=5.2.1" }, + { name = "requests", specifier = ">=2.32.3" }, { name = "starlette", specifier = ">=0.46.2" }, { name = "svcs", specifier = ">=25.1.0" }, { name = "uvicorn", specifier = ">=0.34.2" }, @@ -248,6 +296,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7e/a2/7bcfff86314bd9dd698180e31ba00604001606efb518a06cca6833a54285/django_cors_headers-4.7.0-py3-none-any.whl", hash = "sha256:f1c125dcd58479fe7a67fe2499c16ee38b81b397463cf025f0e2c42937421070", size = 12794 }, ] +[[package]] +name = "django-extensions" +version = "4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/b3/ed0f54ed706ec0b54fd251cc0364a249c6cd6c6ec97f04dc34be5e929eac/django_extensions-4.1.tar.gz", hash = "sha256:7b70a4d28e9b840f44694e3f7feb54f55d495f8b3fa6c5c0e5e12bcb2aa3cdeb", size = 283078 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/96/d967ca440d6a8e3861120f51985d8e5aec79b9a8bdda16041206adfe7adc/django_extensions-4.1-py3-none-any.whl", hash = "sha256:0699a7af28f2523bf8db309a80278519362cd4b6e1fd0a8cd4bf063e1e023336", size = 232980 }, +] + [[package]] name = "django-mongodb-backend" version = "5.1.0b1" @@ -842,6 +902,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3c/5f/fa26b9b2672cbe30e07d9a5bdf39cf16e3b80b42916757c5f92bca88e4ba/redis-5.2.1-py3-none-any.whl", hash = "sha256:ee7e1056b9aea0f04c6c2ed59452947f34c4940ee025f5dd83e6a6418b6989e4", size = 261502 }, ] +[[package]] +name = "requests" +version = "2.32.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, +] + [[package]] name = "ruff" version = "0.11.6" @@ -957,6 +1032,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839 }, ] +[[package]] +name = "urllib3" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680 }, +] + [[package]] name = "uvicorn" version = "0.34.2" From 54d37b8b107d1775193f86fe89868d18f34d3158 Mon Sep 17 00:00:00 2001 From: CaptainAril Date: Tue, 22 Apr 2025 00:46:37 +0100 Subject: [PATCH 2/4] fixed ruff format error --- src/api/services/UserService.py | 29 +++++++++------------ src/api/services/external/RabbitMQRoutes.py | 2 +- 2 files changed, 13 insertions(+), 18 deletions(-) diff --git a/src/api/services/UserService.py b/src/api/services/UserService.py index 0f2d4dc..bc55590 100644 --- a/src/api/services/UserService.py +++ b/src/api/services/UserService.py @@ -118,20 +118,19 @@ async def update(self, id: str, req: UpdateUserRequest) -> UserSuccess: return {"is_success": True, "user": user} - @staticmethod @UserRouter.subscriber("create-user") async def register(message: dict) -> None: - logger = Logger('UserService') + logger = Logger("UserService") user_data = CreateUserRequest( id=message["id"], email=message["email"], - first_name='', - last_name='', - address='', - phone_number='', - profile_picture='', - pin='', + first_name="", + last_name="", + address="", + phone_number="", + profile_picture="", + pin="", ) new_user = await UserRepository.add(user_data) @@ -139,16 +138,14 @@ async def register(message: dict) -> None: { "activity_type": ACTIVITY_TYPES["USER_REGISTRATION"], "message": MESSAGES["REGISTRATION"]["USER_REGISTERED"], - "metadata": { - "user": {"id": new_user.id, "email": new_user.email} - }, + "metadata": {"user": {"id": new_user.id, "email": new_user.email}}, } ) @staticmethod @UserRouter.subscriber("validate-user") async def validate_user(message: dict) -> None: - logger = Logger('UserService') + logger = Logger("UserService") user = await UserRepository.find_by_id(message["id"]) if not user: @@ -159,8 +156,8 @@ async def validate_user(message: dict) -> None: "metadata": {"user": {"id": message["id"]}}, } ) - return - + return + await UserRepository.update_by_user( user, {"is_active": True, "is_enabled": True, "is_validated": True} ) @@ -169,8 +166,6 @@ async def validate_user(message: dict) -> None: { "activity_type": ACTIVITY_TYPES["EMAIL_VALIDATION"], "message": MESSAGES["REGISTRATION"]["VERIFICATION_SUCCESS"], - "metadata": { - "user": {"id": user.id, "email": user.email} - }, + "metadata": {"user": {"id": user.id, "email": user.email}}, } ) diff --git a/src/api/services/external/RabbitMQRoutes.py b/src/api/services/external/RabbitMQRoutes.py index 51aa83c..c713a91 100644 --- a/src/api/services/external/RabbitMQRoutes.py +++ b/src/api/services/external/RabbitMQRoutes.py @@ -2,4 +2,4 @@ from ..UserService import UserRouter -broker.include_router(UserRouter) \ No newline at end of file +broker.include_router(UserRouter) From 341d3eea03dfa462b7ed6dd9ed37f94629f657f7 Mon Sep 17 00:00:00 2001 From: CaptainAril Date: Mon, 28 Apr 2025 14:25:25 +0100 Subject: [PATCH 3/4] merge feat/user-registration --- .env.example | 9 ++- pyproject.toml | 1 - src/api/middlewares/GateWayMiddleware.py | 65 ++++++-------------- src/api/services/UserService.py | 17 +++--- src/api/services/UtilityService.py | 77 ++++++++++++++++++++++-- src/config/asgi.py | 15 ++++- src/config/settings.py | 15 ++++- src/env.py | 24 ++++++-- src/utils/env/__init__.py | 3 +- src/utils/env/env.py | 7 ++- uv.lock | 70 --------------------- 11 files changed, 153 insertions(+), 150 deletions(-) diff --git a/.env.example b/.env.example index 8590fbc..3968564 100644 --- a/.env.example +++ b/.env.example @@ -29,8 +29,11 @@ JWT_ISSUER=xxx # time in minutes OTP_LIFETIME=10 -API_GATEWAY_PUBLIC_KEY=xxxxxxxxxxxxxxxxxxxxxxxx -API_KEY_EXPIRES_AT=2 +GATEWAY_PUBLIC_KEY='XXXXXXXXXXXXXXXXXXX' +GATEWAY_KEY_TTL=2 # amqp://username:password@host[:port]/ -RABBITMQ_URL=amqp://guest:guest@localhost:5672/ \ No newline at end of file +RABBITMQ_URL=amqp://guest:guest@localhost:5672/ + +QUEUE_SECRECT_KEY='XXXXXXXXXXXXXXXXX' +QUEUE_SECRECT_KEY_TTL= 0.5 \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index bbb367b..ee59cc8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,6 @@ dependencies = [ "faststream[rabbit]>=0.5.39", "starlette>=0.46.2", "django-extensions>=4.1", - "requests>=2.32.3", "uvicorn>=0.34.2", ] diff --git a/src/api/middlewares/GateWayMiddleware.py b/src/api/middlewares/GateWayMiddleware.py index 9ce6687..8af1ec2 100644 --- a/src/api/middlewares/GateWayMiddleware.py +++ b/src/api/middlewares/GateWayMiddleware.py @@ -1,12 +1,10 @@ -import hmac -import hashlib -from datetime import datetime, timedelta - from django.http import HttpRequest from ninja.errors import AuthenticationError -from ninja.security import APIKeyHeader from ninja.openapi.schema import OpenAPISchema +from ninja.security import APIKeyHeader +from src.api.constants.signature_sources import SIGNATURE_SOURCES +from src.api.services.UtilityService import SignatureData, UtilityService from src.env import api_gateway from src.utils.logger import Logger @@ -27,7 +25,7 @@ def authenticate(self, request: HttpRequest, key: str | None) -> str | None: message = f"Missing required header: {e}" self.logger.error( { - "activity_type": "Authenticate User", + "activity_type": "Authenticate Gateway Request", "message": message, "metadata": {"headers": request.headers}, } @@ -39,22 +37,31 @@ def authenticate(self, request: HttpRequest, key: str | None) -> str | None: message = "Invalid API key!" self.logger.error( { - "activity_type": "Authenticate User", + "activity_type": "Authenticate Gateway Request", "message": message, "metadata": {"headers": request.headers}, } ) raise AuthenticationError(message=message) + + signature_data: SignatureData = { + "signature": api_signature, + "timestamp": api_timestamp, + "key": valid_api_key, + "ttl": api_gateway["ttl"], + "title": SIGNATURE_SOURCES["gateway"], + } - self._verify_signature(valid_api_key, api_signature, api_timestamp) + UtilityService.verify_signature(logger=self.logger, signature_data=signature_data) self.logger.debug( { - "activity_type": "Authenticate User", - "message": "Successfully authenticated user", + "activity_type": "Authenticate Gateway Request", + "message": "Successfully authenticated gateway request", "metadata": { "user_id": user_id, "user_email": user_email, + "headers": request.headers, }, } ) @@ -63,44 +70,6 @@ def authenticate(self, request: HttpRequest, key: str | None) -> str | None: return api_signature - def _verify_signature( - self, valid_api_key: str, signature: str, timestamp: str - ) -> bool: - valid_signature = self.generate_signature(valid_api_key, timestamp) - is_valid = hmac.compare_digest(valid_signature, signature) - - if not is_valid: - message = "Invalid signature!" - self.logger.error( - { - "activity_type": "Authenticate User", - "message": message, - "metadata": {"signature": signature}, - } - ) - raise AuthenticationError(message=message) - - initial_time = datetime.fromtimestamp(int(timestamp) / 1000) - valid_window = initial_time + timedelta(minutes=api_gateway["expires_at"]) - if valid_window < datetime.now(): - message = "Signature expired!" - self.logger.error( - { - "activity_type": "Authenticate User", - "message": message, - "metadata": {"timestamp": timestamp}, - } - ) - raise AuthenticationError(message=message) - - return True - - def generate_signature(self, api_key: str, timestamp: str) -> str: - signature = hmac.new( - key=api_key.encode(), msg=timestamp.encode(), digestmod=hashlib.sha256 - ).hexdigest() - return signature - def get_authentication() -> GateWayAuth: gateway_auth = GateWayAuth(Logger("Authentication")) diff --git a/src/api/services/UserService.py b/src/api/services/UserService.py index bc55590..b743e74 100644 --- a/src/api/services/UserService.py +++ b/src/api/services/UserService.py @@ -2,15 +2,16 @@ from faststream.rabbit import RabbitRouter -from src.utils.svcs import Service -from src.utils.logger import Logger -from src.api.models.postgres import User -from src.api.constants.messages import MESSAGES, DYNAMIC_MESSAGES -from src.api.typing.UserSuccess import UserSuccess from src.api.constants.activity_types import ACTIVITY_TYPES -from src.api.repositories.UserRepository import UserRepository +from src.api.constants.messages import DYNAMIC_MESSAGES, MESSAGES +from src.api.constants.queues import QUEUE_NAMES from src.api.models.payload.requests.CreateUserRequest import CreateUserRequest from src.api.models.payload.requests.UpdateUserRequest import UpdateUserRequest +from src.api.models.postgres import User +from src.api.repositories.UserRepository import UserRepository +from src.api.typing.UserSuccess import UserSuccess +from src.utils.logger import Logger +from src.utils.svcs import Service from .UtilityService import UtilityService @@ -119,7 +120,7 @@ async def update(self, id: str, req: UpdateUserRequest) -> UserSuccess: return {"is_success": True, "user": user} @staticmethod - @UserRouter.subscriber("create-user") + @UserRouter.subscriber(queue=QUEUE_NAMES["USER_REGISTRATION"]) async def register(message: dict) -> None: logger = Logger("UserService") user_data = CreateUserRequest( @@ -143,7 +144,7 @@ async def register(message: dict) -> None: ) @staticmethod - @UserRouter.subscriber("validate-user") + @UserRouter.subscriber(queue=QUEUE_NAMES["EMAIL_VALIDATION"]) async def validate_user(message: dict) -> None: logger = Logger("UserService") user = await UserRepository.find_by_id(message["id"]) diff --git a/src/api/services/UtilityService.py b/src/api/services/UtilityService.py index e03388c..a524601 100644 --- a/src/api/services/UtilityService.py +++ b/src/api/services/UtilityService.py @@ -1,20 +1,31 @@ +import hashlib +import hmac +from datetime import datetime, timedelta +from typing import TypedDict from uuid import uuid4 -from datetime import timedelta -import jwt import bcrypt -from faker import Faker +import jwt from django.utils import timezone +from faker import Faker +from ninja.errors import AuthenticationError -from src.env import jwt_config -from src.utils.svcs import Service +from src.api.enums.CharacterCasing import CharacterCasing from src.api.models.postgres import User from src.api.typing.ExpireUUID import ExpireUUID -from src.api.enums.CharacterCasing import CharacterCasing +from src.env import jwt_config +from src.utils.logger import Logger +from src.utils.svcs import Service DEFAULT_CHARACTER_LENGTH = 12 fake = Faker() +class SignatureData(TypedDict): + title: str + signature: str + timestamp: str + key: str + ttl: int | float @Service() class UtilityService: @@ -80,3 +91,57 @@ def generate_uuid() -> ExpireUUID: lifespan = timedelta(hours=24) expires_at = current_time + lifespan return {"uuid": uuid4(), "expires_at": expires_at} + + @staticmethod + def generate_signature(key: str, timestamp: str) -> str: + signature = hmac.new( + key=key.encode(), msg=timestamp.encode(), digestmod=hashlib.sha256 + ).hexdigest() + return signature + + + @staticmethod + def verify_signature(signature_data: SignatureData, logger: Logger) -> bool: + + signature = signature_data["signature"] + timestamp = signature_data["timestamp"] + key = signature_data["key"] + ttl = signature_data["ttl"] + title = signature_data["title"] + + + valid_signature = UtilityService.generate_signature(key, timestamp) + is_valid = hmac.compare_digest(valid_signature, signature) + + if not is_valid: + message = "Invalid signature!" + logger.error( + { + "activity_type": f"Authenticate {title} Request", + "message": message, + "metadata": {"signature": signature}, + } + ) + raise AuthenticationError(message=message) + + initial_time = datetime.fromtimestamp(float(timestamp)/ 1000) + valid_window = initial_time + timedelta(minutes=ttl) + + if valid_window < datetime.now(): + message = "Signature expired!" + logger.error( + { + "activity_type": f"Authenticate {title} Request", + "message": message, + "metadata": {"timestamp": timestamp}, + } + ) + raise AuthenticationError(message=message) + + return True + + + @staticmethod + def get_timestamp() -> str: + current_time = datetime.now().timestamp() * 1000 + return str(current_time) \ No newline at end of file diff --git a/src/config/asgi.py b/src/config/asgi.py index 00c0ac7..8ef43eb 100644 --- a/src/config/asgi.py +++ b/src/config/asgi.py @@ -11,8 +11,8 @@ from django.core.asgi import get_asgi_application from faststream.rabbit import RabbitBroker -from starlette.routing import Mount from starlette.applications import Starlette +from starlette.routing import Mount from src.env import rabbitmq_config @@ -21,11 +21,20 @@ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "src.config.settings") +def setup_broker_middlewares(): + from src.api.middlewares.BrokerMiddleware import (PublishMiddleware, + SubscribeMiddleware) + + broker.add_middleware(SubscribeMiddleware) + broker.add_middleware(PublishMiddleware) + + application = Starlette( routes=[Mount("/", get_asgi_application())], # type: ignore - on_startup=[broker.start], + on_startup=[setup_broker_middlewares, broker.start], on_shutdown=[broker.close], ) -from src.api.services.external import RabbitMQRoutes as RabbitMQRoutes # noqa: E402 +from src.api.services.external import \ + RabbitMQRoutes as RabbitMQRoutes # noqa: E402 diff --git a/src/config/settings.py b/src/config/settings.py index fb8cb6e..6fe520b 100644 --- a/src/config/settings.py +++ b/src/config/settings.py @@ -2,11 +2,11 @@ from .apps import INSTALLED_APPS as INSTALLED_APPS from .caches import CACHES as CACHES -from .logger import LOGGING as LOGGING -from .databases import DATABASES as DATABASES from .databases import DATABASE_ROUTERS as DATABASE_ROUTERS -from .templates import TEMPLATES as TEMPLATES +from .databases import DATABASES as DATABASES +from .logger import LOGGING as LOGGING from .middleware import MIDDLEWARE as MIDDLEWARE +from .templates import TEMPLATES as TEMPLATES SECRET_KEY = app["secret_key"] DEBUG = app["debug"] @@ -18,3 +18,12 @@ STATIC_URL = "static/" MEDIA_URL = "media/" + + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_TZ = True \ No newline at end of file diff --git a/src/env.py b/src/env.py index fcecc4e..4c9495b 100644 --- a/src/env.py +++ b/src/env.py @@ -1,7 +1,7 @@ from typing import TypedDict -from src import __name__, __version__, __description__, __display_name__ -from src.utils.env import get_env_int, get_env_str, get_env_list +from src import __description__, __display_name__, __name__, __version__ +from src.utils.env import get_env_float, get_env_int, get_env_list, get_env_str class Env: @@ -54,6 +54,14 @@ class RabbitMQ(TypedDict): url: str +class Gateway(TypedDict): + key: str + ttl: int + +class Queue(TypedDict): + key: str + ttl: float + env = Env() app: App = { @@ -102,9 +110,14 @@ class RabbitMQ(TypedDict): "issuer": get_env_str("JWT_ISSUER"), } -api_gateway = { - "key": get_env_str("API_GATEWAY_PUBLIC_KEY"), - "expires_at": get_env_int("API_KEY_EXPIRES_AT"), +api_gateway: Gateway = { + "key": get_env_str("GATEWAY_PUBLIC_KEY"), + "ttl": get_env_int("GATEWAY_KEY_TTL"), +} + +queue: Queue = { + "key": get_env_str("QUEUE_SECRECT_KEY"), + "ttl": get_env_float("QUEUE_SECRECT_KEY_TTL"), } otp: OTP = {"lifetime": get_env_int("OTP_LIFETIME")} @@ -122,4 +135,5 @@ class RabbitMQ(TypedDict): "log", "otp", "rabbitmq_config", + "queue", ] diff --git a/src/utils/env/__init__.py b/src/utils/env/__init__.py index c231c7c..45de2e4 100644 --- a/src/utils/env/__init__.py +++ b/src/utils/env/__init__.py @@ -1,7 +1,8 @@ -from .env import get_env_int, get_env_str, get_env_list +from .env import get_env_float, get_env_int, get_env_list, get_env_str __all__ = [ "get_env_int", "get_env_list", "get_env_str", + "get_env_float", ] diff --git a/src/utils/env/env.py b/src/utils/env/env.py index a4a9fee..5a7fae8 100644 --- a/src/utils/env/env.py +++ b/src/utils/env/env.py @@ -1,7 +1,7 @@ -from pathlib import Path from functools import cache +from pathlib import Path -from decouple import Config, AutoConfig, RepositoryEnv +from decouple import AutoConfig, Config, RepositoryEnv from decouple import config as decouple_config BASE_DIR = Path(__file__).resolve().parent.parent.parent @@ -25,6 +25,9 @@ def get_env_str(name: str, default: str | None = None) -> str: def get_env_int(name: str, default: str | None = None) -> int: return int(get_env_variable(name, default=default, cast=int)) +def get_env_float(name: str, default: str | None = None) -> float: + return float(get_env_variable(name, default=default, cast=float)) + def get_env_list(name: str, sep: str = ",", default: str | None = None) -> list[str]: return list( diff --git a/uv.lock b/uv.lock index 4c87947..f9977e7 100644 --- a/uv.lock +++ b/uv.lock @@ -120,15 +120,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a9/cf/45fb5261ece3e6b9817d3d82b2f343a505fd58674a92577923bc500bd1aa/bcrypt-4.3.0-cp39-abi3-win_amd64.whl", hash = "sha256:e53e074b120f2877a35cc6c736b8eb161377caae8925c17688bd46ba56daaa5b", size = 152799 }, ] -[[package]] -name = "certifi" -version = "2025.1.31" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 }, -] - [[package]] name = "cfgv" version = "3.4.0" @@ -138,41 +129,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249 }, ] -[[package]] -name = "charset-normalizer" -version = "3.4.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0a/9a/dd1e1cdceb841925b7798369a09279bd1cf183cef0f9ddf15a3a6502ee45/charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545", size = 196105 }, - { url = "https://files.pythonhosted.org/packages/d3/8c/90bfabf8c4809ecb648f39794cf2a84ff2e7d2a6cf159fe68d9a26160467/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7", size = 140404 }, - { url = "https://files.pythonhosted.org/packages/ad/8f/e410d57c721945ea3b4f1a04b74f70ce8fa800d393d72899f0a40526401f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757", size = 150423 }, - { url = "https://files.pythonhosted.org/packages/f0/b8/e6825e25deb691ff98cf5c9072ee0605dc2acfca98af70c2d1b1bc75190d/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa", size = 143184 }, - { url = "https://files.pythonhosted.org/packages/3e/a2/513f6cbe752421f16d969e32f3583762bfd583848b763913ddab8d9bfd4f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d", size = 145268 }, - { url = "https://files.pythonhosted.org/packages/74/94/8a5277664f27c3c438546f3eb53b33f5b19568eb7424736bdc440a88a31f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616", size = 147601 }, - { url = "https://files.pythonhosted.org/packages/7c/5f/6d352c51ee763623a98e31194823518e09bfa48be2a7e8383cf691bbb3d0/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b", size = 141098 }, - { url = "https://files.pythonhosted.org/packages/78/d4/f5704cb629ba5ab16d1d3d741396aec6dc3ca2b67757c45b0599bb010478/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d", size = 149520 }, - { url = "https://files.pythonhosted.org/packages/c5/96/64120b1d02b81785f222b976c0fb79a35875457fa9bb40827678e54d1bc8/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a", size = 152852 }, - { url = "https://files.pythonhosted.org/packages/84/c9/98e3732278a99f47d487fd3468bc60b882920cef29d1fa6ca460a1fdf4e6/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9", size = 150488 }, - { url = "https://files.pythonhosted.org/packages/13/0e/9c8d4cb99c98c1007cc11eda969ebfe837bbbd0acdb4736d228ccaabcd22/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1", size = 146192 }, - { url = "https://files.pythonhosted.org/packages/b2/21/2b6b5b860781a0b49427309cb8670785aa543fb2178de875b87b9cc97746/charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35", size = 95550 }, - { url = "https://files.pythonhosted.org/packages/21/5b/1b390b03b1d16c7e382b561c5329f83cc06623916aab983e8ab9239c7d5c/charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f", size = 102785 }, - { url = "https://files.pythonhosted.org/packages/38/94/ce8e6f63d18049672c76d07d119304e1e2d7c6098f0841b51c666e9f44a0/charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", size = 195698 }, - { url = "https://files.pythonhosted.org/packages/24/2e/dfdd9770664aae179a96561cc6952ff08f9a8cd09a908f259a9dfa063568/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", size = 140162 }, - { url = "https://files.pythonhosted.org/packages/24/4e/f646b9093cff8fc86f2d60af2de4dc17c759de9d554f130b140ea4738ca6/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", size = 150263 }, - { url = "https://files.pythonhosted.org/packages/5e/67/2937f8d548c3ef6e2f9aab0f6e21001056f692d43282b165e7c56023e6dd/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", size = 142966 }, - { url = "https://files.pythonhosted.org/packages/52/ed/b7f4f07de100bdb95c1756d3a4d17b90c1a3c53715c1a476f8738058e0fa/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", size = 144992 }, - { url = "https://files.pythonhosted.org/packages/96/2c/d49710a6dbcd3776265f4c923bb73ebe83933dfbaa841c5da850fe0fd20b/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", size = 147162 }, - { url = "https://files.pythonhosted.org/packages/b4/41/35ff1f9a6bd380303dea55e44c4933b4cc3c4850988927d4082ada230273/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", size = 140972 }, - { url = "https://files.pythonhosted.org/packages/fb/43/c6a0b685fe6910d08ba971f62cd9c3e862a85770395ba5d9cad4fede33ab/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", size = 149095 }, - { url = "https://files.pythonhosted.org/packages/4c/ff/a9a504662452e2d2878512115638966e75633519ec11f25fca3d2049a94a/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", size = 152668 }, - { url = "https://files.pythonhosted.org/packages/6c/71/189996b6d9a4b932564701628af5cee6716733e9165af1d5e1b285c530ed/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", size = 150073 }, - { url = "https://files.pythonhosted.org/packages/e4/93/946a86ce20790e11312c87c75ba68d5f6ad2208cfb52b2d6a2c32840d922/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", size = 145732 }, - { url = "https://files.pythonhosted.org/packages/cd/e5/131d2fb1b0dddafc37be4f3a2fa79aa4c037368be9423061dccadfd90091/charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", size = 95391 }, - { url = "https://files.pythonhosted.org/packages/27/f2/4f9a69cc7712b9b5ad8fdb87039fd89abba997ad5cbe690d1835d40405b0/charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", size = 102702 }, - { url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767 }, -] - [[package]] name = "click" version = "8.1.8" @@ -213,7 +169,6 @@ dependencies = [ { name = "pymongo" }, { name = "python-decouple" }, { name = "redis" }, - { name = "requests" }, { name = "starlette" }, { name = "svcs" }, { name = "uvicorn" }, @@ -245,7 +200,6 @@ requires-dist = [ { name = "pymongo", specifier = ">=4.11.1" }, { name = "python-decouple", specifier = ">=3.8" }, { name = "redis", specifier = ">=5.2.1" }, - { name = "requests", specifier = ">=2.32.3" }, { name = "starlette", specifier = ">=0.46.2" }, { name = "svcs", specifier = ">=25.1.0" }, { name = "uvicorn", specifier = ">=0.34.2" }, @@ -902,21 +856,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3c/5f/fa26b9b2672cbe30e07d9a5bdf39cf16e3b80b42916757c5f92bca88e4ba/redis-5.2.1-py3-none-any.whl", hash = "sha256:ee7e1056b9aea0f04c6c2ed59452947f34c4940ee025f5dd83e6a6418b6989e4", size = 261502 }, ] -[[package]] -name = "requests" -version = "2.32.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "charset-normalizer" }, - { name = "idna" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, -] - [[package]] name = "ruff" version = "0.11.6" @@ -1032,15 +971,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839 }, ] -[[package]] -name = "urllib3" -version = "2.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680 }, -] - [[package]] name = "uvicorn" version = "0.34.2" From efae570f47ad786992c2c010ffea1bb91193bdd0 Mon Sep 17 00:00:00 2001 From: CaptainAril Date: Mon, 28 Apr 2025 15:53:33 +0100 Subject: [PATCH 4/4] inter-service broker security --- src/api/constants/queues.py | 12 ++++ src/api/constants/signature_sources.py | 12 ++++ src/api/middlewares/BrokerMiddleware.py | 76 ++++++++++++++++++++++++ src/api/middlewares/GateWayMiddleware.py | 12 ++-- src/api/services/UserService.py | 14 ++--- src/api/services/UtilityService.py | 32 +++++----- src/config/asgi.py | 14 +++-- src/config/settings.py | 12 ++-- src/env.py | 8 ++- src/utils/env/__init__.py | 4 +- src/utils/env/env.py | 5 +- 11 files changed, 153 insertions(+), 48 deletions(-) create mode 100644 src/api/constants/queues.py create mode 100644 src/api/constants/signature_sources.py create mode 100644 src/api/middlewares/BrokerMiddleware.py diff --git a/src/api/constants/queues.py b/src/api/constants/queues.py new file mode 100644 index 0000000..b7bb09b --- /dev/null +++ b/src/api/constants/queues.py @@ -0,0 +1,12 @@ +from typing import TypedDict + + +class QueueNames(TypedDict): + USER_REGISTRATION: str + EMAIL_VALIDATION: str + + +QUEUE_NAMES: QueueNames = { + "USER_REGISTRATION": "create_user", + "EMAIL_VALIDATION": "validate_email", +} diff --git a/src/api/constants/signature_sources.py b/src/api/constants/signature_sources.py new file mode 100644 index 0000000..38bb96b --- /dev/null +++ b/src/api/constants/signature_sources.py @@ -0,0 +1,12 @@ +from typing import TypedDict + + +class SignatureSources(TypedDict): + gateway: str + queue: str + + +SIGNATURE_SOURCES: SignatureSources = { + "gateway": "Gateway", + "queue": "Broker Queue", +} diff --git a/src/api/middlewares/BrokerMiddleware.py b/src/api/middlewares/BrokerMiddleware.py new file mode 100644 index 0000000..94756a7 --- /dev/null +++ b/src/api/middlewares/BrokerMiddleware.py @@ -0,0 +1,76 @@ +from types import CoroutineType +from typing import Any +from collections.abc import Callable, Awaitable + +from faststream import BaseMiddleware +from faststream.broker.message import StreamMessage +from faststream.rabbit.message import RabbitMessage + +from src.env import queue +from src.utils.logger import Logger +from src.api.services.UtilityService import UtilityService +from src.api.constants.signature_sources import SIGNATURE_SOURCES + + +class PublishMiddleware(BaseMiddleware): + """ + Middleware to handle subscription messages. + """ + + async def publish_scope( + self, + call_next: Callable[..., Awaitable[Any]], + msg: RabbitMessage, + *args: tuple[Any, ...], + **kwargs: dict[str, Any], + ) -> CoroutineType: + timestamp = UtilityService.get_timestamp() + signature = UtilityService.generate_signature(queue["key"], timestamp) + + headers = {} + + headers["X-BROKER-SIGNATURE"] = signature + headers["X-BROKER-TIMESTAMP"] = timestamp + headers["X-BROKER-KEY"] = queue["key"] + + kwargs["headers"] = headers + return await super().publish_scope(call_next, msg, *args, **kwargs) + + +class SubscribeMiddleware(BaseMiddleware): + async def consume_scope( + self, call_next: Callable[[Any], Awaitable[Any]], msg: StreamMessage + ) -> CoroutineType | None: + logger = Logger(__name__) + try: + UtilityService.verify_signature( + logger=logger, + signature_data={ + "signature": msg.headers["X-BROKER-SIGNATURE"], + "timestamp": msg.headers["X-BROKER-TIMESTAMP"], + "key": queue["key"], + "ttl": queue["ttl"], + "title": SIGNATURE_SOURCES["gateway"], + }, + ) + + return await super().consume_scope(call_next, msg) + except KeyError as e: + message = f"Missing required header: {e}" + logger.error( + { + "activity_type": "Authenticate GatewaBroker Queue Request", + "message": message, + "metadata": {"headers": msg.headers}, + } + ) + except Exception as e: + queue_operation = msg.raw_message.routing_key + message = f"`{queue_operation}` operation failed: {e}" + logger.error( + { + "activity_type": "Authenticate GatewaBroker Queue Request", + "message": message, + "metadata": {"headers": msg.headers, "message": msg._decoded_body}, + } + ) diff --git a/src/api/middlewares/GateWayMiddleware.py b/src/api/middlewares/GateWayMiddleware.py index 8af1ec2..932d4af 100644 --- a/src/api/middlewares/GateWayMiddleware.py +++ b/src/api/middlewares/GateWayMiddleware.py @@ -1,12 +1,12 @@ from django.http import HttpRequest from ninja.errors import AuthenticationError -from ninja.openapi.schema import OpenAPISchema from ninja.security import APIKeyHeader +from ninja.openapi.schema import OpenAPISchema -from src.api.constants.signature_sources import SIGNATURE_SOURCES -from src.api.services.UtilityService import SignatureData, UtilityService from src.env import api_gateway from src.utils.logger import Logger +from src.api.services.UtilityService import SignatureData, UtilityService +from src.api.constants.signature_sources import SIGNATURE_SOURCES class GateWayAuth(APIKeyHeader): @@ -43,7 +43,7 @@ def authenticate(self, request: HttpRequest, key: str | None) -> str | None: } ) raise AuthenticationError(message=message) - + signature_data: SignatureData = { "signature": api_signature, "timestamp": api_timestamp, @@ -52,7 +52,9 @@ def authenticate(self, request: HttpRequest, key: str | None) -> str | None: "title": SIGNATURE_SOURCES["gateway"], } - UtilityService.verify_signature(logger=self.logger, signature_data=signature_data) + UtilityService.verify_signature( + logger=self.logger, signature_data=signature_data + ) self.logger.debug( { diff --git a/src/api/services/UserService.py b/src/api/services/UserService.py index b743e74..01f01e9 100644 --- a/src/api/services/UserService.py +++ b/src/api/services/UserService.py @@ -2,16 +2,16 @@ from faststream.rabbit import RabbitRouter -from src.api.constants.activity_types import ACTIVITY_TYPES -from src.api.constants.messages import DYNAMIC_MESSAGES, MESSAGES +from src.utils.svcs import Service +from src.utils.logger import Logger +from src.api.models.postgres import User from src.api.constants.queues import QUEUE_NAMES +from src.api.constants.messages import MESSAGES, DYNAMIC_MESSAGES +from src.api.typing.UserSuccess import UserSuccess +from src.api.constants.activity_types import ACTIVITY_TYPES +from src.api.repositories.UserRepository import UserRepository from src.api.models.payload.requests.CreateUserRequest import CreateUserRequest from src.api.models.payload.requests.UpdateUserRequest import UpdateUserRequest -from src.api.models.postgres import User -from src.api.repositories.UserRepository import UserRepository -from src.api.typing.UserSuccess import UserSuccess -from src.utils.logger import Logger -from src.utils.svcs import Service from .UtilityService import UtilityService diff --git a/src/api/services/UtilityService.py b/src/api/services/UtilityService.py index a524601..695387c 100644 --- a/src/api/services/UtilityService.py +++ b/src/api/services/UtilityService.py @@ -1,25 +1,26 @@ -import hashlib import hmac -from datetime import datetime, timedelta -from typing import TypedDict +import hashlib from uuid import uuid4 +from typing import TypedDict +from datetime import datetime, timedelta -import bcrypt import jwt -from django.utils import timezone +import bcrypt from faker import Faker +from django.utils import timezone from ninja.errors import AuthenticationError -from src.api.enums.CharacterCasing import CharacterCasing -from src.api.models.postgres import User -from src.api.typing.ExpireUUID import ExpireUUID from src.env import jwt_config -from src.utils.logger import Logger from src.utils.svcs import Service +from src.utils.logger import Logger +from src.api.models.postgres import User +from src.api.typing.ExpireUUID import ExpireUUID +from src.api.enums.CharacterCasing import CharacterCasing DEFAULT_CHARACTER_LENGTH = 12 fake = Faker() + class SignatureData(TypedDict): title: str signature: str @@ -27,6 +28,7 @@ class SignatureData(TypedDict): key: str ttl: int | float + @Service() class UtilityService: @staticmethod @@ -91,7 +93,7 @@ def generate_uuid() -> ExpireUUID: lifespan = timedelta(hours=24) expires_at = current_time + lifespan return {"uuid": uuid4(), "expires_at": expires_at} - + @staticmethod def generate_signature(key: str, timestamp: str) -> str: signature = hmac.new( @@ -99,17 +101,14 @@ def generate_signature(key: str, timestamp: str) -> str: ).hexdigest() return signature - @staticmethod def verify_signature(signature_data: SignatureData, logger: Logger) -> bool: - signature = signature_data["signature"] timestamp = signature_data["timestamp"] key = signature_data["key"] ttl = signature_data["ttl"] title = signature_data["title"] - valid_signature = UtilityService.generate_signature(key, timestamp) is_valid = hmac.compare_digest(valid_signature, signature) @@ -124,9 +123,9 @@ def verify_signature(signature_data: SignatureData, logger: Logger) -> bool: ) raise AuthenticationError(message=message) - initial_time = datetime.fromtimestamp(float(timestamp)/ 1000) + initial_time = datetime.fromtimestamp(float(timestamp) / 1000) valid_window = initial_time + timedelta(minutes=ttl) - + if valid_window < datetime.now(): message = "Signature expired!" logger.error( @@ -140,8 +139,7 @@ def verify_signature(signature_data: SignatureData, logger: Logger) -> bool: return True - @staticmethod def get_timestamp() -> str: current_time = datetime.now().timestamp() * 1000 - return str(current_time) \ No newline at end of file + return str(current_time) diff --git a/src/config/asgi.py b/src/config/asgi.py index 8ef43eb..1ff8214 100644 --- a/src/config/asgi.py +++ b/src/config/asgi.py @@ -11,8 +11,8 @@ from django.core.asgi import get_asgi_application from faststream.rabbit import RabbitBroker -from starlette.applications import Starlette from starlette.routing import Mount +from starlette.applications import Starlette from src.env import rabbitmq_config @@ -21,9 +21,12 @@ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "src.config.settings") -def setup_broker_middlewares(): - from src.api.middlewares.BrokerMiddleware import (PublishMiddleware, - SubscribeMiddleware) + +def setup_broker_middlewares() -> None: + from src.api.middlewares.BrokerMiddleware import ( + PublishMiddleware, + SubscribeMiddleware, + ) broker.add_middleware(SubscribeMiddleware) broker.add_middleware(PublishMiddleware) @@ -36,5 +39,4 @@ def setup_broker_middlewares(): ) -from src.api.services.external import \ - RabbitMQRoutes as RabbitMQRoutes # noqa: E402 +from src.api.services.external import RabbitMQRoutes as RabbitMQRoutes # noqa: E402 diff --git a/src/config/settings.py b/src/config/settings.py index 6fe520b..013ed58 100644 --- a/src/config/settings.py +++ b/src/config/settings.py @@ -2,11 +2,11 @@ from .apps import INSTALLED_APPS as INSTALLED_APPS from .caches import CACHES as CACHES -from .databases import DATABASE_ROUTERS as DATABASE_ROUTERS -from .databases import DATABASES as DATABASES from .logger import LOGGING as LOGGING -from .middleware import MIDDLEWARE as MIDDLEWARE +from .databases import DATABASES as DATABASES +from .databases import DATABASE_ROUTERS as DATABASE_ROUTERS from .templates import TEMPLATES as TEMPLATES +from .middleware import MIDDLEWARE as MIDDLEWARE SECRET_KEY = app["secret_key"] DEBUG = app["debug"] @@ -20,10 +20,10 @@ MEDIA_URL = "media/" -LANGUAGE_CODE = 'en-us' +LANGUAGE_CODE = "en-us" -TIME_ZONE = 'UTC' +TIME_ZONE = "UTC" USE_I18N = True -USE_TZ = True \ No newline at end of file +USE_TZ = True diff --git a/src/env.py b/src/env.py index 4c9495b..a41c43c 100644 --- a/src/env.py +++ b/src/env.py @@ -1,7 +1,7 @@ from typing import TypedDict -from src import __description__, __display_name__, __name__, __version__ -from src.utils.env import get_env_float, get_env_int, get_env_list, get_env_str +from src import __name__, __version__, __description__, __display_name__ +from src.utils.env import get_env_int, get_env_str, get_env_list, get_env_float class Env: @@ -58,10 +58,12 @@ class Gateway(TypedDict): key: str ttl: int + class Queue(TypedDict): key: str ttl: float + env = Env() app: App = { @@ -134,6 +136,6 @@ class Queue(TypedDict): "jwt_config", "log", "otp", - "rabbitmq_config", "queue", + "rabbitmq_config", ] diff --git a/src/utils/env/__init__.py b/src/utils/env/__init__.py index 45de2e4..9f765cb 100644 --- a/src/utils/env/__init__.py +++ b/src/utils/env/__init__.py @@ -1,8 +1,8 @@ -from .env import get_env_float, get_env_int, get_env_list, get_env_str +from .env import get_env_int, get_env_str, get_env_list, get_env_float __all__ = [ + "get_env_float", "get_env_int", "get_env_list", "get_env_str", - "get_env_float", ] diff --git a/src/utils/env/env.py b/src/utils/env/env.py index 5a7fae8..d337aee 100644 --- a/src/utils/env/env.py +++ b/src/utils/env/env.py @@ -1,7 +1,7 @@ -from functools import cache from pathlib import Path +from functools import cache -from decouple import AutoConfig, Config, RepositoryEnv +from decouple import Config, AutoConfig, RepositoryEnv from decouple import config as decouple_config BASE_DIR = Path(__file__).resolve().parent.parent.parent @@ -25,6 +25,7 @@ def get_env_str(name: str, default: str | None = None) -> str: def get_env_int(name: str, default: str | None = None) -> int: return int(get_env_variable(name, default=default, cast=int)) + def get_env_float(name: str, default: str | None = None) -> float: return float(get_env_variable(name, default=default, cast=float))