Commit 81d25fa7 authored by Dimitrios Gogos's avatar Dimitrios Gogos
Browse files

feat: add Dockerfile and GUI serving functionality with tests

parent 465e5c9c
Loading
Loading
Loading
Loading

.dockerignore

0 → 100644
+10 −0
Changes for .dockerignore: 10 added lines, 0 removed lines.
Original line number Diff line number Diff line
*
!pyproject.toml
!uv.lock
!README.md
!src/portal_service/
!portal-gui/
**/__pycache__
portal-gui/node_modules
portal-gui/dist
portal-gui/.env*.local

Dockerfile

0 → 100644
+25 −0
Changes for Dockerfile: 25 added lines, 0 removed lines.
Original line number Diff line number Diff line
FROM node:22-slim AS gui
WORKDIR /gui
COPY portal-gui/package.json portal-gui/package-lock.json ./
RUN npm ci --no-audit --no-fund
COPY portal-gui/ ./
RUN npm run build

FROM python:3.12-slim AS backend
RUN pip install --no-cache-dir uv
WORKDIR /app
ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy
COPY pyproject.toml uv.lock ./
RUN uv sync --locked --no-install-project
COPY README.md ./
COPY src/ src/
RUN uv sync --locked --no-editable

FROM python:3.12-slim
WORKDIR /app
COPY --from=backend /app/.venv .venv
COPY --from=gui /gui/dist gui
ENV PATH=/app/.venv/bin:$PATH GUI_DIR=/app/gui
USER 65534
EXPOSE 8000
CMD ["uvicorn", "portal_service.main:create_app", "--factory", "--host", "0.0.0.0", "--port", "8000"]
+20 −0
Changes for src/portal_service/api/gui.py: 20 added lines, 0 removed lines.
Original line number Diff line number Diff line
from starlette.exceptions import HTTPException
from starlette.responses import Response
from starlette.staticfiles import StaticFiles
from starlette.types import Scope


class GuiStaticFiles(StaticFiles):
    """The built GUI. Unknown paths get index.html, so client-side routes survive a reload."""

    async def get_response(self, path: str, scope: Scope) -> Response:
        try:
            response = await super().get_response(path, scope)
        except HTTPException as e:
            if e.status_code != 404 or path.startswith("api/"):
                raise
            response = await super().get_response("index.html", scope)
        # Keeps signed-in pages out of the back-forward cache, as the Vite server does.
        if response.media_type == "text/html":
            response.headers["Cache-Control"] = "no-store"
        return response
+2 −0
Changes for src/portal_service/config.py: 2 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -132,6 +132,8 @@ class Settings(BaseSettings):
    app_name: str = "OOP Portal Backend"
    app_version: str
    app_description: str = "Portal backend API for the Open Operator Platform"
    # Directory of the built GUI, served at /. Empty in dev, where Vite serves it.
    gui_dir: str = ""

    auth: AuthSettings = AuthSettings()
    oidc: OIDCSettings | None = None
+5 −0
Changes for src/portal_service/main.py: 5 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -30,6 +30,7 @@ from portal_service.api.dependencies import (
    get_oeg_access_token,
)
from portal_service.api.errors import register_exception_handlers
from portal_service.api.gui import GuiStaticFiles
from portal_service.api.health import health_router
from portal_service.api.middlewares import register_middlewares
from portal_service.api.oeg.routes import oeg_router
@@ -145,4 +146,8 @@ def create_app() -> FastAPI:
        app.dependency_overrides[get_oeg_access_token] = lambda: None
        logger.warning("security disabled: every request acts as the dev user")

    if settings.gui_dir:
        # Mounted last, so the routes above take precedence.
        app.mount("/", GuiStaticFiles(directory=settings.gui_dir, html=True), name="gui")

    return app
Loading