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:
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
Reference in New Issue
Block a user