package server import ( "context" "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" "ruben/inventory2/internal/server/router" "ruben/inventory2/internal/server/webhooks" etsy_webhooks "ruben/inventory2/internal/server/webhooks/etsy" ) 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( ctx context.Context, logger *slog.Logger, contentDir string, rawEvents *raw_events.Store, accts *accounts.Store, etsy *etsy_platform.Platform, auth *authentication.Authenticator, ) *Server { mux := router.NewMux(middleware.LogRequests(ctx, logger.WithGroup("request"))) 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 // TODO: consider a separate '/login or /auth' router mux.Handle("GET /login", s.loginPage) mux.Handle("GET /login/callback", s.loginCallback) mux.Handle("GET /logout", s.logoutPage) // TODO: consider a separate '/accounts' router // /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)) // TODO: consider a webhook separate router // webhooks webhookHandler := http.StripPrefix("/webhooks", webhooks.New( logger.WithGroup("webhooks"), rawEvents, etsy, webhooks.Config{ Etsy: etsy_webhooks.Config{ OAuthRedirectURIWithAcctIDParam: "/oauth/account/{acctID}/auth_code", }, }, )) mux.Mux.Handle("GET /webhooks/", webhookHandler) mux.Mux.Handle("POST /webhooks/", webhookHandler) mux.Mux.Handle("PUT /webhooks/", webhookHandler) // TODO: consider a separate webpage router // 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 } func newRequestIDProvider(ctx context.Context) <-chan int { reqIDCh := make(chan int) go func() { defer close(reqIDCh) nextReqID := 1 for { select { case reqIDCh <- nextReqID: nextReqID += 1 case <-ctx.Done(): return } } }() return reqIDCh }