diff --git a/.air.toml b/.air.toml index 498951f..e58a42a 100644 --- a/.air.toml +++ b/.air.toml @@ -3,50 +3,57 @@ testdata_dir = "testdata" tmp_dir = "tmp" [build] - args_bin = [] - bin = "./tmp/main" - cmd = "go build -o ./tmp/main ." - delay = 1000 - exclude_dir = ["assets", "tmp", "vendor", "testdata"] - exclude_file = [] - exclude_regex = ["_test.go"] - exclude_unchanged = false - follow_symlink = false - full_bin = "" - include_dir = [] - include_ext = ["go", "tpl", "tmpl", "html"] - include_file = [] - kill_delay = "0s" - log = "build-errors.log" - poll = false - poll_interval = 0 - post_cmd = [] - pre_cmd = [] - rerun = false - rerun_delay = 500 - send_interrupt = false - stop_on_error = false +args_bin = [] +bin = "./tmp/main" +cmd = "go build -o ./tmp/main ." +delay = 1000 +exclude_dir = [ + "assets", + "tmp", + "vendor", + "testdata", + "web/frontend/node_modules", + "web/static", +] +exclude_file = [] +exclude_regex = ["_test.go"] +exclude_unchanged = false +follow_symlink = false +full_bin = "" +include_dir = [] +include_ext = ["go", "ts", "svelte", "html"] +include_file = ["main.go", "vite.config.ts"] +kill_delay = "0s" +log = "build-errors.log" +poll = false +poll_interval = 0 +post_cmd = [] +pre_cmd = [] +rerun = false +rerun_delay = 500 +send_interrupt = false +stop_on_error = false [color] - app = "" - build = "yellow" - main = "magenta" - runner = "green" - watcher = "cyan" +app = "" +build = "yellow" +main = "magenta" +runner = "green" +watcher = "cyan" [log] - main_only = false - silent = false - time = false +main_only = false +silent = false +time = false [misc] - clean_on_exit = false +clean_on_exit = false [proxy] - app_port = 0 - enabled = false - proxy_port = 0 +app_port = 0 +enabled = false +proxy_port = 0 [screen] - clear_on_rebuild = false - keep_scroll = true +clear_on_rebuild = false +keep_scroll = true diff --git a/.sqlfluff b/.sqlfluff new file mode 100644 index 0000000..bc52c80 --- /dev/null +++ b/.sqlfluff @@ -0,0 +1,7 @@ +[sqlfluff] +dialect = sqlite +templater = placeholder +processes = -1 + +[sqlfluff:templater:placeholder] +param_style = question_mark diff --git a/Makefile b/Makefile index 8cf33ea..e5f387b 100644 --- a/Makefile +++ b/Makefile @@ -1,27 +1,17 @@ -.PHONY: dev build +TARGET = ./tmp/server +FRONTEND = ./web/frontend -dev: - @if command -v $(HOME)/go/bin/air > /dev/null; then \ - AIR_CMD="$(HOME)/go/bin/air"; \ - elif command -v air > /dev/null; then \ - AIR_CMD="air"; \ - else \ - read -p "air is not installed. Install it? [Y/n] " choice; \ - if [ "$$choice" != "n" ] && [ "$$choice" != "N" ]; then \ - echo "Installing..."; \ - go install github.com/air-verse/air@latest; \ - AIR_CMD="$(HOME)/go/bin/air"; \ - else \ - echo "Exiting..."; \ - exit 1; \ - fi; \ - fi; \ - echo "Starting Air..."; \ - $$AIR_CMD +SVELTE_COMPONENTS = status -build: - @echo "Generate Tailwind CSS..." - go generate - @echo "Building Go server..." - go build -o tmp/server main.go - @echo "Build complete." +SVELTE_TARGETS = $(addprefix $(FRONTEND)/public/build/,$(addsuffix .ts, $(SVELTE_COMPONENTS))) + +.PHONY: $(TARGET) +.NOTPARALLEL: + +$(TARGET): $(SVELTE_TARGETS) + $(info ===> BUILD Go server) + @go build -o $(TARGET) main.go + +$(SVELTE_TARGETS): $(SVELTE_SRC) + $(info ===> BUILD frontend) + cd $(FRONTEND) && npm install && npm run build diff --git a/go.mod b/go.mod index 64d11ff..3bc30f3 100644 --- a/go.mod +++ b/go.mod @@ -12,15 +12,20 @@ require ( require ( github.com/dustin/go-humanize v1.0.1 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/securecookie v1.1.2 // indirect + github.com/gorilla/sessions v1.4.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-sqlite3 v1.14.24 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect + github.com/olivere/vite v0.1.0 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect go.uber.org/atomic v1.11.0 // indirect + golang.org/x/crypto v0.38.0 // indirect golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect golang.org/x/sys v0.33.0 // indirect + golang.org/x/time v0.11.0 // indirect modernc.org/libc v1.65.7 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect diff --git a/go.sum b/go.sum index a123efb..2797b53 100644 --- a/go.sum +++ b/go.sum @@ -8,6 +8,10 @@ github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17k github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= +github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= +github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ= +github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -23,6 +27,8 @@ github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBW github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/olivere/vite v0.1.0 h1:Wi5zTtS3BbnOrfG+oRT7KZOI9lp48gRv59VptSBmPO4= +github.com/olivere/vite v0.1.0/go.mod h1:ef1SWmGSWAYJxSuY2Bu90YLQ7hUBxYmejIVuFGsIIe8= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= @@ -31,6 +37,8 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= +golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM= golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8= golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= @@ -40,6 +48,8 @@ golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= +golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/internal/api/rest.go b/internal/api/rest.go new file mode 100644 index 0000000..aa71d3d --- /dev/null +++ b/internal/api/rest.go @@ -0,0 +1,131 @@ +package api + +import ( + "bufio" + "encoding/json" + "fmt" + "io" + "log/slog" + "net/http" + "strconv" + + "git.clustercockpit.org/moebiusband/go-http-skeleton/internal/repository" +) + +type ErrorResponse struct { + // Statustext of Errorcode + Status string `json:"status"` + Error string `json:"error"` // Error Message +} + +func MountApiEndpoints(r *http.ServeMux) { + r.HandleFunc("POST /api/news/", createNewsItem) + r.HandleFunc("GET /api/news/", readNewsItems) + r.HandleFunc("GET /api/news/{id}", readNewsItem) + r.HandleFunc("PATCH /api/news/", updateNewsItem) + r.HandleFunc("DELETE /api/news/{id}", deleteNewsItem) +} + +func handleError(err error, statusCode int, rw http.ResponseWriter) { + slog.Warn("rest error", "error", err) + rw.Header().Add("Content-Type", "application/json") + rw.WriteHeader(statusCode) + json.NewEncoder(rw).Encode(ErrorResponse{ + Status: http.StatusText(statusCode), + Error: err.Error(), + }) +} + +func decode(r io.Reader, val any) error { + dec := json.NewDecoder(r) + dec.DisallowUnknownFields() + return dec.Decode(val) +} + +func createNewsItem(rw http.ResponseWriter, r *http.Request) { + req := repository.CreateNewsEntryParams{} + if err := decode(r.Body, &req); err != nil { + handleError(fmt.Errorf("parsing request body failed: %w", err), http.StatusBadRequest, rw) + return + } + + repo := repository.GetRepository() + err := repo.CreateNewsEntry(r.Context(), req) + if err != nil { + handleError(err, http.StatusBadRequest, rw) + return + } +} + +func readNewsItems(rw http.ResponseWriter, r *http.Request) { + repo := repository.GetRepository() + items, err := repo.ListNews(r.Context()) + if err != nil { + handleError(err, http.StatusBadRequest, rw) + return + } + + slog.Debug("/api/news returned", "newscount", len(items)) + rw.Header().Add("Content-Type", "application/json") + bw := bufio.NewWriter(rw) + defer bw.Flush() + + if err := json.NewEncoder(bw).Encode(items); err != nil { + handleError(err, http.StatusInternalServerError, rw) + return + } +} + +func readNewsItem(rw http.ResponseWriter, r *http.Request) { + repo := repository.GetRepository() + id, err := strconv.ParseInt(r.PathValue("id"), 10, 64) + if err != nil { + handleError(err, http.StatusBadRequest, rw) + return + } + + item, err := repo.GetNewsEntry(r.Context(), id) + if err != nil { + handleError(err, http.StatusBadRequest, rw) + return + } + + rw.Header().Add("Content-Type", "application/json") + bw := bufio.NewWriter(rw) + defer bw.Flush() + + if err := json.NewEncoder(bw).Encode(item); err != nil { + handleError(err, http.StatusInternalServerError, rw) + return + } +} + +func updateNewsItem(rw http.ResponseWriter, r *http.Request) { + req := repository.UpdateNewsEntryParams{} + if err := decode(r.Body, &req); err != nil { + handleError(fmt.Errorf("parsing request body failed: %w", err), http.StatusBadRequest, rw) + return + } + + repo := repository.GetRepository() + err := repo.UpdateNewsEntry(r.Context(), req) + if err != nil { + handleError(err, http.StatusBadRequest, rw) + return + } +} + +func deleteNewsItem(rw http.ResponseWriter, r *http.Request) { + repo := repository.GetRepository() + id, err := strconv.ParseInt(r.PathValue("id"), 10, 64) + if err != nil { + handleError(err, http.StatusBadRequest, rw) + return + } + + err = repo.DeleteNewsEntry(r.Context(), id) + if err != nil { + handleError(err, http.StatusBadRequest, rw) + return + } +} diff --git a/internal/auth/auth.go b/internal/auth/auth.go new file mode 100644 index 0000000..e42ca49 --- /dev/null +++ b/internal/auth/auth.go @@ -0,0 +1,248 @@ +// Copyright (C) NHR@FAU, University Erlangen-Nuremberg. +// All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. +package auth + +import ( + "context" + "crypto/rand" + "database/sql" + "encoding/base64" + "errors" + "log/slog" + "net" + "net/http" + "os" + "sync" + "time" + + "git.clustercockpit.org/moebiusband/go-http-skeleton/internal/repository" + "github.com/gorilla/sessions" + "golang.org/x/time/rate" +) + +type Authenticator interface { + CanLogin(user *repository.AppUser, username string, rw http.ResponseWriter, r *http.Request) (*repository.AppUser, bool) + Login(user *repository.AppUser, rw http.ResponseWriter, r *http.Request) (*repository.AppUser, error) +} + +var ( + initOnce sync.Once + authInstance *Authentication +) + +var ipUserLimiters sync.Map + +func getIPUserLimiter(ip, username string) *rate.Limiter { + key := ip + ":" + username + limiter, ok := ipUserLimiters.Load(key) + if !ok { + newLimiter := rate.NewLimiter(rate.Every(time.Hour/10), 10) + ipUserLimiters.Store(key, newLimiter) + return newLimiter + } + return limiter.(*rate.Limiter) +} + +type Authentication struct { + sessionStore *sessions.CookieStore + LocalAuth *LocalAuthenticator + authenticators []Authenticator + SessionMaxAge time.Duration +} + +func (auth *Authentication) AuthViaSession( + rw http.ResponseWriter, + r *http.Request, +) (*repository.AppUser, error) { + session, err := auth.sessionStore.Get(r, "session") + if err != nil { + slog.Error("Error while getting session store") + return nil, err + } + + if session.IsNew { + return nil, nil + } + + // TODO: Check if session keys exist + username, _ := session.Values["username"].(string) + return &repository.AppUser{ + UserName: username, + }, nil +} + +func Init() { + initOnce.Do(func() { + authInstance = &Authentication{} + + sessKey := os.Getenv("SESSION_KEY") + if sessKey == "" { + slog.Warn("environment variable 'SESSION_KEY' not set (will use non-persistent random key)") + bytes := make([]byte, 32) + if _, err := rand.Read(bytes); err != nil { + slog.Error("Error while initializing authentication -> failed to generate random bytes for session key") + os.Exit(1) + } + authInstance.sessionStore = sessions.NewCookieStore(bytes) + } else { + bytes, err := base64.StdEncoding.DecodeString(sessKey) + if err != nil { + slog.Error("Error while initializing authentication -> decoding session key failed") + os.Exit(1) + } + authInstance.sessionStore = sessions.NewCookieStore(bytes) + } + + if d, err := time.ParseDuration("24h"); err == nil { + authInstance.SessionMaxAge = d + } + + authInstance.LocalAuth = &LocalAuthenticator{} + if err := authInstance.LocalAuth.Init(); err != nil { + slog.Error("Error while initializing authentication -> localAuth init failed") + os.Exit(1) + } + authInstance.authenticators = append(authInstance.authenticators, authInstance.LocalAuth) + }) +} + +func GetAuthInstance() *Authentication { + if authInstance == nil { + slog.Error("Authentication module not initialized!") + } + + return authInstance +} + +func (auth *Authentication) SaveSession(rw http.ResponseWriter, + r *http.Request, user *repository.AppUser, +) error { + session, err := auth.sessionStore.New(r, "session") + if err != nil { + slog.Error("session creation failed", "error", err.Error()) + http.Error(rw, err.Error(), http.StatusInternalServerError) + return err + } + + if auth.SessionMaxAge != 0 { + session.Options.MaxAge = int(auth.SessionMaxAge.Seconds()) + } + session.Options.Secure = false + session.Options.SameSite = http.SameSiteStrictMode + session.Values["username"] = user.UserName + if err := auth.sessionStore.Save(r, rw, session); err != nil { + slog.Warn("session save failed", "error", err.Error()) + http.Error(rw, err.Error(), http.StatusInternalServerError) + return err + } + + return nil +} + +func (auth *Authentication) Login( + onfailure func(rw http.ResponseWriter, r *http.Request, loginErr error), +) http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + ip, _, err := net.SplitHostPort(r.RemoteAddr) + if err != nil { + ip = r.RemoteAddr + } + + username := r.FormValue("username") + + limiter := getIPUserLimiter(ip, username) + if !limiter.Allow() { + slog.Warn("AUTH/RATE > Too many login attempts for combination", "ip", ip, "username", username) + onfailure(rw, r, errors.New("too many login attempts, try again in a few minutes")) + return + } + + var dbUser repository.AppUser + if username != "" { + var err error + dbUser, err = repository.GetRepository().GetUser(r.Context(), username) + if err != nil && err != sql.ErrNoRows { + slog.Error("Error while loading user", "username", username) + } + } + + for _, authenticator := range auth.authenticators { + var ok bool + var user *repository.AppUser + if user, ok = authenticator.CanLogin(&dbUser, username, rw, r); !ok { + continue + } else { + slog.Debug("Can login with user", "username", user.UserName) + } + + user, err := authenticator.Login(user, rw, r) + if err != nil { + slog.Warn("user login failed", "error", err.Error()) + onfailure(rw, r, err) + return + } + + if err := auth.SaveSession(rw, r, user); err != nil { + return + } + + slog.Info("login successfull", "user", user.UserName) + ctx := context.WithValue(r.Context(), "user", user) + + if r.FormValue("redirect") != "" { + http.RedirectHandler(r.FormValue("redirect"), http.StatusFound).ServeHTTP(rw, r.WithContext(ctx)) + return + } + + http.RedirectHandler("/", http.StatusFound).ServeHTTP(rw, r.WithContext(ctx)) + return + } + + slog.Debug("login failed: no authenticator applied") + onfailure(rw, r, errors.New("no authenticator applied")) + }) +} + +func (auth *Authentication) Auth( + onsuccess http.Handler, + onfailure func(rw http.ResponseWriter, r *http.Request, authErr error), +) http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + user, err := auth.AuthViaSession(rw, r) + if err != nil { + slog.Info("auth -> authentication failed", "error", err.Error()) + http.Error(rw, err.Error(), http.StatusUnauthorized) + return + } + if user != nil { + ctx := context.WithValue(r.Context(), "user", user) + onsuccess.ServeHTTP(rw, r.WithContext(ctx)) + return + } + + slog.Info("auth -> authentication failed") + onfailure(rw, r, errors.New("unauthorized (please login first)")) + }) +} + +func (auth *Authentication) Logout(onsuccess http.Handler) http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + session, err := auth.sessionStore.Get(r, "session") + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + + if !session.IsNew { + session.Options.MaxAge = -1 + if err := auth.sessionStore.Save(r, rw, session); err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + } + + onsuccess.ServeHTTP(rw, r) + }) +} diff --git a/internal/auth/local.go b/internal/auth/local.go new file mode 100644 index 0000000..a92303b --- /dev/null +++ b/internal/auth/local.go @@ -0,0 +1,47 @@ +// Copyright (C) NHR@FAU, University Erlangen-Nuremberg. +// All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. +package auth + +import ( + "fmt" + "log/slog" + "net/http" + + "git.clustercockpit.org/moebiusband/go-http-skeleton/internal/repository" + "golang.org/x/crypto/bcrypt" +) + +type LocalAuthenticator struct { + auth *Authentication +} + +var _ Authenticator = (*LocalAuthenticator)(nil) + +func (la *LocalAuthenticator) Init() error { + return nil +} + +func (la *LocalAuthenticator) CanLogin( + user *repository.AppUser, + username string, + rw http.ResponseWriter, + r *http.Request, +) (*repository.AppUser, bool) { + return user, user != nil +} + +func (la *LocalAuthenticator) Login( + user *repository.AppUser, + rw http.ResponseWriter, + r *http.Request, +) (*repository.AppUser, error) { + if e := bcrypt.CompareHashAndPassword([]byte(*user.UserPass), + []byte(r.FormValue("password"))); e != nil { + slog.Error("AUTH/LOCAL > Authentication for user failed!", "user", user.UserName) + return nil, fmt.Errorf("Authentication failed") + } + + return user, nil +} diff --git a/internal/handlers/admin.go b/internal/handlers/admin.go new file mode 100644 index 0000000..c2b9d12 --- /dev/null +++ b/internal/handlers/admin.go @@ -0,0 +1,33 @@ +package handlers + +import ( + "html/template" + "log/slog" + "net/http" + + "git.clustercockpit.org/moebiusband/go-http-skeleton/web" + "github.com/olivere/vite" +) + +func AdminHandler() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + tpl := template.Must(template.ParseFS(web.Templates, "templates/admin.html", "templates/base.html")) + viteFragment, err := vite.HTMLFragment(vite.Config{ + FS: web.StaticAssets, + IsDev: false, + }) + if err != nil { + http.Error(w, "Error instantiating vite fragment", http.StatusInternalServerError) + return + } + + data := web.PageData{ + Title: "Admin", + Vite: viteFragment, + } + if err := tpl.ExecuteTemplate(w, "base", data); err != nil { + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + slog.Error("Error executing template", "error", err) + } + } +} diff --git a/internal/handlers/root.go b/internal/handlers/root.go index a9628c7..d3809e5 100644 --- a/internal/handlers/root.go +++ b/internal/handlers/root.go @@ -8,29 +8,12 @@ import ( "git.clustercockpit.org/moebiusband/go-http-skeleton/web" ) -type PageData struct { - Title string -} - func RootHandler() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { slog.Info("Render root handler") tpl := template.Must(template.ParseFS(web.Templates, "templates/index.html", "templates/base.html")) - // basefile, err := web.Templates.ReadFile("templates/base.html") - // if err != nil { - // http.Error(w, "Internal Server Error", http.StatusInternalServerError) - // slog.Error("Error reading template", "error", err) - // return - // } - // file, err := web.Templates.ReadFile("templates/index.html") - // if err != nil { - // http.Error(w, "Internal Server Error", http.StatusInternalServerError) - // slog.Error("Error reading template", "error", err) - // return - // } - - data := PageData{ + data := web.PageData{ Title: "DyeForYarn", } if err := tpl.ExecuteTemplate(w, "base", data); err != nil { diff --git a/internal/middleware/recover.go b/internal/middleware/recover.go index e52a927..bb54a15 100644 --- a/internal/middleware/recover.go +++ b/internal/middleware/recover.go @@ -5,7 +5,7 @@ import ( "net/http" ) -func RecoverMiddleware(next http.Handler) http.Handler { +func Recover(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { defer func() { if err := recover(); err != nil { diff --git a/internal/middleware/securedCheck.go b/internal/middleware/securedCheck.go new file mode 100644 index 0000000..f65e017 --- /dev/null +++ b/internal/middleware/securedCheck.go @@ -0,0 +1,27 @@ +package middleware + +import ( + "net/http" + + "git.clustercockpit.org/moebiusband/go-http-skeleton/internal/auth" + "git.clustercockpit.org/moebiusband/go-http-skeleton/web" +) + +func SecuredCheck(next http.Handler) http.Handler { + authHandle := auth.GetAuthInstance() + + return authHandle.Auth( + // On success; + next, + + // On failure: + func(rw http.ResponseWriter, r *http.Request, err error) { + rw.WriteHeader(http.StatusUnauthorized) + web.RenderTemplate(rw, "login", web.PageData{ + Title: "Authentication failed - ClusterCockpit", + MsgType: "alert-danger", + Message: err.Error(), + Redirect: r.RequestURI, + }) + }) +} diff --git a/internal/repository/db.go b/internal/repository/db.go new file mode 100644 index 0000000..e77f620 --- /dev/null +++ b/internal/repository/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.29.0 + +package repository + +import ( + "context" + "database/sql" +) + +type DBTX interface { + ExecContext(context.Context, string, ...interface{}) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...interface{}) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/internal/repository/dbConn.go b/internal/repository/dbConn.go index 3056b0d..7ff5e14 100644 --- a/internal/repository/dbConn.go +++ b/internal/repository/dbConn.go @@ -3,6 +3,7 @@ package repository import ( "database/sql" "log/slog" + "os" "sync" ) @@ -21,6 +22,11 @@ func Connect(dsnURI string) { } repo = New(dbConn) + err = checkDBVersion(dbConn) + if err != nil { + slog.Error("DB Connection: Failed DB version check", "error", err) + os.Exit(1) + } }) } @@ -32,10 +38,11 @@ func GetConnection() (*sql.DB, error) { return dbConn, nil } -func GetRepository() (*Queries, error) { +func GetRepository() *Queries { if repo == nil { slog.Error("Database connection not initialized!") + os.Exit(1) } - return repo, nil + return repo } diff --git a/internal/repository/migration.go b/internal/repository/migration.go index 91e98ad..8d37323 100644 --- a/internal/repository/migration.go +++ b/internal/repository/migration.go @@ -36,14 +36,14 @@ func checkDBVersion(db *sql.DB) error { v, dirty, err := m.Version() if err != nil { if err == migrate.ErrNilVersion { - slog.Warn("Legacy database without version or missing database file!") + slog.Error("Legacy database without version or missing database file!") } else { return err } } if v < Version { - return fmt.Errorf("unsupported database version %d, need %d.\nPlease backup your database file and run cc-backend -migrate-db", v, Version) + return fmt.Errorf("unsupported database version %d, need %d.\nPlease backup your database file and run server -migrate-db", v, Version) } else if v > Version { return fmt.Errorf("unsupported database version %d, need %d.\nPlease refer to documentation how to downgrade db with external migrate tool", v, Version) } @@ -61,7 +61,7 @@ func getMigrateInstance(dsnURI string) (m *migrate.Migrate, err error) { slog.Error("failed to get instance", "Error", err) } - m, err = migrate.NewWithSourceInstance("iofs", d, dsnURI) + m, err = migrate.NewWithSourceInstance("iofs", d, "sqlite3://"+dsnURI) if err != nil { return m, err } @@ -75,7 +75,7 @@ func MigrateDB(db string) error { return err } - v, dirty, err := m.Version() + _, dirty, err := m.Version() if err != nil { if err == migrate.ErrNilVersion { slog.Warn("Legacy database without version or missing database file!") @@ -84,10 +84,6 @@ func MigrateDB(db string) error { } } - if v < Version { - slog.Info("unsupported database version %d, need %d.\nPlease backup your database file and run cc-backend -migrate-db", v, Version) - } - if dirty { return fmt.Errorf("last migration to version %d has failed, please fix the db manually and force version with -force-db flag", Version) } diff --git a/internal/repository/migrations/0000_schema.up.sql b/internal/repository/migrations/0000_schema.up.sql deleted file mode 100644 index 4ba339c..0000000 --- a/internal/repository/migrations/0000_schema.up.sql +++ /dev/null @@ -1,11 +0,0 @@ -CREATE TABLE news ( - id INTEGER PRIMARY KEY, - name TEXT NOT NULL, - bio TEXT -); - -CREATE TABLE retailer ( - id INTEGER PRIMARY KEY, - name TEXT NOT NULL, - bio TEXT -); diff --git a/internal/repository/migrations/0000_schema.down.sql b/internal/repository/migrations/01_schema.down.sql similarity index 100% rename from internal/repository/migrations/0000_schema.down.sql rename to internal/repository/migrations/01_schema.down.sql diff --git a/internal/repository/migrations/01_schema.up.sql b/internal/repository/migrations/01_schema.up.sql new file mode 100644 index 0000000..0513bfe --- /dev/null +++ b/internal/repository/migrations/01_schema.up.sql @@ -0,0 +1,23 @@ +CREATE TABLE IF NOT EXISTS news ( + id INTEGER PRIMARY KEY, + news_title TEXT NOT NULL, + news_text TEXT NOT NULL, + news_date DATETIME, + news_publish DATETIME, + display TINYINT NOT NULL DEFAULT 0 +); + +CREATE TABLE IF NOT EXISTS retailer ( + id INTEGER PRIMARY KEY, + shopname TEXT NOT NULL, + url TEXT NOT NULL, + country TEXT NOT NULL, + display TINYINT NOT NULL DEFAULT 1 +); + +CREATE TABLE IF NOT EXISTS app_user ( + user_name TEXT PRIMARY KEY, + user_pass TEXT DEFAULT NULL, + realname TEXT DEFAULT NULL, + email TEXT DEFAULT NULL +); diff --git a/internal/repository/models.go b/internal/repository/models.go new file mode 100644 index 0000000..7706f4e --- /dev/null +++ b/internal/repository/models.go @@ -0,0 +1,33 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.29.0 + +package repository + +import ( + "time" +) + +type AppUser struct { + UserName string `db:"user_name" json:"userName"` + UserPass *string `db:"user_pass" json:"userPass"` + Realname *string `db:"realname" json:"realname"` + Email *string `db:"email" json:"email"` +} + +type News struct { + ID int64 `db:"id" json:"id"` + NewsTitle string `db:"news_title" json:"newsTitle"` + NewsText string `db:"news_text" json:"newsText"` + NewsDate *time.Time `db:"news_date" json:"newsDate"` + NewsPublish *time.Time `db:"news_publish" json:"newsPublish"` + Display int64 `db:"display" json:"display"` +} + +type Retailer struct { + ID int64 `db:"id" json:"id"` + Shopname string `db:"shopname" json:"shopname"` + Url string `db:"url" json:"url"` + Country string `db:"country" json:"country"` + Display int64 `db:"display" json:"display"` +} diff --git a/internal/repository/query.sql.go b/internal/repository/query.sql.go new file mode 100644 index 0000000..7b8da4b --- /dev/null +++ b/internal/repository/query.sql.go @@ -0,0 +1,386 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.29.0 +// source: query.sql + +package repository + +import ( + "context" + "time" +) + +const createNewsEntry = `-- name: CreateNewsEntry :exec +INSERT INTO news ( + news_title, news_text, news_date, + news_publish, display +) VALUES (?, ?, ?, ?, ?) +` + +type CreateNewsEntryParams struct { + NewsTitle string `db:"news_title" json:"newsTitle"` + NewsText string `db:"news_text" json:"newsText"` + NewsDate *time.Time `db:"news_date" json:"newsDate"` + NewsPublish *time.Time `db:"news_publish" json:"newsPublish"` + Display int64 `db:"display" json:"display"` +} + +func (q *Queries) CreateNewsEntry(ctx context.Context, arg CreateNewsEntryParams) error { + _, err := q.db.ExecContext(ctx, createNewsEntry, + arg.NewsTitle, + arg.NewsText, + arg.NewsDate, + arg.NewsPublish, + arg.Display, + ) + return err +} + +const createRetailer = `-- name: CreateRetailer :exec +INSERT INTO retailer ( + shopname, url, country, display +) VALUES (?, ?, ?, ?) +` + +type CreateRetailerParams struct { + Shopname string `db:"shopname" json:"shopname"` + Url string `db:"url" json:"url"` + Country string `db:"country" json:"country"` + Display int64 `db:"display" json:"display"` +} + +func (q *Queries) CreateRetailer(ctx context.Context, arg CreateRetailerParams) error { + _, err := q.db.ExecContext(ctx, createRetailer, + arg.Shopname, + arg.Url, + arg.Country, + arg.Display, + ) + return err +} + +const createUser = `-- name: CreateUser :exec +INSERT INTO app_user ( + user_name, user_pass +) +VALUES (?, ?) +` + +type CreateUserParams struct { + UserName string `db:"user_name" json:"userName"` + UserPass *string `db:"user_pass" json:"userPass"` +} + +func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) error { + _, err := q.db.ExecContext(ctx, createUser, arg.UserName, arg.UserPass) + return err +} + +const deleteNewsEntry = `-- name: DeleteNewsEntry :exec +DELETE FROM news +WHERE id = ? +` + +func (q *Queries) DeleteNewsEntry(ctx context.Context, id int64) error { + _, err := q.db.ExecContext(ctx, deleteNewsEntry, id) + return err +} + +const deleteRetailer = `-- name: DeleteRetailer :exec +DELETE FROM news +WHERE id = ? +` + +func (q *Queries) DeleteRetailer(ctx context.Context, id int64) error { + _, err := q.db.ExecContext(ctx, deleteRetailer, id) + return err +} + +const deleteUser = `-- name: DeleteUser :exec +DELETE FROM app_user +WHERE user_name = ? +` + +func (q *Queries) DeleteUser(ctx context.Context, userName string) error { + _, err := q.db.ExecContext(ctx, deleteUser, userName) + return err +} + +const getNewsEntry = `-- name: GetNewsEntry :one +SELECT id, news_title, news_text, news_date, news_publish, display FROM news +WHERE id = ? LIMIT 1 +` + +func (q *Queries) GetNewsEntry(ctx context.Context, id int64) (News, error) { + row := q.db.QueryRowContext(ctx, getNewsEntry, id) + var i News + err := row.Scan( + &i.ID, + &i.NewsTitle, + &i.NewsText, + &i.NewsDate, + &i.NewsPublish, + &i.Display, + ) + return i, err +} + +const getUser = `-- name: GetUser :one +SELECT user_name, user_pass, realname, email FROM app_user +WHERE user_name = ? LIMIT 1 +` + +func (q *Queries) GetUser(ctx context.Context, userName string) (AppUser, error) { + row := q.db.QueryRowContext(ctx, getUser, userName) + var i AppUser + err := row.Scan( + &i.UserName, + &i.UserPass, + &i.Realname, + &i.Email, + ) + return i, err +} + +const listActiveNews = `-- name: ListActiveNews :many +SELECT id, news_title, news_text, news_date, news_publish, display FROM news +WHERE display = 1 +ORDER BY news_date +` + +func (q *Queries) ListActiveNews(ctx context.Context) ([]News, error) { + rows, err := q.db.QueryContext(ctx, listActiveNews) + if err != nil { + return nil, err + } + defer rows.Close() + var items []News + for rows.Next() { + var i News + if err := rows.Scan( + &i.ID, + &i.NewsTitle, + &i.NewsText, + &i.NewsDate, + &i.NewsPublish, + &i.Display, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const listActiveRetailers = `-- name: ListActiveRetailers :many +SELECT id, shopname, url, country, display FROM retailer +WHERE display = 1 +ORDER BY shopname +` + +func (q *Queries) ListActiveRetailers(ctx context.Context) ([]Retailer, error) { + rows, err := q.db.QueryContext(ctx, listActiveRetailers) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Retailer + for rows.Next() { + var i Retailer + if err := rows.Scan( + &i.ID, + &i.Shopname, + &i.Url, + &i.Country, + &i.Display, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const listNews = `-- name: ListNews :many +SELECT id, news_title, news_text, news_date, news_publish, display FROM news +ORDER BY news_date +` + +func (q *Queries) ListNews(ctx context.Context) ([]News, error) { + rows, err := q.db.QueryContext(ctx, listNews) + if err != nil { + return nil, err + } + defer rows.Close() + var items []News + for rows.Next() { + var i News + if err := rows.Scan( + &i.ID, + &i.NewsTitle, + &i.NewsText, + &i.NewsDate, + &i.NewsPublish, + &i.Display, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const listRetailers = `-- name: ListRetailers :many +SELECT id, shopname, url, country, display FROM retailer +ORDER BY shopname +` + +func (q *Queries) ListRetailers(ctx context.Context) ([]Retailer, error) { + rows, err := q.db.QueryContext(ctx, listRetailers) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Retailer + for rows.Next() { + var i Retailer + if err := rows.Scan( + &i.ID, + &i.Shopname, + &i.Url, + &i.Country, + &i.Display, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const listUsers = `-- name: ListUsers :many +SELECT user_name, user_pass, realname, email FROM app_user +ORDER BY user_name +` + +func (q *Queries) ListUsers(ctx context.Context) ([]AppUser, error) { + rows, err := q.db.QueryContext(ctx, listUsers) + if err != nil { + return nil, err + } + defer rows.Close() + var items []AppUser + for rows.Next() { + var i AppUser + if err := rows.Scan( + &i.UserName, + &i.UserPass, + &i.Realname, + &i.Email, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const updateNewsEntry = `-- name: UpdateNewsEntry :exec +UPDATE news +SET + news_title = ?, news_text = ?, + news_date = ?, news_publish = ?, display = ? +WHERE id = ? +` + +type UpdateNewsEntryParams struct { + NewsTitle string `db:"news_title" json:"newsTitle"` + NewsText string `db:"news_text" json:"newsText"` + NewsDate *time.Time `db:"news_date" json:"newsDate"` + NewsPublish *time.Time `db:"news_publish" json:"newsPublish"` + Display int64 `db:"display" json:"display"` + ID int64 `db:"id" json:"id"` +} + +func (q *Queries) UpdateNewsEntry(ctx context.Context, arg UpdateNewsEntryParams) error { + _, err := q.db.ExecContext(ctx, updateNewsEntry, + arg.NewsTitle, + arg.NewsText, + arg.NewsDate, + arg.NewsPublish, + arg.Display, + arg.ID, + ) + return err +} + +const updateRetailer = `-- name: UpdateRetailer :exec +UPDATE retailer +SET shopname = ?, url = ?, country = ?, display = ? +WHERE id = ? +` + +type UpdateRetailerParams struct { + Shopname string `db:"shopname" json:"shopname"` + Url string `db:"url" json:"url"` + Country string `db:"country" json:"country"` + Display int64 `db:"display" json:"display"` + ID int64 `db:"id" json:"id"` +} + +func (q *Queries) UpdateRetailer(ctx context.Context, arg UpdateRetailerParams) error { + _, err := q.db.ExecContext(ctx, updateRetailer, + arg.Shopname, + arg.Url, + arg.Country, + arg.Display, + arg.ID, + ) + return err +} + +const updateUser = `-- name: UpdateUser :exec +UPDATE app_user +SET user_pass = ? +WHERE user_name = ? +` + +type UpdateUserParams struct { + UserPass *string `db:"user_pass" json:"userPass"` + UserName string `db:"user_name" json:"userName"` +} + +func (q *Queries) UpdateUser(ctx context.Context, arg UpdateUserParams) error { + _, err := q.db.ExecContext(ctx, updateUser, arg.UserPass, arg.UserName) + return err +} diff --git a/internal/repository/sql/query.sql b/internal/repository/sql/query.sql index 212a226..3641ff0 100644 --- a/internal/repository/sql/query.sql +++ b/internal/repository/sql/query.sql @@ -1,25 +1,75 @@ --- name: GetAuthor :one -SELECT * FROM authors +-- name: GetUser :one +SELECT * FROM app_user +WHERE user_name = ? LIMIT 1; +-- +-- name: ListUsers :many +SELECT * FROM app_user +ORDER BY user_name; + +-- name: CreateUser :exec +INSERT INTO app_user ( + user_name, user_pass +) +VALUES (?, ?); + +-- name: UpdateUser :exec +UPDATE app_user +SET user_pass = ? +WHERE user_name = ?; + +-- name: DeleteUser :exec +DELETE FROM app_user +WHERE user_name = ?; +-- +-- name: GetNewsEntry :one +SELECT * FROM news WHERE id = ? LIMIT 1; --- name: ListAuthors :many -SELECT * FROM authors -ORDER BY name; +-- name: ListNews :many +SELECT * FROM news +ORDER BY news_date; --- name: CreateAuthor :one -INSERT INTO authors ( - name, bio -) VALUES ( - ?, ? -) -RETURNING *; +-- name: ListActiveNews :many +SELECT * FROM news +WHERE display = 1 +ORDER BY news_date; --- name: UpdateAuthor :exec -UPDATE authors -set name = ?, -bio = ? +-- name: CreateNewsEntry :exec +INSERT INTO news ( + news_title, news_text, news_date, + news_publish, display +) VALUES (?, ?, ?, ?, ?); + +-- name: UpdateNewsEntry :exec +UPDATE news +SET + news_title = ?, news_text = ?, + news_date = ?, news_publish = ?, display = ? WHERE id = ?; --- name: DeleteAuthor :exec -DELETE FROM authors +-- name: DeleteNewsEntry :exec +DELETE FROM news +WHERE id = ?; +-- +-- name: ListRetailers :many +SELECT * FROM retailer +ORDER BY shopname; + +-- name: ListActiveRetailers :many +SELECT * FROM retailer +WHERE display = 1 +ORDER BY shopname; + +-- name: CreateRetailer :exec +INSERT INTO retailer ( + shopname, url, country, display +) VALUES (?, ?, ?, ?); + +-- name: UpdateRetailer :exec +UPDATE retailer +SET shopname = ?, url = ?, country = ?, display = ? +WHERE id = ?; + +-- name: DeleteRetailer :exec +DELETE FROM news WHERE id = ?; diff --git a/main.go b/main.go index a9f00a7..7262715 100644 --- a/main.go +++ b/main.go @@ -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: :") + flag.StringVar(&flagDelUser, "del-user", "", "Remove a existing user. Argument format: ") + 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: ::\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{ diff --git a/web/frontend/.gitignore b/web/frontend/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/web/frontend/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/web/frontend/README.md b/web/frontend/README.md new file mode 100644 index 0000000..e6cd94f --- /dev/null +++ b/web/frontend/README.md @@ -0,0 +1,47 @@ +# Svelte + TS + Vite + +This template should help get you started developing with Svelte and TypeScript in Vite. + +## Recommended IDE Setup + +[VS Code](https://code.visualstudio.com/) + [Svelte](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode). + +## Need an official Svelte framework? + +Check out [SvelteKit](https://github.com/sveltejs/kit#readme), which is also powered by Vite. Deploy anywhere with its serverless-first approach and adapt to various platforms, with out of the box support for TypeScript, SCSS, and Less, and easily-added support for mdsvex, GraphQL, PostCSS, Tailwind CSS, and more. + +## Technical considerations + +**Why use this over SvelteKit?** + +- It brings its own routing solution which might not be preferable for some users. +- It is first and foremost a framework that just happens to use Vite under the hood, not a Vite app. + +This template contains as little as possible to get started with Vite + TypeScript + Svelte, while taking into account the developer experience with regards to HMR and intellisense. It demonstrates capabilities on par with the other `create-vite` templates and is a good starting point for beginners dipping their toes into a Vite + Svelte project. + +Should you later need the extended capabilities and extensibility provided by SvelteKit, the template has been structured similarly to SvelteKit so that it is easy to migrate. + +**Why `global.d.ts` instead of `compilerOptions.types` inside `jsconfig.json` or `tsconfig.json`?** + +Setting `compilerOptions.types` shuts out all other types not explicitly listed in the configuration. Using triple-slash references keeps the default TypeScript setting of accepting type information from the entire workspace, while also adding `svelte` and `vite/client` type information. + +**Why include `.vscode/extensions.json`?** + +Other templates indirectly recommend extensions via the README, but this file allows VS Code to prompt the user to install the recommended extension upon opening the project. + +**Why enable `allowJs` in the TS template?** + +While `allowJs: false` would indeed prevent the use of `.js` files in the project, it does not prevent the use of JavaScript syntax in `.svelte` files. In addition, it would force `checkJs: false`, bringing the worst of both worlds: not being able to guarantee the entire codebase is TypeScript, and also having worse typechecking for the existing JavaScript. In addition, there are valid use cases in which a mixed codebase may be relevant. + +**Why is HMR not preserving my local component state?** + +HMR state preservation comes with a number of gotchas! It has been disabled by default in both `svelte-hmr` and `@sveltejs/vite-plugin-svelte` due to its often surprising behavior. You can read the details [here](https://github.com/rixo/svelte-hmr#svelte-hmr). + +If you have state that's important to retain within a component, consider creating an external store which would not be replaced by HMR. + +```ts +// store.ts +// An extremely simple external store +import { writable } from 'svelte/store' +export default writable(0) +``` diff --git a/web/frontend/index.html b/web/frontend/index.html new file mode 100644 index 0000000..b6c5f0a --- /dev/null +++ b/web/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + Svelte + TS + + +
+ + + diff --git a/web/frontend/package-lock.json b/web/frontend/package-lock.json new file mode 100644 index 0000000..baaf567 --- /dev/null +++ b/web/frontend/package-lock.json @@ -0,0 +1,1412 @@ +{ + "name": "frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "0.0.0", + "devDependencies": { + "@sveltejs/vite-plugin-svelte": "^5.0.3", + "@tsconfig/svelte": "^5.0.4", + "svelte": "^5.28.1", + "svelte-check": "^4.1.6", + "typescript": "~5.8.3", + "vite": "^6.3.5" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz", + "integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz", + "integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz", + "integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz", + "integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz", + "integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz", + "integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz", + "integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz", + "integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz", + "integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz", + "integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz", + "integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz", + "integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz", + "integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz", + "integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz", + "integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz", + "integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz", + "integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz", + "integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz", + "integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz", + "integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz", + "integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz", + "integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz", + "integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz", + "integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz", + "integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.41.1.tgz", + "integrity": "sha512-NELNvyEWZ6R9QMkiytB4/L4zSEaBC03KIXEghptLGLZWJ6VPrL63ooZQCOnlx36aQPGhzuOMwDerC1Eb2VmrLw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.41.1.tgz", + "integrity": "sha512-DXdQe1BJ6TK47ukAoZLehRHhfKnKg9BjnQYUu9gzhI8Mwa1d2fzxA1aw2JixHVl403bwp1+/o/NhhHtxWJBgEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.41.1.tgz", + "integrity": "sha512-5afxvwszzdulsU2w8JKWwY8/sJOLPzf0e1bFuvcW5h9zsEg+RQAojdW0ux2zyYAz7R8HvvzKCjLNJhVq965U7w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.41.1.tgz", + "integrity": "sha512-egpJACny8QOdHNNMZKf8xY0Is6gIMz+tuqXlusxquWu3F833DcMwmGM7WlvCO9sB3OsPjdC4U0wHw5FabzCGZg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.41.1.tgz", + "integrity": "sha512-DBVMZH5vbjgRk3r0OzgjS38z+atlupJ7xfKIDJdZZL6sM6wjfDNo64aowcLPKIx7LMQi8vybB56uh1Ftck/Atg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.41.1.tgz", + "integrity": "sha512-3FkydeohozEskBxNWEIbPfOE0aqQgB6ttTkJ159uWOFn42VLyfAiyD9UK5mhu+ItWzft60DycIN1Xdgiy8o/SA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.41.1.tgz", + "integrity": "sha512-wC53ZNDgt0pqx5xCAgNunkTzFE8GTgdZ9EwYGVcg+jEjJdZGtq9xPjDnFgfFozQI/Xm1mh+D9YlYtl+ueswNEg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.41.1.tgz", + "integrity": "sha512-jwKCca1gbZkZLhLRtsrka5N8sFAaxrGz/7wRJ8Wwvq3jug7toO21vWlViihG85ei7uJTpzbXZRcORotE+xyrLA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.41.1.tgz", + "integrity": "sha512-g0UBcNknsmmNQ8V2d/zD2P7WWfJKU0F1nu0k5pW4rvdb+BIqMm8ToluW/eeRmxCared5dD76lS04uL4UaNgpNA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.41.1.tgz", + "integrity": "sha512-XZpeGB5TKEZWzIrj7sXr+BEaSgo/ma/kCgrZgL0oo5qdB1JlTzIYQKel/RmhT6vMAvOdM2teYlAaOGJpJ9lahg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.41.1.tgz", + "integrity": "sha512-bkCfDJ4qzWfFRCNt5RVV4DOw6KEgFTUZi2r2RuYhGWC8WhCA8lCAJhDeAmrM/fdiAH54m0mA0Vk2FGRPyzI+tw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.41.1.tgz", + "integrity": "sha512-3mr3Xm+gvMX+/8EKogIZSIEF0WUu0HL9di+YWlJpO8CQBnoLAEL/roTCxuLncEdgcfJcvA4UMOf+2dnjl4Ut1A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.41.1.tgz", + "integrity": "sha512-3rwCIh6MQ1LGrvKJitQjZFuQnT2wxfU+ivhNBzmxXTXPllewOF7JR1s2vMX/tWtUYFgphygxjqMl76q4aMotGw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.41.1.tgz", + "integrity": "sha512-LdIUOb3gvfmpkgFZuccNa2uYiqtgZAz3PTzjuM5bH3nvuy9ty6RGc/Q0+HDFrHrizJGVpjnTZ1yS5TNNjFlklw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.41.1.tgz", + "integrity": "sha512-oIE6M8WC9ma6xYqjvPhzZYk6NbobIURvP/lEbh7FWplcMO6gn7MM2yHKA1eC/GvYwzNKK/1LYgqzdkZ8YFxR8g==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.41.1.tgz", + "integrity": "sha512-cWBOvayNvA+SyeQMp79BHPK8ws6sHSsYnK5zDcsC3Hsxr1dgTABKjMnMslPq1DvZIp6uO7kIWhiGwaTdR4Og9A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.41.1.tgz", + "integrity": "sha512-y5CbN44M+pUCdGDlZFzGGBSKCA4A/J2ZH4edTYSSxFg7ce1Xt3GtydbVKWLlzL+INfFIZAEg1ZV6hh9+QQf9YQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.41.1.tgz", + "integrity": "sha512-lZkCxIrjlJlMt1dLO/FbpZbzt6J/A8p4DnqzSa4PWqPEUUUnzXLeki/iyPLfV0BmHItlYgHUqJe+3KiyydmiNQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.41.1.tgz", + "integrity": "sha512-+psFT9+pIh2iuGsxFYYa/LhS5MFKmuivRsx9iPJWNSGbh2XVEjk90fmpUEjCnILPEPJnikAU6SFDiEUyOv90Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.41.1.tgz", + "integrity": "sha512-Wq2zpapRYLfi4aKxf2Xff0tN+7slj2d4R87WEzqw7ZLsVvO5zwYCIuEGSZYiK41+GlwUo1HiR+GdkLEJnCKTCw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sveltejs/acorn-typescript": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.5.tgz", + "integrity": "sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^8.9.0" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-5.0.3.tgz", + "integrity": "sha512-MCFS6CrQDu1yGwspm4qtli0e63vaPCehf6V7pIMP15AsWgMKrqDGCPFF/0kn4SP0ii4aySu4Pa62+fIRGFMjgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sveltejs/vite-plugin-svelte-inspector": "^4.0.1", + "debug": "^4.4.0", + "deepmerge": "^4.3.1", + "kleur": "^4.1.5", + "magic-string": "^0.30.15", + "vitefu": "^1.0.4" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22" + }, + "peerDependencies": { + "svelte": "^5.0.0", + "vite": "^6.0.0" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte-inspector": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-4.0.1.tgz", + "integrity": "sha512-J/Nmb2Q2y7mck2hyCX4ckVHcR5tu2J+MtBEQqpDrrgELZ2uvraQcK/ioCV61AqkdXFgriksOKIceDcQmqnGhVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.7" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22" + }, + "peerDependencies": { + "@sveltejs/vite-plugin-svelte": "^5.0.0", + "svelte": "^5.0.0", + "vite": "^6.0.0" + } + }, + "node_modules/@tsconfig/svelte": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/svelte/-/svelte-5.0.4.tgz", + "integrity": "sha512-BV9NplVgLmSi4mwKzD8BD/NQ8erOY/nUE/GpgWe2ckx+wIQF5RyRirn/QsSSCPeulVpc3RA/iJt6DpfTIZps0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", + "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/acorn": { + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", + "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/esbuild": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz", + "integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.5", + "@esbuild/android-arm": "0.25.5", + "@esbuild/android-arm64": "0.25.5", + "@esbuild/android-x64": "0.25.5", + "@esbuild/darwin-arm64": "0.25.5", + "@esbuild/darwin-x64": "0.25.5", + "@esbuild/freebsd-arm64": "0.25.5", + "@esbuild/freebsd-x64": "0.25.5", + "@esbuild/linux-arm": "0.25.5", + "@esbuild/linux-arm64": "0.25.5", + "@esbuild/linux-ia32": "0.25.5", + "@esbuild/linux-loong64": "0.25.5", + "@esbuild/linux-mips64el": "0.25.5", + "@esbuild/linux-ppc64": "0.25.5", + "@esbuild/linux-riscv64": "0.25.5", + "@esbuild/linux-s390x": "0.25.5", + "@esbuild/linux-x64": "0.25.5", + "@esbuild/netbsd-arm64": "0.25.5", + "@esbuild/netbsd-x64": "0.25.5", + "@esbuild/openbsd-arm64": "0.25.5", + "@esbuild/openbsd-x64": "0.25.5", + "@esbuild/sunos-x64": "0.25.5", + "@esbuild/win32-arm64": "0.25.5", + "@esbuild/win32-ia32": "0.25.5", + "@esbuild/win32-x64": "0.25.5" + } + }, + "node_modules/esm-env": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", + "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esrap": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-1.4.6.tgz", + "integrity": "sha512-F/D2mADJ9SHY3IwksD4DAXjTt7qt7GWUf3/8RhCNWmC/67tyb55dpimHmy7EplakFaflV0R/PC+fdSPqrRHAQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + } + }, + "node_modules/fdir": { + "version": "6.4.5", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.5.tgz", + "integrity": "sha512-4BG7puHpVsIYxZUbiUE3RqGloLaSSwzYie5jvasC4LWuBWzZawynvYouhjbQKw2JuIGYdm0DzIxl8iVidKlUEw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/is-reference": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", + "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.6" + } + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/locate-character": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", + "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.4", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.4.tgz", + "integrity": "sha512-QSa9EBe+uwlGTFmHsPKokv3B/oEMQZxfqW0QqNCyhpa6mB1afzulwn8hihglqAb2pOw+BJgNlmXQ8la2VeHB7w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/rollup": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.41.1.tgz", + "integrity": "sha512-cPmwD3FnFv8rKMBc1MxWCwVQFxwf1JEmSX3iQXrRVVG15zerAIXRjMFVWnd5Q5QvgKF7Aj+5ykXFhUl+QGnyOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.7" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.41.1", + "@rollup/rollup-android-arm64": "4.41.1", + "@rollup/rollup-darwin-arm64": "4.41.1", + "@rollup/rollup-darwin-x64": "4.41.1", + "@rollup/rollup-freebsd-arm64": "4.41.1", + "@rollup/rollup-freebsd-x64": "4.41.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.41.1", + "@rollup/rollup-linux-arm-musleabihf": "4.41.1", + "@rollup/rollup-linux-arm64-gnu": "4.41.1", + "@rollup/rollup-linux-arm64-musl": "4.41.1", + "@rollup/rollup-linux-loongarch64-gnu": "4.41.1", + "@rollup/rollup-linux-powerpc64le-gnu": "4.41.1", + "@rollup/rollup-linux-riscv64-gnu": "4.41.1", + "@rollup/rollup-linux-riscv64-musl": "4.41.1", + "@rollup/rollup-linux-s390x-gnu": "4.41.1", + "@rollup/rollup-linux-x64-gnu": "4.41.1", + "@rollup/rollup-linux-x64-musl": "4.41.1", + "@rollup/rollup-win32-arm64-msvc": "4.41.1", + "@rollup/rollup-win32-ia32-msvc": "4.41.1", + "@rollup/rollup-win32-x64-msvc": "4.41.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/sade": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mri": "^1.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/svelte": { + "version": "5.33.11", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.33.11.tgz", + "integrity": "sha512-BVnvd6T3OShNvsRwYPXdseSO5rnQ4SljmhLVCCpBX1nEQI+e2TopOlazo4z+1+aUukyHZxlIVg3hpZ5TMugrMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@jridgewell/sourcemap-codec": "^1.5.0", + "@sveltejs/acorn-typescript": "^1.0.5", + "@types/estree": "^1.0.5", + "acorn": "^8.12.1", + "aria-query": "^5.3.1", + "axobject-query": "^4.1.0", + "clsx": "^2.1.1", + "esm-env": "^1.2.1", + "esrap": "^1.4.6", + "is-reference": "^3.0.3", + "locate-character": "^3.0.0", + "magic-string": "^0.30.11", + "zimmerframe": "^1.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/svelte-check": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.2.1.tgz", + "integrity": "sha512-e49SU1RStvQhoipkQ/aonDhHnG3qxHSBtNfBRb9pxVXoa+N7qybAo32KgA9wEb2PCYFNaDg7bZCdhLD1vHpdYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "chokidar": "^4.0.1", + "fdir": "^6.2.0", + "picocolors": "^1.0.0", + "sade": "^1.7.4" + }, + "bin": { + "svelte-check": "bin/svelte-check" + }, + "engines": { + "node": ">= 18.0.0" + }, + "peerDependencies": { + "svelte": "^4.0.0 || ^5.0.0-next.0", + "typescript": ">=5.0.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", + "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.4.4", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/vite": { + "version": "6.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", + "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitefu": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.0.6.tgz", + "integrity": "sha512-+Rex1GlappUyNN6UfwbVZne/9cYC4+R2XDk9xkNXBKMw6HQagdX9PgZ8V2v1WUSK1wfBLp7qbI1+XSNIlB1xmA==", + "dev": true, + "license": "MIT", + "workspaces": [ + "tests/deps/*", + "tests/projects/*" + ], + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/zimmerframe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.2.tgz", + "integrity": "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/web/frontend/package.json b/web/frontend/package.json new file mode 100644 index 0000000..76ee115 --- /dev/null +++ b/web/frontend/package.json @@ -0,0 +1,20 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "check": "svelte-check --tsconfig ./tsconfig.app.json && tsc -p tsconfig.node.json" + }, + "devDependencies": { + "@sveltejs/vite-plugin-svelte": "^5.0.3", + "@tsconfig/svelte": "^5.0.4", + "svelte": "^5.28.1", + "svelte-check": "^4.1.6", + "typescript": "~5.8.3", + "vite": "^6.3.5" + } +} diff --git a/web/static/css/app.css b/web/frontend/public/css/app.css similarity index 94% rename from web/static/css/app.css rename to web/frontend/public/css/app.css index e85a61c..9e084ae 100644 --- a/web/static/css/app.css +++ b/web/frontend/public/css/app.css @@ -115,6 +115,25 @@ p { transition: background 0.3s ease-in-out; } +.container { + max-width: 100vw; +} + +.site { + display: flex; + flex-direction: column; + height: 100%; +} + +.site-content { + flex: 1 0 auto; + margin-top: 80px; +} + +.site-footer { + flex: none; +} + .content-section { padding-top: 200px; } diff --git a/web/static/css/bootstrap-grid.css b/web/frontend/public/css/bootstrap-grid.css similarity index 100% rename from web/static/css/bootstrap-grid.css rename to web/frontend/public/css/bootstrap-grid.css diff --git a/web/static/css/bootstrap-grid.css.map b/web/frontend/public/css/bootstrap-grid.css.map similarity index 100% rename from web/static/css/bootstrap-grid.css.map rename to web/frontend/public/css/bootstrap-grid.css.map diff --git a/web/static/css/bootstrap-grid.min.css b/web/frontend/public/css/bootstrap-grid.min.css similarity index 100% rename from web/static/css/bootstrap-grid.min.css rename to web/frontend/public/css/bootstrap-grid.min.css diff --git a/web/static/css/bootstrap-grid.min.css.map b/web/frontend/public/css/bootstrap-grid.min.css.map similarity index 100% rename from web/static/css/bootstrap-grid.min.css.map rename to web/frontend/public/css/bootstrap-grid.min.css.map diff --git a/web/static/css/bootstrap-grid.rtl.css b/web/frontend/public/css/bootstrap-grid.rtl.css similarity index 100% rename from web/static/css/bootstrap-grid.rtl.css rename to web/frontend/public/css/bootstrap-grid.rtl.css diff --git a/web/static/css/bootstrap-grid.rtl.css.map b/web/frontend/public/css/bootstrap-grid.rtl.css.map similarity index 100% rename from web/static/css/bootstrap-grid.rtl.css.map rename to web/frontend/public/css/bootstrap-grid.rtl.css.map diff --git a/web/static/css/bootstrap-grid.rtl.min.css b/web/frontend/public/css/bootstrap-grid.rtl.min.css similarity index 100% rename from web/static/css/bootstrap-grid.rtl.min.css rename to web/frontend/public/css/bootstrap-grid.rtl.min.css diff --git a/web/static/css/bootstrap-grid.rtl.min.css.map b/web/frontend/public/css/bootstrap-grid.rtl.min.css.map similarity index 100% rename from web/static/css/bootstrap-grid.rtl.min.css.map rename to web/frontend/public/css/bootstrap-grid.rtl.min.css.map diff --git a/web/static/css/bootstrap-icons.css b/web/frontend/public/css/bootstrap-icons.css similarity index 100% rename from web/static/css/bootstrap-icons.css rename to web/frontend/public/css/bootstrap-icons.css diff --git a/web/static/css/bootstrap-icons.json b/web/frontend/public/css/bootstrap-icons.json similarity index 100% rename from web/static/css/bootstrap-icons.json rename to web/frontend/public/css/bootstrap-icons.json diff --git a/web/static/css/bootstrap-icons.min.css b/web/frontend/public/css/bootstrap-icons.min.css similarity index 100% rename from web/static/css/bootstrap-icons.min.css rename to web/frontend/public/css/bootstrap-icons.min.css diff --git a/web/static/css/bootstrap-reboot.css b/web/frontend/public/css/bootstrap-reboot.css similarity index 100% rename from web/static/css/bootstrap-reboot.css rename to web/frontend/public/css/bootstrap-reboot.css diff --git a/web/static/css/bootstrap-reboot.css.map b/web/frontend/public/css/bootstrap-reboot.css.map similarity index 100% rename from web/static/css/bootstrap-reboot.css.map rename to web/frontend/public/css/bootstrap-reboot.css.map diff --git a/web/static/css/bootstrap-reboot.min.css b/web/frontend/public/css/bootstrap-reboot.min.css similarity index 100% rename from web/static/css/bootstrap-reboot.min.css rename to web/frontend/public/css/bootstrap-reboot.min.css diff --git a/web/static/css/bootstrap-reboot.min.css.map b/web/frontend/public/css/bootstrap-reboot.min.css.map similarity index 100% rename from web/static/css/bootstrap-reboot.min.css.map rename to web/frontend/public/css/bootstrap-reboot.min.css.map diff --git a/web/static/css/bootstrap-reboot.rtl.css b/web/frontend/public/css/bootstrap-reboot.rtl.css similarity index 100% rename from web/static/css/bootstrap-reboot.rtl.css rename to web/frontend/public/css/bootstrap-reboot.rtl.css diff --git a/web/static/css/bootstrap-reboot.rtl.css.map b/web/frontend/public/css/bootstrap-reboot.rtl.css.map similarity index 100% rename from web/static/css/bootstrap-reboot.rtl.css.map rename to web/frontend/public/css/bootstrap-reboot.rtl.css.map diff --git a/web/static/css/bootstrap-reboot.rtl.min.css b/web/frontend/public/css/bootstrap-reboot.rtl.min.css similarity index 100% rename from web/static/css/bootstrap-reboot.rtl.min.css rename to web/frontend/public/css/bootstrap-reboot.rtl.min.css diff --git a/web/static/css/bootstrap-reboot.rtl.min.css.map b/web/frontend/public/css/bootstrap-reboot.rtl.min.css.map similarity index 100% rename from web/static/css/bootstrap-reboot.rtl.min.css.map rename to web/frontend/public/css/bootstrap-reboot.rtl.min.css.map diff --git a/web/static/css/bootstrap-utilities.css b/web/frontend/public/css/bootstrap-utilities.css similarity index 100% rename from web/static/css/bootstrap-utilities.css rename to web/frontend/public/css/bootstrap-utilities.css diff --git a/web/static/css/bootstrap-utilities.css.map b/web/frontend/public/css/bootstrap-utilities.css.map similarity index 100% rename from web/static/css/bootstrap-utilities.css.map rename to web/frontend/public/css/bootstrap-utilities.css.map diff --git a/web/static/css/bootstrap-utilities.min.css b/web/frontend/public/css/bootstrap-utilities.min.css similarity index 100% rename from web/static/css/bootstrap-utilities.min.css rename to web/frontend/public/css/bootstrap-utilities.min.css diff --git a/web/static/css/bootstrap-utilities.min.css.map b/web/frontend/public/css/bootstrap-utilities.min.css.map similarity index 100% rename from web/static/css/bootstrap-utilities.min.css.map rename to web/frontend/public/css/bootstrap-utilities.min.css.map diff --git a/web/static/css/bootstrap-utilities.rtl.css b/web/frontend/public/css/bootstrap-utilities.rtl.css similarity index 100% rename from web/static/css/bootstrap-utilities.rtl.css rename to web/frontend/public/css/bootstrap-utilities.rtl.css diff --git a/web/static/css/bootstrap-utilities.rtl.css.map b/web/frontend/public/css/bootstrap-utilities.rtl.css.map similarity index 100% rename from web/static/css/bootstrap-utilities.rtl.css.map rename to web/frontend/public/css/bootstrap-utilities.rtl.css.map diff --git a/web/static/css/bootstrap-utilities.rtl.min.css b/web/frontend/public/css/bootstrap-utilities.rtl.min.css similarity index 100% rename from web/static/css/bootstrap-utilities.rtl.min.css rename to web/frontend/public/css/bootstrap-utilities.rtl.min.css diff --git a/web/static/css/bootstrap-utilities.rtl.min.css.map b/web/frontend/public/css/bootstrap-utilities.rtl.min.css.map similarity index 100% rename from web/static/css/bootstrap-utilities.rtl.min.css.map rename to web/frontend/public/css/bootstrap-utilities.rtl.min.css.map diff --git a/web/static/css/bootstrap.css b/web/frontend/public/css/bootstrap.css similarity index 100% rename from web/static/css/bootstrap.css rename to web/frontend/public/css/bootstrap.css diff --git a/web/static/css/bootstrap.css.map b/web/frontend/public/css/bootstrap.css.map similarity index 100% rename from web/static/css/bootstrap.css.map rename to web/frontend/public/css/bootstrap.css.map diff --git a/web/static/css/bootstrap.min.css b/web/frontend/public/css/bootstrap.min.css similarity index 100% rename from web/static/css/bootstrap.min.css rename to web/frontend/public/css/bootstrap.min.css diff --git a/web/static/css/bootstrap.min.css.map b/web/frontend/public/css/bootstrap.min.css.map similarity index 100% rename from web/static/css/bootstrap.min.css.map rename to web/frontend/public/css/bootstrap.min.css.map diff --git a/web/static/css/bootstrap.rtl.css b/web/frontend/public/css/bootstrap.rtl.css similarity index 100% rename from web/static/css/bootstrap.rtl.css rename to web/frontend/public/css/bootstrap.rtl.css diff --git a/web/static/css/bootstrap.rtl.css.map b/web/frontend/public/css/bootstrap.rtl.css.map similarity index 100% rename from web/static/css/bootstrap.rtl.css.map rename to web/frontend/public/css/bootstrap.rtl.css.map diff --git a/web/static/css/bootstrap.rtl.min.css b/web/frontend/public/css/bootstrap.rtl.min.css similarity index 100% rename from web/static/css/bootstrap.rtl.min.css rename to web/frontend/public/css/bootstrap.rtl.min.css diff --git a/web/static/css/bootstrap.rtl.min.css.map b/web/frontend/public/css/bootstrap.rtl.min.css.map similarity index 100% rename from web/static/css/bootstrap.rtl.min.css.map rename to web/frontend/public/css/bootstrap.rtl.min.css.map diff --git a/web/static/css/fonts/bootstrap-icons.woff b/web/frontend/public/css/fonts/bootstrap-icons.woff similarity index 100% rename from web/static/css/fonts/bootstrap-icons.woff rename to web/frontend/public/css/fonts/bootstrap-icons.woff diff --git a/web/static/css/fonts/bootstrap-icons.woff2 b/web/frontend/public/css/fonts/bootstrap-icons.woff2 similarity index 100% rename from web/static/css/fonts/bootstrap-icons.woff2 rename to web/frontend/public/css/fonts/bootstrap-icons.woff2 diff --git a/web/static/favicon.ico b/web/frontend/public/favicon.ico similarity index 100% rename from web/static/favicon.ico rename to web/frontend/public/favicon.ico diff --git a/web/static/img/Bromeliad.jpg b/web/frontend/public/img/Bromeliad.jpg similarity index 100% rename from web/static/img/Bromeliad.jpg rename to web/frontend/public/img/Bromeliad.jpg diff --git a/web/static/img/RookieStripes.jpg b/web/frontend/public/img/RookieStripes.jpg similarity index 100% rename from web/static/img/RookieStripes.jpg rename to web/frontend/public/img/RookieStripes.jpg diff --git a/web/static/img/TreeOfLight.jpg b/web/frontend/public/img/TreeOfLight.jpg similarity index 100% rename from web/static/img/TreeOfLight.jpg rename to web/frontend/public/img/TreeOfLight.jpg diff --git a/web/static/img/action.jpg b/web/frontend/public/img/action.jpg similarity index 100% rename from web/static/img/action.jpg rename to web/frontend/public/img/action.jpg diff --git a/web/static/img/anniversary/image-1.jpg b/web/frontend/public/img/anniversary/image-1.jpg similarity index 100% rename from web/static/img/anniversary/image-1.jpg rename to web/frontend/public/img/anniversary/image-1.jpg diff --git a/web/static/img/anniversary/image-10.jpg b/web/frontend/public/img/anniversary/image-10.jpg similarity index 100% rename from web/static/img/anniversary/image-10.jpg rename to web/frontend/public/img/anniversary/image-10.jpg diff --git a/web/static/img/anniversary/image-11.jpg b/web/frontend/public/img/anniversary/image-11.jpg similarity index 100% rename from web/static/img/anniversary/image-11.jpg rename to web/frontend/public/img/anniversary/image-11.jpg diff --git a/web/static/img/anniversary/image-12.png b/web/frontend/public/img/anniversary/image-12.png similarity index 100% rename from web/static/img/anniversary/image-12.png rename to web/frontend/public/img/anniversary/image-12.png diff --git a/web/static/img/anniversary/image-13.jpg b/web/frontend/public/img/anniversary/image-13.jpg similarity index 100% rename from web/static/img/anniversary/image-13.jpg rename to web/frontend/public/img/anniversary/image-13.jpg diff --git a/web/static/img/anniversary/image-14.jpg b/web/frontend/public/img/anniversary/image-14.jpg similarity index 100% rename from web/static/img/anniversary/image-14.jpg rename to web/frontend/public/img/anniversary/image-14.jpg diff --git a/web/static/img/anniversary/image-15.png b/web/frontend/public/img/anniversary/image-15.png similarity index 100% rename from web/static/img/anniversary/image-15.png rename to web/frontend/public/img/anniversary/image-15.png diff --git a/web/static/img/anniversary/image-16.jpg b/web/frontend/public/img/anniversary/image-16.jpg similarity index 100% rename from web/static/img/anniversary/image-16.jpg rename to web/frontend/public/img/anniversary/image-16.jpg diff --git a/web/static/img/anniversary/image-17.png b/web/frontend/public/img/anniversary/image-17.png similarity index 100% rename from web/static/img/anniversary/image-17.png rename to web/frontend/public/img/anniversary/image-17.png diff --git a/web/static/img/anniversary/image-18.jpg b/web/frontend/public/img/anniversary/image-18.jpg similarity index 100% rename from web/static/img/anniversary/image-18.jpg rename to web/frontend/public/img/anniversary/image-18.jpg diff --git a/web/static/img/anniversary/image-19.jpg b/web/frontend/public/img/anniversary/image-19.jpg similarity index 100% rename from web/static/img/anniversary/image-19.jpg rename to web/frontend/public/img/anniversary/image-19.jpg diff --git a/web/static/img/anniversary/image-2.jpg b/web/frontend/public/img/anniversary/image-2.jpg similarity index 100% rename from web/static/img/anniversary/image-2.jpg rename to web/frontend/public/img/anniversary/image-2.jpg diff --git a/web/static/img/anniversary/image-20.jpg b/web/frontend/public/img/anniversary/image-20.jpg similarity index 100% rename from web/static/img/anniversary/image-20.jpg rename to web/frontend/public/img/anniversary/image-20.jpg diff --git a/web/static/img/anniversary/image-21.jpg b/web/frontend/public/img/anniversary/image-21.jpg similarity index 100% rename from web/static/img/anniversary/image-21.jpg rename to web/frontend/public/img/anniversary/image-21.jpg diff --git a/web/static/img/anniversary/image-22.jpg b/web/frontend/public/img/anniversary/image-22.jpg similarity index 100% rename from web/static/img/anniversary/image-22.jpg rename to web/frontend/public/img/anniversary/image-22.jpg diff --git a/web/static/img/anniversary/image-23.jpg b/web/frontend/public/img/anniversary/image-23.jpg similarity index 100% rename from web/static/img/anniversary/image-23.jpg rename to web/frontend/public/img/anniversary/image-23.jpg diff --git a/web/static/img/anniversary/image-24.jpg b/web/frontend/public/img/anniversary/image-24.jpg similarity index 100% rename from web/static/img/anniversary/image-24.jpg rename to web/frontend/public/img/anniversary/image-24.jpg diff --git a/web/static/img/anniversary/image-25.jpg b/web/frontend/public/img/anniversary/image-25.jpg similarity index 100% rename from web/static/img/anniversary/image-25.jpg rename to web/frontend/public/img/anniversary/image-25.jpg diff --git a/web/static/img/anniversary/image-26.jpg b/web/frontend/public/img/anniversary/image-26.jpg similarity index 100% rename from web/static/img/anniversary/image-26.jpg rename to web/frontend/public/img/anniversary/image-26.jpg diff --git a/web/static/img/anniversary/image-27.jpg b/web/frontend/public/img/anniversary/image-27.jpg similarity index 100% rename from web/static/img/anniversary/image-27.jpg rename to web/frontend/public/img/anniversary/image-27.jpg diff --git a/web/static/img/anniversary/image-28.jpg b/web/frontend/public/img/anniversary/image-28.jpg similarity index 100% rename from web/static/img/anniversary/image-28.jpg rename to web/frontend/public/img/anniversary/image-28.jpg diff --git a/web/static/img/anniversary/image-29.jpg b/web/frontend/public/img/anniversary/image-29.jpg similarity index 100% rename from web/static/img/anniversary/image-29.jpg rename to web/frontend/public/img/anniversary/image-29.jpg diff --git a/web/static/img/anniversary/image-3.jpg b/web/frontend/public/img/anniversary/image-3.jpg similarity index 100% rename from web/static/img/anniversary/image-3.jpg rename to web/frontend/public/img/anniversary/image-3.jpg diff --git a/web/static/img/anniversary/image-30.jpg b/web/frontend/public/img/anniversary/image-30.jpg similarity index 100% rename from web/static/img/anniversary/image-30.jpg rename to web/frontend/public/img/anniversary/image-30.jpg diff --git a/web/static/img/anniversary/image-31.jpg b/web/frontend/public/img/anniversary/image-31.jpg similarity index 100% rename from web/static/img/anniversary/image-31.jpg rename to web/frontend/public/img/anniversary/image-31.jpg diff --git a/web/static/img/anniversary/image-32.jpg b/web/frontend/public/img/anniversary/image-32.jpg similarity index 100% rename from web/static/img/anniversary/image-32.jpg rename to web/frontend/public/img/anniversary/image-32.jpg diff --git a/web/static/img/anniversary/image-33.jpg b/web/frontend/public/img/anniversary/image-33.jpg similarity index 100% rename from web/static/img/anniversary/image-33.jpg rename to web/frontend/public/img/anniversary/image-33.jpg diff --git a/web/static/img/anniversary/image-34.jpg b/web/frontend/public/img/anniversary/image-34.jpg similarity index 100% rename from web/static/img/anniversary/image-34.jpg rename to web/frontend/public/img/anniversary/image-34.jpg diff --git a/web/static/img/anniversary/image-35.jpg b/web/frontend/public/img/anniversary/image-35.jpg similarity index 100% rename from web/static/img/anniversary/image-35.jpg rename to web/frontend/public/img/anniversary/image-35.jpg diff --git a/web/static/img/anniversary/image-36.jpg b/web/frontend/public/img/anniversary/image-36.jpg similarity index 100% rename from web/static/img/anniversary/image-36.jpg rename to web/frontend/public/img/anniversary/image-36.jpg diff --git a/web/static/img/anniversary/image-37.jpg b/web/frontend/public/img/anniversary/image-37.jpg similarity index 100% rename from web/static/img/anniversary/image-37.jpg rename to web/frontend/public/img/anniversary/image-37.jpg diff --git a/web/static/img/anniversary/image-38.jpg b/web/frontend/public/img/anniversary/image-38.jpg similarity index 100% rename from web/static/img/anniversary/image-38.jpg rename to web/frontend/public/img/anniversary/image-38.jpg diff --git a/web/static/img/anniversary/image-39.jpg b/web/frontend/public/img/anniversary/image-39.jpg similarity index 100% rename from web/static/img/anniversary/image-39.jpg rename to web/frontend/public/img/anniversary/image-39.jpg diff --git a/web/static/img/anniversary/image-4.jpg b/web/frontend/public/img/anniversary/image-4.jpg similarity index 100% rename from web/static/img/anniversary/image-4.jpg rename to web/frontend/public/img/anniversary/image-4.jpg diff --git a/web/static/img/anniversary/image-40.jpg b/web/frontend/public/img/anniversary/image-40.jpg similarity index 100% rename from web/static/img/anniversary/image-40.jpg rename to web/frontend/public/img/anniversary/image-40.jpg diff --git a/web/static/img/anniversary/image-41.jpg b/web/frontend/public/img/anniversary/image-41.jpg similarity index 100% rename from web/static/img/anniversary/image-41.jpg rename to web/frontend/public/img/anniversary/image-41.jpg diff --git a/web/static/img/anniversary/image-42.jpg b/web/frontend/public/img/anniversary/image-42.jpg similarity index 100% rename from web/static/img/anniversary/image-42.jpg rename to web/frontend/public/img/anniversary/image-42.jpg diff --git a/web/static/img/anniversary/image-43.jpg b/web/frontend/public/img/anniversary/image-43.jpg similarity index 100% rename from web/static/img/anniversary/image-43.jpg rename to web/frontend/public/img/anniversary/image-43.jpg diff --git a/web/static/img/anniversary/image-44.jpg b/web/frontend/public/img/anniversary/image-44.jpg similarity index 100% rename from web/static/img/anniversary/image-44.jpg rename to web/frontend/public/img/anniversary/image-44.jpg diff --git a/web/static/img/anniversary/image-45.jpg b/web/frontend/public/img/anniversary/image-45.jpg similarity index 100% rename from web/static/img/anniversary/image-45.jpg rename to web/frontend/public/img/anniversary/image-45.jpg diff --git a/web/static/img/anniversary/image-46.jpg b/web/frontend/public/img/anniversary/image-46.jpg similarity index 100% rename from web/static/img/anniversary/image-46.jpg rename to web/frontend/public/img/anniversary/image-46.jpg diff --git a/web/static/img/anniversary/image-47.jpg b/web/frontend/public/img/anniversary/image-47.jpg similarity index 100% rename from web/static/img/anniversary/image-47.jpg rename to web/frontend/public/img/anniversary/image-47.jpg diff --git a/web/static/img/anniversary/image-48.jpg b/web/frontend/public/img/anniversary/image-48.jpg similarity index 100% rename from web/static/img/anniversary/image-48.jpg rename to web/frontend/public/img/anniversary/image-48.jpg diff --git a/web/static/img/anniversary/image-49.jpg b/web/frontend/public/img/anniversary/image-49.jpg similarity index 100% rename from web/static/img/anniversary/image-49.jpg rename to web/frontend/public/img/anniversary/image-49.jpg diff --git a/web/static/img/anniversary/image-5.jpg b/web/frontend/public/img/anniversary/image-5.jpg similarity index 100% rename from web/static/img/anniversary/image-5.jpg rename to web/frontend/public/img/anniversary/image-5.jpg diff --git a/web/static/img/anniversary/image-50.jpg b/web/frontend/public/img/anniversary/image-50.jpg similarity index 100% rename from web/static/img/anniversary/image-50.jpg rename to web/frontend/public/img/anniversary/image-50.jpg diff --git a/web/static/img/anniversary/image-6.jpg b/web/frontend/public/img/anniversary/image-6.jpg similarity index 100% rename from web/static/img/anniversary/image-6.jpg rename to web/frontend/public/img/anniversary/image-6.jpg diff --git a/web/static/img/anniversary/image-7.jpg b/web/frontend/public/img/anniversary/image-7.jpg similarity index 100% rename from web/static/img/anniversary/image-7.jpg rename to web/frontend/public/img/anniversary/image-7.jpg diff --git a/web/static/img/anniversary/image-8.jpg b/web/frontend/public/img/anniversary/image-8.jpg similarity index 100% rename from web/static/img/anniversary/image-8.jpg rename to web/frontend/public/img/anniversary/image-8.jpg diff --git a/web/static/img/anniversary/image-9.jpg b/web/frontend/public/img/anniversary/image-9.jpg similarity index 100% rename from web/static/img/anniversary/image-9.jpg rename to web/frontend/public/img/anniversary/image-9.jpg diff --git a/web/static/img/cordula.jpg b/web/frontend/public/img/cordula.jpg similarity index 100% rename from web/static/img/cordula.jpg rename to web/frontend/public/img/cordula.jpg diff --git a/web/static/img/downloads-bg.jpg b/web/frontend/public/img/downloads-bg.jpg similarity index 100% rename from web/static/img/downloads-bg.jpg rename to web/frontend/public/img/downloads-bg.jpg diff --git a/web/static/img/farben.jpg b/web/frontend/public/img/farben.jpg similarity index 100% rename from web/static/img/farben.jpg rename to web/frontend/public/img/farben.jpg diff --git a/web/static/img/flags/Argentina.png b/web/frontend/public/img/flags/Argentina.png similarity index 100% rename from web/static/img/flags/Argentina.png rename to web/frontend/public/img/flags/Argentina.png diff --git a/web/static/img/flags/Australia.png b/web/frontend/public/img/flags/Australia.png similarity index 100% rename from web/static/img/flags/Australia.png rename to web/frontend/public/img/flags/Australia.png diff --git a/web/static/img/flags/Austria.png b/web/frontend/public/img/flags/Austria.png similarity index 100% rename from web/static/img/flags/Austria.png rename to web/frontend/public/img/flags/Austria.png diff --git a/web/static/img/flags/Belgium.png b/web/frontend/public/img/flags/Belgium.png similarity index 100% rename from web/static/img/flags/Belgium.png rename to web/frontend/public/img/flags/Belgium.png diff --git a/web/static/img/flags/Brazil.png b/web/frontend/public/img/flags/Brazil.png similarity index 100% rename from web/static/img/flags/Brazil.png rename to web/frontend/public/img/flags/Brazil.png diff --git a/web/static/img/flags/Bulgaria.png b/web/frontend/public/img/flags/Bulgaria.png similarity index 100% rename from web/static/img/flags/Bulgaria.png rename to web/frontend/public/img/flags/Bulgaria.png diff --git a/web/static/img/flags/Canada.png b/web/frontend/public/img/flags/Canada.png similarity index 100% rename from web/static/img/flags/Canada.png rename to web/frontend/public/img/flags/Canada.png diff --git a/web/static/img/flags/Chile.png b/web/frontend/public/img/flags/Chile.png similarity index 100% rename from web/static/img/flags/Chile.png rename to web/frontend/public/img/flags/Chile.png diff --git a/web/static/img/flags/China.png b/web/frontend/public/img/flags/China.png similarity index 100% rename from web/static/img/flags/China.png rename to web/frontend/public/img/flags/China.png diff --git a/web/static/img/flags/Croatia.png b/web/frontend/public/img/flags/Croatia.png similarity index 100% rename from web/static/img/flags/Croatia.png rename to web/frontend/public/img/flags/Croatia.png diff --git a/web/static/img/flags/Czech-Republic.png b/web/frontend/public/img/flags/Czech-Republic.png similarity index 100% rename from web/static/img/flags/Czech-Republic.png rename to web/frontend/public/img/flags/Czech-Republic.png diff --git a/web/static/img/flags/Denmark.png b/web/frontend/public/img/flags/Denmark.png similarity index 100% rename from web/static/img/flags/Denmark.png rename to web/frontend/public/img/flags/Denmark.png diff --git a/web/static/img/flags/England.png b/web/frontend/public/img/flags/England.png similarity index 100% rename from web/static/img/flags/England.png rename to web/frontend/public/img/flags/England.png diff --git a/web/static/img/flags/Finland.png b/web/frontend/public/img/flags/Finland.png similarity index 100% rename from web/static/img/flags/Finland.png rename to web/frontend/public/img/flags/Finland.png diff --git a/web/static/img/flags/France.png b/web/frontend/public/img/flags/France.png similarity index 100% rename from web/static/img/flags/France.png rename to web/frontend/public/img/flags/France.png diff --git a/web/static/img/flags/Germany.png b/web/frontend/public/img/flags/Germany.png similarity index 100% rename from web/static/img/flags/Germany.png rename to web/frontend/public/img/flags/Germany.png diff --git a/web/static/img/flags/Greece.png b/web/frontend/public/img/flags/Greece.png similarity index 100% rename from web/static/img/flags/Greece.png rename to web/frontend/public/img/flags/Greece.png diff --git a/web/static/img/flags/Hong-Kong.png b/web/frontend/public/img/flags/Hong-Kong.png similarity index 100% rename from web/static/img/flags/Hong-Kong.png rename to web/frontend/public/img/flags/Hong-Kong.png diff --git a/web/static/img/flags/Hungary.png b/web/frontend/public/img/flags/Hungary.png similarity index 100% rename from web/static/img/flags/Hungary.png rename to web/frontend/public/img/flags/Hungary.png diff --git a/web/static/img/flags/Iceland.png b/web/frontend/public/img/flags/Iceland.png similarity index 100% rename from web/static/img/flags/Iceland.png rename to web/frontend/public/img/flags/Iceland.png diff --git a/web/static/img/flags/India.png b/web/frontend/public/img/flags/India.png similarity index 100% rename from web/static/img/flags/India.png rename to web/frontend/public/img/flags/India.png diff --git a/web/static/img/flags/Indonesia.png b/web/frontend/public/img/flags/Indonesia.png similarity index 100% rename from web/static/img/flags/Indonesia.png rename to web/frontend/public/img/flags/Indonesia.png diff --git a/web/static/img/flags/Iran.png b/web/frontend/public/img/flags/Iran.png similarity index 100% rename from web/static/img/flags/Iran.png rename to web/frontend/public/img/flags/Iran.png diff --git a/web/static/img/flags/Iraq.png b/web/frontend/public/img/flags/Iraq.png similarity index 100% rename from web/static/img/flags/Iraq.png rename to web/frontend/public/img/flags/Iraq.png diff --git a/web/static/img/flags/Ireland.png b/web/frontend/public/img/flags/Ireland.png similarity index 100% rename from web/static/img/flags/Ireland.png rename to web/frontend/public/img/flags/Ireland.png diff --git a/web/static/img/flags/Israel.png b/web/frontend/public/img/flags/Israel.png similarity index 100% rename from web/static/img/flags/Israel.png rename to web/frontend/public/img/flags/Israel.png diff --git a/web/static/img/flags/Italy.png b/web/frontend/public/img/flags/Italy.png similarity index 100% rename from web/static/img/flags/Italy.png rename to web/frontend/public/img/flags/Italy.png diff --git a/web/static/img/flags/Jamaica.png b/web/frontend/public/img/flags/Jamaica.png similarity index 100% rename from web/static/img/flags/Jamaica.png rename to web/frontend/public/img/flags/Jamaica.png diff --git a/web/static/img/flags/Japan.png b/web/frontend/public/img/flags/Japan.png similarity index 100% rename from web/static/img/flags/Japan.png rename to web/frontend/public/img/flags/Japan.png diff --git a/web/static/img/flags/Kenya.png b/web/frontend/public/img/flags/Kenya.png similarity index 100% rename from web/static/img/flags/Kenya.png rename to web/frontend/public/img/flags/Kenya.png diff --git a/web/static/img/flags/Kuwait.png b/web/frontend/public/img/flags/Kuwait.png similarity index 100% rename from web/static/img/flags/Kuwait.png rename to web/frontend/public/img/flags/Kuwait.png diff --git a/web/static/img/flags/Luxembourg.png b/web/frontend/public/img/flags/Luxembourg.png similarity index 100% rename from web/static/img/flags/Luxembourg.png rename to web/frontend/public/img/flags/Luxembourg.png diff --git a/web/static/img/flags/Malaysia.png b/web/frontend/public/img/flags/Malaysia.png similarity index 100% rename from web/static/img/flags/Malaysia.png rename to web/frontend/public/img/flags/Malaysia.png diff --git a/web/static/img/flags/Namibia.png b/web/frontend/public/img/flags/Namibia.png similarity index 100% rename from web/static/img/flags/Namibia.png rename to web/frontend/public/img/flags/Namibia.png diff --git a/web/static/img/flags/Netherlands.png b/web/frontend/public/img/flags/Netherlands.png similarity index 100% rename from web/static/img/flags/Netherlands.png rename to web/frontend/public/img/flags/Netherlands.png diff --git a/web/static/img/flags/Norway.png b/web/frontend/public/img/flags/Norway.png similarity index 100% rename from web/static/img/flags/Norway.png rename to web/frontend/public/img/flags/Norway.png diff --git a/web/static/img/flags/Pakistan.png b/web/frontend/public/img/flags/Pakistan.png similarity index 100% rename from web/static/img/flags/Pakistan.png rename to web/frontend/public/img/flags/Pakistan.png diff --git a/web/static/img/flags/Poland.png b/web/frontend/public/img/flags/Poland.png similarity index 100% rename from web/static/img/flags/Poland.png rename to web/frontend/public/img/flags/Poland.png diff --git a/web/static/img/flags/Portugal.png b/web/frontend/public/img/flags/Portugal.png similarity index 100% rename from web/static/img/flags/Portugal.png rename to web/frontend/public/img/flags/Portugal.png diff --git a/web/static/img/flags/Romania.png b/web/frontend/public/img/flags/Romania.png similarity index 100% rename from web/static/img/flags/Romania.png rename to web/frontend/public/img/flags/Romania.png diff --git a/web/static/img/flags/Russia.png b/web/frontend/public/img/flags/Russia.png similarity index 100% rename from web/static/img/flags/Russia.png rename to web/frontend/public/img/flags/Russia.png diff --git a/web/static/img/flags/Scotland.png b/web/frontend/public/img/flags/Scotland.png similarity index 100% rename from web/static/img/flags/Scotland.png rename to web/frontend/public/img/flags/Scotland.png diff --git a/web/static/img/flags/Senegal.png b/web/frontend/public/img/flags/Senegal.png similarity index 100% rename from web/static/img/flags/Senegal.png rename to web/frontend/public/img/flags/Senegal.png diff --git a/web/static/img/flags/Serbia.png b/web/frontend/public/img/flags/Serbia.png similarity index 100% rename from web/static/img/flags/Serbia.png rename to web/frontend/public/img/flags/Serbia.png diff --git a/web/static/img/flags/Singapore.png b/web/frontend/public/img/flags/Singapore.png similarity index 100% rename from web/static/img/flags/Singapore.png rename to web/frontend/public/img/flags/Singapore.png diff --git a/web/static/img/flags/Slovakia.png b/web/frontend/public/img/flags/Slovakia.png similarity index 100% rename from web/static/img/flags/Slovakia.png rename to web/frontend/public/img/flags/Slovakia.png diff --git a/web/static/img/flags/Slovenia.png b/web/frontend/public/img/flags/Slovenia.png similarity index 100% rename from web/static/img/flags/Slovenia.png rename to web/frontend/public/img/flags/Slovenia.png diff --git a/web/static/img/flags/South-Africa.png b/web/frontend/public/img/flags/South-Africa.png similarity index 100% rename from web/static/img/flags/South-Africa.png rename to web/frontend/public/img/flags/South-Africa.png diff --git a/web/static/img/flags/South-Korea.png b/web/frontend/public/img/flags/South-Korea.png similarity index 100% rename from web/static/img/flags/South-Korea.png rename to web/frontend/public/img/flags/South-Korea.png diff --git a/web/static/img/flags/Spain.png b/web/frontend/public/img/flags/Spain.png similarity index 100% rename from web/static/img/flags/Spain.png rename to web/frontend/public/img/flags/Spain.png diff --git a/web/static/img/flags/Sweden.png b/web/frontend/public/img/flags/Sweden.png similarity index 100% rename from web/static/img/flags/Sweden.png rename to web/frontend/public/img/flags/Sweden.png diff --git a/web/static/img/flags/Switzerland.png b/web/frontend/public/img/flags/Switzerland.png similarity index 100% rename from web/static/img/flags/Switzerland.png rename to web/frontend/public/img/flags/Switzerland.png diff --git a/web/static/img/flags/Taiwan.png b/web/frontend/public/img/flags/Taiwan.png similarity index 100% rename from web/static/img/flags/Taiwan.png rename to web/frontend/public/img/flags/Taiwan.png diff --git a/web/static/img/flags/Thailand.png b/web/frontend/public/img/flags/Thailand.png similarity index 100% rename from web/static/img/flags/Thailand.png rename to web/frontend/public/img/flags/Thailand.png diff --git a/web/static/img/flags/Turkey.png b/web/frontend/public/img/flags/Turkey.png similarity index 100% rename from web/static/img/flags/Turkey.png rename to web/frontend/public/img/flags/Turkey.png diff --git a/web/static/img/flags/United-Kingdom.png b/web/frontend/public/img/flags/United-Kingdom.png similarity index 100% rename from web/static/img/flags/United-Kingdom.png rename to web/frontend/public/img/flags/United-Kingdom.png diff --git a/web/static/img/flags/United-Nations.png b/web/frontend/public/img/flags/United-Nations.png similarity index 100% rename from web/static/img/flags/United-Nations.png rename to web/frontend/public/img/flags/United-Nations.png diff --git a/web/static/img/flags/United-States.png b/web/frontend/public/img/flags/United-States.png similarity index 100% rename from web/static/img/flags/United-States.png rename to web/frontend/public/img/flags/United-States.png diff --git a/web/static/img/intro-bg.jpg b/web/frontend/public/img/intro-bg.jpg similarity index 100% rename from web/static/img/intro-bg.jpg rename to web/frontend/public/img/intro-bg.jpg diff --git a/web/static/img/laden.jpg b/web/frontend/public/img/laden.jpg similarity index 100% rename from web/static/img/laden.jpg rename to web/frontend/public/img/laden.jpg diff --git a/web/static/img/loading.gif b/web/frontend/public/img/loading.gif similarity index 100% rename from web/static/img/loading.gif rename to web/frontend/public/img/loading.gif diff --git a/web/static/img/logo-small.png b/web/frontend/public/img/logo-small.png similarity index 100% rename from web/static/img/logo-small.png rename to web/frontend/public/img/logo-small.png diff --git a/web/static/img/logo.png b/web/frontend/public/img/logo.png similarity index 100% rename from web/static/img/logo.png rename to web/frontend/public/img/logo.png diff --git a/web/static/img/map-marker.png b/web/frontend/public/img/map-marker.png similarity index 100% rename from web/static/img/map-marker.png rename to web/frontend/public/img/map-marker.png diff --git a/web/static/img/montage.jpg b/web/frontend/public/img/montage.jpg similarity index 100% rename from web/static/img/montage.jpg rename to web/frontend/public/img/montage.jpg diff --git a/web/static/img/niggi.jpg b/web/frontend/public/img/niggi.jpg similarity index 100% rename from web/static/img/niggi.jpg rename to web/frontend/public/img/niggi.jpg diff --git a/web/static/img/overview.jpg b/web/frontend/public/img/overview.jpg similarity index 100% rename from web/static/img/overview.jpg rename to web/frontend/public/img/overview.jpg diff --git a/web/static/img/progressbar.gif b/web/frontend/public/img/progressbar.gif similarity index 100% rename from web/static/img/progressbar.gif rename to web/frontend/public/img/progressbar.gif diff --git a/web/static/img/waage.jpg b/web/frontend/public/img/waage.jpg similarity index 100% rename from web/static/img/waage.jpg rename to web/frontend/public/img/waage.jpg diff --git a/web/static/img/wickeln.jpg b/web/frontend/public/img/wickeln.jpg similarity index 100% rename from web/static/img/wickeln.jpg rename to web/frontend/public/img/wickeln.jpg diff --git a/web/static/img/wolle.jpg b/web/frontend/public/img/wolle.jpg similarity index 100% rename from web/static/img/wolle.jpg rename to web/frontend/public/img/wolle.jpg diff --git a/web/static/js/bootstrap.bundle.js b/web/frontend/public/js/bootstrap.bundle.js similarity index 100% rename from web/static/js/bootstrap.bundle.js rename to web/frontend/public/js/bootstrap.bundle.js diff --git a/web/static/js/bootstrap.bundle.js.map b/web/frontend/public/js/bootstrap.bundle.js.map similarity index 100% rename from web/static/js/bootstrap.bundle.js.map rename to web/frontend/public/js/bootstrap.bundle.js.map diff --git a/web/static/js/bootstrap.bundle.min.js b/web/frontend/public/js/bootstrap.bundle.min.js similarity index 100% rename from web/static/js/bootstrap.bundle.min.js rename to web/frontend/public/js/bootstrap.bundle.min.js diff --git a/web/static/js/bootstrap.bundle.min.js.map b/web/frontend/public/js/bootstrap.bundle.min.js.map similarity index 100% rename from web/static/js/bootstrap.bundle.min.js.map rename to web/frontend/public/js/bootstrap.bundle.min.js.map diff --git a/web/static/js/bootstrap.esm.js b/web/frontend/public/js/bootstrap.esm.js similarity index 100% rename from web/static/js/bootstrap.esm.js rename to web/frontend/public/js/bootstrap.esm.js diff --git a/web/static/js/bootstrap.esm.js.map b/web/frontend/public/js/bootstrap.esm.js.map similarity index 100% rename from web/static/js/bootstrap.esm.js.map rename to web/frontend/public/js/bootstrap.esm.js.map diff --git a/web/static/js/bootstrap.esm.min.js b/web/frontend/public/js/bootstrap.esm.min.js similarity index 100% rename from web/static/js/bootstrap.esm.min.js rename to web/frontend/public/js/bootstrap.esm.min.js diff --git a/web/static/js/bootstrap.esm.min.js.map b/web/frontend/public/js/bootstrap.esm.min.js.map similarity index 100% rename from web/static/js/bootstrap.esm.min.js.map rename to web/frontend/public/js/bootstrap.esm.min.js.map diff --git a/web/static/js/bootstrap.js b/web/frontend/public/js/bootstrap.js similarity index 100% rename from web/static/js/bootstrap.js rename to web/frontend/public/js/bootstrap.js diff --git a/web/static/js/bootstrap.js.map b/web/frontend/public/js/bootstrap.js.map similarity index 100% rename from web/static/js/bootstrap.js.map rename to web/frontend/public/js/bootstrap.js.map diff --git a/web/static/js/bootstrap.min.js b/web/frontend/public/js/bootstrap.min.js similarity index 100% rename from web/static/js/bootstrap.min.js rename to web/frontend/public/js/bootstrap.min.js diff --git a/web/static/js/bootstrap.min.js.map b/web/frontend/public/js/bootstrap.min.js.map similarity index 100% rename from web/static/js/bootstrap.min.js.map rename to web/frontend/public/js/bootstrap.min.js.map diff --git a/web/static/robots.txt b/web/frontend/public/robots.txt similarity index 100% rename from web/static/robots.txt rename to web/frontend/public/robots.txt diff --git a/web/frontend/src/App.svelte b/web/frontend/src/App.svelte new file mode 100644 index 0000000..9e85973 --- /dev/null +++ b/web/frontend/src/App.svelte @@ -0,0 +1,45 @@ + + +
+
+ + + +
+

Vite + Svelte

+ +
+ +
+ +

+ Check out SvelteKit, the official Svelte app framework powered by Vite! +

+ +

Click on the Vite and Svelte logos to learn more

+
+ + diff --git a/web/frontend/src/app.css b/web/frontend/src/app.css new file mode 100644 index 0000000..6a278fd --- /dev/null +++ b/web/frontend/src/app.css @@ -0,0 +1,86 @@ +:root { + font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} +a:hover { + color: #535bf2; +} + +html, +body { + position: relative; + width: 100%; + height: 100%; +} + +/* body { */ +/* margin: 0; */ +/* display: flex; */ +/* place-items: center; */ +/* min-width: 320px; */ +/* min-height: 100vh; */ +/* } */ + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +.card { + padding: 2em; +} + +#app { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} diff --git a/web/frontend/src/assets/svelte.svg b/web/frontend/src/assets/svelte.svg new file mode 100644 index 0000000..c5e0848 --- /dev/null +++ b/web/frontend/src/assets/svelte.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/frontend/src/lib/Counter.svelte b/web/frontend/src/lib/Counter.svelte new file mode 100644 index 0000000..37d75ce --- /dev/null +++ b/web/frontend/src/lib/Counter.svelte @@ -0,0 +1,10 @@ + + + diff --git a/web/frontend/src/main.ts b/web/frontend/src/main.ts new file mode 100644 index 0000000..664a057 --- /dev/null +++ b/web/frontend/src/main.ts @@ -0,0 +1,9 @@ +import { mount } from 'svelte' +import './app.css' +import App from './App.svelte' + +const app = mount(App, { + target: document.getElementById('app')!, +}) + +export default app diff --git a/web/frontend/src/vite-env.d.ts b/web/frontend/src/vite-env.d.ts new file mode 100644 index 0000000..4078e74 --- /dev/null +++ b/web/frontend/src/vite-env.d.ts @@ -0,0 +1,2 @@ +/// +/// diff --git a/web/frontend/svelte.config.js b/web/frontend/svelte.config.js new file mode 100644 index 0000000..b0683fd --- /dev/null +++ b/web/frontend/svelte.config.js @@ -0,0 +1,7 @@ +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte' + +export default { + // Consult https://svelte.dev/docs#compile-time-svelte-preprocess + // for more information about preprocessors + preprocess: vitePreprocess(), +} diff --git a/web/frontend/tsconfig.app.json b/web/frontend/tsconfig.app.json new file mode 100644 index 0000000..55a2f9b --- /dev/null +++ b/web/frontend/tsconfig.app.json @@ -0,0 +1,20 @@ +{ + "extends": "@tsconfig/svelte/tsconfig.json", + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "module": "ESNext", + "resolveJsonModule": true, + /** + * Typecheck JS in `.svelte` and `.js` files by default. + * Disable checkJs if you'd like to use dynamic types in JS. + * Note that setting allowJs false does not prevent the use + * of JS in `.svelte` files. + */ + "allowJs": true, + "checkJs": true, + "isolatedModules": true, + "moduleDetection": "force" + }, + "include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte"] +} diff --git a/web/frontend/tsconfig.json b/web/frontend/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/web/frontend/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/web/frontend/tsconfig.node.json b/web/frontend/tsconfig.node.json new file mode 100644 index 0000000..9728af2 --- /dev/null +++ b/web/frontend/tsconfig.node.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2022", + "lib": ["ES2023"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/web/frontend/vite.config.ts b/web/frontend/vite.config.ts new file mode 100644 index 0000000..d32eba1 --- /dev/null +++ b/web/frontend/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import { svelte } from '@sveltejs/vite-plugin-svelte' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [svelte()], +}) diff --git a/web/templates.go b/web/templates.go index eea7d25..11dfec9 100644 --- a/web/templates.go +++ b/web/templates.go @@ -2,7 +2,27 @@ package web import ( "embed" + "net/http" + "text/template" + + "github.com/olivere/vite" ) +type PageData struct { + Title string + MsgType string + Message string + Redirect string + Vite *vite.Fragment +} + //go:embed templates var Templates embed.FS + +//go:embed frontend/dist +var StaticAssets embed.FS + +func RenderTemplate(w http.ResponseWriter, name string, data PageData) error { + tpl := template.Must(template.ParseFS(Templates, "templates/"+name+".html", "templates/base.html")) + return tpl.ExecuteTemplate(w, "base", data) +} diff --git a/web/templates/admin.html b/web/templates/admin.html new file mode 100644 index 0000000..3f9a328 --- /dev/null +++ b/web/templates/admin.html @@ -0,0 +1,3 @@ +{{define "vite"}} {{ .Vite.Tags }} {{end}} {{define "content"}} +
+{{end}} diff --git a/web/templates/base.html b/web/templates/base.html index d5ca8e4..f22543f 100644 --- a/web/templates/base.html +++ b/web/templates/base.html @@ -3,6 +3,7 @@ + {{block "vite" .}} {{end}} {{block "stylesheets" .}} {{end}} - +