From ea174b164289c499eadbfca3e9f92abc80e0ad6a Mon Sep 17 00:00:00 2001 From: Alex Blank Date: Sun, 24 May 2026 15:02:32 +0000 Subject: [PATCH] fix: review fixes for el-1bn - Fix duplicate mode field in config_profiles.py mount response - Fix datetime.UTC import for Python 3.10 compatibility - Add API documentation for config profiles - Update CHANGELOG --- CHANGELOG.md | 1 + apps/api/src/api/config_profiles.py | 1 - apps/api/src/auth/session.py | 6 +- .../tests/integration/test_projects_api.py | 4 +- .../tests/integration/test_tool_types_api.py | 4 +- apps/api/tests/integration/test_users_api.py | 4 +- docs/api/README.md | 1 + docs/api/config-profiles.md | 433 ++++++++++++++++++ 8 files changed, 444 insertions(+), 10 deletions(-) create mode 100644 docs/api/config-profiles.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 32f2077..30d55a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **User Settings** - Theme selection, git identity, and preference management - **SSH Key Management** - Ed25519 key generation with secure storage - **Tool Types** - Built-in development tools (code-server, jupyter-notebook) with custom type support +- **Config Profiles** - User-owned profile CRUD with includes, mounts, path validation, cycle detection, and default profile selection - **Comprehensive Documentation** - Architecture, API, deployment, and development guides ### Changed diff --git a/apps/api/src/api/config_profiles.py b/apps/api/src/api/config_profiles.py index 1ac121f..43a2dd8 100644 --- a/apps/api/src/api/config_profiles.py +++ b/apps/api/src/api/config_profiles.py @@ -441,7 +441,6 @@ async def get_config_profile( "target_path": m.target_path, "mode": m.mode, "files": m.files, - "mode": m.mode, "order_index": m.order_index, "created_at": m.created_at.isoformat() if m.created_at else None, "updated_at": m.updated_at.isoformat() if m.updated_at else None, diff --git a/apps/api/src/auth/session.py b/apps/api/src/auth/session.py index 67f5bed..6ab3c0f 100644 --- a/apps/api/src/auth/session.py +++ b/apps/api/src/auth/session.py @@ -2,7 +2,7 @@ import hmac import hashlib import json import base64 -from datetime import UTC, datetime, timedelta +from datetime import datetime, timedelta, timezone from typing import Any from src.config import Settings @@ -23,7 +23,7 @@ def create_session_cookie(*, settings: Settings, user_id: str) -> str: """Create a signed session cookie value.""" payload = { "user_id": user_id, - "exp": int((datetime.now(UTC) + timedelta(hours=settings.session_ttl_hours)).timestamp()), + "exp": int((datetime.now(timezone.utc) + timedelta(hours=settings.session_ttl_hours)).timestamp()), } header = _base64url_encode(json.dumps({"alg": "HS256", "typ": "session"}).encode()) @@ -65,7 +65,7 @@ def decode_session_cookie(*, settings: Settings, cookie_value: str) -> dict[str, payload = json.loads(payload_bytes) # Check expiry - if payload.get("exp", 0) < int(datetime.now(UTC).timestamp()): + if payload.get("exp", 0) < int(datetime.now(timezone.utc).timestamp()): raise ValueError("session expired") return payload diff --git a/apps/api/tests/integration/test_projects_api.py b/apps/api/tests/integration/test_projects_api.py index d2d7c01..3c216c6 100644 --- a/apps/api/tests/integration/test_projects_api.py +++ b/apps/api/tests/integration/test_projects_api.py @@ -1,5 +1,5 @@ import uuid -from datetime import UTC, datetime, timedelta +from datetime import datetime, timedelta, timezone import asyncio import pytest @@ -58,7 +58,7 @@ def _mint_token(user_id: str) -> str: subject=user_id, email="test@headquarter.local", name="Test User", - expires_at=datetime.now(UTC) + timedelta(minutes=15), + expires_at=datetime.now(timezone.utc) + timedelta(minutes=15), ) diff --git a/apps/api/tests/integration/test_tool_types_api.py b/apps/api/tests/integration/test_tool_types_api.py index 01532f1..944b606 100644 --- a/apps/api/tests/integration/test_tool_types_api.py +++ b/apps/api/tests/integration/test_tool_types_api.py @@ -1,5 +1,5 @@ import uuid -from datetime import UTC, datetime, timedelta +from datetime import datetime, timedelta, timezone import asyncio import pytest @@ -59,7 +59,7 @@ def _mint_token(user_id: str) -> str: subject=user_id, email="test@headquarter.local", name="Test User", - expires_at=datetime.now(UTC) + timedelta(minutes=15), + expires_at=datetime.now(timezone.utc) + timedelta(minutes=15), ) diff --git a/apps/api/tests/integration/test_users_api.py b/apps/api/tests/integration/test_users_api.py index bb9a119..0ca300c 100644 --- a/apps/api/tests/integration/test_users_api.py +++ b/apps/api/tests/integration/test_users_api.py @@ -1,5 +1,5 @@ import uuid -from datetime import UTC, datetime, timedelta +from datetime import datetime, timedelta, timezone import asyncio import io @@ -83,7 +83,7 @@ def _create_auth_cookie(user_id: str) -> str: subject=user_id, email="test@headquarter.local", name="Test User", - expires_at=datetime.now(UTC) + timedelta(minutes=15), + expires_at=datetime.now(timezone.utc) + timedelta(minutes=15), ) diff --git a/docs/api/README.md b/docs/api/README.md index 010785d..22decae 100644 --- a/docs/api/README.md +++ b/docs/api/README.md @@ -35,6 +35,7 @@ All responses are JSON. Error responses follow this format: - [Repositories](repositories.md) - Git repositories and file operations - [Users](users.md) - User management and settings - [Tool Types](tool-types.md) - Tool type management +- [Config Profiles](config-profiles.md) - Config profile management for tool instances - [SSH Keys](ssh-keys.md) - SSH key management ## Testing diff --git a/docs/api/config-profiles.md b/docs/api/config-profiles.md new file mode 100644 index 0000000..9313e06 --- /dev/null +++ b/docs/api/config-profiles.md @@ -0,0 +1,433 @@ +# Config Profiles API + +Config profile management endpoints for customizing tool instances. + +## Authentication + +All endpoints require authentication (session cookie). + +--- + +## GET /config-profiles + +**Description:** List all config profiles for the current user. + +### Query Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `tool_type_id` | `string` | No | Filter by tool type compatibility (currently returns all profiles) | + +### Response + +#### Success (200 OK) + +```json +{ + "profiles": [ + { + "id": "uuid", + "user_id": "uuid", + "name": "my-profile", + "description": "My custom profile", + "created_at": "2024-01-01T00:00:00Z", + "updated_at": "2024-01-01T00:00:00Z" + } + ] +} +``` + +--- + +## POST /config-profiles + +**Description:** Create a new config profile. + +### Request + +#### Request Body + +```json +{ + "name": "my-profile", + "description": "My custom profile" +} +``` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `name` | `string` | Yes | Unique profile name (max 255 chars) | +| `description` | `string` | No | Optional description | + +### Response + +#### Success (201 Created) + +Returns created profile. + +#### Error (409 Conflict) + +```json +{ + "detail": "config profile with name 'my-profile' already exists" +} +``` + +#### Error (422 Unprocessable Entity) + +```json +{ + "detail": "Profile name cannot be empty" +} +``` + +--- + +## GET /config-profiles/{profile_id} + +**Description:** Get a config profile with its includes and mounts. + +### Response + +#### Success (200 OK) + +```json +{ + "id": "uuid", + "user_id": "uuid", + "name": "my-profile", + "description": "My custom profile", + "includes": [ + { + "id": "uuid", + "profile_id": "uuid", + "included_profile_id": "uuid", + "included_profile_name": "base-profile", + "order_index": 0, + "created_at": "2024-01-01T00:00:00Z", + "updated_at": "2024-01-01T00:00:00Z" + } + ], + "mounts": [ + { + "id": "uuid", + "profile_id": "uuid", + "target_path": "/etc/config", + "mode": "rw", + "files": {"test.txt": "hello"}, + "order_index": 0, + "created_at": "2024-01-01T00:00:00Z", + "updated_at": "2024-01-01T00:00:00Z" + } + ], + "created_at": "2024-01-01T00:00:00Z", + "updated_at": "2024-01-01T00:00:00Z" +} +``` + +--- + +## PUT /config-profiles/{profile_id} + +**Description:** Update a config profile. + +### Request + +#### Request Body + +```json +{ + "name": "updated-name", + "description": "Updated description" +} +``` + +### Response + +#### Success (200 OK) + +Returns updated profile. + +--- + +## DELETE /config-profiles/{profile_id} + +**Description:** Delete a config profile and all its includes and mounts. + +### Response + +#### Success (204 No Content) + +--- + +## GET /config-profiles/defaults + +**Description:** Get the current user's default profile assignments per tool type. + +### Response + +#### Success (200 OK) + +```json +{ + "default_profiles": { + "code-server": "profile-uuid-1", + "jupyter-notebook": "profile-uuid-2" + } +} +``` + +--- + +## PUT /config-profiles/defaults + +**Description:** Set the current user's default profile assignments per tool type. + +### Request + +#### Request Body + +```json +{ + "default_profiles": { + "code-server": "profile-uuid-1", + "jupyter-notebook": "profile-uuid-2" + } +} +``` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `default_profiles` | `object` | Yes | Mapping of tool_type_id to profile_id | + +### Response + +#### Success (200 OK) + +Returns updated default profiles. + +#### Error (404 Not Found) + +```json +{ + "detail": "profile {profile_id} not found" +} +``` + +--- + +## GET /config-profiles/defaults/{tool_type_id} + +**Description:** Get the default profile ID for a specific tool type. + +### Response + +#### Success (200 OK) + +```json +{ + "tool_type_id": "code-server", + "profile_id": "profile-uuid-1" +} +``` + +--- + +## GET /config-profiles/{profile_id}/includes + +**Description:** List all includes for a config profile. + +### Response + +#### Success (200 OK) + +```json +{ + "includes": [ + { + "id": "uuid", + "profile_id": "uuid", + "included_profile_id": "uuid", + "included_profile_name": "base-profile", + "order_index": 0, + "created_at": "2024-01-01T00:00:00Z", + "updated_at": "2024-01-01T00:00:00Z" + } + ] +} +``` + +--- + +## POST /config-profiles/{profile_id}/includes + +**Description:** Add an include to a config profile. + +### Request + +#### Request Body + +```json +{ + "included_profile_id": "uuid", + "order_index": 0 +} +``` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `included_profile_id` | `string` | Yes | UUID of the profile to include | +| `order_index` | `integer` | No | Order for include resolution (default: 0) | + +### Response + +#### Success (201 Created) + +Returns created include. + +#### Error (400 Bad Request) + +```json +{ + "detail": "a profile cannot include itself" +} +``` + +```json +{ + "detail": "adding this include would create a circular reference" +} +``` + +--- + +## PUT /config-profiles/{profile_id}/includes/{include_id} + +**Description:** Update the order index of a profile include. + +### Request + +#### Request Body + +```json +{ + "order_index": 5 +} +``` + +### Response + +#### Success (200 OK) + +Returns updated include. + +--- + +## DELETE /config-profiles/{profile_id}/includes/{include_id} + +**Description:** Remove an include from a config profile. + +### Response + +#### Success (204 No Content) + +--- + +## GET /config-profiles/{profile_id}/mounts + +**Description:** List all mounts for a config profile. + +### Response + +#### Success (200 OK) + +```json +{ + "mounts": [ + { + "id": "uuid", + "profile_id": "uuid", + "target_path": "/etc/config", + "mode": "rw", + "files": {"test.txt": "hello"}, + "order_index": 0, + "created_at": "2024-01-01T00:00:00Z", + "updated_at": "2024-01-01T00:00:00Z" + } + ] +} +``` + +--- + +## POST /config-profiles/{profile_id}/mounts + +**Description:** Add a mount to a config profile. + +### Request + +#### Request Body + +```json +{ + "target_path": "/etc/config", + "mode": "rw", + "files": {"test.txt": "hello"}, + "order_index": 0 +} +``` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `target_path` | `string` | Yes | Absolute target path (must start with /) | +| `mode` | `string` | No | Mount mode: "rw" or "ro" (default: "rw") | +| `files` | `object` | No | Files as {path: content} | +| `order_index` | `integer` | No | Order for mount resolution (default: 0) | + +### Response + +#### Success (201 Created) + +Returns created mount. + +#### Error (422 Unprocessable Entity) + +```json +{ + "detail": "Target path must be absolute (start with /)" +} +``` + +--- + +## PUT /config-profiles/{profile_id}/mounts/{mount_id} + +**Description:** Update a mount in a config profile. + +### Request + +#### Request Body + +```json +{ + "target_path": "/new/path", + "files": {"test.txt": "updated"}, + "order_index": 2 +} +``` + +### Response + +#### Success (200 OK) + +Returns updated mount. + +--- + +## DELETE /config-profiles/{profile_id}/mounts/{mount_id} + +**Description:** Remove a mount from a config profile. + +### Response + +#### Success (204 No Content)