Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 | /**
* Account Lockout Middleware
*
* Integrates account lockout checking with Better Auth authentication flow
*
* Usage in API routes:
* ```typescript
* import { withLockoutProtection } from "@snapback/auth/middleware";
*
* export async function POST(request: Request) {
* return withLockoutProtection(request, async () => {
* return auth.handler(request);
* });
* }
* ```
*/
import { logger } from "@snapback/infrastructure";
import { checkAccountLockout, incrementFailedAttempts, resetFailedAttempts } from "../lib/account-lockout";
/**
* Hash email for privacy-safe logging (GDPR compliant)
* Uses SHA-256 first 8 chars for traceability without exposing PII
*/
function hashEmail(email: string): string {
const hash = email.split("").reduce((acc, char) => ((acc << 5) - acc + char.charCodeAt(0)) | 0, 0);
return `email_${Math.abs(hash).toString(16).padStart(8, "0")}`;
}
/**
* Extract email from request body
* Handles both JSON and FormData
*/
async function extractEmail(request: Request): Promise<string | null> {
try {
const contentType = request.headers.get("content-type") || "";
if (contentType.includes("application/json")) {
const clone = request.clone();
const body = await clone.json();
return body.email || null;
}
if (contentType.includes("application/x-www-form-urlencoded")) {
const clone = request.clone();
const formData = await clone.formData();
return formData.get("email")?.toString() || null;
}
return null;
} catch (error) {
logger.warn("Failed to extract email from request", { error });
return null;
}
}
/**
* Check if request is a login attempt
*/
function isLoginRequest(request: Request): boolean {
const url = new URL(request.url);
return (
request.method === "POST" &&
(url.pathname.includes("/sign-in") || url.pathname.includes("/signin") || url.pathname.includes("/login"))
);
}
/**
* Middleware wrapper for account lockout protection
*
* Flow:
* 1. Check if account is locked before authentication
* 2. If locked, return 429 (Too Many Requests)
* 3. Otherwise, proceed with authentication
* 4. On success: reset failed attempts counter
* 5. On failure: increment failed attempts counter
*
* @param request Incoming HTTP request
* @param handler Authentication handler function
* @returns HTTP Response
*/
export async function withLockoutProtection(
request: Request,
handler: (req: Request) => Promise<Response>,
): Promise<Response> {
// Only apply to login requests
if (!isLoginRequest(request)) {
return handler(request);
}
const email = await extractEmail(request);
if (!email) {
// No email in request - let Better Auth handle validation
return handler(request);
}
// Check if account is locked
const lockout = await checkAccountLockout(email);
if (lockout.locked) {
logger.warn("Login attempt blocked - account locked", {
email,
remainingTime: lockout.remainingTime,
});
return new Response(
JSON.stringify({
error: "Account temporarily locked due to too many failed login attempts",
retryAfter: lockout.remainingTime,
lockedUntil: new Date(Date.now() + (lockout.remainingTime || 0) * 1000).toISOString(),
}),
{
status: 429, // Too Many Requests
headers: {
"Content-Type": "application/json",
"Retry-After": String(lockout.remainingTime || 900), // 15 minutes default
"X-RateLimit-Limit": "5",
"X-RateLimit-Remaining": "0",
},
},
);
}
// Proceed with authentication
const response = await handler(request);
// Handle response based on success/failure
if (response.status === 200) {
// Successful login - reset counter
await resetFailedAttempts(email);
logger.debug("Login successful - reset lockout counter", { emailHash: hashEmail(email) });
} else if (response.status === 401 || response.status === 403) {
// Failed login - increment counter
await incrementFailedAttempts(email);
logger.debug("Login failed - incremented lockout counter", { emailHash: hashEmail(email) });
}
return response;
}
/**
* Export individual lockout functions for direct use
*/
export { checkAccountLockout, incrementFailedAttempts, resetFailedAttempts };
|