### Security-Fixes (K1–K6, W1–W4, W7, N1, N5–N6, V1, V5–V7)
- K1: CSRF-Schutz via Double-Submit-Cookie (httpapi/csrf.go + csrf_helpers.go)
- K2: requireScreenAccess() in allen manage-Handlern (Tenant-Isolation)
- K3: Tenant-Check bei DELETE /api/v1/media/{id}
- K4: requirePlaylistAccess() + GetByItemID() für JSON-API Playlist-Routen
- K5: Admin-Passwort nur noch als [gesetzt] geloggt
- K6: POST /api/v1/screens/register mit Pre-Shared-Secret (MORZ_INFOBOARD_REGISTER_SECRET)
- W1: Race Condition bei order_index behoben (atomare Subquery in AddItem)
- W2: Graceful Shutdown mit 15s Timeout auf SIGTERM/SIGINT
- W3: http.MaxBytesReader (512 MB) in allen Upload-Handlern
- W4: err.Error() nicht mehr an den Client
- W7: Template-Execution via bytes.Buffer (kein partial write bei Fehler)
- N1: Rate-Limiting auf /login (5 Versuche/Minute pro IP, httpapi/ratelimit.go)
- N5: Directory-Listing auf /uploads/ deaktiviert (neuteredFileSystem)
- N6: Uploads nach Tenant getrennt (uploads/{tenantSlug}/)
- V1: Upload-Logik konsolidiert in internal/fileutil/fileutil.go
- V5: Cookie-Name als Konstante reqcontext.SessionCookieName
- V6: Strukturiertes Logging mit log/slog + JSON-Handler
- V7: DB-Pool wird im Graceful-Shutdown geschlossen
### Phase 6: Screenshot-Erzeugung
- player/agent/internal/screenshot/screenshot.go erstellt
- Integration in app.go mit MORZ_INFOBOARD_SCREENSHOT_EVERY Config
### UX: PDF.js Integration
- pdf.min.js + pdf.worker.min.js als lokale Assets eingebettet
- Automatisches Seitendurchblättern im Player
### Ansible: Neue Rollen
- signage_base, signage_server, signage_provision erstellt
- inventory.yml und site.yml erweitert
### Konzept-Docs
- GRUPPEN-KONZEPT.md, KAMPAGNEN-AKTIVIERUNG.md, MONITORING-KONZEPT.md
- PROVISION-KONZEPT.md, TEMPLATE-EDITOR.md, WATCHDOG-KONZEPT.md
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
68 lines
1.9 KiB
Go
68 lines
1.9 KiB
Go
// Package fileutil enthält gemeinsame Datei-Hilfsfunktionen für Upload-Handler (V1, N6).
|
||
package fileutil
|
||
|
||
import (
|
||
"fmt"
|
||
"io"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
"time"
|
||
)
|
||
|
||
// SaveUploadedFile speichert einen Datei-Stream in uploadDir/{tenantSlug}/ und
|
||
// gibt den relativen HTTP-Pfad (/uploads/{tenantSlug}/filename) sowie die
|
||
// Anzahl geschriebener Bytes zurück.
|
||
//
|
||
// V1: Gemeinsame Upload-Logik — ersetzt 3× duplizierte Implementierung.
|
||
// N6: Tenant-spezifisches Verzeichnis statt gemeinsamer Ablage.
|
||
func SaveUploadedFile(file io.Reader, originalFilename, title, uploadDir, tenantSlug string) (storagePath string, size int64, err error) {
|
||
safeSlug := sanitize(tenantSlug)
|
||
if safeSlug == "" {
|
||
safeSlug = "default"
|
||
}
|
||
tenantDir := filepath.Join(uploadDir, safeSlug)
|
||
if mkErr := os.MkdirAll(tenantDir, 0755); mkErr != nil {
|
||
return "", 0, fmt.Errorf("fileutil: mkdir %s: %w", tenantDir, mkErr)
|
||
}
|
||
|
||
ext := filepath.Ext(originalFilename)
|
||
safeTitle := sanitize(title)
|
||
if safeTitle == "" {
|
||
safeTitle = "file"
|
||
}
|
||
filename := fmt.Sprintf("%d_%s%s", time.Now().UnixNano(), safeTitle, ext)
|
||
destPath := filepath.Join(tenantDir, filename)
|
||
|
||
dest, createErr := os.Create(destPath)
|
||
if createErr != nil {
|
||
return "", 0, fmt.Errorf("fileutil: create %s: %w", destPath, createErr)
|
||
}
|
||
defer dest.Close()
|
||
|
||
n, copyErr := io.Copy(dest, file)
|
||
if copyErr != nil {
|
||
os.Remove(destPath) //nolint:errcheck
|
||
return "", 0, fmt.Errorf("fileutil: write %s: %w", destPath, copyErr)
|
||
}
|
||
|
||
return "/uploads/" + safeSlug + "/" + filename, n, nil
|
||
}
|
||
|
||
// sanitize konvertiert einen String in einen sicheren Dateinamen-Bestandteil
|
||
// (nur a-z, A-Z, 0-9, -, _; maximal 40 Zeichen).
|
||
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
|
||
}
|