fix: global settings form inside admin panel sidebar
This commit is contained in:
parent
e1e8ad0f08
commit
857ce833cf
1 changed files with 107 additions and 10 deletions
109
main.go
109
main.go
|
|
@ -235,10 +235,30 @@ func proxyToUpstream(upstream string) http.HandlerFunc {
|
||||||
func adminHandler(w http.ResponseWriter, r *http.Request) {
|
func adminHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
apiBase := "http://127.0.0.1:8080"
|
apiBase := "http://127.0.0.1:8080"
|
||||||
apiToken := os.Getenv("AUTHELIA_SECRET")
|
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 {
|
switch r.Method {
|
||||||
case http.MethodGet:
|
case http.MethodGet:
|
||||||
// List users from authelia-api
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// If JSON format requested, return raw API response
|
||||||
|
if r.URL.Query().Get("format") == "json" {
|
||||||
req, _ := http.NewRequest("GET", apiBase+"/api/users", nil)
|
req, _ := http.NewRequest("GET", apiBase+"/api/users", nil)
|
||||||
req.Header.Set("Authorization", "Bearer "+apiToken)
|
req.Header.Set("Authorization", "Bearer "+apiToken)
|
||||||
resp, err := http.DefaultClient.Do(req)
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
|
@ -248,9 +268,6 @@ func adminHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
body, _ := io.ReadAll(resp.Body)
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
|
||||||
// If JSON format requested, return raw API response
|
|
||||||
if r.URL.Query().Get("format") == "json" {
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
w.Write(body)
|
w.Write(body)
|
||||||
return
|
return
|
||||||
|
|
@ -258,8 +275,18 @@ func adminHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
tmpl := template.Must(template.New("admin").Parse(adminHTML))
|
tmpl := template.Must(template.New("admin").Parse(adminHTML))
|
||||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
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{}{
|
tmpl.Execute(w, map[string]interface{}{
|
||||||
"ApiError": resp.StatusCode != 200,
|
"Settings": settings,
|
||||||
|
"Domain": domain,
|
||||||
|
"AdminEmail": adminEmail,
|
||||||
|
"AutheliaUp": autheliaUp,
|
||||||
})
|
})
|
||||||
|
|
||||||
case http.MethodPost:
|
case http.MethodPost:
|
||||||
|
|
@ -744,7 +771,20 @@ const adminHTML = `<!DOCTYPE html>
|
||||||
.btn-sm { padding: 0.3rem 0.6rem; font-size: 0.8rem; }
|
.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, 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); }
|
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; }
|
.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; }
|
.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; }
|
.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; }
|
.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 = `<!DOCTYPE html>
|
||||||
<!-- Global -->
|
<!-- Global -->
|
||||||
<div id="page-global" class="page">
|
<div id="page-global" class="page">
|
||||||
<div class="page-header"><h2>Global Settings</h2><p>General system configuration for your workspace.</p></div>
|
<div class="page-header"><h2>Global Settings</h2><p>General system configuration for your workspace.</p></div>
|
||||||
<div class="card"><h3>System Information</h3><p style="color:#718096;font-size:0.9rem;">NextWorkspace is a self-hosted productivity suite powered by Caddy + Authelia.</p></div>
|
<form id="global-form" onsubmit="return saveGlobal(event)">
|
||||||
|
<div class="card">
|
||||||
|
<h3>Company</h3>
|
||||||
|
<div class="field"><label>Company Name</label><input type="text" name="company_name" value="{{.Settings.Company.Name}}"></div>
|
||||||
|
<div class="field-row">
|
||||||
|
<div class="field"><label>Language</label><select name="language"><option value="en" {{if eq .Settings.Company.Language "en"}}selected{{end}}>English</option><option value="de" {{if eq .Settings.Company.Language "de"}}selected{{end}}>Deutsch</option></select></div>
|
||||||
|
<div class="field"><label>Timezone</label><select name="timezone">{{$tz := .Settings.Company.Timezone}}<option value="UTC" {{if eq $tz "UTC"}}selected{{end}}>UTC</option><option value="Europe/London" {{if eq $tz "Europe/London"}}selected{{end}}>Europe/London</option><option value="Europe/Berlin" {{if eq $tz "Europe/Berlin"}}selected{{end}}>Europe/Berlin</option><option value="America/New_York" {{if eq $tz "America/New_York"}}selected{{end}}>America/New_York</option><option value="Asia/Tokyo" {{if eq $tz "Asia/Tokyo"}}selected{{end}}>Asia/Tokyo</option></select></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h3>SMTP</h3>
|
||||||
|
<div class="field"><label>SMTP Host</label><input type="text" name="smtp_host" value="{{.Settings.SMTP.Host}}" placeholder="smtp.openxchange.eu"></div>
|
||||||
|
<div class="field-row">
|
||||||
|
<div class="field"><label>SMTP Port</label><input type="number" name="smtp_port" value="{{.Settings.SMTP.Port}}" placeholder="587"></div>
|
||||||
|
<div class="field"><label>SMTP User</label><input type="text" name="smtp_user" value="{{.Settings.SMTP.User}}" placeholder="post@nextwks.eu"></div>
|
||||||
|
</div>
|
||||||
|
<div class="field-row">
|
||||||
|
<div class="field"><label>SMTP Password</label><input type="password" name="smtp_password" placeholder="Leave blank to keep current"></div>
|
||||||
|
<div class="field"><label>Sender Email</label><input type="email" name="smtp_sender" value="{{.Settings.SMTP.Sender}}" placeholder="post@nextwks.eu"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h3>System (read-only)</h3>
|
||||||
|
<div class="info-grid">
|
||||||
|
<div class="info-item"><div class="label">Domain</div><div class="value">{{.Domain}}</div></div>
|
||||||
|
<div class="info-item"><div class="label">Admin Email</div><div class="value">{{.AdminEmail}}</div></div>
|
||||||
|
<div class="info-item"><div class="label">Authelia</div><div class="value">{{if .AutheliaUp}}<span class="status-dot status-up"></span>Running{{else}}<span class="status-dot status-down"></span>Unreachable{{end}}</div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="actions"><button type="submit" class="btn btn-primary">Save Settings</button><span id="savemsg" style="display:none;color:#48bb78;font-size:0.9rem;">Saved</span></div>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Access (Users) -->
|
<!-- Access (Users) -->
|
||||||
|
|
@ -859,6 +929,33 @@ const adminHTML = `<!DOCTYPE html>
|
||||||
const page = document.getElementById('page-' + name);
|
const page = document.getElementById('page-' + name);
|
||||||
if (page) page.classList.remove('hidden');
|
if (page) page.classList.remove('hidden');
|
||||||
if (name === 'access') loadUsers();
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue