Medien: offizielle Assets nur Superadmin verwaltbar; Lesemodus; Upload-Verein vorausfüllen
All checks were successful
Deploy Development / deploy (push) Successful in 36s
Test Suite / pytest-backend (push) Successful in 25s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 7s
Test Suite / playwright-tests (push) Successful in 28s
All checks were successful
Deploy Development / deploy (push) Successful in 36s
Test Suite / pytest-backend (push) Successful in 25s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 7s
Test Suite / playwright-tests (push) Successful in 28s
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
f354bd9f77
commit
d3055f6f2f
|
|
@ -27,20 +27,26 @@ HIDDEN_TO_PURGE_DAYS = max(1, int(os.getenv("MEDIA_TRASH_HIDDEN_TO_PURGE_DAYS",
|
|||
def assert_can_manage_media_asset_lifecycle(cur: Any, tenant: Any, asset: dict) -> None:
|
||||
"""
|
||||
Papierkorb Stufe 2 / Recovery / Reaktivierung — nicht für trash_soft (siehe assert_can_trash_soft).
|
||||
§5.2: official nur Plattform-Admin; club Vereinsorga; privat nur Uploader.
|
||||
§5.2: official nur Superadmin; club Vereinsorga; privat nur Uploader (Plattform-Admin sonst wie bisher).
|
||||
"""
|
||||
profile_id = tenant.profile_id
|
||||
role = (tenant.global_role or "").strip().lower()
|
||||
vis = (asset.get("visibility") or "private").strip().lower()
|
||||
if vis == "official":
|
||||
if not is_superadmin(role):
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="Offizielle Medien dürfen nur von Superadmins geändert oder gelöscht werden",
|
||||
)
|
||||
return
|
||||
|
||||
if is_platform_admin(role):
|
||||
return
|
||||
vis = (asset.get("visibility") or "private").strip().lower()
|
||||
uid = asset.get("uploaded_by_profile_id")
|
||||
if vis == "private":
|
||||
if uid is not None and int(uid) == int(profile_id):
|
||||
return
|
||||
raise HTTPException(status_code=403, detail="Keine Berechtigung für dieses Medium")
|
||||
if vis == "official":
|
||||
raise HTTPException(status_code=403, detail="Nur Plattform-Admin")
|
||||
if vis == "club":
|
||||
cid = asset.get("club_id")
|
||||
if cid is None:
|
||||
|
|
@ -54,15 +60,20 @@ def assert_can_manage_media_asset_lifecycle(cur: Any, tenant: Any, asset: dict)
|
|||
def assert_can_trash_soft(cur: Any, tenant: Any, asset: dict) -> None:
|
||||
"""
|
||||
Aktiv → Papierkorb (Stufe 1). Trainer: nur eigene private Uploads.
|
||||
Vereinsmedien: Vereinsorga; official: Plattform-Admin; Superadmin: immer.
|
||||
Vereinsmedien: Vereinsorga; official: nur Superadmin; Plattform-Admin: sonst wie Verein/privat.
|
||||
"""
|
||||
role_raw = tenant.global_role
|
||||
role = (role_raw or "").strip().lower()
|
||||
if is_superadmin(role):
|
||||
return
|
||||
vis = (asset.get("visibility") or "private").strip().lower()
|
||||
if vis == "official":
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="Offizielle Medien dürfen nur von Superadmins in den Papierkorb gelegt werden",
|
||||
)
|
||||
if is_platform_admin(role):
|
||||
return
|
||||
vis = (asset.get("visibility") or "private").strip().lower()
|
||||
uid = asset.get("uploaded_by_profile_id")
|
||||
cid = asset.get("club_id")
|
||||
pid = int(tenant.profile_id)
|
||||
|
|
@ -80,8 +91,6 @@ def assert_can_trash_soft(cur: Any, tenant: Any, asset: dict) -> None:
|
|||
status_code=403,
|
||||
detail="Nur Vereinsorganisation darf Vereinsmedien in den Papierkorb legen",
|
||||
)
|
||||
if vis == "official":
|
||||
raise HTTPException(status_code=403, detail="Nur Plattform-Admin")
|
||||
raise HTTPException(status_code=403, detail="Keine Berechtigung für dieses Medium")
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -427,25 +427,25 @@ def _item_permissions(row: dict, tenant: TenantContext, admin_club_ids: set[int]
|
|||
is_owner = uid is not None and int(uid) == pid
|
||||
club_mgr = plat or (cid is not None and int(cid) in admin_club_ids)
|
||||
|
||||
edit_metadata = (
|
||||
sup
|
||||
or (vis == "official" and plat)
|
||||
or (vis == "club" and club_mgr)
|
||||
or (vis == "private" and is_owner)
|
||||
)
|
||||
|
||||
trash_soft = lc == "active" and (
|
||||
sup or plat or (vis == "private" and is_owner) or (vis == "club" and club_mgr)
|
||||
)
|
||||
if vis == "official" and not (sup or plat):
|
||||
trash_soft = False
|
||||
|
||||
can_manage_adv = (
|
||||
sup
|
||||
or plat
|
||||
or (vis == "private" and is_owner)
|
||||
or (vis == "club" and club_mgr)
|
||||
)
|
||||
if vis == "official":
|
||||
edit_metadata = sup
|
||||
trash_soft = lc == "active" and sup
|
||||
can_manage_adv = sup
|
||||
else:
|
||||
edit_metadata = (
|
||||
sup
|
||||
or (vis == "club" and club_mgr)
|
||||
or (vis == "private" and is_owner)
|
||||
)
|
||||
trash_soft = lc == "active" and (
|
||||
sup or plat or (vis == "private" and is_owner) or (vis == "club" and club_mgr)
|
||||
)
|
||||
can_manage_adv = (
|
||||
sup
|
||||
or plat
|
||||
or (vis == "private" and is_owner)
|
||||
or (vis == "club" and club_mgr)
|
||||
)
|
||||
|
||||
trash_hidden = lc in ("active", "trash_soft") and can_manage_adv
|
||||
recover_from_hidden = lc == "trash_hidden" and can_manage_adv
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Shinkan Jinkendo Version Information
|
||||
|
||||
APP_VERSION = "0.8.57"
|
||||
APP_VERSION = "0.8.58"
|
||||
BUILD_DATE = "2026-05-07"
|
||||
DB_SCHEMA_VERSION = "20260508049"
|
||||
|
||||
|
|
@ -13,7 +13,7 @@ MODULE_VERSIONS = {
|
|||
"club_join_requests": "1.0.1", # Depends(get_tenant_context)
|
||||
"admin_users": "1.0.0", # GET /api/admin/users
|
||||
"platform_media_storage": "1.0.0", # GET/PUT /api/admin/platform-media-storage (Superadmin-Pfad unter MEDIA_ROOT)
|
||||
"media_assets": "1.12.0", # Privat: Plattform-Admin muss club_id; private Ablage wie Verein (.u{pid} im Dateinamen)
|
||||
"media_assets": "1.12.1", # official: nur Superadmin Lifecycle/PATCH; UI Lesemodus; Superadmin Upload-Verein = aktiv
|
||||
"groups": "0.1.0",
|
||||
"skills": "0.1.0",
|
||||
"methods": "0.1.0",
|
||||
|
|
@ -29,6 +29,13 @@ MODULE_VERSIONS = {
|
|||
}
|
||||
|
||||
CHANGELOG = [
|
||||
{
|
||||
"version": "0.8.58",
|
||||
"date": "2026-05-07",
|
||||
"changes": [
|
||||
"Medienbibliothek offiziell: Ändern/Lifecycle nur Superadmin (nicht Plattform-Admin); Bearbeiten-Dialog für andere nur Lesemodus; Superadmin: Vereinsauswahl beim Archiv-Upload folgt aktiv/gesetzt effective_club_id",
|
||||
],
|
||||
},
|
||||
{
|
||||
"version": "0.8.57",
|
||||
"date": "2026-05-07",
|
||||
|
|
|
|||
|
|
@ -240,18 +240,6 @@ export default function MediaLibraryPage() {
|
|||
[isSuperadmin],
|
||||
)
|
||||
|
||||
const modalVisibilityOptions = useMemo(() => {
|
||||
if (!modalDraft) return archiveVisOptions
|
||||
const o = [...archiveVisOptions]
|
||||
if (!o.some((x) => x.value === modalDraft.visibility)) {
|
||||
o.push({
|
||||
value: modalDraft.visibility,
|
||||
label: visibilityUiLabel(modalDraft.visibility),
|
||||
})
|
||||
}
|
||||
return o
|
||||
}, [archiveVisOptions, modalDraft])
|
||||
|
||||
const [lifecycle, setLifecycle] = useState('active')
|
||||
const [q, setQ] = useState('')
|
||||
const [items, setItems] = useState([])
|
||||
|
|
@ -282,6 +270,25 @@ export default function MediaLibraryPage() {
|
|||
const mediaListFetchSeqRef = useRef(0)
|
||||
const gridTopAnchorRef = useRef(null)
|
||||
|
||||
const modalVisibilityOptions = useMemo(() => {
|
||||
if (!modalDraft) return archiveVisOptions
|
||||
const o = [...archiveVisOptions]
|
||||
if (!o.some((x) => x.value === modalDraft.visibility)) {
|
||||
o.push({
|
||||
value: modalDraft.visibility,
|
||||
label: visibilityUiLabel(modalDraft.visibility),
|
||||
})
|
||||
}
|
||||
return o
|
||||
}, [archiveVisOptions, modalDraft])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isSuperadmin) return
|
||||
const cid = user?.effective_club_id ?? user?.active_club_id
|
||||
if (cid == null || cid === '') return
|
||||
setUploadClubId(String(cid))
|
||||
}, [isSuperadmin, user?.effective_club_id, user?.active_club_id])
|
||||
|
||||
const loadClubs = useCallback(async () => {
|
||||
try {
|
||||
const c = await api.listClubs()
|
||||
|
|
@ -1049,13 +1056,20 @@ export default function MediaLibraryPage() {
|
|||
<div className="media-library__overlay" role="dialog" aria-modal="true" aria-labelledby="edit-media-title">
|
||||
<div className="media-library__modal">
|
||||
<div className="media-library__modal-head">
|
||||
<h2 id="edit-media-title">Medium #{modal.id}</h2>
|
||||
<h2 id="edit-media-title">
|
||||
Medium #{modal.id}
|
||||
{(modal.visibility || '').toLowerCase() === 'official' && !isSuperadmin
|
||||
? ' · Nur Lesen'
|
||||
: ''}
|
||||
</h2>
|
||||
<button type="button" className="media-library__icon-btn" onClick={closeModal} aria-label="Schließen">
|
||||
<X size={22} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="media-library__modal-body">
|
||||
{(viewer?.show_club_meta || viewer?.show_uploader_meta) && (
|
||||
{(viewer?.show_club_meta ||
|
||||
viewer?.show_uploader_meta ||
|
||||
(modal.visibility || '').toLowerCase() === 'official') && (
|
||||
<div className="media-library__meta-block">
|
||||
{viewer?.show_club_meta ? (
|
||||
<div>
|
||||
|
|
@ -1102,6 +1116,18 @@ export default function MediaLibraryPage() {
|
|||
placeholder="z. B. Technik, Wurf"
|
||||
/>
|
||||
</>
|
||||
) : (modal.visibility || '').toLowerCase() === 'official' ? (
|
||||
<>
|
||||
<p className="media-library__hint">
|
||||
Offizielle Medien sind für alle sichtbar. Bearbeiten, Sichtbarkeit und Löschung nur als Superadmin.
|
||||
</p>
|
||||
<label className="form-label">Bezeichnung</label>
|
||||
<input className="form-input" readOnly value={modalDraft.display_name} />
|
||||
<label className="form-label">Copyright</label>
|
||||
<textarea className="form-input" readOnly rows={3} value={modalDraft.copyright_notice} />
|
||||
<label className="form-label">Schlagwörter</label>
|
||||
<input className="form-input" readOnly value={modalDraft.tags_input} />
|
||||
</>
|
||||
) : (
|
||||
<p className="media-library__hint">Keine Berechtigung für Metadaten — nur Verwaltende dieser Stufe.</p>
|
||||
)}
|
||||
|
|
@ -1155,9 +1181,11 @@ export default function MediaLibraryPage() {
|
|||
</div>
|
||||
|
||||
<div className="media-library__modal-actions">
|
||||
<button type="button" className="btn btn-secondary" disabled={busy} onClick={saveModal}>
|
||||
Speichern
|
||||
</button>
|
||||
{modal.permissions?.edit_metadata ? (
|
||||
<button type="button" className="btn btn-secondary" disabled={busy} onClick={saveModal}>
|
||||
Speichern
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="media-library__lc-block">
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user