Files
mule-image/backend/app/models/user.py
Claudio bc0bb44c05 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>
2026-04-26 01:06:37 +02:00

50 lines
2.2 KiB
Python

"""
User model definition
"""
from sqlalchemy import Column, String, Boolean, DateTime
from sqlalchemy.sql import func
import uuid
from app.database import Base
class User(Base):
__tablename__ = 'users'
id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))
username = Column(String(50), unique=True, nullable=False, index=True)
email = Column(String, unique=True, nullable=True)
# Nullable: OIDC-only users have no local password. Local accounts
# still always have one.
hashed_password = Column(String, nullable=True)
role = Column(String, nullable=False, default='user') # 'admin' | 'user'
is_active = Column(Boolean, default=True)
created_at = Column(DateTime, server_default=func.now())
# Absolute path to this user's photo directory (e.g., "/photos/daniel")
media_path = Column(String, nullable=False)
# OIDC identity — populated when a user signs in via Authentik (or any
# other OIDC provider later). `oidc_sub` is stable per provider, so
# lookups key on (oidc_issuer, oidc_sub). NULL for password-only users.
oidc_issuer = Column(String, nullable=True)
oidc_sub = Column(String, nullable=True)
# Profile bits that can come from OIDC claims or be filled in later.
# avatar_url wins over Gravatar when set; the /auth/me response
# computes the final avatar URL for the frontend.
avatar_url = Column(String, nullable=True)
display_name = Column(String, nullable=True)
# Nextcloud integration. `nextcloud_username` defaults to the
# `preferred_username` OIDC claim on first login but can be overridden
# in Settings (the local mule-image username doesn't always match the
# Nextcloud user — e.g. authentik `dtoro` ↔ Nextcloud `admin`).
# `nextcloud_app_password_enc` is the user's Nextcloud app password
# (created from Nextcloud → Settings → Security), Fernet-encrypted at
# rest with a key derived from settings.secret_key. Used as HTTP Basic
# auth on outgoing WebDAV calls when the user mutates a file under
# their Nextcloud-rooted SourceRoot.
nextcloud_username = Column(String, nullable=True, index=True)
nextcloud_app_password_enc = Column(String, nullable=True)