feat: SMTP config, install prompts, API proxy, global settings page
This commit is contained in:
parent
707eb9ec81
commit
3e04d8f1c0
4 changed files with 369 additions and 6 deletions
|
|
@ -119,5 +119,14 @@ storage:
|
||||||
path: /data/db.sqlite
|
path: /data/db.sqlite
|
||||||
|
|
||||||
notifier:
|
notifier:
|
||||||
filesystem:
|
smtp:
|
||||||
filename: /config/notification.yml
|
host: "{SMTP_HOST}"
|
||||||
|
port: {SMTP_PORT}
|
||||||
|
username: "{SMTP_USER}"
|
||||||
|
password: "{SMTP_PASS}"
|
||||||
|
sender: "{SMTP_USER}"
|
||||||
|
subject: "NextWorkspace - {DOMAIN}"
|
||||||
|
disable_require_tls: false
|
||||||
|
disable_starttls: false
|
||||||
|
tls:
|
||||||
|
skip_verify: false
|
||||||
|
|
|
||||||
|
|
@ -96,6 +96,10 @@ if [ "$GREENFIELD" = true ]; then
|
||||||
sed -e "s|{DOMAIN}|$DOMAIN|g" \
|
sed -e "s|{DOMAIN}|$DOMAIN|g" \
|
||||||
-e "s|{JWT_SECRET}|$JWT_SECRET|g" \
|
-e "s|{JWT_SECRET}|$JWT_SECRET|g" \
|
||||||
-e "s|{SESSION_SECRET}|$SESSION_SECRET|g" \
|
-e "s|{SESSION_SECRET}|$SESSION_SECRET|g" \
|
||||||
|
-e "s|{SMTP_HOST}|${SMTP_HOST:-smtp.openxchange.eu}|g" \
|
||||||
|
-e "s|{SMTP_PORT}|${SMTP_PORT:-587}|g" \
|
||||||
|
-e "s|{SMTP_USER}|${SMTP_USER:-post@nextwks.eu}|g" \
|
||||||
|
-e "s|{SMTP_PASS}|${SMTP_PASS}|g" \
|
||||||
"$SCRIPT_DIR/config/authelia/configuration.yml" > "$TARGET_DIR/config/authelia/configuration.yml"
|
"$SCRIPT_DIR/config/authelia/configuration.yml" > "$TARGET_DIR/config/authelia/configuration.yml"
|
||||||
|
|
||||||
# Generate users database
|
# Generate users database
|
||||||
|
|
|
||||||
29
install.sh
29
install.sh
|
|
@ -43,6 +43,29 @@ done
|
||||||
# Generate 12-char alphanumeric password (easy to type)
|
# Generate 12-char alphanumeric password (easy to type)
|
||||||
ADMIN_PASSWORD=$(tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 12 2>/dev/null || date +%s | head -c 12)
|
ADMIN_PASSWORD=$(tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 12 2>/dev/null || date +%s | head -c 12)
|
||||||
|
|
||||||
|
# --- SMTP prompts ---
|
||||||
|
read -p "SMTP host [smtp.openxchange.eu]: " SMTP_HOST
|
||||||
|
SMTP_HOST="${SMTP_HOST:-smtp.openxchange.eu}"
|
||||||
|
|
||||||
|
read -p "SMTP port [587]: " SMTP_PORT
|
||||||
|
SMTP_PORT="${SMTP_PORT:-587}"
|
||||||
|
|
||||||
|
read -p "SMTP user [post@nextwks.eu]: " SMTP_USER
|
||||||
|
SMTP_USER="${SMTP_USER:-post@nextwks.eu}"
|
||||||
|
|
||||||
|
read -sp "SMTP password: " SMTP_PASS
|
||||||
|
echo ""
|
||||||
|
if [ -z "$SMTP_PASS" ]; then
|
||||||
|
echo "[ERROR] SMTP password is required."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
read -p "IMAP host [imap.openxchange.eu]: " IMAP_HOST
|
||||||
|
IMAP_HOST="${IMAP_HOST:-imap.openxchange.eu}"
|
||||||
|
|
||||||
|
read -p "IMAP port [993]: " IMAP_PORT
|
||||||
|
IMAP_PORT="${IMAP_PORT:-993}"
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "========================================"
|
echo "========================================"
|
||||||
echo " Domain: $DOMAIN"
|
echo " Domain: $DOMAIN"
|
||||||
|
|
@ -62,6 +85,12 @@ DOMAIN=$DOMAIN
|
||||||
TLS_EMAIL=$TLS_EMAIL
|
TLS_EMAIL=$TLS_EMAIL
|
||||||
ADMIN_USERNAME=$ADMIN_USERNAME
|
ADMIN_USERNAME=$ADMIN_USERNAME
|
||||||
ADMIN_PASSWORD=$ADMIN_PASSWORD
|
ADMIN_PASSWORD=$ADMIN_PASSWORD
|
||||||
|
SMTP_HOST=$SMTP_HOST
|
||||||
|
SMTP_PORT=$SMTP_PORT
|
||||||
|
SMTP_USER=$SMTP_USER
|
||||||
|
SMTP_PASS=$SMTP_PASS
|
||||||
|
IMAP_HOST=$IMAP_HOST
|
||||||
|
IMAP_PORT=$IMAP_PORT
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
chmod 600 "$ENV_FILE"
|
chmod 600 "$ENV_FILE"
|
||||||
|
|
|
||||||
329
main.go
329
main.go
|
|
@ -10,7 +10,9 @@ import (
|
||||||
"net/http/httputil"
|
"net/http/httputil"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
|
|
@ -46,6 +48,20 @@ type AppsFile struct {
|
||||||
Apps []AppEntry `yaml:"apps"`
|
Apps []AppEntry `yaml:"apps"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Settings struct {
|
||||||
|
Company struct {
|
||||||
|
Name string `yaml:"name"`
|
||||||
|
Language string `yaml:"language"`
|
||||||
|
Timezone string `yaml:"timezone"`
|
||||||
|
} `yaml:"company"`
|
||||||
|
SMTP struct {
|
||||||
|
Host string `yaml:"host"`
|
||||||
|
Port int `yaml:"port"`
|
||||||
|
User string `yaml:"user"`
|
||||||
|
Sender string `yaml:"sender"`
|
||||||
|
} `yaml:"smtp"`
|
||||||
|
}
|
||||||
|
|
||||||
// --- Config loading ---
|
// --- Config loading ---
|
||||||
|
|
||||||
func loadConfig(configDir string) (*Config, error) {
|
func loadConfig(configDir string) (*Config, error) {
|
||||||
|
|
@ -77,6 +93,55 @@ func loadApps(configDir string) ([]AppEntry, error) {
|
||||||
return appsFile.Apps, nil
|
return appsFile.Apps, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Settings loading ---
|
||||||
|
|
||||||
|
func loadSettings(configDir string) (*Settings, error) {
|
||||||
|
path := filepath.Join(configDir, "settings.yaml")
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return &Settings{}, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("reading settings: %w", err)
|
||||||
|
}
|
||||||
|
var s Settings
|
||||||
|
if err := yaml.Unmarshal(data, &s); err != nil {
|
||||||
|
return nil, fmt.Errorf("parsing settings: %w", err)
|
||||||
|
}
|
||||||
|
return &s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func saveSettings(configDir string, s *Settings) error {
|
||||||
|
path := filepath.Join(configDir, "settings.yaml")
|
||||||
|
data, err := yaml.Marshal(s)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("marshaling settings: %w", err)
|
||||||
|
}
|
||||||
|
return os.WriteFile(path, data, 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateEnvFile(path string, values map[string]string) error {
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
lines := strings.Split(string(data), "\n")
|
||||||
|
for k, v := range values {
|
||||||
|
found := false
|
||||||
|
for i, line := range lines {
|
||||||
|
if strings.HasPrefix(line, k+"=") {
|
||||||
|
lines[i] = k + "=" + v
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
lines = append(lines, k+"="+v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return os.WriteFile(path, []byte(strings.Join(lines, "\n")), 0600)
|
||||||
|
}
|
||||||
|
|
||||||
// --- Helpers ---
|
// --- Helpers ---
|
||||||
|
|
||||||
func userHasAnyGroup(userGroups []string, requiredGroups []string) bool {
|
func userHasAnyGroup(userGroups []string, requiredGroups []string) bool {
|
||||||
|
|
@ -238,6 +303,112 @@ func settingsHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- API proxy for authelia-api ---
|
||||||
|
|
||||||
|
func apiProxyHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
target, _ := url.Parse("http://127.0.0.1:8080")
|
||||||
|
proxy := httputil.NewSingleHostReverseProxy(target)
|
||||||
|
r.URL.Path = strings.TrimPrefix(r.URL.Path, "/api")
|
||||||
|
proxy.ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Global settings handlers ---
|
||||||
|
|
||||||
|
func globalSettingsHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
configDir := os.Getenv("CONFIG_DIR")
|
||||||
|
if configDir == "" {
|
||||||
|
configDir = "/opt/nextworkspace/config/nextworkspace"
|
||||||
|
}
|
||||||
|
settings, err := loadSettings(configDir)
|
||||||
|
if err != nil {
|
||||||
|
settings = &Settings{}
|
||||||
|
}
|
||||||
|
|
||||||
|
domain := os.Getenv("DOMAIN")
|
||||||
|
adminEmail := os.Getenv("TLS_EMAIL")
|
||||||
|
smtpHost := os.Getenv("SMTP_HOST")
|
||||||
|
smtpPort := os.Getenv("SMTP_PORT")
|
||||||
|
|
||||||
|
// Check Authelia health
|
||||||
|
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 := template.Must(template.New("global").Parse(globalSettingsHTML))
|
||||||
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
tmpl.Execute(w, map[string]interface{}{
|
||||||
|
"CompanyName": settings.Company.Name,
|
||||||
|
"Language": settings.Company.Language,
|
||||||
|
"Timezone": settings.Company.Timezone,
|
||||||
|
"SMTPHost": smtpHost,
|
||||||
|
"SMTPPort": smtpPort,
|
||||||
|
"SMTPUser": settings.SMTP.User,
|
||||||
|
"SMTPSender": settings.SMTP.Sender,
|
||||||
|
"Domain": domain,
|
||||||
|
"AdminEmail": adminEmail,
|
||||||
|
"AutheliaUp": autheliaUp,
|
||||||
|
"Saved": r.URL.Query().Get("saved") == "ok",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func globalSettingsSaveHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
configDir := os.Getenv("CONFIG_DIR")
|
||||||
|
if configDir == "" {
|
||||||
|
configDir = "/opt/nextworkspace/config/nextworkspace"
|
||||||
|
}
|
||||||
|
|
||||||
|
settings, _ := loadSettings(configDir)
|
||||||
|
if settings == nil {
|
||||||
|
settings = &Settings{}
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.Company.Name = r.FormValue("company_name")
|
||||||
|
settings.Company.Language = r.FormValue("language")
|
||||||
|
settings.Company.Timezone = r.FormValue("timezone")
|
||||||
|
settings.SMTP.Host = r.FormValue("smtp_host")
|
||||||
|
settings.SMTP.Port, _ = strconv.Atoi(r.FormValue("smtp_port"))
|
||||||
|
settings.SMTP.User = r.FormValue("smtp_user")
|
||||||
|
settings.SMTP.Sender = r.FormValue("smtp_sender")
|
||||||
|
|
||||||
|
saveSettings(configDir, settings)
|
||||||
|
|
||||||
|
// Update .env with SMTP values
|
||||||
|
envPath := os.Getenv("ENV_FILE")
|
||||||
|
if envPath == "" {
|
||||||
|
envPath = "/opt/backup/.env"
|
||||||
|
}
|
||||||
|
envUpdates := map[string]string{
|
||||||
|
"SMTP_HOST": r.FormValue("smtp_host"),
|
||||||
|
"SMTP_PORT": r.FormValue("smtp_port"),
|
||||||
|
"SMTP_USER": r.FormValue("smtp_user"),
|
||||||
|
"SMTP_SENDER": r.FormValue("smtp_sender"),
|
||||||
|
}
|
||||||
|
if pwd := r.FormValue("smtp_password"); pwd != "" {
|
||||||
|
envUpdates["SMTP_PASS"] = pwd
|
||||||
|
}
|
||||||
|
updateEnvFile(envPath, envUpdates)
|
||||||
|
|
||||||
|
http.Redirect(w, r, "/config?s=global&saved=ok", http.StatusFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
func applySettingsHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
cmd := exec.Command("/opt/NextWks/deploy.sh")
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, fmt.Sprintf("Apply failed: %v\n%s", err, string(output)), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
fmt.Fprintf(w, `{"success":true,"output":"%s"}`, strings.ReplaceAll(string(output), `"`, `\"`))
|
||||||
|
}
|
||||||
|
|
||||||
// --- Templates ---
|
// --- Templates ---
|
||||||
|
|
||||||
const landingPageHTML = `<!DOCTYPE html>
|
const landingPageHTML = `<!DOCTYPE html>
|
||||||
|
|
@ -392,6 +563,140 @@ const settingsHTML = `<!DOCTYPE html>
|
||||||
</body>
|
</body>
|
||||||
</html>`
|
</html>`
|
||||||
|
|
||||||
|
const globalSettingsHTML = `<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Global Settings — NextWorkspace</title>
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f0f2f5; color: #1a1a2e; min-height: 100vh; }
|
||||||
|
.topbar { background: linear-gradient(135deg, #1a1a2e, #16213e); color: #fff; padding: 0.75rem 1.5rem; display: flex; justify-content: space-between; align-items: center; }
|
||||||
|
.topbar h1 { font-size: 1.1rem; font-weight: 600; }
|
||||||
|
.topbar a { color: #a0aec0; text-decoration: none; font-size: 0.85rem; }
|
||||||
|
.topbar a:hover { color: #fff; }
|
||||||
|
.container { max-width: 720px; margin: 2rem auto; padding: 0 1rem; }
|
||||||
|
.banner { padding: 0.75rem 1rem; border-radius: 8px; margin-bottom: 1.25rem; font-size: 0.88rem; }
|
||||||
|
.banner-success { background: #c6f6d5; color: #276749; border: 1px solid #9ae6b4; }
|
||||||
|
.card { background: #fff; border-radius: 10px; padding: 1.5rem; margin-bottom: 1.25rem; box-shadow: 0 1px 3px rgba(0,0,0,0.05); border: 1px solid #edf2f7; }
|
||||||
|
.card h2 { font-size: 1rem; font-weight: 600; color: #2d3748; margin-bottom: 1rem; padding-bottom: 0.5rem; border-bottom: 1px solid #edf2f7; }
|
||||||
|
.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%; padding: 0.5rem 0.7rem; border: 1px solid #e2e8f0; border-radius: 6px; font-size: 0.88rem; color: #4a5568; background: #fff; outline: none; }
|
||||||
|
.field input:focus, .field select:focus { border-color: #63b3ed; box-shadow: 0 0 0 2px rgba(99,179,237,0.15); }
|
||||||
|
.field .note { color: #a0aec0; font-size: 0.78rem; margin-top: 0.2rem; }
|
||||||
|
.field-row { display: flex; gap: 1rem; }
|
||||||
|
.field-row .field { flex: 1; }
|
||||||
|
.info-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0.5rem; }
|
||||||
|
.info-item { padding: 0.5rem 0; border-bottom: 1px solid #f7fafc; }
|
||||||
|
.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; }
|
||||||
|
.btn { display: inline-flex; align-items: center; gap: 0.35rem; padding: 0.5rem 1rem; border-radius: 6px; font-size: 0.88rem; font-weight: 500; cursor: pointer; border: none; text-decoration: none; }
|
||||||
|
.btn-primary { background: #1a1a2e; color: #fff; }
|
||||||
|
.btn-primary:hover { background: #2d3748; }
|
||||||
|
.btn-ghost { background: transparent; color: #718096; border: 1px solid #e2e8f0; }
|
||||||
|
.btn-ghost:hover { background: #f7fafc; }
|
||||||
|
.actions { display: flex; gap: 0.75rem; margin-top: 1.5rem; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="topbar">
|
||||||
|
<h1>NextWorkspace · Global Settings</h1>
|
||||||
|
<a href="/config">Back to Admin</a>
|
||||||
|
</div>
|
||||||
|
<div class="container">
|
||||||
|
{{if .Saved}}<div class="banner banner-success">Settings saved. SMTP changes require applying — click "Apply Settings" below.</div>{{end}}
|
||||||
|
<form method="POST" action="/config/global/save">
|
||||||
|
<div class="card">
|
||||||
|
<h2>Company</h2>
|
||||||
|
<div class="field">
|
||||||
|
<label>Company Name</label>
|
||||||
|
<input name="company_name" value="{{.CompanyName}}" placeholder="NextWorkspace">
|
||||||
|
</div>
|
||||||
|
<div class="field-row">
|
||||||
|
<div class="field">
|
||||||
|
<label>Default Language</label>
|
||||||
|
<select name="language">
|
||||||
|
<option value="en" {{if eq .Language "en"}}selected{{end}}>English</option>
|
||||||
|
<option value="de" {{if eq .Language "de"}}selected{{end}}>Deutsch</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>Timezone</label>
|
||||||
|
<select name="timezone">
|
||||||
|
<option value="UTC" {{if eq .Timezone "UTC"}}selected{{end}}>UTC</option>
|
||||||
|
<option value="Europe/London" {{if eq .Timezone "Europe/London"}}selected{{end}}>Europe/London</option>
|
||||||
|
<option value="Europe/Berlin" {{if eq .Timezone "Europe/Berlin"}}selected{{end}}>Europe/Berlin</option>
|
||||||
|
<option value="America/New_York" {{if eq .Timezone "America/New_York"}}selected{{end}}>America/New_York</option>
|
||||||
|
<option value="America/Los_Angeles" {{if eq .Timezone "America/Los_Angeles"}}selected{{end}}>America/Los_Angeles</option>
|
||||||
|
<option value="Asia/Tokyo" {{if eq .Timezone "Asia/Tokyo"}}selected{{end}}>Asia/Tokyo</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h2>SMTP</h2>
|
||||||
|
<div class="field">
|
||||||
|
<label>SMTP Host</label>
|
||||||
|
<input name="smtp_host" value="{{.SMTPHost}}" placeholder="smtp.openxchange.eu">
|
||||||
|
</div>
|
||||||
|
<div class="field-row">
|
||||||
|
<div class="field">
|
||||||
|
<label>SMTP Port</label>
|
||||||
|
<input name="smtp_port" value="{{.SMTPPort}}" placeholder="587">
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>SMTP User</label>
|
||||||
|
<input name="smtp_user" value="{{.SMTPUser}}" placeholder="post@nextwks.eu">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field-row">
|
||||||
|
<div class="field">
|
||||||
|
<label>SMTP Password</label>
|
||||||
|
<input name="smtp_password" type="password" placeholder="Leave blank to keep current">
|
||||||
|
<div class="note">Leave blank unless changing the password.</div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>Sender Email</label>
|
||||||
|
<input name="smtp_sender" value="{{.SMTPSender}}" placeholder="post@nextwks.eu">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h2>System (read-only)</h2>
|
||||||
|
<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 class="info-item"><div class="label">Version</div><div class="value">0.1.0.0011</div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="actions">
|
||||||
|
<button type="submit" class="btn btn-primary">Save Settings</button>
|
||||||
|
<button type="button" class="btn btn-ghost" onclick="applySettings()">Apply Settings</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
async function applySettings() {
|
||||||
|
const btn = event.target; btn.textContent = 'Applying...'; btn.disabled = true;
|
||||||
|
const resp = await fetch('/config/apply', {method:'POST'});
|
||||||
|
const result = await resp.json();
|
||||||
|
if (result.success) { alert('Settings applied. Containers restarted.'); }
|
||||||
|
else { alert('Error: ' + JSON.stringify(result)); }
|
||||||
|
btn.textContent = 'Apply Settings'; btn.disabled = false;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>`
|
||||||
|
|
||||||
const adminHTML = `<!DOCTYPE html>
|
const adminHTML = `<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
|
|
@ -558,7 +863,7 @@ const adminHTML = `<!DOCTYPE html>
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadUsers() {
|
async function loadUsers() {
|
||||||
const resp = await fetch('/config?format=json');
|
const resp = await fetch('/api/users');
|
||||||
if (!resp.ok) return;
|
if (!resp.ok) return;
|
||||||
const users = await resp.json();
|
const users = await resp.json();
|
||||||
const tbody = document.getElementById('usersTable');
|
const tbody = document.getElementById('usersTable');
|
||||||
|
|
@ -582,7 +887,7 @@ const adminHTML = `<!DOCTYPE html>
|
||||||
const data = Object.fromEntries(new FormData(form));
|
const data = Object.fromEntries(new FormData(form));
|
||||||
data.groups = Array.from(form.querySelector('[name=groups]').selectedOptions).map(o => o.value);
|
data.groups = Array.from(form.querySelector('[name=groups]').selectedOptions).map(o => o.value);
|
||||||
if (!data.groups.length) data.groups = ['users'];
|
if (!data.groups.length) data.groups = ['users'];
|
||||||
const resp = await fetch('/config', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({users:[data]})});
|
const resp = await fetch('/api/users/bulk', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({users:[data]})});
|
||||||
const result = await resp.json();
|
const result = await resp.json();
|
||||||
const div = document.getElementById('createResult');
|
const div = document.getElementById('createResult');
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
|
|
@ -597,7 +902,7 @@ const adminHTML = `<!DOCTYPE html>
|
||||||
|
|
||||||
async function deleteUser(username) {
|
async function deleteUser(username) {
|
||||||
if (!confirm('Delete user "' + username + '"?')) return;
|
if (!confirm('Delete user "' + username + '"?')) return;
|
||||||
const resp = await fetch('/config?username=' + username, {method:'DELETE'});
|
const resp = await fetch('/api/users/' + username, {method:'DELETE'});
|
||||||
if (resp.ok) loadUsers();
|
if (resp.ok) loadUsers();
|
||||||
else alert('Delete failed: ' + await resp.text());
|
else alert('Delete failed: ' + await resp.text());
|
||||||
}
|
}
|
||||||
|
|
@ -626,6 +931,12 @@ func main() {
|
||||||
log.Fatalf("Failed to load apps: %v", err)
|
log.Fatalf("Failed to load apps: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
settings, _ := loadSettings(configDir)
|
||||||
|
companyName := "NextWorkspace"
|
||||||
|
if settings != nil && settings.Company.Name != "" {
|
||||||
|
companyName = settings.Company.Name
|
||||||
|
}
|
||||||
|
|
||||||
addr := fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port)
|
addr := fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port)
|
||||||
|
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
|
|
@ -633,7 +944,10 @@ func main() {
|
||||||
// Public
|
// Public
|
||||||
mux.HandleFunc("/health", healthHandler)
|
mux.HandleFunc("/health", healthHandler)
|
||||||
|
|
||||||
// Protected: launcher
|
// Protectected: launcher
|
||||||
|
if companyName != "" {
|
||||||
|
cfg.App.Name = companyName
|
||||||
|
}
|
||||||
mux.Handle("/home/", authMiddleware(launcherHandler(cfg, apps)))
|
mux.Handle("/home/", authMiddleware(launcherHandler(cfg, apps)))
|
||||||
mux.Handle("/home", authMiddleware(launcherHandler(cfg, apps)))
|
mux.Handle("/home", authMiddleware(launcherHandler(cfg, apps)))
|
||||||
|
|
||||||
|
|
@ -645,6 +959,13 @@ func main() {
|
||||||
mux.Handle("/config", adminGroupMiddleware(authMiddleware(adminHandler)))
|
mux.Handle("/config", adminGroupMiddleware(authMiddleware(adminHandler)))
|
||||||
mux.Handle("/config/", adminGroupMiddleware(authMiddleware(adminHandler)))
|
mux.Handle("/config/", adminGroupMiddleware(authMiddleware(adminHandler)))
|
||||||
|
|
||||||
|
// Global settings save — admins only
|
||||||
|
mux.HandleFunc("/config/global/save", adminGroupMiddleware(authMiddleware(globalSettingsSaveHandler)))
|
||||||
|
mux.HandleFunc("/config/apply", adminGroupMiddleware(authMiddleware(applySettingsHandler)))
|
||||||
|
|
||||||
|
// API proxy — authelia-api (authenticated users only)
|
||||||
|
mux.Handle("/api/", authMiddleware(apiProxyHandler))
|
||||||
|
|
||||||
// Protected: upstream app proxies
|
// Protected: upstream app proxies
|
||||||
for _, app := range apps {
|
for _, app := range apps {
|
||||||
if app.Path != "" && app.Upstream != "" && app.Path != "/config" && app.Path != "/home" {
|
if app.Path != "" && app.Upstream != "" && app.Path != "/config" && app.Path != "/home" {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue