website poc

This commit is contained in:
2025-12-02 00:19:49 +01:00
parent 7330ccd82d
commit 747a77cb39
42 changed files with 8772 additions and 241 deletions

View 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
View 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);
}

View 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'
);
}