fix test failures, new export/backup solutions
This commit is contained in:
+74
-2074
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,96 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const registerSchema = z.object({
|
||||
username: z.string().trim().min(3).max(50).optional(),
|
||||
email: z.string().email().toLowerCase().trim(),
|
||||
password: z.string().min(8).max(100),
|
||||
name: z.string().trim().min(1).max(100),
|
||||
});
|
||||
|
||||
export const loginSchema = z
|
||||
.object({
|
||||
identifier: z.string().trim().min(1).max(255).optional(),
|
||||
email: z.string().email().toLowerCase().trim().optional(),
|
||||
username: z.string().trim().min(1).max(255).optional(),
|
||||
password: z.string(),
|
||||
})
|
||||
.refine((data) => Boolean(data.identifier || data.email || data.username), {
|
||||
message: "identifier/email/username is required",
|
||||
});
|
||||
|
||||
export const registrationToggleSchema = z.object({
|
||||
enabled: z.boolean(),
|
||||
});
|
||||
|
||||
export const adminRoleUpdateSchema = z.object({
|
||||
identifier: z.string().trim().min(1).max(255),
|
||||
role: z.enum(["ADMIN", "USER"]),
|
||||
});
|
||||
|
||||
export const authEnabledToggleSchema = z.object({
|
||||
enabled: z.boolean(),
|
||||
});
|
||||
|
||||
export const adminCreateUserSchema = z.object({
|
||||
username: z.string().trim().min(3).max(50).optional(),
|
||||
email: z.string().email().toLowerCase().trim(),
|
||||
password: z.string().min(8).max(100),
|
||||
name: z.string().trim().min(1).max(100),
|
||||
role: z.enum(["ADMIN", "USER"]).optional(),
|
||||
mustResetPassword: z.boolean().optional(),
|
||||
isActive: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export const adminUpdateUserSchema = z.object({
|
||||
username: z.string().trim().min(3).max(50).nullable().optional(),
|
||||
name: z.string().trim().min(1).max(100).optional(),
|
||||
role: z.enum(["ADMIN", "USER"]).optional(),
|
||||
mustResetPassword: z.boolean().optional(),
|
||||
isActive: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export const impersonateSchema = z
|
||||
.object({
|
||||
userId: z.string().trim().min(1).optional(),
|
||||
identifier: z.string().trim().min(1).optional(),
|
||||
})
|
||||
.refine((data) => Boolean(data.userId || data.identifier), {
|
||||
message: "userId/identifier is required",
|
||||
});
|
||||
|
||||
export const loginRateLimitUpdateSchema = z.object({
|
||||
enabled: z.boolean(),
|
||||
windowMs: z.number().int().min(10_000).max(24 * 60 * 60 * 1000),
|
||||
max: z.number().int().min(1).max(10_000),
|
||||
});
|
||||
|
||||
export const loginRateLimitResetSchema = z.object({
|
||||
identifier: z.string().trim().min(1).max(255),
|
||||
});
|
||||
|
||||
export const passwordResetRequestSchema = z.object({
|
||||
email: z.string().email().toLowerCase().trim(),
|
||||
});
|
||||
|
||||
export const passwordResetConfirmSchema = z.object({
|
||||
token: z.string().min(1),
|
||||
password: z.string().min(8).max(100),
|
||||
});
|
||||
|
||||
export const updateProfileSchema = z.object({
|
||||
name: z.string().trim().min(1).max(100),
|
||||
});
|
||||
|
||||
export const updateEmailSchema = z.object({
|
||||
email: z.string().email().toLowerCase().trim(),
|
||||
currentPassword: z.string().min(1).max(100),
|
||||
});
|
||||
|
||||
export const changePasswordSchema = z.object({
|
||||
currentPassword: z.string(),
|
||||
newPassword: z.string().min(8).max(100),
|
||||
});
|
||||
|
||||
export const mustResetPasswordSchema = z.object({
|
||||
newPassword: z.string().min(8).max(100),
|
||||
});
|
||||
+53
-1575
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,3 @@
|
||||
/**
|
||||
* Authentication middleware for protecting routes
|
||||
*/
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import jwt from "jsonwebtoken";
|
||||
import { config } from "../config";
|
||||
@@ -102,9 +99,6 @@ interface JwtPayload {
|
||||
impersonatorId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if decoded JWT is our expected payload structure
|
||||
*/
|
||||
const isJwtPayload = (decoded: unknown): decoded is JwtPayload => {
|
||||
if (typeof decoded !== "object" || decoded === null) {
|
||||
return false;
|
||||
@@ -120,9 +114,6 @@ const isJwtPayload = (decoded: unknown): decoded is JwtPayload => {
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Extract JWT token from Authorization header
|
||||
*/
|
||||
const extractToken = (req: Request): string | null => {
|
||||
const authHeader = req.headers.authorization;
|
||||
if (!authHeader || typeof authHeader !== "string") return null;
|
||||
@@ -135,9 +126,6 @@ const extractToken = (req: Request): string | null => {
|
||||
return parts[1];
|
||||
};
|
||||
|
||||
/**
|
||||
* Verify and decode JWT token
|
||||
*/
|
||||
const verifyToken = (token: string): JwtPayload | null => {
|
||||
try {
|
||||
const decoded = jwt.verify(token, config.jwtSecret);
|
||||
@@ -170,10 +158,6 @@ const isAllowedWhileMustResetPassword = (req: Request): boolean => {
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Require authentication middleware
|
||||
* Protects routes that require a valid JWT token
|
||||
*/
|
||||
export const requireAuth = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
@@ -276,10 +260,6 @@ export const requireAuth = async (
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Optional authentication middleware
|
||||
* Attaches user to request if token is present, but doesn't require it
|
||||
*/
|
||||
export const optionalAuth = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+1
-31
@@ -552,21 +552,6 @@ const CSRF_TOKEN_FUTURE_SKEW_MS = 5 * 60 * 1000; // 5 minutes clock skew toleran
|
||||
const CSRF_NONCE_BYTES = 16;
|
||||
const CSRF_TOKEN_MAX_LENGTH = 2048; // sanity limit against abuse
|
||||
|
||||
/**
|
||||
* IMPORTANT (Horizontal Scaling / K8s)
|
||||
* -----------------------------------
|
||||
* CSRF tokens must validate across multiple stateless instances.
|
||||
*
|
||||
* The prior in-memory Map-based token store breaks under horizontal scaling
|
||||
* because each pod has its own memory. This implementation is stateless:
|
||||
*
|
||||
* - Token payload: { ts, nonce }
|
||||
* - Signature: HMAC_SHA256(secret, `${clientId}|${ts}|${nonce}`)
|
||||
*
|
||||
* As long as all pods share the same `CSRF_SECRET`, any pod can validate
|
||||
* any token without shared state (works on Kubernetes).
|
||||
*/
|
||||
|
||||
let cachedCsrfSecret: Buffer | null = null;
|
||||
const getCsrfSecret = (): Buffer => {
|
||||
if (cachedCsrfSecret) return cachedCsrfSecret;
|
||||
@@ -577,9 +562,7 @@ const getCsrfSecret = (): Buffer => {
|
||||
return cachedCsrfSecret;
|
||||
}
|
||||
|
||||
// If not configured, generate an ephemeral secret for this process.
|
||||
// This keeps single-instance deployments working out of the box, but:
|
||||
// - Horizontal scaling will BREAK unless CSRF_SECRET is set and shared.
|
||||
// Fallback for local/single-instance setups.
|
||||
cachedCsrfSecret = crypto.randomBytes(32);
|
||||
const envLabel = process.env.NODE_ENV ? ` (${process.env.NODE_ENV})` : "";
|
||||
console.warn(
|
||||
@@ -609,9 +592,7 @@ const base64UrlDecode = (input: string): Buffer => {
|
||||
};
|
||||
|
||||
type CsrfTokenPayload = {
|
||||
/** Issued-at timestamp (ms since epoch) */
|
||||
ts: number;
|
||||
/** Random nonce (base64url) */
|
||||
nonce: string;
|
||||
};
|
||||
|
||||
@@ -621,10 +602,6 @@ const signCsrfToken = (clientId: string, payload: CsrfTokenPayload): Buffer => {
|
||||
return crypto.createHmac("sha256", secret).update(data, "utf8").digest();
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a new CSRF token for a client
|
||||
* Returns the token to be sent to the client
|
||||
*/
|
||||
export const createCsrfToken = (clientId: string): string => {
|
||||
const payload: CsrfTokenPayload = {
|
||||
ts: Date.now(),
|
||||
@@ -638,10 +615,6 @@ export const createCsrfToken = (clientId: string): string => {
|
||||
return `${payloadB64}.${sigB64}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate a CSRF token for a client
|
||||
* Uses timing-safe comparison to prevent timing attacks
|
||||
*/
|
||||
export const validateCsrfToken = (clientId: string, token: string): boolean => {
|
||||
if (!token || typeof token !== "string") {
|
||||
return false;
|
||||
@@ -688,9 +661,6 @@ export const validateCsrfToken = (clientId: string, token: string): boolean => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Revoke a CSRF token (e.g., on logout or token refresh)
|
||||
*/
|
||||
export const revokeCsrfToken = (clientId: string): void => {
|
||||
// Stateless CSRF tokens cannot be selectively revoked without shared state.
|
||||
// If revocation is required, implement token blacklisting in a shared store
|
||||
|
||||
Reference in New Issue
Block a user