All files / lib/auth login-events.ts

97.4% Statements 75/77
94.11% Branches 16/17
100% Functions 7/7
97.4% Lines 75/77

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 1421x             1x                                                       1x 19x 19x         12x 12x 12x 12x 12x 12x 12x 12x 12x 12x   12x 12x 12x 12x 12x 12x 12x 12x   12x 1x 1x 1x   11x 11x         1x     13x 13x 13x   13x   13x 13x         2x 2x 2x 2x 2x 2x 2x 2x 2x         2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x         4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x         4x 4x 2x 2x 2x 2x 2x 2x      
/**
 * Login Events Logging
 *
 * Tracks authentication events for security and auditing
 */
 
import { getAdminClient } from '@/lib/supabase/admin';
import { createHash } from 'crypto';
 
export type LoginEventType =
  | 'login_success'
  | 'login_failed'
  | 'logout'
  | 'otp_sent'
  | 'otp_verified'
  | 'otp_failed'
  | 'magic_link_sent'
  | 'magic_link_verified'
  | 'password_reset_requested'
  | 'password_changed'
  | 'session_refreshed';
 
export type AuthMethod = 'password' | 'magic_link' | 'phone_otp' | 'oauth' | 'system';
 
export interface LoginEventMetadata {
  provider?: string;
  error_code?: string;
  error_message?: string;
  country?: string;
  [key: string]: unknown;
}
 
/**
 * Hash IP address for privacy
 */
export function hashIP(ip: string): string {
  return createHash('sha256').update(ip).digest('hex').substring(0, 16);
}
 
/**
 * Log a login event
 */
export async function logLoginEvent(
  userId: string | null,
  eventType: LoginEventType,
  authMethod: AuthMethod,
  ipAddress: string,
  userAgent?: string,
  metadata?: LoginEventMetadata
): Promise<string | null> {
  const adminClient = getAdminClient();
  const ipHash = hashIP(ipAddress);
 
  const { data, error } = await adminClient.rpc('log_login_event', {
    p_user_id: userId,
    p_event_type: eventType,
    p_auth_method: authMethod,
    p_ip_hash: ipHash,
    p_user_agent: userAgent || null,
    p_metadata: metadata || {},
  });
 
  if (error) {
    console.error('Login event logging error:', error);
    return null;
  }
 
  return data;
}
 
/**
 * Get client info from request headers
 */
export function getClientInfo(headers: Headers): {
  ipAddress: string;
  userAgent: string;
} {
  const ipAddress =
    headers.get('x-forwarded-for')?.split(',')[0].trim() || headers.get('x-real-ip') || 'unknown';
 
  const userAgent = headers.get('user-agent') || 'unknown';
 
  return { ipAddress, userAgent };
}
 
/**
 * Log successful login
 */
export async function logSuccessfulLogin(
  userId: string,
  authMethod: AuthMethod,
  headers: Headers,
  metadata?: LoginEventMetadata
): Promise<void> {
  const { ipAddress, userAgent } = getClientInfo(headers);
  await logLoginEvent(userId, 'login_success', authMethod, ipAddress, userAgent, metadata);
}
 
/**
 * Log failed login attempt
 */
export async function logFailedLogin(
  userId: string | null,
  authMethod: AuthMethod,
  headers: Headers,
  errorMessage?: string
): Promise<void> {
  const { ipAddress, userAgent } = getClientInfo(headers);
  await logLoginEvent(userId, 'login_failed', authMethod, ipAddress, userAgent, {
    error_message: errorMessage,
  });
}
 
/**
 * Log OTP sent
 */
export async function logOtpSent(
  identifier: string,
  identifierType: 'phone' | 'email',
  headers: Headers
): Promise<void> {
  const { ipAddress, userAgent } = getClientInfo(headers);
  await logLoginEvent(null, 'otp_sent', 'phone_otp', ipAddress, userAgent, {
    identifier_type: identifierType,
    identifier_masked: maskIdentifier(identifier, identifierType),
  });
}
 
/**
 * Mask identifier for logging
 */
function maskIdentifier(identifier: string, type: 'phone' | 'email'): string {
  if (type === 'phone') {
    return identifier.slice(0, 4) + '****' + identifier.slice(-2);
  }
  if (type === 'email') {
    const [local, domain] = identifier.split('@');
    return local.slice(0, 2) + '***@' + domain;
  }
  return '***';
}