Refactor login flow with reset token support
All checks were successful
Automated Container Build / build-and-push (push) Successful in 1m6s
All checks were successful
Automated Container Build / build-and-push (push) Successful in 1m6s
This commit is contained in:
parent
f0548bcc6e
commit
95af276d2e
11 changed files with 679 additions and 226 deletions
|
|
@ -3,6 +3,12 @@ import { cookies } from "next/headers";
|
|||
|
||||
export interface SessionData {
|
||||
isAuthenticated: boolean;
|
||||
sessionGeneration?: number;
|
||||
}
|
||||
|
||||
interface PasswordResetSessionData {
|
||||
nonce?: string;
|
||||
expiresAt?: string;
|
||||
}
|
||||
|
||||
const sessionOptions: SessionOptions = {
|
||||
|
|
@ -15,14 +21,27 @@ const sessionOptions: SessionOptions = {
|
|||
},
|
||||
};
|
||||
|
||||
const passwordResetSessionOptions: SessionOptions = {
|
||||
password: sessionOptions.password,
|
||||
cookieName: "study-app-password-reset",
|
||||
cookieOptions: {
|
||||
secure: sessionOptions.cookieOptions?.secure,
|
||||
httpOnly: true,
|
||||
sameSite: "lax" as const,
|
||||
maxAge: 15 * 60,
|
||||
path: "/",
|
||||
},
|
||||
};
|
||||
|
||||
export async function getSession() {
|
||||
const cookieStore = await cookies();
|
||||
return getIronSession<SessionData>(cookieStore, sessionOptions);
|
||||
}
|
||||
|
||||
export async function createSession() {
|
||||
export async function createSession(sessionGeneration: number) {
|
||||
const session = await getSession();
|
||||
session.isAuthenticated = true;
|
||||
session.sessionGeneration = sessionGeneration;
|
||||
await session.save();
|
||||
}
|
||||
|
||||
|
|
@ -35,3 +54,23 @@ export async function isAuthenticated() {
|
|||
const session = await getSession();
|
||||
return session.isAuthenticated === true;
|
||||
}
|
||||
|
||||
export async function getPasswordResetSession() {
|
||||
const cookieStore = await cookies();
|
||||
return getIronSession<PasswordResetSessionData>(
|
||||
cookieStore,
|
||||
passwordResetSessionOptions
|
||||
);
|
||||
}
|
||||
|
||||
export async function authorizePasswordReset(nonce: string, expiresAt: string) {
|
||||
const session = await getPasswordResetSession();
|
||||
session.nonce = nonce;
|
||||
session.expiresAt = expiresAt;
|
||||
await session.save();
|
||||
}
|
||||
|
||||
export async function destroyPasswordResetAuthorization() {
|
||||
const session = await getPasswordResetSession();
|
||||
session.destroy();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,20 +12,25 @@ const store = new Map<string, RateLimitEntry>();
|
|||
const WINDOW_MS = 60 * 1000; // 1 minute
|
||||
const MAX_REQUESTS = 10;
|
||||
|
||||
export function checkRateLimit(ip: string): { allowed: boolean; retryAfterMs: number } {
|
||||
export function checkRateLimit(
|
||||
key: string,
|
||||
options: { windowMs?: number; maxRequests?: number } = {}
|
||||
): { allowed: boolean; retryAfterMs: number } {
|
||||
const now = Date.now();
|
||||
const entry = store.get(ip) ?? { timestamps: [] };
|
||||
const windowMs = options.windowMs ?? WINDOW_MS;
|
||||
const maxRequests = options.maxRequests ?? MAX_REQUESTS;
|
||||
const entry = store.get(key) ?? { timestamps: [] };
|
||||
|
||||
// Remove timestamps outside the window
|
||||
entry.timestamps = entry.timestamps.filter((t) => now - t < WINDOW_MS);
|
||||
entry.timestamps = entry.timestamps.filter((t) => now - t < windowMs);
|
||||
|
||||
if (entry.timestamps.length >= MAX_REQUESTS) {
|
||||
if (entry.timestamps.length >= maxRequests) {
|
||||
const oldestInWindow = entry.timestamps[0];
|
||||
const retryAfterMs = WINDOW_MS - (now - oldestInWindow);
|
||||
const retryAfterMs = windowMs - (now - oldestInWindow);
|
||||
return { allowed: false, retryAfterMs };
|
||||
}
|
||||
|
||||
entry.timestamps.push(now);
|
||||
store.set(ip, entry);
|
||||
store.set(key, entry);
|
||||
return { allowed: true, retryAfterMs: 0 };
|
||||
}
|
||||
|
|
|
|||
19
src/lib/validation/authSchemas.ts
Normal file
19
src/lib/validation/authSchemas.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { z } from "zod";
|
||||
|
||||
export const loginSchema = z.object({
|
||||
password: z.string().min(8, "Password must be at least 8 characters"),
|
||||
});
|
||||
|
||||
export const resetTokenSchema = z.object({
|
||||
token: z.string().min(1, "Reset token is required"),
|
||||
});
|
||||
|
||||
export const completePasswordResetSchema = z
|
||||
.object({
|
||||
password: z.string().min(8, "Password must be at least 8 characters"),
|
||||
confirmPassword: z.string(),
|
||||
})
|
||||
.refine((value) => value.password === value.confirmPassword, {
|
||||
message: "Passwords do not match",
|
||||
path: ["confirmPassword"],
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue