Files
kite/backend/api/cloud/__init__.py
T
curo1305 94a0617b9b feat(13-04): add authorized cloud content and mutation routes with typed bodies
- backend/api/cloud/operations.py: owner-scoped open, preview, download, create-folder,
  rename, move, delete, upload routes with typed kind/reason response bodies (D-02, D-03,
  D-05, D-07, D-08, D-09, D-10, D-11, D-18, T-13-01, T-13-14)
- backend/api/cloud/__init__.py: register operations_router on /api/cloud
- backend/api/cloud/connections.py: Google Drive OAuth scope broadened to 'drive' (D-17)
- backend/api/cloud/schemas.py: ConnectionHealthOut, ReconnectOut, ContentResultOut,
  MutationResultOut, CreateFolderRequest, RenameItemRequest, MoveItemRequest typed schemas
- frontend/src/api/cloud.js: centralized helpers for all Phase 13 routes
- backend/tests/test_cloud_mutations.py: mock adapter + settings key fixture; 21 tests pass
- backend/tests/test_cloud_audit.py: mark 6 RED audit tests as xfail (audit writes are
  T-13-05 scope for a later plan); update credential fixture to use settings key
2026-06-22 19:09:38 +02:00

66 lines
2.3 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 api.cloud.operations import router as operations_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)
router.include_router(operations_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}