package server import ( "context" "embed" "errors" "fmt" "net/http" "os" "os/signal" "time" "github.com/getsentry/sentry-go" sentryecho "github.com/getsentry/sentry-go/echo" "github.com/gorilla/sessions" "github.com/labstack/echo-contrib/session" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" "github.com/rjNemo/rentease/config" "github.com/rjNemo/rentease/internal/auth" "github.com/rjNemo/rentease/internal/booking" "github.com/rjNemo/rentease/internal/pdf" ) type Server struct { Router *echo.Echo bs *booking.Service as *auth.Service ps *pdf.PdfService hc *config.Host addr string secretKey string apiKey string } type options struct { port *int fs *embed.FS debug *bool secretKey *string apiKey *string origins []string } type Option func(*options) error func WithPort(port int) Option { return func(o *options) error { if port <= 0 { return errors.New("port should be positive") } o.port = &port return nil } } func WithFileSystem(fs embed.FS) Option { return func(o *options) error { o.fs = &fs return nil } } func WithDebug(debug bool) Option { return func(o *options) error { o.debug = &debug return nil } } func WithSecretKey(secretKey string) Option { return func(o *options) error { o.secretKey = &secretKey return nil } } func WithOrigins(origins []string) Option { return func(o *options) error { o.origins = origins return nil } } func WithApiKey(apiKey string) Option { return func(o *options) error { o.apiKey = &apiKey return nil } } func New(bs *booking.Service, as *auth.Service, ps *pdf.PdfService, hc *config.Host, opts ...Option) (*Server, error) { option := new(options) for _, opt := range opts { err := opt(option) if err != nil { return nil, err } } s := &Server{ Router: NewRouter(*option.fs, *option.debug, *option.secretKey, option.origins), bs: bs, as: as, ps: ps, hc: hc, addr: fmt.Sprintf("0.0.0.0:%d", *option.port), secretKey: *option.secretKey, apiKey: *option.apiKey, } s.MountHandlers() return s, nil } func (s Server) Start() { go func() { if err := s.Router.Start(s.addr); err != nil && !errors.Is(err, http.ErrServerClosed) { s.Router.Logger.Fatalf("shutting down the server: %s", err) } }() quit := make(chan os.Signal, 1) signal.Notify(quit, os.Interrupt) <-quit ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() if err := s.Router.Shutdown(ctx); err != nil { s.Router.Logger.Fatal(err) } } func NewRouter(fs embed.FS, debug bool, secret string, origins []string) *echo.Echo { e := echo.New() // config e.HideBanner = true e.Debug = debug e.HTTPErrorHandler = func(err error, c echo.Context) { if hub := sentryecho.GetHubFromContext(c); hub != nil { hub.WithScope(func(s *sentry.Scope) { hub.CaptureMessage(err.Error()) }) } code := http.StatusInternalServerError var he *echo.HTTPError if errors.As(err, &he) { code = he.Code } errorPage := fmt.Sprintf("assets/html/HTTP%d.html", code) if err := c.File(errorPage); err != nil { c.Logger().Error(err) } } // middlewares e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{ Format: "${time_rfc3339} [${method}: ${status}] ${uri}; ip=${remote_ip}; ${latency_human}; ${user_agent}\n", })) e.Use(middleware.Recover()) e.Use(middleware.Secure()) e.Use(middleware.Gzip()) e.Use(middleware.CORSWithConfig(middleware.CORSConfig{AllowOrigins: origins})) e.Use(sentryecho.New(sentryecho.Options{})) e.Use(SentryTracingMiddleware) e.Use(session.Middleware(sessions.NewCookieStore([]byte(secret)))) // static assets e.StaticFS("/static", echo.MustSubFS(fs, "assets")) return e }