From d4a615f10b69699f8744148b031dd05aaaa83de0 Mon Sep 17 00:00:00 2001 From: NiveditJain Date: Sun, 6 Jul 2025 11:47:59 +0530 Subject: [PATCH 1/6] token creation init --- .../app/auth/controllers/create_token.py | 59 +++++++++++++++++++ api-server/app/auth/models/token_request.py | 5 ++ api-server/app/auth/models/token_response.py | 4 ++ api-server/app/auth/router.py | 17 ++++++ api-server/app/main.py | 6 +- .../unhandled_exceptions_middleware.py | 2 +- .../app/user/controllers/create_user.py | 5 +- .../app/user/models/user_database_model.py | 4 +- api-server/pyproject.toml | 1 + api-server/uv.lock | 11 ++++ 10 files changed, 107 insertions(+), 7 deletions(-) create mode 100644 api-server/app/auth/controllers/create_token.py create mode 100644 api-server/app/auth/models/token_request.py create mode 100644 api-server/app/auth/models/token_response.py create mode 100644 api-server/app/auth/router.py diff --git a/api-server/app/auth/controllers/create_token.py b/api-server/app/auth/controllers/create_token.py new file mode 100644 index 00000000..767d685b --- /dev/null +++ b/api-server/app/auth/controllers/create_token.py @@ -0,0 +1,59 @@ +import os +import jwt +from datetime import datetime, timedelta +from fastapi import HTTPException + +from ..models.token_request import TokenRequest +from ..models.token_response import TokenResponse + +from app.singletons.logs_manager import LogsManager + +from app.user.models.user_database_model import User + +logger = LogsManager().get_logger() + +JWT_SECRET_KEY = os.getenv("JWT_SECRET_KEY") +if not JWT_SECRET_KEY: + raise ValueError("JWT_SECRET_KEY environment variable is not set or is empty.") +JWT_ALGORITHM = "HS256" +JWT_EXPIRES_IN = 3600 # 1 hour + + +async def create_token(request: TokenRequest, x_exosphere_request_id: str) -> TokenResponse: + + try: + logger.info("Finding user", x_exosphere_request_id=x_exosphere_request_id) + + print(request.identifier) + print(request.credential) + user = await User.objects.get(identifier=request.identifier) + print(user) + if not user: + logger.error("User not found", x_exosphere_request_id=x_exosphere_request_id) + raise HTTPException(status_code=404, detail="User not found") + + if not user.verify_credential(request.credential): + logger.error("Invalid credential", x_exosphere_request_id=x_exosphere_request_id) + raise HTTPException(status_code=401, detail="Invalid credential") + + logger.info("User found and credential verified", x_exosphere_request_id=x_exosphere_request_id) + + logger.info("User is a super admin", x_exosphere_request_id=x_exosphere_request_id) + + token_claims = { + "user_id": user.id, + "user_type": user.type, + "verification_status": user.verification_status, + "status": user.status, + "exp": datetime.now() + timedelta(seconds=JWT_EXPIRES_IN) + } + + return TokenResponse( + access_token=jwt.encode(token_claims, JWT_SECRET_KEY, algorithm=JWT_ALGORITHM) + ) + + except Exception as e: + print(e.message) + logger.error("Error creating token", error=e, x_exosphere_request_id=x_exosphere_request_id) + + raise HTTPException(status_code=500, detail=str(e)) \ No newline at end of file diff --git a/api-server/app/auth/models/token_request.py b/api-server/app/auth/models/token_request.py new file mode 100644 index 00000000..bc6deda0 --- /dev/null +++ b/api-server/app/auth/models/token_request.py @@ -0,0 +1,5 @@ +from pydantic import BaseModel, Field + +class TokenRequest(BaseModel): + identifier: str = Field(..., description="Identifier of the user, could be an email, phone, username, etc.") + credential: str = Field(..., description="Credential of the user, could be a password, api secret, etc.") \ No newline at end of file diff --git a/api-server/app/auth/models/token_response.py b/api-server/app/auth/models/token_response.py new file mode 100644 index 00000000..a272f968 --- /dev/null +++ b/api-server/app/auth/models/token_response.py @@ -0,0 +1,4 @@ +from pydantic import BaseModel, Field + +class TokenResponse(BaseModel): + access_token: str = Field(..., description="Access token for the user") \ No newline at end of file diff --git a/api-server/app/auth/router.py b/api-server/app/auth/router.py new file mode 100644 index 00000000..c135c9f1 --- /dev/null +++ b/api-server/app/auth/router.py @@ -0,0 +1,17 @@ +from fastapi import APIRouter, status, Request + +from .controllers.create_token import create_token +from .models.token_request import TokenRequest +from .models.token_response import TokenResponse + +router = APIRouter(prefix="/auth", tags=["auth"]) + +@router.post( + "/token", + response_model=TokenResponse, + status_code=status.HTTP_200_OK, + response_description="Token created successfully" +) +async def create_token_route(body: TokenRequest, request: Request): + x_exosphere_request_id = getattr(request.state, "x_exosphere_request_id", None) + return await create_token(body, x_exosphere_request_id) \ No newline at end of file diff --git a/api-server/app/main.py b/api-server/app/main.py index 2374a828..4c4b575a 100644 --- a/api-server/app/main.py +++ b/api-server/app/main.py @@ -23,6 +23,7 @@ # injecting routers from .user.routes import router as user_router +from .auth.router import router as auth_router load_dotenv() @@ -59,5 +60,6 @@ async def lifespan(app: FastAPI): def health() -> dict: return {"message": "OK"} - -app.include_router(user_router) \ No newline at end of file +# injecting routers +app.include_router(user_router) +app.include_router(auth_router) diff --git a/api-server/app/middlewares/unhandled_exceptions_middleware.py b/api-server/app/middlewares/unhandled_exceptions_middleware.py index afb1f951..3e1878ff 100644 --- a/api-server/app/middlewares/unhandled_exceptions_middleware.py +++ b/api-server/app/middlewares/unhandled_exceptions_middleware.py @@ -25,6 +25,6 @@ async def dispatch(self, request: Request, call_next): status_code=500, content={ "success": False, - "detail": "internal server error, please reach out to support team at support@exosphere.com", + "detail": "internal server error, please reach out to support team at nivedit@exosphere.host", }, ) diff --git a/api-server/app/user/controllers/create_user.py b/api-server/app/user/controllers/create_user.py index dfc3a033..ebd9cfc3 100644 --- a/api-server/app/user/controllers/create_user.py +++ b/api-server/app/user/controllers/create_user.py @@ -1,4 +1,5 @@ from fastapi import HTTPException +from pymongo.errors import DuplicateKeyError from ..models.create_user_request import CreateUserRequest from ..models.create_user_response import CreateUserResponse @@ -33,8 +34,8 @@ async def create_user(request: CreateUserRequest, x_exosphere_request_id: str) - created_at=new_user.created_at, updated_at=new_user.updated_at ) - + except Exception as e: - logger.error("Error creating user error", error=e, x_exosphere_request_id=x_exosphere_request_id) + logger.error("Error creating user error", error=e.with_traceback(), x_exosphere_request_id=x_exosphere_request_id) raise HTTPException(status_code=500, detail="Error creating user") \ No newline at end of file diff --git a/api-server/app/user/models/user_database_model.py b/api-server/app/user/models/user_database_model.py index c8ae38f2..7920797d 100644 --- a/api-server/app/user/models/user_database_model.py +++ b/api-server/app/user/models/user_database_model.py @@ -1,6 +1,6 @@ import bcrypt from datetime import datetime -from beanie import Document, before_event, Replace, Save +from beanie import Document, before_event, Replace, Save, Indexed from pydantic import Field from .user_type_enum import UserTypeEnum from .verification_status_enum import VerificationStatusEnum @@ -13,7 +13,7 @@ class User(Document): type: UserTypeEnum = Field(..., description="Type of the user, human, api, etc. (enum)") - identifier: str = Field(..., description="Unique identifier for the user, could be an email, phone, username, etc.", unique=True) + identifier: Indexed(str, unique=True) = Field(..., description="Unique identifier for the user, could be an email, phone, username, etc.") verification_status: VerificationStatusEnum = Field(default=VerificationStatusEnum.NOT_VERIFIED, description="Verification status of the user, verified, not_verified, blocked, deleted, not_required (enum)") diff --git a/api-server/pyproject.toml b/api-server/pyproject.toml index a45a6a23..0d0f1e90 100644 --- a/api-server/pyproject.toml +++ b/api-server/pyproject.toml @@ -8,6 +8,7 @@ dependencies = [ "bcrypt>=4.3.0", "beanie>=1.30.0", "fastapi>=0.115.14", + "pyjwt>=2.10.1", "python-dotenv>=1.1.1", "structlog>=25.4.0", "uvicorn>=0.35.0", diff --git a/api-server/uv.lock b/api-server/uv.lock index be6ca87a..600606c5 100644 --- a/api-server/uv.lock +++ b/api-server/uv.lock @@ -33,6 +33,7 @@ dependencies = [ { name = "bcrypt" }, { name = "beanie" }, { name = "fastapi" }, + { name = "pyjwt" }, { name = "python-dotenv" }, { name = "structlog" }, { name = "uvicorn" }, @@ -48,6 +49,7 @@ requires-dist = [ { name = "bcrypt", specifier = ">=4.3.0" }, { name = "beanie", specifier = ">=1.30.0" }, { name = "fastapi", specifier = ">=0.115.14" }, + { name = "pyjwt", specifier = ">=2.10.1" }, { name = "python-dotenv", specifier = ">=1.1.1" }, { name = "structlog", specifier = ">=25.4.0" }, { name = "uvicorn", specifier = ">=0.35.0" }, @@ -265,6 +267,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" }, ] +[[package]] +name = "pyjwt" +version = "2.10.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785, upload-time = "2024-11-28T03:43:29.933Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997, upload-time = "2024-11-28T03:43:27.893Z" }, +] + [[package]] name = "pymongo" version = "4.13.2" From 6a9bea676af1849d794f03f5abde260e45983a31 Mon Sep 17 00:00:00 2001 From: NiveditJain Date: Sun, 6 Jul 2025 12:21:28 +0530 Subject: [PATCH 2/6] fixed JSON issues --- api-server/app/auth/controllers/create_token.py | 14 +++++--------- api-server/app/user/controllers/create_user.py | 13 ++++++++----- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/api-server/app/auth/controllers/create_token.py b/api-server/app/auth/controllers/create_token.py index 767d685b..6f96bf88 100644 --- a/api-server/app/auth/controllers/create_token.py +++ b/api-server/app/auth/controllers/create_token.py @@ -1,7 +1,7 @@ import os import jwt from datetime import datetime, timedelta -from fastapi import HTTPException +from starlette.responses import JSONResponse from ..models.token_request import TokenRequest from ..models.token_response import TokenResponse @@ -24,17 +24,15 @@ async def create_token(request: TokenRequest, x_exosphere_request_id: str) -> To try: logger.info("Finding user", x_exosphere_request_id=x_exosphere_request_id) - print(request.identifier) - print(request.credential) user = await User.objects.get(identifier=request.identifier) - print(user) + if not user: logger.error("User not found", x_exosphere_request_id=x_exosphere_request_id) - raise HTTPException(status_code=404, detail="User not found") + return JSONResponse(status_code=404, content={"success": False, "detail": "User not found"}) if not user.verify_credential(request.credential): logger.error("Invalid credential", x_exosphere_request_id=x_exosphere_request_id) - raise HTTPException(status_code=401, detail="Invalid credential") + return JSONResponse(status_code=401, content={"success": False, "detail": "Invalid credential"}) logger.info("User found and credential verified", x_exosphere_request_id=x_exosphere_request_id) @@ -53,7 +51,5 @@ async def create_token(request: TokenRequest, x_exosphere_request_id: str) -> To ) except Exception as e: - print(e.message) logger.error("Error creating token", error=e, x_exosphere_request_id=x_exosphere_request_id) - - raise HTTPException(status_code=500, detail=str(e)) \ No newline at end of file + raise e \ No newline at end of file diff --git a/api-server/app/user/controllers/create_user.py b/api-server/app/user/controllers/create_user.py index ebd9cfc3..7892f47d 100644 --- a/api-server/app/user/controllers/create_user.py +++ b/api-server/app/user/controllers/create_user.py @@ -1,5 +1,6 @@ from fastapi import HTTPException from pymongo.errors import DuplicateKeyError +from starlette.responses import JSONResponse from ..models.create_user_request import CreateUserRequest from ..models.create_user_response import CreateUserResponse @@ -20,10 +21,9 @@ async def create_user(request: CreateUserRequest, x_exosphere_request_id: str) - identifier=request.identifier, credential=request.credential ) - await new_user.save() + await new_user.insert() logger.info("User created", x_exosphere_request_id=x_exosphere_request_id) - return CreateUserResponse( id=str(new_user.id), name=new_user.name, @@ -35,7 +35,10 @@ async def create_user(request: CreateUserRequest, x_exosphere_request_id: str) - updated_at=new_user.updated_at ) + except DuplicateKeyError as e: + logger.error("Error creating user", error=e, x_exosphere_request_id=x_exosphere_request_id) + return JSONResponse(status_code=400, content={"success": False, "detail": "User already exists"}) + except Exception as e: - logger.error("Error creating user error", error=e.with_traceback(), x_exosphere_request_id=x_exosphere_request_id) - - raise HTTPException(status_code=500, detail="Error creating user") \ No newline at end of file + logger.error("Error creating user", error=e, x_exosphere_request_id=x_exosphere_request_id) + raise e \ No newline at end of file From 764cd8aa8594bd567be55328a4f306cb1c2b83ae Mon Sep 17 00:00:00 2001 From: NiveditJain Date: Sun, 6 Jul 2025 12:23:59 +0530 Subject: [PATCH 3/6] fixed lint issues --- api-server/app/user/controllers/create_user.py | 1 - 1 file changed, 1 deletion(-) diff --git a/api-server/app/user/controllers/create_user.py b/api-server/app/user/controllers/create_user.py index 7892f47d..c705cac8 100644 --- a/api-server/app/user/controllers/create_user.py +++ b/api-server/app/user/controllers/create_user.py @@ -1,4 +1,3 @@ -from fastapi import HTTPException from pymongo.errors import DuplicateKeyError from starlette.responses import JSONResponse From 7584d34ca3a67654db88abd9042cdce073f68e1c Mon Sep 17 00:00:00 2001 From: NiveditJain Date: Sun, 6 Jul 2025 12:40:45 +0530 Subject: [PATCH 4/6] token is generating --- api-server/app/auth/controllers/create_token.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/api-server/app/auth/controllers/create_token.py b/api-server/app/auth/controllers/create_token.py index 6f96bf88..14009d80 100644 --- a/api-server/app/auth/controllers/create_token.py +++ b/api-server/app/auth/controllers/create_token.py @@ -24,7 +24,7 @@ async def create_token(request: TokenRequest, x_exosphere_request_id: str) -> To try: logger.info("Finding user", x_exosphere_request_id=x_exosphere_request_id) - user = await User.objects.get(identifier=request.identifier) + user = await User.find_one(User.identifier == request.identifier) if not user: logger.error("User not found", x_exosphere_request_id=x_exosphere_request_id) @@ -39,7 +39,8 @@ async def create_token(request: TokenRequest, x_exosphere_request_id: str) -> To logger.info("User is a super admin", x_exosphere_request_id=x_exosphere_request_id) token_claims = { - "user_id": user.id, + "user_id": str(user.id), + "user_name": user.name, "user_type": user.type, "verification_status": user.verification_status, "status": user.status, From 4b579078365d3030800290e4b475e65869c2cfca Mon Sep 17 00:00:00 2001 From: NiveditJain Date: Sun, 6 Jul 2025 14:43:10 +0530 Subject: [PATCH 5/6] added method to get claims --- .../app/auth/controllers/create_token.py | 19 ++++---- .../app/auth/middlewares/get_token_claims.py | 43 +++++++++++++++++++ api-server/app/auth/models/token_claims.py | 9 ++++ api-server/app/main.py | 6 ++- 4 files changed, 67 insertions(+), 10 deletions(-) create mode 100644 api-server/app/auth/middlewares/get_token_claims.py create mode 100644 api-server/app/auth/models/token_claims.py diff --git a/api-server/app/auth/controllers/create_token.py b/api-server/app/auth/controllers/create_token.py index 14009d80..bebd151b 100644 --- a/api-server/app/auth/controllers/create_token.py +++ b/api-server/app/auth/controllers/create_token.py @@ -5,6 +5,7 @@ from ..models.token_request import TokenRequest from ..models.token_response import TokenResponse +from ..models.token_claims import TokenClaims from app.singletons.logs_manager import LogsManager @@ -38,17 +39,17 @@ async def create_token(request: TokenRequest, x_exosphere_request_id: str) -> To logger.info("User is a super admin", x_exosphere_request_id=x_exosphere_request_id) - token_claims = { - "user_id": str(user.id), - "user_name": user.name, - "user_type": user.type, - "verification_status": user.verification_status, - "status": user.status, - "exp": datetime.now() + timedelta(seconds=JWT_EXPIRES_IN) - } + token_claims = TokenClaims( + user_id=str(user.id), + user_name=user.name, + user_type=user.type, + verification_status=user.verification_status, + status=user.status, + exp=int((datetime.now() + timedelta(seconds=JWT_EXPIRES_IN)).timestamp()) + ) return TokenResponse( - access_token=jwt.encode(token_claims, JWT_SECRET_KEY, algorithm=JWT_ALGORITHM) + access_token=jwt.encode(token_claims.model_dump(), JWT_SECRET_KEY, algorithm=JWT_ALGORITHM) ) except Exception as e: diff --git a/api-server/app/auth/middlewares/get_token_claims.py b/api-server/app/auth/middlewares/get_token_claims.py new file mode 100644 index 00000000..efef3c32 --- /dev/null +++ b/api-server/app/auth/middlewares/get_token_claims.py @@ -0,0 +1,43 @@ +import jwt +import os + +from starlette.middleware.base import BaseHTTPMiddleware +from app.singletons.logs_manager import LogsManager +from starlette.requests import Request +from starlette.responses import JSONResponse + +from ..models.token_claims import TokenClaims + +logger = LogsManager().get_logger() + +JWT_SECRET_KEY = os.getenv("JWT_SECRET_KEY") +if not JWT_SECRET_KEY: + raise ValueError("JWT_SECRET_KEY environment variable is not set or is empty.") +JWT_ALGORITHM = "HS256" + + +class GetTokenClaimsMiddleware(BaseHTTPMiddleware): + async def dispatch(self, request: Request, call_next): + + token = request.headers.get("Authorization") + + if token: + if not token.startswith("Bearer "): + logger.error("Invalid token format", x_exosphere_request_id=getattr(request.state, 'x_exosphere_request_id', None)) + return JSONResponse(status_code=401, content={"message": "Invalid token format", "success": False}) + + try: + token_claims = jwt.decode(token.split(" ")[1], JWT_SECRET_KEY, algorithms=[JWT_ALGORITHM]) + request.state.token_claims = TokenClaims(**token_claims) + logger.info("Token claims decoded", x_exosphere_request_id=getattr(request.state, 'x_exosphere_request_id', None), user_id=request.state.token_claims.user_id) + + except Exception as e: + logger.error("Error decoding token", error=e, x_exosphere_request_id=getattr(request.state, 'x_exosphere_request_id', None)) + + return JSONResponse(status_code=401, content={"message": "Invalid token", "success": False}) + + else: + logger.error("No token provided", x_exosphere_request_id=getattr(request.state, 'x_exosphere_request_id', None)) + request.state.token_claims = None + + return await call_next(request) \ No newline at end of file diff --git a/api-server/app/auth/models/token_claims.py b/api-server/app/auth/models/token_claims.py new file mode 100644 index 00000000..3283ea65 --- /dev/null +++ b/api-server/app/auth/models/token_claims.py @@ -0,0 +1,9 @@ +from pydantic import BaseModel + +class TokenClaims(BaseModel): + user_id: str + user_name: str + user_type: str + verification_status: str + status: str + exp: int \ No newline at end of file diff --git a/api-server/app/main.py b/api-server/app/main.py index 4c4b575a..d8b4b1d1 100644 --- a/api-server/app/main.py +++ b/api-server/app/main.py @@ -16,6 +16,7 @@ UnhandledExceptionsMiddleware, ) from .middlewares.request_id_middleware import RequestIdMiddleware +from .auth.middlewares.get_token_claims import GetTokenClaimsMiddleware # injecting databases from .user.models.user_database_model import User @@ -49,13 +50,16 @@ async def lifespan(app: FastAPI): app = FastAPI(lifespan=lifespan) + # this middleware should be the first one +app.add_middleware(GetTokenClaimsMiddleware) + app.add_middleware(RequestIdMiddleware) -# this middleware should be the last one app.add_middleware(UnhandledExceptionsMiddleware) + @app.get("/health-check") def health() -> dict: return {"message": "OK"} From 924eafceac786fa2e516627cb6ffc1235b47ff2f Mon Sep 17 00:00:00 2001 From: NiveditJain Date: Sun, 6 Jul 2025 14:59:08 +0530 Subject: [PATCH 6/6] added api-server --- api-server/app/main.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/api-server/app/main.py b/api-server/app/main.py index d8b4b1d1..ff450a47 100644 --- a/api-server/app/main.py +++ b/api-server/app/main.py @@ -7,6 +7,7 @@ from contextlib import asynccontextmanager from dotenv import load_dotenv from motor.motor_asyncio import AsyncIOMotorClient +from fastapi.security import OAuth2PasswordBearer # injecting singletons from .singletons.logs_manager import LogsManager @@ -50,6 +51,7 @@ async def lifespan(app: FastAPI): app = FastAPI(lifespan=lifespan) +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth/token") # this middleware should be the first one app.add_middleware(GetTokenClaimsMiddleware)