From 857ce833cf70d7dd88ac8016fea79700d70f2f10 Mon Sep 17 00:00:00 2001 From: cclohmar Date: Wed, 8 Jul 2026 15:20:41 +0100 Subject: [PATCH] fix: global settings form inside admin panel sidebar --- main.go | 117 +++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 107 insertions(+), 10 deletions(-) diff --git a/main.go b/main.go index 11c4bdd..dee408e 100644 --- a/main.go +++ b/main.go @@ -235,22 +235,39 @@ func proxyToUpstream(upstream string) http.HandlerFunc { func adminHandler(w http.ResponseWriter, r *http.Request) { apiBase := "http://127.0.0.1:8080" apiToken := os.Getenv("AUTHELIA_SECRET") + configDir := os.Getenv("CONFIG_DIR") + if configDir == "" { + configDir = "/opt/nextworkspace/config/nextworkspace" + } + + // Load settings for Global tab + settings, _ := loadSettings(configDir) + domain := os.Getenv("DOMAIN") + adminEmail := os.Getenv("TLS_EMAIL") switch r.Method { case http.MethodGet: - // List users from authelia-api - req, _ := http.NewRequest("GET", apiBase+"/api/users", nil) - req.Header.Set("Authorization", "Bearer "+apiToken) - resp, err := http.DefaultClient.Do(req) - if err != nil { - http.Error(w, fmt.Sprintf("API error: %v", err), http.StatusBadGateway) + // If settings JSON format requested + if r.URL.Query().Get("format") == "settings" { + w.Header().Set("Content-Type", "application/json") + jsonData := fmt.Sprintf(`{"company_name":"%s","language":"%s","timezone":"%s","smtp_host":"%s","smtp_port":"%d","smtp_user":"%s","smtp_sender":"%s"}`, + settings.Company.Name, settings.Company.Language, settings.Company.Timezone, + settings.SMTP.Host, settings.SMTP.Port, settings.SMTP.User, settings.SMTP.Sender) + w.Write([]byte(jsonData)) return } - defer resp.Body.Close() - body, _ := io.ReadAll(resp.Body) // If JSON format requested, return raw API response if r.URL.Query().Get("format") == "json" { + req, _ := http.NewRequest("GET", apiBase+"/api/users", nil) + req.Header.Set("Authorization", "Bearer "+apiToken) + resp, err := http.DefaultClient.Do(req) + if err != nil { + http.Error(w, fmt.Sprintf("API error: %v", err), http.StatusBadGateway) + return + } + defer resp.Body.Close() + body, _ := io.ReadAll(resp.Body) w.Header().Set("Content-Type", "application/json") w.Write(body) return @@ -258,8 +275,18 @@ func adminHandler(w http.ResponseWriter, r *http.Request) { tmpl := template.Must(template.New("admin").Parse(adminHTML)) w.Header().Set("Content-Type", "text/html; charset=utf-8") + + autheliaUp := false + if resp, err := http.Get("http://127.0.0.1:9091/api/health"); err == nil { + autheliaUp = resp.StatusCode == 200 + resp.Body.Close() + } + tmpl.Execute(w, map[string]interface{}{ - "ApiError": resp.StatusCode != 200, + "Settings": settings, + "Domain": domain, + "AdminEmail": adminEmail, + "AutheliaUp": autheliaUp, }) case http.MethodPost: @@ -744,7 +771,20 @@ const adminHTML = ` .btn-sm { padding: 0.3rem 0.6rem; font-size: 0.8rem; } input, select { padding: 0.45rem 0.7rem; border: 1px solid #e2e8f0; border-radius: 6px; font-size: 0.88rem; color: #4a5568; background: #fff; outline: none; transition: border .12s; } input:focus, select:focus { border-color: #63b3ed; box-shadow: 0 0 0 2px rgba(99,179,237,0.15); } + .field { margin-bottom: 0.9rem; } + .field label { display: block; font-size: 0.82rem; font-weight: 500; color: #4a5568; margin-bottom: 0.25rem; } + .field input, .field select { width: 100%; } + .field-row { display: flex; gap: 1rem; } + .field-row .field { flex: 1; } .form-row { display: flex; gap: 0.75rem; align-items: center; flex-wrap: wrap; margin-bottom: 1rem; } + .info-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0.5rem; } + .info-item { padding: 0.5rem 0; border-bottom: 1px solid #f7fafc; font-size: 0.9rem; } + .info-item .label { font-size: 0.78rem; color: #a0aec0; } + .info-item .value { font-size: 0.9rem; color: #4a5568; } + .status-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 0.35rem; } + .status-up { background: #48bb78; } + .status-down { background: #fc8181; } + .actions { display: flex; gap: 0.75rem; align-items: center; margin-top: 0.5rem; } .success { background: #c6f6d5; color: #276749; padding: 0.75rem 1rem; border-radius: 8px; margin-bottom: 1rem; font-size: 0.88rem; border: 1px solid #9ae6b4; } .error { background: #fed7d7; color: #c53030; padding: 0.75rem 1rem; border-radius: 8px; margin-bottom: 1rem; font-size: 0.88rem; border: 1px solid #feb2b2; } .password-box { background: #1a1a2e; color: #63b3ed; padding: 0.65rem 1rem; border-radius: 6px; font-family: 'SF Mono', 'Fira Code', monospace; font-size: 0.85rem; margin-top: 0.5rem; display: inline-block; } @@ -785,7 +825,37 @@ const adminHTML = `
-

System Information

NextWorkspace is a self-hosted productivity suite powered by Caddy + Authelia.

+
+
+

Company

+
+
+
+
+
+
+
+

SMTP

+
+
+
+
+
+
+
+
+
+
+
+

System (read-only)

+
+
Domain
{{.Domain}}
+
Admin Email
{{.AdminEmail}}
+
Authelia
{{if .AutheliaUp}}Running{{else}}Unreachable{{end}}
+
+
+
+
@@ -859,6 +929,33 @@ const adminHTML = ` const page = document.getElementById('page-' + name); if (page) page.classList.remove('hidden'); if (name === 'access') loadUsers(); + if (name === 'global') loadGlobal(); + return false; + } + + async function loadGlobal() { + const resp = await fetch('/config?format=settings'); + if (!resp.ok) return; + const s = await resp.json(); + document.querySelector('[name="company_name"]').value = s.company_name || ''; + document.querySelector('[name="language"]').value = s.language || 'en'; + document.querySelector('[name="timezone"]').value = s.timezone || 'UTC'; + document.querySelector('[name="smtp_host"]').value = s.smtp_host || ''; + document.querySelector('[name="smtp_port"]').value = s.smtp_port || '587'; + document.querySelector('[name="smtp_user"]').value = s.smtp_user || ''; + document.querySelector('[name="smtp_sender"]').value = s.smtp_sender || ''; + } + + async function saveGlobal(e) { + e.preventDefault(); + const form = document.getElementById('global-form'); + const data = new FormData(form); + const resp = await fetch('/config/global/save', {method:'POST', body:new URLSearchParams(data)}); + const msg = document.getElementById('savemsg'); + if (resp.ok) { + msg.style.display = 'inline'; + setTimeout(() => msg.style.display = 'none', 3000); + } return false; }