import { fetchDataFromTable, insertDataIntoTable, updateDataInTable } from '../../db/supabaseHelper'; import { createLogger } from '../../utils/logger'; import { v4 as uuidv4 } from 'uuid'; import { supabase } from '../../db/supabase'; import { BotContext } from '../types/botTypes'; import { getBotIdFromContext } from '../../utils/botUtils'; const logger = createLogger('AuthService'); interface UserBotTelegraf { id?: number; // SERIAL in SQL becomes number in TypeScript telegramId: number; // BIGINT in SQL becomes number in TypeScript username?: string | null; // VARCHAR(255), optional firstName?: string | null; // VARCHAR(255), matches SQL column name lastName?: string | null; // VARCHAR(255), matches SQL column name email: string; // VARCHAR(255) NOT NULL passwordHash: string; // VARCHAR(255) NOT NULL, matches SQL column name language?: string; // VARCHAR(10) DEFAULT 'en' role?: string; // VARCHAR(10) DEFAULT 'user' balance?: number; // DECIMAL(10, 2) DEFAULT 0 isBanned?: boolean; // BOOLEAN DEFAULT false lastLogin?: Date | string | null; // TIMESTAMPTZ (can be Date, ISO string, or null) botId?: string | null; // UUID (string in TypeScript) createdAt?: Date | string; // TIMESTAMPTZ DEFAULT NOW() updatedAt?: Date | string; // TIMESTAMPTZ DEFAULT NOW() } export class AuthService { private static instance: AuthService; private constructor() { // Private constructor for singleton pattern } public static getInstance(): AuthService { if (!AuthService.instance) { AuthService.instance = new AuthService(); } return AuthService.instance; } private generatePassword(): string { return uuidv4().replace(/-/g, '').substring(0, 12); } public async hashPassword(password: string): Promise { const encoder = new TextEncoder(); const data = encoder.encode(password); const hashBuffer = await crypto.subtle.digest('SHA-256', data); const hashArray = Array.from(new Uint8Array(hashBuffer)); return hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); } private generateEmail(telegramId: number, ctx: BotContext): string { const botData = ctx.botData; const suffix = botData?.suffix_email || 'saerosms.com'; return `user_${telegramId}@${suffix}`; } public async createUser(telegramId: number, name: string, ctx: BotContext): Promise<{ user: UserBotTelegraf; password: string }> { try { const botId = getBotIdFromContext(ctx); if (!botId) { throw new Error('Could not determine bot ID from context'); } const email = this.generateEmail(telegramId, ctx); const password = this.generatePassword(); const passwordHash = await this.hashPassword(password); const userData: Omit = { telegramId: telegramId, firstName: name, email, passwordHash: passwordHash, botId: botId, balance: 0, language: 'ar', role: 'user', isBanned: false }; const user = await insertDataIntoTable('users_bot_telegram', userData); logger.info(`User ${telegramId} created successfully for bot ${botId}`); return { user, password }; } catch (error: any) { logger.error(`Error creating user ${telegramId}: ${error.message}`); throw new Error(`Failed to create user: ${error.message}`); } } private loggedInUsers: Map = new Map(); private generateUserKey(telegramId: number, ctx: BotContext): string { const botId = getBotIdFromContext(ctx); if (!botId) { throw new Error('Could not determine bot ID from context'); } return `${telegramId}_${botId}`; } public setUserLoggedIn(telegramId: number, ctx: BotContext, isLoggedIn: boolean = true): void { const key = this.generateUserKey(telegramId, ctx); const botId = getBotIdFromContext(ctx); this.loggedInUsers.set(key, isLoggedIn); logger.info(`User ${telegramId} for bot ${botId} login state set to ${isLoggedIn}`); } public isUserLoggedIn(telegramId: number, ctx: BotContext): boolean { const key = this.generateUserKey(telegramId, ctx); return this.loggedInUsers.get(key) || false; } public async loginUser(telegramId: number, ctx: BotContext): Promise { try { const botId = getBotIdFromContext(ctx); if (!botId) { throw new Error('Could not determine bot ID from context'); } const { data: user, error } = await supabase .from('users_bot_telegram') .select('*') .eq('telegram_id', telegramId) .eq('bot_id', botId) .single(); if (error) { if (error.code === 'PGRST116') { return null; // User not found } throw error; } if (user) { this.setUserLoggedIn(telegramId, ctx, true); return user; } return null; } catch (error) { logger.error('Error in loginUser:', error); return null; } } public async getUserById(userId: string): Promise { try { const { data: user, error } = await supabase .from('users_bot_telegram') .select('*') .eq('id', userId) .single(); if (error) { if (error.code === 'PGRST116') { return null; } throw error; } return user; } catch (error: any) { logger.error(`Error fetching user ${userId}: ${error.message}`); throw new Error(`Failed to get user: ${error.message}`); } } async getUserByTelegramId(telegramId: number, ctx: BotContext): Promise { try { const botId = getBotIdFromContext(ctx); if (!botId) { throw new Error('Could not determine bot ID from context'); } const { data: user, error } = await supabase .from('users_bot_telegram') .select('*') .eq('telegram_id', telegramId) .eq('bot_id', botId) .single(); if (error) { if (error.code === 'PGRST116') { return null; } throw error; } return user; } catch (error: any) { logger.error(`Error getting user by Telegram ID: ${error.message}`); return null; } } async updateUserBalance(telegramId: number, ctx: BotContext, newBalance: number): Promise { try { const botId = getBotIdFromContext(ctx); if (!botId) { throw new Error('Could not determine bot ID from context'); } const { error } = await supabase .from('users_bot_telegram') .update({ balance: newBalance }) .eq('telegram_id', telegramId) .eq('bot_id', botId); if (error) throw error; return true; } catch (error: any) { logger.error(`Error updating user balance: ${error.message}`); return false; } } } // Singleton instance export const authService = AuthService.getInstance();