Skip to content

Commit adcc92b

Browse files
committed
mearge: feat/user-registration
1 parent cef0832 commit adcc92b

File tree

9 files changed

+206
-39
lines changed

9 files changed

+206
-39
lines changed

.env.example

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,5 +28,14 @@ JWT_ISSUER=xxx
2828
# time in minutes
2929
OTP_LIFETIME=10
3030

31+
GATEWAY_PUBLIC_KEY='XXXXXXXXXXXXXXXXXXX'
32+
GATEWAY_KEY_TTL=2
33+
34+
GATEWAY_PUBLIC_KEY='XXXXXXXXXXXXXXXXXXX'
35+
GATEWAY_KEY_TTL=2
36+
3137
# amqp://username:password@host[:port]/
32-
RABBITMQ_URL=amqp://guest:guest@localhost:5672/
38+
RABBITMQ_URL=amqp://guest:guest@localhost:5672/
39+
40+
QUEUE_SECRECT_KEY='XXXXXXXXXXXXXXXXX'
41+
QUEUE_SECRECT_KEY_TTL=0.5

src/api/routes/__init__.py

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,69 @@
1-
from ninja import NinjaAPI
21
from django.http import HttpRequest
2+
from ninja import NinjaAPI
3+
from ninja.openapi.schema import OpenAPISchema
34

5+
from src.api.middlewares.GateWayMiddleware import (add_global_headers,
6+
authentication)
47
from src.env import app
58

69
api: NinjaAPI = NinjaAPI(
710
version=app["version"],
811
title=app["display_name"],
912
description=app["description"],
13+
auth=authentication,
1014
)
1115

16+
17+
original_get_openapi_schema = api.get_openapi_schema
18+
19+
20+
def custom_openapi_schema(path_params: dict | None = None) -> OpenAPISchema:
21+
schema = original_get_openapi_schema()
22+
23+
schema["components"]["securitySchemes"] = {
24+
"Gateway Key": {
25+
"type": "apiKey",
26+
"in": "header",
27+
"name": "X-API-GATEWAY-KEY",
28+
},
29+
"API Timestamp": {
30+
"type": "apiKey",
31+
"in": "header",
32+
"name": "X-API-GATEWAY-TIMESTAMP",
33+
},
34+
"API Signature": {
35+
"type": "apiKey",
36+
"in": "header",
37+
"name": "X-API-GATEWAY-SIGNATURE",
38+
},
39+
# "User ID": {
40+
# "type": "apiKey",
41+
# "in": "header",
42+
# "name": "X-USER-ID",
43+
# },
44+
# "User Email": {
45+
# "type": "apiKey",
46+
# "in": "header",
47+
# "name": "X-USER-EMAIL",
48+
# },
49+
}
50+
51+
schema["security"] = [
52+
{
53+
"Gateway Key": [],
54+
"API Timestamp": [],
55+
"API Signature": [],
56+
# "User ID": [],
57+
# "User Email": [],
58+
}
59+
]
60+
61+
schema = add_global_headers(schema)
62+
return schema
63+
64+
65+
setattr(api, "get_openapi_schema", custom_openapi_schema)
66+
1267
from src.api.utils import error_handlers # noqa: E402, F401
1368

1469

src/api/services/AuthService.py

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,24 @@
11
from typing import Annotated
22

3-
from src.utils.svcs import Service
4-
from src.config.asgi import broker
5-
from src.utils.logger import Logger
6-
from src.api.typing.JWT import JWTSuccess
7-
from src.api.typing.UserExists import UserExists
8-
from src.api.constants.messages import MESSAGES, DYNAMIC_MESSAGES
9-
from src.api.typing.UserSuccess import UserSuccess
103
from src.api.constants.activity_types import ACTIVITY_TYPES
4+
from src.api.constants.messages import DYNAMIC_MESSAGES, MESSAGES
5+
from src.api.constants.queues import QUEUE_NAMES
6+
from src.api.models.payload.requests.AuthenticateUserOtp import \
7+
AuthenticateUserOtp
8+
from src.api.models.payload.requests.AuthenticateUserRequest import \
9+
AuthenticateUserRequest
10+
from src.api.models.payload.requests.ChangeUserPasswordRequest import \
11+
ChangeUserPasswordRequest
12+
from src.api.models.payload.requests.CreateUserRequest import CreateUserRequest
1113
from src.api.models.payload.requests.JWT import JWT
12-
from src.api.repositories.UserRepository import UserRepository
1314
from src.api.models.payload.requests.ResendUserOtp import ResendUserOtp
14-
from src.api.models.payload.requests.CreateUserRequest import CreateUserRequest
15-
from src.api.models.payload.requests.AuthenticateUserOtp import AuthenticateUserOtp
16-
from src.api.models.payload.requests.AuthenticateUserRequest import (
17-
AuthenticateUserRequest,
18-
)
19-
from src.api.models.payload.requests.ChangeUserPasswordRequest import (
20-
ChangeUserPasswordRequest,
21-
)
15+
from src.api.repositories.UserRepository import UserRepository
16+
from src.api.typing.JWT import JWTSuccess
17+
from src.api.typing.UserExists import UserExists
18+
from src.api.typing.UserSuccess import UserSuccess
19+
from src.config.asgi import broker
20+
from src.utils.logger import Logger
21+
from src.utils.svcs import Service
2222

