chore: weitere Code-Aufteilung (api.ts, hooks, ldap_sync)
- src/lib/api.ts (1085 Zeilen) → 5 thematische Module unter src/lib/api/ (core, users, ldap, tenants, mail, system) + index.ts Re-Export - useLDAPConfig / useTenantLDAPConfig / useTenantUsers Hooks extrahiert; admin/page.tsx nutzt diese statt roher useState-Blöcke - handleSyncTenantLDAP, handleAdminSyncTenantLDAP, doSyncTenantLDAP, buildTenantTestConfig, syncResult aus ldap_tenants.go in ldap_sync.go verschoben Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,134 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/archivmail/internal/audit"
|
||||||
|
"github.com/archivmail/internal/ldapauth"
|
||||||
|
ldapcfg "github.com/archivmail/internal/ldapconfig"
|
||||||
|
)
|
||||||
|
|
||||||
|
// syncResult is the JSON response returned by LDAP sync endpoints.
|
||||||
|
type syncResult struct {
|
||||||
|
Synced int `json:"synced"`
|
||||||
|
Errors []string `json:"errors"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleSyncTenantLDAP imports all LDAP users into the tenant (domain_admin).
|
||||||
|
func (s *Server) handleSyncTenantLDAP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if s.tenantLdapStore == nil {
|
||||||
|
writeError(w, http.StatusServiceUnavailable, "tenant ldap store not available")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sess := sessionFromCtx(r.Context())
|
||||||
|
if sess.TenantID == nil {
|
||||||
|
writeError(w, http.StatusBadRequest, "no tenant context")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
res := s.doSyncTenantLDAP(r, *sess.TenantID)
|
||||||
|
s.audlog.Log(audit.Entry{
|
||||||
|
EventType: "tenant_ldap_sync",
|
||||||
|
Username: sess.Username,
|
||||||
|
IPAddress: s.remoteIP(r),
|
||||||
|
Success: len(res.Errors) == 0,
|
||||||
|
Detail: fmt.Sprintf("%d Benutzer synchronisiert", res.Synced),
|
||||||
|
})
|
||||||
|
writeJSON(w, http.StatusOK, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleAdminSyncTenantLDAP imports all LDAP users into any tenant (superadmin).
|
||||||
|
func (s *Server) handleAdminSyncTenantLDAP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if s.tenantLdapStore == nil {
|
||||||
|
writeError(w, http.StatusServiceUnavailable, "tenant ldap store not available")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id, err := parseTenantID(r)
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusBadRequest, "invalid tenant id")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
res := s.doSyncTenantLDAP(r, id)
|
||||||
|
sess := sessionFromCtx(r.Context())
|
||||||
|
s.audlog.Log(audit.Entry{
|
||||||
|
EventType: "tenant_ldap_sync",
|
||||||
|
Username: sess.Username,
|
||||||
|
IPAddress: s.remoteIP(r),
|
||||||
|
Success: len(res.Errors) == 0,
|
||||||
|
Detail: fmt.Sprintf("%d Benutzer synchronisiert (tenant %d)", res.Synced, id),
|
||||||
|
})
|
||||||
|
writeJSON(w, http.StatusOK, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
// doSyncTenantLDAP is the shared sync logic: fetch all LDAP users and upsert
|
||||||
|
// them into the local userstore with source='ldap'.
|
||||||
|
func (s *Server) doSyncTenantLDAP(r *http.Request, tenantID int64) syncResult {
|
||||||
|
saved, err := s.tenantLdapStore.GetWithPassword(r.Context(), tenantID)
|
||||||
|
if err != nil || saved == nil {
|
||||||
|
return syncResult{Errors: []string{"keine LDAP-Konfiguration vorhanden"}}
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := ldapauth.Config{
|
||||||
|
URL: saved.URL,
|
||||||
|
BindDN: saved.BindDN,
|
||||||
|
BindPassword: saved.BindPassword,
|
||||||
|
BaseDN: saved.BaseDN,
|
||||||
|
UserFilter: saved.UserFilter,
|
||||||
|
TLS: saved.TLS,
|
||||||
|
TLSSkipVerify: saved.TLSSkipVerify,
|
||||||
|
}
|
||||||
|
|
||||||
|
ldapUsers, err := ldapauth.FetchUsers(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return syncResult{Errors: []string{err.Error()}}
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultRole := saved.DefaultRole
|
||||||
|
if defaultRole == "" {
|
||||||
|
defaultRole = "user"
|
||||||
|
}
|
||||||
|
|
||||||
|
var res syncResult
|
||||||
|
res.Errors = []string{}
|
||||||
|
for _, u := range ldapUsers {
|
||||||
|
mail := u.Mail
|
||||||
|
if mail == "" {
|
||||||
|
mail = u.UID + "@ldap.local"
|
||||||
|
}
|
||||||
|
if _, uErr := s.users.UpsertLDAPUser(u.UID, mail, defaultRole, &tenantID); uErr != nil {
|
||||||
|
res.Errors = append(res.Errors, fmt.Sprintf("%s: %s", u.UID, uErr.Error()))
|
||||||
|
} else {
|
||||||
|
res.Synced++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildTenantTestConfig constructs an ldapauth.Config for testing, either from
|
||||||
|
// the saved tenant config or from the provided request body.
|
||||||
|
func (s *Server) buildTenantTestConfig(r *http.Request, useSaved bool, tenantID int64, provided ldapcfg.TenantLDAPConfig) *ldapauth.Config {
|
||||||
|
if useSaved {
|
||||||
|
saved, err := s.tenantLdapStore.GetWithPassword(r.Context(), tenantID)
|
||||||
|
if err != nil || saved == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &ldapauth.Config{
|
||||||
|
URL: saved.URL,
|
||||||
|
BindDN: saved.BindDN,
|
||||||
|
BindPassword: saved.BindPassword,
|
||||||
|
BaseDN: saved.BaseDN,
|
||||||
|
UserFilter: saved.UserFilter,
|
||||||
|
TLS: saved.TLS,
|
||||||
|
TLSSkipVerify: saved.TLSSkipVerify,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &ldapauth.Config{
|
||||||
|
URL: provided.URL,
|
||||||
|
BindDN: provided.BindDN,
|
||||||
|
BindPassword: provided.BindPassword,
|
||||||
|
BaseDN: provided.BaseDN,
|
||||||
|
UserFilter: provided.UserFilter,
|
||||||
|
TLS: provided.TLS,
|
||||||
|
TLSSkipVerify: provided.TLSSkipVerify,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -771,131 +771,6 @@ func (s *Server) handleAdminTestTenantLDAP(w http.ResponseWriter, r *http.Reques
|
|||||||
writeJSON(w, http.StatusOK, result)
|
writeJSON(w, http.StatusOK, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── LDAP Sync handlers ───────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
type syncResult struct {
|
|
||||||
Synced int `json:"synced"`
|
|
||||||
Errors []string `json:"errors"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleSyncTenantLDAP imports all LDAP users into the tenant (domain_admin).
|
|
||||||
func (s *Server) handleSyncTenantLDAP(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if s.tenantLdapStore == nil {
|
|
||||||
writeError(w, http.StatusServiceUnavailable, "tenant ldap store not available")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
sess := sessionFromCtx(r.Context())
|
|
||||||
if sess.TenantID == nil {
|
|
||||||
writeError(w, http.StatusBadRequest, "no tenant context")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
res := s.doSyncTenantLDAP(r, *sess.TenantID)
|
|
||||||
s.audlog.Log(audit.Entry{
|
|
||||||
EventType: "tenant_ldap_sync",
|
|
||||||
Username: sess.Username,
|
|
||||||
IPAddress: s.remoteIP(r),
|
|
||||||
Success: len(res.Errors) == 0,
|
|
||||||
Detail: fmt.Sprintf("%d Benutzer synchronisiert", res.Synced),
|
|
||||||
})
|
|
||||||
writeJSON(w, http.StatusOK, res)
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleAdminSyncTenantLDAP imports all LDAP users into any tenant (superadmin).
|
|
||||||
func (s *Server) handleAdminSyncTenantLDAP(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if s.tenantLdapStore == nil {
|
|
||||||
writeError(w, http.StatusServiceUnavailable, "tenant ldap store not available")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
id, err := parseTenantID(r)
|
|
||||||
if err != nil {
|
|
||||||
writeError(w, http.StatusBadRequest, "invalid tenant id")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
res := s.doSyncTenantLDAP(r, id)
|
|
||||||
sess := sessionFromCtx(r.Context())
|
|
||||||
s.audlog.Log(audit.Entry{
|
|
||||||
EventType: "tenant_ldap_sync",
|
|
||||||
Username: sess.Username,
|
|
||||||
IPAddress: s.remoteIP(r),
|
|
||||||
Success: len(res.Errors) == 0,
|
|
||||||
Detail: fmt.Sprintf("%d Benutzer synchronisiert (tenant %d)", res.Synced, id),
|
|
||||||
})
|
|
||||||
writeJSON(w, http.StatusOK, res)
|
|
||||||
}
|
|
||||||
|
|
||||||
// doSyncTenantLDAP is the shared sync logic: fetch all LDAP users and upsert
|
|
||||||
// them into the local userstore with source='ldap'.
|
|
||||||
func (s *Server) doSyncTenantLDAP(r *http.Request, tenantID int64) syncResult {
|
|
||||||
saved, err := s.tenantLdapStore.GetWithPassword(r.Context(), tenantID)
|
|
||||||
if err != nil || saved == nil {
|
|
||||||
return syncResult{Errors: []string{"keine LDAP-Konfiguration vorhanden"}}
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg := ldapauth.Config{
|
|
||||||
URL: saved.URL,
|
|
||||||
BindDN: saved.BindDN,
|
|
||||||
BindPassword: saved.BindPassword,
|
|
||||||
BaseDN: saved.BaseDN,
|
|
||||||
UserFilter: saved.UserFilter,
|
|
||||||
TLS: saved.TLS,
|
|
||||||
TLSSkipVerify: saved.TLSSkipVerify,
|
|
||||||
}
|
|
||||||
|
|
||||||
ldapUsers, err := ldapauth.FetchUsers(cfg)
|
|
||||||
if err != nil {
|
|
||||||
return syncResult{Errors: []string{err.Error()}}
|
|
||||||
}
|
|
||||||
|
|
||||||
defaultRole := saved.DefaultRole
|
|
||||||
if defaultRole == "" {
|
|
||||||
defaultRole = "user"
|
|
||||||
}
|
|
||||||
|
|
||||||
var res syncResult
|
|
||||||
res.Errors = []string{}
|
|
||||||
for _, u := range ldapUsers {
|
|
||||||
mail := u.Mail
|
|
||||||
if mail == "" {
|
|
||||||
mail = u.UID + "@ldap.local"
|
|
||||||
}
|
|
||||||
if _, uErr := s.users.UpsertLDAPUser(u.UID, mail, defaultRole, &tenantID); uErr != nil {
|
|
||||||
res.Errors = append(res.Errors, fmt.Sprintf("%s: %s", u.UID, uErr.Error()))
|
|
||||||
} else {
|
|
||||||
res.Synced++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
// buildTenantTestConfig constructs an ldapauth.Config for testing, either from
|
|
||||||
// the saved tenant config or from the provided request body.
|
|
||||||
func (s *Server) buildTenantTestConfig(r *http.Request, useSaved bool, tenantID int64, provided ldapcfg.TenantLDAPConfig) *ldapauth.Config {
|
|
||||||
if useSaved {
|
|
||||||
saved, err := s.tenantLdapStore.GetWithPassword(r.Context(), tenantID)
|
|
||||||
if err != nil || saved == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return &ldapauth.Config{
|
|
||||||
URL: saved.URL,
|
|
||||||
BindDN: saved.BindDN,
|
|
||||||
BindPassword: saved.BindPassword,
|
|
||||||
BaseDN: saved.BaseDN,
|
|
||||||
UserFilter: saved.UserFilter,
|
|
||||||
TLS: saved.TLS,
|
|
||||||
TLSSkipVerify: saved.TLSSkipVerify,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return &ldapauth.Config{
|
|
||||||
URL: provided.URL,
|
|
||||||
BindDN: provided.BindDN,
|
|
||||||
BindPassword: provided.BindPassword,
|
|
||||||
BaseDN: provided.BaseDN,
|
|
||||||
UserFilter: provided.UserFilter,
|
|
||||||
TLS: provided.TLS,
|
|
||||||
TLSSkipVerify: provided.TLSSkipVerify,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── helpers ──────────────────────────────────────────────────────────────────
|
// ── helpers ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
func parseTenantID(r *http.Request) (int64, error) {
|
func parseTenantID(r *http.Request) (int64, error) {
|
||||||
|
|||||||
+57
-256
@@ -2,6 +2,9 @@
|
|||||||
|
|
||||||
import { useState, useEffect, useCallback, useRef } from "react";
|
import { useState, useEffect, useCallback, useRef } from "react";
|
||||||
import { useAuth } from "@/hooks/useAuth";
|
import { useAuth } from "@/hooks/useAuth";
|
||||||
|
import { useLDAPConfig } from "@/hooks/useLDAPConfig";
|
||||||
|
import { useTenantLDAPConfig } from "@/hooks/useTenantLDAPConfig";
|
||||||
|
import { useTenantUsers } from "@/hooks/useTenantUsers";
|
||||||
import {
|
import {
|
||||||
getUsers,
|
getUsers,
|
||||||
createUser,
|
createUser,
|
||||||
@@ -18,24 +21,13 @@ import {
|
|||||||
getUploadProgress,
|
getUploadProgress,
|
||||||
getSecurityAudit,
|
getSecurityAudit,
|
||||||
fixSecurityIssue,
|
fixSecurityIssue,
|
||||||
getLDAPConfig,
|
|
||||||
saveLDAPConfig,
|
|
||||||
deleteLDAPConfig,
|
|
||||||
testLDAPConfig,
|
|
||||||
getTenants,
|
getTenants,
|
||||||
getTenantUsers,
|
|
||||||
createTenant,
|
createTenant,
|
||||||
updateTenant,
|
updateTenant,
|
||||||
deleteTenant,
|
deleteTenant,
|
||||||
getTenantDomains,
|
getTenantDomains,
|
||||||
addTenantDomain,
|
addTenantDomain,
|
||||||
removeTenantDomain,
|
removeTenantDomain,
|
||||||
getTenantLDAPConfig,
|
|
||||||
saveTenantLDAPConfig,
|
|
||||||
deleteTenantLDAPConfig,
|
|
||||||
testTenantLDAPConfig,
|
|
||||||
syncAdminTenantLDAP,
|
|
||||||
type LDAPSyncResult,
|
|
||||||
getAdminLabels,
|
getAdminLabels,
|
||||||
createAdminLabel,
|
createAdminLabel,
|
||||||
deleteAdminLabel,
|
deleteAdminLabel,
|
||||||
@@ -55,9 +47,6 @@ import {
|
|||||||
type SystemStats,
|
type SystemStats,
|
||||||
type UploadJob,
|
type UploadJob,
|
||||||
type SecurityAuditResult,
|
type SecurityAuditResult,
|
||||||
type LDAPConfig,
|
|
||||||
type LDAPTestResult,
|
|
||||||
type TenantLDAPConfig,
|
|
||||||
type Tenant,
|
type Tenant,
|
||||||
type TenantDefaultUser,
|
type TenantDefaultUser,
|
||||||
type TenantDomain,
|
type TenantDomain,
|
||||||
@@ -155,26 +144,23 @@ export default function AdminPage() {
|
|||||||
const [uploadLoading, setUploadLoading] = useState(false);
|
const [uploadLoading, setUploadLoading] = useState(false);
|
||||||
const uploadPollRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
const uploadPollRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||||
|
|
||||||
// LDAP state
|
// LDAP state (global, superadmin) — managed by useLDAPConfig hook
|
||||||
const [ldapConfig, setLdapConfig] = useState<LDAPConfig | null>(null);
|
const {
|
||||||
const [ldapLoading, setLdapLoading] = useState(false);
|
ldapConfig,
|
||||||
const [ldapSaving, setLdapSaving] = useState(false);
|
ldapLoading,
|
||||||
const [ldapTesting, setLdapTesting] = useState(false);
|
ldapSaving,
|
||||||
const [ldapError, setLdapError] = useState("");
|
ldapTesting,
|
||||||
const [ldapTestResult, setLdapTestResult] = useState<LDAPTestResult | null>(null);
|
ldapError,
|
||||||
const [ldapForm, setLdapForm] = useState<LDAPConfig>({
|
ldapTestResult,
|
||||||
enabled: false,
|
ldapForm,
|
||||||
url: "ldap://",
|
setLdapForm,
|
||||||
bind_dn: "",
|
ldapChangePassword,
|
||||||
bind_password: "",
|
setLdapChangePassword,
|
||||||
base_dn: "",
|
loadLDAP,
|
||||||
user_filter: "(sAMAccountName=%s)",
|
handleSaveLDAP,
|
||||||
tls: false,
|
handleTestLDAP,
|
||||||
tls_skip_verify: false,
|
handleDeleteLDAP,
|
||||||
default_role: "user",
|
} = useLDAPConfig();
|
||||||
group_mappings: [],
|
|
||||||
});
|
|
||||||
const [ldapChangePassword, setLdapChangePassword] = useState(false);
|
|
||||||
|
|
||||||
// Tenants state
|
// Tenants state
|
||||||
const [tenants, setTenants] = useState<Tenant[]>([]);
|
const [tenants, setTenants] = useState<Tenant[]>([]);
|
||||||
@@ -197,26 +183,29 @@ export default function AdminPage() {
|
|||||||
const [addDomainLoading, setAddDomainLoading] = useState(false);
|
const [addDomainLoading, setAddDomainLoading] = useState(false);
|
||||||
const [domainError, setDomainError] = useState("");
|
const [domainError, setDomainError] = useState("");
|
||||||
|
|
||||||
// Tenant LDAP state (domain_admin own tenant)
|
// Tenant LDAP state (domain_admin own tenant) — managed by useTenantLDAPConfig hook
|
||||||
const [tenantLdapConfig, setTenantLdapConfig] = useState<TenantLDAPConfig | null>(null);
|
const {
|
||||||
const [tenantLdapLoading, setTenantLdapLoading] = useState(false);
|
tenantLdapConfig,
|
||||||
const [tenantLdapSaving, setTenantLdapSaving] = useState(false);
|
tenantLdapLoading,
|
||||||
const [tenantLdapTesting, setTenantLdapTesting] = useState(false);
|
tenantLdapSaving,
|
||||||
const [tenantLdapError, setTenantLdapError] = useState("");
|
tenantLdapTesting,
|
||||||
const [tenantLdapTestResult, setTenantLdapTestResult] = useState<LDAPTestResult | null>(null);
|
tenantLdapError,
|
||||||
const [tenantLdapForm, setTenantLdapForm] = useState<TenantLDAPConfig>({
|
tenantLdapTestResult,
|
||||||
enabled: false,
|
tenantLdapForm,
|
||||||
url: "ldap://",
|
setTenantLdapForm,
|
||||||
bind_dn: "",
|
tenantLdapChangePassword,
|
||||||
bind_password: "",
|
setTenantLdapChangePassword,
|
||||||
base_dn: "",
|
ownLogoPreviewUrl,
|
||||||
user_filter: "(sAMAccountName=%s)",
|
setOwnLogoPreviewUrl,
|
||||||
tls: false,
|
ownLogoUploading,
|
||||||
tls_skip_verify: false,
|
setOwnLogoUploading,
|
||||||
default_role: "user",
|
ownLogoError,
|
||||||
group_mappings: [],
|
setOwnLogoError,
|
||||||
});
|
loadTenantLDAP,
|
||||||
const [tenantLdapChangePassword, setTenantLdapChangePassword] = useState(false);
|
handleSaveTenantLDAP,
|
||||||
|
handleTestTenantLDAP,
|
||||||
|
handleDeleteTenantLDAP,
|
||||||
|
} = useTenantLDAPConfig();
|
||||||
|
|
||||||
// Superadmin: tenant LDAP dialog
|
// Superadmin: tenant LDAP dialog
|
||||||
const [tenantLdapDialogId, setTenantLdapDialogId] = useState<number | null>(null);
|
const [tenantLdapDialogId, setTenantLdapDialogId] = useState<number | null>(null);
|
||||||
@@ -227,20 +216,20 @@ export default function AdminPage() {
|
|||||||
const [logoUploading, setLogoUploading] = useState(false);
|
const [logoUploading, setLogoUploading] = useState(false);
|
||||||
const [logoError, setLogoError] = useState("");
|
const [logoError, setLogoError] = useState("");
|
||||||
|
|
||||||
// Logo for domain_admin own tenant
|
// Tenant users dialog — managed by useTenantUsers hook
|
||||||
const [ownLogoPreviewUrl, setOwnLogoPreviewUrl] = useState<string | null>(null);
|
const {
|
||||||
const [ownLogoUploading, setOwnLogoUploading] = useState(false);
|
tenantUsersDialogId,
|
||||||
const [ownLogoError, setOwnLogoError] = useState("");
|
setTenantUsersDialogId,
|
||||||
|
tenantUsersDialogName,
|
||||||
// Tenant users dialog
|
tenantUsersDialogLdap,
|
||||||
const [tenantUsersDialogId, setTenantUsersDialogId] = useState<number | null>(null);
|
tenantUsers,
|
||||||
const [tenantUsersDialogName, setTenantUsersDialogName] = useState("");
|
tenantUsersLoading,
|
||||||
const [tenantUsersDialogLdap, setTenantUsersDialogLdap] = useState(false);
|
tenantUsersError,
|
||||||
const [tenantUsers, setTenantUsers] = useState<User[]>([]);
|
tenantUsersSyncing,
|
||||||
const [tenantUsersLoading, setTenantUsersLoading] = useState(false);
|
tenantUsersSyncResult,
|
||||||
const [tenantUsersError, setTenantUsersError] = useState("");
|
openUsersDialog,
|
||||||
const [tenantUsersSyncing, setTenantUsersSyncing] = useState(false);
|
handleSyncLDAPUsers,
|
||||||
const [tenantUsersSyncResult, setTenantUsersSyncResult] = useState<LDAPSyncResult | null>(null);
|
} = useTenantUsers();
|
||||||
|
|
||||||
// Labels state
|
// Labels state
|
||||||
const [adminLabels, setAdminLabels] = useState<MailLabel[]>([]);
|
const [adminLabels, setAdminLabels] = useState<MailLabel[]>([]);
|
||||||
@@ -528,78 +517,6 @@ export default function AdminPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// LDAP handlers
|
|
||||||
const loadLDAP = useCallback(async () => {
|
|
||||||
setLdapLoading(true);
|
|
||||||
setLdapError("");
|
|
||||||
try {
|
|
||||||
const cfg = await getLDAPConfig();
|
|
||||||
if (cfg) {
|
|
||||||
setLdapConfig(cfg);
|
|
||||||
setLdapForm({ ...cfg, bind_password: "" });
|
|
||||||
setLdapChangePassword(false);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
setLdapError("LDAP-Konfiguration konnte nicht geladen werden.");
|
|
||||||
} finally {
|
|
||||||
setLdapLoading(false);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
async function handleSaveLDAP(e: React.FormEvent) {
|
|
||||||
e.preventDefault();
|
|
||||||
setLdapSaving(true);
|
|
||||||
setLdapError("");
|
|
||||||
try {
|
|
||||||
const payload: Partial<LDAPConfig> = { ...ldapForm };
|
|
||||||
if (!ldapChangePassword) {
|
|
||||||
delete payload.bind_password;
|
|
||||||
}
|
|
||||||
await saveLDAPConfig(payload);
|
|
||||||
await loadLDAP();
|
|
||||||
} catch (err: unknown) {
|
|
||||||
setLdapError(err instanceof Error ? err.message : "Speichern fehlgeschlagen.");
|
|
||||||
} finally {
|
|
||||||
setLdapSaving(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleTestLDAP() {
|
|
||||||
setLdapTesting(true);
|
|
||||||
setLdapError("");
|
|
||||||
setLdapTestResult(null);
|
|
||||||
try {
|
|
||||||
const payload = ldapConfig
|
|
||||||
? { use_saved: true }
|
|
||||||
: { use_saved: false, ...ldapForm };
|
|
||||||
const result = await testLDAPConfig(payload as Parameters<typeof testLDAPConfig>[0]);
|
|
||||||
setLdapTestResult(result);
|
|
||||||
} catch (err: unknown) {
|
|
||||||
setLdapError(err instanceof Error ? err.message : "Test fehlgeschlagen.");
|
|
||||||
} finally {
|
|
||||||
setLdapTesting(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleDeleteLDAP() {
|
|
||||||
setLdapSaving(true);
|
|
||||||
setLdapError("");
|
|
||||||
try {
|
|
||||||
await deleteLDAPConfig();
|
|
||||||
setLdapConfig(null);
|
|
||||||
setLdapForm({
|
|
||||||
enabled: false, url: "ldap://", bind_dn: "", bind_password: "",
|
|
||||||
base_dn: "", user_filter: "(sAMAccountName=%s)", tls: false,
|
|
||||||
tls_skip_verify: false, default_role: "user", group_mappings: [],
|
|
||||||
});
|
|
||||||
setLdapTestResult(null);
|
|
||||||
} catch (err: unknown) {
|
|
||||||
setLdapError(err instanceof Error ? err.message : "Löschen fehlgeschlagen.");
|
|
||||||
} finally {
|
|
||||||
setLdapSaving(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tenants handlers
|
// Tenants handlers
|
||||||
const loadTenants = useCallback(async () => {
|
const loadTenants = useCallback(async () => {
|
||||||
setTenantsLoading(true);
|
setTenantsLoading(true);
|
||||||
@@ -747,40 +664,6 @@ export default function AdminPage() {
|
|||||||
finally { setDomainsLoading(false); }
|
finally { setDomainsLoading(false); }
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openUsersDialog(t: Tenant) {
|
|
||||||
setTenantUsersDialogId(t.id);
|
|
||||||
setTenantUsersDialogName(t.name);
|
|
||||||
setTenantUsersDialogLdap(t.ldap_enabled === true);
|
|
||||||
setTenantUsersLoading(true);
|
|
||||||
setTenantUsers([]);
|
|
||||||
setTenantUsersError("");
|
|
||||||
setTenantUsersSyncResult(null);
|
|
||||||
try {
|
|
||||||
const users = await getTenantUsers(t.id);
|
|
||||||
setTenantUsers(users || []);
|
|
||||||
} catch (err: unknown) {
|
|
||||||
setTenantUsersError(err instanceof Error ? err.message : "Nutzer konnten nicht geladen werden.");
|
|
||||||
} finally {
|
|
||||||
setTenantUsersLoading(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleSyncLDAPUsers() {
|
|
||||||
if (!tenantUsersDialogId) return;
|
|
||||||
setTenantUsersSyncing(true);
|
|
||||||
setTenantUsersSyncResult(null);
|
|
||||||
try {
|
|
||||||
const result = await syncAdminTenantLDAP(tenantUsersDialogId);
|
|
||||||
setTenantUsersSyncResult(result);
|
|
||||||
const users = await getTenantUsers(tenantUsersDialogId);
|
|
||||||
setTenantUsers(users || []);
|
|
||||||
} catch (err: unknown) {
|
|
||||||
setTenantUsersSyncResult({ synced: 0, errors: [err instanceof Error ? err.message : "Sync fehlgeschlagen"] });
|
|
||||||
} finally {
|
|
||||||
setTenantUsersSyncing(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleAddDomain() {
|
async function handleAddDomain() {
|
||||||
if (!domainDialogTenant || !newDomain) return;
|
if (!domainDialogTenant || !newDomain) return;
|
||||||
setAddDomainLoading(true);
|
setAddDomainLoading(true);
|
||||||
@@ -892,88 +775,6 @@ export default function AdminPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tenant LDAP handlers (domain_admin)
|
|
||||||
const loadTenantLDAP = useCallback(async () => {
|
|
||||||
setTenantLdapLoading(true);
|
|
||||||
setTenantLdapError("");
|
|
||||||
try {
|
|
||||||
const cfg = await getTenantLDAPConfig();
|
|
||||||
if (cfg) {
|
|
||||||
setTenantLdapConfig(cfg);
|
|
||||||
setTenantLdapForm({ ...cfg, bind_password: "" });
|
|
||||||
setTenantLdapChangePassword(false);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
setTenantLdapError("LDAP-Konfiguration konnte nicht geladen werden.");
|
|
||||||
} finally {
|
|
||||||
setTenantLdapLoading(false);
|
|
||||||
}
|
|
||||||
// Load own tenant logo preview
|
|
||||||
try {
|
|
||||||
const res = await fetch("/api/tenant/logo", { credentials: "include" });
|
|
||||||
if (res.ok) {
|
|
||||||
const blob = await res.blob();
|
|
||||||
setOwnLogoPreviewUrl(URL.createObjectURL(blob));
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// no logo or not available
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
async function handleSaveTenantLDAP(e: React.FormEvent) {
|
|
||||||
e.preventDefault();
|
|
||||||
setTenantLdapSaving(true);
|
|
||||||
setTenantLdapError("");
|
|
||||||
try {
|
|
||||||
const payload: Partial<TenantLDAPConfig> = { ...tenantLdapForm };
|
|
||||||
if (!tenantLdapChangePassword) {
|
|
||||||
delete payload.bind_password;
|
|
||||||
}
|
|
||||||
await saveTenantLDAPConfig(payload);
|
|
||||||
await loadTenantLDAP();
|
|
||||||
} catch (err: unknown) {
|
|
||||||
setTenantLdapError(err instanceof Error ? err.message : "Speichern fehlgeschlagen.");
|
|
||||||
} finally {
|
|
||||||
setTenantLdapSaving(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleTestTenantLDAP() {
|
|
||||||
setTenantLdapTesting(true);
|
|
||||||
setTenantLdapError("");
|
|
||||||
setTenantLdapTestResult(null);
|
|
||||||
try {
|
|
||||||
const payload = tenantLdapConfig
|
|
||||||
? { use_saved: true }
|
|
||||||
: { use_saved: false, ...tenantLdapForm };
|
|
||||||
const result = await testTenantLDAPConfig(payload as Parameters<typeof testTenantLDAPConfig>[0]);
|
|
||||||
setTenantLdapTestResult(result);
|
|
||||||
} catch (err: unknown) {
|
|
||||||
setTenantLdapError(err instanceof Error ? err.message : "Test fehlgeschlagen.");
|
|
||||||
} finally {
|
|
||||||
setTenantLdapTesting(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleDeleteTenantLDAP() {
|
|
||||||
setTenantLdapSaving(true);
|
|
||||||
setTenantLdapError("");
|
|
||||||
try {
|
|
||||||
await deleteTenantLDAPConfig();
|
|
||||||
setTenantLdapConfig(null);
|
|
||||||
setTenantLdapForm({
|
|
||||||
enabled: false, url: "ldap://", bind_dn: "", bind_password: "",
|
|
||||||
base_dn: "", user_filter: "(sAMAccountName=%s)", tls: false,
|
|
||||||
tls_skip_verify: false, default_role: "user", group_mappings: [],
|
|
||||||
});
|
|
||||||
setTenantLdapTestResult(null);
|
|
||||||
} catch (err: unknown) {
|
|
||||||
setTenantLdapError(err instanceof Error ? err.message : "Löschen fehlgeschlagen.");
|
|
||||||
} finally {
|
|
||||||
setTenantLdapSaving(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen">
|
<div className="min-h-screen">
|
||||||
<Navbar username={user?.username ?? ""} role={user?.role ?? ""} />
|
<Navbar username={user?.username ?? ""} role={user?.role ?? ""} />
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useCallback } from "react";
|
||||||
|
import {
|
||||||
|
getLDAPConfig,
|
||||||
|
saveLDAPConfig,
|
||||||
|
deleteLDAPConfig,
|
||||||
|
testLDAPConfig,
|
||||||
|
type LDAPConfig,
|
||||||
|
type LDAPTestResult,
|
||||||
|
} from "@/lib/api";
|
||||||
|
|
||||||
|
const defaultLDAPForm = (): LDAPConfig => ({
|
||||||
|
enabled: false,
|
||||||
|
url: "ldap://",
|
||||||
|
bind_dn: "",
|
||||||
|
bind_password: "",
|
||||||
|
base_dn: "",
|
||||||
|
user_filter: "(sAMAccountName=%s)",
|
||||||
|
tls: false,
|
||||||
|
tls_skip_verify: false,
|
||||||
|
default_role: "user",
|
||||||
|
group_mappings: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
export function useLDAPConfig() {
|
||||||
|
const [ldapConfig, setLdapConfig] = useState<LDAPConfig | null>(null);
|
||||||
|
const [ldapLoading, setLdapLoading] = useState(false);
|
||||||
|
const [ldapSaving, setLdapSaving] = useState(false);
|
||||||
|
const [ldapTesting, setLdapTesting] = useState(false);
|
||||||
|
const [ldapError, setLdapError] = useState("");
|
||||||
|
const [ldapTestResult, setLdapTestResult] = useState<LDAPTestResult | null>(null);
|
||||||
|
const [ldapForm, setLdapForm] = useState<LDAPConfig>(defaultLDAPForm());
|
||||||
|
const [ldapChangePassword, setLdapChangePassword] = useState(false);
|
||||||
|
|
||||||
|
const loadLDAP = useCallback(async () => {
|
||||||
|
setLdapLoading(true);
|
||||||
|
setLdapError("");
|
||||||
|
try {
|
||||||
|
const cfg = await getLDAPConfig();
|
||||||
|
if (cfg) {
|
||||||
|
setLdapConfig(cfg);
|
||||||
|
setLdapForm({ ...cfg, bind_password: "" });
|
||||||
|
setLdapChangePassword(false);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
setLdapError("LDAP-Konfiguration konnte nicht geladen werden.");
|
||||||
|
} finally {
|
||||||
|
setLdapLoading(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
async function handleSaveLDAP(e: React.FormEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
setLdapSaving(true);
|
||||||
|
setLdapError("");
|
||||||
|
try {
|
||||||
|
const payload: Partial<LDAPConfig> = { ...ldapForm };
|
||||||
|
if (!ldapChangePassword) {
|
||||||
|
delete payload.bind_password;
|
||||||
|
}
|
||||||
|
await saveLDAPConfig(payload);
|
||||||
|
await loadLDAP();
|
||||||
|
} catch (err: unknown) {
|
||||||
|
setLdapError(err instanceof Error ? err.message : "Speichern fehlgeschlagen.");
|
||||||
|
} finally {
|
||||||
|
setLdapSaving(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleTestLDAP() {
|
||||||
|
setLdapTesting(true);
|
||||||
|
setLdapError("");
|
||||||
|
setLdapTestResult(null);
|
||||||
|
try {
|
||||||
|
const payload = ldapConfig
|
||||||
|
? { use_saved: true }
|
||||||
|
: { use_saved: false, ...ldapForm };
|
||||||
|
const result = await testLDAPConfig(payload as Parameters<typeof testLDAPConfig>[0]);
|
||||||
|
setLdapTestResult(result);
|
||||||
|
} catch (err: unknown) {
|
||||||
|
setLdapError(err instanceof Error ? err.message : "Test fehlgeschlagen.");
|
||||||
|
} finally {
|
||||||
|
setLdapTesting(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDeleteLDAP() {
|
||||||
|
setLdapSaving(true);
|
||||||
|
setLdapError("");
|
||||||
|
try {
|
||||||
|
await deleteLDAPConfig();
|
||||||
|
setLdapConfig(null);
|
||||||
|
setLdapForm(defaultLDAPForm());
|
||||||
|
setLdapTestResult(null);
|
||||||
|
} catch (err: unknown) {
|
||||||
|
setLdapError(err instanceof Error ? err.message : "Löschen fehlgeschlagen.");
|
||||||
|
} finally {
|
||||||
|
setLdapSaving(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
ldapConfig,
|
||||||
|
ldapLoading,
|
||||||
|
ldapSaving,
|
||||||
|
ldapTesting,
|
||||||
|
ldapError,
|
||||||
|
ldapTestResult,
|
||||||
|
ldapForm,
|
||||||
|
setLdapForm,
|
||||||
|
ldapChangePassword,
|
||||||
|
setLdapChangePassword,
|
||||||
|
loadLDAP,
|
||||||
|
handleSaveLDAP,
|
||||||
|
handleTestLDAP,
|
||||||
|
handleDeleteLDAP,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useCallback } from "react";
|
||||||
|
import {
|
||||||
|
getTenantLDAPConfig,
|
||||||
|
saveTenantLDAPConfig,
|
||||||
|
deleteTenantLDAPConfig,
|
||||||
|
testTenantLDAPConfig,
|
||||||
|
type TenantLDAPConfig,
|
||||||
|
type LDAPTestResult,
|
||||||
|
} from "@/lib/api";
|
||||||
|
|
||||||
|
const defaultTenantLDAPForm = (): TenantLDAPConfig => ({
|
||||||
|
enabled: false,
|
||||||
|
url: "ldap://",
|
||||||
|
bind_dn: "",
|
||||||
|
bind_password: "",
|
||||||
|
base_dn: "",
|
||||||
|
user_filter: "(sAMAccountName=%s)",
|
||||||
|
tls: false,
|
||||||
|
tls_skip_verify: false,
|
||||||
|
default_role: "user",
|
||||||
|
group_mappings: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
export function useTenantLDAPConfig() {
|
||||||
|
const [tenantLdapConfig, setTenantLdapConfig] = useState<TenantLDAPConfig | null>(null);
|
||||||
|
const [tenantLdapLoading, setTenantLdapLoading] = useState(false);
|
||||||
|
const [tenantLdapSaving, setTenantLdapSaving] = useState(false);
|
||||||
|
const [tenantLdapTesting, setTenantLdapTesting] = useState(false);
|
||||||
|
const [tenantLdapError, setTenantLdapError] = useState("");
|
||||||
|
const [tenantLdapTestResult, setTenantLdapTestResult] = useState<LDAPTestResult | null>(null);
|
||||||
|
const [tenantLdapForm, setTenantLdapForm] = useState<TenantLDAPConfig>(defaultTenantLDAPForm());
|
||||||
|
const [tenantLdapChangePassword, setTenantLdapChangePassword] = useState(false);
|
||||||
|
|
||||||
|
// Own tenant logo state (lives here because it's loaded together with tenant LDAP)
|
||||||
|
const [ownLogoPreviewUrl, setOwnLogoPreviewUrl] = useState<string | null>(null);
|
||||||
|
const [ownLogoUploading, setOwnLogoUploading] = useState(false);
|
||||||
|
const [ownLogoError, setOwnLogoError] = useState("");
|
||||||
|
|
||||||
|
const loadTenantLDAP = useCallback(async () => {
|
||||||
|
setTenantLdapLoading(true);
|
||||||
|
setTenantLdapError("");
|
||||||
|
try {
|
||||||
|
const cfg = await getTenantLDAPConfig();
|
||||||
|
if (cfg) {
|
||||||
|
setTenantLdapConfig(cfg);
|
||||||
|
setTenantLdapForm({ ...cfg, bind_password: "" });
|
||||||
|
setTenantLdapChangePassword(false);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
setTenantLdapError("LDAP-Konfiguration konnte nicht geladen werden.");
|
||||||
|
} finally {
|
||||||
|
setTenantLdapLoading(false);
|
||||||
|
}
|
||||||
|
// Load own tenant logo preview
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/tenant/logo", { credentials: "include" });
|
||||||
|
if (res.ok) {
|
||||||
|
const blob = await res.blob();
|
||||||
|
setOwnLogoPreviewUrl(URL.createObjectURL(blob));
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// no logo or not available
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
async function handleSaveTenantLDAP(e: React.FormEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
setTenantLdapSaving(true);
|
||||||
|
setTenantLdapError("");
|
||||||
|
try {
|
||||||
|
const payload: Partial<TenantLDAPConfig> = { ...tenantLdapForm };
|
||||||
|
if (!tenantLdapChangePassword) {
|
||||||
|
delete payload.bind_password;
|
||||||
|
}
|
||||||
|
await saveTenantLDAPConfig(payload);
|
||||||
|
await loadTenantLDAP();
|
||||||
|
} catch (err: unknown) {
|
||||||
|
setTenantLdapError(err instanceof Error ? err.message : "Speichern fehlgeschlagen.");
|
||||||
|
} finally {
|
||||||
|
setTenantLdapSaving(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleTestTenantLDAP() {
|
||||||
|
setTenantLdapTesting(true);
|
||||||
|
setTenantLdapError("");
|
||||||
|
setTenantLdapTestResult(null);
|
||||||
|
try {
|
||||||
|
const payload = tenantLdapConfig
|
||||||
|
? { use_saved: true }
|
||||||
|
: { use_saved: false, ...tenantLdapForm };
|
||||||
|
const result = await testTenantLDAPConfig(payload as Parameters<typeof testTenantLDAPConfig>[0]);
|
||||||
|
setTenantLdapTestResult(result);
|
||||||
|
} catch (err: unknown) {
|
||||||
|
setTenantLdapError(err instanceof Error ? err.message : "Test fehlgeschlagen.");
|
||||||
|
} finally {
|
||||||
|
setTenantLdapTesting(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDeleteTenantLDAP() {
|
||||||
|
setTenantLdapSaving(true);
|
||||||
|
setTenantLdapError("");
|
||||||
|
try {
|
||||||
|
await deleteTenantLDAPConfig();
|
||||||
|
setTenantLdapConfig(null);
|
||||||
|
setTenantLdapForm(defaultTenantLDAPForm());
|
||||||
|
setTenantLdapTestResult(null);
|
||||||
|
} catch (err: unknown) {
|
||||||
|
setTenantLdapError(err instanceof Error ? err.message : "Löschen fehlgeschlagen.");
|
||||||
|
} finally {
|
||||||
|
setTenantLdapSaving(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
tenantLdapConfig,
|
||||||
|
tenantLdapLoading,
|
||||||
|
tenantLdapSaving,
|
||||||
|
tenantLdapTesting,
|
||||||
|
tenantLdapError,
|
||||||
|
tenantLdapTestResult,
|
||||||
|
tenantLdapForm,
|
||||||
|
setTenantLdapForm,
|
||||||
|
tenantLdapChangePassword,
|
||||||
|
setTenantLdapChangePassword,
|
||||||
|
ownLogoPreviewUrl,
|
||||||
|
setOwnLogoPreviewUrl,
|
||||||
|
ownLogoUploading,
|
||||||
|
setOwnLogoUploading,
|
||||||
|
ownLogoError,
|
||||||
|
setOwnLogoError,
|
||||||
|
loadTenantLDAP,
|
||||||
|
handleSaveTenantLDAP,
|
||||||
|
handleTestTenantLDAP,
|
||||||
|
handleDeleteTenantLDAP,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import {
|
||||||
|
getTenantUsers,
|
||||||
|
syncAdminTenantLDAP,
|
||||||
|
type User,
|
||||||
|
type Tenant,
|
||||||
|
type LDAPSyncResult,
|
||||||
|
} from "@/lib/api";
|
||||||
|
|
||||||
|
export function useTenantUsers() {
|
||||||
|
const [tenantUsersDialogId, setTenantUsersDialogId] = useState<number | null>(null);
|
||||||
|
const [tenantUsersDialogName, setTenantUsersDialogName] = useState("");
|
||||||
|
const [tenantUsersDialogLdap, setTenantUsersDialogLdap] = useState(false);
|
||||||
|
const [tenantUsers, setTenantUsers] = useState<User[]>([]);
|
||||||
|
const [tenantUsersLoading, setTenantUsersLoading] = useState(false);
|
||||||
|
const [tenantUsersError, setTenantUsersError] = useState("");
|
||||||
|
const [tenantUsersSyncing, setTenantUsersSyncing] = useState(false);
|
||||||
|
const [tenantUsersSyncResult, setTenantUsersSyncResult] = useState<LDAPSyncResult | null>(null);
|
||||||
|
|
||||||
|
async function openUsersDialog(t: Tenant) {
|
||||||
|
setTenantUsersDialogId(t.id);
|
||||||
|
setTenantUsersDialogName(t.name);
|
||||||
|
setTenantUsersDialogLdap(t.ldap_enabled === true);
|
||||||
|
setTenantUsersLoading(true);
|
||||||
|
setTenantUsers([]);
|
||||||
|
setTenantUsersError("");
|
||||||
|
setTenantUsersSyncResult(null);
|
||||||
|
try {
|
||||||
|
const users = await getTenantUsers(t.id);
|
||||||
|
setTenantUsers(users || []);
|
||||||
|
} catch (err: unknown) {
|
||||||
|
setTenantUsersError(err instanceof Error ? err.message : "Nutzer konnten nicht geladen werden.");
|
||||||
|
} finally {
|
||||||
|
setTenantUsersLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSyncLDAPUsers() {
|
||||||
|
if (!tenantUsersDialogId) return;
|
||||||
|
setTenantUsersSyncing(true);
|
||||||
|
setTenantUsersSyncResult(null);
|
||||||
|
try {
|
||||||
|
const result = await syncAdminTenantLDAP(tenantUsersDialogId);
|
||||||
|
setTenantUsersSyncResult(result);
|
||||||
|
const users = await getTenantUsers(tenantUsersDialogId);
|
||||||
|
setTenantUsers(users || []);
|
||||||
|
} catch (err: unknown) {
|
||||||
|
setTenantUsersSyncResult({ synced: 0, errors: [err instanceof Error ? err.message : "Sync fehlgeschlagen"] });
|
||||||
|
} finally {
|
||||||
|
setTenantUsersSyncing(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
tenantUsersDialogId,
|
||||||
|
setTenantUsersDialogId,
|
||||||
|
tenantUsersDialogName,
|
||||||
|
tenantUsersDialogLdap,
|
||||||
|
tenantUsers,
|
||||||
|
tenantUsersLoading,
|
||||||
|
tenantUsersError,
|
||||||
|
tenantUsersSyncing,
|
||||||
|
tenantUsersSyncResult,
|
||||||
|
openUsersDialog,
|
||||||
|
handleSyncLDAPUsers,
|
||||||
|
};
|
||||||
|
}
|
||||||
+6
-1085
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,33 @@
|
|||||||
|
import { clearAuthCache } from "@/lib/auth-cache";
|
||||||
|
|
||||||
|
export const API_BASE = process.env.NEXT_PUBLIC_API_URL || "";
|
||||||
|
|
||||||
|
export async function request<T>(
|
||||||
|
path: string,
|
||||||
|
options: RequestInit = {}
|
||||||
|
): Promise<T> {
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
...(options.headers as Record<string, string>),
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await fetch(`${API_BASE}${path}`, {
|
||||||
|
...options,
|
||||||
|
headers,
|
||||||
|
credentials: "include",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status === 401) {
|
||||||
|
clearAuthCache();
|
||||||
|
throw new Error("Unauthorized");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const body = await res.text();
|
||||||
|
throw new Error(body || `Request failed: ${res.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res.status === 204) return {} as T;
|
||||||
|
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
// Re-export everything from domain modules so that existing imports
|
||||||
|
// from "@/lib/api" continue to work without modification.
|
||||||
|
|
||||||
|
export { API_BASE, request } from "./core";
|
||||||
|
|
||||||
|
export type {
|
||||||
|
LoginResponse,
|
||||||
|
User,
|
||||||
|
MeResponse,
|
||||||
|
CreateUserRequest,
|
||||||
|
UpdateUserRequest,
|
||||||
|
} from "./users";
|
||||||
|
export {
|
||||||
|
login,
|
||||||
|
getMe,
|
||||||
|
logout,
|
||||||
|
changePassword,
|
||||||
|
changeEmail,
|
||||||
|
getUsers,
|
||||||
|
createUser,
|
||||||
|
updateUser,
|
||||||
|
deleteUser,
|
||||||
|
getTOTPSetup,
|
||||||
|
confirmTOTPSetup,
|
||||||
|
disableTOTP,
|
||||||
|
} from "./users";
|
||||||
|
|
||||||
|
export type {
|
||||||
|
LDAPGroupMapping,
|
||||||
|
LDAPConfig,
|
||||||
|
LDAPTestUser,
|
||||||
|
LDAPTestResult,
|
||||||
|
} from "./ldap";
|
||||||
|
export {
|
||||||
|
getLDAPConfig,
|
||||||
|
saveLDAPConfig,
|
||||||
|
deleteLDAPConfig,
|
||||||
|
testLDAPConfig,
|
||||||
|
} from "./ldap";
|
||||||
|
|
||||||
|
export type {
|
||||||
|
Tenant,
|
||||||
|
TenantDomain,
|
||||||
|
TenantDefaultUser,
|
||||||
|
CreateTenantResponse,
|
||||||
|
TenantLDAPConfig,
|
||||||
|
LDAPSyncResult,
|
||||||
|
} from "./tenants";
|
||||||
|
export {
|
||||||
|
getTenants,
|
||||||
|
getTenantUsers,
|
||||||
|
createTenant,
|
||||||
|
updateTenant,
|
||||||
|
deleteTenant,
|
||||||
|
getTenantDomains,
|
||||||
|
addTenantDomain,
|
||||||
|
removeTenantDomain,
|
||||||
|
getTenantLogoUrl,
|
||||||
|
uploadTenantLogo,
|
||||||
|
deleteTenantLogo,
|
||||||
|
uploadMyTenantLogo,
|
||||||
|
deleteMyTenantLogo,
|
||||||
|
getTenantLDAPConfig,
|
||||||
|
saveTenantLDAPConfig,
|
||||||
|
deleteTenantLDAPConfig,
|
||||||
|
testTenantLDAPConfig,
|
||||||
|
getAdminTenantLDAPConfig,
|
||||||
|
saveAdminTenantLDAPConfig,
|
||||||
|
deleteAdminTenantLDAPConfig,
|
||||||
|
testAdminTenantLDAPConfig,
|
||||||
|
syncAdminTenantLDAP,
|
||||||
|
} from "./tenants";
|
||||||
|
|
||||||
|
export type {
|
||||||
|
SearchHit,
|
||||||
|
SearchResponse,
|
||||||
|
MailAttachment,
|
||||||
|
MailDetail,
|
||||||
|
ImapFolder,
|
||||||
|
ImapAccount,
|
||||||
|
ImapTestResult,
|
||||||
|
Pop3Account,
|
||||||
|
Pop3TestResult,
|
||||||
|
UploadJob,
|
||||||
|
} from "./mail";
|
||||||
|
export {
|
||||||
|
searchEmails,
|
||||||
|
getMail,
|
||||||
|
downloadMailAttachment,
|
||||||
|
downloadMailRaw,
|
||||||
|
getImapAccounts,
|
||||||
|
createImapAccount,
|
||||||
|
deleteImapAccount,
|
||||||
|
testImapConnection,
|
||||||
|
startImapImport,
|
||||||
|
getImapProgress,
|
||||||
|
triggerImapSync,
|
||||||
|
updateImapInterval,
|
||||||
|
updateImapAccount,
|
||||||
|
getPop3Accounts,
|
||||||
|
createPop3Account,
|
||||||
|
deletePop3Account,
|
||||||
|
testPop3Connection,
|
||||||
|
startPop3Import,
|
||||||
|
getPop3Progress,
|
||||||
|
exportMailPDF,
|
||||||
|
exportMailsZIP,
|
||||||
|
uploadMailFiles,
|
||||||
|
getUploadProgress,
|
||||||
|
uploadMailFilesUser,
|
||||||
|
getUploadProgressUser,
|
||||||
|
} from "./mail";
|
||||||
|
|
||||||
|
export type {
|
||||||
|
HealthResponse,
|
||||||
|
SMTPStatus,
|
||||||
|
StorageStats,
|
||||||
|
ServiceStatus,
|
||||||
|
AuditEntry,
|
||||||
|
AuditResponse,
|
||||||
|
SystemStatsCPU,
|
||||||
|
SystemStatsRAM,
|
||||||
|
SystemStatsDisk,
|
||||||
|
SystemStatsMailInfo,
|
||||||
|
SystemStats,
|
||||||
|
SecurityCheck,
|
||||||
|
SecurityAuditResult,
|
||||||
|
MailLabel,
|
||||||
|
LabelRule,
|
||||||
|
CertInfo,
|
||||||
|
SelfSignedRequest,
|
||||||
|
ACMERequest,
|
||||||
|
} from "./system";
|
||||||
|
export {
|
||||||
|
getHealth,
|
||||||
|
getSMTPStatus,
|
||||||
|
getStorageStats,
|
||||||
|
getSystemStats,
|
||||||
|
getServices,
|
||||||
|
serviceAction,
|
||||||
|
getAuditLog,
|
||||||
|
getSecurityAudit,
|
||||||
|
fixSecurityIssue,
|
||||||
|
getLabels,
|
||||||
|
createLabel,
|
||||||
|
updateLabel,
|
||||||
|
deleteLabel,
|
||||||
|
assignLabel,
|
||||||
|
removeLabelFromEmail,
|
||||||
|
getMailLabelIds,
|
||||||
|
createAdminLabel,
|
||||||
|
getAdminLabels,
|
||||||
|
deleteAdminLabel,
|
||||||
|
getLabelRules,
|
||||||
|
createLabelRule,
|
||||||
|
deleteLabelRule,
|
||||||
|
getCertInfo,
|
||||||
|
uploadCert,
|
||||||
|
generateSelfSignedCert,
|
||||||
|
requestACMECert,
|
||||||
|
} from "./system";
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import { request } from "./core";
|
||||||
|
|
||||||
|
// ── Types ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export interface LDAPGroupMapping {
|
||||||
|
group_dn: string;
|
||||||
|
role: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LDAPConfig {
|
||||||
|
id?: number;
|
||||||
|
enabled: boolean;
|
||||||
|
url: string;
|
||||||
|
bind_dn: string;
|
||||||
|
bind_password: string;
|
||||||
|
base_dn: string;
|
||||||
|
user_filter: string;
|
||||||
|
tls: boolean;
|
||||||
|
tls_skip_verify: boolean;
|
||||||
|
default_role: string;
|
||||||
|
group_mappings: LDAPGroupMapping[];
|
||||||
|
updated_at?: string;
|
||||||
|
updated_by?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LDAPTestUser {
|
||||||
|
dn: string;
|
||||||
|
uid: string;
|
||||||
|
display_name: string;
|
||||||
|
mail: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LDAPTestResult {
|
||||||
|
ok: boolean;
|
||||||
|
message: string;
|
||||||
|
latency_ms: number;
|
||||||
|
server_info: string;
|
||||||
|
users_found: number;
|
||||||
|
users: LDAPTestUser[];
|
||||||
|
error_detail: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Global LDAP (superadmin) ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export async function getLDAPConfig(): Promise<LDAPConfig | null> {
|
||||||
|
try {
|
||||||
|
return await request<LDAPConfig>("/api/admin/ldap");
|
||||||
|
} catch (e: unknown) {
|
||||||
|
if (e instanceof Error && e.message.includes("404")) return null;
|
||||||
|
if (e instanceof Error && e.message.includes("no ldap config")) return null;
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function saveLDAPConfig(cfg: Partial<LDAPConfig>): Promise<void> {
|
||||||
|
await request<void>("/api/admin/ldap", {
|
||||||
|
method: "PUT",
|
||||||
|
body: JSON.stringify(cfg),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteLDAPConfig(): Promise<void> {
|
||||||
|
await request<void>("/api/admin/ldap", { method: "DELETE" });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function testLDAPConfig(
|
||||||
|
payload: { use_saved: boolean } & Partial<LDAPConfig>
|
||||||
|
): Promise<LDAPTestResult> {
|
||||||
|
return request<LDAPTestResult>("/api/admin/ldap/test", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,350 @@
|
|||||||
|
import { API_BASE, request } from "./core";
|
||||||
|
|
||||||
|
// ── Types ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export interface SearchHit {
|
||||||
|
id: string;
|
||||||
|
score: number;
|
||||||
|
from?: string;
|
||||||
|
to?: string;
|
||||||
|
subject?: string;
|
||||||
|
date?: string;
|
||||||
|
size?: number;
|
||||||
|
has_attachments?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SearchResponse {
|
||||||
|
total: number;
|
||||||
|
hits: SearchHit[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MailAttachment {
|
||||||
|
index: number;
|
||||||
|
filename: string;
|
||||||
|
content_type: string;
|
||||||
|
size: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MailDetail {
|
||||||
|
id: string;
|
||||||
|
from: string;
|
||||||
|
to: string;
|
||||||
|
cc?: string;
|
||||||
|
subject: string;
|
||||||
|
date: string;
|
||||||
|
size: number;
|
||||||
|
body_html?: string;
|
||||||
|
body_plain?: string;
|
||||||
|
raw_headers: string;
|
||||||
|
attachments: MailAttachment[];
|
||||||
|
verify_ok: boolean | null;
|
||||||
|
verified_at: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ImapFolder {
|
||||||
|
name: string;
|
||||||
|
excluded: boolean;
|
||||||
|
reason?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ImapAccount {
|
||||||
|
id: number;
|
||||||
|
owner: string;
|
||||||
|
name: string;
|
||||||
|
host: string;
|
||||||
|
port: number;
|
||||||
|
tls: string;
|
||||||
|
username: string;
|
||||||
|
excluded_folders: string[];
|
||||||
|
status: string;
|
||||||
|
error_msg: string;
|
||||||
|
last_import_at?: string;
|
||||||
|
last_import_count: number;
|
||||||
|
progress_current: number;
|
||||||
|
progress_total: number;
|
||||||
|
created_at: string;
|
||||||
|
// PROJ-8: Auto-sync fields
|
||||||
|
sync_interval_min: number;
|
||||||
|
last_sync_at?: string;
|
||||||
|
last_sync_count: number;
|
||||||
|
sync_running: boolean;
|
||||||
|
sync_status: string;
|
||||||
|
sync_error_msg: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ImapTestResult {
|
||||||
|
ok: boolean;
|
||||||
|
folders?: ImapFolder[];
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Pop3Account {
|
||||||
|
id: number;
|
||||||
|
owner: string;
|
||||||
|
name: string;
|
||||||
|
host: string;
|
||||||
|
port: number;
|
||||||
|
tls: string;
|
||||||
|
tls_skip_verify: boolean;
|
||||||
|
username: string;
|
||||||
|
status: string;
|
||||||
|
error_msg: string;
|
||||||
|
last_import_at?: string;
|
||||||
|
last_import_count: number;
|
||||||
|
progress_current: number;
|
||||||
|
progress_total: number;
|
||||||
|
created_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Pop3TestResult {
|
||||||
|
ok: boolean;
|
||||||
|
message: string;
|
||||||
|
message_count?: number;
|
||||||
|
total_size_bytes?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UploadJob {
|
||||||
|
id: string;
|
||||||
|
status: "running" | "done" | "error";
|
||||||
|
total: number;
|
||||||
|
imported: number;
|
||||||
|
skipped: number;
|
||||||
|
errors: number;
|
||||||
|
error_msg?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Search ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export async function searchEmails(params: {
|
||||||
|
q?: string;
|
||||||
|
from?: string;
|
||||||
|
to?: string;
|
||||||
|
date_from?: string;
|
||||||
|
date_to?: string;
|
||||||
|
sort?: string;
|
||||||
|
has_attachment?: boolean;
|
||||||
|
label_id?: number;
|
||||||
|
page?: number;
|
||||||
|
page_size?: number;
|
||||||
|
}): Promise<SearchResponse> {
|
||||||
|
const sp = new URLSearchParams();
|
||||||
|
if (params.q) sp.set("q", params.q);
|
||||||
|
if (params.from) sp.set("from", params.from);
|
||||||
|
if (params.to) sp.set("to", params.to);
|
||||||
|
if (params.date_from) sp.set("date_from", params.date_from);
|
||||||
|
if (params.date_to) sp.set("date_to", params.date_to);
|
||||||
|
if (params.sort) sp.set("sort", params.sort);
|
||||||
|
if (params.has_attachment !== undefined) sp.set("has_attachment", String(params.has_attachment));
|
||||||
|
if (params.label_id !== undefined) sp.set("label_id", String(params.label_id));
|
||||||
|
if (params.page) sp.set("page", String(params.page));
|
||||||
|
if (params.page_size) sp.set("page_size", String(params.page_size));
|
||||||
|
return request<SearchResponse>(`/api/search?${sp.toString()}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Mail detail ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export async function getMail(id: string): Promise<MailDetail> {
|
||||||
|
return request<MailDetail>(`/api/mails/${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function downloadMailAttachment(
|
||||||
|
id: string,
|
||||||
|
index: number
|
||||||
|
): Promise<{ blob: Blob; filename: string }> {
|
||||||
|
const res = await fetch(`${API_BASE}/api/mails/${id}/attachments/${index}`, {
|
||||||
|
credentials: "include",
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error(`Download fehlgeschlagen: ${res.status}`);
|
||||||
|
const disposition = res.headers.get("Content-Disposition") || "";
|
||||||
|
const match = disposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/);
|
||||||
|
const filename = match ? match[1].replace(/['"]/g, "") : `anhang-${index}`;
|
||||||
|
return { blob: await res.blob(), filename };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function downloadMailRaw(
|
||||||
|
id: string
|
||||||
|
): Promise<{ blob: Blob; filename: string }> {
|
||||||
|
const res = await fetch(`${API_BASE}/api/mails/${id}/raw`, {
|
||||||
|
credentials: "include",
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error(`Download fehlgeschlagen: ${res.status}`);
|
||||||
|
return { blob: await res.blob(), filename: `${id}.eml` };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── IMAP ──────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export async function getImapAccounts(): Promise<ImapAccount[]> {
|
||||||
|
return request<ImapAccount[]>("/api/imap");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createImapAccount(data: {
|
||||||
|
name: string;
|
||||||
|
host: string;
|
||||||
|
port: number;
|
||||||
|
tls: string;
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
excluded_folders: string[];
|
||||||
|
}): Promise<ImapAccount> {
|
||||||
|
return request<ImapAccount>("/api/imap", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteImapAccount(id: number): Promise<void> {
|
||||||
|
await request<void>(`/api/imap/${id}`, { method: "DELETE" });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function testImapConnection(data: {
|
||||||
|
host: string;
|
||||||
|
port: number;
|
||||||
|
tls: string;
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
}): Promise<ImapTestResult> {
|
||||||
|
return request<ImapTestResult>("/api/imap/test", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function startImapImport(id: number): Promise<ImapAccount> {
|
||||||
|
return request<ImapAccount>(`/api/imap/${id}/import`, { method: "POST" });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getImapProgress(id: number): Promise<ImapAccount> {
|
||||||
|
return request<ImapAccount>(`/api/imap/${id}/progress`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function triggerImapSync(id: number): Promise<ImapAccount> {
|
||||||
|
return request<ImapAccount>(`/api/imap/${id}/sync`, { method: "POST" });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateImapInterval(id: number, intervalMin: number): Promise<ImapAccount> {
|
||||||
|
return request<ImapAccount>(`/api/imap/${id}`, {
|
||||||
|
method: "PATCH",
|
||||||
|
body: JSON.stringify({ sync_interval_min: intervalMin }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateImapAccount(
|
||||||
|
id: number,
|
||||||
|
data: { name: string; host: string; port: number; tls: string; username: string; password?: string }
|
||||||
|
): Promise<ImapAccount> {
|
||||||
|
return request<ImapAccount>(`/api/imap/${id}`, {
|
||||||
|
method: "PATCH",
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── POP3 ──────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export async function getPop3Accounts(): Promise<Pop3Account[]> {
|
||||||
|
return request<Pop3Account[]>("/api/pop3");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createPop3Account(data: {
|
||||||
|
name: string;
|
||||||
|
host: string;
|
||||||
|
port: number;
|
||||||
|
tls: string;
|
||||||
|
tls_skip_verify: boolean;
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
}): Promise<Pop3Account> {
|
||||||
|
return request<Pop3Account>("/api/pop3", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deletePop3Account(id: number): Promise<void> {
|
||||||
|
await request<void>(`/api/pop3/${id}`, { method: "DELETE" });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function testPop3Connection(data: {
|
||||||
|
host: string;
|
||||||
|
port: number;
|
||||||
|
tls: string;
|
||||||
|
tls_skip_verify: boolean;
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
}): Promise<Pop3TestResult> {
|
||||||
|
return request<Pop3TestResult>("/api/pop3/test", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function startPop3Import(id: number): Promise<Pop3Account> {
|
||||||
|
return request<Pop3Account>(`/api/pop3/${id}/import`, { method: "POST" });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getPop3Progress(id: number): Promise<Pop3Account> {
|
||||||
|
return request<Pop3Account>(`/api/pop3/${id}/progress`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Export ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export async function exportMailPDF(id: string): Promise<{ blob: Blob; filename: string }> {
|
||||||
|
const res = await fetch(`${API_BASE}/api/export/pdf/${id}`, {
|
||||||
|
credentials: "include",
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error("PDF export failed");
|
||||||
|
const blob = await res.blob();
|
||||||
|
const cd = res.headers.get("Content-Disposition") || "";
|
||||||
|
const filename = cd.match(/filename="([^"]+)"/)?.[1] || `${id.slice(0, 16)}.pdf`;
|
||||||
|
return { blob, filename };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function exportMailsZIP(ids: string[], attachments: boolean): Promise<{ blob: Blob }> {
|
||||||
|
const res = await fetch(`${API_BASE}/api/export/zip`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
credentials: "include",
|
||||||
|
body: JSON.stringify({ ids, attachments }),
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error("ZIP export failed");
|
||||||
|
return { blob: await res.blob() };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Upload ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export async function uploadMailFiles(files: File[]): Promise<{ job_id: string }> {
|
||||||
|
const form = new FormData();
|
||||||
|
for (const f of files) form.append("files", f);
|
||||||
|
const res = await fetch(`${API_BASE}/api/admin/upload`, {
|
||||||
|
method: "POST",
|
||||||
|
credentials: "include",
|
||||||
|
body: form,
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
const body = await res.text();
|
||||||
|
throw new Error(body || `Upload failed: ${res.status}`);
|
||||||
|
}
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getUploadProgress(jobID: string): Promise<UploadJob> {
|
||||||
|
return request<UploadJob>(`/api/admin/upload/${jobID}/progress`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function uploadMailFilesUser(files: File[]): Promise<{ job_id: string }> {
|
||||||
|
const form = new FormData();
|
||||||
|
for (const f of files) form.append("files", f);
|
||||||
|
const res = await fetch(`${API_BASE}/api/upload`, {
|
||||||
|
method: "POST",
|
||||||
|
credentials: "include",
|
||||||
|
body: form,
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
const body = await res.text();
|
||||||
|
throw new Error(body || `Upload failed: ${res.status}`);
|
||||||
|
}
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getUploadProgressUser(jobID: string): Promise<UploadJob> {
|
||||||
|
return request<UploadJob>(`/api/upload/${jobID}/progress`);
|
||||||
|
}
|
||||||
@@ -0,0 +1,322 @@
|
|||||||
|
import { API_BASE, request } from "./core";
|
||||||
|
|
||||||
|
// ── Types ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export interface HealthResponse {
|
||||||
|
status: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SMTPStatus {
|
||||||
|
// global daemon fields (superadmin)
|
||||||
|
running?: boolean;
|
||||||
|
enabled?: boolean;
|
||||||
|
bind?: string;
|
||||||
|
domain?: string;
|
||||||
|
tls?: boolean;
|
||||||
|
max_size_mb?: number;
|
||||||
|
allowed_ips?: string[];
|
||||||
|
received?: number;
|
||||||
|
rejected?: number;
|
||||||
|
last_mail_at?: string;
|
||||||
|
// tenant-scoped fields (domain_admin)
|
||||||
|
tenant_only?: boolean;
|
||||||
|
domains?: string[];
|
||||||
|
total_mails?: number;
|
||||||
|
total_bytes?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StorageStats {
|
||||||
|
total_mails: number;
|
||||||
|
total_bytes: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ServiceStatus {
|
||||||
|
name: string;
|
||||||
|
display_name: string;
|
||||||
|
active: string;
|
||||||
|
sub: string;
|
||||||
|
enabled: string;
|
||||||
|
description: string;
|
||||||
|
external_blocked?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuditEntry {
|
||||||
|
id: string;
|
||||||
|
timestamp: string;
|
||||||
|
event_type: string;
|
||||||
|
username: string;
|
||||||
|
detail: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuditResponse {
|
||||||
|
total: number;
|
||||||
|
entries: AuditEntry[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SystemStatsCPU {
|
||||||
|
load1: number;
|
||||||
|
load5: number;
|
||||||
|
load15: number;
|
||||||
|
num_cpu: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SystemStatsRAM {
|
||||||
|
total_bytes: number;
|
||||||
|
used_bytes: number;
|
||||||
|
free_bytes: number;
|
||||||
|
used_pct: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SystemStatsDisk {
|
||||||
|
mount: string;
|
||||||
|
total_bytes: number;
|
||||||
|
used_bytes: number;
|
||||||
|
free_bytes: number;
|
||||||
|
used_pct: number;
|
||||||
|
fstype: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SystemStatsMailInfo {
|
||||||
|
id: string;
|
||||||
|
date: string;
|
||||||
|
from: string;
|
||||||
|
subject: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SystemStats {
|
||||||
|
cpu: SystemStatsCPU;
|
||||||
|
ram: SystemStatsRAM;
|
||||||
|
disks: SystemStatsDisk[];
|
||||||
|
archive: {
|
||||||
|
first_mail: SystemStatsMailInfo | null;
|
||||||
|
last_mail: SystemStatsMailInfo | null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SecurityCheck {
|
||||||
|
name: string;
|
||||||
|
status: "ok" | "warning" | "error";
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SecurityAuditResult {
|
||||||
|
checks: SecurityCheck[];
|
||||||
|
run_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MailLabel {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
color: string;
|
||||||
|
owner_id?: number;
|
||||||
|
tenant_id: number;
|
||||||
|
is_global: boolean;
|
||||||
|
created_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LabelRule {
|
||||||
|
id: number;
|
||||||
|
condition_field: "from_domain" | "source" | "subject_contains";
|
||||||
|
condition_value: string;
|
||||||
|
label_id: number;
|
||||||
|
tenant_id: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CertInfo {
|
||||||
|
exists: boolean;
|
||||||
|
subject?: string;
|
||||||
|
issuer?: string;
|
||||||
|
not_before?: string;
|
||||||
|
not_after?: string;
|
||||||
|
dns_names?: string[];
|
||||||
|
ip_addresses?: string[];
|
||||||
|
fingerprint_sha256?: string;
|
||||||
|
is_self_signed?: boolean;
|
||||||
|
days_remaining?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SelfSignedRequest {
|
||||||
|
common_name: string;
|
||||||
|
dns_names: string[];
|
||||||
|
ip_addresses: string[];
|
||||||
|
validity_years: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ACMERequest {
|
||||||
|
domain: string;
|
||||||
|
email: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Health & Stats ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export async function getHealth(): Promise<HealthResponse> {
|
||||||
|
return request<HealthResponse>("/api/health");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getSMTPStatus(): Promise<SMTPStatus> {
|
||||||
|
return request<SMTPStatus>("/api/admin/smtp/status");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getStorageStats(): Promise<StorageStats> {
|
||||||
|
return request<StorageStats>("/api/admin/storage/stats");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getSystemStats(): Promise<SystemStats> {
|
||||||
|
return request<SystemStats>("/api/admin/system/stats");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Services ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export async function getServices(): Promise<ServiceStatus[]> {
|
||||||
|
return request<ServiceStatus[]>("/api/admin/services");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function serviceAction(
|
||||||
|
name: string,
|
||||||
|
action: "start" | "stop" | "restart" | "enable" | "disable" | "block_external" | "allow_external"
|
||||||
|
): Promise<ServiceStatus> {
|
||||||
|
return request<ServiceStatus>(`/api/admin/services/${encodeURIComponent(name)}/action`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({ action }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Audit ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export async function getAuditLog(params: {
|
||||||
|
page?: number;
|
||||||
|
page_size?: number;
|
||||||
|
username?: string;
|
||||||
|
event_type?: string;
|
||||||
|
}): Promise<AuditResponse> {
|
||||||
|
const sp = new URLSearchParams();
|
||||||
|
if (params.page) sp.set("page", String(params.page));
|
||||||
|
if (params.page_size) sp.set("page_size", String(params.page_size));
|
||||||
|
if (params.username) sp.set("username", params.username);
|
||||||
|
if (params.event_type) sp.set("event_type", params.event_type);
|
||||||
|
return request<AuditResponse>(`/api/audit?${sp.toString()}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Security ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export async function getSecurityAudit(): Promise<SecurityAuditResult> {
|
||||||
|
return request<SecurityAuditResult>("/api/admin/security/audit");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fixSecurityIssue(action: string): Promise<{ message: string }> {
|
||||||
|
return request<{ message: string }>("/api/admin/security/fix", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({ action }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Labels ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export async function getLabels(): Promise<MailLabel[]> {
|
||||||
|
return request<MailLabel[]>("/api/labels");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createLabel(name: string, color: string): Promise<MailLabel> {
|
||||||
|
return request<MailLabel>("/api/labels", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({ name, color }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateLabel(id: number, name: string, color: string): Promise<void> {
|
||||||
|
return request<void>(`/api/labels/${id}`, {
|
||||||
|
method: "PATCH",
|
||||||
|
body: JSON.stringify({ name, color }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteLabel(id: number): Promise<void> {
|
||||||
|
return request<void>(`/api/labels/${id}`, { method: "DELETE" });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function assignLabel(emailId: string, labelId: number): Promise<void> {
|
||||||
|
return request<void>(`/api/mails/${emailId}/labels`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({ label_id: labelId }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function removeLabelFromEmail(emailId: string, labelId: number): Promise<void> {
|
||||||
|
return request<void>(`/api/mails/${emailId}/labels/${labelId}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getMailLabelIds(emailId: string): Promise<number[]> {
|
||||||
|
return request<number[]>(`/api/mails/${emailId}/labels`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createAdminLabel(name: string, color: string): Promise<MailLabel> {
|
||||||
|
return request<MailLabel>("/api/admin/labels", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({ name, color }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAdminLabels(): Promise<MailLabel[]> {
|
||||||
|
return request<MailLabel[]>("/api/admin/labels");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteAdminLabel(id: number): Promise<void> {
|
||||||
|
return request<void>(`/api/admin/labels/${id}`, { method: "DELETE" });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getLabelRules(): Promise<LabelRule[]> {
|
||||||
|
return request<LabelRule[]>("/api/admin/label-rules");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createLabelRule(
|
||||||
|
condition_field: string,
|
||||||
|
condition_value: string,
|
||||||
|
label_id: number
|
||||||
|
): Promise<LabelRule> {
|
||||||
|
return request<LabelRule>("/api/admin/label-rules", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({ condition_field, condition_value, label_id }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteLabelRule(id: number): Promise<void> {
|
||||||
|
return request<void>(`/api/admin/label-rules/${id}`, { method: "DELETE" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Certificates ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export async function getCertInfo(): Promise<CertInfo> {
|
||||||
|
return request<CertInfo>("/api/admin/cert/info");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function uploadCert(cert: File, key: File): Promise<{ ok: boolean; message: string }> {
|
||||||
|
const form = new FormData();
|
||||||
|
form.append("cert", cert);
|
||||||
|
form.append("key", key);
|
||||||
|
const res = await fetch(`${API_BASE}/api/admin/cert/upload`, {
|
||||||
|
method: "POST",
|
||||||
|
credentials: "include",
|
||||||
|
body: form,
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
const body = await res.text();
|
||||||
|
throw new Error(body || `Upload failed: ${res.status}`);
|
||||||
|
}
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateSelfSignedCert(req: SelfSignedRequest): Promise<CertInfo & { ok: boolean }> {
|
||||||
|
return request<CertInfo & { ok: boolean }>("/api/admin/cert/self-signed", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(req),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function requestACMECert(req: ACMERequest): Promise<{ ok: boolean; output: string }> {
|
||||||
|
return request<{ ok: boolean; output: string }>("/api/admin/cert/acme", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(req),
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,206 @@
|
|||||||
|
import { API_BASE, request } from "./core";
|
||||||
|
import { User } from "./users";
|
||||||
|
import { LDAPConfig, LDAPTestResult } from "./ldap";
|
||||||
|
|
||||||
|
// ── Types ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export interface Tenant {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
active: boolean;
|
||||||
|
created_at: string;
|
||||||
|
domain_count?: number;
|
||||||
|
user_count?: number;
|
||||||
|
ldap_enabled?: boolean;
|
||||||
|
ldap_url?: string;
|
||||||
|
has_logo?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TenantDomain {
|
||||||
|
id: number;
|
||||||
|
tenant_id: number;
|
||||||
|
domain: string;
|
||||||
|
created_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TenantDefaultUser {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
role: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateTenantResponse extends Tenant {
|
||||||
|
default_users: TenantDefaultUser[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TenantLDAPConfig extends LDAPConfig {
|
||||||
|
tenant_id?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LDAPSyncResult {
|
||||||
|
synced: number;
|
||||||
|
errors: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Tenant CRUD ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export async function getTenants(): Promise<Tenant[]> {
|
||||||
|
return request<Tenant[]>("/api/tenants");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getTenantUsers(tenantId: number): Promise<User[]> {
|
||||||
|
return request<User[]>(`/api/tenants/${tenantId}/users`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createTenant(name: string, slug: string): Promise<CreateTenantResponse> {
|
||||||
|
return request<CreateTenantResponse>("/api/tenants", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({ name, slug }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateTenant(
|
||||||
|
id: number,
|
||||||
|
data: { name?: string; active?: boolean }
|
||||||
|
): Promise<Tenant> {
|
||||||
|
return request<Tenant>(`/api/tenants/${id}`, {
|
||||||
|
method: "PATCH",
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteTenant(id: number): Promise<void> {
|
||||||
|
await request<void>(`/api/tenants/${id}`, { method: "DELETE" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Tenant Domains ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export async function getTenantDomains(id: number): Promise<TenantDomain[]> {
|
||||||
|
return request<TenantDomain[]>(`/api/tenants/${id}/domains`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function addTenantDomain(
|
||||||
|
tenantId: number,
|
||||||
|
domain: string
|
||||||
|
): Promise<TenantDomain> {
|
||||||
|
return request<TenantDomain>(`/api/tenants/${tenantId}/domains`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({ domain }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function removeTenantDomain(
|
||||||
|
tenantId: number,
|
||||||
|
domainId: number
|
||||||
|
): Promise<void> {
|
||||||
|
await request<void>(`/api/tenants/${tenantId}/domains/${domainId}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Tenant Logo ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export function getTenantLogoUrl(tenantId: number): string {
|
||||||
|
return `${API_BASE}/api/tenants/${tenantId}/logo`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function uploadTenantLogo(tenantId: number, file: File): Promise<void> {
|
||||||
|
const form = new FormData();
|
||||||
|
form.append("logo", file);
|
||||||
|
const res = await fetch(`${API_BASE}/api/tenants/${tenantId}/logo`, {
|
||||||
|
method: "POST",
|
||||||
|
body: form,
|
||||||
|
credentials: "include",
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
const body = await res.text();
|
||||||
|
throw new Error(body || `Upload failed: ${res.status}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteTenantLogo(tenantId: number): Promise<void> {
|
||||||
|
await request<void>(`/api/tenants/${tenantId}/logo`, { method: "DELETE" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// domain_admin: own tenant logo
|
||||||
|
export async function uploadMyTenantLogo(file: File): Promise<void> {
|
||||||
|
const form = new FormData();
|
||||||
|
form.append("logo", file);
|
||||||
|
const res = await fetch(`${API_BASE}/api/tenant/logo`, {
|
||||||
|
method: "POST",
|
||||||
|
body: form,
|
||||||
|
credentials: "include",
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
const body = await res.text();
|
||||||
|
throw new Error(body || `Upload failed: ${res.status}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteMyTenantLogo(): Promise<void> {
|
||||||
|
await request<void>("/api/tenant/logo", { method: "DELETE" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Per-Tenant LDAP (domain_admin: own tenant) ────────────────────────────────
|
||||||
|
|
||||||
|
export async function getTenantLDAPConfig(): Promise<TenantLDAPConfig | null> {
|
||||||
|
try {
|
||||||
|
return await request<TenantLDAPConfig>("/api/tenant/ldap");
|
||||||
|
} catch (e: unknown) {
|
||||||
|
if (e instanceof Error && e.message.includes("404")) return null;
|
||||||
|
if (e instanceof Error && e.message.includes("no ldap config")) return null;
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function saveTenantLDAPConfig(cfg: Partial<TenantLDAPConfig>): Promise<void> {
|
||||||
|
await request<void>("/api/tenant/ldap", { method: "PUT", body: JSON.stringify(cfg) });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteTenantLDAPConfig(): Promise<void> {
|
||||||
|
await request<void>("/api/tenant/ldap", { method: "DELETE" });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function testTenantLDAPConfig(
|
||||||
|
payload: { use_saved: boolean } & Partial<TenantLDAPConfig>
|
||||||
|
): Promise<LDAPTestResult> {
|
||||||
|
return request<LDAPTestResult>("/api/tenant/ldap/test", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Per-Tenant LDAP (superadmin: arbitrary tenant) ────────────────────────────
|
||||||
|
|
||||||
|
export async function getAdminTenantLDAPConfig(tenantID: number): Promise<TenantLDAPConfig | null> {
|
||||||
|
try {
|
||||||
|
return await request<TenantLDAPConfig>(`/api/admin/tenants/${tenantID}/ldap`);
|
||||||
|
} catch (e: unknown) {
|
||||||
|
if (e instanceof Error && e.message.includes("404")) return null;
|
||||||
|
if (e instanceof Error && e.message.includes("no ldap config")) return null;
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function saveAdminTenantLDAPConfig(tenantID: number, cfg: Partial<TenantLDAPConfig>): Promise<void> {
|
||||||
|
await request<void>(`/api/admin/tenants/${tenantID}/ldap`, { method: "PUT", body: JSON.stringify(cfg) });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteAdminTenantLDAPConfig(tenantID: number): Promise<void> {
|
||||||
|
await request<void>(`/api/admin/tenants/${tenantID}/ldap`, { method: "DELETE" });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function testAdminTenantLDAPConfig(
|
||||||
|
tenantID: number,
|
||||||
|
payload: { use_saved: boolean } & Partial<TenantLDAPConfig>
|
||||||
|
): Promise<LDAPTestResult> {
|
||||||
|
return request<LDAPTestResult>(`/api/admin/tenants/${tenantID}/ldap/test`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function syncAdminTenantLDAP(tenantID: number): Promise<LDAPSyncResult> {
|
||||||
|
return request<LDAPSyncResult>(`/api/admin/tenants/${tenantID}/ldap/sync`, { method: "POST", body: "{}" });
|
||||||
|
}
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
import { clearAuthCache } from "@/lib/auth-cache";
|
||||||
|
import { request } from "./core";
|
||||||
|
|
||||||
|
// ── Types ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export interface LoginResponse {
|
||||||
|
user: {
|
||||||
|
id: number;
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
role: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface User {
|
||||||
|
id: number;
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
role: string;
|
||||||
|
source?: string;
|
||||||
|
active: boolean;
|
||||||
|
tenant_id?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MeResponse {
|
||||||
|
username: string;
|
||||||
|
role: string;
|
||||||
|
email: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateUserRequest {
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
role: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateUserRequest {
|
||||||
|
email?: string;
|
||||||
|
role?: string;
|
||||||
|
active?: boolean;
|
||||||
|
password?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Auth functions ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export async function login(
|
||||||
|
username: string,
|
||||||
|
password: string
|
||||||
|
): Promise<LoginResponse> {
|
||||||
|
return request<LoginResponse>("/api/auth/login", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({ username, password }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getMe(): Promise<MeResponse> {
|
||||||
|
return request<MeResponse>("/api/auth/me");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function logout(): Promise<void> {
|
||||||
|
clearAuthCache();
|
||||||
|
await request<void>("/api/auth/logout", { method: "POST" });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function changePassword(
|
||||||
|
currentPassword: string,
|
||||||
|
newPassword: string
|
||||||
|
): Promise<{ ok: boolean }> {
|
||||||
|
return request<{ ok: boolean }>("/api/auth/password", {
|
||||||
|
method: "PATCH",
|
||||||
|
body: JSON.stringify({ current_password: currentPassword, new_password: newPassword }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function changeEmail(
|
||||||
|
email: string
|
||||||
|
): Promise<{ ok: boolean; email: string }> {
|
||||||
|
return request<{ ok: boolean; email: string }>("/api/auth/email", {
|
||||||
|
method: "PATCH",
|
||||||
|
body: JSON.stringify({ email }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── User management functions ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export async function getUsers(): Promise<User[]> {
|
||||||
|
return request<User[]>("/api/users");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createUser(data: CreateUserRequest): Promise<User> {
|
||||||
|
return request<User>("/api/users", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateUser(id: number, data: UpdateUserRequest): Promise<User> {
|
||||||
|
return request<User>(`/api/users/${id}`, {
|
||||||
|
method: "PATCH",
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteUser(id: number): Promise<void> {
|
||||||
|
await request<void>(`/api/users/${id}`, { method: "DELETE" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── TOTP / 2FA ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export async function getTOTPSetup(): Promise<{ secret: string; otpauth_url: string; qr_code: string }> {
|
||||||
|
return request<{ secret: string; otpauth_url: string; qr_code: string }>("/api/auth/totp/setup");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function confirmTOTPSetup(code: string): Promise<{ ok: boolean }> {
|
||||||
|
return request<{ ok: boolean }>("/api/auth/totp/setup", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({ code }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function disableTOTP(code: string): Promise<{ ok: boolean }> {
|
||||||
|
return request<{ ok: boolean }>("/api/auth/totp", {
|
||||||
|
method: "DELETE",
|
||||||
|
body: JSON.stringify({ code }),
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user