diff --git a/config/www/index.html b/config/www/index.html index 9234016..ef293d0 100644 --- a/config/www/index.html +++ b/config/www/index.html @@ -3,81 +3,42 @@ - NextWorkspace — Your Self-Hosted Workspace for Startups + NextWorkspace -
-

NextWorkspace

-

Your Self-Hosted Workspace for Startups

-
nextwks.eu
-
-
-

Components

- - -
-

Built with AI

-

NextWorkspace was developed with assistance from AI coding tools, using DeepSeek as the provider and OpenCode as the development framework.

+
+ +

NextWorkspace

+

Your Self-Hosted Workspace for Startups

+ Launch Workspace +
-
- © 2026 NextWorkspace — nextwks.eu -
+ diff --git a/lng/de/admin.yaml b/lng/de/admin.yaml new file mode 100644 index 0000000..db24380 --- /dev/null +++ b/lng/de/admin.yaml @@ -0,0 +1,32 @@ +page_title: "Admin Bereich" +nav_global: "Global" +nav_access: "Zugriff" +nav_security: "Sicherheit" +nav_domain: "Domain" +nav_mail: "E-Mail" +nav_docs: "Dokumente" +nav_calendar: "Kalender" +save: "Einstellungen speichern" +saved: "✓ Gespeichert" +cancel: "Abbrechen" +company_section: "Unternehmen" +company_name: "Unternehmensname" +company_subtitle: "Untertitel" +company_logo: "Logo URL" +language: "Standardsprache" +timezone: "Zeitzone" +smtp_section: "SMTP" +smtp_host: "SMTP Host" +smtp_port: "SMTP Port" +smtp_user: "SMTP Benutzer" +smtp_password: "SMTP Passwort" +smtp_sender: "Absender E-Mail" +system_section: "System (schreibgeschützt)" +domain_label: "Domain" +admin_email: "Admin E-Mail" +authelia_status: "Authelia" +version_label: "Version" +global_title: "Globale Einstellungen" +access_title: "Zugriffsverwaltung" +users_label: "Benutzer" +create_user: "Benutzer anlegen" diff --git a/lng/de/launcher.yaml b/lng/de/launcher.yaml new file mode 100644 index 0000000..d79eedd --- /dev/null +++ b/lng/de/launcher.yaml @@ -0,0 +1,8 @@ +app_title: "NextWorkspace" +welcome: "Willkommen, {user}" +login: "Anmelden" +logout: "Abmelden" +launcher_title: "Ihr Arbeitsbereich" +no_apps: "Keine Anwendungen verfügbar" +admin_panel: "Admin Bereich" +settings: "Einstellungen" diff --git a/lng/en/admin.yaml b/lng/en/admin.yaml new file mode 100644 index 0000000..0084c81 --- /dev/null +++ b/lng/en/admin.yaml @@ -0,0 +1,32 @@ +page_title: "Admin Panel" +nav_global: "Global" +nav_access: "Access" +nav_security: "Security" +nav_domain: "Domain" +nav_mail: "Mail" +nav_docs: "Docs" +nav_calendar: "Calendar" +save: "Save Settings" +saved: "✓ Saved" +cancel: "Cancel" +company_section: "Company" +company_name: "Company Name" +company_subtitle: "Subtitle" +company_logo: "Logo URL" +language: "Default Language" +timezone: "Timezone" +smtp_section: "SMTP" +smtp_host: "SMTP Host" +smtp_port: "SMTP Port" +smtp_user: "SMTP User" +smtp_password: "SMTP Password" +smtp_sender: "Sender Email" +system_section: "System (read-only)" +domain_label: "Domain" +admin_email: "Admin Email" +authelia_status: "Authelia" +version_label: "Version" +global_title: "Global Settings" +access_title: "Access Management" +users_label: "Users" +create_user: "Create User" diff --git a/lng/en/launcher.yaml b/lng/en/launcher.yaml new file mode 100644 index 0000000..e231f90 --- /dev/null +++ b/lng/en/launcher.yaml @@ -0,0 +1,8 @@ +app_title: "NextWorkspace" +welcome: "Welcome, {user}" +login: "Sign In" +logout: "Logout" +launcher_title: "Your Workspace" +no_apps: "No applications available" +admin_panel: "Admin Panel" +settings: "Settings" diff --git a/main.go b/main.go index dee408e..7824601 100644 --- a/main.go +++ b/main.go @@ -51,6 +51,8 @@ type AppsFile struct { type Settings struct { Company struct { Name string `yaml:"name"` + Subtitle string `yaml:"subtitle"` + Logo string `yaml:"logo"` Language string `yaml:"language"` Timezone string `yaml:"timezone"` } `yaml:"company"` @@ -188,12 +190,22 @@ func healthHandler(w http.ResponseWriter, r *http.Request) { } func launcherHandler(cfg *Config, apps []AppEntry) http.HandlerFunc { - tmpl := template.Must(template.New("launcher").Parse(launcherHTML)) + funcMap := template.FuncMap{"t": func(key string, args ...string) string { + lang := "en" + settings, _ := loadSettings(os.Getenv("CONFIG_DIR")) + if settings != nil && settings.Company.Language != "" { + lang = settings.Company.Language + } + return T(lang, key, args...) + }} + tmpl := template.Must(template.New("launcher").Funcs(funcMap).Parse(launcherHTML)) return func(w http.ResponseWriter, r *http.Request) { user := r.Header.Get("Remote-User") groupsHeader := r.Header.Get("Remote-Groups") userGroups := strings.Split(groupsHeader, ",") + lang := getUserLang(r) + settings, _ := loadSettings(os.Getenv("CONFIG_DIR")) var allowedApps []AppEntry for _, app := range apps { @@ -208,12 +220,18 @@ func launcherHandler(cfg *Config, apps []AppEntry) http.HandlerFunc { User string IsAdmin bool Apps []AppEntry + Lang string + CompanyLogo string + Subtitle string }{ AppName: cfg.App.Name, Description: cfg.App.Description, User: user, IsAdmin: strings.Contains(groupsHeader, "admins"), Apps: allowedApps, + Lang: lang, + CompanyLogo: settings.Company.Logo, + Subtitle: settings.Company.Subtitle, } w.Header().Set("Content-Type", "text/html; charset=utf-8") tmpl.Execute(w, data) @@ -273,7 +291,10 @@ func adminHandler(w http.ResponseWriter, r *http.Request) { return } - tmpl := template.Must(template.New("admin").Parse(adminHTML)) + funcMap := template.FuncMap{"t": func(key string) string { + return T(settings.Company.Language, key) + }} + tmpl := template.Must(template.New("admin").Funcs(funcMap).Parse(adminHTML)) w.Header().Set("Content-Type", "text/html; charset=utf-8") autheliaUp := false @@ -287,6 +308,7 @@ func adminHandler(w http.ResponseWriter, r *http.Request) { "Domain": domain, "AdminEmail": adminEmail, "AutheliaUp": autheliaUp, + "T": func(key string) string { return T(settings.Company.Language, key) }, }) case http.MethodPost: @@ -397,6 +419,8 @@ func globalSettingsSaveHandler(w http.ResponseWriter, r *http.Request) { } settings.Company.Name = r.FormValue("company_name") + settings.Company.Subtitle = r.FormValue("company_subtitle") + settings.Company.Logo = r.FormValue("company_logo") settings.Company.Language = r.FormValue("language") settings.Company.Timezone = r.FormValue("timezone") settings.SMTP.Host = r.FormValue("smtp_host") @@ -436,6 +460,82 @@ func applySettingsHandler(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, `{"success":true,"output":"%s"}`, strings.ReplaceAll(string(output), `"`, `\"`)) } +// --- Public settings API --- + +func publicSettingsHandler(w http.ResponseWriter, r *http.Request) { + configDir := os.Getenv("CONFIG_DIR") + if configDir == "" { + configDir = "/opt/nextworkspace/config/nextworkspace" + } + settings, _ := loadSettings(configDir) + w.Header().Set("Content-Type", "application/json") + fmt.Fprintf(w, `{"name":"%s","subtitle":"%s","logo":"%s"}`, + settings.Company.Name, settings.Company.Subtitle, settings.Company.Logo) +} + +// --- Translation system --- + +type Translations map[string]string + +var translationCache map[string]Translations + +func loadTranslations(lang string) Translations { + if translationCache != nil { + if cached, ok := translationCache[lang]; ok { + return cached + } + } + basePath := filepath.Join(os.Getenv("CONFIG_DIR"), "../../lng", lang) + result := Translations{} + files := []string{"launcher.yaml", "admin.yaml"} + for _, f := range files { + data, err := os.ReadFile(filepath.Join(basePath, f)) + if err != nil { + continue + } + var m map[string]string + yaml.Unmarshal(data, &m) + for k, v := range m { + result[k] = v + } + } + if translationCache == nil { + translationCache = make(map[string]Translations) + } + translationCache[lang] = result + return result +} + +func T(lang, key string, args ...string) string { + t := loadTranslations(lang) + text, ok := t[key] + if !ok { + en := loadTranslations("en") + text = en[key] + if text == "" { + return key + } + } + for i := 0; i < len(args); i += 2 { + if i+1 < len(args) { + text = strings.ReplaceAll(text, "{"+args[i]+"}", args[i+1]) + } + } + return text +} + +func getUserLang(r *http.Request) string { + headerLang := r.Header.Get("Remote-Lang") + if headerLang == "de" || headerLang == "en" { + return headerLang + } + settings, _ := loadSettings(os.Getenv("CONFIG_DIR")) + if settings != nil && settings.Company.Language == "de" { + return "de" + } + return "en" +} + // --- Templates --- const landingPageHTML = ` @@ -495,7 +595,7 @@ const landingPageHTML = ` ` const launcherHTML = ` - + @@ -509,6 +609,7 @@ const launcherHTML = ` min-height: 100vh; } header { background: linear-gradient(135deg, #1a1a2e, #16213e); color: #fff; padding: 2rem; text-align: center; } + header .logo { max-height: 64px; margin-bottom: 0.5rem; } header h1 { font-size: 2rem; margin-bottom: 0.25rem; } header p { color: #a0aec0; font-size: 1rem; } .user-banner { background: #2d3748; color: #e2e8f0; padding: 0.75rem 2rem; text-align: center; font-size: 0.9rem; display: flex; justify-content: center; gap: 1rem; } @@ -524,13 +625,15 @@ const launcherHTML = `
+ {{if .CompanyLogo}}{{end}}

{{.AppName}}

-

{{.Description}}

+ {{if .Subtitle}}

{{.Subtitle}}

{{end}}
-
- Welcome, {{.User}} - ⚙ Settings - Logout +
+ {{t "welcome" "user" .User}} + {{if .IsAdmin}}{{t "admin_panel"}}{{end}} + {{t "settings"}} + {{t "logout"}}
{{range .Apps}} @@ -828,10 +931,12 @@ const adminHTML = `

Company

-
+
+
+
-
-
+
+
@@ -1040,6 +1145,7 @@ func main() { // Public mux.HandleFunc("/health", healthHandler) + mux.HandleFunc("/api/settings/public", publicSettingsHandler) // Protectected: launcher if companyName != "" {