MVP ready to test
This commit is contained in:
@@ -19,14 +19,15 @@ export function getAuthCookieOptions() {
|
||||
|
||||
/**
|
||||
* Get cookie options for CSRF tokens
|
||||
* Note: httpOnly is false so client-side JavaScript can read the token
|
||||
* Note: httpOnly is false so the client can read it, but we verify via double-submit pattern
|
||||
*/
|
||||
export function getCsrfCookieOptions() {
|
||||
return {
|
||||
httpOnly: false, // Client needs to read this token
|
||||
httpOnly: false, // Client needs to read this token for the header
|
||||
secure: isProduction, // HTTPS only in production
|
||||
sameSite: 'lax' as const,
|
||||
maxAge: 60 * 60 * 24, // 24 hours
|
||||
path: '/', // Available on all paths
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,8 @@ const rateLimitStore = new Map<string, RateLimitEntry>();
|
||||
// Cleanup old entries every 5 minutes
|
||||
setInterval(() => {
|
||||
const now = Date.now();
|
||||
for (const [key, entry] of rateLimitStore.entries()) {
|
||||
const entries = Array.from(rateLimitStore.entries());
|
||||
for (const [key, entry] of entries) {
|
||||
if (entry.resetAt < now) {
|
||||
rateLimitStore.delete(key);
|
||||
}
|
||||
@@ -108,6 +109,7 @@ export function getClientIdentifier(request: Request): string {
|
||||
* Predefined rate limit configurations
|
||||
*/
|
||||
export const RateLimits = {
|
||||
// Auth endpoints
|
||||
// Login: 5 attempts per 15 minutes
|
||||
LOGIN: {
|
||||
name: 'login',
|
||||
@@ -122,17 +124,98 @@ export const RateLimits = {
|
||||
windowSeconds: 60 * 60,
|
||||
},
|
||||
|
||||
// API: 100 requests per minute
|
||||
API: {
|
||||
name: 'api',
|
||||
maxRequests: 100,
|
||||
windowSeconds: 60,
|
||||
},
|
||||
|
||||
// Password reset: 3 attempts per hour
|
||||
PASSWORD_RESET: {
|
||||
name: 'password-reset',
|
||||
maxRequests: 3,
|
||||
windowSeconds: 60 * 60,
|
||||
},
|
||||
|
||||
// QR Code endpoints
|
||||
// Create QR: 20 per minute
|
||||
QR_CREATE: {
|
||||
name: 'qr-create',
|
||||
maxRequests: 20,
|
||||
windowSeconds: 60,
|
||||
},
|
||||
|
||||
// Modify QR: 30 per minute
|
||||
QR_MODIFY: {
|
||||
name: 'qr-modify',
|
||||
maxRequests: 30,
|
||||
windowSeconds: 60,
|
||||
},
|
||||
|
||||
// Delete all QRs: 3 per hour
|
||||
QR_DELETE_ALL: {
|
||||
name: 'qr-delete-all',
|
||||
maxRequests: 3,
|
||||
windowSeconds: 60 * 60,
|
||||
},
|
||||
|
||||
// Bulk create: 3 per hour
|
||||
BULK_CREATE: {
|
||||
name: 'bulk-create',
|
||||
maxRequests: 3,
|
||||
windowSeconds: 60 * 60,
|
||||
},
|
||||
|
||||
// User settings endpoints
|
||||
// Profile update: 10 per minute
|
||||
PROFILE_UPDATE: {
|
||||
name: 'profile-update',
|
||||
maxRequests: 10,
|
||||
windowSeconds: 60,
|
||||
},
|
||||
|
||||
// Password change: 5 per hour
|
||||
PASSWORD_CHANGE: {
|
||||
name: 'password-change',
|
||||
maxRequests: 5,
|
||||
windowSeconds: 60 * 60,
|
||||
},
|
||||
|
||||
// Account delete: 2 per day
|
||||
ACCOUNT_DELETE: {
|
||||
name: 'account-delete',
|
||||
maxRequests: 2,
|
||||
windowSeconds: 24 * 60 * 60,
|
||||
},
|
||||
|
||||
// Analytics endpoints
|
||||
// Analytics summary: 30 per minute
|
||||
ANALYTICS: {
|
||||
name: 'analytics',
|
||||
maxRequests: 30,
|
||||
windowSeconds: 60,
|
||||
},
|
||||
|
||||
// Stripe endpoints
|
||||
// Checkout session: 5 per minute
|
||||
STRIPE_CHECKOUT: {
|
||||
name: 'stripe-checkout',
|
||||
maxRequests: 5,
|
||||
windowSeconds: 60,
|
||||
},
|
||||
|
||||
// Customer portal: 10 per minute
|
||||
STRIPE_PORTAL: {
|
||||
name: 'stripe-portal',
|
||||
maxRequests: 10,
|
||||
windowSeconds: 60,
|
||||
},
|
||||
|
||||
// Cancel subscription: 3 per hour
|
||||
STRIPE_CANCEL: {
|
||||
name: 'stripe-cancel',
|
||||
maxRequests: 3,
|
||||
windowSeconds: 60 * 60,
|
||||
},
|
||||
|
||||
// General API: 100 requests per minute
|
||||
API: {
|
||||
name: 'api',
|
||||
maxRequests: 100,
|
||||
windowSeconds: 60,
|
||||
},
|
||||
};
|
||||
|
||||
174
src/lib/validationSchemas.ts
Normal file
174
src/lib/validationSchemas.ts
Normal file
@@ -0,0 +1,174 @@
|
||||
/**
|
||||
* Zod Validation Schemas for API endpoints
|
||||
* Centralized validation logic for type-safety and security
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
|
||||
// ==========================================
|
||||
// QR Code Schemas
|
||||
// ==========================================
|
||||
|
||||
export const qrStyleSchema = z.object({
|
||||
fgColor: z.string().regex(/^#[0-9A-F]{6}$/i, 'Invalid foreground color format').optional(),
|
||||
bgColor: z.string().regex(/^#[0-9A-F]{6}$/i, 'Invalid background color format').optional(),
|
||||
cornerStyle: z.enum(['square', 'rounded']).optional(),
|
||||
size: z.number().min(100).max(1000).optional(),
|
||||
});
|
||||
|
||||
export const createQRSchema = z.object({
|
||||
title: z.string()
|
||||
.min(1, 'Title is required')
|
||||
.max(100, 'Title must be less than 100 characters'),
|
||||
|
||||
content: z.record(z.any()), // Accept any object structure for content
|
||||
|
||||
isStatic: z.boolean().optional(),
|
||||
|
||||
contentType: z.enum(['URL', 'WIFI', 'EMAIL', 'PHONE', 'SMS', 'WHATSAPP', 'TEXT'], {
|
||||
errorMap: () => ({ message: 'Invalid content type' })
|
||||
}),
|
||||
|
||||
tags: z.array(z.string()).optional(),
|
||||
|
||||
style: z.object({
|
||||
foregroundColor: z.string().optional(),
|
||||
backgroundColor: z.string().optional(),
|
||||
cornerStyle: z.enum(['square', 'rounded']).optional(),
|
||||
size: z.number().optional(),
|
||||
}).optional(),
|
||||
});
|
||||
|
||||
export const updateQRSchema = z.object({
|
||||
title: z.string()
|
||||
.min(1, 'Title is required')
|
||||
.max(100, 'Title must be less than 100 characters')
|
||||
.optional(),
|
||||
|
||||
content: z.string()
|
||||
.min(1, 'Content is required')
|
||||
.max(5000, 'Content must be less than 5000 characters')
|
||||
.optional(),
|
||||
|
||||
style: qrStyleSchema.optional(),
|
||||
|
||||
isActive: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export const bulkQRSchema = z.object({
|
||||
qrs: z.array(
|
||||
z.object({
|
||||
title: z.string().min(1).max(100),
|
||||
content: z.string().min(1).max(5000),
|
||||
contentType: z.enum(['URL', 'WIFI', 'EMAIL', 'PHONE', 'SMS', 'WHATSAPP', 'TEXT']),
|
||||
})
|
||||
).min(1, 'At least one QR code is required')
|
||||
.max(100, 'Maximum 100 QR codes per bulk creation'),
|
||||
});
|
||||
|
||||
// ==========================================
|
||||
// Authentication Schemas
|
||||
// ==========================================
|
||||
|
||||
export const loginSchema = z.object({
|
||||
email: z.string()
|
||||
.email('Invalid email format')
|
||||
.toLowerCase(),
|
||||
|
||||
password: z.string()
|
||||
.min(1, 'Password is required'),
|
||||
});
|
||||
|
||||
export const signupSchema = z.object({
|
||||
name: z.string()
|
||||
.min(2, 'Name must be at least 2 characters')
|
||||
.max(100, 'Name must be less than 100 characters')
|
||||
.trim(),
|
||||
|
||||
email: z.string()
|
||||
.email('Invalid email format')
|
||||
.toLowerCase()
|
||||
.trim(),
|
||||
|
||||
password: z.string()
|
||||
.min(8, 'Password must be at least 8 characters')
|
||||
.max(100, 'Password must be less than 100 characters'),
|
||||
// Password complexity rules removed for easier testing
|
||||
});
|
||||
|
||||
export const forgotPasswordSchema = z.object({
|
||||
email: z.string()
|
||||
.email('Invalid email format')
|
||||
.toLowerCase()
|
||||
.trim(),
|
||||
});
|
||||
|
||||
export const resetPasswordSchema = z.object({
|
||||
token: z.string().min(1, 'Reset token is required'),
|
||||
password: z.string()
|
||||
.min(8, 'Password must be at least 8 characters')
|
||||
.max(100, 'Password must be less than 100 characters'),
|
||||
// Password complexity rules removed for easier testing
|
||||
});
|
||||
|
||||
// ==========================================
|
||||
// Settings Schemas
|
||||
// ==========================================
|
||||
|
||||
export const updateProfileSchema = z.object({
|
||||
name: z.string()
|
||||
.min(2, 'Name must be at least 2 characters')
|
||||
.max(100, 'Name must be less than 100 characters')
|
||||
.trim(),
|
||||
});
|
||||
|
||||
export const changePasswordSchema = z.object({
|
||||
currentPassword: z.string()
|
||||
.min(1, 'Current password is required'),
|
||||
|
||||
newPassword: z.string()
|
||||
.min(8, 'Password must be at least 8 characters')
|
||||
.max(100, 'Password must be less than 100 characters'),
|
||||
// Password complexity rules removed for easier testing
|
||||
});
|
||||
|
||||
// ==========================================
|
||||
// Stripe Schemas
|
||||
// ==========================================
|
||||
|
||||
export const createCheckoutSchema = z.object({
|
||||
priceId: z.string().min(1, 'Price ID is required'),
|
||||
});
|
||||
|
||||
// ==========================================
|
||||
// Helper: Format Zod Errors
|
||||
// ==========================================
|
||||
|
||||
export function formatZodError(error: z.ZodError) {
|
||||
return {
|
||||
error: 'Validation failed',
|
||||
details: error.errors.map(err => ({
|
||||
field: err.path.join('.'),
|
||||
message: err.message,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Helper: Validate with Zod
|
||||
// ==========================================
|
||||
|
||||
export async function validateRequest<T>(
|
||||
schema: z.ZodSchema<T>,
|
||||
data: unknown
|
||||
): Promise<{ success: true; data: T } | { success: false; error: any }> {
|
||||
try {
|
||||
const validatedData = schema.parse(data);
|
||||
return { success: true, data: validatedData };
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
return { success: false, error: formatZodError(error) };
|
||||
}
|
||||
return { success: false, error: { error: 'Invalid request data' } };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user