project-standalo-todo-super/app/lib/auth.ts

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