feat: add @nest-native/lockout login lockout (eighth library)#59
Merged
Conversation
Dogfood @nest-native/lockout in the auth flow. Failed logins now lock an identity (by email OR source IP) *before* the credential is checked, with a Drizzle-backed counter on the app's SQLite DB, a Retry-After on the lock, and reset on success. tRPC carries no HTTP request body for the guard's default extractor to read, so the login handler calls LockoutService directly — the library's documented non-HTTP-transport recipe. - schema + migration 0005: lockout_attempts (sqliteLockoutTable from @authlock/core) - config: LOCKOUT_LIMIT / LOCKOUT_COOLOFF_MS (defaults 5 / 15m) - tRPC context exposes the source IP so the login procedure can key on it - e2e spec: N failures -> 429, and the correct password is refused while locked - README + constitution reframed seven -> eight libraries/chapters Requires @nest-native/lockout 0.3.1: integrating here surfaced that LockoutModuleAsyncOptions.useFactory was typed (...args: unknown[]), which a typed factory parameter (db: AppDatabase) => ... does not satisfy under strictFunctionTypes. Fixed upstream to (...args: any[]) — matching NestJS's own FactoryProvider.useFactory — and released as 0.3.1 before wiring it in here.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Dogfoods
@nest-native/lockoutin the auth flow, bringing the reference app to 8/8 nest-native libraries. Failed logins now lock an identity before the credential is checked; the lock is keyed by email OR source IP, backed by a Drizzle counter on the same SQLite database, returns aRetry-After, and resets on a successful login.Why
LockoutService, notLockoutGuardLockoutGuard's default extractor reads the credential from the HTTP request body (req.body). tRPC's batched/superjson payload isn't shaped that way, so the guard doesn't fit. This app uses the library's documented non-HTTP-transport recipe: callLockoutServicefrom the login handler, building the identity from the validated input plus a source IP the tRPC context exposes. That friction is exactly what surfaced the doc/API gaps below — the feedback loop working as intended.The library improvement this surfaced (released first, per the repo's feedback-loop rule)
Wiring the Drizzle-backed store needed a typed async factory:
LockoutModuleAsyncOptions.useFactorywas typed(...args: unknown[]), and understrictFunctionTypesa factory with a typed injected parameter is not assignable to it — forcing callers to widen tounknownand re-narrow. Fixed upstream to(...args: any[])(matching NestJS's ownFactoryProvider.useFactory) and shipped as@nest-native/lockout@0.3.1(adapter-only;@authlock/coreunchanged at 0.3.0) before this integration, which now pins^0.3.1. The 0.3.1 docs also gained the non-HTTP-transport note above.Changes
0005—lockout_attemptsviasqliteLockoutTablefrom@authlock/core/sqlite(byte-identical DDL, same precedent as the jobs/messaging tables)auth.service.ts— pre-authcheck→ 429HttpException(mapped to tRPCTOO_MANY_REQUESTS);reportFailureon bad credentials,reportSuccesson login. Injected by explicit class token (@Inject(LockoutService)) like every other provider here — esbuild/tsx doesn't emitdesign:paramtypes, so the app never relies on type-based DIlockout.setup.ts—AppLockoutModulewires the store on the global Drizzle client, keyed[['email'], ['ip']], outside the request transaction so a failure is recorded even if the login tx rolls backLOCKOUT_LIMIT/LOCKOUT_COOLOFF_MS(defaults5/15m)req.ip ?? req.socket?.remoteAddress; connection address, not a proxy header)Tests / gates
New e2e spec
test/e2e/auth-lockout.spec.ts: afterLIMITfailed attempts the identity is locked and even the correct password is refused with 429 — the brute-force control gates before the credential check.All green locally:
typecheck,lint,complexity(≤15),client-smoke:typecheck, andtest(84 pass, 0 fail; the live-Kafka spec self-skips — Docker not run). Mutation testing not run (docs + wiring change, no reworked algorithm).