Files
rehearshalhub/api/alembic/versions/0007_band_storage.py
Mistral Vibe b2d6b4d113 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>
2026-04-10 23:22:36 +02:00

69 lines
2.1 KiB
Python

"""Add band_storage table for provider-agnostic, encrypted storage configs.
Each band can have one active storage provider (Nextcloud, Google Drive, etc.).
Credentials are Fernet-encrypted at the application layer — never stored in plaintext.
A partial unique index enforces at most one active config per band at the DB level.
Revision ID: 0007_band_storage
Revises: 0006_waveform_peaks_in_db
Create Date: 2026-04-10
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects.postgresql import UUID
revision = "0007_band_storage"
down_revision = "0006_waveform_peaks_in_db"
branch_labels = None
depends_on = None
def upgrade() -> None:
op.create_table(
"band_storage",
sa.Column("id", UUID(as_uuid=True), primary_key=True),
sa.Column(
"band_id",
UUID(as_uuid=True),
sa.ForeignKey("bands.id", ondelete="CASCADE"),
nullable=False,
),
sa.Column("provider", sa.String(20), nullable=False),
sa.Column("label", sa.String(255), nullable=True),
sa.Column("is_active", sa.Boolean, nullable=False, server_default="false"),
sa.Column("root_path", sa.Text, nullable=True),
# Fernet-encrypted JSON — never plaintext
sa.Column("credentials", sa.Text, nullable=False),
sa.Column(
"created_at",
sa.DateTime(timezone=True),
server_default=sa.func.now(),
nullable=False,
),
sa.Column(
"updated_at",
sa.DateTime(timezone=True),
server_default=sa.func.now(),
nullable=False,
),
)
# Index for fast per-band lookups
op.create_index("ix_band_storage_band_id", "band_storage", ["band_id"])
# Partial unique index: at most one active storage per band
op.execute(
"""
CREATE UNIQUE INDEX uq_band_active_storage
ON band_storage (band_id)
WHERE is_active = true
"""
)
def downgrade() -> None:
op.execute("DROP INDEX IF EXISTS uq_band_active_storage")
op.drop_index("ix_band_storage_band_id", table_name="band_storage")
op.drop_table("band_storage")