Lets each mule-image user (matched via OIDC preferred_username, overridable in Settings) browse their Nextcloud files/ tree from the mule-image UI and register subfolders as per-user SourceRoots. Reads stay direct on the bind-mounted /nextcloud-users path; mutations (upload, delete, rename, move within NC) dispatch through Nextcloud WebDAV so oc_filecache, trashbin, comments, and desktop-sync clients stay coherent. Backend: - users.nextcloud_username + nextcloud_app_password_enc (Fernet at rest, key derived from SECRET_KEY) — alembic 0016 - services/nextcloud_dav.py: minimal WebDAV client (PUT, MKCOL, DELETE, MOVE) with HTTP Basic auth via the per-user app password - routers/nextcloud.py: GET /browse, /whoami, GET/POST/DELETE /source-roots (path-scoped to current_user.nextcloud_username with realpath traversal guard) - PATCH /api/v1/auth/me to update nextcloud_username and app password - OIDC callback defaults nextcloud_username from preferred_username on first login; backfill on existing users; never overwrites a manual override - routers/upload.py: stream upload to NamedTemporaryFile, then PUT to WebDAV (with MKCOL chain) when destination is NC-rooted; existing Photo row creation runs unchanged - routers/discard.py empty-trash: WebDAV DELETE for NC files - routers/photos.py rename + move: WebDAV MOVE for NC paths; cross-system move/copy returns a clean error - routers/folders.py rename + create + permanent-delete: dispatch via WebDAV when targeting NC-rooted paths Frontend: - AuthUser carries nextcloud_username + has_nextcloud_app_password - services/api.ts: nextcloud + account namespaces - components/dialogs/NextcloudFolderPicker.tsx: lazy tree browser, name + submit -> POST /source-roots - SettingsDialog: new "Nextcloud library" card with username override + validate, app-password input, list/remove of NC libraries, and the picker entry point docker-compose.yml: NEXTCLOUD_USERS_HOST_PATH bind to /nextcloud-users on backend + 3 workers; NEXTCLOUD_USERS_ROOT + NEXTCLOUD_BASE_URL env. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
53 lines
2.0 KiB
Python
53 lines
2.0 KiB
Python
"""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/<nc_user>/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}"))
|