/** * Authentication utilities for user management and token handling */ import { User } from './types'; import { findUser } from './db/store'; // Simple password hashing for MVP (use bcrypt in production) export async function hashPassword(password: string): Promise { // For MVP, use base64 encoding with a salt // In production, use bcrypt or similar const salt = 'app-salt-2024'; const combined = `${salt}:${password}`; // Use browser-compatible encoding if (typeof btoa !== 'undefined') { return btoa(combined); } // Node.js environment return Buffer.from(combined).toString('base64'); } export async function verifyPassword( password: string, hash: string ): Promise { const expectedHash = await hashPassword(password); return expectedHash === hash; } // Simple JWT-like token generation interface TokenPayload { userId: string; exp: number; } export function generateToken(userId: string): string { const payload: TokenPayload = { userId, exp: Date.now() + 7 * 24 * 60 * 60 * 1000 // 7 days from now }; const tokenData = JSON.stringify(payload); // Use browser-compatible encoding if (typeof btoa !== 'undefined') { return btoa(tokenData); } // Node.js environment return Buffer.from(tokenData).toString('base64'); } export function verifyToken(token: string): { userId: string } | null { try { let decoded: string; // Use browser-compatible decoding if (typeof atob !== 'undefined') { decoded = atob(token); } else { // Node.js environment decoded = Buffer.from(token, 'base64').toString('utf-8'); } const payload: TokenPayload = JSON.parse(decoded); // Check expiration if (payload.exp < Date.now()) { return null; } return { userId: payload.userId }; } catch (error) { console.error('Token verification failed:', error); return null; } } export function getCurrentUser(request: Request): User | null { try { // Extract token from Authorization header const authHeader = request.headers.get('Authorization'); if (!authHeader || !authHeader.startsWith('Bearer ')) { return null; } const token = authHeader.substring(7); // Remove 'Bearer ' prefix const payload = verifyToken(token); if (!payload) { return null; } const user = findUser(payload.userId); return user || null; } catch (error) { console.error('getCurrentUser failed:', error); return null; } } // Middleware helper for protected routes export function requireAuth(request: Request): User { const user = getCurrentUser(request); if (!user) { throw new Error('Unauthorized'); } return user; } // Extract user ID from request without throwing export function getUserId(request: Request): string | null { const user = getCurrentUser(request); return user?.id || null; } // Validate email format export function isValidEmail(email: string): boolean { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return emailRegex.test(email); } // Validate password strength export function isValidPassword(password: string): { valid: boolean; errors: string[]; } { const errors: string[] = []; if (password.length < 8) { errors.push('Password must be at least 8 characters long'); } if (!/[A-Z]/.test(password)) { errors.push('Password must contain at least one uppercase letter'); } if (!/[a-z]/.test(password)) { errors.push('Password must contain at least one lowercase letter'); } if (!/[0-9]/.test(password)) { errors.push('Password must contain at least one number'); } return { valid: errors.length === 0, errors }; } // Create a safe user object (without password hash) export function sanitizeUser(user: User): Omit { const { passwordHash, ...safeUser } = user; return safeUser; }