mirror of
https://github.com/kjanat/livedash-node.git
synced 2026-01-16 10:12:09 +01:00
feat: comprehensive security and architecture improvements
- Add Zod validation schemas with strong password requirements (12+ chars, complexity) - Implement rate limiting for authentication endpoints (registration, password reset) - Remove duplicate MetricCard component, consolidate to ui/metric-card.tsx - Update README.md to use pnpm commands consistently - Enhance authentication security with 12-round bcrypt hashing - Add comprehensive input validation for all API endpoints - Fix security vulnerabilities in user registration and password reset flows 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
134
lib/validation.ts
Normal file
134
lib/validation.ts
Normal file
@ -0,0 +1,134 @@
|
||||
import { z } from "zod";
|
||||
|
||||
// Password validation with strong requirements
|
||||
const passwordSchema = z
|
||||
.string()
|
||||
.min(12, "Password must be at least 12 characters long")
|
||||
.regex(/^(?=.*[a-z])/, "Password must contain at least one lowercase letter")
|
||||
.regex(/^(?=.*[A-Z])/, "Password must contain at least one uppercase letter")
|
||||
.regex(/^(?=.*\d)/, "Password must contain at least one number")
|
||||
.regex(
|
||||
/^(?=.*[@$!%*?&])/,
|
||||
"Password must contain at least one special character (@$!%*?&)"
|
||||
);
|
||||
|
||||
// Email validation
|
||||
const emailSchema = z
|
||||
.string()
|
||||
.email("Invalid email format")
|
||||
.max(255, "Email must be less than 255 characters")
|
||||
.toLowerCase();
|
||||
|
||||
// Company name validation
|
||||
const companyNameSchema = z
|
||||
.string()
|
||||
.min(1, "Company name is required")
|
||||
.max(100, "Company name must be less than 100 characters")
|
||||
.regex(/^[a-zA-Z0-9\s\-_.]+$/, "Company name contains invalid characters");
|
||||
|
||||
// User registration schema
|
||||
export const registerSchema = z.object({
|
||||
email: emailSchema,
|
||||
password: passwordSchema,
|
||||
company: companyNameSchema,
|
||||
});
|
||||
|
||||
// User login schema
|
||||
export const loginSchema = z.object({
|
||||
email: emailSchema,
|
||||
password: z.string().min(1, "Password is required"),
|
||||
});
|
||||
|
||||
// Password reset request schema
|
||||
export const forgotPasswordSchema = z.object({
|
||||
email: emailSchema,
|
||||
});
|
||||
|
||||
// Password reset schema
|
||||
export const resetPasswordSchema = z.object({
|
||||
token: z.string().min(1, "Reset token is required"),
|
||||
password: passwordSchema,
|
||||
});
|
||||
|
||||
// Session filter schema
|
||||
export const sessionFilterSchema = z.object({
|
||||
search: z.string().max(100).optional(),
|
||||
sentiment: z.enum(["POSITIVE", "NEUTRAL", "NEGATIVE"]).optional(),
|
||||
category: z
|
||||
.enum([
|
||||
"SCHEDULE_HOURS",
|
||||
"LEAVE_VACATION",
|
||||
"SICK_LEAVE_RECOVERY",
|
||||
"SALARY_COMPENSATION",
|
||||
"CONTRACT_HOURS",
|
||||
"ONBOARDING",
|
||||
"OFFBOARDING",
|
||||
"WORKWEAR_STAFF_PASS",
|
||||
"TEAM_CONTACTS",
|
||||
"PERSONAL_QUESTIONS",
|
||||
"ACCESS_LOGIN",
|
||||
"SOCIAL_QUESTIONS",
|
||||
"UNRECOGNIZED_OTHER",
|
||||
])
|
||||
.optional(),
|
||||
startDate: z.string().datetime().optional(),
|
||||
endDate: z.string().datetime().optional(),
|
||||
page: z.number().int().min(1).default(1),
|
||||
limit: z.number().int().min(1).max(100).default(20),
|
||||
});
|
||||
|
||||
// Company settings schema
|
||||
export const companySettingsSchema = z.object({
|
||||
name: companyNameSchema,
|
||||
csvUrl: z.string().url("Invalid CSV URL"),
|
||||
csvUsername: z.string().max(100).optional(),
|
||||
csvPassword: z.string().max(100).optional(),
|
||||
sentimentAlert: z.number().min(0).max(1).optional(),
|
||||
dashboardOpts: z.object({}).passthrough().optional(),
|
||||
});
|
||||
|
||||
// User management schema
|
||||
export const userUpdateSchema = z.object({
|
||||
email: emailSchema.optional(),
|
||||
role: z.enum(["ADMIN", "USER", "AUDITOR"]).optional(),
|
||||
password: passwordSchema.optional(),
|
||||
});
|
||||
|
||||
// Metrics query schema
|
||||
export const metricsQuerySchema = z.object({
|
||||
startDate: z.string().datetime().optional(),
|
||||
endDate: z.string().datetime().optional(),
|
||||
companyId: z.string().uuid().optional(),
|
||||
});
|
||||
|
||||
// Helper function to validate and sanitize input
|
||||
export function validateInput<T>(
|
||||
schema: z.ZodSchema<T>,
|
||||
data: unknown
|
||||
): { success: true; data: T } | { success: false; errors: string[] } {
|
||||
try {
|
||||
const result = schema.parse(data);
|
||||
return { success: true, data: result };
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
const errors = error.errors.map(
|
||||
(err) => `${err.path.join(".")}: ${err.message}`
|
||||
);
|
||||
return { success: false, errors };
|
||||
}
|
||||
return { success: false, errors: ["Invalid input"] };
|
||||
}
|
||||
}
|
||||
|
||||
// Rate limiting helper types
|
||||
export interface RateLimitConfig {
|
||||
windowMs: number;
|
||||
maxRequests: number;
|
||||
skipSuccessfulRequests?: boolean;
|
||||
}
|
||||
|
||||
export const rateLimitConfigs = {
|
||||
auth: { windowMs: 15 * 60 * 1000, maxRequests: 5 }, // 5 requests per 15 minutes
|
||||
registration: { windowMs: 60 * 60 * 1000, maxRequests: 3 }, // 3 registrations per hour
|
||||
api: { windowMs: 15 * 60 * 1000, maxRequests: 100 }, // 100 API requests per 15 minutes
|
||||
} as const;
|
||||
Reference in New Issue
Block a user