Files
galaxy-game/authsession/internal/api/internalhttp/server.go
T
2026-04-08 16:23:07 +02:00

272 lines
8.1 KiB
Go

package internalhttp
import (
"context"
"errors"
"fmt"
"net"
"net/http"
"sync"
"time"
"galaxy/authsession/internal/service/blockuser"
"galaxy/authsession/internal/service/getsession"
"galaxy/authsession/internal/service/listusersessions"
"galaxy/authsession/internal/service/revokeallusersessions"
"galaxy/authsession/internal/service/revokedevicesession"
"galaxy/authsession/internal/telemetry"
"go.uber.org/zap"
)
const (
defaultAddr = ":8081"
defaultReadHeaderTimeout = 2 * time.Second
defaultReadTimeout = 10 * time.Second
defaultIdleTimeout = time.Minute
defaultRequestTimeout = 3 * time.Second
)
// GetSessionUseCase describes the trusted internal get-session service
// consumed by the HTTP transport layer.
type GetSessionUseCase interface {
// Execute loads one device session for trusted internal callers.
Execute(ctx context.Context, input getsession.Input) (getsession.Result, error)
}
// ListUserSessionsUseCase describes the trusted internal list-user-sessions
// service consumed by the HTTP transport layer.
type ListUserSessionsUseCase interface {
// Execute lists all sessions of one user for trusted internal callers.
Execute(ctx context.Context, input listusersessions.Input) (listusersessions.Result, error)
}
// RevokeDeviceSessionUseCase describes the trusted internal single-session
// revoke service consumed by the HTTP transport layer.
type RevokeDeviceSessionUseCase interface {
// Execute revokes one device session and returns the frozen
// acknowledgement.
Execute(ctx context.Context, input revokedevicesession.Input) (revokedevicesession.Result, error)
}
// RevokeAllUserSessionsUseCase describes the trusted internal bulk-revoke
// service consumed by the HTTP transport layer.
type RevokeAllUserSessionsUseCase interface {
// Execute revokes all active sessions of one user and returns the frozen
// acknowledgement.
Execute(ctx context.Context, input revokeallusersessions.Input) (revokeallusersessions.Result, error)
}
// BlockUserUseCase describes the trusted internal block-user service consumed
// by the HTTP transport layer.
type BlockUserUseCase interface {
// Execute applies a block state to one subject and returns the frozen
// acknowledgement.
Execute(ctx context.Context, input blockuser.Input) (blockuser.Result, error)
}
// Config describes the trusted internal HTTP listener owned by authsession.
type Config struct {
// Addr is the TCP listen address used by the trusted internal HTTP server.
Addr string
// ReadHeaderTimeout bounds how long the listener may spend reading request
// headers before the server rejects the connection.
ReadHeaderTimeout time.Duration
// ReadTimeout bounds how long the listener may spend reading one trusted
// internal request.
ReadTimeout time.Duration
// IdleTimeout bounds how long the listener keeps an idle keep-alive
// connection open.
IdleTimeout time.Duration
// RequestTimeout bounds one application-layer internal use-case call.
RequestTimeout time.Duration
}
// Validate reports whether cfg contains a usable internal HTTP listener
// configuration.
func (cfg Config) Validate() error {
switch {
case cfg.Addr == "":
return errors.New("internal HTTP addr must not be empty")
case cfg.ReadHeaderTimeout <= 0:
return errors.New("internal HTTP read header timeout must be positive")
case cfg.ReadTimeout <= 0:
return errors.New("internal HTTP read timeout must be positive")
case cfg.IdleTimeout <= 0:
return errors.New("internal HTTP idle timeout must be positive")
case cfg.RequestTimeout <= 0:
return errors.New("internal HTTP request timeout must be positive")
default:
return nil
}
}
// DefaultConfig returns the default trusted internal HTTP listener settings.
func DefaultConfig() Config {
return Config{
Addr: defaultAddr,
ReadHeaderTimeout: defaultReadHeaderTimeout,
ReadTimeout: defaultReadTimeout,
IdleTimeout: defaultIdleTimeout,
RequestTimeout: defaultRequestTimeout,
}
}
// Dependencies describes the collaborators used by the trusted internal HTTP
// transport layer.
type Dependencies struct {
// GetSession executes the trusted internal get-session use case.
GetSession GetSessionUseCase
// ListUserSessions executes the trusted internal list-user-sessions use
// case.
ListUserSessions ListUserSessionsUseCase
// RevokeDeviceSession executes the trusted internal single-session revoke
// use case.
RevokeDeviceSession RevokeDeviceSessionUseCase
// RevokeAllUserSessions executes the trusted internal bulk-revoke use case.
RevokeAllUserSessions RevokeAllUserSessionsUseCase
// BlockUser executes the trusted internal block-user use case.
BlockUser BlockUserUseCase
// Logger writes structured transport logs. When nil, a no-op logger is
// used.
Logger *zap.Logger
// Telemetry records OpenTelemetry spans and low-cardinality HTTP metrics.
// When nil, the transport still serves requests with no-op providers.
Telemetry *telemetry.Runtime
}
// Server owns the trusted internal HTTP listener exposed by authsession.
type Server struct {
cfg Config
handler http.Handler
logger *zap.Logger
stateMu sync.RWMutex
server *http.Server
listener net.Listener
}
// NewServer constructs one trusted internal HTTP server for cfg and deps.
func NewServer(cfg Config, deps Dependencies) (*Server, error) {
if err := cfg.Validate(); err != nil {
return nil, fmt.Errorf("new internal HTTP server: %w", err)
}
handler, err := newHandlerWithConfig(cfg, deps)
if err != nil {
return nil, fmt.Errorf("new internal HTTP server: %w", err)
}
logger := deps.Logger
if logger == nil {
logger = zap.NewNop()
}
logger = logger.Named("internal_http")
return &Server{
cfg: cfg,
handler: handler,
logger: logger,
}, nil
}
// Run binds the configured listener and serves the trusted internal HTTP
// surface until Shutdown closes the server.
func (s *Server) Run(ctx context.Context) error {
if ctx == nil {
return errors.New("run internal HTTP server: nil context")
}
if err := ctx.Err(); err != nil {
return err
}
listener, err := net.Listen("tcp", s.cfg.Addr)
if err != nil {
return fmt.Errorf("run internal HTTP server: listen on %q: %w", s.cfg.Addr, err)
}
server := &http.Server{
Handler: s.handler,
ReadHeaderTimeout: s.cfg.ReadHeaderTimeout,
ReadTimeout: s.cfg.ReadTimeout,
IdleTimeout: s.cfg.IdleTimeout,
}
s.stateMu.Lock()
s.server = server
s.listener = listener
s.stateMu.Unlock()
s.logger.Info("internal HTTP server started", zap.String("addr", listener.Addr().String()))
defer func() {
s.stateMu.Lock()
s.server = nil
s.listener = nil
s.stateMu.Unlock()
}()
err = server.Serve(listener)
switch {
case err == nil:
return nil
case errors.Is(err, http.ErrServerClosed):
s.logger.Info("internal HTTP server stopped")
return nil
default:
return fmt.Errorf("run internal HTTP server: serve on %q: %w", s.cfg.Addr, err)
}
}
// Shutdown gracefully stops the trusted internal HTTP server within ctx.
func (s *Server) Shutdown(ctx context.Context) error {
if ctx == nil {
return errors.New("shutdown internal HTTP server: nil context")
}
s.stateMu.RLock()
server := s.server
s.stateMu.RUnlock()
if server == nil {
return nil
}
if err := server.Shutdown(ctx); err != nil && !errors.Is(err, http.ErrServerClosed) {
return fmt.Errorf("shutdown internal HTTP server: %w", err)
}
return nil
}
func normalizeDependencies(deps Dependencies) (Dependencies, error) {
switch {
case deps.GetSession == nil:
return Dependencies{}, errors.New("get session use case must not be nil")
case deps.ListUserSessions == nil:
return Dependencies{}, errors.New("list user sessions use case must not be nil")
case deps.RevokeDeviceSession == nil:
return Dependencies{}, errors.New("revoke device session use case must not be nil")
case deps.RevokeAllUserSessions == nil:
return Dependencies{}, errors.New("revoke all user sessions use case must not be nil")
case deps.BlockUser == nil:
return Dependencies{}, errors.New("block user use case must not be nil")
case deps.Logger == nil:
deps.Logger = zap.NewNop()
}
deps.Logger = deps.Logger.Named("internal_http")
return deps, nil
}