package api import ( "encoding/json" "net/http" "strconv" "github.com/archivmail/internal/audit" "github.com/archivmail/internal/ldapauth" ldapcfg "github.com/archivmail/internal/ldapconfig" "github.com/archivmail/internal/tenantstore" "github.com/archivmail/internal/userstore" ) // ── Server extension fields and wiring ────────────────────────────────────── // ldapStore and tenantStore are added to the Server struct via SetLDAP / SetTenants. // They are declared separately from server.go to keep that file unmodified. // Access is via the embedded pointer fields on *Server. // SetLDAP wires the LDAP config store into the API server. func (s *Server) SetLDAP(store *ldapcfg.Store) { s.ldapStore = store // Register LDAP routes only after the store is available. s.mux.HandleFunc("GET /api/admin/ldap", s.authMiddleware(s.requireRole(userstore.RoleAdmin, s.handleGetLDAP))) s.mux.HandleFunc("PUT /api/admin/ldap", s.authMiddleware(s.requireRole(userstore.RoleAdmin, s.handleSaveLDAP))) s.mux.HandleFunc("DELETE /api/admin/ldap", s.authMiddleware(s.requireRole(userstore.RoleAdmin, s.handleDeleteLDAP))) s.mux.HandleFunc("POST /api/admin/ldap/test", s.authMiddleware(s.requireRole(userstore.RoleAdmin, s.handleTestLDAP))) } // SetTenants wires the tenant store into the API server. func (s *Server) SetTenants(store *tenantstore.Store) { s.tenantStore = store // Register tenant routes only after the store is available. s.mux.HandleFunc("GET /api/tenants", s.authMiddleware(s.requireRole(userstore.RoleAdmin, s.handleListTenants))) s.mux.HandleFunc("POST /api/tenants", s.authMiddleware(s.requireRole(userstore.RoleAdmin, s.handleCreateTenant))) s.mux.HandleFunc("GET /api/tenants/{id}", s.authMiddleware(s.requireRole(userstore.RoleAdmin, s.handleGetTenant))) s.mux.HandleFunc("PATCH /api/tenants/{id}", s.authMiddleware(s.requireRole(userstore.RoleAdmin, s.handleUpdateTenant))) s.mux.HandleFunc("DELETE /api/tenants/{id}", s.authMiddleware(s.requireRole(userstore.RoleAdmin, s.handleDeleteTenant))) s.mux.HandleFunc("GET /api/tenants/{id}/domains", s.authMiddleware(s.requireRole(userstore.RoleAdmin, s.handleListTenantDomains))) s.mux.HandleFunc("POST /api/tenants/{id}/domains", s.authMiddleware(s.requireRole(userstore.RoleAdmin, s.handleAddTenantDomain))) s.mux.HandleFunc("DELETE /api/tenants/{id}/domains/{did}", s.authMiddleware(s.requireRole(userstore.RoleAdmin, s.handleRemoveTenantDomain))) } // ── LDAP handlers ──────────────────────────────────────────────────────────── func (s *Server) handleGetLDAP(w http.ResponseWriter, r *http.Request) { if s.ldapStore == nil { writeError(w, http.StatusServiceUnavailable, "ldap store not available") return } cfg, err := s.ldapStore.Get(r.Context()) if err != nil { writeError(w, http.StatusInternalServerError, "failed to load ldap config") return } if cfg == nil { writeError(w, http.StatusNotFound, "no ldap config") return } writeJSON(w, http.StatusOK, cfg) } func (s *Server) handleSaveLDAP(w http.ResponseWriter, r *http.Request) { if s.ldapStore == nil { writeError(w, http.StatusServiceUnavailable, "ldap store not available") return } var cfg ldapcfg.LDAPConfig if err := json.NewDecoder(r.Body).Decode(&cfg); err != nil { writeError(w, http.StatusBadRequest, "invalid request body") return } sess := sessionFromCtx(r.Context()) if err := s.ldapStore.Save(r.Context(), cfg, sess.Username); err != nil { writeError(w, http.StatusInternalServerError, "failed to save ldap config") return } s.audlog.Log(audit.Entry{ EventType: "ldap_config_saved", Username: sess.Username, IPAddress: remoteIP(r), Success: true, Detail: "LDAP-Konfiguration gespeichert", }) // Return the saved config (with masked password) saved, err := s.ldapStore.Get(r.Context()) if err != nil || saved == nil { writeJSON(w, http.StatusOK, map[string]bool{"ok": true}) return } writeJSON(w, http.StatusOK, saved) } func (s *Server) handleDeleteLDAP(w http.ResponseWriter, r *http.Request) { if s.ldapStore == nil { writeError(w, http.StatusServiceUnavailable, "ldap store not available") return } if err := s.ldapStore.Delete(r.Context()); err != nil { writeError(w, http.StatusInternalServerError, "failed to delete ldap config") return } sess := sessionFromCtx(r.Context()) s.audlog.Log(audit.Entry{ EventType: "ldap_config_deleted", Username: sess.Username, IPAddress: remoteIP(r), Success: true, Detail: "LDAP-Konfiguration gelöscht", }) w.WriteHeader(http.StatusNoContent) } func (s *Server) handleTestLDAP(w http.ResponseWriter, r *http.Request) { if s.ldapStore == nil { writeError(w, http.StatusServiceUnavailable, "ldap store not available") return } var body struct { UseSaved bool `json:"use_saved"` ldapcfg.LDAPConfig } if err := json.NewDecoder(r.Body).Decode(&body); err != nil { writeError(w, http.StatusBadRequest, "invalid request body") return } var testCfg ldapauth.Config if body.UseSaved { saved, err := s.ldapStore.GetWithPassword(r.Context()) if err != nil || saved == nil { writeError(w, http.StatusNotFound, "no ldap config saved") return } testCfg = ldapauth.Config{ URL: saved.URL, BindDN: saved.BindDN, BindPassword: saved.BindPassword, BaseDN: saved.BaseDN, UserFilter: saved.UserFilter, TLS: saved.TLS, TLSSkipVerify: saved.TLSSkipVerify, } } else { testCfg = ldapauth.Config{ URL: body.URL, BindDN: body.BindDN, BindPassword: body.BindPassword, BaseDN: body.BaseDN, UserFilter: body.UserFilter, TLS: body.TLS, TLSSkipVerify: body.TLSSkipVerify, } } result := ldapauth.TestConnection(testCfg) sess := sessionFromCtx(r.Context()) s.audlog.Log(audit.Entry{ EventType: "ldap_connection_test", Username: sess.Username, IPAddress: remoteIP(r), Success: result.OK, Detail: result.Message, }) writeJSON(w, http.StatusOK, result) } // ── Tenant handlers ────────────────────────────────────────────────────────── func (s *Server) handleListTenants(w http.ResponseWriter, r *http.Request) { if s.tenantStore == nil { writeError(w, http.StatusServiceUnavailable, "tenant store not available") return } tenants, err := s.tenantStore.List(r.Context()) if err != nil { writeError(w, http.StatusInternalServerError, "failed to list tenants") return } writeJSON(w, http.StatusOK, tenants) } func (s *Server) handleCreateTenant(w http.ResponseWriter, r *http.Request) { if s.tenantStore == nil { writeError(w, http.StatusServiceUnavailable, "tenant store not available") return } var req struct { Name string `json:"name"` Slug string `json:"slug"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid request body") return } if req.Name == "" || req.Slug == "" { writeError(w, http.StatusBadRequest, "name and slug are required") return } tenant, err := s.tenantStore.Create(r.Context(), req.Name, req.Slug) if err != nil { writeError(w, http.StatusInternalServerError, "failed to create tenant") return } sess := sessionFromCtx(r.Context()) s.audlog.Log(audit.Entry{ EventType: "tenant_created", Username: sess.Username, IPAddress: remoteIP(r), Success: true, Detail: "Mandant erstellt: " + req.Name, }) writeJSON(w, http.StatusCreated, tenant) } func (s *Server) handleGetTenant(w http.ResponseWriter, r *http.Request) { if s.tenantStore == nil { writeError(w, http.StatusServiceUnavailable, "tenant store not available") return } id, err := parseTenantID(r) if err != nil { writeError(w, http.StatusBadRequest, "invalid tenant id") return } tenant, err := s.tenantStore.Get(r.Context(), id) if err != nil { writeError(w, http.StatusNotFound, "tenant not found") return } writeJSON(w, http.StatusOK, tenant) } func (s *Server) handleUpdateTenant(w http.ResponseWriter, r *http.Request) { if s.tenantStore == nil { writeError(w, http.StatusServiceUnavailable, "tenant store not available") return } id, err := parseTenantID(r) if err != nil { writeError(w, http.StatusBadRequest, "invalid tenant id") return } var req struct { Name string `json:"name"` Active *bool `json:"active"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid request body") return } // Load existing to keep unset fields existing, err := s.tenantStore.Get(r.Context(), id) if err != nil { writeError(w, http.StatusNotFound, "tenant not found") return } name := existing.Name active := existing.Active if req.Name != "" { name = req.Name } if req.Active != nil { active = *req.Active } tenant, err := s.tenantStore.Update(r.Context(), id, name, active) if err != nil { writeError(w, http.StatusInternalServerError, "failed to update tenant") return } writeJSON(w, http.StatusOK, tenant) } func (s *Server) handleDeleteTenant(w http.ResponseWriter, r *http.Request) { if s.tenantStore == nil { writeError(w, http.StatusServiceUnavailable, "tenant store not available") return } id, err := parseTenantID(r) if err != nil { writeError(w, http.StatusBadRequest, "invalid tenant id") return } sess := sessionFromCtx(r.Context()) if err := s.tenantStore.Delete(r.Context(), id); err != nil { writeError(w, http.StatusInternalServerError, "failed to delete tenant") return } s.audlog.Log(audit.Entry{ EventType: "tenant_deleted", Username: sess.Username, IPAddress: remoteIP(r), Success: true, Detail: "Mandant gelöscht: " + strconv.FormatInt(id, 10), }) w.WriteHeader(http.StatusNoContent) } func (s *Server) handleListTenantDomains(w http.ResponseWriter, r *http.Request) { if s.tenantStore == nil { writeError(w, http.StatusServiceUnavailable, "tenant store not available") return } id, err := parseTenantID(r) if err != nil { writeError(w, http.StatusBadRequest, "invalid tenant id") return } domains, err := s.tenantStore.ListDomains(r.Context(), id) if err != nil { writeError(w, http.StatusInternalServerError, "failed to list domains") return } writeJSON(w, http.StatusOK, domains) } func (s *Server) handleAddTenantDomain(w http.ResponseWriter, r *http.Request) { if s.tenantStore == nil { writeError(w, http.StatusServiceUnavailable, "tenant store not available") return } id, err := parseTenantID(r) if err != nil { writeError(w, http.StatusBadRequest, "invalid tenant id") return } var req struct { Domain string `json:"domain"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.Domain == "" { writeError(w, http.StatusBadRequest, "domain is required") return } domain, err := s.tenantStore.AddDomain(r.Context(), id, req.Domain) if err != nil { writeError(w, http.StatusInternalServerError, "failed to add domain") return } writeJSON(w, http.StatusCreated, domain) } func (s *Server) handleRemoveTenantDomain(w http.ResponseWriter, r *http.Request) { if s.tenantStore == nil { writeError(w, http.StatusServiceUnavailable, "tenant store not available") return } tenantID, err := parseTenantID(r) if err != nil { writeError(w, http.StatusBadRequest, "invalid tenant id") return } didStr := r.PathValue("did") domainID, err := strconv.ParseInt(didStr, 10, 64) if err != nil { writeError(w, http.StatusBadRequest, "invalid domain id") return } if err := s.tenantStore.RemoveDomain(r.Context(), tenantID, domainID); err != nil { writeError(w, http.StatusInternalServerError, "failed to remove domain") return } w.WriteHeader(http.StatusNoContent) } // ── helpers ────────────────────────────────────────────────────────────────── func parseTenantID(r *http.Request) (int64, error) { return strconv.ParseInt(r.PathValue("id"), 10, 64) }