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:
68
api/alembic/versions/0007_band_storage.py
Normal file
68
api/alembic/versions/0007_band_storage.py
Normal file
@@ -0,0 +1,68 @@
|
||||
"""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")
|
||||
42
api/alembic/versions/0008_drop_nc_columns.py
Normal file
42
api/alembic/versions/0008_drop_nc_columns.py
Normal file
@@ -0,0 +1,42 @@
|
||||
"""Remove Nextcloud-specific columns from members and bands.
|
||||
|
||||
Prior to this migration, storage credentials lived directly on the Member
|
||||
and Band rows. They are now in the band_storage table (migration 0007),
|
||||
encrypted at the application layer.
|
||||
|
||||
Run 0007 first; if you still need to migrate existing data, do it in a
|
||||
separate script before applying this migration.
|
||||
|
||||
Revision ID: 0008_drop_nc_columns
|
||||
Revises: 0007_band_storage
|
||||
Create Date: 2026-04-10
|
||||
"""
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision = "0008_drop_nc_columns"
|
||||
down_revision = "0007_band_storage"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# Drop Nextcloud credential columns from members
|
||||
op.drop_column("members", "nc_url")
|
||||
op.drop_column("members", "nc_username")
|
||||
op.drop_column("members", "nc_password")
|
||||
|
||||
# Drop Nextcloud-specific columns from bands
|
||||
op.drop_column("bands", "nc_folder_path")
|
||||
op.drop_column("bands", "nc_user")
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# Restore columns (data is lost — this is intentional)
|
||||
op.add_column("bands", sa.Column("nc_user", sa.String(255), nullable=True))
|
||||
op.add_column("bands", sa.Column("nc_folder_path", sa.Text, nullable=True))
|
||||
|
||||
op.add_column("members", sa.Column("nc_password", sa.Text, nullable=True))
|
||||
op.add_column("members", sa.Column("nc_username", sa.String(255), nullable=True))
|
||||
op.add_column("members", sa.Column("nc_url", sa.Text, nullable=True))
|
||||
Reference in New Issue
Block a user