"""Nextcloud integration: per-user username override + encrypted app password Revision ID: 0016_nextcloud_integration Revises: 0015_oidc_and_avatar Create Date: 2026-04-26 Lets each mule-image user wire their account to a Nextcloud account so photos can be browsed, indexed, and mutated under their own Nextcloud file tree (`/mnt/library/homecloud//files/...` mounted into the backend + workers as `/nextcloud-users`). The OIDC `preferred_username` claim is the default mapping; the override field handles cases where the authentik username and the Nextcloud username don't match. 1. users.nextcloud_username — default sourced from preferred_username on OIDC login (only when null), editable via PATCH /api/v1/auth/me. 2. users.nextcloud_app_password_enc — Fernet-encrypted Nextcloud app password used for HTTP Basic auth on WebDAV calls. Set from the Settings UI; the cleartext is never persisted. """ from typing import Sequence, Union from alembic import op import sqlalchemy as sa revision: str = "0016_nextcloud_integration" down_revision: Union[str, None] = "0015_oidc_and_avatar" branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: conn = op.get_bind() for col_def in ( "nextcloud_username VARCHAR", "nextcloud_app_password_enc VARCHAR", ): conn.execute(sa.text(f"ALTER TABLE users ADD COLUMN IF NOT EXISTS {col_def}")) # Index the username for the per-user path-scoping check on /browse # and /source-roots — keeps lookups fast even on tiny user tables. conn.execute(sa.text( "CREATE INDEX IF NOT EXISTS ix_users_nextcloud_username " "ON users (nextcloud_username)" )) def downgrade() -> None: conn = op.get_bind() conn.execute(sa.text("DROP INDEX IF EXISTS ix_users_nextcloud_username")) for col in ("nextcloud_app_password_enc", "nextcloud_username"): conn.execute(sa.text(f"ALTER TABLE users DROP COLUMN IF EXISTS {col}"))