From f82cae96e37aa4edf47e49516c4524f67616d9d0 Mon Sep 17 00:00:00 2001 From: NiveditJain Date: Mon, 7 Jul 2025 15:26:21 +0530 Subject: [PATCH 01/13] added dockerfile --- api-server/.dockerignore | 26 ++++++++++++++++++++++++++ api-server/Dockerfile | 14 ++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 api-server/.dockerignore create mode 100644 api-server/Dockerfile diff --git a/api-server/.dockerignore b/api-server/.dockerignore new file mode 100644 index 00000000..ba4cefd6 --- /dev/null +++ b/api-server/.dockerignore @@ -0,0 +1,26 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*.pyo +*.pyd +*.so + +# C extensions +*.egg +*.egg-info +.eggs + +# Virtual environments +.venv/ + +# Git, CI and editor config +.git/ +.github/ +.vscode/ + +# Logs and temp files +*.log + +# Other +.env +Dockerfile \ No newline at end of file diff --git a/api-server/Dockerfile b/api-server/Dockerfile new file mode 100644 index 00000000..0f9cb814 --- /dev/null +++ b/api-server/Dockerfile @@ -0,0 +1,14 @@ +FROM python:3.12-slim-bookworm +COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ + +WORKDIR /api-server + +COPY pyproject.toml uv.lock ./ + +RUN uv sync --locked + +COPY . . + +EXPOSE 8000 + +CMD ["uv", "run", "run.py", "--mode", "production", "--workers", "4"] \ No newline at end of file From 2ebb09dd066299b3f787f63f3c20e15fed0dc03f Mon Sep 17 00:00:00 2001 From: NiveditJain Date: Mon, 7 Jul 2025 15:36:51 +0530 Subject: [PATCH 02/13] added docker-compose file --- api-server/docker-compose.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 api-server/docker-compose.yml diff --git a/api-server/docker-compose.yml b/api-server/docker-compose.yml new file mode 100644 index 00000000..049711f7 --- /dev/null +++ b/api-server/docker-compose.yml @@ -0,0 +1,12 @@ +version: '3.8' + +services: + api-server: + build: + context: . + dockerfile: Dockerfile + ports: + - "8000:8000" + env_file: + - .env + command: ["uv", "run", "run.py", "--mode", "production", "--workers", "4"] From bf863f9f45f286dc5c51139b8e420d63557e468f Mon Sep 17 00:00:00 2001 From: NiveditJain Date: Wed, 9 Jul 2025 19:57:18 +0530 Subject: [PATCH 03/13] setting up pipeline to push to ghcr --- .github/workflows/deploy-api-server.yml | 50 +++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 .github/workflows/deploy-api-server.yml diff --git a/.github/workflows/deploy-api-server.yml b/.github/workflows/deploy-api-server.yml new file mode 100644 index 00000000..873ca3b9 --- /dev/null +++ b/.github/workflows/deploy-api-server.yml @@ -0,0 +1,50 @@ +name: Publish API server image to GHCR and deploys to Kubernetes Cluster + +on: + push: + branches: [main] + release: + types: [published] + workflow_dispatch: + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository_owner }}/exosphere-api-server + +jobs: + publish-image: + runs-on: ubuntu-latest + + permissions: + contents: read + packages: write + + steps: + - uses: actions/checkout@v4 + + - uses: docker/setup-buildx-action@v3 + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Generate tags & labels + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=raw,value=latest + type=sha,format=short + type=ref,event=tag # v1.2.3 → v1.2.3 + + - name: Build and push + uses: docker/build-push-action@v5 + with: + context: ./api-server + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} From bee35f49006d47d631b892a4599e540676195cfc Mon Sep 17 00:00:00 2001 From: Nivedit Jain Date: Sun, 6 Jul 2025 15:32:17 +0530 Subject: [PATCH 04/13] Adding Auth Module (#18) * token creation init * fixed JSON issues * fixed lint issues * token is generating * added method to get claims * added api-server --- .../app/auth/controllers/create_token.py | 57 +++++++++++++++++++ .../app/auth/middlewares/get_token_claims.py | 43 ++++++++++++++ api-server/app/auth/models/token_claims.py | 9 +++ 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 | 14 ++++- .../unhandled_exceptions_middleware.py | 2 +- .../app/user/controllers/create_user.py | 17 +++--- .../app/user/models/user_database_model.py | 4 +- api-server/pyproject.toml | 1 + api-server/uv.lock | 11 ++++ 12 files changed, 171 insertions(+), 13 deletions(-) create mode 100644 api-server/app/auth/controllers/create_token.py create mode 100644 api-server/app/auth/middlewares/get_token_claims.py create mode 100644 api-server/app/auth/models/token_claims.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..bebd151b --- /dev/null +++ b/api-server/app/auth/controllers/create_token.py @@ -0,0 +1,57 @@ +import os +import jwt +from datetime import datetime, timedelta +from starlette.responses import JSONResponse + +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 + +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) + + 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) + 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) + 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) + + logger.info("User is a super admin", x_exosphere_request_id=x_exosphere_request_id) + + 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.model_dump(), JWT_SECRET_KEY, algorithm=JWT_ALGORITHM) + ) + + except Exception as e: + logger.error("Error creating token", error=e, x_exosphere_request_id=x_exosphere_request_id) + raise e \ 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 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/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..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 @@ -16,6 +17,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 @@ -23,6 +25,7 @@ # injecting routers from .user.routes import router as user_router +from .auth.router import router as auth_router load_dotenv() @@ -48,16 +51,21 @@ 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) + 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"} - -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..c705cac8 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 starlette.responses import JSONResponse from ..models.create_user_request import CreateUserRequest from ..models.create_user_response import CreateUserResponse @@ -19,10 +20,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, @@ -33,8 +33,11 @@ async def create_user(request: CreateUserRequest, x_exosphere_request_id: str) - created_at=new_user.created_at, 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, 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 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 3a5342d29e94a2df9cc4e2e51b7b04aad07e7f13 Mon Sep 17 00:00:00 2001 From: Nivedit Jain Date: Mon, 7 Jul 2025 13:34:47 +0530 Subject: [PATCH 05/13] Adding project APIs and Auth APIs (#23) * token creation init * fixed JSON issues * fixed lint issues * token is generating * added method to get claims * added api-server * added more apis * fixed authentication * fixing linting issues --- api-server/app/auth/get_token_claims.py | 31 +++++++++++++ .../app/auth/middlewares/get_token_claims.py | 43 ------------------- api-server/app/auth/routes.py | 17 ++++++++ api-server/app/main.py | 9 ++-- .../app/project/controllers/create_project.py | 36 ++++++++++++++++ .../app/project/models/billing_account.py | 3 +- .../project/models/create_project_request.py | 4 ++ .../project/models/create_project_response.py | 14 ++++++ api-server/app/project/routes.py | 27 ++++++++++++ api-server/app/user/routes.py | 6 +-- 10 files changed, 138 insertions(+), 52 deletions(-) create mode 100644 api-server/app/auth/get_token_claims.py delete mode 100644 api-server/app/auth/middlewares/get_token_claims.py create mode 100644 api-server/app/auth/routes.py create mode 100644 api-server/app/project/controllers/create_project.py create mode 100644 api-server/app/project/models/create_project_request.py create mode 100644 api-server/app/project/models/create_project_response.py create mode 100644 api-server/app/project/routes.py 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" From a611f68f7631b41eef5fdbaef786f1e065fc73c0 Mon Sep 17 00:00:00 2001 From: NiveditJain Date: Mon, 7 Jul 2025 15:26:21 +0530 Subject: [PATCH 06/13] added dockerfile --- api-server/.dockerignore | 26 ++++++++++++++++++++++++++ api-server/Dockerfile | 14 ++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 api-server/.dockerignore create mode 100644 api-server/Dockerfile diff --git a/api-server/.dockerignore b/api-server/.dockerignore new file mode 100644 index 00000000..ba4cefd6 --- /dev/null +++ b/api-server/.dockerignore @@ -0,0 +1,26 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*.pyo +*.pyd +*.so + +# C extensions +*.egg +*.egg-info +.eggs + +# Virtual environments +.venv/ + +# Git, CI and editor config +.git/ +.github/ +.vscode/ + +# Logs and temp files +*.log + +# Other +.env +Dockerfile \ No newline at end of file diff --git a/api-server/Dockerfile b/api-server/Dockerfile new file mode 100644 index 00000000..0f9cb814 --- /dev/null +++ b/api-server/Dockerfile @@ -0,0 +1,14 @@ +FROM python:3.12-slim-bookworm +COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ + +WORKDIR /api-server + +COPY pyproject.toml uv.lock ./ + +RUN uv sync --locked + +COPY . . + +EXPOSE 8000 + +CMD ["uv", "run", "run.py", "--mode", "production", "--workers", "4"] \ No newline at end of file From 3b2c259f8006c3157652a5c8b2cd6b29d4604606 Mon Sep 17 00:00:00 2001 From: NiveditJain Date: Mon, 7 Jul 2025 15:36:51 +0530 Subject: [PATCH 07/13] added docker-compose file --- api-server/docker-compose.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 api-server/docker-compose.yml diff --git a/api-server/docker-compose.yml b/api-server/docker-compose.yml new file mode 100644 index 00000000..049711f7 --- /dev/null +++ b/api-server/docker-compose.yml @@ -0,0 +1,12 @@ +version: '3.8' + +services: + api-server: + build: + context: . + dockerfile: Dockerfile + ports: + - "8000:8000" + env_file: + - .env + command: ["uv", "run", "run.py", "--mode", "production", "--workers", "4"] From f5f65462c1ef4aed9f74fe0c63d9cd4ed33b9112 Mon Sep 17 00:00:00 2001 From: NiveditJain Date: Wed, 9 Jul 2025 19:57:18 +0530 Subject: [PATCH 08/13] setting up pipeline to push to ghcr --- .github/workflows/deploy-api-server.yml | 50 +++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 .github/workflows/deploy-api-server.yml diff --git a/.github/workflows/deploy-api-server.yml b/.github/workflows/deploy-api-server.yml new file mode 100644 index 00000000..873ca3b9 --- /dev/null +++ b/.github/workflows/deploy-api-server.yml @@ -0,0 +1,50 @@ +name: Publish API server image to GHCR and deploys to Kubernetes Cluster + +on: + push: + branches: [main] + release: + types: [published] + workflow_dispatch: + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository_owner }}/exosphere-api-server + +jobs: + publish-image: + runs-on: ubuntu-latest + + permissions: + contents: read + packages: write + + steps: + - uses: actions/checkout@v4 + + - uses: docker/setup-buildx-action@v3 + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Generate tags & labels + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=raw,value=latest + type=sha,format=short + type=ref,event=tag # v1.2.3 → v1.2.3 + + - name: Build and push + uses: docker/build-push-action@v5 + with: + context: ./api-server + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} From 8ca77f14446dc90f1154ecc3b134fd767d363bbe Mon Sep 17 00:00:00 2001 From: NiveditJain Date: Wed, 9 Jul 2025 20:04:09 +0530 Subject: [PATCH 09/13] cleaned tagging --- .github/workflows/deploy-api-server.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deploy-api-server.yml b/.github/workflows/deploy-api-server.yml index 873ca3b9..4c625a8f 100644 --- a/.github/workflows/deploy-api-server.yml +++ b/.github/workflows/deploy-api-server.yml @@ -3,6 +3,8 @@ name: Publish API server image to GHCR and deploys to Kubernetes Cluster on: push: branches: [main] + paths: + - 'api-server/**' release: types: [published] workflow_dispatch: @@ -37,9 +39,9 @@ jobs: with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} tags: | - type=raw,value=latest - type=sha,format=short - type=ref,event=tag # v1.2.3 → v1.2.3 + type=raw,value=latest,enable={{is_default_branch}} + type=ref,event=tag + type=sha,format={{sha}},enable={{is_default_branch}} - name: Build and push uses: docker/build-push-action@v5 From c464d108a40b6c1e3b70cf780439d88b796c07f3 Mon Sep 17 00:00:00 2001 From: NiveditJain Date: Wed, 9 Jul 2025 20:09:43 +0530 Subject: [PATCH 10/13] testing docker file changes --- api-server/docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api-server/docker-compose.yml b/api-server/docker-compose.yml index 049711f7..99f7664e 100644 --- a/api-server/docker-compose.yml +++ b/api-server/docker-compose.yml @@ -9,4 +9,4 @@ services: - "8000:8000" env_file: - .env - command: ["uv", "run", "run.py", "--mode", "production", "--workers", "4"] + command: ["uv", "run", "run.py", "--mode", "production", "--workers", "2"] From f7c038319096ef4a2d31faeecf6ee23005254aee Mon Sep 17 00:00:00 2001 From: NiveditJain Date: Wed, 9 Jul 2025 20:19:28 +0530 Subject: [PATCH 11/13] fallback versioning --- .github/workflows/deploy-api-server.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/deploy-api-server.yml b/.github/workflows/deploy-api-server.yml index 7ffd101c..67968831 100644 --- a/.github/workflows/deploy-api-server.yml +++ b/.github/workflows/deploy-api-server.yml @@ -39,10 +39,9 @@ jobs: with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} tags: | - type=raw,value=latest,enable={{is_default_branch}} - type=ref,event=tag - type=sha,format={{sha}},enable={{is_default_branch}} - + type=raw,value=latest + type=sha,format=short + type=ref,event=tag # v1.2.3 → v1.2.3 - name: Build and push uses: docker/build-push-action@v5 From c7ec7ab4bd2888f8fbba3fc64003d5bdc82ba64b Mon Sep 17 00:00:00 2001 From: NiveditJain Date: Wed, 9 Jul 2025 23:38:15 +0530 Subject: [PATCH 12/13] added kubernetes files --- api-server/app/main.py | 3 +-- k8s/deployment.yml | 41 +++++++++++++++++++++++++++++++++++++++++ k8s/service.yaml | 10 ++++++++++ 3 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 k8s/deployment.yml create mode 100644 k8s/service.yaml diff --git a/api-server/app/main.py b/api-server/app/main.py index 7bff6228..dbab737f 100644 --- a/api-server/app/main.py +++ b/api-server/app/main.py @@ -59,8 +59,7 @@ async def lifespan(app: FastAPI): app.add_middleware(UnhandledExceptionsMiddleware) - -@app.get("/health-check") +@app.get("/health") def health() -> dict: return {"message": "OK"} diff --git a/k8s/deployment.yml b/k8s/deployment.yml new file mode 100644 index 00000000..fb4208b1 --- /dev/null +++ b/k8s/deployment.yml @@ -0,0 +1,41 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: exosphere-api-server +spec: + replicas: 2 + selector: + matchLabels: + app: exosphere-api-server + template: + metadata: + labels: + app: exosphere-api-server + spec: + containers: + - name: exosphere-api-server + image: ghcr.io/niveditjain/exosphere-api-server:latest + ports: + - containerPort: 8000 + envFrom: + - secretRef: + name: exosphere-api-server-secrets + resources: + requests: + memory: "256Mi" + cpu: "250m" + limits: + memory: "512Mi" + cpu: "500m" + readinessProbe: + httpGet: + path: /health + port: 8000 + initialDelaySeconds: 5 + periodSeconds: 10 + livenessProbe: + httpGet: + path: /health + port: 8000 + initialDelaySeconds: 5 + periodSeconds: 10 \ No newline at end of file diff --git a/k8s/service.yaml b/k8s/service.yaml new file mode 100644 index 00000000..2b2e255b --- /dev/null +++ b/k8s/service.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: Service +metadata: + name: exosphere-api-server +spec: + selector: + app: exosphere-api-server + ports: + - port: 8000 + targetPort: 8000 \ No newline at end of file From f08e630912178ff481b2ab5141f6b80cce7476df Mon Sep 17 00:00:00 2001 From: NiveditJain Date: Wed, 9 Jul 2025 23:39:22 +0530 Subject: [PATCH 13/13] niveditjain -> exospherehost --- k8s/deployment.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/k8s/deployment.yml b/k8s/deployment.yml index fb4208b1..7b70a77a 100644 --- a/k8s/deployment.yml +++ b/k8s/deployment.yml @@ -14,7 +14,7 @@ spec: spec: containers: - name: exosphere-api-server - image: ghcr.io/niveditjain/exosphere-api-server:latest + image: ghcr.io/exospherehost/exosphere-api-server:latest ports: - containerPort: 8000 envFrom: