feat(nextcloud): per-user Nextcloud library integration
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>
This commit is contained in:
52
backend/alembic/versions/0016_nextcloud_integration.py
Normal file
52
backend/alembic/versions/0016_nextcloud_integration.py
Normal file
@@ -0,0 +1,52 @@
|
||||
"""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}"))
|
||||
Reference in New Issue
Block a user