import fetch from 'node-fetch'; import { v4 as uuidv4 } from 'uuid'; import { IdNotService } from '../services/idnot'; import { authTokens } from '../utils/auth-tokens'; import { IdNotUser, AuthToken } from '../types'; import { Logger } from '../utils/logger'; import { NotFoundError, ExternalServiceError, BusinessRuleError } from '../types/errors'; /** * Pure controller methods that handle business logic * without depending on Express Request/Response objects */ export class IdNotController { /** * Get user rattachements by idNot */ static async getUserRattachements(idNot: string): Promise { Logger.info('Getting user rattachements', { idNot }); const json = await IdNotService.getUserRattachements(idNot); // Check if any rattachements found if (!json.result || json.result.length === 0) { throw new NotFoundError('No rattachements found'); } // Get office data for each rattachement const officeData = await Promise.all(json.result.map(async (result: any) => { const searchParams = new URLSearchParams({ key: process.env.IDNOT_API_KEY || '', deleted: 'false' }); try { const response = await fetch(`${process.env.IDNOT_ANNUARY_BASE_URL}${result.entiteUrl}?` + searchParams, { method: 'GET' }); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } return await response.json(); } catch (error) { Logger.error('Failed to fetch office data', { entiteUrl: result.entiteUrl, error: error instanceof Error ? error.message : 'Unknown error' }); throw new ExternalServiceError('IdNot', `Failed to fetch office data: ${error instanceof Error ? error.message : 'Unknown error'}`); } })); Logger.info('Successfully retrieved user rattachements', { idNot, count: officeData.length }); return officeData; } /** * Get office rattachements by office idNot */ static async getOfficeRattachements(idNot: string): Promise { Logger.info('Getting office rattachements', { idNot }); try { const result = await IdNotService.getOfficeRattachements(idNot); Logger.info('Successfully retrieved office rattachements', { idNot, count: result.result?.length || 0 }); return result; } catch (error) { Logger.error('Failed to get office rattachements', { idNot, error: error instanceof Error ? error.message : 'Unknown error' }); throw new ExternalServiceError('IdNot', `Failed to get office rattachements: ${error instanceof Error ? error.message : 'Unknown error'}`); } } /** * Authenticate user with authorization code */ static async authenticate(code: string): Promise<{ idNotUser: IdNotUser; authToken: string }> { Logger.info('IdNot authentication initiated', { codePrefix: code.substring(0, 8) + '...' }); try { // Exchange code for tokens const tokens = await IdNotService.exchangeCodeForTokens(code); const jwt = tokens.id_token; if (!jwt) { throw new BusinessRuleError('No ID token received from IdNot'); } // Decode JWT payload const payload = JSON.parse(Buffer.from(jwt.split('.')[1], 'base64').toString('utf8')); // Get user data const userData = await IdNotService.getUserData(payload.profile_idn); if (!userData || !userData.statutDuRattachement || userData.entite.typeEntite.name !== 'office') { throw new BusinessRuleError('User not attached to an office'); } // Get office location data const officeLocationData = await IdNotService.getOfficeLocationData(userData.entite.locationsUrl); if (!officeLocationData || !officeLocationData.result || officeLocationData.result.length === 0) { throw new BusinessRuleError('Office location data not found'); } // Build IdNotUser object const idNotUser: IdNotUser = { idNot: payload.sub, office: { idNot: payload.entity_idn, name: userData.entite.denominationSociale ?? userData.entite.codeCrpcen, crpcen: userData.entite.codeCrpcen, office_status: IdNotService.getOfficeStatus(userData.entite.statutEntite.name), address: { address: officeLocationData.result[0].adrGeo4, city: officeLocationData.result[0].adrGeoVille.split(' ')[0] ?? officeLocationData.result[0].adrGeoVille, zip_code: Number(officeLocationData.result[0].adrGeoCodePostal) }, status: 'ACTIVE' }, role: IdNotService.getRole(userData.typeLien.name), contact: { first_name: userData.personne.prenom, last_name: userData.personne.nomUsuel, email: userData.mailRattachement, phone_number: userData.numeroTelephone, cell_phone_number: userData.numeroMobile ?? userData.numeroTelephone, civility: IdNotService.getCivility(userData.personne.civilite) }, office_role: IdNotService.getOfficeRole(userData.typeLien.name) }; if (!idNotUser.contact.email) { throw new BusinessRuleError('User professional email is empty'); } // Generate auth token const authToken = uuidv4(); const tokenData: AuthToken = { idNot: idNotUser.idNot, authToken, idNotUser: idNotUser, pairingId: null, defaultStorage: null, createdAt: Date.now(), expiresAt: Date.now() + (24 * 60 * 60 * 1000) // 24 hours }; authTokens.push(tokenData); Logger.info('IdNot authentication successful', { idNot: idNotUser.idNot, office: idNotUser.office.name }); return { idNotUser, authToken }; } catch (error) { Logger.error('IdNot authentication failed', { codePrefix: code.substring(0, 8) + '...', error: error instanceof Error ? error.message : 'Unknown error' }); if (error instanceof BusinessRuleError || error instanceof ExternalServiceError) { throw error; } throw new ExternalServiceError('IdNot', `Authentication failed: ${error instanceof Error ? error.message : 'Unknown error'}`); } } /** * Get current user data by idNot and authToken */ static async getCurrentUser(idNot: string, authToken: string): Promise<{ success: boolean; data: IdNotUser }> { Logger.info('Getting current user data', { idNot }); // Find the full token data const userAuth = authTokens.find(auth => auth.authToken === authToken); if (!userAuth || !userAuth.idNotUser) { throw new NotFoundError('User data not found. Please log in again.'); } Logger.info('Current user data retrieved', { idNot, office: userAuth.idNotUser.office.name }); return { success: true, data: userAuth.idNotUser }; } /** * Logout user by removing auth token */ static async logout(authToken: string): Promise<{ success: boolean; message: string }> { Logger.info('User logout initiated'); // Remove the auth token from the array const tokenIndex = authTokens.findIndex(auth => auth.authToken === authToken); if (tokenIndex > -1) { const removedToken = authTokens.splice(tokenIndex, 1)[0]; Logger.info('User logout successful', { idNot: removedToken.idNot }); } else { Logger.warn('Logout attempted with invalid token'); } return { success: true, message: 'Déconnexion réussie' }; } /** * Validate if a token is still valid */ static async validateToken(idNot: string): Promise<{ success: boolean; message: string; data: { idNot: string; valid: boolean } }> { Logger.debug('Token validation requested', { idNot }); return { success: true, message: 'Token valide', data: { idNot, valid: true } }; } }