176 lines
4.9 KiB
Go
176 lines
4.9 KiB
Go
package serve
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"os/signal"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
"gitea.dwysokinski.me/Kichiyaki/chiclientip"
|
|
"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/realclientip/realclientip-go"
|
|
"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(newChiMiddlewares(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 newChiMiddlewares(logger *zap.Logger) chi.Middlewares {
|
|
return chi.Middlewares{
|
|
chiclientip.ClientIP(
|
|
realclientip.NewChainStrategy(
|
|
realclientip.Must(realclientip.NewRightmostNonPrivateStrategy(http.CanonicalHeaderKey("X-Forwarded-For"))),
|
|
realclientip.RemoteAddrStrategy{},
|
|
),
|
|
),
|
|
chizap.Logger(
|
|
logger,
|
|
chizap.WithFilter(func(r *http.Request) bool {
|
|
return !strings.HasPrefix(r.URL.Path, metaEndpointsPrefix)
|
|
}),
|
|
chizap.WithIPFn(func(r *http.Request) string {
|
|
clientIP, _ := chiclientip.ClientIPFromContext(r.Context())
|
|
return clientIP
|
|
}),
|
|
),
|
|
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()
|
|
}
|