2323
from .OtpService import OtpService
2424
from .UtilityService import UtilityService
@@ -62,7 +62,7 @@ async def register(self, req: CreateUserRequest) -> UserExists:
6262
created_user = await UserRepository.add(req)
6363

6464
user_data = {"id": created_user.id, "email": created_user.email}
65-
queue = "create-user"
65+
queue = QUEUE_NAMES["USER_REGISTRATION"]
6666

6767
await broker.publish(message=user_data, queue=queue, persist=True)
6868

@@ -126,8 +126,9 @@ async def validate_email(self, req: AuthenticateUserOtp) -> bool:
126126
await UserRepository.update_by_user(
127127
user, {"is_active": True, "is_enabled": True, "is_validated": True}
128128
)
129+
129130
user_data = {"id": user.id, "email": user.email}
130-
queue = "validate-user"
131+
queue = QUEUE_NAMES["EMAIL_VALIDATION"]
131132
await broker.publish(message=user_data, queue=queue, persist=True)
132133

133134
return True

src/api/services/UtilityService.py

Lines changed: 70 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,32 @@
1+
import hashlib
2+
import hmac
3+
from datetime import datetime, timedelta
4+
from typing import TypedDict
15
from uuid import uuid4
2-
from datetime import timedelta
36

4-
import jwt
57
import bcrypt
6-
from faker import Faker
8+
import jwt
79
from django.utils import timezone
10+
from faker import Faker
11+
from ninja.errors import AuthenticationError
812

9-
from src.env import jwt_config
10-
from src.utils.svcs import Service
11-
from src.api.typing.JWT import JWTData
13+
from src.api.enums.CharacterCasing import CharacterCasing
1214
from src.api.models.postgres import User
1315
from src.api.typing.ExpireUUID import ExpireUUID
14-
from src.api.enums.CharacterCasing import CharacterCasing
16+
from src.api.typing.JWT import JWTData
17+
from src.env import jwt_config
18+
from src.utils.logger import Logger
19+
from src.utils.svcs import Service
1520

1621
DEFAULT_CHARACTER_LENGTH = 12
1722
fake = Faker()
1823

24+
class SignatureData(TypedDict):
25+
title: str
26+
signature: str
27+
timestamp: str
28+
key: str
29+
ttl: int | float
1930

2031
@Service()
2132
class UtilityService:
@@ -95,3 +106,55 @@ def generate_uuid() -> ExpireUUID:
95106
lifespan = timedelta(hours=24)
96107
expires_at = current_time + lifespan
97108
return {"uuid": uuid4(), "expires_at": expires_at}
109+
110+
@staticmethod
111+
def generate_signature(key: str, timestamp: str) -> str:
112+
signature = hmac.new(
113+
key=key.encode(), msg=timestamp.encode(), digestmod=hashlib.sha256
114+
).hexdigest()
115+
return signature
116+
117+
118+
@staticmethod
119+
def verify_signature(signature_data: SignatureData, logger: Logger) -> bool:
120+
121+
signature = signature_data["signature"]
122+
timestamp = signature_data["timestamp"]
123+
key = signature_data["key"]
124+
ttl = signature_data["ttl"]
125+
title = signature_data["title"]
126+
127+
128+
valid_signature = UtilityService.generate_signature(key, timestamp)
129+
is_valid = hmac.compare_digest(valid_signature, signature)
130+
131+
if not is_valid:
132+
message = "Invalid signature!"
133+
logger.error(
134+
{
135+
"activity_type": f"Authenticate {title} Request",
136+
"message": message,
137+
"metadata": {"signature": signature},
138+
}
139+
)
140+
raise AuthenticationError(message=message)
141+
142+
initial_time = datetime.fromtimestamp(float(timestamp) / 1000)
143+
valid_window = initial_time + timedelta(minutes=ttl)
144+
if valid_window < datetime.now():
145+
message = "Signature expired!"
146+
logger.error(
147+
{
148+
"activity_type": f"Authenticate {title} Request",
149+
"message": message,
150+
"metadata": {"timestamp": timestamp},
151+
}
152+
)
153+
raise AuthenticationError(message=message)
154+
155+
return True
156+
157+
@staticmethod
158+
def get_timestamp() -> str:
159+
current_time = datetime.now().timestamp() * 1000
160+
return str(current_time)

src/config/asgi.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@
1111

1212
from django.core.asgi import get_asgi_application
1313
from faststream.rabbit import RabbitBroker
14-
from starlette.routing import Mount
1514
from starlette.applications import Starlette
15+
from starlette.routing import Mount
1616

