Files
mule-image/backend/app/models/folders.py
dtoro 348e9c3585 feat: multi-user auth with per-user media isolation
Introduce username/password authentication with admin and user roles.
Each user gets their own media directory under /photos/{username}/ with
isolated photos, folders, heaps, and tags. Admins manage users and
observe the full library from a dedicated Settings page.

Backend:
- User model with bcrypt passwords and JWT access/refresh tokens
- Auth router (login, refresh, setup, change-password, status)
- Admin router (user CRUD with last-admin protection)
- user_id FK added to photos, folders, source_roots, heaps, tags
- All data routers scoped by authenticated user
- Scanner inherits user_id from source root owner
- Thumbnails stored under user-prefixed paths for isolation
- Library endpoints accept ?scope=global for admin cross-user view
- Alembic migration 0009 with data migration for existing installs
- Defensive bootstrap.py handles fresh vs existing DB startup

Frontend:
- AuthContext with token lifecycle, auto-refresh, login/logout
- Login page, first-run setup page, auth gate in App.tsx
- Bearer token interceptor on all API requests
- User identity + logout in left sidebar
- Admin-only Settings page with Library Management and Users tabs
- UserManagement panel (add, edit role, reset password, deactivate)
- Settings shows global stats across all users for admin
- Filter bar, right sidebar, keyboard hints hidden on settings page

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 21:47:43 +02:00

58 lines
2.2 KiB
Python

"""
Folder and SourceRoot model definitions
"""
from sqlalchemy import Column, String, Integer, Boolean, DateTime, ForeignKey, Index
from sqlalchemy.sql import func
from sqlalchemy.orm import relationship
import uuid
from app.database import Base
class SourceRoot(Base):
__tablename__ = 'source_roots'
id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))
name = Column(String, nullable=False)
path = Column(String, unique=True, nullable=False)
is_active = Column(Boolean, default=True)
added_at = Column(DateTime, server_default=func.now())
# Owner
user_id = Column(String, ForeignKey('users.id'), nullable=True, index=True)
# Relationships
folders = relationship("Folder", back_populates="source_root")
class Folder(Base):
__tablename__ = 'folders'
id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))
name = Column(String, nullable=False)
path = Column(String, unique=True, nullable=False)
parent_id = Column(String, ForeignKey('folders.id'))
source_root_id = Column(String, ForeignKey('source_roots.id'))
# Owner
user_id = Column(String, ForeignKey('users.id'), nullable=True, index=True)
photo_count = Column(Integer, default=0)
last_scanned = Column(DateTime)
# "Hide from views" — when true, photos in this folder (and every
# descendant folder) are excluded from cross-cutting views like
# All Photos, Map, Tags, People, Search and the sidebar counts.
# Photos are still scanned, thumbnailed and indexed — they just
# stop showing up unless the user navigates directly to a folder
# inside the hidden subtree. The effective flag is materialized
# onto Photo.is_hidden so queries don't have to walk parent_id.
is_hidden = Column(Boolean, nullable=False, default=False, server_default='false')
# Relationships
source_root = relationship("SourceRoot", back_populates="folders")
photos = relationship("Photo", backref="folder")
# Indexes
__table_args__ = (
Index('ix_folders_path', 'path'),
Index('ix_folders_parent_id', 'parent_id'),
Index('ix_folders_source_root_id', 'source_root_id'),
)