diff --git a/src/controllers/idnot.controller.ts b/src/controllers/idnot.controller.ts index 6fedd9f..a6611e5 100644 --- a/src/controllers/idnot.controller.ts +++ b/src/controllers/idnot.controller.ts @@ -1,89 +1,121 @@ -import { Request, Response } from 'express'; 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 { - static async getUserRattachements(req: Request, res: Response): Promise { - const { idNot } = req.query; + + /** + * Get user rattachements by idNot + */ + static async getUserRattachements(idNot: string): Promise { + Logger.info('Getting user rattachements', { idNot }); - const json = await IdNotService.getUserRattachements(idNot as string); + const json = await IdNotService.getUserRattachements(idNot); - // if json.result.length is 0, return 404 - if (json.result.length === 0) { - return res.status(404).json({ - success: false, - message: 'No rattachements found' - }); + // Check if any rattachements found + if (!json.result || json.result.length === 0) { + throw new NotFoundError('No rattachements found'); } - // Iterate over all results and get the office data by calling the entiteUrl endpoint + // 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' }); - const officeData = await ( - await fetch(`${process.env.IDNOT_ANNUARY_BASE_URL}${result.entiteUrl}?` + searchParams, { + try { + const response = await fetch(`${process.env.IDNOT_ANNUARY_BASE_URL}${result.entiteUrl}?` + searchParams, { method: 'GET' - }) - ).json(); - return officeData; + }); + + 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'}`); + } })); - res.json(officeData); + Logger.info('Successfully retrieved user rattachements', { + idNot, + count: officeData.length + }); + + return officeData; } - static async getOfficeRattachements(req: Request, res: Response) { - const { idNot } = req.query; - - const json = await IdNotService.getOfficeRattachements(idNot as string); - - res.json(json); - } - - static async authenticate(req: Request, res: Response): Promise { - const code = req.params.code; + /** + * 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) { - console.error('jwt not defined'); - return; + throw new BusinessRuleError('No ID token received from IdNot'); } + + // Decode JWT payload const payload = JSON.parse(Buffer.from(jwt.split('.')[1], 'base64').toString('utf8')); - let userData: any; - try { - userData = await IdNotService.getUserData(payload.profile_idn); - } catch (error) { - console.error('Error fetching user data:', error); - return; - } + // Get user data + const userData = await IdNotService.getUserData(payload.profile_idn); if (!userData || !userData.statutDuRattachement || userData.entite.typeEntite.name !== 'office') { - console.error('User not attached to an office (May be a partner)'); - return; + throw new BusinessRuleError('User not attached to an office'); } - let officeLocationData: any; - try { - officeLocationData = await IdNotService.getOfficeLocationData(userData.entite.locationsUrl); - } catch (error) { - console.error('Error fetching office location data:', error); - return; - } + // Get office location data + const officeLocationData = await IdNotService.getOfficeLocationData(userData.entite.locationsUrl); if (!officeLocationData || !officeLocationData.result || officeLocationData.result.length === 0) { - console.error('Office location data not found'); - return; + throw new BusinessRuleError('Office location data not found'); } + // Build IdNotUser object const idNotUser: IdNotUser = { idNot: payload.sub, office: { @@ -111,89 +143,105 @@ export class IdNotController { }; if (!idNotUser.contact.email) { - console.error('User pro email empty'); - return; + throw new BusinessRuleError('User professional email is empty'); } + // Generate auth token const authToken = uuidv4(); const tokenData: AuthToken = { idNot: idNotUser.idNot, authToken, - idNotUser: idNotUser, // Store the full user data - pairingId: null, // To be set on a separate call - defaultStorage: null, // To be set on a separate call + idNotUser: idNotUser, + pairingId: null, + defaultStorage: null, createdAt: Date.now(), expiresAt: Date.now() + (24 * 60 * 60 * 1000) // 24 hours }; + authTokens.push(tokenData); - res.json({ idNotUser, authToken }); - } catch (error: any) { - res.status(500).json({ - error: 'Internal Server Error', - message: error.message + Logger.info('IdNot authentication successful', { + idNot: idNotUser.idNot, + office: idNotUser.office.name }); - } - } - static async getCurrentUser(req: Request, res: Response): Promise { - console.log('Received request to get user data'); - try { - // Find the full token data which should contain the original idNotUser data - const userAuth = authTokens.find(auth => auth.authToken === req.idNotUser!.authToken); + return { idNotUser, authToken }; + + } catch (error) { + Logger.error('IdNot authentication failed', { + codePrefix: code.substring(0, 8) + '...', + error: error instanceof Error ? error.message : 'Unknown error' + }); - if (!userAuth || !userAuth.idNotUser) { - // If we don't have the stored user data, we need to re-fetch it - // This requires decoding the original JWT or re-fetching from IdNot APIs - return res.status(404).json({ - success: false, - message: 'Données utilisateur non trouvées. Veuillez vous reconnecter.' - }); - } - - // Return the stored idNotUser data without the authToken - res.json({ - success: true, - data: userAuth.idNotUser - }); - } catch (error: any) { - res.status(500).json({ - success: false, - message: 'Erreur lors de la récupération des données utilisateur', - error: error.message - }); - } - } - - static logout(req: Request, res: Response) { - try { - // Remove the auth token from the array - const tokenIndex = authTokens.findIndex(auth => auth.authToken === req.idNotUser!.authToken); - if (tokenIndex > -1) { - authTokens.splice(tokenIndex, 1); + if (error instanceof BusinessRuleError || error instanceof ExternalServiceError) { + throw error; } - res.json({ - success: true, - message: 'Déconnexion réussie' - }); - } catch (error: any) { - res.status(500).json({ - success: false, - message: 'Erreur lors de la déconnexion', - error: error.message - }); + throw new ExternalServiceError('IdNot', `Authentication failed: ${error instanceof Error ? error.message : 'Unknown error'}`); } } - static validateToken(req: Request, res: Response) { - res.json({ + /** + * 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: req.idNotUser!.idNot, + idNot, valid: true } - }); + }; } } diff --git a/src/handlers/idnot.handlers.ts b/src/handlers/idnot.handlers.ts new file mode 100644 index 0000000..d79d214 --- /dev/null +++ b/src/handlers/idnot.handlers.ts @@ -0,0 +1,135 @@ +import { Request, Response } from 'express'; +import { IdNotController } from '../controllers/idnot.controller'; +import { asyncHandler } from '../middleware/error-handler'; +import { ValidationError, BusinessRuleError } from '../types/errors'; + +/** + * Route handlers that extract and validate HTTP request data + * before calling pure controller methods + */ +export class IdNotHandlers { + + /** + * GET /user/rattachements + * Extract idNot from query params and call controller + */ + static getUserRattachements = asyncHandler(async (req: Request, res: Response): Promise => { + const requestId = req.headers['x-request-id'] as string; + + // Extract and validate parameters + const { idNot } = req.query; + + if (!idNot || typeof idNot !== 'string') { + throw new ValidationError('idNot parameter is required', [ + { field: 'idNot', value: idNot, constraints: ['Must be a valid string'] } + ], requestId); + } + + // Call pure controller method with extracted parameters + const result = await IdNotController.getUserRattachements(idNot); + + res.json(result); + }); + + /** + * GET /office/rattachements + * Extract idNot from query params and call controller + */ + static getOfficeRattachements = asyncHandler(async (req: Request, res: Response): Promise => { + const requestId = req.headers['x-request-id'] as string; + + // Extract and validate parameters + const { idNot } = req.query; + + if (!idNot || typeof idNot !== 'string') { + throw new ValidationError('idNot parameter is required', [ + { field: 'idNot', value: idNot, constraints: ['Must be a valid string'] } + ], requestId); + } + + // Call pure controller method + const result = await IdNotController.getOfficeRattachements(idNot); + + res.json(result); + }); + + /** + * POST /auth/:code + * Extract code from URL params and call controller + */ + static authenticate = asyncHandler(async (req: Request, res: Response): Promise => { + const requestId = req.headers['x-request-id'] as string; + + // Extract and validate parameters + const { code } = req.params; + + if (!code || typeof code !== 'string' || code.length < 10) { + throw new ValidationError('Invalid authentication code', [ + { field: 'code', value: code, constraints: ['Must be a valid authorization code'] } + ], requestId); + } + + // Call pure controller method + const result = await IdNotController.authenticate(code); + + res.json(result); + }); + + /** + * GET /user (protected) + * Extract user info from middleware and call controller + */ + static getCurrentUser = asyncHandler(async (req: Request, res: Response): Promise => { + const requestId = req.headers['x-request-id'] as string; + + // Extract user info (set by authenticateIdNot middleware) + if (!req.idNotUser) { + throw new BusinessRuleError('User authentication required', undefined, requestId); + } + + const { idNot, authToken } = req.idNotUser; + + // Call pure controller method + const result = await IdNotController.getCurrentUser(idNot, authToken); + + res.json(result); + }); + + /** + * POST /logout (protected) + * Extract user info and call controller + */ + static logout = asyncHandler(async (req: Request, res: Response): Promise => { + const requestId = req.headers['x-request-id'] as string; + + if (!req.idNotUser) { + throw new BusinessRuleError('User authentication required', undefined, requestId); + } + + const { authToken } = req.idNotUser; + + // Call pure controller method + const result = await IdNotController.logout(authToken); + + res.json(result); + }); + + /** + * GET /validate (protected) + * Extract user info and call controller + */ + static validateToken = asyncHandler(async (req: Request, res: Response): Promise => { + const requestId = req.headers['x-request-id'] as string; + + if (!req.idNotUser) { + throw new BusinessRuleError('User authentication required', undefined, requestId); + } + + const { idNot } = req.idNotUser; + + // Call pure controller method + const result = await IdNotController.validateToken(idNot); + + res.json(result); + }); +} diff --git a/src/routes/idnot.routes.ts b/src/routes/idnot.routes.ts index 6b6750f..596108d 100644 --- a/src/routes/idnot.routes.ts +++ b/src/routes/idnot.routes.ts @@ -1,17 +1,15 @@ import { Router } from 'express'; -import { IdNotController } from '../controllers/idnot.controller'; +import { IdNotHandlers } from '../handlers/idnot.handlers'; import { authenticateIdNot } from '../middleware/auth'; const router = Router(); -// Public routes -router.get('/user/rattachements', IdNotController.getUserRattachements); -router.get('/office/rattachements', IdNotController.getOfficeRattachements); -router.post('/auth/:code', IdNotController.authenticate); +router.get('/user/rattachements', IdNotHandlers.getUserRattachements); +router.get('/office/rattachements', IdNotHandlers.getOfficeRattachements); +router.post('/auth/:code', IdNotHandlers.authenticate); -// Protected routes -router.get('/user', authenticateIdNot, IdNotController.getCurrentUser); -router.post('/logout', authenticateIdNot, IdNotController.logout); -router.get('/validate', authenticateIdNot, IdNotController.validateToken); +router.get('/user', authenticateIdNot, IdNotHandlers.getCurrentUser); +router.post('/logout', authenticateIdNot, IdNotHandlers.logout); +router.get('/validate', authenticateIdNot, IdNotHandlers.validateToken); export { router as idnotRoutes };