1717
from src.env import rabbitmq_config
1818

@@ -21,11 +21,21 @@
2121

2222
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "src.config.settings")
2323

24+
25+
def setup_broker_middlewares():
26+
from src.api.middlewares.BrokerMiddleware import (PublishMiddleware,
27+
SubscribeMiddleware)
28+
29+
broker.add_middleware(SubscribeMiddleware)
30+
broker.add_middleware(PublishMiddleware)
31+
32+
2433
application = Starlette(
2534
routes=[Mount("/", get_asgi_application())], # type: ignore
26-
on_startup=[broker.start],
35+
on_startup=[setup_broker_middlewares, broker.start],
2736
on_shutdown=[broker.close],
2837
)
2938

3039

31-
from src.api.services.external import RabbitMQRoutes as RabbitMQRoutes # noqa: E402
40+
from src.api.services.external import \
41+
RabbitMQRoutes as RabbitMQRoutes # noqa: E402

src/config/settings.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22

33
from .apps import INSTALLED_APPS as INSTALLED_APPS
44
from .caches import CACHES as CACHES
5-
from .logger import LOGGING as LOGGING
6-
from .databases import DATABASES as DATABASES
75
from .databases import DATABASE_ROUTERS as DATABASE_ROUTERS
8-
from .templates import TEMPLATES as TEMPLATES
6+
from .databases import DATABASES as DATABASES
7+
from .logger import LOGGING as LOGGING
98
from .middleware import MIDDLEWARE as MIDDLEWARE
9+
from .templates import TEMPLATES as TEMPLATES
1010

1111
SECRET_KEY = app["secret_key"]
1212
DEBUG = app["debug"]
@@ -18,3 +18,11 @@
1818

1919
STATIC_URL = "static/"
2020
MEDIA_URL = "media/"
21+
22+
LANGUAGE_CODE = 'en-us'
23+
24+
TIME_ZONE = 'UTC'
25+
26+
USE_I18N = True
27+
28+
USE_TZ = True

src/env.py

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from typing import TypedDict
22

3-
from src import __name__, __version__, __description__, __display_name__
4-
from src.utils.env import get_env_int, get_env_str, get_env_list
3+
from src import __description__, __display_name__, __name__, __version__
4+
from src.utils.env import get_env_float, get_env_int, get_env_list, get_env_str
55

66

77
class Env:
@@ -54,6 +54,14 @@ class RabbitMQ(TypedDict):
5454
url: str
5555

5656

57+
class Gateway(TypedDict):
58+
key: str
59+
ttl: int
60+
61+
class Queue(TypedDict):
62+
key: str
63+
ttl: float
64+
5765
env = Env()
5866

5967
app: App = {
@@ -102,8 +110,18 @@ class RabbitMQ(TypedDict):
102110
"issuer": get_env_str("JWT_ISSUER"),
103111
}
104112

113+
api_gateway: Gateway = {
114+
"key": get_env_str("GATEWAY_PUBLIC_KEY"),
115+
"ttl": get_env_int("GATEWAY_KEY_TTL"),
116+
}
117+
118+
queue: Queue = {
119+
"key": get_env_str("QUEUE_SECRECT_KEY"),
120+
"ttl": get_env_float("QUEUE_SECRECT_KEY_TTL"),
121+
}
122+
105123
otp: OTP = {"lifetime": get_env_int("OTP_LIFETIME")}
106124

107125
rabbitmq_config: RabbitMQ = {"url": get_env_str("RABBITMQ_URL")}
108126

109-
__all__ = ["app", "cache", "db", "env", "jwt_config", "log", "otp", "rabbitmq_config"]
127+
__all__ = ["app", "cache", "db", "env", "jwt_config", "log", "otp", "rabbitmq_config", "queue", "api_gateway"]

src/utils/env/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
from .env import get_env_int, get_env_str, get_env_list
1+
from .env import get_env_float, get_env_int, get_env_list, get_env_str
22

33
__all__ = [
44
"get_env_int",
55
"get_env_list",
66
"get_env_str",
7+
"get_env_float",
78
]

src/utils/env/env.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
from pathlib import Path
21
from functools import cache
2+
from pathlib import Path
33

4-
from decouple import Config, AutoConfig, RepositoryEnv
4+
from decouple import AutoConfig, Config, RepositoryEnv
55
from decouple import config as decouple_config
66

77
BASE_DIR = Path(__file__).resolve().parent.parent.parent
@@ -25,6 +25,8 @@ def get_env_str(name: str, default: str | None = None) -> str:
2525
def get_env_int(name: str, default: str | None = None) -> int:
2626
return int(get_env_variable(name, default=default, cast=int))
2727

28+
def get_env_float(name: str, default: str | None = None) -> float:
29+
return float(get_env_variable(name, default=default, cast=float))
2830

2931
def get_env_list(name: str, sep: str = ",", default: str | None = None) -> list[str]:
3032
return list(

0 commit comments

Comments
 (0)