Use handlers for idNot method

This commit is contained in:
Sosthene 2025-09-07 23:24:26 +02:00
parent c07591a97a
commit e8cb97b6c6
3 changed files with 297 additions and 116 deletions

View File

@ -1,89 +1,121 @@
import { Request, Response } from 'express';
import fetch from 'node-fetch'; import fetch from 'node-fetch';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { IdNotService } from '../services/idnot'; import { IdNotService } from '../services/idnot';
import { authTokens } from '../utils/auth-tokens'; import { authTokens } from '../utils/auth-tokens';
import { IdNotUser, AuthToken } from '../types'; 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 { export class IdNotController {
static async getUserRattachements(req: Request, res: Response): Promise<any> {
const { idNot } = req.query;
const json = await IdNotService.getUserRattachements(idNot as string); /**
* Get user rattachements by idNot
*/
static async getUserRattachements(idNot: string): Promise<any[]> {
Logger.info('Getting user rattachements', { idNot });
// if json.result.length is 0, return 404 const json = await IdNotService.getUserRattachements(idNot);
if (json.result.length === 0) {
return res.status(404).json({ // Check if any rattachements found
success: false, if (!json.result || json.result.length === 0) {
message: 'No rattachements found' 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 officeData = await Promise.all(json.result.map(async (result: any) => {
const searchParams = new URLSearchParams({ const searchParams = new URLSearchParams({
key: process.env.IDNOT_API_KEY || '', key: process.env.IDNOT_API_KEY || '',
deleted: 'false' deleted: 'false'
}); });
const officeData = await ( try {
await fetch(`${process.env.IDNOT_ANNUARY_BASE_URL}${result.entiteUrl}?` + searchParams, { const response = await fetch(`${process.env.IDNOT_ANNUARY_BASE_URL}${result.entiteUrl}?` + searchParams, {
method: 'GET' 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; * Get office rattachements by office idNot
*/
const json = await IdNotService.getOfficeRattachements(idNot as string); static async getOfficeRattachements(idNot: string): Promise<any> {
Logger.info('Getting office rattachements', { idNot });
res.json(json);
}
static async authenticate(req: Request, res: Response): Promise<any> {
const code = req.params.code;
try { 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 tokens = await IdNotService.exchangeCodeForTokens(code);
const jwt = tokens.id_token; const jwt = tokens.id_token;
if (!jwt) { if (!jwt) {
console.error('jwt not defined'); throw new BusinessRuleError('No ID token received from IdNot');
return;
} }
// Decode JWT payload
const payload = JSON.parse(Buffer.from(jwt.split('.')[1], 'base64').toString('utf8')); const payload = JSON.parse(Buffer.from(jwt.split('.')[1], 'base64').toString('utf8'));
let userData: any; // Get user data
try { const userData = await IdNotService.getUserData(payload.profile_idn);
userData = await IdNotService.getUserData(payload.profile_idn);
} catch (error) {
console.error('Error fetching user data:', error);
return;
}
if (!userData || !userData.statutDuRattachement || userData.entite.typeEntite.name !== 'office') { if (!userData || !userData.statutDuRattachement || userData.entite.typeEntite.name !== 'office') {
console.error('User not attached to an office (May be a partner)'); throw new BusinessRuleError('User not attached to an office');
return;
} }
let officeLocationData: any; // Get office location data
try { const officeLocationData = await IdNotService.getOfficeLocationData(userData.entite.locationsUrl);
officeLocationData = await IdNotService.getOfficeLocationData(userData.entite.locationsUrl);
} catch (error) {
console.error('Error fetching office location data:', error);
return;
}
if (!officeLocationData || !officeLocationData.result || officeLocationData.result.length === 0) { if (!officeLocationData || !officeLocationData.result || officeLocationData.result.length === 0) {
console.error('Office location data not found'); throw new BusinessRuleError('Office location data not found');
return;
} }
// Build IdNotUser object
const idNotUser: IdNotUser = { const idNotUser: IdNotUser = {
idNot: payload.sub, idNot: payload.sub,
office: { office: {
@ -111,89 +143,105 @@ export class IdNotController {
}; };
if (!idNotUser.contact.email) { if (!idNotUser.contact.email) {
console.error('User pro email empty'); throw new BusinessRuleError('User professional email is empty');
return;
} }
// Generate auth token
const authToken = uuidv4(); const authToken = uuidv4();
const tokenData: AuthToken = { const tokenData: AuthToken = {
idNot: idNotUser.idNot, idNot: idNotUser.idNot,
authToken, authToken,
idNotUser: idNotUser, // Store the full user data idNotUser: idNotUser,
pairingId: null, // To be set on a separate call pairingId: null,
defaultStorage: null, // To be set on a separate call defaultStorage: null,
createdAt: Date.now(), createdAt: Date.now(),
expiresAt: Date.now() + (24 * 60 * 60 * 1000) // 24 hours expiresAt: Date.now() + (24 * 60 * 60 * 1000) // 24 hours
}; };
authTokens.push(tokenData); authTokens.push(tokenData);
res.json({ idNotUser, authToken }); Logger.info('IdNot authentication successful', {
} catch (error: any) { idNot: idNotUser.idNot,
res.status(500).json({ office: idNotUser.office.name
error: 'Internal Server Error',
message: error.message
}); });
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'}`);
} }
} }
static async getCurrentUser(req: Request, res: Response): Promise<any> { /**
console.log('Received request to get user data'); * Get current user data by idNot and authToken
try { */
// Find the full token data which should contain the original idNotUser data static async getCurrentUser(idNot: string, authToken: string): Promise<{ success: boolean; data: IdNotUser }> {
const userAuth = authTokens.find(auth => auth.authToken === req.idNotUser!.authToken); Logger.info('Getting current user data', { idNot });
// Find the full token data
const userAuth = authTokens.find(auth => auth.authToken === authToken);
if (!userAuth || !userAuth.idNotUser) { if (!userAuth || !userAuth.idNotUser) {
// If we don't have the stored user data, we need to re-fetch it throw new NotFoundError('User data not found. Please log in again.');
// 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 Logger.info('Current user data retrieved', {
res.json({ idNot,
office: userAuth.idNotUser.office.name
});
return {
success: true, success: true,
data: userAuth.idNotUser 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 { * 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 // Remove the auth token from the array
const tokenIndex = authTokens.findIndex(auth => auth.authToken === req.idNotUser!.authToken); const tokenIndex = authTokens.findIndex(auth => auth.authToken === authToken);
if (tokenIndex > -1) { if (tokenIndex > -1) {
authTokens.splice(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');
} }
res.json({ return {
success: true, success: true,
message: 'Déconnexion réussie' message: 'Déconnexion réussie'
}); };
} catch (error: any) {
res.status(500).json({
success: false,
message: 'Erreur lors de la déconnexion',
error: error.message
});
}
} }
static validateToken(req: Request, res: Response) { /**
res.json({ * 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, success: true,
message: 'Token valide', message: 'Token valide',
data: { data: {
idNot: req.idNotUser!.idNot, idNot,
valid: true valid: true
} }
}); };
} }
} }

View File

@ -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<void> => {
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<void> => {
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<void> => {
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<void> => {
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<void> => {
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<void> => {
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);
});
}

View File

@ -1,17 +1,15 @@
import { Router } from 'express'; import { Router } from 'express';
import { IdNotController } from '../controllers/idnot.controller'; import { IdNotHandlers } from '../handlers/idnot.handlers';
import { authenticateIdNot } from '../middleware/auth'; import { authenticateIdNot } from '../middleware/auth';
const router = Router(); const router = Router();
// Public routes router.get('/user/rattachements', IdNotHandlers.getUserRattachements);
router.get('/user/rattachements', IdNotController.getUserRattachements); router.get('/office/rattachements', IdNotHandlers.getOfficeRattachements);
router.get('/office/rattachements', IdNotController.getOfficeRattachements); router.post('/auth/:code', IdNotHandlers.authenticate);
router.post('/auth/:code', IdNotController.authenticate);
// Protected routes router.get('/user', authenticateIdNot, IdNotHandlers.getCurrentUser);
router.get('/user', authenticateIdNot, IdNotController.getCurrentUser); router.post('/logout', authenticateIdNot, IdNotHandlers.logout);
router.post('/logout', authenticateIdNot, IdNotController.logout); router.get('/validate', authenticateIdNot, IdNotHandlers.validateToken);
router.get('/validate', authenticateIdNot, IdNotController.validateToken);
export { router as idnotRoutes }; export { router as idnotRoutes };