← 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 } = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
'/': '/',
};
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();