package main import ( "fmt" "html/template" "log" "net/http" "net/http/httputil" "net/url" "os" "path/filepath" "strings" "gopkg.in/yaml.v3" ) // --- Config types --- type ServerConfig struct { Port int `yaml:"port"` Host string `yaml:"host"` } type AppConfig struct { Name string `yaml:"name"` Description string `yaml:"description"` } type Config struct { Server ServerConfig `yaml:"server"` App AppConfig `yaml:"app"` } type AppEntry struct { Name string `yaml:"name"` Subtitle string `yaml:"subtitle"` Path string `yaml:"path"` Upstream string `yaml:"upstream"` Icon string `yaml:"icon"` } type AppsFile struct { Apps []AppEntry `yaml:"apps"` } // --- Config loading --- func loadConfig(configDir string) (*Config, error) { path := filepath.Join(configDir, "config.yaml") data, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("reading config: %w", err) } var cfg Config if err := yaml.Unmarshal(data, &cfg); err != nil { return nil, fmt.Errorf("parsing config: %w", err) } return &cfg, nil } func loadApps(configDir string) ([]AppEntry, error) { path := filepath.Join(configDir, "apps.yaml") data, err := os.ReadFile(path) if err != nil { if os.IsNotExist(err) { return []AppEntry{}, nil } return nil, fmt.Errorf("reading apps: %w", err) } var appsFile AppsFile if err := yaml.Unmarshal(data, &appsFile); err != nil { return nil, fmt.Errorf("parsing apps: %w", err) } return appsFile.Apps, nil } // --- Auth middleware (trusts Remote-User from Caddy forward auth) --- func authMiddleware(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { user := r.Header.Get("Remote-User") if user == "" { http.Error(w, "Unauthorized", http.StatusUnauthorized) return } r.Header.Set("X-Auth-User", user) next(w, r) } } // --- Handlers --- func healthHandler(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) fmt.Fprint(w, "OK") } func launcherHandler(cfg *Config, apps []AppEntry) http.HandlerFunc { tmpl := template.Must(template.New("launcher").Parse(launcherHTML)) return func(w http.ResponseWriter, r *http.Request) { user := r.Header.Get("Remote-User") data := struct { AppName string Description string User string Apps []AppEntry }{ AppName: cfg.App.Name, Description: cfg.App.Description, User: user, Apps: apps, } w.Header().Set("Content-Type", "text/html; charset=utf-8") tmpl.Execute(w, data) } } func proxyToUpstream(upstream string) http.HandlerFunc { target, err := url.Parse(upstream) if err != nil { log.Fatalf("Invalid upstream URL %q: %v", upstream, err) } proxy := httputil.NewSingleHostReverseProxy(target) return func(w http.ResponseWriter, r *http.Request) { r.Header.Set("Remote-User", r.Header.Get("Remote-User")) proxy.ServeHTTP(w, r) } } // --- Templates --- const landingPageHTML = `
Your Self-Hosted Workspace for Startups
NextWorkspace was developed with assistance from AI coding tools, using DeepSeek as the provider and OpenCode as the development framework.
{{.Description}}
{{.Subtitle}}
{{if .Upstream}}{{else}}