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
This commit is contained in:
2026-05-24 15:02:32 +00:00
parent a1dbfcf2a8
commit ea174b1642
8 changed files with 444 additions and 10 deletions
+1
View File
@@ -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 - **User Settings** - Theme selection, git identity, and preference management
- **SSH Key Management** - Ed25519 key generation with secure storage - **SSH Key Management** - Ed25519 key generation with secure storage
- **Tool Types** - Built-in development tools (code-server, jupyter-notebook) with custom type support - **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 - **Comprehensive Documentation** - Architecture, API, deployment, and development guides
### Changed ### Changed
-1
View File
@@ -441,7 +441,6 @@ async def get_config_profile(
"target_path": m.target_path, "target_path": m.target_path,
"mode": m.mode, "mode": m.mode,
"files": m.files, "files": m.files,
"mode": m.mode,
"order_index": m.order_index, "order_index": m.order_index,
"created_at": m.created_at.isoformat() if m.created_at else None, "created_at": m.created_at.isoformat() if m.created_at else None,
"updated_at": m.updated_at.isoformat() if m.updated_at else None, "updated_at": m.updated_at.isoformat() if m.updated_at else None,
+3 -3
View File
@@ -2,7 +2,7 @@ import hmac
import hashlib import hashlib
import json import json
import base64 import base64
from datetime import UTC, datetime, timedelta from datetime import datetime, timedelta, timezone
from typing import Any from typing import Any
from src.config import Settings 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.""" """Create a signed session cookie value."""
payload = { payload = {
"user_id": user_id, "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()) 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) payload = json.loads(payload_bytes)
# Check expiry # 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") raise ValueError("session expired")
return payload return payload
@@ -1,5 +1,5 @@
import uuid import uuid
from datetime import UTC, datetime, timedelta from datetime import datetime, timedelta, timezone
import asyncio import asyncio
import pytest import pytest
@@ -58,7 +58,7 @@ def _mint_token(user_id: str) -> str:
subject=user_id, subject=user_id,
email="test@headquarter.local", email="test@headquarter.local",
name="Test User", name="Test User",
expires_at=datetime.now(UTC) + timedelta(minutes=15), expires_at=datetime.now(timezone.utc) + timedelta(minutes=15),
) )
@@ -1,5 +1,5 @@
import uuid import uuid
from datetime import UTC, datetime, timedelta from datetime import datetime, timedelta, timezone
import asyncio import asyncio
import pytest import pytest
@@ -59,7 +59,7 @@ def _mint_token(user_id: str) -> str:
subject=user_id, subject=user_id,
email="test@headquarter.local", email="test@headquarter.local",
name="Test User", name="Test User",
expires_at=datetime.now(UTC) + timedelta(minutes=15), expires_at=datetime.now(timezone.utc) + timedelta(minutes=15),
) )
+2 -2
View File
@@ -1,5 +1,5 @@
import uuid import uuid
from datetime import UTC, datetime, timedelta from datetime import datetime, timedelta, timezone
import asyncio import asyncio
import io import io
@@ -83,7 +83,7 @@ def _create_auth_cookie(user_id: str) -> str:
subject=user_id, subject=user_id,
email="test@headquarter.local", email="test@headquarter.local",
name="Test User", name="Test User",
expires_at=datetime.now(UTC) + timedelta(minutes=15), expires_at=datetime.now(timezone.utc) + timedelta(minutes=15),
) )
+1
View File
@@ -35,6 +35,7 @@ All responses are JSON. Error responses follow this format:
- [Repositories](repositories.md) - Git repositories and file operations - [Repositories](repositories.md) - Git repositories and file operations
- [Users](users.md) - User management and settings - [Users](users.md) - User management and settings
- [Tool Types](tool-types.md) - Tool type management - [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 - [SSH Keys](ssh-keys.md) - SSH key management
## Testing ## Testing
+433
View File
@@ -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)