← back to Wine Finder Next

lib/auth.ts

341 lines

// Authentication and Authorization System
import crypto from 'crypto';

export interface User {
  id: string;
  email: string;
  passwordHash: string;
  role: 'admin' | 'member' | 'guest';
  walletAddress?: string;
  emailVerified: boolean;
  twoFactorEnabled: boolean;
  createdAt: Date;
  lastLogin?: Date;
}

export interface Session {
  id: string;
  userId: string;
  token: string;
  expiresAt: Date;
  ipAddress: string;
  userAgent: string;
  createdAt: Date;
}

// In-memory stores (replace with database in production)
const users: Map<string, User> = new Map();
const sessions: Map<string, Session> = new Map();
const rateLimitMap: Map<string, { count: number; resetAt: number }> = new Map();

// Security configuration
const SECURITY_CONFIG = {
  sessionDuration: 24 * 60 * 60 * 1000, // 24 hours
  maxLoginAttempts: 5,
  lockoutDuration: 15 * 60 * 1000, // 15 minutes
  rateLimitWindow: 60 * 1000, // 1 minute
  rateLimitMax: 100, // 100 requests per minute
  bcryptRounds: 12,
  jwtSecret: process.env.JWT_SECRET || 'change-me-in-production-2025',
  passwordMinLength: 12,
  requireSpecialChar: true,
  requireNumber: true,
  requireUppercase: true
};

// Hash password using crypto (in production, use bcrypt)
export function hashPassword(password: string): string {
  const salt = crypto.randomBytes(16).toString('hex');
  const hash = crypto.pbkdf2Sync(password, salt, 10000, 64, 'sha512').toString('hex');
  return `${salt}:${hash}`;
}

// Verify password
export function verifyPassword(password: string, storedHash: string): boolean {
  const [salt, hash] = storedHash.split(':');
  const verifyHash = crypto.pbkdf2Sync(password, salt, 10000, 64, 'sha512').toString('hex');
  return hash === verifyHash;
}

