161 lines
3.8 KiB
TypeScript
161 lines
3.8 KiB
TypeScript
/**
|
|
* 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<string> {
|
|
// 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<boolean> {
|
|
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<User, 'passwordHash'> {
|
|
const { passwordHash, ...safeUser } = user;
|
|
return safeUser;
|
|
}
|