285 lines
6.9 KiB
Go
285 lines
6.9 KiB
Go
package server
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"html/template"
|
|
"log/slog"
|
|
"net/http"
|
|
"path"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"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/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 {
|
|
reqIDCh := newRequestIDProvider(ctx)
|
|
|
|
reqLog := logger.WithGroup("request")
|
|
mux := response.NewMux(func(fn response.HandlerFunc) response.HandlerFunc {
|
|
return func(r *http.Request) (response.Response, error) {
|
|
start := time.Now()
|
|
|
|
args := []any{
|
|
"id", <-reqIDCh,
|
|
"url", r.URL,
|
|
}
|
|
|
|
reqLog.Debug("Request", args...)
|
|
|
|
res, err := fn(r)
|
|
end := time.Now()
|
|
|
|
var status int
|
|
if err != nil {
|
|
status = response.GetStatusFromError(err)
|
|
} else {
|
|
var ok bool
|
|
if status, ok = res.GetStatus(); !ok {
|
|
status = http.StatusOK
|
|
}
|
|
}
|
|
|
|
args = append(args,
|
|
"status", status,
|
|
"elapsed", time.Duration(end.UnixNano()-start.UnixNano()),
|
|
)
|
|
|
|
switch status / 100 {
|
|
case 1:
|
|
reqLog.Debug("Response", args...)
|
|
case 2:
|
|
reqLog.Debug("Response", args...)
|
|
case 3:
|
|
reqLog.Debug("Response", args...)
|
|
case 4:
|
|
reqLog.Warn("Response", args...)
|
|
default:
|
|
reqLog.Error("Response", args...)
|
|
}
|
|
|
|
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
|
|
|
|
// 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
|
|
}
|