sessions/cmd/sessions/internal/serve/serve.go

164 lines
4.5 KiB
Go

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)
}