Use handlers for idNot method
This commit is contained in:
parent
c07591a97a
commit
e8cb97b6c6
@ -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
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
135
src/handlers/idnot.handlers.ts
Normal file
135
src/handlers/idnot.handlers.ts
Normal 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);
|
||||||
|
});
|
||||||
|
}
|
@ -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 };
|
||||||
|
Loading…
x
Reference in New Issue
Block a user