- Add api/cloud/ package: connections.py, browse.py, schemas.py, __init__.py
- Add GET /api/cloud/connections/{connection_id}/items (T-12-01 IDOR, T-12-03 cred-free)
- Add PATCH /api/cloud/connections/{id} for display_name_override rename
- Add display_name_override ORM field to CloudConnection model
- Add CloudResourceAdapter service layer with str/UUID coercion
- Fix UUID type compatibility: test_cloud_items.py now uses UUID(as_uuid=True)
matching conftest — removes String(36) patch that caused type incompatibility
- Add 11 Phase 12 integration tests (IDOR, admin block, credential exclusion,
duplicate providers, rename, malformed UUID)
- Remove deleted api/cloud.py (replaced by api/cloud/ package)
64 lines
2.2 KiB
Python
64 lines
2.2 KiB
Python
"""
|
|
Cloud API package — router aggregator.
|
|
|
|
One parent router owns the /api/cloud prefix.
|
|
Sub-routers (connections, browse) carry no prefix — paths are declared
|
|
relative to the parent (D-04 pattern, same as api/documents/__init__.py).
|
|
|
|
main.py imports:
|
|
from api.cloud import router as cloud_router, users_router
|
|
app.include_router(cloud_router)
|
|
app.include_router(users_router)
|
|
"""
|
|
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
|
from pydantic import BaseModel
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from api.cloud.connections import router as connections_router, _VALID_BACKENDS
|
|
from api.cloud.browse import router as browse_router
|
|
from db.models import User
|
|
from deps.auth import get_regular_user
|
|
from deps.db import get_db
|
|
from services.rate_limiting import account_limiter
|
|
|
|
router = APIRouter(prefix="/api/cloud", tags=["cloud"])
|
|
router.include_router(connections_router)
|
|
router.include_router(browse_router)
|
|
|
|
# ── Users router (default storage) ───────────────────────────────────────────
|
|
|
|
users_router = APIRouter(prefix="/api/users", tags=["users"])
|
|
|
|
|
|
class DefaultStorageRequest(BaseModel):
|
|
backend: str
|
|
|
|
|
|
@users_router.patch("/me/default-storage")
|
|
@account_limiter.limit("100/minute")
|
|
async def update_default_storage(
|
|
request: Request,
|
|
body: DefaultStorageRequest,
|
|
session: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_regular_user),
|
|
) -> dict:
|
|
"""Update the current user's default storage backend.
|
|
|
|
Validated against the allowlist before storage.
|
|
"""
|
|
if body.backend not in _VALID_BACKENDS:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
|
detail=f"Invalid backend. Valid values: {sorted(_VALID_BACKENDS)}",
|
|
)
|
|
request.state.current_user = current_user
|
|
user = await session.get(User, current_user.id)
|
|
if user is None:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
|
|
|
|
user.default_storage_backend = body.backend
|
|
session.add(user)
|
|
await session.commit()
|
|
|
|
return {"default_storage_backend": user.default_storage_backend}
|