diff --git a/internal/app/README.md b/internal/app/README.md index 74a735a..60fcb4a 100644 --- a/internal/app/README.md +++ b/internal/app/README.md @@ -5,12 +5,13 @@ This package provides a dependency injection (DI) container for assembling and m ## Overview -The DI container centralizes the creation and wiring of all application dependencies, including: +The DI container centralizes the creation and wiring of all application dependencies. Not everything is a container field: -- Infrastructure components (database, logger) -- Repositories (data access layer) -- Use cases (business logic layer) -- HTTP servers and handlers +- **Infrastructure** (database, logger, tx manager, keyring, metrics) — memoized `once[T]` fields, shared widely. +- **Use cases** (business logic) — memoized `once[T]` fields, because both the HTTP path and the CLI consume them. +- **Authorizer** — a single memoized `once[T]` shared by every feature's Route Module. +- **Repositories** — *not* container fields. Each is built inline inside its use case's `init` (single consumer, stateless over `*sql.DB`). +- **HTTP handlers + Route Modules** — *not* container fields. Each feature owns a `buildModule` that builds use case → handler → module in one place. ## Key Features @@ -55,8 +56,12 @@ defer container.Shutdown(ctx) ### Dependency Graph +Container fields are the memoized nodes. Repositories and handlers do not appear +because they are built inline (repos inside use cases, handlers inside the +feature module builders). + ``` -Container +Container (memoized once[T] fields) ├── Config (provided) ├── Logger │ └── depends on: Config.LogLevel @@ -64,32 +69,24 @@ Container │ └── depends on: Config.DB* ├── TxManager │ └── depends on: Database -├── Crypto Services -│ ├── KMS Provider -│ │ └── depends on: Config.KMS* -│ └── Encryption Service -│ └── depends on: KMS Provider -├── Repositories (by domain) -│ ├── AuthRepository -│ │ └── depends on: Database -│ ├── SecretsRepository -│ │ └── depends on: Database -│ ├── TransitRepository -│ │ └── depends on: Database -│ └── TokenizationRepository -│ └── depends on: Database +├── Keyring (envelope encryption) +│ └── depends on: Database, MasterKeyChain (KMS) ├── Use Cases (by domain) -│ ├── AuthUseCase -│ │ └── depends on: AuthRepository, Crypto -│ ├── SecretsUseCase -│ │ └── depends on: SecretsRepository, Crypto -│ ├── TransitUseCase -│ │ └── depends on: TransitRepository, Crypto -│ └── TokenizationUseCase -│ └── depends on: TokenizationRepository, Crypto +│ ├── ClientUseCase / TokenUseCase / AuditLogUseCase +│ │ └── depends on: TxManager, inline repos, Keyring (via KeySigner) +│ ├── SecretUseCase +│ │ └── depends on: TxManager, inline repo, Keyring +│ ├── TransitKeyUseCase +│ │ └── depends on: TxManager, inline repo, Keyring +│ └── TokenizationKey/TokenizationUseCase +│ └── depends on: TxManager, inline repos, Keyring +├── Authorizer +│ └── depends on: AuditLogUseCase └── HTTP Server - ├── depends on: Logger, Config - └── depends on: All Use Cases + ├── depends on: Logger, Config, global auth/rate-limit middleware + └── mounts: buildAuthModule, buildSecretsModule, buildTransitModule, + buildTokenizationModule (each: use case → handler → Route Module, + with the shared Authorizer + business metrics bound) ``` ### Layer Separation @@ -175,54 +172,65 @@ func setupTestContainer(t *testing.T) *app.Container { ## Adding New Components -To add a new component to the container: +### Adding a use case (or shared infrastructure) + +Use cases and shared infrastructure are memoized `once[T]` fields. -### 1. Add field to Container struct +1. Add the field to the `Container` struct in `di.go`: ```go -type Container struct { - // ... existing fields - - // New component - orderUseCase *orderUsecase.OrderUseCase - orderUseCaseInit sync.Once -} +orderUseCase once[orderUseCase.OrderUseCase] ``` -### 2. Add getter method +2. Add the accessor + `init` (build the repository inline — it is a single + consumer, stateless over `*sql.DB`): ```go -func (c *Container) OrderUseCase() (*orderUsecase.OrderUseCase, error) { - var err error - c.orderUseCaseInit.Do(func() { - c.orderUseCase, err = c.initOrderUseCase() - if err != nil { - c.initErrors["orderUseCase"] = err - } +func (c *Container) OrderUseCase(ctx context.Context) (orderUseCase.OrderUseCase, error) { + return c.orderUseCase.get(func() (orderUseCase.OrderUseCase, error) { + return c.initOrderUseCase(ctx) }) +} + +func (c *Container) initOrderUseCase(ctx context.Context) (orderUseCase.OrderUseCase, error) { + db, err := c.DB(ctx) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to get database for order use case: %w", err) } - if storedErr, exists := c.initErrors["orderUseCase"]; exists { - return nil, storedErr + txManager, err := c.TxManager(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get tx manager for order use case: %w", err) } - return c.orderUseCase, nil + return orderUseCase.NewOrderUseCase(txManager, orderRepository.NewOrderRepository(db)), nil } ``` -### 3. Add initialization method +### Adding a feature (new HTTP endpoints) + +Each feature owns one `buildModule` that assembles use case → handler → +Route Module. Handlers are locals, never container fields. ```go -func (c *Container) initProductRepository() (productUsecase.ProductRepository, error) { - db, err := c.DB() +func (c *Container) buildOrderModule(ctx context.Context) (*orderHTTP.Module, error) { + uc, err := c.OrderUseCase(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get order use case for order module: %w", err) + } + authz, err := c.Authorizer(ctx) if err != nil { - return nil, fmt.Errorf("failed to get database: %w", err) + return nil, fmt.Errorf("failed to get authorizer for order module: %w", err) } - - return productRepository.NewProductRepository(db), nil + bm, err := c.BusinessMetrics(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get business metrics for order module: %w", err) + } + handler := orderHTTP.NewOrderHandler(uc, c.Logger()) + return orderHTTP.NewModule(handler, authz, bm), nil } ``` +Then add `c.buildOrderModule(ctx)` to the `registrars` slice in `initHTTPServer`. + ## Benefits of This Approach ### 1. Centralized Dependency Management diff --git a/internal/app/di.go b/internal/app/di.go index 62d2e70..8970e04 100644 --- a/internal/app/di.go +++ b/internal/app/di.go @@ -12,7 +12,6 @@ import ( "github.com/allisson/go-pwdhash" "github.com/gin-gonic/gin" - authDomain "github.com/allisson/secrets/internal/auth/domain" authHTTP "github.com/allisson/secrets/internal/auth/http" authUseCase "github.com/allisson/secrets/internal/auth/usecase" "github.com/allisson/secrets/internal/config" @@ -20,14 +19,8 @@ import ( "github.com/allisson/secrets/internal/http" "github.com/allisson/secrets/internal/keyring" "github.com/allisson/secrets/internal/metrics" - secretsDomain "github.com/allisson/secrets/internal/secrets/domain" - secretsHTTP "github.com/allisson/secrets/internal/secrets/http" secretsUseCase "github.com/allisson/secrets/internal/secrets/usecase" - tokenizationDomain "github.com/allisson/secrets/internal/tokenization/domain" - tokenizationHTTP "github.com/allisson/secrets/internal/tokenization/http" tokenizationUseCase "github.com/allisson/secrets/internal/tokenization/usecase" - transitDomain "github.com/allisson/secrets/internal/transit/domain" - transitHTTP "github.com/allisson/secrets/internal/transit/http" transitUseCase "github.com/allisson/secrets/internal/transit/usecase" ) @@ -55,16 +48,11 @@ type Container struct { // Keyring (envelope encryption) keyring once[keyring.Keyring] - // Repositories - secretRepository once[secretsDomain.SecretRepository] - clientRepository once[authDomain.ClientRepository] - tokenRepository once[authDomain.TokenRepository] - auditLogRepository once[authDomain.AuditLogRepository] - transitKeyRepository once[transitDomain.TransitKeyRepository] - tokenizationKeyRepository once[tokenizationDomain.TokenizationKeyRepository] - tokenizationTokenRepository once[tokenizationDomain.TokenRepository] - - // Use Cases + // Use Cases. Repositories and HTTP handlers are not container fields: + // repositories are built inline inside each use case's init (single + // consumer, stateless over *sql.DB), and handlers are built inline inside + // each feature's buildModule. Use cases remain here because both + // the HTTP path and the CLI consume them. kekUseCase once[keyring.KekUseCase] secretUseCase once[secretsUseCase.SecretUseCase] clientUseCase once[authUseCase.ClientUseCase] @@ -74,15 +62,8 @@ type Container struct { tokenizationKeyUseCase once[tokenizationUseCase.TokenizationKeyUseCase] tokenizationUseCase once[tokenizationUseCase.TokenizationUseCase] - // HTTP Handlers - clientHandler once[*authHTTP.ClientHandler] - tokenHandler once[*authHTTP.TokenHandler] - auditLogHandler once[*authHTTP.AuditLogHandler] - secretHandler once[*secretsHTTP.SecretHandler] - transitKeyHandler once[*transitHTTP.TransitKeyHandler] - cryptoHandler once[*transitHTTP.CryptoHandler] - tokenizationKeyHandler once[*tokenizationHTTP.TokenizationKeyHandler] - tokenizationHandler once[*tokenizationHTTP.TokenizationHandler] + // Authorizer — shared by every feature's Route Module. + authorizer once[*authHTTP.Authorizer] // Servers httpServer once[*http.Server] @@ -287,68 +268,19 @@ func (c *Container) initHTTPServer(ctx context.Context) (*http.Server, error) { logger, ) - clientHandler, err := c.ClientHandler(ctx) - if err != nil { - return nil, fmt.Errorf("failed to get client handler: %w", err) - } - - tokenHandler, err := c.TokenHandler(ctx) - if err != nil { - return nil, fmt.Errorf("failed to get token handler: %w", err) - } - - auditLogHandler, err := c.AuditLogHandler(ctx) - if err != nil { - return nil, fmt.Errorf("failed to get audit log handler: %w", err) - } - - secretHandler, err := c.SecretHandler(ctx) - if err != nil { - return nil, fmt.Errorf("failed to get secret handler: %w", err) - } - - transitKeyHandler, err := c.TransitKeyHandler(ctx) - if err != nil { - return nil, fmt.Errorf("failed to get transit key handler: %w", err) - } - - cryptoHandler, err := c.CryptoHandler(ctx) - if err != nil { - return nil, fmt.Errorf("failed to get crypto handler: %w", err) - } - - tokenizationKeyHandler, err := c.TokenizationKeyHandler(ctx) - if err != nil { - return nil, fmt.Errorf("failed to get tokenization key handler: %w", err) - } - - tokenizationHandler, err := c.TokenizationHandler(ctx) - if err != nil { - return nil, fmt.Errorf("failed to get tokenization handler: %w", err) - } - tokenUseCase, err := c.TokenUseCase(ctx) if err != nil { return nil, fmt.Errorf("failed to get token use case: %w", err) } - auditLogUseCase, err := c.AuditLogUseCase(ctx) - if err != nil { - return nil, fmt.Errorf("failed to get audit log use case: %w", err) - } - metricsProvider, err := c.MetricsProvider(ctx) if err != nil { return nil, fmt.Errorf("failed to get metrics provider: %w", err) } - businessMetrics, err := c.BusinessMetrics(ctx) - if err != nil { - return nil, fmt.Errorf("failed to get business metrics: %w", err) - } - - // Build the shared per-route middleware. Authentication is always present; - // the two rate limiters are optional and stay nil when disabled. + // Build the global middleware chain handed to SetupRouter. Authentication is + // always present; the shared rate limiter is optional and stays nil when + // disabled. Per-route authorization and metrics live inside each module. authMiddleware := authHTTP.AuthenticationMiddleware(tokenUseCase, logger) var rateLimitMiddleware gin.HandlerFunc @@ -361,34 +293,34 @@ func (c *Container) initHTTPServer(ctx context.Context) (*http.Server, error) { ) } - var tokenRateLimitMiddleware gin.HandlerFunc - if c.config.RateLimitTokenEnabled { - tokenRateLimitMiddleware = authHTTP.TokenRateLimitMiddleware( - ctx, - c.config.RateLimitTokenRequestsPerSec, - c.config.RateLimitTokenBurst, - logger, - ) + // Each feature owns its wiring behind a buildModule; the composition + // root assembles the Route Modules and the server mounts them without knowing + // any feature type. + authModule, err := c.buildAuthModule(ctx) + if err != nil { + return nil, fmt.Errorf("failed to build auth module: %w", err) + } + + secretsModule, err := c.buildSecretsModule(ctx) + if err != nil { + return nil, fmt.Errorf("failed to build secrets module: %w", err) } - // Build the per-route authorizer once; each module captures it so route - // registrations only carry the capability. - authz := authHTTP.NewAuthorizer(auditLogUseCase, logger) + transitModule, err := c.buildTransitModule(ctx) + if err != nil { + return nil, fmt.Errorf("failed to build transit module: %w", err) + } + + tokenizationModule, err := c.buildTokenizationModule(ctx) + if err != nil { + return nil, fmt.Errorf("failed to build tokenization module: %w", err) + } - // Each feature owns its route registration; the composition root assembles - // the modules and the server mounts them without knowing any feature type. registrars := []http.RouteRegistrar{ - authHTTP.NewModule( - clientHandler, - tokenHandler, - auditLogHandler, - authz, - businessMetrics, - tokenRateLimitMiddleware, - ), - secretsHTTP.NewModule(secretHandler, authz, businessMetrics), - transitHTTP.NewModule(transitKeyHandler, cryptoHandler, authz, businessMetrics), - tokenizationHTTP.NewModule(tokenizationKeyHandler, tokenizationHandler, authz, businessMetrics), + authModule, + secretsModule, + transitModule, + tokenizationModule, } server.SetupRouter( diff --git a/internal/app/di_auth.go b/internal/app/di_auth.go index 7d67cf5..e5eed06 100644 --- a/internal/app/di_auth.go +++ b/internal/app/di_auth.go @@ -5,8 +5,8 @@ import ( "fmt" "github.com/allisson/go-pwdhash" + "github.com/gin-gonic/gin" - authDomain "github.com/allisson/secrets/internal/auth/domain" authHTTP "github.com/allisson/secrets/internal/auth/http" authRepository "github.com/allisson/secrets/internal/auth/repository" authUseCase "github.com/allisson/secrets/internal/auth/usecase" @@ -48,13 +48,6 @@ func (c *Container) CompareSecret() authUseCase.CompareSecretFunc { } } -// ClientRepository returns the client repository. -func (c *Container) ClientRepository(ctx context.Context) (authDomain.ClientRepository, error) { - return c.clientRepository.get(func() (authDomain.ClientRepository, error) { - return c.initClientRepository(ctx) - }) -} - // ClientUseCase returns the client use case. func (c *Container) ClientUseCase(ctx context.Context) (authUseCase.ClientUseCase, error) { return c.clientUseCase.get(func() (authUseCase.ClientUseCase, error) { @@ -62,20 +55,6 @@ func (c *Container) ClientUseCase(ctx context.Context) (authUseCase.ClientUseCas }) } -// TokenRepository returns the token repository. -func (c *Container) TokenRepository(ctx context.Context) (authDomain.TokenRepository, error) { - return c.tokenRepository.get(func() (authDomain.TokenRepository, error) { - return c.initTokenRepository(ctx) - }) -} - -// AuditLogRepository returns the audit log repository. -func (c *Container) AuditLogRepository(ctx context.Context) (authDomain.AuditLogRepository, error) { - return c.auditLogRepository.get(func() (authDomain.AuditLogRepository, error) { - return c.initAuditLogRepository(ctx) - }) -} - // TokenUseCase returns the token use case. func (c *Container) TokenUseCase(ctx context.Context) (authUseCase.TokenUseCase, error) { return c.tokenUseCase.get(func() (authUseCase.TokenUseCase, error) { @@ -90,54 +69,26 @@ func (c *Container) AuditLogUseCase(ctx context.Context) (authUseCase.AuditLogUs }) } -// ClientHandler returns the HTTP handler for client management operations. -func (c *Container) ClientHandler(ctx context.Context) (*authHTTP.ClientHandler, error) { - return c.clientHandler.get(func() (*authHTTP.ClientHandler, error) { - return c.initClientHandler(ctx) - }) -} - -// TokenHandler returns the HTTP handler for token operations. -func (c *Container) TokenHandler(ctx context.Context) (*authHTTP.TokenHandler, error) { - return c.tokenHandler.get(func() (*authHTTP.TokenHandler, error) { - return c.initTokenHandler(ctx) - }) -} - -// AuditLogHandler returns the HTTP handler for audit log operations. -func (c *Container) AuditLogHandler(ctx context.Context) (*authHTTP.AuditLogHandler, error) { - return c.auditLogHandler.get(func() (*authHTTP.AuditLogHandler, error) { - return c.initAuditLogHandler(ctx) +// Authorizer returns the per-route authorizer shared by every feature's Route +// Module. Memoized so repeated builder calls (e.g. in tests) reuse one instance. +func (c *Container) Authorizer(ctx context.Context) (*authHTTP.Authorizer, error) { + return c.authorizer.get(func() (*authHTTP.Authorizer, error) { + return c.initAuthorizer(ctx) }) } -// initClientRepository creates the client repository. -func (c *Container) initClientRepository(ctx context.Context) (authDomain.ClientRepository, error) { +// initClientUseCase creates the client use case with all its dependencies. +func (c *Container) initClientUseCase(ctx context.Context) (authUseCase.ClientUseCase, error) { db, err := c.DB(ctx) if err != nil { - return nil, fmt.Errorf("failed to get database for client repository: %w", err) + return nil, fmt.Errorf("failed to get database for client use case: %w", err) } - return authRepository.NewClientRepository(db), nil -} - -// initClientUseCase creates the client use case with all its dependencies. -func (c *Container) initClientUseCase(ctx context.Context) (authUseCase.ClientUseCase, error) { txManager, err := c.TxManager(ctx) if err != nil { return nil, fmt.Errorf("failed to get tx manager for client use case: %w", err) } - clientRepository, err := c.ClientRepository(ctx) - if err != nil { - return nil, fmt.Errorf("failed to get client repository for client use case: %w", err) - } - - tokenRepository, err := c.TokenRepository(ctx) - if err != nil { - return nil, fmt.Errorf("failed to get token repository for client use case: %w", err) - } - auditLogUseCase, err := c.AuditLogUseCase(ctx) if err != nil { return nil, fmt.Errorf("failed to get audit log use case for client use case: %w", err) @@ -145,43 +96,18 @@ func (c *Container) initClientUseCase(ctx context.Context) (authUseCase.ClientUs return authUseCase.NewClientUseCase( txManager, - clientRepository, - tokenRepository, + authRepository.NewClientRepository(db), + authRepository.NewTokenRepository(db), auditLogUseCase, c.HashSecret(), ), nil } -// initTokenRepository creates the token repository. -func (c *Container) initTokenRepository(ctx context.Context) (authDomain.TokenRepository, error) { - db, err := c.DB(ctx) - if err != nil { - return nil, fmt.Errorf("failed to get database for token repository: %w", err) - } - - return authRepository.NewTokenRepository(db), nil -} - -// initAuditLogRepository creates the audit log repository. -func (c *Container) initAuditLogRepository(ctx context.Context) (authDomain.AuditLogRepository, error) { - db, err := c.DB(ctx) - if err != nil { - return nil, fmt.Errorf("failed to get database for audit log repository: %w", err) - } - - return authRepository.NewAuditLogRepository(db), nil -} - // initTokenUseCase creates the token use case with all its dependencies. func (c *Container) initTokenUseCase(ctx context.Context) (authUseCase.TokenUseCase, error) { - clientRepository, err := c.ClientRepository(ctx) - if err != nil { - return nil, fmt.Errorf("failed to get client repository for token use case: %w", err) - } - - tokenRepository, err := c.TokenRepository(ctx) + db, err := c.DB(ctx) if err != nil { - return nil, fmt.Errorf("failed to get token repository for token use case: %w", err) + return nil, fmt.Errorf("failed to get database for token use case: %w", err) } auditLogUseCase, err := c.AuditLogUseCase(ctx) @@ -191,8 +117,8 @@ func (c *Container) initTokenUseCase(ctx context.Context) (authUseCase.TokenUseC return authUseCase.NewTokenUseCase( c.config, - clientRepository, - tokenRepository, + authRepository.NewClientRepository(db), + authRepository.NewTokenRepository(db), auditLogUseCase, c.CompareSecret(), c.Logger(), @@ -201,9 +127,9 @@ func (c *Container) initTokenUseCase(ctx context.Context) (authUseCase.TokenUseC // initAuditLogUseCase creates the audit log use case with all its dependencies. func (c *Container) initAuditLogUseCase(ctx context.Context) (authUseCase.AuditLogUseCase, error) { - auditLogRepository, err := c.AuditLogRepository(ctx) + db, err := c.DB(ctx) if err != nil { - return nil, fmt.Errorf("failed to get audit log repository for audit log use case: %w", err) + return nil, fmt.Errorf("failed to get database for audit log use case: %w", err) } keySigner, err := c.KeySigner(ctx) @@ -211,35 +137,70 @@ func (c *Container) initAuditLogUseCase(ctx context.Context) (authUseCase.AuditL return nil, fmt.Errorf("failed to get key signer for audit log use case: %w", err) } - return authUseCase.NewAuditLogUseCase(auditLogRepository, keySigner), nil + return authUseCase.NewAuditLogUseCase(authRepository.NewAuditLogRepository(db), keySigner), nil } -// initClientHandler creates the client HTTP handler with all its dependencies. -func (c *Container) initClientHandler(ctx context.Context) (*authHTTP.ClientHandler, error) { - clientUseCase, err := c.ClientUseCase(ctx) +// initAuthorizer builds the per-route authorizer from the shared audit log use case. +func (c *Container) initAuthorizer(ctx context.Context) (*authHTTP.Authorizer, error) { + auditLogUseCase, err := c.AuditLogUseCase(ctx) if err != nil { - return nil, fmt.Errorf("failed to get client use case for client handler: %w", err) + return nil, fmt.Errorf("failed to get audit log use case for authorizer: %w", err) } - return authHTTP.NewClientHandler(clientUseCase, c.Logger()), nil + return authHTTP.NewAuthorizer(auditLogUseCase, c.Logger()), nil } -// initTokenHandler creates the token HTTP handler with all its dependencies. -func (c *Container) initTokenHandler(ctx context.Context) (*authHTTP.TokenHandler, error) { - tokenUseCase, err := c.TokenUseCase(ctx) +// buildAuthModule assembles the auth Route Module: use cases → handlers → +// module. The optional token rate limiter is built last so the error paths above +// never start its background goroutine. +func (c *Container) buildAuthModule(ctx context.Context) (*authHTTP.Module, error) { + clientUseCase, err := c.ClientUseCase(ctx) if err != nil { - return nil, fmt.Errorf("failed to get token use case for token handler: %w", err) + return nil, fmt.Errorf("failed to get client use case for auth module: %w", err) } - return authHTTP.NewTokenHandler(tokenUseCase, c.Logger()), nil -} + tokenUseCase, err := c.TokenUseCase(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get token use case for auth module: %w", err) + } -// initAuditLogHandler creates the audit log HTTP handler with all its dependencies. -func (c *Container) initAuditLogHandler(ctx context.Context) (*authHTTP.AuditLogHandler, error) { auditLogUseCase, err := c.AuditLogUseCase(ctx) if err != nil { - return nil, fmt.Errorf("failed to get audit log use case for audit log handler: %w", err) + return nil, fmt.Errorf("failed to get audit log use case for auth module: %w", err) } - return authHTTP.NewAuditLogHandler(auditLogUseCase, c.Logger()), nil + authz, err := c.Authorizer(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get authorizer for auth module: %w", err) + } + + bm, err := c.BusinessMetrics(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get business metrics for auth module: %w", err) + } + + clientHandler := authHTTP.NewClientHandler(clientUseCase, c.Logger()) + tokenHandler := authHTTP.NewTokenHandler(tokenUseCase, c.Logger()) + auditLogHandler := authHTTP.NewAuditLogHandler(auditLogUseCase, c.Logger()) + + // Built last: TokenRateLimitMiddleware starts a cleanup goroutine, so it must + // not run on any of the error paths above. + var tokenRateLimitMiddleware gin.HandlerFunc + if c.config.RateLimitTokenEnabled { + tokenRateLimitMiddleware = authHTTP.TokenRateLimitMiddleware( + ctx, + c.config.RateLimitTokenRequestsPerSec, + c.config.RateLimitTokenBurst, + c.Logger(), + ) + } + + return authHTTP.NewModule( + clientHandler, + tokenHandler, + auditLogHandler, + authz, + bm, + tokenRateLimitMiddleware, + ), nil } diff --git a/internal/app/di_secrets.go b/internal/app/di_secrets.go index 2de7231..8719394 100644 --- a/internal/app/di_secrets.go +++ b/internal/app/di_secrets.go @@ -4,19 +4,11 @@ import ( "context" "fmt" - secretsDomain "github.com/allisson/secrets/internal/secrets/domain" secretsHTTP "github.com/allisson/secrets/internal/secrets/http" secretsRepository "github.com/allisson/secrets/internal/secrets/repository" secretsUseCase "github.com/allisson/secrets/internal/secrets/usecase" ) -// SecretRepository returns the secret repository. -func (c *Container) SecretRepository(ctx context.Context) (secretsDomain.SecretRepository, error) { - return c.secretRepository.get(func() (secretsDomain.SecretRepository, error) { - return c.initSecretRepository(ctx) - }) -} - // SecretUseCase returns the secret use case. func (c *Container) SecretUseCase(ctx context.Context) (secretsUseCase.SecretUseCase, error) { return c.secretUseCase.get(func() (secretsUseCase.SecretUseCase, error) { @@ -24,23 +16,12 @@ func (c *Container) SecretUseCase(ctx context.Context) (secretsUseCase.SecretUse }) } -// SecretHandler returns the HTTP handler for secret management operations. -func (c *Container) SecretHandler(ctx context.Context) (*secretsHTTP.SecretHandler, error) { - return c.secretHandler.get(func() (*secretsHTTP.SecretHandler, error) { - return c.initSecretHandler(ctx) - }) -} - -func (c *Container) initSecretRepository(ctx context.Context) (secretsDomain.SecretRepository, error) { +func (c *Container) initSecretUseCase(ctx context.Context) (secretsUseCase.SecretUseCase, error) { db, err := c.DB(ctx) if err != nil { - return nil, fmt.Errorf("failed to get database for secret repository: %w", err) + return nil, fmt.Errorf("failed to get database for secret use case: %w", err) } - return secretsRepository.NewSecretRepository(db), nil -} - -func (c *Container) initSecretUseCase(ctx context.Context) (secretsUseCase.SecretUseCase, error) { txManager, err := c.TxManager(ctx) if err != nil { return nil, fmt.Errorf("failed to get tx manager for secret use case: %w", err) @@ -51,24 +32,33 @@ func (c *Container) initSecretUseCase(ctx context.Context) (secretsUseCase.Secre return nil, fmt.Errorf("failed to get keyring for secret use case: %w", err) } - secretRepository, err := c.SecretRepository(ctx) - if err != nil { - return nil, fmt.Errorf("failed to get secret repository for secret use case: %w", err) - } - return secretsUseCase.NewSecretUseCase( txManager, kr, - secretRepository, + secretsRepository.NewSecretRepository(db), c.config.SecretValueSizeLimitBytes, ), nil } -func (c *Container) initSecretHandler(ctx context.Context) (*secretsHTTP.SecretHandler, error) { +// buildSecretsModule assembles the secrets Route Module: use case → handler → +// module, with the shared authorizer and business metrics bound. +func (c *Container) buildSecretsModule(ctx context.Context) (*secretsHTTP.Module, error) { secretUseCase, err := c.SecretUseCase(ctx) if err != nil { - return nil, fmt.Errorf("failed to get secret use case for secret handler: %w", err) + return nil, fmt.Errorf("failed to get secret use case for secrets module: %w", err) + } + + authz, err := c.Authorizer(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get authorizer for secrets module: %w", err) } - return secretsHTTP.NewSecretHandler(secretUseCase, c.Logger()), nil + bm, err := c.BusinessMetrics(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get business metrics for secrets module: %w", err) + } + + handler := secretsHTTP.NewSecretHandler(secretUseCase, c.Logger()) + + return secretsHTTP.NewModule(handler, authz, bm), nil } diff --git a/internal/app/di_test.go b/internal/app/di_test.go index b24e7a8..2fe141a 100644 --- a/internal/app/di_test.go +++ b/internal/app/di_test.go @@ -602,7 +602,8 @@ func TestContainerAuthComponents(t *testing.T) { } } -// TestContainerAuthModule verifies that auth repositories and use cases can be retrieved. +// TestContainerAuthModule verifies that auth use cases and the auth Route Module +// surface the DB error through their construction chain. func TestContainerAuthModule(t *testing.T) { cfg := &config.Config{ LogLevel: "info", @@ -610,49 +611,29 @@ func TestContainerAuthModule(t *testing.T) { container := NewContainer(cfg) ctx := context.Background() - _, err := container.ClientRepository(ctx) - if err == nil { - t.Error("expected error for client repository with invalid db config") - } - - _, err = container.ClientUseCase(ctx) + _, err := container.ClientUseCase(ctx) if err == nil { t.Error("expected error for client use case with invalid db config") } - _, err = container.TokenRepository(ctx) - if err == nil { - t.Error("expected error for token repository with invalid db config") - } - _, err = container.TokenUseCase(ctx) if err == nil { t.Error("expected error for token use case with invalid db config") } - _, err = container.AuditLogRepository(ctx) - if err == nil { - t.Error("expected error for audit log repository with invalid db config") - } - _, err = container.AuditLogUseCase(ctx) if err == nil { t.Error("expected error for audit log use case with invalid db config") } - _, err = container.ClientHandler(ctx) - if err == nil { - t.Error("expected error for client handler with invalid db config") - } - - _, err = container.TokenHandler(ctx) + _, err = container.Authorizer(ctx) if err == nil { - t.Error("expected error for token handler with invalid db config") + t.Error("expected error for authorizer with invalid db config") } - _, err = container.AuditLogHandler(ctx) + _, err = container.buildAuthModule(ctx) if err == nil { - t.Error("expected error for audit log handler with invalid db config") + t.Error("expected error for auth module with invalid db config") } } @@ -665,26 +646,21 @@ func TestContainerSecretsComponents(t *testing.T) { container := NewContainer(cfg) ctx := context.Background() - // Since repositories need a DB, we expect errors if DB is not and cannot be connected + // Since the use case needs a DB, we expect errors if DB is not and cannot be connected _, err := container.Keyring(ctx) if err == nil { t.Error("expected error for keyring with invalid db config") } - _, err = container.SecretRepository(ctx) - if err == nil { - t.Error("expected error for secret repository with invalid db config") - } - _, err = container.SecretUseCase(ctx) if err == nil { t.Error("expected error for secret use case with invalid db config") } - _, err = container.SecretHandler(ctx) + _, err = container.buildSecretsModule(ctx) if err == nil { - t.Error("expected error for secret handler with invalid db config") + t.Error("expected error for secrets module with invalid db config") } } @@ -697,24 +673,14 @@ func TestContainerTransitComponents(t *testing.T) { container := NewContainer(cfg) ctx := context.Background() - _, err := container.TransitKeyRepository(ctx) - if err == nil { - t.Error("expected error for transit key repository with invalid db config") - } - - _, err = container.TransitKeyUseCase(ctx) + _, err := container.TransitKeyUseCase(ctx) if err == nil { t.Error("expected error for transit key use case with invalid db config") } - _, err = container.TransitKeyHandler(ctx) - if err == nil { - t.Error("expected error for transit key handler with invalid db config") - } - - _, err = container.CryptoHandler(ctx) + _, err = container.buildTransitModule(ctx) if err == nil { - t.Error("expected error for crypto handler with invalid db config") + t.Error("expected error for transit module with invalid db config") } } @@ -727,17 +693,7 @@ func TestContainerTokenizationComponents(t *testing.T) { container := NewContainer(cfg) ctx := context.Background() - _, err := container.TokenizationKeyRepository(ctx) - if err == nil { - t.Error("expected error for tokenization key repository with invalid db config") - } - - _, err = container.TokenizationTokenRepository(ctx) - if err == nil { - t.Error("expected error for tokenization token repository with invalid db config") - } - - _, err = container.TokenizationKeyUseCase(ctx) + _, err := container.TokenizationKeyUseCase(ctx) if err == nil { t.Error("expected error for tokenization key use case with invalid db config") } @@ -747,14 +703,9 @@ func TestContainerTokenizationComponents(t *testing.T) { t.Error("expected error for tokenization use case with invalid db config") } - _, err = container.TokenizationKeyHandler(ctx) - if err == nil { - t.Error("expected error for tokenization key handler with invalid db config") - } - - _, err = container.TokenizationHandler(ctx) + _, err = container.buildTokenizationModule(ctx) if err == nil { - t.Error("expected error for tokenization handler with invalid db config") + t.Error("expected error for tokenization module with invalid db config") } } @@ -770,7 +721,7 @@ func TestContainerSyncMapConcurrency(t *testing.T) { go func() { _, _ = container.DB(ctx) _, _ = container.TxManager(ctx) - _, _ = container.ClientRepository(ctx) + _, _ = container.ClientUseCase(ctx) done <- true }() } diff --git a/internal/app/di_tokenization.go b/internal/app/di_tokenization.go index 91fd836..0f92d8d 100644 --- a/internal/app/di_tokenization.go +++ b/internal/app/di_tokenization.go @@ -4,28 +4,11 @@ import ( "context" "fmt" - tokenizationDomain "github.com/allisson/secrets/internal/tokenization/domain" tokenizationHTTP "github.com/allisson/secrets/internal/tokenization/http" tokenizationRepository "github.com/allisson/secrets/internal/tokenization/repository" tokenizationUseCase "github.com/allisson/secrets/internal/tokenization/usecase" ) -func (c *Container) TokenizationKeyRepository( - ctx context.Context, -) (tokenizationDomain.TokenizationKeyRepository, error) { - return c.tokenizationKeyRepository.get(func() (tokenizationDomain.TokenizationKeyRepository, error) { - return c.initTokenizationKeyRepository(ctx) - }) -} - -func (c *Container) TokenizationTokenRepository( - ctx context.Context, -) (tokenizationDomain.TokenRepository, error) { - return c.tokenizationTokenRepository.get(func() (tokenizationDomain.TokenRepository, error) { - return c.initTokenizationTokenRepository(ctx) - }) -} - func (c *Container) TokenizationKeyUseCase( ctx context.Context, ) (tokenizationUseCase.TokenizationKeyUseCase, error) { @@ -42,85 +25,42 @@ func (c *Container) TokenizationUseCase( }) } -func (c *Container) TokenizationKeyHandler( - ctx context.Context, -) (*tokenizationHTTP.TokenizationKeyHandler, error) { - return c.tokenizationKeyHandler.get(func() (*tokenizationHTTP.TokenizationKeyHandler, error) { - return c.initTokenizationKeyHandler(ctx) - }) -} - -func (c *Container) TokenizationHandler(ctx context.Context) (*tokenizationHTTP.TokenizationHandler, error) { - return c.tokenizationHandler.get(func() (*tokenizationHTTP.TokenizationHandler, error) { - return c.initTokenizationHandler(ctx) - }) -} - -func (c *Container) initTokenizationKeyRepository( - ctx context.Context, -) (tokenizationDomain.TokenizationKeyRepository, error) { - db, err := c.DB(ctx) - if err != nil { - return nil, fmt.Errorf("failed to get database for tokenization key repository: %w", err) - } - - return tokenizationRepository.NewTokenizationKeyRepository(db), nil -} - -func (c *Container) initTokenizationTokenRepository( +func (c *Container) initTokenizationKeyUseCase( ctx context.Context, -) (tokenizationDomain.TokenRepository, error) { +) (tokenizationUseCase.TokenizationKeyUseCase, error) { db, err := c.DB(ctx) if err != nil { - return nil, fmt.Errorf("failed to get database for tokenization token repository: %w", err) + return nil, fmt.Errorf("failed to get database for tokenization key use case: %w", err) } - return tokenizationRepository.NewTokenRepository(db), nil -} - -func (c *Container) initTokenizationKeyUseCase( - ctx context.Context, -) (tokenizationUseCase.TokenizationKeyUseCase, error) { txManager, err := c.TxManager(ctx) if err != nil { return nil, fmt.Errorf("failed to get tx manager for tokenization key use case: %w", err) } - tokenizationKeyRepository, err := c.TokenizationKeyRepository(ctx) - if err != nil { - return nil, fmt.Errorf( - "failed to get tokenization key repository for tokenization key use case: %w", - err, - ) - } - kr, err := c.Keyring(ctx) if err != nil { return nil, fmt.Errorf("failed to get keyring for tokenization key use case: %w", err) } - return tokenizationUseCase.NewTokenizationKeyUseCase(txManager, tokenizationKeyRepository, kr), nil + return tokenizationUseCase.NewTokenizationKeyUseCase( + txManager, + tokenizationRepository.NewTokenizationKeyRepository(db), + kr, + ), nil } func (c *Container) initTokenizationUseCase( ctx context.Context, ) (tokenizationUseCase.TokenizationUseCase, error) { - txManager, err := c.TxManager(ctx) - if err != nil { - return nil, fmt.Errorf("failed to get tx manager for tokenization use case: %w", err) - } - - tokenizationKeyRepository, err := c.TokenizationKeyRepository(ctx) + db, err := c.DB(ctx) if err != nil { - return nil, fmt.Errorf( - "failed to get tokenization key repository for tokenization use case: %w", - err, - ) + return nil, fmt.Errorf("failed to get database for tokenization use case: %w", err) } - tokenRepository, err := c.TokenizationTokenRepository(ctx) + txManager, err := c.TxManager(ctx) if err != nil { - return nil, fmt.Errorf("failed to get token repository for tokenization use case: %w", err) + return nil, fmt.Errorf("failed to get tx manager for tokenization use case: %w", err) } kr, err := c.Keyring(ctx) @@ -130,37 +70,41 @@ func (c *Container) initTokenizationUseCase( return tokenizationUseCase.NewTokenizationUseCase( txManager, - tokenizationKeyRepository, - tokenRepository, + tokenizationRepository.NewTokenizationKeyRepository(db), + tokenizationRepository.NewTokenRepository(db), kr, ), nil } -func (c *Container) initTokenizationKeyHandler( - ctx context.Context, -) (*tokenizationHTTP.TokenizationKeyHandler, error) { +// buildTokenizationModule assembles the tokenization Route Module: use cases → +// both handlers → module, with the shared authorizer and business metrics bound. +func (c *Container) buildTokenizationModule(ctx context.Context) (*tokenizationHTTP.Module, error) { tokenizationKeyUseCase, err := c.TokenizationKeyUseCase(ctx) if err != nil { - return nil, fmt.Errorf( - "failed to get tokenization key use case for tokenization key handler: %w", - err, - ) + return nil, fmt.Errorf("failed to get tokenization key use case for tokenization module: %w", err) } - return tokenizationHTTP.NewTokenizationKeyHandler(tokenizationKeyUseCase, c.Logger()), nil -} - -func (c *Container) initTokenizationHandler( - ctx context.Context, -) (*tokenizationHTTP.TokenizationHandler, error) { tokenizationUC, err := c.TokenizationUseCase(ctx) if err != nil { - return nil, fmt.Errorf("failed to get tokenization use case for tokenization handler: %w", err) + return nil, fmt.Errorf("failed to get tokenization use case for tokenization module: %w", err) } - return tokenizationHTTP.NewTokenizationHandler( + authz, err := c.Authorizer(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get authorizer for tokenization module: %w", err) + } + + bm, err := c.BusinessMetrics(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get business metrics for tokenization module: %w", err) + } + + keyHandler := tokenizationHTTP.NewTokenizationKeyHandler(tokenizationKeyUseCase, c.Logger()) + tokenizationHandler := tokenizationHTTP.NewTokenizationHandler( tokenizationUC, c.config.TokenizationBatchLimit, c.Logger(), - ), nil + ) + + return tokenizationHTTP.NewModule(keyHandler, tokenizationHandler, authz, bm), nil } diff --git a/internal/app/di_transit.go b/internal/app/di_transit.go index 01b8399..120156b 100644 --- a/internal/app/di_transit.go +++ b/internal/app/di_transit.go @@ -4,80 +4,60 @@ import ( "context" "fmt" - transitDomain "github.com/allisson/secrets/internal/transit/domain" transitHTTP "github.com/allisson/secrets/internal/transit/http" transitRepository "github.com/allisson/secrets/internal/transit/repository" transitUseCase "github.com/allisson/secrets/internal/transit/usecase" ) -func (c *Container) TransitKeyRepository(ctx context.Context) (transitDomain.TransitKeyRepository, error) { - return c.transitKeyRepository.get(func() (transitDomain.TransitKeyRepository, error) { - return c.initTransitKeyRepository(ctx) - }) -} - func (c *Container) TransitKeyUseCase(ctx context.Context) (transitUseCase.TransitKeyUseCase, error) { return c.transitKeyUseCase.get(func() (transitUseCase.TransitKeyUseCase, error) { return c.initTransitKeyUseCase(ctx) }) } -func (c *Container) TransitKeyHandler(ctx context.Context) (*transitHTTP.TransitKeyHandler, error) { - return c.transitKeyHandler.get(func() (*transitHTTP.TransitKeyHandler, error) { - return c.initTransitKeyHandler(ctx) - }) -} - -func (c *Container) CryptoHandler(ctx context.Context) (*transitHTTP.CryptoHandler, error) { - return c.cryptoHandler.get(func() (*transitHTTP.CryptoHandler, error) { - return c.initCryptoHandler(ctx) - }) -} - -func (c *Container) initTransitKeyRepository( - ctx context.Context, -) (transitDomain.TransitKeyRepository, error) { +func (c *Container) initTransitKeyUseCase(ctx context.Context) (transitUseCase.TransitKeyUseCase, error) { db, err := c.DB(ctx) if err != nil { - return nil, fmt.Errorf("failed to get database for transit key repository: %w", err) + return nil, fmt.Errorf("failed to get database for transit key use case: %w", err) } - return transitRepository.NewTransitKeyRepository(db), nil -} - -func (c *Container) initTransitKeyUseCase(ctx context.Context) (transitUseCase.TransitKeyUseCase, error) { txManager, err := c.TxManager(ctx) if err != nil { return nil, fmt.Errorf("failed to get tx manager for transit key use case: %w", err) } - transitKeyRepository, err := c.TransitKeyRepository(ctx) - if err != nil { - return nil, fmt.Errorf("failed to get transit key repository for transit key use case: %w", err) - } - kr, err := c.Keyring(ctx) if err != nil { return nil, fmt.Errorf("failed to get keyring for transit key use case: %w", err) } - return transitUseCase.NewTransitKeyUseCase(txManager, transitKeyRepository, kr), nil + return transitUseCase.NewTransitKeyUseCase( + txManager, + transitRepository.NewTransitKeyRepository(db), + kr, + ), nil } -func (c *Container) initTransitKeyHandler(ctx context.Context) (*transitHTTP.TransitKeyHandler, error) { +// buildTransitModule assembles the transit Route Module: use case → both +// handlers → module, with the shared authorizer and business metrics bound. +func (c *Container) buildTransitModule(ctx context.Context) (*transitHTTP.Module, error) { transitKeyUseCase, err := c.TransitKeyUseCase(ctx) if err != nil { - return nil, fmt.Errorf("failed to get transit key use case for transit key handler: %w", err) + return nil, fmt.Errorf("failed to get transit key use case for transit module: %w", err) } - return transitHTTP.NewTransitKeyHandler(transitKeyUseCase, c.Logger()), nil -} + authz, err := c.Authorizer(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get authorizer for transit module: %w", err) + } -func (c *Container) initCryptoHandler(ctx context.Context) (*transitHTTP.CryptoHandler, error) { - transitKeyUseCase, err := c.TransitKeyUseCase(ctx) + bm, err := c.BusinessMetrics(ctx) if err != nil { - return nil, fmt.Errorf("failed to get transit key use case for crypto handler: %w", err) + return nil, fmt.Errorf("failed to get business metrics for transit module: %w", err) } - return transitHTTP.NewCryptoHandler(transitKeyUseCase, c.Logger()), nil + keyHandler := transitHTTP.NewTransitKeyHandler(transitKeyUseCase, c.Logger()) + cryptoHandler := transitHTTP.NewCryptoHandler(transitKeyUseCase, c.Logger()) + + return transitHTTP.NewModule(keyHandler, cryptoHandler, authz, bm), nil } diff --git a/test/integration/audit_log_signature_test.go b/test/integration/audit_log_signature_test.go index 75e0d6b..03f72a8 100644 --- a/test/integration/audit_log_signature_test.go +++ b/test/integration/audit_log_signature_test.go @@ -16,6 +16,7 @@ import ( "github.com/allisson/secrets/internal/app" authDomain "github.com/allisson/secrets/internal/auth/domain" + authRepository "github.com/allisson/secrets/internal/auth/repository" authUseCase "github.com/allisson/secrets/internal/auth/usecase" "github.com/allisson/secrets/internal/config" "github.com/allisson/secrets/internal/keyring" @@ -39,8 +40,9 @@ func TestAuditLogSignature_EndToEnd(t *testing.T) { keySigner, err := testCtx.container.KeySigner(ctx) require.NoError(t, err, "failed to get key signer") - auditLogRepo, err := testCtx.container.AuditLogRepository(context.Background()) - require.NoError(t, err, "failed to get audit log repository") + db, err := testCtx.container.DB(context.Background()) + require.NoError(t, err, "failed to get database") + auditLogRepo := authRepository.NewAuditLogRepository(db) // Create use case with signing enabled auditLogUseCase := authUseCase.NewAuditLogUseCase(auditLogRepo, keySigner)