package serve import ( "context" "errors" "fmt" "net/http" "os" "os/signal" "strings" "syscall" "time" "gitea.dwysokinski.me/Kichiyaki/chizap" "gitea.dwysokinski.me/twhelp/sessions/cmd/sessions/internal" "gitea.dwysokinski.me/twhelp/sessions/internal/bundb" "gitea.dwysokinski.me/twhelp/sessions/internal/router/meta" "gitea.dwysokinski.me/twhelp/sessions/internal/router/rest" "gitea.dwysokinski.me/twhelp/sessions/internal/service" "github.com/kelseyhightower/envconfig" "github.com/uptrace/bun" "github.com/urfave/cli/v2" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" "go.uber.org/zap" ) const ( defaultPort = "9234" readTimeout = 5 * time.Second readHeaderTimeout = time.Second handlerTimeout = time.Second writeTimeout = 10 * time.Second idleTimeout = 180 * time.Second serverShutdownTimeout = 10 * time.Second metaEndpointsPrefix = "/_meta" ) func New() *cli.Command { return &cli.Command{ Name: "serve", Usage: "Runs the http server", Action: func(c *cli.Context) error { logger := zap.L() db, err := internal.NewBunDB() if err != nil { return fmt.Errorf("internal.NewBunDB: %w", err) } defer func() { _ = db.Close() }() srv, err := newServer(logger, db) if err != nil { return fmt.Errorf("newServer: %w", err) } go runServer(srv, logger) defer func() { ctxShutdown, cancelShutdown := context.WithTimeout(c.Context, serverShutdownTimeout) defer cancelShutdown() _ = srv.Shutdown(ctxShutdown) }() logger.Info("Server is listening on the port "+defaultPort, zap.String("port", defaultPort)) waitForSignal(c.Context) return nil }, } } func newServer(logger *zap.Logger, db *bun.DB) (*http.Server, error) { apiCfg, err := newAPIConfig() if err != nil { return nil, fmt.Errorf("newAPIConfig: %w", err) } // repos userRepo := bundb.NewUser(db) apiKeyRepo := bundb.NewAPIKey(db) sessionRepo := bundb.NewSession(db) // services userSvc := service.NewUser(userRepo) apiKeySvc := service.NewAPIKey(apiKeyRepo, userSvc) sessionSvc := service.NewSession(sessionRepo, userSvc) // router r := chi.NewRouter() r.Use(getMiddlewares(logger)...) r.Mount(metaEndpointsPrefix, meta.New(bundb.NewChecker(db))) r.Mount("/api", rest.New( apiKeySvc, sessionSvc, rest.WithCORSConfig(rest.CORSConfig{ Enabled: apiCfg.CORSEnabled, AllowedOrigins: apiCfg.CORSAllowedOrigins, AllowedMethods: apiCfg.CORSAllowedMethods, AllowCredentials: apiCfg.CORSAllowCredentials, MaxAge: int(apiCfg.CORSMaxAge), }), rest.WithSwaggerConfig(rest.SwaggerConfig{ Enabled: apiCfg.SwaggerEnabled, Host: apiCfg.SwaggerHost, Schemes: apiCfg.SwaggerSchemes, }), )) return &http.Server{ Addr: ":" + defaultPort, Handler: http.TimeoutHandler(r, handlerTimeout, "Timeout"), ReadTimeout: readTimeout, ReadHeaderTimeout: readHeaderTimeout, WriteTimeout: writeTimeout, IdleTimeout: idleTimeout, }, nil } type apiConfig struct { SwaggerEnabled bool `envconfig:"SWAGGER_ENABLED" default:"false"` SwaggerHost string `envconfig:"SWAGGER_HOST" default:""` SwaggerSchemes []string `envconfig:"SWAGGER_SCHEMES" default:"http,https"` CORSEnabled bool `envconfig:"CORS_ENABLED" default:"false"` CORSAllowedOrigins []string `envconfig:"CORS_ALLOWED_ORIGINS" default:""` CORSAllowCredentials bool `envconfig:"CORS_ALLOW_CREDENTIALS" default:"false"` CORSAllowedMethods []string `envconfig:"CORS_ALLOWED_METHODS" default:"HEAD,GET,POST,PUT"` CORSMaxAge uint32 `envconfig:"CORS_MAX_AGE" default:"300"` } func newAPIConfig() (apiConfig, error) { var cfg apiConfig if err := envconfig.Process("API", &cfg); err != nil { return apiConfig{}, fmt.Errorf("envconfig.Process: %w", err) } return cfg, nil } func getMiddlewares(logger *zap.Logger) chi.Middlewares { return chi.Middlewares{ middleware.RealIP, chizap.Logger(logger, chizap.WithFilter(omitMetaEndpoints)), middleware.Recoverer, } } func runServer(srv *http.Server, logger *zap.Logger) { if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { logger.Fatal("srv.ListenAndServe:" + err.Error()) } } func waitForSignal(ctx context.Context) { ctx, stop := signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM) defer stop() <-ctx.Done() } func omitMetaEndpoints(r *http.Request) bool { return !strings.HasPrefix(r.URL.Path, metaEndpointsPrefix) }