FDN-01: repository & projektgerüst
Git-Repository für bestehenden archivdms-Code initialisiert, Branch-/Commit-Konvention (feature/<ticket>-<slug>-Branches, Ticket-Prefix in Commit-Nachricht) etabliert.
This commit is contained in:
@@ -0,0 +1,233 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"archivdms/internal/audit"
|
||||
"archivdms/internal/dateformat"
|
||||
"archivdms/internal/storage"
|
||||
"archivdms/internal/userstore"
|
||||
)
|
||||
|
||||
// resolveTenantSettingsTenant determines which tenant the request targets,
|
||||
// mirroring resolveLDAPTenant: domain_admin is pinned to its own signed
|
||||
// session tenant (IDOR-safe, query params ignored); superadmin (no session
|
||||
// tenant) must pass ?tenant_id=. Returns the tenant ID and false when the
|
||||
// request is not authorised or the tenant cannot be determined (the caller
|
||||
// has already written the response).
|
||||
func (s *Server) resolveTenantSettingsTenant(w http.ResponseWriter, r *http.Request) (int64, bool) {
|
||||
sess := sessionFromCtx(r.Context())
|
||||
|
||||
if sess.Role == userstore.RoleSuperAdmin {
|
||||
q := r.URL.Query().Get("tenant_id")
|
||||
if q == "" {
|
||||
writeError(w, http.StatusBadRequest, "tenant_id query parameter required for superadmin")
|
||||
return 0, false
|
||||
}
|
||||
tid, err := strconv.ParseInt(q, 10, 64)
|
||||
if err != nil || tid <= 0 {
|
||||
writeError(w, http.StatusBadRequest, "invalid tenant_id")
|
||||
return 0, false
|
||||
}
|
||||
return tid, true
|
||||
}
|
||||
|
||||
if sess.TenantID == nil {
|
||||
writeError(w, http.StatusForbidden, "no tenant context")
|
||||
return 0, false
|
||||
}
|
||||
return *sess.TenantID, true
|
||||
}
|
||||
|
||||
// availableScanTitleFormats returns the example token patterns the frontend can
|
||||
// offer as clickable starting points. These are suggestions only — any free
|
||||
// token pattern is accepted (see dateformat.Translate) — not a validation
|
||||
// constraint. Returned as a fresh slice so callers never share backing storage.
|
||||
func availableScanTitleFormats() []string {
|
||||
out := make([]string, len(exampleScanTitleFormats))
|
||||
copy(out, exampleScanTitleFormats)
|
||||
return out
|
||||
}
|
||||
|
||||
type tenantSettingsResponse struct {
|
||||
ScanTitleDateFormat string `json:"scan_title_date_format"`
|
||||
ScanTitlePrefix string `json:"scan_title_prefix"`
|
||||
// DefaultTitleTemplate is the tenant-wide fallback title template (Go
|
||||
// text/template) used when an applied classification template carries no
|
||||
// own title_template. Empty string means "no tenant default".
|
||||
DefaultTitleTemplate string `json:"default_title_template"`
|
||||
// AvailableFormats is a list of example patterns (suggestions), not an
|
||||
// allow-list; the field name is kept for frontend compatibility.
|
||||
AvailableFormats []string `json:"available_formats"`
|
||||
}
|
||||
|
||||
// effectiveScanTitlePrefix returns the tenant's stored prefix, or the default
|
||||
// for empty/legacy rows, so the response always reflects what would actually be
|
||||
// used at upload time.
|
||||
func effectiveScanTitlePrefix(stored string) string {
|
||||
if p := strings.TrimSpace(stored); p != "" {
|
||||
return p
|
||||
}
|
||||
return defaultScanTitlePrefix
|
||||
}
|
||||
|
||||
// effectiveScanTitleFormat returns the tenant's stored token pattern, or the
|
||||
// default for empty/invalid rows, so the response always reflects the pattern
|
||||
// that would actually be used at upload time.
|
||||
func effectiveScanTitleFormat(stored string) string {
|
||||
if s := strings.TrimSpace(stored); s != "" {
|
||||
if _, err := dateformat.Translate(s); err == nil {
|
||||
return s
|
||||
}
|
||||
}
|
||||
return defaultScanTitleDateFormat
|
||||
}
|
||||
|
||||
// handleGetTenantSettings returns the tenant's own settings (currently the
|
||||
// placeholder-title date format) plus a list of example format patterns
|
||||
// (suggestions for the frontend, not an allow-list).
|
||||
func (s *Server) handleGetTenantSettings(w http.ResponseWriter, r *http.Request) {
|
||||
if s.tenantStore == nil {
|
||||
writeError(w, http.StatusServiceUnavailable, "tenant store not configured")
|
||||
return
|
||||
}
|
||||
tenantID, ok := s.resolveTenantSettingsTenant(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
t, err := s.tenantStore.GetByID(r.Context(), tenantID)
|
||||
if err != nil || t == nil {
|
||||
writeError(w, http.StatusInternalServerError, "load tenant settings failed")
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, tenantSettingsResponse{
|
||||
ScanTitleDateFormat: effectiveScanTitleFormat(t.ScanTitleDateFormat),
|
||||
ScanTitlePrefix: effectiveScanTitlePrefix(t.ScanTitlePrefix),
|
||||
DefaultTitleTemplate: t.DefaultTitleTemplate,
|
||||
AvailableFormats: availableScanTitleFormats(),
|
||||
})
|
||||
}
|
||||
|
||||
// updateTenantSettingsRequest uses pointer fields for PATCH-like semantics:
|
||||
// only the fields present in the JSON body are updated, so the two settings
|
||||
// can be changed independently of one another.
|
||||
type updateTenantSettingsRequest struct {
|
||||
ScanTitleDateFormat *string `json:"scan_title_date_format"`
|
||||
ScanTitlePrefix *string `json:"scan_title_prefix"`
|
||||
DefaultTitleTemplate *string `json:"default_title_template"`
|
||||
}
|
||||
|
||||
// handleUpdateTenantSettings persists the tenant's placeholder-title date
|
||||
// format. The submitted value is a free token pattern (e.g. "DD.MM.YYYY HH:mm")
|
||||
// validated via dateformat.Translate; an unparseable pattern yields 400 with
|
||||
// the translator's error message. Every attempt — success or failure — is
|
||||
// audit-logged (GoBD).
|
||||
func (s *Server) handleUpdateTenantSettings(w http.ResponseWriter, r *http.Request) {
|
||||
sess := sessionFromCtx(r.Context())
|
||||
|
||||
if s.tenantStore == nil {
|
||||
writeError(w, http.StatusServiceUnavailable, "tenant store not configured")
|
||||
return
|
||||
}
|
||||
tenantID, ok := s.resolveTenantSettingsTenant(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
logFail := func(detail string) {
|
||||
tid := tenantID
|
||||
s.audlog.Log(audit.Entry{
|
||||
EventType: audit.EventTenantMgmt, Username: sess.Username,
|
||||
TenantID: &tid, Success: false, Detail: detail,
|
||||
})
|
||||
}
|
||||
|
||||
var req updateTenantSettingsRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
logFail("tenant_settings invalid_body")
|
||||
writeError(w, http.StatusBadRequest, "invalid request body")
|
||||
return
|
||||
}
|
||||
if req.ScanTitleDateFormat == nil && req.ScanTitlePrefix == nil && req.DefaultTitleTemplate == nil {
|
||||
logFail("tenant_settings no_fields")
|
||||
writeError(w, http.StatusBadRequest, "no settings provided")
|
||||
return
|
||||
}
|
||||
|
||||
// Validate everything before persisting anything, so a bad prefix can never
|
||||
// leave a half-applied update.
|
||||
if req.ScanTitleDateFormat != nil {
|
||||
if _, err := dateformat.Translate(*req.ScanTitleDateFormat); err != nil {
|
||||
logFail("tenant_settings invalid_scan_title_date_format:" + *req.ScanTitleDateFormat)
|
||||
writeError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
if req.ScanTitlePrefix != nil && strings.TrimSpace(*req.ScanTitlePrefix) == "" {
|
||||
logFail("tenant_settings empty_scan_title_prefix")
|
||||
writeError(w, http.StatusBadRequest, "scan_title_prefix must not be empty")
|
||||
return
|
||||
}
|
||||
// An empty default_title_template is allowed (clears the tenant default);
|
||||
// a non-empty one must parse as a valid Go text/template.
|
||||
if req.DefaultTitleTemplate != nil {
|
||||
if err := storage.ValidateTitleTemplate(*req.DefaultTitleTemplate); err != nil {
|
||||
logFail("tenant_settings invalid_default_title_template")
|
||||
writeError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var detail string
|
||||
if req.ScanTitleDateFormat != nil {
|
||||
if err := s.tenantStore.UpdateScanTitleDateFormat(r.Context(), tenantID, *req.ScanTitleDateFormat); err != nil {
|
||||
logFail("tenant_settings update_failed")
|
||||
writeError(w, http.StatusInternalServerError, "save tenant settings failed")
|
||||
return
|
||||
}
|
||||
detail += " scan_title_date_format:" + *req.ScanTitleDateFormat
|
||||
}
|
||||
if req.ScanTitlePrefix != nil {
|
||||
if err := s.tenantStore.UpdateScanTitlePrefix(r.Context(), tenantID, *req.ScanTitlePrefix); err != nil {
|
||||
// Store-level validation (length) or DB error.
|
||||
logFail("tenant_settings update_failed")
|
||||
writeError(w, http.StatusBadRequest, "invalid scan_title_prefix")
|
||||
return
|
||||
}
|
||||
detail += " scan_title_prefix:" + strings.TrimSpace(*req.ScanTitlePrefix)
|
||||
}
|
||||
if req.DefaultTitleTemplate != nil {
|
||||
if err := s.tenantStore.UpdateDefaultTitleTemplate(r.Context(), tenantID, *req.DefaultTitleTemplate); err != nil {
|
||||
logFail("tenant_settings update_failed")
|
||||
writeError(w, http.StatusBadRequest, "invalid default_title_template")
|
||||
return
|
||||
}
|
||||
detail += " default_title_template:" + strings.TrimSpace(*req.DefaultTitleTemplate)
|
||||
}
|
||||
|
||||
tid := tenantID
|
||||
s.audlog.Log(audit.Entry{
|
||||
EventType: audit.EventTenantMgmt, Username: sess.Username,
|
||||
TenantID: &tid, Success: true,
|
||||
Detail: "tenant_settings" + detail,
|
||||
})
|
||||
|
||||
// Reload so the response reflects the effective persisted state for both
|
||||
// fields, regardless of which one this request changed.
|
||||
t, err := s.tenantStore.GetByID(r.Context(), tenantID)
|
||||
if err != nil || t == nil {
|
||||
writeError(w, http.StatusInternalServerError, "load tenant settings failed")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, tenantSettingsResponse{
|
||||
ScanTitleDateFormat: effectiveScanTitleFormat(t.ScanTitleDateFormat),
|
||||
ScanTitlePrefix: effectiveScanTitlePrefix(t.ScanTitlePrefix),
|
||||
DefaultTitleTemplate: t.DefaultTitleTemplate,
|
||||
AvailableFormats: availableScanTitleFormats(),
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user