package manage import ( "encoding/json" "net/http" "os" "path/filepath" "strings" "git.az-it.net/az/morz-infoboard/server/backend/internal/fileutil" "git.az-it.net/az/morz-infoboard/server/backend/internal/reqcontext" "git.az-it.net/az/morz-infoboard/server/backend/internal/store" ) const maxUploadSize = 512 << 20 // 512 MB // HandleListMedia returns all media assets for a tenant as JSON. // restricted-Users sehen nur ihre eigenen Medien. func HandleListMedia(tenants *store.TenantStore, media *store.MediaStore) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { tenant, err := tenants.Get(r.Context(), r.PathValue("tenantSlug")) if err != nil { http.Error(w, "tenant not found", http.StatusNotFound) return } u := reqcontext.UserFromContext(r.Context()) ownerUserID := "" if u != nil && u.Role == "restricted" { ownerUserID = u.ID } assets, err := media.List(r.Context(), tenant.ID, ownerUserID) if err != nil { http.Error(w, "db error", http.StatusInternalServerError) return } if assets == nil { assets = []*store.MediaAsset{} } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(assets) //nolint:errcheck } } // HandleUploadMedia handles multipart file upload and web-URL registration. func HandleUploadMedia(tenants *store.TenantStore, media *store.MediaStore, uploadDir string) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { tenant, err := tenants.Get(r.Context(), r.PathValue("tenantSlug")) if err != nil { http.Error(w, "tenant not found", http.StatusNotFound) return } tenantID := tenant.ID u := reqcontext.UserFromContext(r.Context()) createdByUserID := "" if u != nil { createdByUserID = u.ID } // W3: MaxBytesReader begrenzt den gesamten Request-Body auf maxUploadSize. r.Body = http.MaxBytesReader(w, r.Body, maxUploadSize) if err := r.ParseMultipartForm(maxUploadSize); err != nil { http.Error(w, "request too large or not multipart", http.StatusBadRequest) return } assetType := strings.TrimSpace(r.FormValue("type")) title := strings.TrimSpace(r.FormValue("title")) switch assetType { case "web": url := strings.TrimSpace(r.FormValue("url")) if url == "" { http.Error(w, "url required for type=web", http.StatusBadRequest) return } if title == "" { title = url } asset, err := media.Create(r.Context(), tenantID, title, "web", "", url, "", createdByUserID, 0) if err != nil { http.Error(w, "db error", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(asset) //nolint:errcheck return case "image", "video", "pdf": file, header, err := r.FormFile("file") if err != nil { http.Error(w, "file required for type="+assetType, http.StatusBadRequest) return } defer file.Close() mimeType := header.Header.Get("Content-Type") if detectedType := mimeToAssetType(mimeType); detectedType != "" { assetType = detectedType } if title == "" { title = strings.TrimSuffix(header.Filename, filepath.Ext(header.Filename)) } // V1+N6: Gemeinsame Upload-Funktion, tenant-spezifisches Verzeichnis. storagePath, size, err := fileutil.SaveUploadedFile(file, header.Filename, title, uploadDir, r.PathValue("tenantSlug")) if err != nil { http.Error(w, "storage error", http.StatusInternalServerError) return } asset, err := media.Create(r.Context(), tenantID, title, assetType, storagePath, "", mimeType, createdByUserID, size) if err != nil { http.Error(w, "db error", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(asset) //nolint:errcheck default: http.Error(w, "type must be one of: image, video, pdf, web", http.StatusBadRequest) } } } // HandleDeleteMedia deletes a media asset from the database (and file if local). func HandleDeleteMedia(media *store.MediaStore, uploadDir string) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") asset, err := media.Get(r.Context(), id) if err != nil { http.Error(w, "not found", http.StatusNotFound) return } // K3: Rolle-bewusste Berechtigungsprüfung. u := reqcontext.UserFromContext(r.Context()) if u == nil || !canDeleteMedia(u, asset) { http.Error(w, "Forbidden", http.StatusForbidden) return } // Delete physical file if it's a local upload. if asset.StoragePath != "" { filename := filepath.Base(asset.StoragePath) os.Remove(filepath.Join(uploadDir, filename)) //nolint:errcheck } if err := media.Delete(r.Context(), id); err != nil { http.Error(w, "db error", http.StatusInternalServerError) return } w.WriteHeader(http.StatusNoContent) } } // mimeToAssetType leitet den Asset-Typ aus dem MIME-Type ab. func mimeToAssetType(mime string) string { mime = strings.ToLower(strings.TrimSpace(mime)) switch { case strings.HasPrefix(mime, "image/"): return "image" case strings.HasPrefix(mime, "video/"): return "video" case mime == "application/pdf": return "pdf" default: return "" } } func sanitize(s string) string { var b strings.Builder for _, r := range s { if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '-' || r == '_' { b.WriteRune(r) } else { b.WriteRune('_') } } out := b.String() if len(out) > 40 { out = out[:40] } return out } // canDeleteMedia prüft ob User u das Medium asset löschen darf. // admin: immer erlaubt // screen_user: Tenant-Match reicht // restricted: Tenant-Match UND Besitzer-Match func canDeleteMedia(u *store.User, asset *store.MediaAsset) bool { if u.Role == "admin" { return true } if u.TenantID != asset.TenantID { return false } if u.Role == "restricted" { return asset.CreatedByUserID == u.ID } return true }