// Validate password strength
export function validatePassword(password: string): { valid: boolean; errors: string[] } {
  const errors: string[] = [];

  if (password.length < SECURITY_CONFIG.passwordMinLength) {
    errors.push(`Password must be at least ${SECURITY_CONFIG.passwordMinLength} characters`);
  }

  if (SECURITY_CONFIG.requireSpecialChar && !/[!@#$%^&*(),.?":{}|<>]/.test(password)) {
    errors.push('Password must contain at least one special character');
  }

  if (SECURITY_CONFIG.requireNumber && !/\d/.test(password)) {
    errors.push('Password must contain at least one number');
  }

  if (SECURITY_CONFIG.requireUppercase && !/[A-Z]/.test(password)) {
    errors.push('Password must contain at least one uppercase letter');
  }

  if (/[A-Z]/.test(password) && !/[a-z]/.test(password)) {
    errors.push('Password must contain at least one lowercase letter');
  }

  return { valid: errors.length === 0, errors };
}

// Generate secure random token
export function generateToken(): string {
  return crypto.randomBytes(32).toString('hex');
}

// Create session
export function createSession(userId: string, ipAddress: string, userAgent: string): Session {
  const session: Session = {
    id: generateToken(),
    userId,
    token: generateToken(),
    expiresAt: new Date(Date.now() + SECURITY_CONFIG.sessionDuration),
    ipAddress,
    userAgent,
    createdAt: new Date()
  };

  sessions.set(session.token, session);
  return session;
}

// Verify session
export function verifySession(token: string): { valid: boolean; session?: Session; user?: User } {
  const session = sessions.get(token);

  if (!session) {
    return { valid: false };
  }

  if (new Date() > session.expiresAt) {
    sessions.delete(token);
    return { valid: false };
  }

  const user = users.get(session.userId);
  if (!user) {
    sessions.delete(token);
    return { valid: false };
  }

  return { valid: true, session, user };
}

// Rate limiting
export function checkRateLimit(identifier: string): { allowed: boolean; remaining: number } {
  const now = Date.now();
  const record = rateLimitMap.get(identifier);

  if (!record || now > record.resetAt) {
    rateLimitMap.set(identifier, {
      count: 1,
      resetAt: now + SECURITY_CONFIG.rateLimitWindow
    });
    return { allowed: true, remaining: SECURITY_CONFIG.rateLimitMax - 1 };
  }

  if (record.count >= SECURITY_CONFIG.rateLimitMax) {
    return { allowed: false, remaining: 0 };
  }

  record.count++;
  return { allowed: true, remaining: SECURITY_CONFIG.rateLimitMax - record.count };
}

// Input sanitization
export function sanitizeInput(input: string): string {
  return input
    .replace(/[<>]/g, '') // Remove HTML tags
    .replace(/javascript:/gi, '') // Remove javascript: protocol
    .replace(/on\w+=/gi, '') // Remove event handlers
    .trim();
}

// Validate email
export function validateEmail(email: string): boolean {
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  return emailRegex.test(email);
}

// Validate wallet address (Ethereum format)
export function validateWalletAddress(address: string): boolean {
  return /^0x[a-fA-F0-9]{40}$/.test(address);
}

// SQL Injection prevention (for parameterized queries)
export function escapeSQL(value: any): string {
  if (typeof value === 'string') {
    return value.replace(/'/g, "''");
  }
  return String(value);
}

// XSS prevention
export function escapeHTML(str: string): string {
  const map: { [key: string]: string } = {
    '&': '&amp;',
    '<': '&lt;',
    '>': '&gt;',
    '"': '&quot;',
    "'": '&#x27;',
    '/': '&#x2F;',
  };
  return str.replace(/[&<>"'/]/g, (char) => map[char]);
}

// CSRF token generation and validation
const csrfTokens: Map<string, { token: string; expiresAt: Date }> = new Map();

export function generateCSRFToken(sessionId: string): string {
  const token = generateToken();
  csrfTokens.set(sessionId, {
    token,
    expiresAt: new Date(Date.now() + 60 * 60 * 1000) // 1 hour
  });
  return token;
}

export function verifyCSRFToken(sessionId: string, token: string): boolean {
  const stored = csrfTokens.get(sessionId);
  if (!stored) return false;
  if (new Date() > stored.expiresAt) {
    csrfTokens.delete(sessionId);
    return false;
  }
  return stored.token === token;
}

// Audit logging
export interface AuditLog {
  id: string;
  userId?: string;
  action: string;
  resource: string;
  ipAddress: string;
  userAgent: string;
  timestamp: Date;
  success: boolean;
  metadata?: any;
}

const auditLogs: AuditLog[] = [];

export function logAudit(log: Omit<AuditLog, 'id' | 'timestamp'>): void {
  auditLogs.push({
    ...log,
    id: generateToken(),
    timestamp: new Date()
  });

  // In production, send to logging service
  console.log('[AUDIT]', JSON.stringify(log));
}

// Get audit logs
export function getAuditLogs(filters?: {
  userId?: string;
  action?: string;
  startDate?: Date;
  endDate?: Date;
}): AuditLog[] {
  let logs = auditLogs;

  if (filters?.userId) {
    logs = logs.filter(l => l.userId === filters.userId);
  }

  if (filters?.action) {
    logs = logs.filter(l => l.action === filters.action);
  }

  if (filters?.startDate) {
    logs = logs.filter(l => l.timestamp >= filters.startDate!);
  }

  if (filters?.endDate) {
    logs = logs.filter(l => l.timestamp <= filters.endDate!);
  }

  return logs.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
}

// Initialize admin user
export function initializeAdminUser(): void {
  const adminEmail = process.env.ADMIN_EMAIL || 'admin@winedao.com';
  const adminPassword = process.env.ADMIN_PASSWORD || 'ChangeMe2025!@#$';

  if (!users.has(adminEmail)) {
    const adminUser: User = {
      id: 'admin_1',
      email: adminEmail,
      passwordHash: hashPassword(adminPassword),
      role: 'admin',
      emailVerified: true,
      twoFactorEnabled: false,
      createdAt: new Date()
    };

    users.set(adminEmail, adminUser);
    console.log('[SECURITY] Admin user initialized:', adminEmail);
    console.log('[SECURITY] CHANGE DEFAULT PASSWORD IMMEDIATELY');
  }
}

// Export user management functions
export const userDb = {
  create: (email: string, password: string, role: User['role'] = 'member'): User | null => {
    if (users.has(email)) {
      return null;
    }

    const passwordValidation = validatePassword(password);
    if (!passwordValidation.valid) {
      throw new Error(passwordValidation.errors.join(', '));
    }

    const user: User = {
      id: generateToken(),
      email,
      passwordHash: hashPassword(password),
      role,
      emailVerified: false,
      twoFactorEnabled: false,
      createdAt: new Date()
    };

    users.set(email, user);
    return user;
  },

  authenticate: (email: string, password: string): User | null => {
    const user = users.get(email);
    if (!user) {
      return null;
    }

    if (!verifyPassword(password, user.passwordHash)) {
      return null;
    }

    user.lastLogin = new Date();
    return user;
  },

  getByEmail: (email: string): User | undefined => {
    return users.get(email);
  },

  getById: (id: string): User | undefined => {
    return Array.from(users.values()).find(u => u.id === id);
  }
};

// Initialize on module load
initializeAdminUser();