fix(19): CR-04 scope login lockout to username, add TTL auto-expiry + admin-reset unlock

This commit is contained in:
Lucas Berger
2026-06-17 20:20:43 -04:00
parent 6ef8e03f8c
commit b083cb7193
4 changed files with 108 additions and 31 deletions
+9 -2
View File
@@ -35,6 +35,7 @@ import {
CredentialValidationError,
} from '../broker/credentialSync.js';
import { hashPassword } from '../auth/localCredentials.js';
import { resetLoginAttempts } from './localAuth.js';
import { COLOR_PALETTE } from '../auth/user.js';
// Side-effect import: brings in the ContextVariableMap augmentation for c.get('user')
import '../auth/devBypass.js';
@@ -220,9 +221,10 @@ adminRouter.post(
const { newPassword } = c.req.valid('json');
// T-19-06: NEVER log newPassword or the request body
// Verify the target user has a local_credentials row (404 if not)
// Verify the target user has a local_credentials row (404 if not).
// Also read the username so we can clear any login lockout for it (CR-04).
const [credRow] = await db
.select({ id: localCredentials.id })
.select({ id: localCredentials.id, username: localCredentials.username })
.from(localCredentials)
.where(eq(localCredentials.userId, targetId))
.limit(1);
@@ -237,6 +239,11 @@ adminRouter.post(
.set({ passwordHash: hashPassword(newPassword) })
.where(eq(localCredentials.userId, targetId));
// CR-04: an admin password reset must immediately clear any rate-limit / lockout
// state for this username, so a locked-out member regains access at once rather than
// waiting for the TTL. The lockout is keyed on username (not member id).
resetLoginAttempts(credRow.username);
return c.json({ ok: true }, 200);
} catch (err) {
console.error(