Initial commit
This commit is contained in:
38
lib/auth/jwt.ts
Normal file
38
lib/auth/jwt.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { SignJWT, jwtVerify } from "jose";
|
||||
import { loadSystemConfig } from "../system-config";
|
||||
|
||||
export type SessionToken = {
|
||||
sub: string; // userId
|
||||
role: "ADMIN" | "USER";
|
||||
email: string;
|
||||
forcePasswordReset: boolean;
|
||||
};
|
||||
|
||||
async function getSecret() {
|
||||
const cfg = await loadSystemConfig();
|
||||
const s = cfg.auth?.jwtSecret || process.env.JWT_SECRET;
|
||||
if (!s) return null;
|
||||
return new TextEncoder().encode(s);
|
||||
}
|
||||
|
||||
export async function signSession(payload: SessionToken) {
|
||||
const secret = await getSecret();
|
||||
if (!secret) throw new Error("JWT secret not configured");
|
||||
const token = await new SignJWT(payload)
|
||||
.setProtectedHeader({ alg: "HS256" })
|
||||
.setIssuedAt()
|
||||
.setExpirationTime("7d")
|
||||
.sign(secret);
|
||||
return token;
|
||||
}
|
||||
|
||||
export async function verifySession(token: string) {
|
||||
const secret = await getSecret();
|
||||
if (!secret) return null;
|
||||
try {
|
||||
const { payload } = await jwtVerify(token, secret);
|
||||
return payload as any as SessionToken;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
9
lib/auth/password.ts
Normal file
9
lib/auth/password.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import bcrypt from "bcryptjs";
|
||||
|
||||
export async function hashPassword(pw: string) {
|
||||
return bcrypt.hash(pw, 10);
|
||||
}
|
||||
|
||||
export async function verifyPassword(pw: string, hash: string) {
|
||||
return bcrypt.compare(pw, hash);
|
||||
}
|
||||
94
lib/auth/rate-limit.ts
Normal file
94
lib/auth/rate-limit.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* Simple in-memory rate limiter for login attempts
|
||||
* Tracks login attempts per email address
|
||||
*/
|
||||
|
||||
interface RateLimitEntry {
|
||||
attempts: number;
|
||||
resetTime: number;
|
||||
}
|
||||
|
||||
const loginAttempts = new Map<string, RateLimitEntry>();
|
||||
const ATTEMPT_LIMIT = 5;
|
||||
const TIME_WINDOW_MS = 15 * 60 * 1000; // 15 minutes
|
||||
|
||||
export function checkRateLimit(email: string): {
|
||||
allowed: boolean;
|
||||
attempts: number;
|
||||
remainingTime: number;
|
||||
} {
|
||||
const now = Date.now();
|
||||
const entry = loginAttempts.get(email);
|
||||
|
||||
// No previous attempts
|
||||
if (!entry) {
|
||||
loginAttempts.set(email, {
|
||||
attempts: 1,
|
||||
resetTime: now + TIME_WINDOW_MS,
|
||||
});
|
||||
return {
|
||||
allowed: true,
|
||||
attempts: 1,
|
||||
remainingTime: TIME_WINDOW_MS,
|
||||
};
|
||||
}
|
||||
|
||||
// Window has expired
|
||||
if (now > entry.resetTime) {
|
||||
loginAttempts.set(email, {
|
||||
attempts: 1,
|
||||
resetTime: now + TIME_WINDOW_MS,
|
||||
});
|
||||
return {
|
||||
allowed: true,
|
||||
attempts: 1,
|
||||
remainingTime: TIME_WINDOW_MS,
|
||||
};
|
||||
}
|
||||
|
||||
// Still within the window
|
||||
const remainingTime = entry.resetTime - now;
|
||||
const allowed = entry.attempts < ATTEMPT_LIMIT;
|
||||
|
||||
if (!allowed) {
|
||||
// Still increment attempts to track that we rejected this attempt
|
||||
return {
|
||||
allowed: false,
|
||||
attempts: entry.attempts,
|
||||
remainingTime,
|
||||
};
|
||||
}
|
||||
|
||||
// Increment attempts
|
||||
entry.attempts++;
|
||||
return {
|
||||
allowed: true,
|
||||
attempts: entry.attempts,
|
||||
remainingTime,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Record a failed login attempt (call this on invalid credentials)
|
||||
* This increments the counter without checking the limit
|
||||
*/
|
||||
export function recordFailedAttempt(email: string): void {
|
||||
const now = Date.now();
|
||||
const entry = loginAttempts.get(email);
|
||||
|
||||
if (!entry || now > entry.resetTime) {
|
||||
loginAttempts.set(email, {
|
||||
attempts: 1,
|
||||
resetTime: now + TIME_WINDOW_MS,
|
||||
});
|
||||
} else {
|
||||
entry.attempts++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all rate limit entries (useful for testing or admin reset)
|
||||
*/
|
||||
export function clearRateLimits(): void {
|
||||
loginAttempts.clear();
|
||||
}
|
||||
11
lib/auth/session.ts
Normal file
11
lib/auth/session.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { cookies } from "next/headers";
|
||||
import { verifySession } from "./jwt";
|
||||
|
||||
export const SESSION_COOKIE = "ep_session";
|
||||
|
||||
export async function getSession() {
|
||||
const cookieStore = await cookies();
|
||||
const c = cookieStore.get(SESSION_COOKIE)?.value;
|
||||
if (!c) return null;
|
||||
return await verifySession(c);
|
||||
}
|
||||
45
lib/auth/validation.ts
Normal file
45
lib/auth/validation.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
export function sanitizeText(value: string) {
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
export function sanitizeMessage(value: string) {
|
||||
// Allow only alphanumeric, spaces, underscores, newlines, and basic punctuation
|
||||
// Remove any HTML tags, scripts, and dangerous characters
|
||||
return value
|
||||
.trim()
|
||||
.replace(/<[^>]*>/g, '') // Remove HTML tags
|
||||
.replace(/[^\w\s\n\r_.,!?'-]/g, '') // Keep only safe characters
|
||||
.slice(0, 5000); // Limit length
|
||||
}
|
||||
|
||||
export interface PasswordRequirement {
|
||||
name: string;
|
||||
met: boolean;
|
||||
}
|
||||
|
||||
export function getPasswordRequirements(value: string): PasswordRequirement[] {
|
||||
return [
|
||||
{
|
||||
name: "8-20 Characters",
|
||||
met: value.length >= 8 && value.length <= 20,
|
||||
},
|
||||
{
|
||||
name: "At least one capital letter",
|
||||
met: /[A-Z]/.test(value),
|
||||
},
|
||||
{
|
||||
name: "At least one number",
|
||||
met: /\d/.test(value),
|
||||
},
|
||||
{
|
||||
name: "No spaces",
|
||||
met: !/\s/.test(value),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export function isStrongPassword(value: string) {
|
||||
// 8-20 chars, at least one uppercase, at least one number, no spaces
|
||||
return /^(?=.*[A-Z])(?=.*\d)[A-Za-z0-9!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]{8,20}$/.test(value) && !/\s/.test(value);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user