package admin import ( "context" "encoding/json" "strings" "time" "github.com/jackc/pgx/v5" "github.com/ultisuite/ulti-backend/internal/config" "github.com/ultisuite/ulti-backend/internal/authentik" "github.com/ultisuite/ulti-backend/internal/orgpolicy" ) const orgSettingsSingletonID = 1 func defaultOrgPolicy() map[string]any { return map[string]any{ "authentik": map[string]any{ "enabled": true, "api_url": "", "slug": "ulti-suite", "client_id": "", "enforce_sso": true, "allow_password_fallback": false, "default_groups": "ulti-users", }, "identity_providers": map[string]any{ "allow_self_enrollment": true, "default_login_source": "", "providers": []any{}, }, "two_factor": map[string]any{ "required_for_all": false, "required_for_admins": true, "allowed_methods": []any{"totp", "webauthn"}, "grace_period_days": 7, "remember_device_days": 30, }, "storage_quotas": map[string]any{ "default_mail_gib": 5, "default_drive_gib": 5, "default_photos_gib": 5, "warn_threshold_pct": 90, }, "usage_quotas": map[string]any{ "llm_requests_per_day": 100, "llm_tokens_per_month": 500000, "search_requests_per_day": 50, "max_api_tokens_per_user": 10, "max_webhooks_per_user": 20, }, "file_policies": map[string]any{ "max_upload_mib": 512, "allowed_extensions": "", "block_executable": true, "external_sharing": "authenticated", "default_link_expiry_days": 30, "virus_scan_enabled": false, "virustotal_api_key": "", "retention_trash_days": 30, "mount_oauth": orgpolicy.DefaultMountOAuthSection(), }, "llm": map[string]any{ "default_provider_id": "", "providers": []any{}, "enforce_org_providers": false, "allow_user_override": true, }, "search": map[string]any{ "suite_engine": "postgres", "meilisearch_url": "", "meilisearch_api_key": "", "typesense_url": "", "typesense_api_key": "", "web_search": map[string]any{ "default_provider_id": "brave-default", "providers": []any{}, }, "enforce_org_search": false, }, "administrators": []any{}, "nextcloud": map[string]any{ "enabled": false, "base_url": "", "admin_user": "", "admin_password": "", "drive_enabled": true, "calendar_enabled": true, "contacts_enabled": true, "talk_enabled": false, }, "mailing": map[string]any{ "enabled": false, "smtp_host": "", "smtp_port": 587, "smtp_user": "", "smtp_password": "", "from_email": "noreply@example.com", "from_name": "Ulti Suite", "tls_mode": "starttls", }, "onlyoffice": map[string]any{ "enabled": false, "document_server_url": "", "jwt_secret": "", "jwt_header": "Authorization", }, "richtext": map[string]any{ "enabled": true, "storage_mode": "sidecar", "export_mirror_format": "", "hocuspocus_url": "", }, "ai_assistant": map[string]any{ "enabled": false, "openwebui_internal_url": "", "public_path": "/ai", "embed_default_temporary": true, "default_model": "", "enabled_tools": []any{"mail", "drive", "contacts", "search"}, "chat_sync_enabled": true, "chat_nc_path": "/.ultimail/ai/chats", "models": []any{}, }, "agenda": map[string]any{ "default_theme_mode": "system", "enforce_org_theme": false, "default_video_provider": "ultimeet", "enforce_org_video_provider": false, "video_provider_api_keys": map[string]any{}, }, "meet": map[string]any{ "transcription_enabled": false, "transcription_mode": "live", "transcription_engine": "faster_whisper_local", "skynet_url": "http://skynet:8000", "whisper_model": "tiny", "external_api_url": "", "external_api_provider": "openai_compatible", "external_api_key": "", "auto_start_transcription": false, "post_actions": map[string]any{ "email_enabled": false, "email_recipients": "organizer", "email_custom_addresses": "", "drive_enabled": true, "drive_folder_path": "/UltiMeet/Transcripts", "llm_enabled": false, "llm_provider_id": "", "llm_prompt": "Résume cette réunion en français : points clés, décisions et actions à suivre.", "llm_then_email": true, "llm_then_drive": true, }, }, "plugins": []any{ map[string]any{"id": "mail-automation", "name": "Automatisations mail", "description": "Règles, webhooks et tri IA sur la réception.", "enabled": true, "version": "1.0.0"}, map[string]any{"id": "contact-discovery", "name": "Découverte contacts", "description": "Enrichissement IA et signatures détectées.", "enabled": true, "version": "1.0.0"}, map[string]any{"id": "public-share", "name": "Partage public Drive", "description": "Liens publics et partages externes.", "enabled": true, "version": "1.0.0"}, map[string]any{"id": "office-editor", "name": "Édition OnlyOffice", "description": "Édition collaborative de documents.", "enabled": false, "version": "1.0.0"}, map[string]any{"id": "ai-assistant", "name": "UltiAI", "description": "Assistant IA intégré (chat, tools mail/drive/contacts).", "enabled": false, "version": "1.0.0"}, }, "integrations": []any{ map[string]any{"id": "authentik", "name": "Authentik", "description": "SSO, groupes et provisionnement des comptes.", "enabled": true, "configured": false}, map[string]any{"id": "nextcloud", "name": "Nextcloud", "description": "Drive, agenda, contacts et Talk.", "enabled": false, "configured": false}, map[string]any{"id": "onlyoffice", "name": "OnlyOffice", "description": "Édition de documents dans le navigateur.", "enabled": false, "configured": false}, map[string]any{"id": "smtp", "name": "Mailing unifié", "description": "SMTP pour notifications suite (partages, mentions).", "enabled": false, "configured": false}, }, } } func (s *Service) loadOrgPolicyRaw(ctx context.Context) (map[string]any, time.Time, string, error) { var raw []byte var updatedAt time.Time var updatedBy string err := s.db.QueryRow(ctx, ` SELECT settings, updated_at, updated_by FROM org_settings WHERE id = $1 `, orgSettingsSingletonID).Scan(&raw, &updatedAt, &updatedBy) if err != nil { if err == pgx.ErrNoRows { return map[string]any{}, time.Time{}, "", nil } return nil, time.Time{}, "", err } stored := map[string]any{} if len(raw) > 0 { if err := json.Unmarshal(raw, &stored); err != nil { return nil, time.Time{}, "", err } } return stored, updatedAt, updatedBy, nil } func mergeMaps(base, patch map[string]any) map[string]any { if base == nil { base = map[string]any{} } out := make(map[string]any, len(base)+len(patch)) for k, v := range base { out[k] = v } for k, patchVal := range patch { if patchVal == nil { continue } baseVal, ok := out[k] if !ok { out[k] = patchVal continue } baseMap, baseOK := baseVal.(map[string]any) patchMap, patchOK := patchVal.(map[string]any) if baseOK && patchOK { out[k] = mergeMaps(baseMap, patchMap) continue } out[k] = patchVal } return out } func mergeOrgSecrets(existing, patch map[string]any) map[string]any { merged := mergeMaps(existing, patch) secretPaths := []struct { section string key string }{ {"nextcloud", "admin_password"}, {"mailing", "smtp_password"}, {"onlyoffice", "jwt_secret"}, {"search", "meilisearch_api_key"}, {"search", "typesense_api_key"}, {"file_policies", "virustotal_api_key"}, } for _, p := range secretPaths { patchSection, _ := patch[p.section].(map[string]any) if patchSection == nil { continue } val, _ := patchSection[p.key].(string) if strings.TrimSpace(val) == "" { if existingSection, ok := existing[p.section].(map[string]any); ok { if existingVal, ok := existingSection[p.key].(string); ok && existingVal != "" { mergedSection, _ := merged[p.section].(map[string]any) if mergedSection == nil { mergedSection = map[string]any{} } mergedSection[p.key] = existingVal merged[p.section] = mergedSection } } } } if patchLLM, ok := patch["llm"].(map[string]any); ok { mergeLLMProviderSecrets(existing, patchLLM, merged) } if patchSearch, ok := patch["search"].(map[string]any); ok { if patchWS, ok := patchSearch["web_search"].(map[string]any); ok { mergeSearchProviderSecrets(existing, patchWS, merged) } } if patchIDP, ok := patch["identity_providers"].(map[string]any); ok { mergeIdentityProviderSecrets(existing, patchIDP, merged) } if patchAgenda, ok := patch["agenda"].(map[string]any); ok { mergeAgendaProviderSecrets(existing, patchAgenda, merged) } if patchMeet, ok := patch["meet"].(map[string]any); ok { mergeMeetSecrets(existing, patchMeet, merged) } if patchFilePolicies, ok := patch["file_policies"].(map[string]any); ok { mergeMountOAuthSecrets(existing, patchFilePolicies, merged) } return merged } func mergeMountOAuthSecrets(existing, patchFilePolicies, merged map[string]any) { patchMountOAuth, _ := patchFilePolicies["mount_oauth"].(map[string]any) if patchMountOAuth == nil { return } existingFilePolicies, _ := existing["file_policies"].(map[string]any) existingMountOAuth, _ := existingFilePolicies["mount_oauth"].(map[string]any) mergedFilePolicies, _ := merged["file_policies"].(map[string]any) if mergedFilePolicies == nil { return } out := map[string]any{} for k, v := range patchMountOAuth { if k == "redirect_uri" { out[k] = v continue } providerPatch, ok := v.(map[string]any) if !ok { out[k] = v continue } mergedProvider := map[string]any{} for pk, pv := range providerPatch { mergedProvider[pk] = pv } if secret, _ := providerPatch["client_secret"].(string); strings.TrimSpace(secret) == "" { if existingMountOAuth != nil { if existingProvider, ok := existingMountOAuth[k].(map[string]any); ok { if prev, ok := existingProvider["client_secret"].(string); ok && prev != "" { mergedProvider["client_secret"] = prev } } } } out[k] = mergedProvider } mergedFilePolicies["mount_oauth"] = out merged["file_policies"] = mergedFilePolicies } func mergeMeetSecrets(existing, patchMeet, merged map[string]any) { if strings.TrimSpace(stringValueMap(patchMeet, "external_api_key")) != "" { return } existingMeet, _ := existing["meet"].(map[string]any) prev := stringValueMap(existingMeet, "external_api_key") if prev == "" { return } mergedMeet, _ := merged["meet"].(map[string]any) if mergedMeet == nil { return } mergedMeet["external_api_key"] = prev merged["meet"] = mergedMeet } func stringValueMap(m map[string]any, key string) string { if m == nil { return "" } s, _ := m[key].(string) return s } func mergeAgendaProviderSecrets(existing, patchAgenda, merged map[string]any) { patchKeys, _ := patchAgenda["video_provider_api_keys"].(map[string]any) if len(patchKeys) == 0 { return } existingAgenda, _ := existing["agenda"].(map[string]any) existingKeys, _ := existingAgenda["video_provider_api_keys"].(map[string]any) mergedAgenda, _ := merged["agenda"].(map[string]any) if mergedAgenda == nil { return } outKeys := map[string]any{} for k, v := range patchKeys { s, _ := v.(string) if strings.TrimSpace(s) == "" { if existingKeys != nil { if prev, ok := existingKeys[k].(string); ok && prev != "" { outKeys[k] = prev continue } } } outKeys[k] = v } mergedAgenda["video_provider_api_keys"] = outKeys merged["agenda"] = mergedAgenda } func mergeLLMProviderSecrets(existing, patchLLM, merged map[string]any) { patchProviders, _ := patchLLM["providers"].([]any) if len(patchProviders) == 0 { return } existingLLM, _ := existing["llm"].(map[string]any) existingProviders, _ := existingLLM["providers"].([]any) mergedLLM, _ := merged["llm"].(map[string]any) if mergedLLM == nil { return } mergedProviders := make([]any, len(patchProviders)) for i, item := range patchProviders { pm, ok := item.(map[string]any) if !ok { mergedProviders[i] = item continue } id, _ := pm["id"].(string) apiKey, _ := pm["api_key"].(string) if strings.TrimSpace(apiKey) == "" && id != "" { for _, ep := range existingProviders { em, ok := ep.(map[string]any) if !ok { continue } if eid, _ := em["id"].(string); eid == id { if ek, ok := em["api_key"].(string); ok && ek != "" { pm["api_key"] = ek } } } } mergedProviders[i] = pm } mergedLLM["providers"] = mergedProviders merged["llm"] = mergedLLM } func mergeSearchProviderSecrets(existing map[string]any, patchWS map[string]any, merged map[string]any) { patchProviders, _ := patchWS["providers"].([]any) if len(patchProviders) == 0 { return } existingSearch, _ := existing["search"].(map[string]any) existingWS, _ := existingSearch["web_search"].(map[string]any) existingProviders, _ := existingWS["providers"].([]any) mergedSearch, _ := merged["search"].(map[string]any) if mergedSearch == nil { return } mergedWS, _ := mergedSearch["web_search"].(map[string]any) if mergedWS == nil { mergedWS = map[string]any{} } mergedProviders := make([]any, len(patchProviders)) for i, item := range patchProviders { pm, ok := item.(map[string]any) if !ok { mergedProviders[i] = item continue } id, _ := pm["id"].(string) apiKey, _ := pm["api_key"].(string) if strings.TrimSpace(apiKey) == "" && id != "" { for _, ep := range existingProviders { em, ok := ep.(map[string]any) if !ok { continue } if eid, _ := em["id"].(string); eid == id { if ek, ok := em["api_key"].(string); ok && ek != "" { pm["api_key"] = ek } } } } mergedProviders[i] = pm } mergedWS["providers"] = mergedProviders mergedSearch["web_search"] = mergedWS merged["search"] = mergedSearch } func mergeIdentityProviderSecrets(existing, patchIDP, merged map[string]any) { patchProviders, _ := patchIDP["providers"].([]any) if len(patchProviders) == 0 { return } existingIDP, _ := existing["identity_providers"].(map[string]any) existingProviders, _ := existingIDP["providers"].([]any) mergedIDP, _ := merged["identity_providers"].(map[string]any) if mergedIDP == nil { return } mergedProviders := make([]any, len(patchProviders)) for i, item := range patchProviders { pm, ok := item.(map[string]any) if !ok { mergedProviders[i] = item continue } id, _ := pm["id"].(string) for _, secretPath := range []struct{ section, key string }{ {"oauth", "client_secret"}, {"ldap", "bind_password"}, {"saml", "signing_cert"}, } { mergeProviderSecretField(pm, existingProviders, id, secretPath.section, secretPath.key) } mergedProviders[i] = pm } mergedIDP["providers"] = mergedProviders merged["identity_providers"] = mergedIDP } func mergeProviderSecretField(pm map[string]any, existingProviders []any, id, section, key string) { nested, _ := pm[section].(map[string]any) if nested == nil { return } val, _ := nested[key].(string) if strings.TrimSpace(val) != "" || id == "" { return } for _, ep := range existingProviders { em, ok := ep.(map[string]any) if !ok { continue } if eid, _ := em["id"].(string); eid != id { continue } esec, _ := em[section].(map[string]any) if esec == nil { continue } if ek, ok := esec[key].(string); ok && ek != "" { nested[key] = ek } } } func maskOrgPolicy(policy map[string]any) map[string]any { cloned := deepCloneMap(policy) maskStringField(cloned, "nextcloud", "admin_password") maskStringField(cloned, "mailing", "smtp_password") maskStringField(cloned, "onlyoffice", "jwt_secret") maskStringField(cloned, "search", "meilisearch_api_key") maskStringField(cloned, "search", "typesense_api_key") maskStringField(cloned, "file_policies", "virustotal_api_key") if filePolicies, ok := cloned["file_policies"].(map[string]any); ok { if mountOAuth, ok := filePolicies["mount_oauth"].(map[string]any); ok { for _, provider := range []string{"google", "dropbox", "microsoft"} { if section, ok := mountOAuth[provider].(map[string]any); ok { if secret, _ := section["client_secret"].(string); strings.TrimSpace(secret) != "" { section["client_secret"] = "" } mountOAuth[provider] = section } } filePolicies["mount_oauth"] = mountOAuth } } if agenda, ok := cloned["agenda"].(map[string]any); ok { if keys, ok := agenda["video_provider_api_keys"].(map[string]any); ok { for k, v := range keys { if s, ok := v.(string); ok && strings.TrimSpace(s) != "" { keys[k] = "" } } agenda["video_provider_api_keys"] = keys } } if meet, ok := cloned["meet"].(map[string]any); ok { if v, _ := meet["external_api_key"].(string); strings.TrimSpace(v) != "" { meet["external_api_key"] = "" } } if llm, ok := cloned["llm"].(map[string]any); ok { if providers, ok := llm["providers"].([]any); ok { for i, p := range providers { pm, ok := p.(map[string]any) if !ok { continue } if v, _ := pm["api_key"].(string); strings.TrimSpace(v) != "" { pm["api_key"] = "" } providers[i] = pm } llm["providers"] = providers } } if search, ok := cloned["search"].(map[string]any); ok { if ws, ok := search["web_search"].(map[string]any); ok { if providers, ok := ws["providers"].([]any); ok { for i, p := range providers { pm, ok := p.(map[string]any) if !ok { continue } if v, _ := pm["api_key"].(string); strings.TrimSpace(v) != "" { pm["api_key"] = "" } providers[i] = pm } ws["providers"] = providers } } } maskIdentityProviderSecrets(cloned) return cloned } func maskIdentityProviderSecrets(policy map[string]any) { idp, ok := policy["identity_providers"].(map[string]any) if !ok { return } providers, ok := idp["providers"].([]any) if !ok { return } for i, item := range providers { pm, ok := item.(map[string]any) if !ok { continue } for _, section := range []string{"oauth", "ldap", "saml"} { nested, ok := pm[section].(map[string]any) if !ok { continue } for _, key := range []string{"client_secret", "bind_password", "signing_cert"} { if v, _ := nested[key].(string); strings.TrimSpace(v) != "" { nested[key] = "" } } pm[section] = nested } providers[i] = pm } idp["providers"] = providers } func maskStringField(m map[string]any, section, key string) { sec, ok := m[section].(map[string]any) if !ok { return } if v, _ := sec[key].(string); strings.TrimSpace(v) != "" { sec[key] = "" } } func deepCloneMap(in map[string]any) map[string]any { raw, _ := json.Marshal(in) out := map[string]any{} _ = json.Unmarshal(raw, &out) return out } func secretConfigured(policy map[string]any, section, key string) bool { sec, ok := policy[section].(map[string]any) if !ok { return false } v, _ := sec[key].(string) return strings.TrimSpace(v) != "" } func mountOAuthProviderSecretConfigured(policy map[string]any, provider string) bool { fp, ok := policy["file_policies"].(map[string]any) if !ok { return false } mo, ok := fp["mount_oauth"].(map[string]any) if !ok { return false } section, ok := mo[provider].(map[string]any) if !ok { return false } enabled, _ := section["enabled"].(bool) if !enabled { return false } clientID, _ := section["client_id"].(string) clientSecret, _ := section["client_secret"].(string) return strings.TrimSpace(clientID) != "" && strings.TrimSpace(clientSecret) != "" } func buildOrgSecretsStatus(policy map[string]any, cfg *config.Config) map[string]any { secrets := map[string]any{ "nextcloud_admin_password": map[string]any{ "configured": secretConfigured(policy, "nextcloud", "admin_password") || strings.TrimSpace(cfg.NCAdminPass) != "", }, "mailing_smtp_password": map[string]any{ "configured": secretConfigured(policy, "mailing", "smtp_password"), }, "onlyoffice_jwt_secret": map[string]any{ "configured": secretConfigured(policy, "onlyoffice", "jwt_secret") || strings.TrimSpace(cfg.OnlyOfficeJWTSecret) != "", }, "meilisearch_api_key": map[string]any{ "configured": secretConfigured(policy, "search", "meilisearch_api_key") || strings.TrimSpace(cfg.MeilisearchKey) != "", }, "typesense_api_key": map[string]any{ "configured": secretConfigured(policy, "search", "typesense_api_key") || strings.TrimSpace(cfg.TypesenseKey) != "", }, "virustotal_api_key": map[string]any{ "configured": secretConfigured(policy, "file_policies", "virustotal_api_key") || strings.TrimSpace(cfg.VirusTotalAPIKey) != "", }, "mount_oauth_google": map[string]any{ "configured": mountOAuthProviderSecretConfigured(policy, "google"), }, "mount_oauth_dropbox": map[string]any{ "configured": mountOAuthProviderSecretConfigured(policy, "dropbox"), }, "mount_oauth_microsoft": map[string]any{ "configured": mountOAuthProviderSecretConfigured(policy, "microsoft"), }, } if idpSecrets := buildIdentityProviderSecretsStatus(policy); len(idpSecrets) > 0 { secrets["identity_providers"] = idpSecrets } if llmSecrets := buildLLMProviderSecretsStatus(policy); len(llmSecrets) > 0 { secrets["llm_providers"] = llmSecrets } return secrets } func buildIdentityProviderSecretsStatus(policy map[string]any) map[string]any { idp, ok := policy["identity_providers"].(map[string]any) if !ok { return nil } providers, ok := idp["providers"].([]any) if !ok { return nil } out := make(map[string]any) for _, item := range providers { pm, ok := item.(map[string]any) if !ok { continue } id, _ := pm["id"].(string) if id == "" { continue } entry := map[string]any{} if oauth, ok := pm["oauth"].(map[string]any); ok { if v, _ := oauth["client_secret"].(string); strings.TrimSpace(v) != "" { entry["oauth_client_secret"] = map[string]any{"configured": true} } } if ldap, ok := pm["ldap"].(map[string]any); ok { if v, _ := ldap["bind_password"].(string); strings.TrimSpace(v) != "" { entry["ldap_bind_password"] = map[string]any{"configured": true} } } if saml, ok := pm["saml"].(map[string]any); ok { if v, _ := saml["signing_cert"].(string); strings.TrimSpace(v) != "" { entry["saml_signing_cert"] = map[string]any{"configured": true} } } if len(entry) > 0 { out[id] = entry } } return out } func buildOrgEffective(cfg *config.Config) map[string]any { authentikEnabled := strings.TrimSpace(cfg.AuthentikAPIToken) != "" || strings.TrimSpace(cfg.OIDCIssuer) != "" publicBase := authentik.AuthentikPublicBaseURL(cfg.AuthentikAPIURL, cfg.AuthentikPublicHTTPS) return map[string]any{ "authentik": map[string]any{ "enabled": authentikEnabled, "api_url": cfg.AuthentikAPIURL, "client_id": cfg.OIDCClientID, "issuer": cfg.OIDCIssuer, }, "identity_providers": map[string]any{ "authentik_public_url": publicBase, "oauth_redirect_template": publicBase + "/source/oauth/callback/{slug}/", }, "nextcloud": map[string]any{ "enabled": cfg.NextcloudEnabled, "base_url": firstNonEmpty(cfg.NextcloudPublicURL, cfg.NextcloudURL), "admin_user": cfg.NCAdminUser, }, "onlyoffice": map[string]any{ "enabled": cfg.OnlyOfficeEnabled, "document_server_url": firstNonEmpty(cfg.OnlyOfficePublicURL, cfg.OnlyOfficeURL), }, "search": map[string]any{ "suite_engine": cfg.SearchEngine, "meilisearch_url": cfg.MeilisearchURL, "typesense_url": cfg.TypesenseURL, }, "immich": map[string]any{ "enabled": cfg.ImmichEnabled, "api_url": cfg.ImmichAPIURL, }, "jitsi": map[string]any{ "enabled": cfg.JitsiEnabled, "public_url": cfg.JitsiPublicURL, }, "ai_assistant": map[string]any{ "enabled": cfg.AIAssistantEnabled, "openwebui_internal_url": cfg.OpenWebUIInternalURL, "public_path": cfg.AIAssistantPublicPath, }, } } func firstNonEmpty(values ...string) string { for _, v := range values { if strings.TrimSpace(v) != "" { return v } } return "" } func (s *Service) GetOrgSettings(ctx context.Context) (map[string]any, error) { stored, updatedAt, updatedBy, err := s.loadOrgPolicyRaw(ctx) if err != nil { return nil, err } policy := mergeMaps(defaultOrgPolicy(), stored) masked := maskOrgPolicy(policy) return map[string]any{ "policy": masked, "effective": buildOrgEffective(s.cfg), "secrets": buildOrgSecretsStatus(policy, s.cfg), "env_vars": buildOrgEnvVars(), "deploy_locked": buildOrgDeployLocked(s.cfg), "updated_at": updatedAt.UTC().Format(time.RFC3339), "updated_by": updatedBy, }, nil } func (s *Service) PutOrgSettings(ctx context.Context, actorSub string, patch map[string]any) (map[string]any, error) { storedBefore, _, _, err := s.loadOrgPolicyRaw(ctx) if err != nil { return nil, err } merged := mergeOrgSecrets(storedBefore, patch) raw, err := json.Marshal(merged) if err != nil { return nil, err } _, err = s.db.Exec(ctx, ` INSERT INTO org_settings (id, settings, updated_at, updated_by) VALUES ($1, $2, NOW(), $3) ON CONFLICT (id) DO UPDATE SET settings = EXCLUDED.settings, updated_at = NOW(), updated_by = EXCLUDED.updated_by `, orgSettingsSingletonID, raw, actorSub) if err != nil { return nil, err } s.logAudit(ctx, actorSub, "update_org_settings", map[string]any{ "sections": mapKeys(patch), }) if _, ok := patch["identity_providers"]; ok { removed := authentik.RemovedIdentityProviders(storedBefore, merged) syncer := authentik.NewSourceSyncer(s.db, s.cfg) idpSection, _ := merged["identity_providers"].(map[string]any) if err := syncer.SyncSectionWithRemoved(ctx, idpSection, removed); err != nil { s.logger.Warn("identity provider sync failed", "error", err) } } return s.GetOrgSettings(ctx) } func mapKeys(m map[string]any) []string { keys := make([]string, 0, len(m)) for k := range m { keys = append(keys, k) } return keys } const gibBytes = int64(1024 * 1024 * 1024) func policyGibValue(v any, fallback int64) int64 { switch n := v.(type) { case float64: if n > 0 { return int64(n * float64(gibBytes)) } case int: if n > 0 { return int64(n) * gibBytes } case int64: if n > 0 { return n * gibBytes } } return fallback * gibBytes } func (s *Service) applyOrgDefaultQuotas(ctx context.Context, userID string) error { stored, _, _, err := s.loadOrgPolicyRaw(ctx) if err != nil { return err } policy := mergeMaps(defaultOrgPolicy(), stored) storage, _ := policy["storage_quotas"].(map[string]any) const defaultGib = int64(5) mail := policyGibValue(storage["default_mail_gib"], defaultGib) drive := policyGibValue(storage["default_drive_gib"], defaultGib) photos := policyGibValue(storage["default_photos_gib"], defaultGib) _, err = s.db.Exec(ctx, ` INSERT INTO settings (user_id, preferences) VALUES ( $1, jsonb_build_object( 'mail_max_storage_bytes', $2::text, 'drive_max_storage_bytes', $3::text, 'photos_max_storage_bytes', $4::text ) ) ON CONFLICT (user_id) DO NOTHING `, userID, mail, drive, photos) return err }