feat(P-13): implement content reporting enhancements, including email notifications and audit log entries
Some checks failed
Deploy Development / deploy (push) Successful in 36s
Test Suite / pytest-backend (push) Failing after 0s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 10s
Test Suite / playwright-tests (push) Failing after 3m41s
Some checks failed
Deploy Development / deploy (push) Successful in 36s
Test Suite / pytest-backend (push) Failing after 0s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 10s
Test Suite / playwright-tests (push) Failing after 3m41s
This commit is contained in:
parent
24bf3f7035
commit
bacba311ae
|
|
@ -330,7 +330,7 @@ def write_rights_declaration(
|
||||||
def write_audit_log_entry(
|
def write_audit_log_entry(
|
||||||
cur: Any,
|
cur: Any,
|
||||||
asset_id: int,
|
asset_id: int,
|
||||||
acting_profile_id: int,
|
acting_profile_id: Optional[int],
|
||||||
event_type: str,
|
event_type: str,
|
||||||
old_values: dict,
|
old_values: dict,
|
||||||
new_values: dict,
|
new_values: dict,
|
||||||
|
|
|
||||||
15
backend/migrations/053_content_report_audit_event.sql
Normal file
15
backend/migrations/053_content_report_audit_event.sql
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
-- Migration 053: 'content_report_filed' als neuer event_type im Medien-Audit-Log
|
||||||
|
-- Notwendig fuer P-13: Journaleintrag beim Einreichen einer Inhaltsmeldung.
|
||||||
|
|
||||||
|
ALTER TABLE media_asset_audit_log
|
||||||
|
DROP CONSTRAINT IF EXISTS media_asset_audit_log_event_type_check;
|
||||||
|
|
||||||
|
ALTER TABLE media_asset_audit_log
|
||||||
|
ADD CONSTRAINT media_asset_audit_log_event_type_check
|
||||||
|
CHECK (event_type IN (
|
||||||
|
'visibility_change',
|
||||||
|
'copyright_change',
|
||||||
|
'metadata_change',
|
||||||
|
'lifecycle_change',
|
||||||
|
'content_report_filed'
|
||||||
|
));
|
||||||
|
|
@ -11,12 +11,20 @@ Architektur:
|
||||||
|
|
||||||
Berechtigungen:
|
Berechtigungen:
|
||||||
- Meldung einreichen: optional Auth (ohne Auth nur fuer official-Medien erlaubt)
|
- Meldung einreichen: optional Auth (ohne Auth nur fuer official-Medien erlaubt)
|
||||||
- Liste/Detail: Plattform-Admin (admin/superadmin)
|
- Liste/Detail: Plattform-Admin (alle Meldungen) oder Club-Admin (nur eigene Vereinsmedien)
|
||||||
- Status-Bearbeitung: Plattform-Admin
|
- Status-Bearbeitung: Plattform-Admin
|
||||||
- Legal Hold aus Meldung: ausschliesslich Superadmin
|
- Legal Hold aus Meldung: ausschliesslich Superadmin
|
||||||
|
|
||||||
|
E-Mail-Benachrichtigungen:
|
||||||
|
- Nach Eingang: Bestaetigung an Melder
|
||||||
|
- Nach Eingang: Benachrichtigung aller Plattform-Admins (best-effort, kein Fehler wenn SMTP fehlt)
|
||||||
|
|
||||||
|
Audit-Log (Migration 053):
|
||||||
|
- Bei media_asset-Meldungen: Eintrag in media_asset_audit_log (event_type='content_report_filed')
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
|
@ -30,6 +38,7 @@ from media_legal_hold import (
|
||||||
assert_superadmin_for_legal_hold,
|
assert_superadmin_for_legal_hold,
|
||||||
set_legal_hold,
|
set_legal_hold,
|
||||||
)
|
)
|
||||||
|
from media_rights import write_audit_log_entry
|
||||||
from tenant_context import TenantContext, get_tenant_context
|
from tenant_context import TenantContext, get_tenant_context
|
||||||
|
|
||||||
router = APIRouter(prefix="/api", tags=["content_reports"])
|
router = APIRouter(prefix="/api", tags=["content_reports"])
|
||||||
|
|
@ -46,6 +55,17 @@ REPORT_REASONS = frozenset({
|
||||||
"other",
|
"other",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
REASON_LABELS_DE = {
|
||||||
|
"copyright": "Urheberrecht",
|
||||||
|
"image_rights": "Bildrechte",
|
||||||
|
"privacy": "Datenschutz / Persönlichkeitsrecht",
|
||||||
|
"minors": "Minderjährige",
|
||||||
|
"illegal_content": "Rechtswidriger Inhalt",
|
||||||
|
"youth_protection": "Jugendschutz",
|
||||||
|
"offensive_content": "Beleidigender Inhalt",
|
||||||
|
"other": "Sonstiges",
|
||||||
|
}
|
||||||
|
|
||||||
# Gruende mit hoher Prioritaet (illegal, Minderjaehrigenschutz)
|
# Gruende mit hoher Prioritaet (illegal, Minderjaehrigenschutz)
|
||||||
HIGH_PRIORITY_REASONS = frozenset({
|
HIGH_PRIORITY_REASONS = frozenset({
|
||||||
"minors",
|
"minors",
|
||||||
|
|
@ -157,7 +177,6 @@ def _is_media_asset_visible_to_profile(cur, asset_id: int, profile_id: int, glob
|
||||||
if is_platform_admin(global_role):
|
if is_platform_admin(global_role):
|
||||||
cur.execute("SELECT 1 FROM media_assets WHERE id = %s", (asset_id,))
|
cur.execute("SELECT 1 FROM media_assets WHERE id = %s", (asset_id,))
|
||||||
return cur.fetchone() is not None
|
return cur.fetchone() is not None
|
||||||
# Sichtbarkeit pruefen: official (immer), club (Mitglied), private (Eigentuemerrechte sind breiter)
|
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"""
|
"""
|
||||||
SELECT ma.id, ma.visibility, ma.club_id, ma.uploaded_by_profile_id
|
SELECT ma.id, ma.visibility, ma.club_id, ma.uploaded_by_profile_id
|
||||||
|
|
@ -188,7 +207,6 @@ def _is_media_asset_visible_to_profile(cur, asset_id: int, profile_id: int, glob
|
||||||
)
|
)
|
||||||
return cur.fetchone() is not None
|
return cur.fetchone() is not None
|
||||||
if visibility == "private":
|
if visibility == "private":
|
||||||
# Private Medien: Eigentuemerrechte oder gleicher Verein mit club_admin
|
|
||||||
if asset.get("uploaded_by_profile_id") == profile_id:
|
if asset.get("uploaded_by_profile_id") == profile_id:
|
||||||
return True
|
return True
|
||||||
club_id = asset.get("club_id")
|
club_id = asset.get("club_id")
|
||||||
|
|
@ -265,6 +283,96 @@ def _resolve_optional_auth(cur, x_auth_token: Optional[str]) -> tuple[Optional[i
|
||||||
return d.get("profile_id"), d.get("role")
|
return d.get("profile_id"), d.get("role")
|
||||||
|
|
||||||
|
|
||||||
|
def _get_platform_admin_emails(cur) -> list[str]:
|
||||||
|
"""Gibt E-Mail-Adressen aller aktiven Plattform-Admins zurueck."""
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
SELECT email FROM profiles
|
||||||
|
WHERE role IN ('admin', 'superadmin')
|
||||||
|
AND email IS NOT NULL AND email <> ''
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
return [r2d(row)["email"] for row in cur.fetchall()]
|
||||||
|
|
||||||
|
|
||||||
|
def _send_email(to: str, subject: str, body: str) -> None:
|
||||||
|
"""SMTP-Versand (best-effort, ignoriert Fehler und nicht konfiguriertes SMTP)."""
|
||||||
|
try:
|
||||||
|
import smtplib, ssl
|
||||||
|
from email.mime.text import MIMEText
|
||||||
|
|
||||||
|
smtp_host = (os.getenv("SMTP_HOST") or "").strip()
|
||||||
|
smtp_user = (os.getenv("SMTP_USER") or "").strip()
|
||||||
|
smtp_pass = os.getenv("SMTP_PASS") or ""
|
||||||
|
if not smtp_host or not smtp_user or not smtp_pass:
|
||||||
|
return
|
||||||
|
smtp_port = int(os.getenv("SMTP_PORT") or "587")
|
||||||
|
smtp_from = os.getenv("SMTP_FROM", "noreply@jinkendo.de")
|
||||||
|
force_ssl = (os.getenv("SMTP_SSL", "").strip().lower() in ("1", "true", "yes")) or smtp_port == 465
|
||||||
|
use_tls = os.getenv("SMTP_STARTTLS", "true").strip().lower() not in ("0", "false", "no")
|
||||||
|
|
||||||
|
msg = MIMEText(body, "plain", "utf-8")
|
||||||
|
msg["Subject"] = subject
|
||||||
|
msg["From"] = smtp_from
|
||||||
|
msg["To"] = to
|
||||||
|
ctx = ssl.create_default_context()
|
||||||
|
if force_ssl:
|
||||||
|
with smtplib.SMTP_SSL(smtp_host, smtp_port, context=ctx, timeout=30) as srv:
|
||||||
|
srv.login(smtp_user, smtp_pass)
|
||||||
|
srv.send_message(msg)
|
||||||
|
else:
|
||||||
|
with smtplib.SMTP(smtp_host, smtp_port, timeout=30) as srv:
|
||||||
|
srv.ehlo()
|
||||||
|
if use_tls:
|
||||||
|
srv.starttls(context=ctx)
|
||||||
|
srv.ehlo()
|
||||||
|
srv.login(smtp_user, smtp_pass)
|
||||||
|
srv.send_message(msg)
|
||||||
|
except Exception as exc:
|
||||||
|
print(f"[SMTP content_reports] Fehler: {exc}")
|
||||||
|
|
||||||
|
|
||||||
|
def _notify_report_submitted(
|
||||||
|
*,
|
||||||
|
report_id: int,
|
||||||
|
reporter_name: str,
|
||||||
|
reporter_email: str,
|
||||||
|
reason: str,
|
||||||
|
priority: str,
|
||||||
|
target_type: str,
|
||||||
|
target_id: int,
|
||||||
|
admin_emails: list[str],
|
||||||
|
) -> None:
|
||||||
|
"""Sendet Bestaetigungs-Mail an Melder + Benachrichtigung an alle Plattform-Admins (best-effort)."""
|
||||||
|
app_url = (os.getenv("APP_URL") or "https://shinkan.jinkendo.de").rstrip("/")
|
||||||
|
reason_label = REASON_LABELS_DE.get(reason, reason)
|
||||||
|
target_label = ("Medium" if target_type == "media_asset" else "Übung") + f" #{target_id}"
|
||||||
|
|
||||||
|
# Bestaetigungs-Mail an Melder
|
||||||
|
confirmation_body = (
|
||||||
|
f"Hallo {reporter_name},\n\n"
|
||||||
|
f"Ihre Meldung (#{report_id}) wurde entgegengenommen.\n\n"
|
||||||
|
f"Meldegrund: {reason_label}\n"
|
||||||
|
f"Gemeldeter Inhalt: {target_label}\n\n"
|
||||||
|
f"Ein Administrator wird Ihre Meldung zeitnah prüfen.\n\n"
|
||||||
|
f"Shinkan Jinkendo"
|
||||||
|
)
|
||||||
|
_send_email(reporter_email, f"Meldung #{report_id} eingegangen – Shinkan Jinkendo", confirmation_body)
|
||||||
|
|
||||||
|
# Admin-Benachrichtigung
|
||||||
|
prio_label = "DRINGEND" if priority == "high" else "normal"
|
||||||
|
admin_body = (
|
||||||
|
f"Neue Inhaltsmeldung #{report_id} [{prio_label}]\n\n"
|
||||||
|
f"Meldegrund: {reason_label}\n"
|
||||||
|
f"Ziel: {target_label}\n"
|
||||||
|
f"Gemeldet von: {reporter_name} <{reporter_email}>\n\n"
|
||||||
|
f"Zur Inbox: {app_url}/inbox\n"
|
||||||
|
)
|
||||||
|
subject = f"Inhaltsmeldung #{report_id} [{prio_label}] – Shinkan Jinkendo"
|
||||||
|
for email in admin_emails:
|
||||||
|
_send_email(email, subject, admin_body)
|
||||||
|
|
||||||
|
|
||||||
def _report_row_to_dict(row) -> dict:
|
def _report_row_to_dict(row) -> dict:
|
||||||
return r2d(row)
|
return r2d(row)
|
||||||
|
|
||||||
|
|
@ -282,6 +390,8 @@ def submit_content_report(
|
||||||
- Ohne Login: nur fuer 'official'-Medien erlaubt (öffentlich erreichbare Inhalte)
|
- Ohne Login: nur fuer 'official'-Medien erlaubt (öffentlich erreichbare Inhalte)
|
||||||
- Mit Login: fuer alle Inhalte, die der Nutzer laut Sichtbarkeitslogik sehen darf
|
- Mit Login: fuer alle Inhalte, die der Nutzer laut Sichtbarkeitslogik sehen darf
|
||||||
- Gutglaubenserklärung (good_faith_confirmed=true) ist Pflicht
|
- Gutglaubenserklärung (good_faith_confirmed=true) ist Pflicht
|
||||||
|
- Journaleintrag im Medien-Audit-Log (wenn target_type=media_asset)
|
||||||
|
- E-Mail-Bestaetigung an Melder + Benachrichtigung an Plattform-Admins (best-effort)
|
||||||
"""
|
"""
|
||||||
if not body.good_faith_confirmed:
|
if not body.good_faith_confirmed:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
|
|
@ -291,6 +401,9 @@ def submit_content_report(
|
||||||
|
|
||||||
priority = "high" if body.report_reason in HIGH_PRIORITY_REASONS else "normal"
|
priority = "high" if body.report_reason in HIGH_PRIORITY_REASONS else "normal"
|
||||||
|
|
||||||
|
report_id: int
|
||||||
|
admin_emails: list[str] = []
|
||||||
|
|
||||||
with get_db() as conn:
|
with get_db() as conn:
|
||||||
cur = get_cursor(conn)
|
cur = get_cursor(conn)
|
||||||
|
|
||||||
|
|
@ -341,10 +454,43 @@ def submit_content_report(
|
||||||
)
|
)
|
||||||
row = cur.fetchone()
|
row = cur.fetchone()
|
||||||
result = r2d(row)
|
result = r2d(row)
|
||||||
|
report_id = int(result["id"])
|
||||||
|
|
||||||
|
# Audit-Log-Eintrag (nur fuer media_asset-Meldungen)
|
||||||
|
if body.target_type == "media_asset":
|
||||||
|
write_audit_log_entry(
|
||||||
|
cur,
|
||||||
|
body.target_id,
|
||||||
|
profile_id,
|
||||||
|
"content_report_filed",
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
"content_report_id": report_id,
|
||||||
|
"report_reason": body.report_reason,
|
||||||
|
"priority": priority,
|
||||||
|
"reporter_email": body.reporter_email,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Admin-E-Mails vor Commit abfragen (Verbindung noch offen)
|
||||||
|
admin_emails = _get_platform_admin_emails(cur)
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
|
# E-Mails nach Commit senden (best-effort, kein Rollback-Risiko)
|
||||||
|
_notify_report_submitted(
|
||||||
|
report_id=report_id,
|
||||||
|
reporter_name=body.reporter_name,
|
||||||
|
reporter_email=body.reporter_email,
|
||||||
|
reason=body.report_reason,
|
||||||
|
priority=priority,
|
||||||
|
target_type=body.target_type,
|
||||||
|
target_id=body.target_id,
|
||||||
|
admin_emails=admin_emails,
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"id": result["id"],
|
"id": report_id,
|
||||||
"status": result["status"],
|
"status": result["status"],
|
||||||
"priority": result["priority"],
|
"priority": result["priority"],
|
||||||
"submitted_at": str(result["submitted_at"]),
|
"submitted_at": str(result["submitted_at"]),
|
||||||
|
|
@ -357,14 +503,20 @@ def list_inbox_content_reports(
|
||||||
tenant: TenantContext = Depends(get_tenant_context),
|
tenant: TenantContext = Depends(get_tenant_context),
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Alle Meldungen fuer die Admin-Inbox.
|
Meldungen fuer die Inbox.
|
||||||
Sichtbar fuer Plattform-Admins (admin + superadmin).
|
- Plattform-Admins (admin/superadmin): alle Meldungen
|
||||||
In der InboxPage neben den Join-Requests angezeigt.
|
- Club-Admins: nur Meldungen ueber Medien ihres Vereins
|
||||||
"""
|
"""
|
||||||
_assert_platform_admin(tenant.global_role)
|
pid = tenant.profile_id
|
||||||
|
role = tenant.global_role
|
||||||
|
|
||||||
|
is_padmin = is_platform_admin(role)
|
||||||
|
|
||||||
with get_db() as conn:
|
with get_db() as conn:
|
||||||
cur = get_cursor(conn)
|
cur = get_cursor(conn)
|
||||||
|
|
||||||
|
if is_padmin:
|
||||||
|
# Plattform-Admin: alle Meldungen
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"""
|
"""
|
||||||
SELECT
|
SELECT
|
||||||
|
|
@ -385,16 +537,12 @@ def list_inbox_content_reports(
|
||||||
cr.submitted_at,
|
cr.submitted_at,
|
||||||
cr.created_at,
|
cr.created_at,
|
||||||
cr.updated_at,
|
cr.updated_at,
|
||||||
-- Ziel-Medium-Infos (wenn target_type = media_asset)
|
|
||||||
ma.original_filename AS target_filename,
|
ma.original_filename AS target_filename,
|
||||||
ma.visibility AS target_visibility,
|
ma.visibility AS target_visibility,
|
||||||
ma.media_kind AS target_media_kind,
|
ma.media_kind AS target_media_kind,
|
||||||
ma.legal_hold_active AS target_legal_hold_active,
|
ma.legal_hold_active AS target_legal_hold_active,
|
||||||
-- Ziel-Uebungs-Infos (wenn target_type = exercise)
|
|
||||||
ex.name AS target_exercise_name,
|
ex.name AS target_exercise_name,
|
||||||
-- Reviewer-Name
|
|
||||||
rev.name AS reviewed_by_name,
|
rev.name AS reviewed_by_name,
|
||||||
-- Zugewiesen-Name
|
|
||||||
asgn.name AS assigned_to_name
|
asgn.name AS assigned_to_name
|
||||||
FROM content_reports cr
|
FROM content_reports cr
|
||||||
LEFT JOIN media_assets ma ON ma.id = cr.target_id AND cr.target_type = 'media_asset'
|
LEFT JOIN media_assets ma ON ma.id = cr.target_id AND cr.target_type = 'media_asset'
|
||||||
|
|
@ -410,6 +558,72 @@ def list_inbox_content_reports(
|
||||||
LIMIT 500
|
LIMIT 500
|
||||||
""",
|
""",
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
# Club-Admin: Meldungen ueber Medien der eigenen Vereine
|
||||||
|
# Zuerst pruefen ob ueberhaupt Club-Admin-Rolle vorhanden
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
SELECT COUNT(*) AS cnt
|
||||||
|
FROM club_members cm
|
||||||
|
INNER JOIN club_member_roles r ON r.club_member_id = cm.id
|
||||||
|
WHERE cm.profile_id = %s AND cm.status = 'active' AND r.role_code = 'club_admin'
|
||||||
|
""",
|
||||||
|
(pid,),
|
||||||
|
)
|
||||||
|
cnt_row = r2d(cur.fetchone())
|
||||||
|
if int(cnt_row.get("cnt", 0)) == 0:
|
||||||
|
raise HTTPException(status_code=403, detail="Kein Zugriff auf Inhaltsmeldungen")
|
||||||
|
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
SELECT
|
||||||
|
cr.id,
|
||||||
|
cr.target_type,
|
||||||
|
cr.target_id,
|
||||||
|
cr.report_reason,
|
||||||
|
cr.report_description,
|
||||||
|
cr.reporter_name,
|
||||||
|
cr.reporter_email,
|
||||||
|
cr.reporter_profile_id,
|
||||||
|
cr.priority,
|
||||||
|
cr.status,
|
||||||
|
cr.assigned_to_profile_id,
|
||||||
|
cr.reviewed_by_profile_id,
|
||||||
|
cr.reviewed_at,
|
||||||
|
cr.resolution_note,
|
||||||
|
cr.submitted_at,
|
||||||
|
cr.created_at,
|
||||||
|
cr.updated_at,
|
||||||
|
ma.original_filename AS target_filename,
|
||||||
|
ma.visibility AS target_visibility,
|
||||||
|
ma.media_kind AS target_media_kind,
|
||||||
|
ma.legal_hold_active AS target_legal_hold_active,
|
||||||
|
ex.name AS target_exercise_name,
|
||||||
|
rev.name AS reviewed_by_name,
|
||||||
|
asgn.name AS assigned_to_name
|
||||||
|
FROM content_reports cr
|
||||||
|
LEFT JOIN media_assets ma ON ma.id = cr.target_id AND cr.target_type = 'media_asset'
|
||||||
|
LEFT JOIN exercises ex ON ex.id = cr.target_id AND cr.target_type = 'exercise'
|
||||||
|
LEFT JOIN profiles rev ON rev.id = cr.reviewed_by_profile_id
|
||||||
|
LEFT JOIN profiles asgn ON asgn.id = cr.assigned_to_profile_id
|
||||||
|
WHERE cr.target_type = 'media_asset'
|
||||||
|
AND ma.club_id IN (
|
||||||
|
SELECT cm.club_id
|
||||||
|
FROM club_members cm
|
||||||
|
INNER JOIN club_member_roles r ON r.club_member_id = cm.id
|
||||||
|
WHERE cm.profile_id = %s AND cm.status = 'active' AND r.role_code = 'club_admin'
|
||||||
|
)
|
||||||
|
ORDER BY
|
||||||
|
CASE WHEN cr.status = 'submitted' THEN 0
|
||||||
|
WHEN cr.status = 'under_review' THEN 1
|
||||||
|
ELSE 2 END ASC,
|
||||||
|
cr.priority DESC,
|
||||||
|
cr.created_at ASC
|
||||||
|
LIMIT 200
|
||||||
|
""",
|
||||||
|
(pid,),
|
||||||
|
)
|
||||||
|
|
||||||
return [_report_row_to_dict(row) for row in cur.fetchall()]
|
return [_report_row_to_dict(row) for row in cur.fetchall()]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
# Shinkan Jinkendo Version Information
|
# Shinkan Jinkendo Version Information
|
||||||
|
|
||||||
APP_VERSION = "0.8.89"
|
APP_VERSION = "0.8.90"
|
||||||
BUILD_DATE = "2026-05-11"
|
BUILD_DATE = "2026-05-11"
|
||||||
DB_SCHEMA_VERSION = "20260511052"
|
DB_SCHEMA_VERSION = "20260511053"
|
||||||
|
|
||||||
MODULE_VERSIONS = {
|
MODULE_VERSIONS = {
|
||||||
"legal_documents": "1.2.0", # jsPDF-Download auf LegalPage (oeffentlich) + Admin; Abschnitts-Sortierung/-Einfuegen
|
"legal_documents": "1.2.0", # jsPDF-Download auf LegalPage (oeffentlich) + Admin; Abschnitts-Sortierung/-Einfuegen
|
||||||
|
|
@ -14,7 +14,7 @@ MODULE_VERSIONS = {
|
||||||
"club_join_requests": "1.0.1", # Depends(get_tenant_context)
|
"club_join_requests": "1.0.1", # Depends(get_tenant_context)
|
||||||
"admin_users": "1.0.0", # GET /api/admin/users
|
"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)
|
"platform_media_storage": "1.0.0", # GET/PUT /api/admin/platform-media-storage (Superadmin-Pfad unter MEDIA_ROOT)
|
||||||
"media_rights": "1.3.0", # P-11: write_audit_log_entry + legal_hold_set/released events
|
"media_rights": "1.3.1", # acting_profile_id in write_audit_log_entry auf Optional[int] (P-13 anonyme Meldungen)
|
||||||
"media_assets": "1.18.0", # P-11: Legal-Hold nur fuer Superadmin sichtbar (nicht fuer alle Plattform-Admins)
|
"media_assets": "1.18.0", # P-11: Legal-Hold nur fuer Superadmin sichtbar (nicht fuer alle Plattform-Admins)
|
||||||
"media_legal_hold": "1.0.0", # P-11: Sofortsperre-Services (set_legal_hold, release_legal_hold)
|
"media_legal_hold": "1.0.0", # P-11: Sofortsperre-Services (set_legal_hold, release_legal_hold)
|
||||||
"media_lifecycle": "1.1.0", # P-11: Retention-Job ueberspringt Legal-Hold-Assets
|
"media_lifecycle": "1.1.0", # P-11: Retention-Job ueberspringt Legal-Hold-Assets
|
||||||
|
|
@ -30,10 +30,21 @@ MODULE_VERSIONS = {
|
||||||
"membership": "1.0.0",
|
"membership": "1.0.0",
|
||||||
"catalogs": "1.5.0", # Updated: Trainer Contexts API (Migration 012)
|
"catalogs": "1.5.0", # Updated: Trainer Contexts API (Migration 012)
|
||||||
"maturity_models": "1.4.0", # matrix_stack_bundle: vollständiger Katalog+Modelle+Bindings Export/Import
|
"maturity_models": "1.4.0", # matrix_stack_bundle: vollständiger Katalog+Modelle+Bindings Export/Import
|
||||||
"content_reports": "1.2.0", # P-13: Viewer konsolidiert — MediaPreviewModal (geteilt); Melden in ExerciseFormPage-Viewer
|
"content_reports": "1.3.0", # P-13: Club-Admin-Zugriff; Audit-Log; E-Mail (Admin + Melder); target_name-Fix
|
||||||
}
|
}
|
||||||
|
|
||||||
CHANGELOG = [
|
CHANGELOG = [
|
||||||
|
{
|
||||||
|
"version": "0.8.90",
|
||||||
|
"date": "2026-05-11",
|
||||||
|
"changes": [
|
||||||
|
"Fix P-13: Club-Admins sehen jetzt Inhaltsmeldungen ihrer Vereinsmedien in der Inbox (Backend-Filter + Frontend canSeeContentReports).",
|
||||||
|
"Fix P-13: Journaleintrag (content_report_filed) in media_asset_audit_log beim Einreichen einer Meldung (Migration 053).",
|
||||||
|
"Fix P-13: E-Mail-Benachrichtigung an alle Plattform-Admins bei neuer Meldung (best-effort).",
|
||||||
|
"Fix P-13: Bestätigungs-E-Mail an Melder beim Einreichen (best-effort).",
|
||||||
|
"Fix P-13: Medienname (target_filename/target_exercise_name) korrekt in Inbox-Liste angezeigt.",
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"version": "0.8.89",
|
"version": "0.8.89",
|
||||||
"date": "2026-05-11",
|
"date": "2026-05-11",
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,9 @@ export function canAccessOrgInbox(user) {
|
||||||
return activeClubMemberships(user.clubs).some((c) => (c.roles || []).includes('club_admin'))
|
return activeClubMemberships(user.clubs).some((c) => (c.roles || []).includes('club_admin'))
|
||||||
}
|
}
|
||||||
|
|
||||||
function isPlatformAdmin(user) {
|
function canSeeContentReports(user) {
|
||||||
return user?.role === 'admin' || user?.role === 'superadmin'
|
if (user?.role === 'admin' || user?.role === 'superadmin') return true
|
||||||
|
return activeClubMemberships(user?.clubs || []).some((c) => (c.roles || []).includes('club_admin'))
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Nach Annahme/Ablehnung: Posteingang-Badges & Widget aktualisieren */
|
/** Nach Annahme/Ablehnung: Posteingang-Badges & Widget aktualisieren */
|
||||||
|
|
@ -30,7 +31,7 @@ export function OrgInboxProvider({ user, children }) {
|
||||||
const [items, setItems] = useState([])
|
const [items, setItems] = useState([])
|
||||||
const [contentReports, setContentReports] = useState([])
|
const [contentReports, setContentReports] = useState([])
|
||||||
const canAccess = useMemo(() => canAccessOrgInbox(user), [user])
|
const canAccess = useMemo(() => canAccessOrgInbox(user), [user])
|
||||||
const canAccessReports = useMemo(() => isPlatformAdmin(user), [user])
|
const canAccessReports = useMemo(() => canSeeContentReports(user), [user])
|
||||||
|
|
||||||
const refresh = useCallback(async () => {
|
const refresh = useCallback(async () => {
|
||||||
if (!canAccess) {
|
if (!canAccess) {
|
||||||
|
|
|
||||||
|
|
@ -512,7 +512,7 @@ export default function InboxPage() {
|
||||||
</span>
|
</span>
|
||||||
<span className="muted" style={{ marginLeft: '0.75rem', fontSize: '0.85rem' }}>
|
<span className="muted" style={{ marginLeft: '0.75rem', fontSize: '0.85rem' }}>
|
||||||
{rep.target_type === 'media_asset' ? 'Medium' : 'Übung'} #{rep.target_id}
|
{rep.target_type === 'media_asset' ? 'Medium' : 'Übung'} #{rep.target_id}
|
||||||
{rep.target_name ? ` – ${rep.target_name}` : ''}
|
{rep.target_filename || rep.target_exercise_name ? ` – ${rep.target_filename || rep.target_exercise_name}` : ''}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
// Shinkan Jinkendo Frontend Version
|
// Shinkan Jinkendo Frontend Version
|
||||||
|
|
||||||
export const APP_VERSION = "0.8.89"
|
export const APP_VERSION = "0.8.90"
|
||||||
export const BUILD_DATE = "2026-05-11"
|
export const BUILD_DATE = "2026-05-11"
|
||||||
|
|
||||||
export const PAGE_VERSIONS = {
|
export const PAGE_VERSIONS = {
|
||||||
|
|
@ -27,7 +27,8 @@ export const PAGE_VERSIONS = {
|
||||||
ExerciseMediaEmbed: "1.1.0", // P-11: Legal-Hold-Placeholder statt Datei
|
ExerciseMediaEmbed: "1.1.0", // P-11: Legal-Hold-Placeholder statt Datei
|
||||||
ExerciseMediaThumbTile: "1.1.0", // P-11: Legal-Hold-Kachel statt Datei-Vorschau
|
ExerciseMediaThumbTile: "1.1.0", // P-11: Legal-Hold-Kachel statt Datei-Vorschau
|
||||||
ExerciseAttachmentMediaStrip: "1.2.0", // P-13: MediaPreviewModal (geteilt) + Melden im Viewer
|
ExerciseAttachmentMediaStrip: "1.2.0", // P-13: MediaPreviewModal (geteilt) + Melden im Viewer
|
||||||
InboxPage: "2.0.0", // P-13: Inhaltsmeldungen-Abschnitt integriert
|
InboxPage: "2.1.0", // P-13: target_filename/target_exercise_name fix; Club-Admin-Sicht
|
||||||
|
OrgInboxContext: "1.1.0", // P-13: canSeeContentReports schliesst Club-Admins ein
|
||||||
MediaPreviewModal: "1.0.0", // P-13: geteilter Medienvorschau-Dialog (Melden + Bearbeiten optional)
|
MediaPreviewModal: "1.0.0", // P-13: geteilter Medienvorschau-Dialog (Melden + Bearbeiten optional)
|
||||||
ReportContentModal: "1.0.0", // P-13: Melde-Formular (Grund, Beschreibung, Name, E-Mail)
|
ReportContentModal: "1.0.0", // P-13: Melde-Formular (Grund, Beschreibung, Name, E-Mail)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user