From de347483a9c2f69abcb7ecf17fc65efa4abb0509 Mon Sep 17 00:00:00 2001 From: inimaz <93inigo93@gmail.com> Date: Sun, 21 Jun 2026 12:40:40 +0200 Subject: [PATCH 1/2] chore: docker compose runs with no auth --- .../carbonserver/api/routers/authenticate.py | 18 ++- .../carbonserver/api/services/auth_service.py | 15 ++ carbonserver/docker/Dockerfile | 12 +- .../tests/api/service/test_auth_service.py | 69 +++++++++ docker-compose.yml | 146 ++++-------------- 5 files changed, 138 insertions(+), 122 deletions(-) create mode 100644 carbonserver/tests/api/service/test_auth_service.py diff --git a/carbonserver/carbonserver/api/routers/authenticate.py b/carbonserver/carbonserver/api/routers/authenticate.py index bb96f85d3..7e9ad466e 100644 --- a/carbonserver/carbonserver/api/routers/authenticate.py +++ b/carbonserver/carbonserver/api/routers/authenticate.py @@ -2,6 +2,7 @@ import json import logging from typing import Optional +from urllib.parse import urlparse from authlib.integrations.starlette_client import OAuthError from dependency_injector.wiring import Provide, inject @@ -27,6 +28,19 @@ router = APIRouter() +def get_redirect_url(request: Request) -> str: + base_url = settings.frontend_url or "http://localhost:3000" + redirect = request.query_params.get("redirect") + if redirect: + parsed = urlparse(redirect) + if parsed.scheme in ("http", "https") and parsed.netloc in ( + "localhost:3000", + "127.0.0.1:3000", + ): + return redirect + return f"{base_url.rstrip('/')}/home" + + @router.get("/auth/check", name="auth-check") @inject def check_login( @@ -81,7 +95,7 @@ async def get_login( login and redirect to frontend app with token """ if auth_provider is None: - raise HTTPException(status_code=501, detail="Authentication not configured") + return RedirectResponse(get_redirect_url(request)) login_url = request.url_for("login") if code: try: @@ -133,7 +147,7 @@ async def logout( Logout user by clearing session and removing cookie """ if auth_provider is None: - raise HTTPException(status_code=501, detail="Authentication not configured") + return RedirectResponse(get_redirect_url(request)) # Revoke the access token at the OIDC provider before clearing it locally access_token = request.cookies.get(SESSION_COOKIE_NAME) diff --git a/carbonserver/carbonserver/api/services/auth_service.py b/carbonserver/carbonserver/api/services/auth_service.py index 163d3cdfd..1ed71f7a5 100644 --- a/carbonserver/carbonserver/api/services/auth_service.py +++ b/carbonserver/carbonserver/api/services/auth_service.py @@ -10,6 +10,7 @@ from carbonserver.api.services.auth_providers.oidc_auth_provider import ( OIDCAuthProvider, ) +from carbonserver.api.services.signup_service import SignUpService from carbonserver.api.services.user_service import UserService from carbonserver.config import settings from carbonserver.container import ServerContainer @@ -25,6 +26,11 @@ class FullUser: SESSION_COOKIE_NAME = "user_session" +LOCAL_DEV_AUTH_USER = { + "sub": "d1b9d5e0-58e8-45f0-9ef5-4549b3d6f3f0", + "email": "local.user@example.com", + "fields": {"name": "Local user"}, +} web_scheme = APIKeyCookie(name=SESSION_COOKIE_NAME, auto_error=False) @@ -49,11 +55,20 @@ async def __call__( user_service: Optional[UserService] = Depends( Provide[ServerContainer.user_service] ), + sign_up_service: Optional[SignUpService] = Depends( + Provide[ServerContainer.sign_up_service] + ), auth_provider: Optional[OIDCAuthProvider] = Depends( Provide[ServerContainer.auth_provider] ), ): self.user_service = user_service + if settings.auth_provider.lower() == "none": + self.auth_user = LOCAL_DEV_AUTH_USER + sign_up_service.check_jwt_user(self.auth_user, create=True) + self.db_user = user_service.get_user_by_id(self.auth_user["sub"]) + return self + if cookie_token is not None: self.auth_user = jwt.decode( cookie_token, diff --git a/carbonserver/docker/Dockerfile b/carbonserver/docker/Dockerfile index 3202223b6..f27bbedb6 100644 --- a/carbonserver/docker/Dockerfile +++ b/carbonserver/docker/Dockerfile @@ -1,7 +1,7 @@ # Dockerfile # Use Ubuntu to install Python and uv -# For production, you could use python:3.11-slim +# For production, you could use python:3.12-slim FROM ubuntu:22.04@sha256:3c61d3759c2639d4b836d32a2d3c83fa0214e36f195a3421018dbaaf79cbe37f @@ -17,10 +17,10 @@ RUN apt-get update && apt-get upgrade -y && \ apt-get install -y software-properties-common curl && \ add-apt-repository ppa:deadsnakes/ppa -y && \ apt-get update && \ - apt-get install -y gcc libpq-dev python3.11 python3.11-dev + apt-get install -y gcc libpq-dev python3.12 python3.12-dev -RUN ln -sf /usr/bin/python3.11 /usr/bin/python && \ - ln -sf /usr/bin/python3.11 /usr/bin/python3 +RUN ln -sf /usr/bin/python3.12 /usr/bin/python && \ + ln -sf /usr/bin/python3.12 /usr/bin/python3 # Download the latest UV installer ADD https://astral.sh/uv/install.sh /uv-installer.sh @@ -36,8 +36,8 @@ COPY pyproject.toml /app/ COPY codecarbon /app/codecarbon COPY carbonserver /app/carbonserver -# Install dependencies using uv with the api dependency group -RUN uv pip install --system -e ".[api]" +# Install carbonserver dependencies. +RUN uv pip install --system -e ./carbonserver COPY ./carbonserver/docker/entrypoint.sh /opt RUN chmod a+x /opt/entrypoint.sh diff --git a/carbonserver/tests/api/service/test_auth_service.py b/carbonserver/tests/api/service/test_auth_service.py new file mode 100644 index 000000000..40a625297 --- /dev/null +++ b/carbonserver/tests/api/service/test_auth_service.py @@ -0,0 +1,69 @@ +from unittest import mock + +import pytest +from starlette.requests import Request + +from carbonserver.api.routers import authenticate +from carbonserver.api.services import auth_service + + +@pytest.mark.asyncio +async def test_no_auth_provider_uses_local_dev_user(monkeypatch): + monkeypatch.setattr(auth_service.settings, "auth_provider", "none") + user_service = mock.Mock() + sign_up_service = mock.Mock() + + dependency = auth_service.UserWithAuthDependency(error_if_not_found=True) + result = await dependency( + user_service=user_service, + sign_up_service=sign_up_service, + auth_provider=None, + ) + + assert result.auth_user == auth_service.LOCAL_DEV_AUTH_USER + sign_up_service.check_jwt_user.assert_called_once_with( + auth_service.LOCAL_DEV_AUTH_USER, create=True + ) + user_service.get_user_by_id.assert_called_once_with( + auth_service.LOCAL_DEV_AUTH_USER["sub"] + ) + + +@pytest.mark.asyncio +async def test_no_auth_login_redirects_to_frontend(monkeypatch): + monkeypatch.setattr(authenticate.settings, "frontend_url", "http://localhost:3000") + request = Request( + { + "type": "http", + "method": "GET", + "path": "/auth/login", + "headers": [], + "scheme": "http", + "server": ("localhost", 8008), + "client": ("testclient", 50000), + "query_string": b"redirect=http%3A%2F%2Flocalhost%3A3000%2Fhome%3Fauth%3Dtrue", + } + ) + + response = await authenticate.get_login(request, auth_provider=None) + + assert response.status_code == 307 + assert response.headers["location"] == "http://localhost:3000/home?auth=true" + + +def test_no_auth_login_rejects_external_redirect(monkeypatch): + monkeypatch.setattr(authenticate.settings, "frontend_url", "http://localhost:3000") + request = Request( + { + "type": "http", + "method": "GET", + "path": "/auth/login", + "headers": [], + "scheme": "http", + "server": ("localhost", 8008), + "client": ("testclient", 50000), + "query_string": b"redirect=https%3A%2F%2Fexample.com%2Fhome", + } + ) + + assert authenticate.get_redirect_url(request) == "http://localhost:3000/home" diff --git a/docker-compose.yml b/docker-compose.yml index ef89bff19..3e55afeb4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,133 +1,51 @@ -# cp .env.example .env -# docker compose up -d services: - ############################################### - # Codecarbon-related services - ############################################### + postgres: + image: postgres:13 + environment: + POSTGRES_DB: codecarbon_db + POSTGRES_USER: codecarbon-user + POSTGRES_PASSWORD: supersecret + ports: + - "5432:5432" + volumes: + - postgres_codecarbon_data:/var/lib/postgresql/data + carbonserver: - depends_on: - - postgres build: context: . dockerfile: ./carbonserver/docker/Dockerfile + depends_on: + - postgres volumes: - ./carbonserver:/carbonserver - labels: - - "traefik.enable=true" - # - > - # traefik.http.routers.carbonserver.rule=( - # Host(`${APP_HOSTNAME}`) && ( - # PathPrefix(`/users`) || PathPrefix(`/auth`) || PathPrefix(`/docs`) || - # PathPrefix(`/organizations`) || PathPrefix(`/runs`) || PathPrefix(`/emissions`) || - # PathPrefix(`/projects`)|| PathPrefix(`/api`) || PathPrefix(`/auth-callback`) - # ))" - - "traefik.http.routers.carbonserver.rule=(Host(`${APP_HOSTNAME}`) && (PathPrefix(`/users`) || PathPrefix(`/auth`)|| PathPrefix(`/docs`)|| PathPrefix(`/organizations`) || PathPrefix(`/runs`) || PathPrefix(`/emissions`) || PathPrefix(`/projects`)|| PathPrefix(`/api`) || PathPrefix(`/auth-callback`) ))" - - "traefik.http.routers.carbonserver.entrypoints=web,websecure" - # - "traefik.http.routers.carbonserver.tls.certresolver=myresolver" - # - "traefik.http.routers.carbonserver.tls={}" - - "traefik.http.routers.carbonserver.priority=10000" - - "traefik.http.services.carbonserver.loadbalancer.server.port=8000" - - "traefik.docker.network=shared" - # ports: - # - "8000:8000" - env_file: - - ./.env environment: - CODECARBON_LOG_LEVEL: DEBUG - DATABASE_URL: postgresql://${DATABASE_USER:-codecarbon-user}:${DATABASE_PASS:-supersecret}@${DATABASE_HOST:-postgres}:${DATABASE_PORT:-5432}/${DATABASE_NAME:-codecarbon_db} - networks: - - default - - shared + AUTH_PROVIDER: ${AUTH_PROVIDER:-none} + ENVIRONMENT: ${ENVIRONMENT:-local} + DATABASE_URL: postgresql://codecarbon-user:supersecret@postgres:5432/codecarbon_db + FRONTEND_URL: http://localhost:3000 + CORS_ORIGINS: http://localhost:3000 + ports: + - "8008:8000" ui: build: context: ./webapp dockerfile: Dockerfile - - # Set environment variables based on the .env file - env_file: - - ./webapp/.env.development restart: always - labels: - - "traefik.enable=true" - - "traefik.http.routers.ui.rule=Host(`${APP_HOSTNAME}`)" - - "traefik.http.routers.ui.entrypoints=web,websecure" - # - "traefik.http.routers.ui.tls.certresolver=myresolver" - - "traefik.http.routers.ui.priority=1" - - "traefik.http.services.ui.loadbalancer.server.port=3000" - - "traefik.docker.network=shared" - - # ports: - # - "3000:3000" - networks: - - default - - shared - - postgres: - # container_name: ${DATABASE_HOST:-postgres_codecarbon} + depends_on: + - carbonserver environment: - HOSTNAME: ${DATABASE_HOST:-postgres_codecarbon} - POSTGRES_DB: ${DATABASE_NAME:-codecarbon_db} - POSTGRES_PASSWORD: ${DATABASE_PASS:-supersecret} - POSTGRES_USER: ${DATABASE_USER:-codecarbon-user} - image: postgres:13 - # ports: - # - 5480:5432 - restart: unless-stopped + VITE_API_URL: http://localhost:8008 + VITE_BASE_URL: http://localhost:3000 + VITE_USE_MOCK_DATA: "false" + VITE_PROJECT_ENCRYPTION_KEY: 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef volumes: - - postgres_codecarbon_data:/var/lib/postgresql/data:rw - networks: - - default - - # pgadmin: - # # container_name: pgadmin_codecarbon - # image: dpage/pgadmin4 - # environment: - # PGADMIN_DEFAULT_EMAIL: ${PGADMIN_DEFAULT_EMAIL:-test@test.com} - # PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_DEFAULT_PASSWORD:-test} - # volumes: - # - pgadmin:/root/.pgadmin - # - ./carbonserver/docker/pgpassfile:/pgadmin4/pgpassfile - # - ./carbonserver/docker/pgadmin-servers.json:/pgadmin4/servers.json - # # ports: - # # - "${PGADMIN_PORT:-5080}:80" - # networks: - # - default - # restart: unless-stopped - -############################################### -# Prometheus-related services -############################################### -# Uncomment the following to enable prometheus and pushgateway - -# prometheus: -# image: prom/prometheus:latest -# ports: -# - "9090:9090" -# volumes: -# - ./docker/prometheus.yml:/etc/prometheus/prometheus.yml -# depends_on: -# - "prometheus-pushgateway" -# networks: -# - default -# - shared - -# prometheus-pushgateway: -# image: prom/pushgateway -# ports: -# - "9091:9091" -# networks: -# - default -# - shared + - ./webapp:/app + - webapp_node_modules:/app/node_modules + command: sh -c "corepack enable pnpm && pnpm dev --host 0.0.0.0 --port 5173" + ports: + - "3000:5173" volumes: postgres_codecarbon_data: - name: postgres_codecarbon_data1 - pgadmin: - name: pgadmin_codecarbon_data1 - -networks: - default: - driver: bridge - shared: # traefik network - external: true + webapp_node_modules: From da71fbd393346fe74fdb236d182d5bf03fa603ab Mon Sep 17 00:00:00 2001 From: inimaz <93inigo93@gmail.com> Date: Sun, 21 Jun 2026 17:14:16 +0200 Subject: [PATCH 2/2] fix: redirect url calculated once + update dockerfile node 24 --- .../carbonserver/api/routers/authenticate.py | 21 +++++-------------- .../tests/api/service/test_auth_service.py | 20 +----------------- docker-compose.yml | 4 ++-- webapp/.dockerignore | 4 ++++ webapp/Dockerfile | 6 +++--- 5 files changed, 15 insertions(+), 40 deletions(-) create mode 100644 webapp/.dockerignore diff --git a/carbonserver/carbonserver/api/routers/authenticate.py b/carbonserver/carbonserver/api/routers/authenticate.py index 7e9ad466e..2bade44e6 100644 --- a/carbonserver/carbonserver/api/routers/authenticate.py +++ b/carbonserver/carbonserver/api/routers/authenticate.py @@ -2,7 +2,6 @@ import json import logging from typing import Optional -from urllib.parse import urlparse from authlib.integrations.starlette_client import OAuthError from dependency_injector.wiring import Provide, inject @@ -24,23 +23,13 @@ LOGGER = logging.getLogger(__name__) OAUTH_SCOPES = ["openid", "email", "profile"] SESSION_COOKIE_NAME = "user_session" +DEFAULT_REDIRECT_URL = ( + f"{(settings.frontend_url or 'http://localhost:3000').rstrip('/')}/home" +) router = APIRouter() -def get_redirect_url(request: Request) -> str: - base_url = settings.frontend_url or "http://localhost:3000" - redirect = request.query_params.get("redirect") - if redirect: - parsed = urlparse(redirect) - if parsed.scheme in ("http", "https") and parsed.netloc in ( - "localhost:3000", - "127.0.0.1:3000", - ): - return redirect - return f"{base_url.rstrip('/')}/home" - - @router.get("/auth/check", name="auth-check") @inject def check_login( @@ -95,7 +84,7 @@ async def get_login( login and redirect to frontend app with token """ if auth_provider is None: - return RedirectResponse(get_redirect_url(request)) + return RedirectResponse(DEFAULT_REDIRECT_URL) login_url = request.url_for("login") if code: try: @@ -147,7 +136,7 @@ async def logout( Logout user by clearing session and removing cookie """ if auth_provider is None: - return RedirectResponse(get_redirect_url(request)) + return RedirectResponse(DEFAULT_REDIRECT_URL) # Revoke the access token at the OIDC provider before clearing it locally access_token = request.cookies.get(SESSION_COOKIE_NAME) diff --git a/carbonserver/tests/api/service/test_auth_service.py b/carbonserver/tests/api/service/test_auth_service.py index 40a625297..84dd08089 100644 --- a/carbonserver/tests/api/service/test_auth_service.py +++ b/carbonserver/tests/api/service/test_auth_service.py @@ -48,22 +48,4 @@ async def test_no_auth_login_redirects_to_frontend(monkeypatch): response = await authenticate.get_login(request, auth_provider=None) assert response.status_code == 307 - assert response.headers["location"] == "http://localhost:3000/home?auth=true" - - -def test_no_auth_login_rejects_external_redirect(monkeypatch): - monkeypatch.setattr(authenticate.settings, "frontend_url", "http://localhost:3000") - request = Request( - { - "type": "http", - "method": "GET", - "path": "/auth/login", - "headers": [], - "scheme": "http", - "server": ("localhost", 8008), - "client": ("testclient", 50000), - "query_string": b"redirect=https%3A%2F%2Fexample.com%2Fhome", - } - ) - - assert authenticate.get_redirect_url(request) == "http://localhost:3000/home" + assert response.headers["location"] == "http://localhost:3000/home" diff --git a/docker-compose.yml b/docker-compose.yml index 3e55afeb4..acd89778e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -31,6 +31,7 @@ services: build: context: ./webapp dockerfile: Dockerfile + target: build restart: always depends_on: - carbonserver @@ -41,8 +42,7 @@ services: VITE_PROJECT_ENCRYPTION_KEY: 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef volumes: - ./webapp:/app - - webapp_node_modules:/app/node_modules - command: sh -c "corepack enable pnpm && pnpm dev --host 0.0.0.0 --port 5173" + command: sh -c "pnpm install --frozen-lockfile --force && pnpm dev --host 0.0.0.0 --port 5173" ports: - "3000:5173" diff --git a/webapp/.dockerignore b/webapp/.dockerignore new file mode 100644 index 000000000..ce870b823 --- /dev/null +++ b/webapp/.dockerignore @@ -0,0 +1,4 @@ +node_modules +.pnpm-store +dist +.next diff --git a/webapp/Dockerfile b/webapp/Dockerfile index cc37098b7..33ccf9d85 100644 --- a/webapp/Dockerfile +++ b/webapp/Dockerfile @@ -1,11 +1,11 @@ -FROM node:18-alpine AS build +FROM node:24-alpine AS build WORKDIR /app COPY package.json pnpm-lock.yaml ./ -RUN corepack enable pnpm && pnpm install --frozen-lockfile +RUN npm install -g pnpm@10.8.0 && pnpm install --frozen-lockfile COPY . . RUN pnpm build -FROM nginx:alpine +FROM nginx:1.27-alpine COPY --from=build /app/dist /usr/share/nginx/html COPY nginx.conf /etc/nginx/conf.d/default.conf EXPOSE 80