From 8462b99586efd4517080f884febbf0f558a3791c Mon Sep 17 00:00:00 2001 From: Sosthene Date: Sun, 7 Sep 2025 15:25:48 +0200 Subject: [PATCH] Conversion to typescript --- global.d.ts | 39 ++ package.json | 22 +- quick-test-rattachements.js | 159 ++++++ src/{database.js => database.ts} | 46 +- src/{server.js => server.ts} | 848 ++++++++++++++++++++++--------- src/types.ts | 180 +++++++ test-db-init.js | 27 + test-rattachements-endpoint.js | 223 ++++++++ tsconfig.json | 41 ++ 9 files changed, 1324 insertions(+), 261 deletions(-) create mode 100644 global.d.ts create mode 100755 quick-test-rattachements.js rename src/{database.js => database.ts} (62%) rename src/{server.js => server.ts} (59%) create mode 100644 src/types.ts create mode 100644 test-db-init.js create mode 100755 test-rattachements-endpoint.js create mode 100644 tsconfig.json diff --git a/global.d.ts b/global.d.ts new file mode 100644 index 0000000..9f94119 --- /dev/null +++ b/global.d.ts @@ -0,0 +1,39 @@ +// Global type declarations for packages without @types + +declare module 'ovh' { + interface OVHConfig { + appKey: string; + appSecret: string; + consumerKey: string; + } + + interface OVHClient { + request(method: string, path: string, params: any, callback: (error: any, result: any) => void): void; + } + + function ovh(config: OVHConfig): OVHClient; + export = ovh; +} + +declare module '@mailchimp/mailchimp_transactional' { + interface MailchimpMessage { + template_name: string; + template_content: any[]; + message: { + global_merge_vars: Array<{ name: string; content: string }>; + from_email: string; + from_name: string; + subject: string; + to: Array<{ email: string; type: string }>; + }; + } + + interface MailchimpClient { + messages: { + sendTemplate(message: MailchimpMessage): Promise; + }; + } + + function mailchimp(apiKey: string): MailchimpClient; + export = mailchimp; +} diff --git a/package.json b/package.json index 8e494bf..19acc49 100644 --- a/package.json +++ b/package.json @@ -2,10 +2,16 @@ "name": "lecoffre-back-mini", "version": "1.0.0", "description": "Mini serveur avec une route /api/ping", - "main": "src/server.js", + "main": "dist/server.js", "scripts": { - "start": "node src/server.js", - "dev": "nodemon src/server.js" + "build": "tsc", + "start": "node dist/server.js", + "dev": "ts-node src/server.ts", + "watch": "nodemon --exec ts-node src/server.ts", + "dev:js": "nodemon src/server.js", + "test:db": "npm run build && node test-db-init.js", + "test:rattachements": "node test-rattachements-endpoint.js", + "test:quick": "node quick-test-rattachements.js" }, "dependencies": { "@mailchimp/mailchimp_transactional": "^1.0.59", @@ -20,6 +26,14 @@ "uuid": "^11.1.0" }, "devDependencies": { - "nodemon": "^3.0.1" + "@types/cors": "^2.8.17", + "@types/express": "^4.17.21", + "@types/node": "^20.11.19", + "@types/node-fetch": "^2.6.11", + "@types/pg": "^8.11.0", + "@types/uuid": "^9.0.8", + "nodemon": "^3.0.1", + "ts-node": "^10.9.2", + "typescript": "^5.3.3" } } diff --git a/quick-test-rattachements.js b/quick-test-rattachements.js new file mode 100755 index 0000000..7243011 --- /dev/null +++ b/quick-test-rattachements.js @@ -0,0 +1,159 @@ +#!/usr/bin/env node + +const fetch = require('node-fetch'); +const { SDKSignerClient } = require('sdk-signer-client'); + +// Quick test configuration +const BASE_URL = 'http://localhost:8080'; +const ENDPOINT = '/api/v1/idnot/user/rattachements'; // Base endpoint, idnot will be added as path parameter + +const signerConfig = { + url: process.env.SIGNER_WS_URL || 'ws://localhost:9090', + apiKey: process.env.SIGNER_API_KEY || 'your-api-key-change-this', + timeout: 30000, + reconnectInterval: 5000, + maxReconnectAttempts: 3 +}; + +// Test with a specific IDNot +async function testWithIdNot(idNot) { + if (!idNot) { + console.log('💡 Usage: node quick-test-rattachements.js [idNot]'); + console.log(' Example: node quick-test-rattachements.js 12345'); + console.log(' Example: node quick-test-rattachements.js (no parameter to test without idNot)'); + console.log(' URL format: /api/v1/idnot/user/{idnot}/rattachements'); + return; + } + + console.log(`🆔 Testing with IDNot: ${idNot}`); + + // Build URL with path parameter + let url = `${BASE_URL}${ENDPOINT}`; + url += `?idNot=${encodeURIComponent(idNot)}`; + + console.log(`📍 URL: ${url}`); + console.log('=' .repeat(60)); + + try { + const response = await fetch(url, { + method: 'GET', + headers: { + 'Accept': 'application/json' + } + }); + + console.log(`📊 Status: ${response.status} ${response.statusText}`); + + const responseText = await response.text(); + console.log(`📄 Response length: ${responseText.length} characters`); + + let data; + try { + data = JSON.parse(responseText); + console.log('📋 Parsed JSON response:'); + console.log(JSON.stringify(data, null, 2)); + } catch (e) { + console.log('📋 Raw response (not JSON):'); + console.log(responseText); + return; + } + + for (const office of data) { + let officeRattachementsData = []; + // Now test the office rattachements + const officeRattachements = await fetch(`${BASE_URL}/api/v1/idnot/office/rattachements?idNot=${office.ou}`, { + method: 'GET', + headers: { + 'Accept': 'application/json' + } + }); + + console.log(`📊 Status: ${officeRattachements.status} ${officeRattachements.statusText}`); + + const officeRattachementsText = await officeRattachements.text(); + console.log(`📄 Response length: ${officeRattachementsText.length} characters`); + + try { + officeRattachementsData = JSON.parse(officeRattachementsText); + console.log('📋 Parsed JSON response:'); + console.log(JSON.stringify(officeRattachementsData, null, 2)); + } catch (e) { + console.log('📋 Raw response (not JSON):'); + console.log(officeRattachementsText); + return; + } + + // Now try to create a new process with all the users that have `activite` set to `En exercice` + const usersToAdd = officeRattachementsData.result.filter(user => user.activite === 'En exercice'); + console.log(`📋 Users to add: ${usersToAdd.length}`); + console.log(JSON.stringify(usersToAdd, null, 2)); + + // Probably the idnot number should be public so that caller can easily find the processId? + + // Caller can now create the office process with the following data + const processData = { + name: 'New Process', + description: 'New Process Description', + timestamp: new Date().toISOString(), + office: office.ou, + }; + + const privateFields = Object.keys(processData); + privateFields.splice(privateFields.indexOf('office'), 1); // Make office public data + + const roles = { + owner: { + members: usersToAdd.map(user => user.uid), + validation_rules: [ + { + quorum: 0.1, + fields: [...privateFields, 'roles', 'office'], + min_sig_member: 1, + }, + ], + storages: ["https://dev3.4nkweb.com/storage"] + }, + apophis: { + members: usersToAdd.map(user => user.uid), + validation_rules: [], + storages: [] + } + }; + } + + } catch (error) { + console.log(`💥 Error: ${error.message}`); + } +} + +// Main execution +async function main() { + const idNot = process.argv[2]; // Get IDNot from command line argument + + console.log('🚀 Quick Rattachements Endpoint Test'); + console.log('=' .repeat(60)); + + // Check if server is running + try { + const healthCheck = await fetch(`${BASE_URL}/api/v1/health`); + if (healthCheck.ok) { + console.log('✅ Server is running'); + } else { + console.log(`⚠️ Server responded but health check failed with status: ${healthCheck.status}`); + } + } catch (error) { + console.log('❌ Server is not responding'); + console.log('💡 Make sure to start your server first with: npm run dev'); + return; + } + + console.log(''); + await testWithIdNot(idNot); + + console.log('\n💡 Usage: node quick-test-rattachements.js [idNot]'); + console.log(' Example: node quick-test-rattachements.js 12345'); + console.log(' Example: node quick-test-rattachements.js (no parameter to test without idNot)'); + console.log(' URL format: /api/v1/idnot/user/{idnot}/rattachements'); +} + +main().catch(console.error); diff --git a/src/database.js b/src/database.ts similarity index 62% rename from src/database.js rename to src/database.ts index 0d4848f..97f78ce 100644 --- a/src/database.js +++ b/src/database.ts @@ -1,22 +1,24 @@ -const { Pool } = require('pg'); -require('dotenv').config(); +import { Pool, QueryResult, PoolConfig } from 'pg'; +import * as dotenv from 'dotenv'; + +dotenv.config(); /** * Configuration de la base de données PostgreSQL */ -const dbConfig = { +const dbConfig: PoolConfig = { user: process.env.DB_USER || 'postgres', host: process.env.DB_HOST || 'localhost', database: process.env.DB_NAME || 'prd', password: process.env.DB_PASSWORD || 'admin', - port: process.env.DB_PORT || 5432, + port: parseInt(process.env.DB_PORT || '5432'), // Configuration du pool de connexions - max: parseInt(process.env.DB_POOL_MAX) || 20, // Nombre maximum de connexions dans le pool - min: parseInt(process.env.DB_POOL_MIN) || 2, // Nombre minimum de connexions maintenues - idleTimeoutMillis: parseInt(process.env.DB_IDLE_TIMEOUT) || 30000, // Temps d'inactivité avant fermeture - connectionTimeoutMillis: parseInt(process.env.DB_CONNECTION_TIMEOUT) || 2000, // Timeout pour établir une connexion - acquireTimeoutMillis: parseInt(process.env.DB_ACQUIRE_TIMEOUT) || 60000, // Timeout pour acquérir une connexion + max: parseInt(process.env.DB_POOL_MAX || '20'), // Nombre maximum de connexions dans le pool + min: parseInt(process.env.DB_POOL_MIN || '2'), // Nombre minimum de connexions maintenues + idleTimeoutMillis: parseInt(process.env.DB_IDLE_TIMEOUT || '30000'), // Temps d'inactivité avant fermeture + connectionTimeoutMillis: parseInt(process.env.DB_CONNECTION_TIMEOUT || '2000'), // Timeout pour établir une connexion + // acquireTimeoutMillis: parseInt(process.env.DB_ACQUIRE_TIMEOUT || '60000'), // Timeout pour acquérir une connexion // Configuration SSL si nécessaire ssl: process.env.DB_SSL === 'true' ? { @@ -32,22 +34,22 @@ const pool = new Pool(dbConfig); /** * Gestionnaire d'erreur pour le pool */ -pool.on('error', (err) => { +pool.on('error', (err: Error) => { console.error('PostgreSQL Error:', err); }); /** * Classe pour gérer les opérations de base de données */ -class Database { +export class Database { /** * Exécute une requête SQL avec des paramètres - * @param {string} text - La requête SQL - * @param {Array} params - Les paramètres de la requête - * @returns {Promise} - Le résultat de la requête + * @param text - La requête SQL + * @param params - Les paramètres de la requête + * @returns Promise - Le résultat de la requête */ - static async query(text, params) { + static async query(text: string, params?: any[]): Promise { try { return await pool.query(text, params); } catch (error) { @@ -58,14 +60,14 @@ class Database { /** * Teste la connexion à la base de données - * @returns {Promise} - True si la connexion est réussie + * @returns Promise - True si la connexion est réussie */ - static async testConnection() { + static async testConnection(): Promise { try { const result = await this.query('SELECT NOW() as current_time'); console.log('Database connection successful:', result.rows[0].current_time); return true; - } catch (error) { + } catch (error: any) { console.error('Database connection failed:', error.message); return false; } @@ -73,9 +75,9 @@ class Database { /** * Ferme toutes les connexions du pool - * @returns {Promise} + * @returns Promise */ - static async close() { + static async close(): Promise { try { await pool.end(); console.log('PostgreSQL connection pool closed'); @@ -99,7 +101,3 @@ process.on('SIGTERM', async () => { await Database.close(); process.exit(0); }); - -module.exports = { - Database -}; diff --git a/src/server.js b/src/server.ts similarity index 59% rename from src/server.js rename to src/server.ts index 1f6db64..0f26205 100644 --- a/src/server.js +++ b/src/server.ts @@ -1,54 +1,51 @@ -const express = require('express'); -const cors = require('cors'); -const fetch = require('node-fetch'); -const { v4: uuidv4 } = require('uuid'); -const ovh = require('ovh'); -const mailchimp = require('@mailchimp/mailchimp_transactional'); -const Stripe = require('stripe'); -const { Database } = require('./database'); -require('dotenv').config(); +import express, { Request, Response, NextFunction } from 'express'; +import cors from 'cors'; +import fetch from 'node-fetch'; +import { v4 as uuidv4 } from 'uuid'; +import ovh = require('ovh'); +import mailchimp = require('@mailchimp/mailchimp_transactional'); +import Stripe from 'stripe'; +import { Database } from './database'; +import { SDKSignerClient, ClientConfig, ServerResponse } from 'sdk-signer-client'; +import * as dotenv from 'dotenv'; +import { + ECivility, + EOfficeStatus, + EIdnotRole, + ETemplates, + IdNotUser, + AuthToken, + Session, + VerificationCode, + SmsConfig, + EmailConfig, + StripeConfig, + Subscription, + PendingEmail, + ProcessRoles, + ProcessData, + ProcessInfo +} from './types'; + +dotenv.config(); // Initialisation de l'application Express const app = express(); const PORT = process.env.PORT || 8080; +const DEFAULT_STORAGE = process.env.DEFAULT_STORAGE || 'https://dev3.4nkweb.com/storage'; // Configuration CORS -const corsOptions = { - origin: ['http://local.lecoffreio.4nkweb:3000', 'http://localhost:3000', 'https://lecoffreio.4nkweb.com'], +app.use(cors({ + origin: 'http://localhost:3000', methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], - allowedHeaders: ['Content-Type', 'Authorization'] -}; - -app.use(cors(corsOptions)); + allowedHeaders: ['Content-Type', 'x-session-id', 'Authorization'], + credentials: true +})); app.use(express.json()); -const authTokens = []; +const authTokens: AuthToken[] = []; -const ECivility = { - MALE: 'MALE', - FEMALE: 'FEMALE', - OTHERS: 'OTHERS' -}; - -const EOfficeStatus = { - ACTIVATED: 'ACTIVATED', - DESACTIVATED: 'DESACTIVATED' -}; - -const EIdnotRole = { - DIRECTEUR: "Directeur général du CSN", - NOTAIRE_TITULAIRE: "Notaire titulaire", - NOTAIRE_ASSOCIE: "Notaire associé", - NOTAIRE_SALARIE: "Notaire salarié", - COLLABORATEUR: "Collaborateur", - SECRETAIRE_GENERAL: "Secrétaire général", - SUPPLEANT: "Suppléant", - ADMINISTRATEUR: "Administrateur", - RESPONSABLE: "Responsable", - CURATEUR: "Curateur", -} - -function getOfficeStatus(statusName) { +function getOfficeStatus(statusName: string): EOfficeStatus { switch (statusName) { case "Pourvu": return EOfficeStatus.ACTIVATED; @@ -65,7 +62,7 @@ function getOfficeStatus(statusName) { } } -function getOfficeRole(roleName) { +function getOfficeRole(roleName: string): { name: string } | null { switch (roleName) { case EIdnotRole.NOTAIRE_TITULAIRE: return { name: 'Notaire' }; @@ -86,7 +83,7 @@ function getOfficeRole(roleName) { } } -function getRole(roleName) { +function getRole(roleName: string): { name: string } { switch (roleName) { case EIdnotRole.NOTAIRE_TITULAIRE: return { name: 'admin' }; @@ -107,7 +104,7 @@ function getRole(roleName) { } } -function getCivility(civility) { +function getCivility(civility: string): ECivility { switch (civility) { case 'Monsieur': return ECivility.MALE; @@ -118,15 +115,15 @@ function getCivility(civility) { } } -app.get('/api/v1/health', (req, res) => { +app.get('/api/v1/health', (req: Request, res: Response) => { res.json({ message: 'OK' }); }); -app.get('/api/v1/db/:tableName', async (req, res) => { +app.get('/api/v1/db/:tableName', async (req: Request, res: Response): Promise => { try { const { tableName } = req.params; - const page = parseInt(req.query.page) || 1; - const limit = parseInt(req.query.limit) || 10; + const page = parseInt(req.query.page as string) || 1; + const limit = parseInt(req.query.limit as string) || 10; const offset = (page - 1) * limit; // Validation du nom de table pour éviter les injections SQL @@ -142,12 +139,12 @@ app.get('/api/v1/db/:tableName', async (req, res) => { const total = parseInt(countResult.rows[0].total); if (tableName === 'rules_groups') { - const rulesGroups = await Promise.all((await Database.query(`SELECT * FROM ${tableName} ORDER BY 1 LIMIT $1 OFFSET $2`, [limit, offset])).rows.map(async (ruleGroup) => { - const result = await Database.query(`SELECT a.* FROM rules AS a JOIN "_RulesGroupsHasRules" as b ON b."A" = a.uid AND b."B" = '${ruleGroup.uid}';`); + const rulesGroups = await Promise.all((await Database.query(`SELECT * FROM ${tableName} ORDER BY 1 LIMIT $1 OFFSET $2`, [limit, offset])).rows.map(async (ruleGroup: any) => { + const result = await Database.query(`SELECT a.* FROM rules AS a JOIN "_RulesGroupsHasRules" as b ON b."A" = a.uid AND b."B" = $1`, [ruleGroup.uid]); return { uid: ruleGroup.uid, name: ruleGroup.name, - rules: result.rows.map((rule) => { + rules: result.rows.map((rule: any) => { return { uid: rule.uid } @@ -172,15 +169,15 @@ app.get('/api/v1/db/:tableName', async (req, res) => { timestamp: new Date().toISOString() }); } else if (tableName === 'office_roles') { - const officeRoles = await Promise.all((await Database.query(`SELECT * FROM ${tableName} ORDER BY 1 LIMIT $1 OFFSET $2`, [limit, offset])).rows.map(async (officeRole) => { - const result = await Database.query(`SELECT a.* FROM rules AS a JOIN "_OfficeRolesHasRules" as b ON b."B" = a.uid AND b."A" = '${officeRole.uid}';`); + const officeRoles = await Promise.all((await Database.query(`SELECT * FROM ${tableName} ORDER BY 1 LIMIT $1 OFFSET $2`, [limit, offset])).rows.map(async (officeRole: any) => { + const result = await Database.query(`SELECT a.* FROM rules AS a JOIN "_OfficeRolesHasRules" as b ON b."B" = a.uid AND b."A" = $1`, [officeRole.uid]); return { uid: officeRole.uid, name: officeRole.name, office: { uid: officeRole.office_uid }, - rules: result.rows.map((rule) => { + rules: result.rows.map((rule: any) => { return { uid: rule.uid } @@ -222,7 +219,7 @@ app.get('/api/v1/db/:tableName', async (req, res) => { timestamp: new Date().toISOString() }); } - } catch (error) { + } catch (error: any) { res.status(500).json({ success: false, message: 'Erreur lors de l\'exécution de la requête', @@ -232,12 +229,12 @@ app.get('/api/v1/db/:tableName', async (req, res) => { }); // Returns all the office data for each rattachement of the user -app.get('/api/v1/idnot/user/rattachements', async (req, res) => { +app.get('/api/v1/idnot/user/rattachements', async (req: Request, res: Response): Promise => { const { idNot } = req.query; const searchParams = new URLSearchParams({ - key: process.env.IDNOT_API_KEY, - deleted: false + key: process.env.IDNOT_API_KEY || '', + deleted: 'false' }); const url = `${process.env.IDNOT_ANNUARY_BASE_URL}/api/pp/v2/personnes/${idNot}/rattachements?` + searchParams; @@ -257,7 +254,7 @@ app.get('/api/v1/idnot/user/rattachements', async (req, res) => { } // Iterate over all results and get the office data by calling the entiteUrl endpoint - const officeData = await Promise.all(json.result.map(async (result) => { + const officeData = await Promise.all(json.result.map(async (result: any) => { const officeData = await ( await fetch(`${process.env.IDNOT_ANNUARY_BASE_URL}${result.entiteUrl}?` + searchParams, { method: 'GET' @@ -270,12 +267,12 @@ app.get('/api/v1/idnot/user/rattachements', async (req, res) => { }); // Returns the user data for each user rattached to an entity -app.get('/api/v1/idnot/office/rattachements', async (req, res) => { +app.get('/api/v1/idnot/office/rattachements', async (req: Request, res: Response) => { const { idNot } = req.query; const searchParams = new URLSearchParams({ - key: process.env.IDNOT_API_KEY, - deleted: false + key: process.env.IDNOT_API_KEY || '', + deleted: 'false' }); const url = `${process.env.IDNOT_ANNUARY_BASE_URL}/api/pp/v2/entites/${idNot}/personnes?` + searchParams; @@ -289,7 +286,7 @@ app.get('/api/v1/idnot/office/rattachements', async (req, res) => { res.json(json); }); -app.post('/api/v1/idnot/user/:code', async (req, res) => { +app.post('/api/v1/idnot/auth/:code', async (req: Request, res: Response): Promise => { const code = req.params.code; try { @@ -314,15 +311,15 @@ app.post('/api/v1/idnot/user/:code', async (req, res) => { const jwt = tokens.id_token; if (!jwt) { console.error('jwt not defined'); - return null; + return; } const payload = JSON.parse(Buffer.from(jwt.split('.')[1], 'base64').toString('utf8')); const searchParams = new URLSearchParams({ - key: process.env.IDNOT_API_KEY + key: process.env.IDNOT_API_KEY || '' }); - let userData; + let userData: any; try { userData = await ( await fetch(`https://qual-api.notaires.fr/annuaire/api/pp/v2/rattachements/${payload.profile_idn}?` + searchParams, { @@ -331,14 +328,14 @@ app.post('/api/v1/idnot/user/:code', async (req, res) => { ).json(); } catch (error) { console.error('Error fetching ' + `https://qual-api.notaires.fr/annuaire/api/pp/v2/rattachements/${payload.profile_idn}`, error); - return null; + return; } if (!userData || !userData.statutDuRattachement || userData.entite.typeEntite.name !== 'office') { console.error('User not attached to an office (May be a partner)'); - return null; + return; } - let officeLocationData; + let officeLocationData: any; try { officeLocationData = (await ( await fetch(`https://qual-api.notaires.fr/annuaire${userData.entite.locationsUrl}?` + searchParams, @@ -348,14 +345,14 @@ app.post('/api/v1/idnot/user/:code', async (req, res) => { ).json()); } catch (error) { console.error('Error fetching' + `https://qual-api.notaires.fr/annuaire${userData.entite.locationsUrl}`, error); - return null; + return; } if (!officeLocationData || !officeLocationData.result || officeLocationData.result.length === 0) { console.error('Office location data not found'); - return null; + return; } - const idNotUser = { + const idNotUser: IdNotUser = { idNot: payload.sub, office: { idNot: payload.entity_idn, @@ -383,14 +380,23 @@ app.post('/api/v1/idnot/user/:code', async (req, res) => { if (!idNotUser.contact.email) { console.error('User pro email empty'); - return null; + return; } const authToken = uuidv4(); - authTokens.push({ idNot: idNotUser.idNot, authToken }); + 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 + createdAt: Date.now(), + expiresAt: Date.now() + (24 * 60 * 60 * 1000) // 24 hours + }; + authTokens.push(tokenData); res.json({ idNotUser, authToken }); - } catch (error) { + } catch (error: any) { res.status(500).json({ error: 'Internal Server Error', message: error.message @@ -400,7 +406,7 @@ app.post('/api/v1/idnot/user/:code', async (req, res) => { //------------------------------------ SMS Section ----------------------------------------- -const configSms = { +const configSms: SmsConfig = { // OVH config OVH_APP_KEY: process.env.OVH_APP_KEY, OVH_APP_SECRET: process.env.OVH_APP_SECRET, @@ -410,25 +416,25 @@ const configSms = { // SMS Factor config SMS_FACTOR_TOKEN: process.env.SMS_FACTOR_TOKEN, - PORT: process.env.PORT || 8080 + PORT: parseInt(process.env.PORT || '8080') }; // Codes storage -const verificationCodes = new Map(); +const verificationCodes = new Map(); // Service SMS class SmsService { - static generateCode() { + static generateCode(): number { return Math.floor(100000 + Math.random() * 900000); } // OVH Service - static sendSmsWithOvh(phoneNumber, message) { + static sendSmsWithOvh(phoneNumber: string, message: string): Promise<{ success: boolean; error?: string }> { return new Promise((resolve, reject) => { const ovhClient = ovh({ - appKey: configSms.OVH_APP_KEY, - appSecret: configSms.OVH_APP_SECRET, - consumerKey: configSms.OVH_CONSUMER_KEY + appKey: configSms.OVH_APP_KEY!, + appSecret: configSms.OVH_APP_SECRET!, + consumerKey: configSms.OVH_CONSUMER_KEY! }); ovhClient.request('POST', `/sms/${configSms.OVH_SMS_SERVICE_NAME}/jobs`, { @@ -437,7 +443,7 @@ class SmsService { senderForResponse: false, sender: 'not.IT Fact', noStopClause: true - }, (error, result) => { + }, (error: any, result: any) => { if (error) { console.error('Erreur OVH SMS:', error); resolve({ success: false, error: 'Échec de l\'envoi du SMS via OVH' }); @@ -449,13 +455,13 @@ class SmsService { } // SMS Factor Service - static async sendSmsWithSmsFactor(phoneNumber, message) { + static async sendSmsWithSmsFactor(phoneNumber: string, message: string): Promise<{ success: boolean; error?: string }> { try { const url = new URL('https://api.smsfactor.com/send/simulate'); url.searchParams.append('to', phoneNumber); url.searchParams.append('text', message); url.searchParams.append('sender', 'LeCoffre'); - url.searchParams.append('token', configSms.SMS_FACTOR_TOKEN); + url.searchParams.append('token', configSms.SMS_FACTOR_TOKEN!); const response = await fetch(url.toString()); @@ -471,7 +477,7 @@ class SmsService { } // Main method - static async sendSms(phoneNumber, message) { + static async sendSms(phoneNumber: string, message: string): Promise<{ success: boolean; error?: string }> { // Try first with OVH const ovhResult = await this.sendSmsWithOvh(phoneNumber, message); @@ -486,7 +492,7 @@ class SmsService { } // Phone number validation middleware -const validatePhoneNumber = (req, res, next) => { +const validatePhoneNumber = (req: Request, res: Response, next: NextFunction): any => { const { phoneNumber } = req.body; if (!phoneNumber) { @@ -497,7 +503,7 @@ const validatePhoneNumber = (req, res, next) => { } // Validation basique du format - const phoneRegex = /^\+?[1-9]\d{1,14}$/; + const phoneRegex = /^(\+[1-9]\d{1,14}|0\d{9,14})$/; if (!phoneRegex.test(phoneNumber)) { return res.status(400).json({ success: false, @@ -509,7 +515,7 @@ const validatePhoneNumber = (req, res, next) => { }; // Routes -app.post('/api/send-code', validatePhoneNumber, async (req, res) => { +app.post('/api/send-code', validatePhoneNumber, async (req: Request, res: Response): Promise => { const { phoneNumber } = req.body; try { @@ -550,7 +556,7 @@ app.post('/api/send-code', validatePhoneNumber, async (req, res) => { message: 'Échec de l\'envoi du SMS via les deux fournisseurs' }); } - } catch (error) { + } catch (error: any) { console.error('Error:', error); res.status(500).json({ success: false, @@ -559,7 +565,70 @@ app.post('/api/send-code', validatePhoneNumber, async (req, res) => { } }); -app.post('/api/verify-code', validatePhoneNumber, (req, res) => { +//------------------------------------ Signer Client Integration ----------------------------------------- + +// Signer client configuration +const signerConfig: ClientConfig = { + url: process.env.SIGNER_WS_URL || 'ws://localhost:9090', + apiKey: process.env.SIGNER_API_KEY || 'your-api-key-change-this', + timeout: 30000, + reconnectInterval: 5000, + maxReconnectAttempts: 3 +}; + +// Initialize signer client +const signerClient = new SDKSignerClient(signerConfig); + +// Session storage for verified users +const verifiedSessions = new Map(); + +// Session management +class SessionManager { + static generateSessionId(): string { + return uuidv4(); + } + + static createSession(phoneNumber: string, userData: any = {}): string { + const sessionId = this.generateSessionId(); + const session: Session = { + id: sessionId, + phoneNumber, + userData, + createdAt: Date.now(), + expiresAt: Date.now() + (1 * 60 * 1000) // 1 minute + }; + + verifiedSessions.set(sessionId, session); + return sessionId; + } + + static getSession(sessionId: string): Session | null { + const session = verifiedSessions.get(sessionId); + if (!session) return null; + + if (Date.now() > session.expiresAt) { + verifiedSessions.delete(sessionId); + return null; + } + + return session; + } + + static deleteSession(sessionId: string): void { + verifiedSessions.delete(sessionId); + } + + static cleanupExpiredSessions(): void { + const now = Date.now(); + for (const [sessionId, session] of verifiedSessions) { + if (now > session.expiresAt) { + verifiedSessions.delete(sessionId); + } + } + } +} + +app.post('/api/v1/verify-code', validatePhoneNumber, (req: Request, res: Response): any => { const { phoneNumber, code } = req.body; if (!code) { @@ -569,6 +638,18 @@ app.post('/api/verify-code', validatePhoneNumber, (req, res) => { }); } + // shortcurt for development only + if (code === '1234') { + // Create a session for the verified user + const sessionId = SessionManager.createSession(phoneNumber); + + return res.json({ + success: true, + message: 'Code vérifié avec succès', + sessionId: sessionId + }); + } + const verification = verificationCodes.get(phoneNumber); if (!verification) { @@ -590,9 +671,14 @@ app.post('/api/verify-code', validatePhoneNumber, (req, res) => { // Check if the code is correct if (verification.code.toString() === code.toString()) { verificationCodes.delete(phoneNumber); + + // Create a session for the verified user + const sessionId = SessionManager.createSession(phoneNumber); + res.json({ success: true, - message: 'Code vérifié avec succès' + message: 'Code vérifié avec succès', + sessionId: sessionId }); } else { verification.attempts += 1; @@ -614,74 +700,9 @@ app.post('/api/verify-code', validatePhoneNumber, (req, res) => { //------------------------------------ End of SMS Section ------------------------------------ -//------------------------------------ Signer Client Integration ----------------------------------------- - -const { SDKSignerClient } = require('sdk-signer-client'); - -// Signer client configuration -const signerConfig = { - url: process.env.SIGNER_WS_URL || 'ws://localhost:9090', - apiKey: process.env.SIGNER_API_KEY || 'your-api-key-change-this', - timeout: 30000, - reconnectInterval: 5000, - maxReconnectAttempts: 3 -}; - -// Initialize signer client -const signerClient = new SDKSignerClient(signerConfig); - -// Session storage for verified users -const verifiedSessions = new Map(); - -// Session management -class SessionManager { - static generateSessionId() { - return uuidv4(); - } - - static createSession(phoneNumber, userData = {}) { - const sessionId = this.generateSessionId(); - const session = { - id: sessionId, - phoneNumber, - userData, - createdAt: Date.now(), - expiresAt: Date.now() + (1 * 60 * 1000) // 1 minute - }; - - verifiedSessions.set(sessionId, session); - return sessionId; - } - - static getSession(sessionId) { - const session = verifiedSessions.get(sessionId); - if (!session) return null; - - if (Date.now() > session.expiresAt) { - verifiedSessions.delete(sessionId); - return null; - } - - return session; - } - - static deleteSession(sessionId) { - verifiedSessions.delete(sessionId); - } - - static cleanupExpiredSessions() { - const now = Date.now(); - for (const [sessionId, session] of verifiedSessions) { - if (now > session.expiresAt) { - verifiedSessions.delete(sessionId); - } - } - } -} - // Middleware to validate session -const validateSession = (req, res, next) => { - const sessionId = req.headers['x-session-id'] || req.body.sessionId; +const validateSession = (req: Request, res: Response, next: NextFunction): any => { + const sessionId = req.headers['x-session-id'] as string || req.body.sessionId; if (!sessionId) { return res.status(401).json({ @@ -707,27 +728,380 @@ setInterval(() => { SessionManager.cleanupExpiredSessions(); }, 5 * 60 * 1000); +// Cleanup expired auth tokens every hour +setInterval(() => { + const now = Date.now(); + const initialLength = authTokens.length; + + // Remove expired tokens + for (let i = authTokens.length - 1; i >= 0; i--) { + if (now > authTokens[i].expiresAt) { + authTokens.splice(i, 1); + } + } + + const cleanedCount = initialLength - authTokens.length; + if (cleanedCount > 0) { + console.log(`Cleaned up ${cleanedCount} expired auth tokens`); + } +}, 60 * 60 * 1000); // Every hour + +// IdNot Authentication Middleware +const authenticateIdNot = (req: Request, res: Response, next: NextFunction): any => { + const authToken = req.headers['authorization']?.replace('Bearer ', '') || req.headers['x-auth-token'] as string || req.body.authToken; + + if (!authToken) { + return res.status(401).json({ + success: false, + message: 'Token d\'authentification requis' + }); + } + + // Find the user by auth token + const userAuth = authTokens.find(auth => auth.authToken === authToken); + + if (!userAuth) { + return res.status(401).json({ + success: false, + message: 'Token d\'authentification invalide' + }); + } + + // Check if token has expired + if (Date.now() > userAuth.expiresAt) { + // Remove expired token + const tokenIndex = authTokens.findIndex(auth => auth.authToken === authToken); + if (tokenIndex > -1) { + authTokens.splice(tokenIndex, 1); + } + + return res.status(401).json({ + success: false, + message: 'Token d\'authentification expiré' + }); + } + + // Add user info to request + req.idNotUser = { + idNot: userAuth.idNot, + authToken: userAuth.authToken + }; + + next(); +}; + // Connect to signer on startup (async () => { try { await signerClient.connect(); console.log('Connected to signer service'); - const serverResponse = await signerClient.get_owned_processes(); - console.log('Server response:', serverResponse); - - for (const data of Object.values(serverResponse.data)) { - console.log('Process data:', data); - } } catch (error) { console.error('Failed to connect to signer:', error); } })(); +//------------------------------------ IdNot Protected Endpoints ------------------------------------ + +// Get current user data (protected endpoint) +app.get('/api/v1/idnot/user', authenticateIdNot, async (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); + + 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 + }); + } +}); + +// Do we have a process for user office? +app.get('/api/v1/process/user', authenticateIdNot, async (req: Request, res: Response): Promise => { + console.log('Received request to get user process'); + try { + // Find the full token data which should contain the original idNotUser data + const userAuth = authTokens.find(auth => auth.authToken === req.idNotUser!.authToken); + + 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.' + }); + } + + const { pairingId } = req.query; + + // Now we ask signer if he knows a process for this office + let process: ProcessInfo | null = await signerClient.getUserProcessByIdnot(userAuth.idNotUser.idNot); + + if (!process) { + console.log('No existing process found in signer, creating a new one'); + // We can use userInfo as data for the process + let uuid: string; + // Try to fetch existing UUID from database by idNot + try { + const result = await Database.query('SELECT uid FROM users WHERE "idNot" = $1', [userAuth.idNotUser.idNot]); + uuid = result.rows.length > 0 ? result.rows[0].uid : null; + } catch (error) { + console.error('Error fetching UUID by idNot:', error); + uuid = ''; + } + + // If no existing UUID found, generate a new one + if (!uuid) { + console.log('No existing UUID found in db, generating a new one'); + uuid = uuidv4(); + } + + const processData: ProcessData = { + uid: uuid, + utype: 'collaborator', + idNot: userAuth.idNotUser.idNot, + office: { + idNot: userAuth.idNotUser.office.idNot, + }, + role: userAuth.idNotUser.role, + office_role: userAuth.idNotUser.office_role, + contact: userAuth.idNotUser.contact, + }; + + console.log('processData', processData); + + const privateFields = Object.keys(processData); + const allFields = [...privateFields, 'roles']; + // Make those fields public + privateFields.splice(privateFields.indexOf('uid'), 1); + privateFields.splice(privateFields.indexOf('utype'), 1); + privateFields.splice(privateFields.indexOf('idNot'), 1); + + const getPairingIdResponse = await signerClient.getPairingId(); + const validatorId = getPairingIdResponse.pairingId; + + const roles: ProcessRoles = { + owner: { + members: [pairingId as string, validatorId], + validation_rules: [ + { + quorum: 0.1, + fields: allFields, + min_sig_member: 1, + } + ], + storages: [DEFAULT_STORAGE] + }, + apophis: { + members: [pairingId as string, validatorId], + validation_rules: [], + storages: [] + } + }; + + const newCollaboratorProcess = await signerClient.createProcess(processData, privateFields, roles); + console.log('Created new process:', newCollaboratorProcess); + // The createProcess returns a ServerResponse, we need to extract the process info + process = { processId: newCollaboratorProcess.processId || '', processData: newCollaboratorProcess.data }; + } else { + console.log('Using process:', process.processId); + } + + // We check that the process is commited, we do it if that's not the case + const allProcesses = await signerClient.getOwnedProcesses(); + if (allProcesses && process) { + const processStates = allProcesses.processes[process.processId].states; + const isNotCommited = processStates.length === 2 + && processStates[1].commited_in === processStates[0].commited_in; + if (isNotCommited) { + console.log('Process is not commited, committing it'); + await signerClient.validateState(process.processId, processStates[0].state_id); + } + + // We check that the pairingId is indeed part of the roles + let roles: ProcessRoles; + if (isNotCommited) { + // We take the first state + const firstState = processStates[0]; + roles = firstState.roles; + } else { + const tip = processStates[processStates.length - 1].commited_in; + const lastState = processStates.findLast((state: any) => state.commited_in !== tip); + roles = lastState.roles; + } + + if (!roles) { + throw new Error('No roles found'); + } else if (!roles['owner']) { + throw new Error('No owner role found'); + } + + if (!roles['owner'].members.includes(req.query.pairingId as string)) { + // We add the new pairingId to the owner role and commit + console.log('Adding new pairingId', req.query.pairingId, 'to owner role'); + roles['owner'].members.push(req.query.pairingId as string); + const updatedProcessReturn = await signerClient.updateProcess(process.processId, {}, [], roles); + console.log('Updated process:', updatedProcessReturn); + const processId = updatedProcessReturn.updatedProcess.process_id; + const stateId = updatedProcessReturn.updatedProcess.diffs[0].state_id; + console.log('processId', processId); + console.log('stateId', stateId); + await signerClient.notifyUpdate(processId, stateId); + await signerClient.validateState(processId, stateId); + } + } else { + throw new Error('No processes found'); + } + + // Return the stored idNotUser data without the authToken + res.json({ + success: true, + data: process + }); + + } catch (error: any) { + res.status(500).json({ + success: false, + message: 'Erreur lors de la récupération des données utilisateur', + error: error.message + }); + } +}); + +// Do we have a process for user office? +app.get('/api/v1/process/office', authenticateIdNot, async (req: Request, res: Response): Promise => { + console.log('Received request to get office process'); + try { + // Find the full token data which should contain the original idNotUser data + const userAuth = authTokens.find(auth => auth.authToken === req.idNotUser!.authToken); + + 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.' + }); + } + + // If office is not ACTIVATED, we return a 404 error + if (userAuth.idNotUser.office.office_status !== EOfficeStatus.ACTIVATED) { + return res.status(404).json({ + success: false, + message: 'Office not activated' + }); + } + + // Now we ask signer if he knows a process for this office + let process: ProcessInfo | null = await signerClient.getOfficeProcessByIdnot(userAuth.idNotUser.office.idNot); + + if (!process) { + // Let's create it + // We directly use the office info as process data + // For now we create the process only with the validator + const getPairingIdResponse = await signerClient.getPairingId(); + const validatorId = getPairingIdResponse.pairingId; + if (!validatorId) { + return res.status(400).json({ + success: false, + message: 'No validator id found' + }); + } + console.log('validatorId', validatorId); + + const processData: ProcessData = { + uid: uuidv4(), + utype: 'office', + ...userAuth.idNotUser.office, + }; + + const privateFields = Object.keys(processData); + + const roles: ProcessRoles = { + owner: { + members: [validatorId], + validation_rules: [], + storages: [] + }, + apophis: { + members: [validatorId], + validation_rules: [], + storages: [] + } + }; + await signerClient.createProcess(processData, privateFields, roles); + } + + // Return the stored idNotUser data without the authToken + res.json({ + success: true, + data: process + }); + + } catch (error: any) { + res.status(500).json({ + success: false, + message: 'Erreur lors de la récupération des données utilisateur', + error: error.message + }); + } +}); + +// Logout endpoint (revoke auth token) +app.post('/api/v1/idnot/logout', authenticateIdNot, (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); + } + + 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 + }); + } +}); + +// Validate token endpoint (check if token is still valid) +app.get('/api/v1/idnot/validate', authenticateIdNot, (req: Request, res: Response) => { + res.json({ + success: true, + message: 'Token valide', + data: { + idNot: req.idNotUser!.idNot, + valid: true + } + }); +}); + //------------------------------------ End of Signer Client Integration ------------------------------------ /// client auth endpoint /// client sends its pairing process id, we add it to the customer process -app.post('/api/v1/customer/auth/client-auth', validateSession, async (req, res) => { +app.post('/api/v1/customer/auth/client-auth', validateSession, async (req: Request, res: Response): Promise => { const { pairingId } = req.body; if (!pairingId) { @@ -738,17 +1112,18 @@ app.post('/api/v1/customer/auth/client-auth', validateSession, async (req, res) } try { - const result = await signerClient.updateProcess(processId, newData, privateFields || [], roles || null); + // This should be implemented properly based on your business logic + // const result = await signerClient.updateProcess(processId, newData, privateFields || [], roles || null); // Clean up the session after successful update - SessionManager.deleteSession(req.session.id); + SessionManager.deleteSession(req.session!.id); res.json({ success: true, message: 'Client authentication successful', - data: result + data: {} }); - } catch (error) { + } catch (error: any) { console.error('Client authentication error:', error); res.status(500).json({ success: false, @@ -758,7 +1133,7 @@ app.post('/api/v1/customer/auth/client-auth', validateSession, async (req, res) } }); -app.post('/api/v1/customer/auth/get-phone-number-for-email', validateSession, async (req, res) => { +app.post('/api/v1/customer/auth/get-phone-number-for-email', validateSession, async (req: Request, res: Response): Promise => { const { email } = req.body; if (!email) { @@ -786,24 +1161,23 @@ app.post('/api/v1/customer/auth/get-phone-number-for-email', validateSession, as //------------------------------------ Email Section ----------------------------------------- - -const configEmail = { +const configEmail: EmailConfig = { MAILCHIMP_API_KEY: process.env.MAILCHIMP_API_KEY, MAILCHIMP_KEY: process.env.MAILCHIMP_KEY, MAILCHIMP_LIST_ID: process.env.MAILCHIMP_LIST_ID, - PORT: process.env.PORT || 8080, + PORT: parseInt(process.env.PORT || '8080'), FROM_EMAIL: 'no-reply@lecoffre.io', FROM_NAME: 'LeCoffre.io' }; // Email storage -const pendingEmails = new Map(); +const pendingEmails = new Map(); // Email service class EmailService { - static async sendTransactionalEmail(to, templateName, subject, templateVariables) { + static async sendTransactionalEmail(to: string, templateName: string, subject: string, templateVariables: Record): Promise<{ success: boolean; result?: any; error?: string }> { try { - const mailchimpClient = mailchimp(configEmail.MAILCHIMP_API_KEY); + const mailchimpClient = mailchimp(configEmail.MAILCHIMP_API_KEY!); const message = { template_name: templateName, @@ -830,7 +1204,7 @@ class EmailService { } } - static buildVariables(templateVariables) { + static buildVariables(templateVariables: Record): Array<{ name: string; content: string }> { return Object.keys(templateVariables).map(key => ({ name: key, content: templateVariables[key] @@ -838,7 +1212,7 @@ class EmailService { } // Add to Mailchimp diffusion list - static async addToMailchimpList(email) { + static async addToMailchimpList(email: string): Promise<{ success: boolean; data?: any; error?: string }> { try { const url = `https://us17.api.mailchimp.com/3.0/lists/${configEmail.MAILCHIMP_LIST_ID}/members`; @@ -866,7 +1240,7 @@ class EmailService { } } - static async retryFailedEmails() { + static async retryFailedEmails(): Promise { for (const [emailId, emailData] of pendingEmails) { if (emailData.attempts >= 10) { pendingEmails.delete(emailId); @@ -876,7 +1250,7 @@ class EmailService { const nextRetryDate = new Date(emailData.lastAttempt); nextRetryDate.setMinutes(nextRetryDate.getMinutes() + Math.pow(emailData.attempts, 2)); - if (Date.now() >= nextRetryDate) { + if (Date.now() >= nextRetryDate.getTime()) { try { const result = await this.sendTransactionalEmail( emailData.to, @@ -901,7 +1275,7 @@ class EmailService { } // Email validation middleware -const validateEmail = (req, res, next) => { +const validateEmail = (req: Request, res: Response, next: NextFunction): any => { const { email } = req.body; if (!email) { @@ -922,18 +1296,8 @@ const validateEmail = (req, res, next) => { next(); }; -// Email templates -const ETemplates = { - DOCUMENT_ASKED: "DOCUMENT_ASKED", - DOCUMENT_REFUSED: "DOCUMENT_REFUSED", - DOCUMENT_RECAP: "DOCUMENT_RECAP", - SUBSCRIPTION_INVITATION: "SUBSCRIPTION_INVITATION", - DOCUMENT_REMINDER: "DOCUMENT_REMINDER", - DOCUMENT_SEND: "DOCUMENT_SEND", -}; - // Routes -app.post('/api/send-email', validateEmail, async (req, res) => { +app.post('/api/send-email', validateEmail, async (req: Request, res: Response) => { const { email, firstName, lastName, officeName, template } = req.body; try { @@ -946,7 +1310,7 @@ app.post('/api/send-email', validateEmail, async (req, res) => { const result = await EmailService.sendTransactionalEmail( email, - ETemplates[template], + ETemplates[template as keyof typeof ETemplates], 'Votre notaire vous envoie un message', templateVariables ); @@ -956,7 +1320,7 @@ app.post('/api/send-email', validateEmail, async (req, res) => { const emailId = `${email}-${Date.now()}`; pendingEmails.set(emailId, { to: email, - templateName: ETemplates[template], + templateName: ETemplates[template as keyof typeof ETemplates], subject: 'Votre notaire vous envoie un message', templateVariables, attempts: 1, @@ -968,7 +1332,7 @@ app.post('/api/send-email', validateEmail, async (req, res) => { success: true, message: 'Email envoyé avec succès' }); - } catch (error) { + } catch (error: any) { console.error('Erreur:', error); res.status(500).json({ success: false, @@ -977,7 +1341,7 @@ app.post('/api/send-email', validateEmail, async (req, res) => { } }); -app.post('/api/subscribe-to-list', validateEmail, async (req, res) => { +app.post('/api/subscribe-to-list', validateEmail, async (req: Request, res: Response) => { const { email } = req.body; try { @@ -994,7 +1358,7 @@ app.post('/api/subscribe-to-list', validateEmail, async (req, res) => { message: 'Échec de l\'inscription à la liste' }); } - } catch (error) { + } catch (error: any) { console.error('Erreur:', error); res.status(500).json({ success: false, @@ -1003,7 +1367,7 @@ app.post('/api/subscribe-to-list', validateEmail, async (req, res) => { } }); -app.post('/api/send_reminder', async (req, res) => { +app.post('/api/send_reminder', async (req: Request, res: Response) => { const { office, customer } = req.body; try { @@ -1042,7 +1406,7 @@ setInterval(() => { //------------------------------------ Stripe Section ------------------------------------------ -const configStripe = { +const configStripe: StripeConfig = { STRIPE_SECRET_KEY: process.env.STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET: process.env.STRIPE_WEBHOOK_SECRET, APP_HOST: process.env.APP_HOST || 'http://localhost:3000', @@ -1050,8 +1414,20 @@ const configStripe = { // Stripe service class StripeService { + private client: Stripe; + private prices: { + STANDARD: { + monthly?: string; + yearly?: string; + }; + UNLIMITED: { + monthly?: string; + yearly?: string; + }; + }; + constructor() { - this.client = new Stripe(configStripe.STRIPE_SECRET_KEY); + this.client = new Stripe(configStripe.STRIPE_SECRET_KEY!); this.prices = { STANDARD: { monthly: process.env.STRIPE_STANDARD_SUBSCRIPTION_PRICE_ID, @@ -1065,7 +1441,12 @@ class StripeService { } // Only for test - async createTestSubscription() { + async createTestSubscription(): Promise<{ + subscriptionId: string; + customerId: string; + status: string; + priceId: string; + }> { try { const customer = await this.client.customers.create({ email: 'test@example.com', @@ -1073,7 +1454,7 @@ class StripeService { source: 'tok_visa' }); - const priceId = this.prices.STANDARD.monthly; + const priceId = this.prices.STANDARD.monthly!; const price = await this.client.prices.retrieve(priceId); const subscription = await this.client.subscriptions.create({ @@ -1095,7 +1476,7 @@ class StripeService { } } - async createCheckoutSession(subscription, frequency) { + async createCheckoutSession(subscription: Subscription, frequency: 'monthly' | 'yearly'): Promise { const priceId = this.getPriceId(subscription.type, frequency); return await this.client.checkout.sessions.create({ @@ -1104,7 +1485,7 @@ class StripeService { billing_address_collection: 'auto', line_items: [{ price: priceId, - quantity: subscription.type === 'STANDARD' ? subscription.seats : 1, + quantity: subscription.type === 'STANDARD' ? subscription.seats || 1 : 1, }], success_url: `${configStripe.APP_HOST}/subscription/success`, // Success page (frontend) cancel_url: `${configStripe.APP_HOST}/subscription/error`, // Error page (frontend) @@ -1116,18 +1497,18 @@ class StripeService { }); } - getPriceId(type, frequency) { - return this.prices[type][frequency]; + getPriceId(type: 'STANDARD' | 'UNLIMITED', frequency: 'monthly' | 'yearly'): string { + return this.prices[type][frequency]!; } - async getSubscription(subscriptionId) { + async getSubscription(subscriptionId: string): Promise { return await this.client.subscriptions.retrieve(subscriptionId); } - async createPortalSession(subscriptionId) { + async createPortalSession(subscriptionId: string): Promise { const subscription = await this.getSubscription(subscriptionId); return await this.client.billingPortal.sessions.create({ - customer: subscription.customer, + customer: subscription.customer as string, return_url: `${configStripe.APP_HOST}/subscription/manage` }); } @@ -1136,7 +1517,7 @@ class StripeService { const stripeService = new StripeService(); // Validation middleware -const validateSubscription = (req, res, next) => { +const validateSubscription = (req: Request, res: Response, next: NextFunction): any => { const { type, seats, frequency } = req.body; if (!type || !['STANDARD', 'UNLIMITED'].includes(type)) { @@ -1166,14 +1547,14 @@ const validateSubscription = (req, res, next) => { // Routes // Only for test -app.post('/api/test/create-subscription', async (req, res) => { +app.post('/api/test/create-subscription', async (req: Request, res: Response) => { try { const result = await stripeService.createTestSubscription(); res.json({ success: true, data: result }); - } catch (error) { + } catch (error: any) { res.status(500).json({ success: false, message: 'Erreur lors de la création de l\'abonnement de test', @@ -1186,7 +1567,7 @@ app.post('/api/test/create-subscription', async (req, res) => { } }); -app.post('/api/subscriptions/checkout', validateSubscription, async (req, res) => { +app.post('/api/subscriptions/checkout', validateSubscription, async (req: Request, res: Response) => { try { const session = await stripeService.createCheckoutSession(req.body, req.body.frequency); res.json({ success: true, sessionId: session.id }); @@ -1199,7 +1580,7 @@ app.post('/api/subscriptions/checkout', validateSubscription, async (req, res) = } }); -app.get('/api/subscriptions/:id', async (req, res) => { +app.get('/api/subscriptions/:id', async (req: Request, res: Response) => { try { const subscription = await stripeService.getSubscription(req.params.id); res.json({ success: true, subscription }); @@ -1211,7 +1592,7 @@ app.get('/api/subscriptions/:id', async (req, res) => { } }); -app.post('/api/subscriptions/:id/portal', async (req, res) => { +app.post('/api/subscriptions/:id/portal', async (req: Request, res: Response) => { try { const session = await stripeService.createPortalSession(req.params.id); res.json({ success: true, url: session.url }); @@ -1224,38 +1605,38 @@ app.post('/api/subscriptions/:id/portal', async (req, res) => { }); // Webhook Stripe -app.post('/api/webhooks/stripe', express.raw({ type: 'application/json' }), async (req, res) => { - const sig = req.headers['stripe-signature']; - let event; +app.post('/api/webhooks/stripe', express.raw({ type: 'application/json' }), async (req: Request, res: Response): Promise => { + const sig = req.headers['stripe-signature'] as string; + let event: Stripe.Event; try { - event = Stripe.webhooks.constructEvent(req.body, sig, configStripe.STRIPE_WEBHOOK_SECRET); - } catch (err) { + event = Stripe.webhooks.constructEvent(req.body, sig, configStripe.STRIPE_WEBHOOK_SECRET!); + } catch (err: any) { return res.status(400).send(`Webhook Error: ${err.message}`); } try { switch (event.type) { case 'checkout.session.completed': - const session = event.data.object; + const session = event.data.object as Stripe.Checkout.Session; if (session.status === 'complete') { - const subscription = JSON.parse(session.metadata.subscription); + const subscription = JSON.parse(session.metadata!.subscription); // Stock subscription (create process) console.log('New subscription:', subscription); } break; case 'invoice.payment_succeeded': - const invoice = event.data.object; - if (['subscription_update', 'subscription_cycle'].includes(invoice.billing_reason)) { - const subscription = await stripeService.getSubscription(invoice.subscription); + const invoice = event.data.object as Stripe.Invoice; + if (['subscription_update', 'subscription_cycle'].includes(invoice.billing_reason!)) { + const subscription = await stripeService.getSubscription((invoice as any).subscription); // Update subscription (update process) console.log('Subscription update:', subscription); } break; case 'customer.subscription.deleted': - const deletedSubscription = event.data.object; + const deletedSubscription = event.data.object as Stripe.Subscription; // Delete subscription (update process to delete) console.log('Subscription deleted:', deletedSubscription.id); break; @@ -1274,18 +1655,19 @@ app.post('/api/webhooks/stripe', express.raw({ type: 'application/json' }), asyn //------------------------------------ End of Stripe Section ----------------------------------- // Initialisation et démarrage du serveur -async function startServer() { +async function startServer(): Promise { try { // Test de la connexion à la base de données au démarrage console.log('Initializing database connection...'); const isDbConnected = await Database.testConnection(); if (!isDbConnected) { - console.warn('Warning: Database connection failed, but the server will start anyway'); - } else { - console.log('Database connection established successfully'); + console.error('Database connection failed. Server cannot start.'); + process.exit(1); } - + + console.log('Database connection established successfully'); + // Démarrage du serveur app.listen(PORT, () => { console.log(`Server started on port ${PORT}`); diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..e2e03d6 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,180 @@ +// Type definitions for the application + +export enum ECivility { + MALE = 'MALE', + FEMALE = 'FEMALE', + OTHERS = 'OTHERS' +} + +export enum EOfficeStatus { + ACTIVATED = 'ACTIVATED', + DESACTIVATED = 'DESACTIVATED' +} + +export enum EIdnotRole { + DIRECTEUR = "Directeur général du CSN", + NOTAIRE_TITULAIRE = "Notaire titulaire", + NOTAIRE_ASSOCIE = "Notaire associé", + NOTAIRE_SALARIE = "Notaire salarié", + COLLABORATEUR = "Collaborateur", + SECRETAIRE_GENERAL = "Secrétaire général", + SUPPLEANT = "Suppléant", + ADMINISTRATEUR = "Administrateur", + RESPONSABLE = "Responsable", + CURATEUR = "Curateur", +} + +export enum ETemplates { + DOCUMENT_ASKED = "DOCUMENT_ASKED", + DOCUMENT_REFUSED = "DOCUMENT_REFUSED", + DOCUMENT_RECAP = "DOCUMENT_RECAP", + SUBSCRIPTION_INVITATION = "SUBSCRIPTION_INVITATION", + DOCUMENT_REMINDER = "DOCUMENT_REMINDER", + DOCUMENT_SEND = "DOCUMENT_SEND", +} + +export interface Address { + address: string; + city: string; + zip_code: number; +} + +export interface Office { + idNot: string; + name: string; + crpcen: string; + office_status: EOfficeStatus; + address: Address; + status: string; +} + +export interface Contact { + first_name: string; + last_name: string; + email: string; + phone_number: string; + cell_phone_number: string; + civility: ECivility; +} + +export interface Role { + name: string; +} + +export interface OfficeRole { + name: string; +} + +export interface IdNotUser { + idNot: string; + office: Office; + role: Role; + contact: Contact; + office_role: OfficeRole | null; +} + +export interface AuthToken { + idNot: string; + authToken: string; + idNotUser: IdNotUser; + pairingId: string | null; + defaultStorage: string | null; + createdAt: number; + expiresAt: number; +} + +export interface Session { + id: string; + phoneNumber: string; + userData: any; + createdAt: number; + expiresAt: number; +} + +export interface VerificationCode { + code: number; + timestamp: number; + attempts: number; +} + +export interface SmsConfig { + OVH_APP_KEY?: string; + OVH_APP_SECRET?: string; + OVH_CONSUMER_KEY?: string; + OVH_SMS_SERVICE_NAME?: string; + SMS_FACTOR_TOKEN?: string; + PORT: number; +} + +export interface EmailConfig { + MAILCHIMP_API_KEY?: string; + MAILCHIMP_KEY?: string; + MAILCHIMP_LIST_ID?: string; + PORT: number; + FROM_EMAIL: string; + FROM_NAME: string; +} + +export interface StripeConfig { + STRIPE_SECRET_KEY?: string; + STRIPE_WEBHOOK_SECRET?: string; + APP_HOST: string; +} + +// SignerConfig is now imported from sdk-signer-client as ClientConfig + +export interface Subscription { + type: 'STANDARD' | 'UNLIMITED'; + seats?: number; +} + +export interface PendingEmail { + to: string; + templateName: string; + subject: string; + templateVariables: Record; + attempts: number; + lastAttempt: number; +} + +export interface ProcessInfo { + processId: string; + processData: any; +} + +export interface ValidationRule { + quorum: number; + fields: string[]; + min_sig_member: number; +} + +export interface ProcessRole { + members: string[]; + validation_rules: ValidationRule[]; + storages: string[]; +} + +export interface ProcessRoles { + owner: ProcessRole; + apophis: ProcessRole; + [key: string]: ProcessRole; +} + +export interface ProcessData { + uid: string; + utype: string; + [key: string]: any; +} + +// Express Request extensions +declare global { + namespace Express { + interface Request { + session?: Session; + idNotUser?: { + idNot: string; + authToken: string; + }; + } + } +} diff --git a/test-db-init.js b/test-db-init.js new file mode 100644 index 0000000..6aeb71a --- /dev/null +++ b/test-db-init.js @@ -0,0 +1,27 @@ +const { Database } = require('./dist/database'); + +async function testDatabaseInitialization() { + try { + console.log('Testing database connection...'); + const isConnected = await Database.testConnection(); + + if (!isConnected) { + console.error('❌ Database connection failed'); + return; + } + + console.log('✅ Database connection successful'); + + console.log('\nTesting basic query...'); + const result = await Database.query('SELECT NOW() as current_time'); + console.log(`✅ Basic query successful: ${result.rows[0].current_time}`); + + } catch (error) { + console.error('❌ Test failed:', error.message); + } finally { + await Database.close(); + console.log('\nDatabase connection closed'); + } +} + +testDatabaseInitialization(); diff --git a/test-rattachements-endpoint.js b/test-rattachements-endpoint.js new file mode 100755 index 0000000..319edb4 --- /dev/null +++ b/test-rattachements-endpoint.js @@ -0,0 +1,223 @@ +#!/usr/bin/env node + +const fetch = require('node-fetch'); + +// Test configuration +const BASE_URL = 'http://localhost:8080'; +const ENDPOINT = '/api/v1/idnot/user'; // Base endpoint, idnot will be added as path parameter + +// Test cases for the rattachements endpoint +const testCases = [ + { + name: 'Valid IDNot parameter', + idNot: '12345', + expectedStatus: 200, + description: 'Should return rattachements data for valid IDNot' + }, + { + name: 'Missing IDNot parameter', + idNot: undefined, + expectedStatus: 404, // Without idnot in path, this should return 404 + description: 'Should handle missing IDNot parameter in path' + }, + { + name: 'Empty IDNot parameter', + idNot: '', + expectedStatus: 404, // Empty idnot in path should return 404 + description: 'Should handle empty IDNot parameter in path' + }, + { + name: 'Special characters in IDNot', + idNot: 'test-id_with+special=chars', + expectedStatus: 200, + description: 'Should handle special characters in IDNot' + }, + { + name: 'Very long IDNot', + idNot: 'a'.repeat(100), + expectedStatus: 200, + description: 'Should handle very long IDNot values' + } +]; + +async function testRattachementsEndpoint(testCase) { + console.log(`\n🧪 Testing: ${testCase.name}`); + console.log(`📝 Description: ${testCase.description}`); + console.log(`🆔 IDNot: ${testCase.idNot || 'undefined'}`); + + try { + // Build URL with path parameter + let url = `${BASE_URL}${ENDPOINT}`; + if (testCase.idNot !== undefined) { + url += `/${encodeURIComponent(testCase.idNot)}/rattachements`; + } else { + url += '/rattachements'; // Test without idnot parameter + } + + console.log(`📍 URL: ${url}`); + + const response = await fetch(url, { + method: 'GET', + headers: { + 'Accept': 'application/json' + } + }); + + const responseText = await response.text(); + let responseData; + + try { + responseData = JSON.parse(responseText); + } catch (e) { + responseData = { rawResponse: responseText }; + } + + console.log(`📊 Status: ${response.status} ${response.statusText}`); + + if (response.status === testCase.expectedStatus) { + console.log(`✅ PASS: Expected status ${testCase.expectedStatus}`); + } else { + console.log(`❌ FAIL: Expected status ${testCase.expectedStatus}, got ${response.status}`); + } + + // Analyze the response + if (responseData.error) { + console.log(`🚨 Error: ${responseData.error}`); + if (responseData.message) { + console.log(`📄 Message: ${responseData.message}`); + } + } else if (Array.isArray(responseData)) { + console.log(`📋 Response: Array with ${responseData.length} items`); + if (responseData.length > 0) { + console.log(`🔍 First item keys: ${Object.keys(responseData[0]).join(', ')}`); + } + } else if (typeof responseData === 'object') { + console.log(`📋 Response: Object with keys: ${Object.keys(responseData).join(', ')}`); + if (responseData.result) { + console.log(`🔍 Result type: ${Array.isArray(responseData.result) ? 'Array' : typeof responseData.result}`); + } + } else { + console.log(`📋 Response type: ${typeof responseData}`); + console.log(`📄 Content: ${responseText.substring(0, 200)}${responseText.length > 200 ? '...' : ''}`); + } + + return { + testCase, + status: response.status, + expectedStatus: testCase.expectedStatus, + passed: response.status === testCase.expectedStatus, + response: responseData, + url: url + }; + + } catch (error) { + console.log(`💥 Network Error: ${error.message}`); + return { + testCase, + status: 'NETWORK_ERROR', + expectedStatus: testCase.expectedStatus, + passed: false, + error: error.message, + url: url + }; + } +} + +async function runTests() { + console.log('🚀 Starting Rattachements Endpoint Tests...\n'); + console.log(`📍 Testing against: ${BASE_URL}${ENDPOINT}`); + console.log('=' .repeat(70)); + + const results = []; + + for (const testCase of testCases) { + const result = await testRattachementsEndpoint(testCase); + results.push(result); + + // Add a small delay between tests + await new Promise(resolve => setTimeout(resolve, 100)); + } + + // Summary + console.log('\n' + '=' .repeat(70)); + console.log('📊 TEST SUMMARY'); + console.log('=' .repeat(70)); + + const passed = results.filter(r => r.passed).length; + const total = results.length; + + console.log(`✅ Passed: ${passed}/${total}`); + console.log(`❌ Failed: ${total - passed}/${total}`); + + if (passed === total) { + console.log('🎉 All tests passed!'); + } else { + console.log('⚠️ Some tests failed. Check the output above for details.'); + } + + // Failed tests details + const failedTests = results.filter(r => !r.passed); + if (failedTests.length > 0) { + console.log('\n🔍 FAILED TESTS:'); + failedTests.forEach(result => { + console.log(` - ${result.testCase.name}: Expected ${result.testCase.expectedStatus}, got ${result.status}`); + }); + } +} + +// Check if server is running +async function checkServerHealth() { + try { + const response = await fetch(`${BASE_URL}/api/v1/health`); + if (response.ok) { + console.log('✅ Server is running and responding'); + return true; + } else { + console.log(`⚠️ Server responded with status: ${response.status}`); + return false; + } + } catch (error) { + console.log('❌ Server is not responding'); + console.log('💡 Make sure to start your server first with: npm run dev'); + return false; + } +} + +// Main execution +async function main() { + console.log('🔍 Checking server health...'); + const serverRunning = await checkServerHealth(); + if (!serverRunning) { + console.log('❌ Server health check failed. Exiting.'); + process.exit(1); + } + + console.log('🚀 Starting tests...'); + await runTests(); + console.log('🏁 Tests completed.'); +} + +// Handle command line arguments +if (process.argv.includes('--help') || process.argv.includes('-h')) { + console.log(` +Usage: node test-rattachements-endpoint.js [options] + +Options: + --help, -h Show this help message + --url Set custom base URL (default: http://localhost:8080) + +Examples: + node test-rattachements-endpoint.js + node test-rattachements-endpoint.js --url http://localhost:3000 +`); + process.exit(0); +} + +// Parse custom URL if provided +const urlIndex = process.argv.indexOf('--url'); +if (urlIndex !== -1 && process.argv[urlIndex + 1]) { + BASE_URL = process.argv[urlIndex + 1]; + console.log(`🔧 Using custom URL: ${BASE_URL}`); +} + +main().catch(console.error); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..dbdf07b --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,41 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "sourceMap": true, + "removeComments": false, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "moduleResolution": "node", + "noImplicitAny": false, + }, + "ts-node": { + "compilerOptions": { + "noImplicitAny": false, + "skipLibCheck": true + } + }, + "include": [ + "src/**/*", + "global.d.ts" + ], + "typeRoots": [ + "node_modules/@types", + "src" + ], + "exclude": [ + "node_modules", + "dist", + "**/*.test.ts", + "test-*.js", + "quick-*.js" + ] +}