diff --git a/api-server/app/auth/get_token_claims.py b/api-server/app/auth/get_token_claims.py new file mode 100644 index 00000000..ef9a1858 --- /dev/null +++ b/api-server/app/auth/get_token_claims.py @@ -0,0 +1,31 @@ +import jwt +import os +import time + +from app.singletons.logs_manager import LogsManager + +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" + + +async def get_token_claims(token: str, x_exosphere_request_id: str): + try: + claims = TokenClaims(**jwt.decode(token, JWT_SECRET_KEY, algorithms=[JWT_ALGORITHM])) + logger.info("Token claims decoded", x_exosphere_request_id=x_exosphere_request_id, user_id=claims.user_id) + + if claims.exp < time.time(): + logger.error("Token expired", x_exosphere_request_id=x_exosphere_request_id, user_id=claims.user_id) + return None + + return claims + + except Exception as e: + logger.error("Error decoding token", error=e, x_exosphere_request_id=x_exosphere_request_id) + return None + \ No newline at end of file diff --git a/api-server/app/auth/middlewares/get_token_claims.py b/api-server/app/auth/middlewares/get_token_claims.py deleted file mode 100644 index efef3c32..00000000 --- a/api-server/app/auth/middlewares/get_token_claims.py +++ /dev/null @@ -1,43 +0,0 @@ -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/routes.py b/api-server/app/auth/routes.py new file mode 100644 index 00000000..11f5b5bb --- /dev/null +++ b/api-server/app/auth/routes.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="/v0/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 ff450a47..7bff6228 100644 --- a/api-server/app/main.py +++ b/api-server/app/main.py @@ -17,7 +17,6 @@ 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 @@ -25,8 +24,9 @@ # injecting routers from .user.routes import router as user_router -from .auth.router import router as auth_router - +from .auth.routes import router as auth_router +from .project.routes import router as project_router + load_dotenv() @@ -54,8 +54,6 @@ async def lifespan(app: FastAPI): oauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth/token") # this middleware should be the first one -app.add_middleware(GetTokenClaimsMiddleware) - app.add_middleware(RequestIdMiddleware) app.add_middleware(UnhandledExceptionsMiddleware) @@ -69,3 +67,4 @@ def health() -> dict: # injecting routers app.include_router(user_router) app.include_router(auth_router) +app.include_router(project_router) diff --git a/api-server/app/project/controllers/create_project.py b/api-server/app/project/controllers/create_project.py new file mode 100644 index 00000000..c7b339d6 --- /dev/null +++ b/api-server/app/project/controllers/create_project.py @@ -0,0 +1,36 @@ +from beanie import Link +from bson import ObjectId + +from ..models.create_project_request import CreateProjectRequest +from ..models.create_project_response import CreateProjectResponse +from ..models.project_database_model import Project + +from app.singletons.logs_manager import LogsManager +from app.auth.models.token_claims import TokenClaims +from app.user.models.user_database_model import User + +logger = LogsManager().get_logger() + + +async def create_project(request: CreateProjectRequest, claims: TokenClaims, x_exosphere_request_id: str) -> CreateProjectResponse: + try: + logger.info("Creating new project", x_exosphere_request_id=x_exosphere_request_id, user_id=claims.user_id) + project = Project( + name=request.name, + super_admin=Link(ObjectId(claims.user_id), User) + ) + await project.save() + print(project) + return CreateProjectResponse( + id=str(project.id), + name=project.name, + status=project.status, + billing_account=project.billing_account, + super_admin=claims.user_id, + created_at=project.created_at, + updated_at=project.updated_at + ) + + except Exception as e: + logger.error("Error creating new project", x_exosphere_request_id=x_exosphere_request_id, error=e) + raise e \ No newline at end of file diff --git a/api-server/app/project/models/billing_account.py b/api-server/app/project/models/billing_account.py index 7dbfff92..0b94ca93 100644 --- a/api-server/app/project/models/billing_account.py +++ b/api-server/app/project/models/billing_account.py @@ -3,6 +3,7 @@ class TaxInformationTypeEnum(str, Enum): + NOT_DEFINED = "NOT_DEFINED" GST = "GST" VAT = "VAT" EIN = "EIN" @@ -15,7 +16,7 @@ class BillingAccount(BaseModel): billing_address: str = Field(default="", description="Billing address of the company") - tax_number_type: TaxInformationTypeEnum = Field(default="", description="Type of tax information") + tax_number_type: TaxInformationTypeEnum = Field(default=TaxInformationTypeEnum.NOT_DEFINED, description="Type of tax information") tax_number: str = Field(default="", description="Tax information of the company") diff --git a/api-server/app/project/models/create_project_request.py b/api-server/app/project/models/create_project_request.py new file mode 100644 index 00000000..20a1191d --- /dev/null +++ b/api-server/app/project/models/create_project_request.py @@ -0,0 +1,4 @@ +from pydantic import BaseModel + +class CreateProjectRequest(BaseModel): + name: str diff --git a/api-server/app/project/models/create_project_response.py b/api-server/app/project/models/create_project_response.py new file mode 100644 index 00000000..57fbb254 --- /dev/null +++ b/api-server/app/project/models/create_project_response.py @@ -0,0 +1,14 @@ +from pydantic import BaseModel +from .billing_account import BillingAccount +from .project_status_enum import ProjectStatusEnum +from datetime import datetime + + +class CreateProjectResponse(BaseModel): + id: str + name: str + status: ProjectStatusEnum + billing_account: BillingAccount + super_admin: str + created_at: datetime + updated_at: datetime \ No newline at end of file diff --git a/api-server/app/project/routes.py b/api-server/app/project/routes.py new file mode 100644 index 00000000..480a4964 --- /dev/null +++ b/api-server/app/project/routes.py @@ -0,0 +1,27 @@ +from fastapi import APIRouter, status, Request, Depends +from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials +from typing import Annotated +from starlette.responses import JSONResponse + +from .controllers.create_project import create_project +from .models.create_project_request import CreateProjectRequest +from .models.create_project_response import CreateProjectResponse + +from app.auth.get_token_claims import get_token_claims + +router = APIRouter(prefix="/v0/project", tags=["project"]) + +@router.post( + "/", + response_model=CreateProjectResponse, + status_code=status.HTTP_201_CREATED, + response_description="Project created successfully" +) +async def create_project_route(body: CreateProjectRequest, request: Request, token: Annotated[HTTPAuthorizationCredentials, Depends(HTTPBearer())]): + x_exosphere_request_id = getattr(request.state, "x_exosphere_request_id", None) + claims = await get_token_claims(token.credentials, x_exosphere_request_id) + + if claims is None: + return JSONResponse(status_code=401, content={"message": "Invalid token", "success": False}) + + return await create_project(body, claims, x_exosphere_request_id) \ No newline at end of file diff --git a/api-server/app/user/routes.py b/api-server/app/user/routes.py index 577d8a01..29d860b6 100644 --- a/api-server/app/user/routes.py +++ b/api-server/app/user/routes.py @@ -3,11 +3,11 @@ from .controllers.create_user import create_user from .models.create_user_request import CreateUserRequest from .models.create_user_response import CreateUserResponse - -router = APIRouter(prefix="/user", tags=["user"]) + +router = APIRouter(prefix="/v0/user", tags=["user"]) @router.post( - "/create", + "/", response_model=CreateUserResponse, status_code=status.HTTP_201_CREATED, response_description="User created successfully"