website poc
This commit is contained in:
53
apps/website/lib/email-validation.ts
Normal file
53
apps/website/lib/email-validation.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
/**
|
||||
* Email validation schema using Zod
|
||||
*/
|
||||
export const emailSchema = z.string()
|
||||
.email('Invalid email format')
|
||||
.min(3, 'Email too short')
|
||||
.max(254, 'Email too long')
|
||||
.toLowerCase()
|
||||
.trim();
|
||||
|
||||
/**
|
||||
* Validates an email address
|
||||
* @param email - The email address to validate
|
||||
* @returns Validation result with sanitized email or error
|
||||
*/
|
||||
export function validateEmail(email: string): {
|
||||
success: boolean;
|
||||
email?: string;
|
||||
error?: string;
|
||||
} {
|
||||
const result = emailSchema.safeParse(email);
|
||||
|
||||
if (result.success) {
|
||||
return {
|
||||
success: true,
|
||||
email: result.data,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: result.error.errors[0]?.message || 'Invalid email',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if email appears to be from a disposable email service
|
||||
* Basic check - can be extended with comprehensive list
|
||||
*/
|
||||
const DISPOSABLE_DOMAINS = new Set([
|
||||
'tempmail.com',
|
||||
'throwaway.email',
|
||||
'guerrillamail.com',
|
||||
'mailinator.com',
|
||||
'10minutemail.com',
|
||||
]);
|
||||
|
||||
export function isDisposableEmail(email: string): boolean {
|
||||
const domain = email.split('@')[1]?.toLowerCase();
|
||||
return domain ? DISPOSABLE_DOMAINS.has(domain) : false;
|
||||
}
|
||||
75
apps/website/lib/mode.ts
Normal file
75
apps/website/lib/mode.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* Mode detection system for GridPilot website
|
||||
*
|
||||
* Controls whether the site shows pre-launch content or full platform
|
||||
* Based on GRIDPILOT_MODE environment variable
|
||||
*/
|
||||
|
||||
export type AppMode = 'pre-launch' | 'post-launch';
|
||||
|
||||
const VALID_MODES: readonly AppMode[] = ['pre-launch', 'post-launch'] as const;
|
||||
|
||||
/**
|
||||
* Get the current application mode from environment variable
|
||||
* Defaults to 'pre-launch' if not set or invalid
|
||||
*
|
||||
* @throws {Error} If mode is set but invalid (development only)
|
||||
* @returns {AppMode} The current application mode
|
||||
*/
|
||||
export function getAppMode(): AppMode {
|
||||
const mode = process.env.GRIDPILOT_MODE;
|
||||
|
||||
if (!mode) {
|
||||
return 'pre-launch';
|
||||
}
|
||||
|
||||
if (!isValidMode(mode)) {
|
||||
const validModes = VALID_MODES.join(', ');
|
||||
const error = `Invalid GRIDPILOT_MODE: "${mode}". Must be one of: ${validModes}`;
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
throw new Error(error);
|
||||
}
|
||||
|
||||
console.error(error);
|
||||
return 'pre-launch';
|
||||
}
|
||||
|
||||
return mode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if a string is a valid AppMode
|
||||
*/
|
||||
function isValidMode(mode: string): mode is AppMode {
|
||||
return VALID_MODES.includes(mode as AppMode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if currently in pre-launch mode
|
||||
*/
|
||||
export function isPreLaunch(): boolean {
|
||||
return getAppMode() === 'pre-launch';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if currently in post-launch mode
|
||||
*/
|
||||
export function isPostLaunch(): boolean {
|
||||
return getAppMode() === 'post-launch';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of public routes that are always accessible
|
||||
*/
|
||||
export function getPublicRoutes(): readonly string[] {
|
||||
return ['/', '/api/signup'] as const;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a route is public (accessible in all modes)
|
||||
*/
|
||||
export function isPublicRoute(pathname: string): boolean {
|
||||
const publicRoutes = getPublicRoutes();
|
||||
return publicRoutes.includes(pathname);
|
||||
}
|
||||
89
apps/website/lib/rate-limit.ts
Normal file
89
apps/website/lib/rate-limit.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { kv } from '@vercel/kv';
|
||||
|
||||
/**
|
||||
* Rate limit configuration
|
||||
*/
|
||||
const RATE_LIMIT_WINDOW = 60 * 60 * 1000; // 1 hour in milliseconds
|
||||
const MAX_REQUESTS_PER_WINDOW = 5;
|
||||
|
||||
/**
|
||||
* Rate limit key prefix
|
||||
*/
|
||||
const RATE_LIMIT_PREFIX = 'ratelimit:signup:';
|
||||
|
||||
/**
|
||||
* Check if an IP address has exceeded rate limits
|
||||
* @param identifier - IP address or unique identifier
|
||||
* @returns Object with allowed status and retry information
|
||||
*/
|
||||
export async function checkRateLimit(identifier: string): Promise<{
|
||||
allowed: boolean;
|
||||
remaining: number;
|
||||
resetAt: number;
|
||||
}> {
|
||||
const key = `${RATE_LIMIT_PREFIX}${identifier}`;
|
||||
const now = Date.now();
|
||||
|
||||
try {
|
||||
// Get current count
|
||||
const count = await kv.get<number>(key) || 0;
|
||||
|
||||
if (count >= MAX_REQUESTS_PER_WINDOW) {
|
||||
// Get TTL to determine reset time
|
||||
const ttl = await kv.ttl(key);
|
||||
const resetAt = now + (ttl * 1000);
|
||||
|
||||
return {
|
||||
allowed: false,
|
||||
remaining: 0,
|
||||
resetAt,
|
||||
};
|
||||
}
|
||||
|
||||
// Increment counter
|
||||
const newCount = count + 1;
|
||||
|
||||
if (count === 0) {
|
||||
// First request - set with expiry
|
||||
await kv.set(key, newCount, {
|
||||
px: RATE_LIMIT_WINDOW,
|
||||
});
|
||||
} else {
|
||||
// Subsequent request - increment without changing TTL
|
||||
await kv.incr(key);
|
||||
}
|
||||
|
||||
// Calculate reset time
|
||||
const ttl = await kv.ttl(key);
|
||||
const resetAt = now + (ttl * 1000);
|
||||
|
||||
return {
|
||||
allowed: true,
|
||||
remaining: MAX_REQUESTS_PER_WINDOW - newCount,
|
||||
resetAt,
|
||||
};
|
||||
} catch (error) {
|
||||
// If rate limiting fails, allow the request
|
||||
console.error('Rate limit check failed:', error);
|
||||
return {
|
||||
allowed: true,
|
||||
remaining: MAX_REQUESTS_PER_WINDOW,
|
||||
resetAt: now + RATE_LIMIT_WINDOW,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get client IP from request headers
|
||||
*/
|
||||
export function getClientIp(request: Request): string {
|
||||
// Try various headers that might contain the IP
|
||||
const headers = request.headers;
|
||||
|
||||
return (
|
||||
headers.get('x-forwarded-for')?.split(',')[0]?.trim() ||
|
||||
headers.get('x-real-ip') ||
|
||||
headers.get('cf-connecting-ip') || // Cloudflare
|
||||
'unknown'
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user