Refactor storage to provider-agnostic band-scoped model
Replaces per-member Nextcloud credentials with a BandStorage model that supports multiple providers. Credentials are Fernet-encrypted at rest; worker receives audio via an internal streaming endpoint instead of direct storage access. - Add BandStorage DB model with partial unique index (one active per band) - Add migrations 0007 (create band_storage) and 0008 (drop old nc columns) - Add StorageFactory that builds the correct StorageClient from BandStorage - Add storage router: connect/nextcloud, OAuth2 authorize/callback, list, disconnect - Add Fernet encryption helpers in security/encryption.py - Rewrite watcher for per-band polling via internal API config endpoint - Update worker to stream audio from API instead of accessing storage directly - Update frontend: new storage API in bands.ts, rewritten StorageSection, simplified band creation modal (no storage step) - Add STORAGE_ENCRYPTION_KEY to all docker-compose files Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
66
api/src/rehearsalhub/repositories/band_storage.py
Normal file
66
api/src/rehearsalhub/repositories/band_storage.py
Normal file
@@ -0,0 +1,66 @@
|
||||
"""Repository for BandStorage — per-band storage provider configuration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
|
||||
from sqlalchemy import select, update
|
||||
|
||||
from rehearsalhub.db.models import BandStorage
|
||||
from rehearsalhub.repositories.base import BaseRepository
|
||||
|
||||
|
||||
class BandStorageRepository(BaseRepository[BandStorage]):
|
||||
model = BandStorage
|
||||
|
||||
async def get_active_for_band(self, band_id: uuid.UUID) -> BandStorage | None:
|
||||
"""Return the single active storage config for *band_id*, or None."""
|
||||
result = await self.session.execute(
|
||||
select(BandStorage).where(
|
||||
BandStorage.band_id == band_id,
|
||||
BandStorage.is_active.is_(True),
|
||||
)
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
async def list_for_band(self, band_id: uuid.UUID) -> list[BandStorage]:
|
||||
result = await self.session.execute(
|
||||
select(BandStorage)
|
||||
.where(BandStorage.band_id == band_id)
|
||||
.order_by(BandStorage.created_at)
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
|
||||
async def list_active_by_provider(self, provider: str) -> list[BandStorage]:
|
||||
"""Return all active configs for a given provider (used by the watcher)."""
|
||||
result = await self.session.execute(
|
||||
select(BandStorage).where(
|
||||
BandStorage.provider == provider,
|
||||
BandStorage.is_active.is_(True),
|
||||
)
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
|
||||
async def activate(self, storage_id: uuid.UUID, band_id: uuid.UUID) -> BandStorage:
|
||||
"""Deactivate all configs for *band_id*, then activate *storage_id*."""
|
||||
await self.session.execute(
|
||||
update(BandStorage)
|
||||
.where(BandStorage.band_id == band_id)
|
||||
.values(is_active=False)
|
||||
)
|
||||
storage = await self.get_by_id(storage_id)
|
||||
if storage is None:
|
||||
raise LookupError(f"BandStorage {storage_id} not found")
|
||||
storage.is_active = True
|
||||
await self.session.flush()
|
||||
await self.session.refresh(storage)
|
||||
return storage
|
||||
|
||||
async def deactivate_all(self, band_id: uuid.UUID) -> None:
|
||||
"""Deactivate every storage config for a band (disconnect)."""
|
||||
await self.session.execute(
|
||||
update(BandStorage)
|
||||
.where(BandStorage.band_id == band_id)
|
||||
.values(is_active=False)
|
||||
)
|
||||
await self.session.flush()
|
||||
Reference in New Issue
Block a user