diff --git a/carbonserver/carbonserver/api/routers/authenticate.py b/carbonserver/carbonserver/api/routers/authenticate.py index bb96f85d3..2bade44e6 100644 --- a/carbonserver/carbonserver/api/routers/authenticate.py +++ b/carbonserver/carbonserver/api/routers/authenticate.py @@ -23,6 +23,9 @@ 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() @@ -81,7 +84,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(DEFAULT_REDIRECT_URL) login_url = request.url_for("login") if code: try: @@ -133,7 +136,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(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/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..84dd08089 --- /dev/null +++ b/carbonserver/tests/api/service/test_auth_service.py @@ -0,0 +1,51 @@ +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" diff --git a/docker-compose.yml b/docker-compose.yml index ef89bff19..acd89778e 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 + target: build 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 + command: sh -c "pnpm install --frozen-lockfile --force && 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: 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