Add auth, rest api, svelte frontend, build structure

This commit is contained in:
2025-06-02 08:44:10 +02:00
parent 97be451306
commit 17ab7c4929
222 changed files with 3057 additions and 136 deletions

117
main.go
View File

@@ -1,26 +1,26 @@
package main
import (
"embed"
"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"
)
//go:embed web/static/*
var static embed.FS
func init() {
_, jsonLogger := os.LookupEnv("JSON_LOGGER")
_, debug := os.LookupEnv("DEBUG")
@@ -48,10 +48,14 @@ func init() {
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 {
@@ -60,7 +64,7 @@ func main() {
dbURL := os.Getenv("DB")
if dbURL == "" {
dbURL = "file:app.db"
dbURL = "app.db"
}
if flagMigrateDB {
@@ -70,6 +74,7 @@ func main() {
os.Exit(1)
}
slog.Info("MigrateDB Success: Migrated database at location.\n", "version", repository.Version)
os.Exit(0)
}
if flagRevertDB {
@@ -79,6 +84,7 @@ func main() {
os.Exit(1)
}
slog.Info("RevertDB Success: Reverted database", "version", (repository.Version - 1))
os.Exit(0)
}
if flagForceDB {
@@ -87,7 +93,8 @@ func main() {
slog.Error("ForceDB Failed: Could not force database version", "version", repository.Version, "error", err)
os.Exit(1)
}
slog.Error("ForceDB Success: Forced database version", "version", repository.Version)
slog.Info("ForceDB Success: Forced database version", "version", repository.Version)
os.Exit(0)
}
repository.Connect(dbURL)
@@ -98,20 +105,58 @@ func main() {
}
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()
// Use an embedded filesystem rooted at "web/static"
fs, err := fs.Sub(static, "web/static")
sfs, err := fs.Sub(web.StaticAssets, "frontend/dist")
if err != nil {
slog.Error("Failed to create sub filesystem", "error", err)
return
}
// Serve files from the embedded /web/static directory at /static
fileServer := http.FileServer(http.FS(fs))
mux.Handle("GET /static/", http.StripPrefix("/static/", fileServer))
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 := static.ReadFile("web/static/favicon.ico")
data, err := web.StaticAssets.ReadFile("frontend/dist/favicon.ico")
if err != nil {
http.NotFound(w, r)
return
@@ -120,7 +165,7 @@ func main() {
w.Write(data)
})
mux.HandleFunc("GET /robots.txt", func(w http.ResponseWriter, r *http.Request) {
data, err := static.ReadFile("web/static/robots.txt")
data, err := web.StaticAssets.ReadFile("frontend/dist/robots.txt")
if err != nil {
http.NotFound(w, r)
return
@@ -134,10 +179,54 @@ func main() {
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.RecoverMiddleware)
chain.Use(middleware.Recover)
wrappedMux := chain.Then(mux)
server := &http.Server{