package main import ( "context" "flag" "fmt" "io/fs" "log/slog" "net/http" "os" "strings" "time" "git.clustercockpit.org/moebiusband/go-http-skeleton/internal/auth" "git.clustercockpit.org/moebiusband/go-http-skeleton/internal/handlers" "git.clustercockpit.org/moebiusband/go-http-skeleton/internal/middleware" "git.clustercockpit.org/moebiusband/go-http-skeleton/internal/repository" "git.clustercockpit.org/moebiusband/go-http-skeleton/web" "github.com/joho/godotenv" _ "modernc.org/sqlite" ) func init() { _, jsonLogger := os.LookupEnv("JSON_LOGGER") _, debug := os.LookupEnv("DEBUG") var programLevel slog.Level if debug { programLevel = slog.LevelDebug } programLevel = slog.LevelDebug if jsonLogger { jsonHandler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ Level: programLevel, }) slog.SetDefault(slog.New(jsonHandler)) } else { textHandler := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ Level: programLevel, }) slog.SetDefault(slog.New(textHandler)) } slog.Info("Logger initialized", slog.Bool("debug", debug)) } func main() { var flagMigrateDB, flagRevertDB, flagForceDB bool var flagNewUser, flagDelUser string flag.BoolVar(&flagMigrateDB, "migrate-db", false, "Migrate database to supported version and exit") flag.BoolVar(&flagRevertDB, "revert-db", false, "Migrate database to previous version and exit") flag.BoolVar(&flagForceDB, "force-db", false, "Force database version, clear dirty flag and exit") flag.StringVar(&flagNewUser, "add-user", "", "Add a new user. Argument format: :") flag.StringVar(&flagDelUser, "del-user", "", "Remove a existing user. Argument format: ") flag.Parse() err := godotenv.Load() if err != nil { slog.Error("Could not parse existing .env file at location './.env'. Application startup failed, exited.\nError: %s\n", "Error", err.Error()) } dbURL := os.Getenv("DB") if dbURL == "" { dbURL = "app.db" } if flagMigrateDB { err := repository.MigrateDB(dbURL) if err != nil { slog.Error("MigrateDB Failed: Could not migrate database at location.", "version", repository.Version, "error", err) os.Exit(1) } slog.Info("MigrateDB Success: Migrated database at location.\n", "version", repository.Version) os.Exit(0) } if flagRevertDB { err := repository.RevertDB(dbURL) if err != nil { slog.Error("RevertDB Failed: Could not revert database at location", "version", (repository.Version - 1), "error", err) os.Exit(1) } slog.Info("RevertDB Success: Reverted database", "version", (repository.Version - 1)) os.Exit(0) } if flagForceDB { err := repository.ForceDB(dbURL) if err != nil { slog.Error("ForceDB Failed: Could not force database version", "version", repository.Version, "error", err) os.Exit(1) } slog.Info("ForceDB Success: Forced database version", "version", repository.Version) os.Exit(0) } repository.Connect(dbURL) port := os.Getenv("PORT") if port == "" { port = "8080" } addr := ":" + port auth.Init() if flagNewUser != "" { parts := strings.SplitN(flagNewUser, ":", 2) if len(parts) != 2 || len(parts[0]) == 0 { slog.Error("Add User: Could not parse supplied argument format: No changes.\n"+ "Want: ::\n", "have", flagNewUser) } ctx := context.Background() q := repository.GetRepository() if err := q.CreateUser(ctx, repository.CreateUserParams{ UserName: parts[0], UserPass: &parts[1], }); err != nil { slog.Error("Add User: Could not add new user authentication", "username", parts[0], "error", err.Error()) os.Exit(1) } else { slog.Info("add new user", "username", parts[0]) } os.Exit(0) } if flagDelUser != "" { ctx := context.Background() q := repository.GetRepository() if err := q.DeleteUser(ctx, flagDelUser); err != nil { slog.Error("Delete User: Could not delete user", "username", flagDelUser, "error", err) os.Exit(1) } else { slog.Info("deleted user from DB", "username", flagDelUser) } os.Exit(0) } mux := http.NewServeMux() sfs, err := fs.Sub(web.StaticAssets, "frontend/dist") if err != nil { slog.Error("Failed to create sub filesystem", "error", err) return } mux.Handle("GET /static/", http.StripPrefix("/static", http.FileServer(http.FS(sfs)))) afs, err := fs.Sub(web.StaticAssets, "frontend/dist/assets") if err != nil { slog.Error("Failed to create sub filesystem", "error", err) return } mux.Handle("GET /assets/", http.StripPrefix("/assets", http.FileServer(http.FS(afs)))) mux.HandleFunc("GET /favicon.ico", func(w http.ResponseWriter, r *http.Request) { data, err := web.StaticAssets.ReadFile("frontend/dist/favicon.ico") if err != nil { http.NotFound(w, r) return } w.Header().Set("Content-Type", "text/plain") w.Write(data) }) mux.HandleFunc("GET /robots.txt", func(w http.ResponseWriter, r *http.Request) { data, err := web.StaticAssets.ReadFile("frontend/dist/robots.txt") if err != nil { http.NotFound(w, r) return } w.Header().Set("Content-Type", "text/plain") w.Write(data) }) mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/plain") w.Write([]byte(`OK`)) }) mux.HandleFunc("GET /login", func(rw http.ResponseWriter, r *http.Request) { rw.Header().Add("Content-Type", "text/html; charset=utf-8") web.RenderTemplate(rw, "login", web.PageData{Title: "Login"}) }) mux.HandleFunc("GET /imprint", func(rw http.ResponseWriter, r *http.Request) { rw.Header().Add("Content-Type", "text/html; charset=utf-8") web.RenderTemplate(rw, "imprint", web.PageData{Title: "Imprint"}) }) mux.HandleFunc("GET /privacy", func(rw http.ResponseWriter, r *http.Request) { rw.Header().Add("Content-Type", "text/html; charset=utf-8") web.RenderTemplate(rw, "privacy", web.PageData{Title: "Privacy"}) }) authHandle := auth.GetAuthInstance() mux.Handle("POST /login", authHandle.Login( func(rw http.ResponseWriter, r *http.Request, err error) { rw.Header().Add("Content-Type", "text/html; charset=utf-8") rw.WriteHeader(http.StatusUnauthorized) web.RenderTemplate(rw, "login", web.PageData{ Title: "Login failed - ClusterCockpit", MsgType: "alert-warning", Message: err.Error(), }) })) mux.Handle("POST /logout", authHandle.Logout( http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { rw.Header().Add("Content-Type", "text/html; charset=utf-8") rw.WriteHeader(http.StatusOK) web.RenderTemplate(rw, "login", web.PageData{ Title: "Bye - ClusterCockpit", MsgType: "alert-info", Message: "Logout successful", }) }))) mux.HandleFunc("GET /", handlers.RootHandler()) securedMux := http.NewServeMux() securedMux.HandleFunc("GET /", handlers.AdminHandler()) securedChain := &middleware.Chain{} securedChain.Use(middleware.SecuredCheck) mux.Handle("GET /admin/", http.StripPrefix("/admin", securedChain.Then(securedMux))) chain := &middleware.Chain{} chain.Use(middleware.Recover) wrappedMux := chain.Then(mux) server := &http.Server{ Addr: fmt.Sprintf(":%s", port), Handler: wrappedMux, // Recommended timeouts from // https://blog.cloudflare.com/exposing-go-on-the-internet/ ReadTimeout: 5 * time.Second, WriteTimeout: 10 * time.Second, IdleTimeout: 120 * time.Second, } slog.Info("Server listening", "addr", addr) if err := server.ListenAndServe(); err != nil { slog.Error("Server failed to start", "error", err) } }