248 lines
7.3 KiB
Go
248 lines
7.3 KiB
Go
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: <username>:<password>")
|
|
flag.StringVar(&flagDelUser, "del-user", "", "Remove a existing user. Argument format: <username>")
|
|
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: <username>::<password>\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)
|
|
}
|
|
}
|