package server import ( "encoding/json" "fmt" "html/template" "log/slog" "net/http" "path" "strconv" "strings" "github.com/angelbeltran/templater" "ruben/inventory2/internal/consts" "ruben/inventory2/internal/domains/accounts" "ruben/inventory2/internal/domains/authentication" etsy_platform "ruben/inventory2/internal/domains/platforms/etsy" "ruben/inventory2/internal/domains/raw_events" "ruben/inventory2/internal/server/middleware" "ruben/inventory2/internal/server/response" ) type Server struct { log *slog.Logger http.Handler contentDir string templater *templater.Templater rawEvents *raw_events.Store accts *accounts.Store etsy *etsy_platform.Platform auth *authentication.Authenticator authMiddleware *middleware.Auth } func NewServer( logger *slog.Logger, contentDir string, rawEvents *raw_events.Store, accts *accounts.Store, etsy *etsy_platform.Platform, auth *authentication.Authenticator, ) *Server { mux := response.NewMux(func(fn response.HandlerFunc) response.HandlerFunc { return func(r *http.Request) (response.Response, error) { res, err := fn(r) if err != nil { status := response.GetStatusFromError(err) // TODO: get better logger fmt.Printf("[ERROR]: %d: %s; %s\n", status, r.URL, err) } return res, err } }) s := &Server{ log: logger, Handler: mux, contentDir: contentDir, templater: templater.NewTemplater( contentDir+"/templates", func() template.FuncMap { return template.FuncMap{ // paths "buildSitePath": func(parts ...any) string { strParts := make([]string, len(parts)) for i, p := range parts { strParts[i] = fmt.Sprint(p) } // TODO: make "/site" dynamic somehow //return path.Join(append([]string{"/site"}, strParts...)...) return path.Join(strParts...) }, "splitPath": func(p string) []string { if p == "" { return nil } return strings.Split(strings.TrimSuffix(strings.TrimPrefix(p, "/"), "/"), "/") }, // params "addPathParam": func(k string, v any, args map[string]any) (map[string]any, error) { pathParams, ok := args["PathParams"].(map[string]string) if !ok { return nil, fmt.Errorf("PathParams no set are args: %v", args) } pathParams[k] = fmt.Sprint(v) return args, nil }, // parsing "parseInt": func(s string) (int, error) { return strconv.Atoi(s) }, "parseInt64": func(s string) (int64, error) { return strconv.ParseInt(s, 10, 64) }, "parsePlatform": func(s string) (accounts.Platform, error) { return accounts.NewPlatform(s) }, // arithmetic "addInt": func(a, b int) int { return a + b }, "subInt": func(a, b int) int { return a - b }, "multInt": func(a, b int) int { return a * b }, // json "prettyPrintJSON": func(j json.RawMessage) string { b, err := json.MarshalIndent(j, " ", "") if err != nil { return string(j) } return string(b) }, } }, ), rawEvents: rawEvents, accts: accts, etsy: etsy, auth: auth, authMiddleware: middleware.NewAuth( auth, newLoginURL, accts, ), } withAuth := func(fn response.HandlerFunc) response.HandlerFunc { return s.authMiddleware.AuthenticateAndAddIdentity(fn) } // login mux.Handle("GET /login", s.loginPage) mux.Handle("GET /login/callback", s.loginCallback) mux.Handle("GET /logout", s.logoutPage) // /accounts mux.Handle("POST /accounts", withAuth(s.createAccount)) mux.Handle("POST /accounts/{acctID}/inventory/sync-groups/draft/listings", withAuth(s.createSyncGroupListingDraft)) mux.Handle("PUT /accounts/{acctID}/inventory/sync-groups/draft/listings/{orderIndex}/shop", withAuth(s.setShopInSyncGroupListingDraft)) mux.Handle("PUT /accounts/{acctID}/inventory/sync-groups/draft/listings/{orderIndex}/listing", withAuth(s.setListingInSyncGroupListingDraft)) mux.Handle("DELETE /accounts/{acctID}/inventory/sync-groups/draft/listings/{orderIndex}", withAuth(s.deleteSyncGroupListingDraft)) mux.Handle("POST /accounts/{acctID}/inventory/sync-groups", withAuth(s.saveNewSyncGroup)) // webpage content // non-html content: scripts, styles, images, etc scfs := http.FileServer(http.Dir(contentDir + "/scripts")) mux.Mux.Handle("GET /scripts/", http.StripPrefix("/scripts", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/javascript") if path.Ext(r.URL.Path) == ".gz" { w.Header().Set("Content-Encoding", "gzip") } scfs.ServeHTTP(w, r) }))) mux.Mux.Handle("GET /styles/", http.StripPrefix("/styles", http.FileServer(http.Dir(contentDir+"/styles")))) // html // non-authenticated mux.Handle("GET /{$}", s.authMiddleware.AddIdentity(s.serveTemplates)) // authenticated mux.Handle("GET /", withAuth(s.serveTemplates)) return s } func mapConstantErrorsToHTTPErrors(err error) error { cerr := err for cerr != nil { switch cerr { case consts.ErrNotFound: return response.NotFound() case consts.ErrConflict: return response.Conflict() } uerr, ok := cerr.(interface { Unwrap() error }) if !ok { return err } cerr = uerr.Unwrap() } return err }