"fix-wrap-all-async-handlers"

This commit is contained in:
deploy
2026-04-29 13:09:55 +02:00
parent 1fc7655fa4
commit d6b62f5596
5 changed files with 83 additions and 65 deletions
+19 -15
View File
@@ -104,27 +104,31 @@ router.post('/refresh', async (req: Request, res: Response): Promise<void> => {
* POST /api/auth/logout * POST /api/auth/logout
* Révoque le refresh token. * Révoque le refresh token.
*/ */
router.post('/logout', requireAuth, async (req: AuthRequest, res: Response): Promise<void> => { router.post('/logout', requireAuth, async (req: AuthRequest, res: Response, next: express.NextFunction): Promise<void> => {
const { refreshToken } = req.body; try {
if (refreshToken) { const { refreshToken } = req.body;
await db.query('DELETE FROM refresh_tokens WHERE token_hash = $1', [hashToken(refreshToken)]); if (refreshToken) {
} await db.query('DELETE FROM refresh_tokens WHERE token_hash = $1', [hashToken(refreshToken)]);
res.json({ success: true }); }
res.json({ success: true });
} catch (err) { next(err); }
}); });
/** /**
* GET /api/auth/me * GET /api/auth/me
* Retourne l'utilisateur connecté (sans données sensibles). * Retourne l'utilisateur connecté (sans données sensibles).
*/ */
router.get('/me', requireAuth, async (req: AuthRequest, res: Response): Promise<void> => { router.get('/me', requireAuth, async (req: AuthRequest, res: Response, next: express.NextFunction): Promise<void> => {
const result = await db.query( try {
`SELECT id, name, email, const result = await db.query(
smtp_host, smtp_port, smtp_secure, smtp_user, `SELECT id, name, email,
smtp_from_name, smtp_from_email smtp_host, smtp_port, smtp_secure, smtp_user,
FROM users WHERE id = $1`, smtp_from_name, smtp_from_email
[req.user!.id] FROM users WHERE id = $1`,
); [req.user!.id]
res.json(result.rows[0]); );
res.json(result.rows[0]);
} catch (err) { next(err); }
}); });
export default router; export default router;
+12 -9
View File
@@ -1,4 +1,4 @@
import { Router, Response } from 'express'; import { Router, Response, NextFunction } from 'express';
import { z } from 'zod'; import { z } from 'zod';
import { db } from '../db'; import { db } from '../db';
import { requireAuth, AuthRequest } from '../middleware/auth'; import { requireAuth, AuthRequest } from '../middleware/auth';
@@ -7,31 +7,34 @@ import { validate } from '../middleware/validate';
const router = Router(); const router = Router();
router.use(requireAuth); router.use(requireAuth);
const wrap = (fn: (req: AuthRequest, res: Response, next: NextFunction) => Promise<void>) =>
(req: AuthRequest, res: Response, next: NextFunction) => fn(req, res, next).catch(next);
const categorySchema = z.object({ const categorySchema = z.object({
name: z.string().min(1).max(100), name: z.string().min(1).max(100),
sort_order: z.number().int().optional().default(0), sort_order: z.number().int().optional().default(0),
}); });
/** GET /api/categories */ /** GET /api/categories */
router.get('/', async (_req: AuthRequest, res: Response): Promise<void> => { router.get('/', wrap(async (_req: AuthRequest, res: Response): Promise<void> => {
const result = await db.query( const result = await db.query(
'SELECT * FROM categories WHERE is_active=TRUE ORDER BY sort_order, name' 'SELECT * FROM categories WHERE is_active=TRUE ORDER BY sort_order, name'
); );
res.json(result.rows); res.json(result.rows);
}); }));
/** POST /api/categories */ /** POST /api/categories */
router.post('/', validate(categorySchema), async (req: AuthRequest, res: Response): Promise<void> => { router.post('/', validate(categorySchema), wrap(async (req: AuthRequest, res: Response): Promise<void> => {
const { name, sort_order } = req.body; const { name, sort_order } = req.body;
const result = await db.query( const result = await db.query(
'INSERT INTO categories (name, sort_order) VALUES ($1, $2) RETURNING *', 'INSERT INTO categories (name, sort_order) VALUES ($1, $2) RETURNING *',
[name, sort_order] [name, sort_order]
); );
res.status(201).json(result.rows[0]); res.status(201).json(result.rows[0]);
}); }));
/** PUT /api/categories/:id */ /** PUT /api/categories/:id */
router.put('/:id', validate(categorySchema), async (req: AuthRequest, res: Response): Promise<void> => { router.put('/:id', validate(categorySchema), wrap(async (req: AuthRequest, res: Response): Promise<void> => {
const { name, sort_order } = req.body; const { name, sort_order } = req.body;
const result = await db.query( const result = await db.query(
'UPDATE categories SET name=$1, sort_order=$2 WHERE id=$3 AND is_active=TRUE RETURNING *', 'UPDATE categories SET name=$1, sort_order=$2 WHERE id=$3 AND is_active=TRUE RETURNING *',
@@ -39,12 +42,12 @@ router.put('/:id', validate(categorySchema), async (req: AuthRequest, res: Respo
); );
if (!result.rows[0]) { res.status(404).json({ error: 'Catégorie introuvable' }); return; } if (!result.rows[0]) { res.status(404).json({ error: 'Catégorie introuvable' }); return; }
res.json(result.rows[0]); res.json(result.rows[0]);
}); }));
/** DELETE /api/categories/:id — Soft-delete */ /** DELETE /api/categories/:id — Soft-delete */
router.delete('/:id', async (req: AuthRequest, res: Response): Promise<void> => { router.delete('/:id', wrap(async (req: AuthRequest, res: Response): Promise<void> => {
await db.query('UPDATE categories SET is_active=FALSE WHERE id=$1', [req.params.id]); await db.query('UPDATE categories SET is_active=FALSE WHERE id=$1', [req.params.id]);
res.json({ success: true }); res.json({ success: true });
}); }));
export default router; export default router;
+12 -9
View File
@@ -1,4 +1,4 @@
import { Router, Response } from 'express'; import { Router, Response, NextFunction } from 'express';
import { z } from 'zod'; import { z } from 'zod';
import { db } from '../db'; import { db } from '../db';
import { requireAuth, AuthRequest } from '../middleware/auth'; import { requireAuth, AuthRequest } from '../middleware/auth';
@@ -7,31 +7,34 @@ import { validate } from '../middleware/validate';
const router = Router(); const router = Router();
router.use(requireAuth); router.use(requireAuth);
const wrap = (fn: (req: AuthRequest, res: Response, next: NextFunction) => Promise<void>) =>
(req: AuthRequest, res: Response, next: NextFunction) => fn(req, res, next).catch(next);
const companySchema = z.object({ const companySchema = z.object({
name: z.string().min(1).max(255), name: z.string().min(1).max(255),
email: z.string().email(), email: z.string().email(),
}); });
/** GET /api/companies — Liste toutes les sociétés actives */ /** GET /api/companies — Liste toutes les sociétés actives */
router.get('/', async (_req: AuthRequest, res: Response): Promise<void> => { router.get('/', wrap(async (_req: AuthRequest, res: Response): Promise<void> => {
const result = await db.query( const result = await db.query(
'SELECT * FROM companies WHERE is_active = TRUE ORDER BY name' 'SELECT * FROM companies WHERE is_active = TRUE ORDER BY name'
); );
res.json(result.rows); res.json(result.rows);
}); }));
/** POST /api/companies — Crée une société */ /** POST /api/companies — Crée une société */
router.post('/', validate(companySchema), async (req: AuthRequest, res: Response): Promise<void> => { router.post('/', validate(companySchema), wrap(async (req: AuthRequest, res: Response): Promise<void> => {
const { name, email } = req.body; const { name, email } = req.body;
const result = await db.query( const result = await db.query(
`INSERT INTO companies (name, email) VALUES ($1, $2) RETURNING *`, `INSERT INTO companies (name, email) VALUES ($1, $2) RETURNING *`,
[name, email] [name, email]
); );
res.status(201).json(result.rows[0]); res.status(201).json(result.rows[0]);
}); }));
/** PUT /api/companies/:id — Met à jour une société */ /** PUT /api/companies/:id — Met à jour une société */
router.put('/:id', validate(companySchema), async (req: AuthRequest, res: Response): Promise<void> => { router.put('/:id', validate(companySchema), wrap(async (req: AuthRequest, res: Response): Promise<void> => {
const { name, email } = req.body; const { name, email } = req.body;
const result = await db.query( const result = await db.query(
`UPDATE companies SET name=$1, email=$2, updated_at=NOW() `UPDATE companies SET name=$1, email=$2, updated_at=NOW()
@@ -40,15 +43,15 @@ router.put('/:id', validate(companySchema), async (req: AuthRequest, res: Respon
); );
if (!result.rows[0]) { res.status(404).json({ error: 'Société introuvable' }); return; } if (!result.rows[0]) { res.status(404).json({ error: 'Société introuvable' }); return; }
res.json(result.rows[0]); res.json(result.rows[0]);
}); }));
/** DELETE /api/companies/:id — Soft-delete (is_active = false) */ /** DELETE /api/companies/:id — Soft-delete (is_active = false) */
router.delete('/:id', async (req: AuthRequest, res: Response): Promise<void> => { router.delete('/:id', wrap(async (req: AuthRequest, res: Response): Promise<void> => {
await db.query( await db.query(
'UPDATE companies SET is_active=FALSE, updated_at=NOW() WHERE id=$1', 'UPDATE companies SET is_active=FALSE, updated_at=NOW() WHERE id=$1',
[req.params.id] [req.params.id]
); );
res.json({ success: true }); res.json({ success: true });
}); }));
export default router; export default router;
+26 -21
View File
@@ -1,4 +1,4 @@
import { Router, Response } from 'express'; import { Router, Response, NextFunction } from 'express';
import { z } from 'zod'; import { z } from 'zod';
import multer from 'multer'; import multer from 'multer';
import path from 'path'; import path from 'path';
@@ -14,6 +14,11 @@ import { addRowToExcel } from '../services/sharepoint';
const router = Router(); const router = Router();
router.use(requireAuth); router.use(requireAuth);
// Wrapper pour éviter les unhandledRejection qui crashent Node 20
function wrap(fn: (req: AuthRequest, res: Response, next: NextFunction) => Promise<void>) {
return (req: AuthRequest, res: Response, next: NextFunction) => fn(req, res, next).catch(next);
}
// ─── Upload images ──────────────────────────────────────────── // ─── Upload images ────────────────────────────────────────────
const storage = multer.diskStorage({ const storage = multer.diskStorage({
@@ -42,13 +47,13 @@ const upload = multer({
* Upload d'une photo de facture (avant soumission du formulaire). * Upload d'une photo de facture (avant soumission du formulaire).
* Retourne: { filename } * Retourne: { filename }
*/ */
router.post('/upload-image', upload.single('image'), async (req: AuthRequest, res: Response): Promise<void> => { router.post('/upload-image', upload.single('image'), wrap(async (req: AuthRequest, res: Response): Promise<void> => {
if (!req.file) { if (!req.file) {
res.status(400).json({ error: 'Aucun fichier reçu ou format non supporté (jpg/png/webp)' }); res.status(400).json({ error: 'Aucun fichier reçu ou format non supporté (jpg/png/webp)' });
return; return;
} }
res.json({ filename: req.file.filename }); res.json({ filename: req.file.filename });
}); }));
// ─── Schémas de validation ──────────────────────────────────── // ─── Schémas de validation ────────────────────────────────────
@@ -105,7 +110,7 @@ async function getInvoiceById(id: string) {
* Récapitulatif des montants par société × statut (En attente / Remboursé) * Récapitulatif des montants par société × statut (En attente / Remboursé)
* ⚠ Doit être AVANT /:id pour ne pas être capturé * ⚠ Doit être AVANT /:id pour ne pas être capturé
*/ */
router.get('/summary', async (req: AuthRequest, res: Response): Promise<void> => { router.get('/summary', wrap(async (req: AuthRequest, res: Response): Promise<void> => {
const result = await db.query( const result = await db.query(
`SELECT co.name AS company_name, i.status, `SELECT co.name AS company_name, i.status,
SUM(i.amount) AS total, SUM(i.amount) AS total,
@@ -118,13 +123,13 @@ router.get('/summary', async (req: AuthRequest, res: Response): Promise<void> =>
[req.user!.id] [req.user!.id]
); );
res.json(result.rows); res.json(result.rows);
}); }));
/** /**
* GET /api/invoices/export/csv * GET /api/invoices/export/csv
* Export CSV du listing filtré (mêmes filtres que GET /) * Export CSV du listing filtré (mêmes filtres que GET /)
*/ */
router.get('/export/csv', async (req: AuthRequest, res: Response): Promise<void> => { router.get('/export/csv', wrap(async (req: AuthRequest, res: Response): Promise<void> => {
const { company_ids, category_ids, status, date_from, date_to, search } = req.query; const { company_ids, category_ids, status, date_from, date_to, search } = req.query;
const conditions: string[] = ['i.user_id = $1']; const conditions: string[] = ['i.user_id = $1'];
@@ -177,13 +182,13 @@ router.get('/export/csv', async (req: AuthRequest, res: Response): Promise<void>
res.setHeader('Content-Type', 'text/csv; charset=utf-8'); res.setHeader('Content-Type', 'text/csv; charset=utf-8');
res.setHeader('Content-Disposition', `attachment; filename="factures-${new Date().toISOString().split('T')[0]}.csv"`); res.setHeader('Content-Disposition', `attachment; filename="factures-${new Date().toISOString().split('T')[0]}.csv"`);
res.send('' + csvLines.join('\n')); // BOM pour Excel res.send('' + csvLines.join('\n')); // BOM pour Excel
}); }));
/** /**
* GET /api/invoices * GET /api/invoices
* Liste paginée avec filtres combinables * Liste paginée avec filtres combinables
*/ */
router.get('/', async (req: AuthRequest, res: Response): Promise<void> => { router.get('/', wrap(async (req: AuthRequest, res: Response): Promise<void> => {
const { company_ids, category_ids, status, date_from, date_to, search, const { company_ids, category_ids, status, date_from, date_to, search,
sort_by, sort_dir, page, limit } = req.query; sort_by, sort_dir, page, limit } = req.query;
@@ -247,13 +252,13 @@ router.get('/', async (req: AuthRequest, res: Response): Promise<void> => {
page: pageNum, page: pageNum,
limit: limitNum, limit: limitNum,
}); });
}); }));
/** /**
* POST /api/invoices * POST /api/invoices
* Crée une facture (avec invités) * Crée une facture (avec invités)
*/ */
router.post('/', validate(createSchema), async (req: AuthRequest, res: Response): Promise<void> => { router.post('/', validate(createSchema), wrap(async (req: AuthRequest, res: Response): Promise<void> => {
const { company_id, category_id, supplier, amount, invoice_date, const { company_id, category_id, supplier, amount, invoice_date,
comment, images, add_to_tracking, guests } = req.body; comment, images, add_to_tracking, guests } = req.body;
@@ -287,7 +292,7 @@ router.post('/', validate(createSchema), async (req: AuthRequest, res: Response)
} finally { } finally {
client.release(); client.release();
} }
}); }));
/** /**
* POST /api/invoices/:id/send * POST /api/invoices/:id/send
@@ -296,7 +301,7 @@ router.post('/', validate(createSchema), async (req: AuthRequest, res: Response)
* 3. Ajoute au fichier Excel SharePoint (si add_to_tracking = true) * 3. Ajoute au fichier Excel SharePoint (si add_to_tracking = true)
* 4. Met à jour la facture en BDD * 4. Met à jour la facture en BDD
*/ */
router.post('/:id/send', async (req: AuthRequest, res: Response): Promise<void> => { router.post('/:id/send', wrap(async (req: AuthRequest, res: Response): Promise<void> => {
const invoice = await getInvoiceById(req.params.id); const invoice = await getInvoiceById(req.params.id);
if (!invoice) { res.status(404).json({ error: 'Facture introuvable' }); return; } if (!invoice) { res.status(404).json({ error: 'Facture introuvable' }); return; }
if (invoice.user_id !== req.user!.id) { res.status(403).json({ error: 'Accès refusé' }); return; } if (invoice.user_id !== req.user!.id) { res.status(403).json({ error: 'Accès refusé' }); return; }
@@ -361,25 +366,25 @@ router.post('/:id/send', async (req: AuthRequest, res: Response): Promise<void>
); );
res.json(await getInvoiceById(invoice.id)); res.json(await getInvoiceById(invoice.id));
}); }));
/** /**
* GET /api/invoices/:id * GET /api/invoices/:id
*/ */
router.get('/:id', async (req: AuthRequest, res: Response): Promise<void> => { router.get('/:id', wrap(async (req: AuthRequest, res: Response): Promise<void> => {
const invoice = await getInvoiceById(req.params.id); const invoice = await getInvoiceById(req.params.id);
if (!invoice || invoice.user_id !== req.user!.id) { if (!invoice || invoice.user_id !== req.user!.id) {
res.status(404).json({ error: 'Facture introuvable' }); res.status(404).json({ error: 'Facture introuvable' });
return; return;
} }
res.json(invoice); res.json(invoice);
}); }));
/** /**
* PATCH /api/invoices/:id/status * PATCH /api/invoices/:id/status
* Toggle du statut (pending ↔ reimbursed) * Toggle du statut (pending ↔ reimbursed)
*/ */
router.patch('/:id/status', async (req: AuthRequest, res: Response): Promise<void> => { router.patch('/:id/status', wrap(async (req: AuthRequest, res: Response): Promise<void> => {
const { status } = req.body; const { status } = req.body;
if (!['pending', 'reimbursed'].includes(status)) { if (!['pending', 'reimbursed'].includes(status)) {
res.status(400).json({ error: 'Statut invalide (pending ou reimbursed)' }); res.status(400).json({ error: 'Statut invalide (pending ou reimbursed)' });
@@ -391,13 +396,13 @@ router.patch('/:id/status', async (req: AuthRequest, res: Response): Promise<voi
); );
if (!result.rows[0]) { res.status(404).json({ error: 'Facture introuvable' }); return; } if (!result.rows[0]) { res.status(404).json({ error: 'Facture introuvable' }); return; }
res.json(result.rows[0]); res.json(result.rows[0]);
}); }));
/** /**
* GET /api/invoices/:id/pdf * GET /api/invoices/:id/pdf
* Téléchargement du PDF généré * Téléchargement du PDF généré
*/ */
router.get('/:id/pdf', async (req: AuthRequest, res: Response): Promise<void> => { router.get('/:id/pdf', wrap(async (req: AuthRequest, res: Response): Promise<void> => {
const result = await db.query( const result = await db.query(
'SELECT pdf_path, pdf_filename FROM invoices WHERE id=$1 AND user_id=$2', 'SELECT pdf_path, pdf_filename FROM invoices WHERE id=$1 AND user_id=$2',
[req.params.id, req.user!.id] [req.params.id, req.user!.id]
@@ -407,13 +412,13 @@ router.get('/:id/pdf', async (req: AuthRequest, res: Response): Promise<void> =>
const fullPath = path.join(config.uploadsDir, invoice.pdf_path); const fullPath = path.join(config.uploadsDir, invoice.pdf_path);
res.download(fullPath, invoice.pdf_filename); res.download(fullPath, invoice.pdf_filename);
}); }));
/** /**
* DELETE /api/invoices/:id * DELETE /api/invoices/:id
* Supprime la facture + nettoyage des fichiers * Supprime la facture + nettoyage des fichiers
*/ */
router.delete('/:id', async (req: AuthRequest, res: Response): Promise<void> => { router.delete('/:id', wrap(async (req: AuthRequest, res: Response): Promise<void> => {
const result = await db.query( const result = await db.query(
'DELETE FROM invoices WHERE id=$1 AND user_id=$2 RETURNING id, pdf_path, images', 'DELETE FROM invoices WHERE id=$1 AND user_id=$2 RETURNING id, pdf_path, images',
[req.params.id, req.user!.id] [req.params.id, req.user!.id]
@@ -433,6 +438,6 @@ router.delete('/:id', async (req: AuthRequest, res: Response): Promise<void> =>
} }
res.json({ success: true }); res.json({ success: true });
}); }));
export default router; export default router;
+14 -11
View File
@@ -1,4 +1,4 @@
import { Router, Response } from 'express'; import { Router, Response, NextFunction } from 'express';
import { z } from 'zod'; import { z } from 'zod';
import { db } from '../db'; import { db } from '../db';
import { requireAuth, AuthRequest } from '../middleware/auth'; import { requireAuth, AuthRequest } from '../middleware/auth';
@@ -8,6 +8,9 @@ import { encrypt } from '../crypto';
const router = Router(); const router = Router();
router.use(requireAuth); router.use(requireAuth);
const wrap = (fn: (req: AuthRequest, res: Response, next: NextFunction) => Promise<void>) =>
(req: AuthRequest, res: Response, next: NextFunction) => fn(req, res, next).catch(next);
// ─── Schémas ───────────────────────────────────────────────── // ─── Schémas ─────────────────────────────────────────────────
const smtpSchema = z.object({ const smtpSchema = z.object({
@@ -32,7 +35,7 @@ const appSettingsSchema = z.object({
// ─── Routes SMTP ───────────────────────────────────────────── // ─── Routes SMTP ─────────────────────────────────────────────
/** GET /api/settings/smtp — Config SMTP de l'utilisateur (sans mot de passe) */ /** GET /api/settings/smtp — Config SMTP de l'utilisateur (sans mot de passe) */
router.get('/smtp', async (req: AuthRequest, res: Response): Promise<void> => { router.get('/smtp', wrap(async (req: AuthRequest, res: Response): Promise<void> => {
const result = await db.query( const result = await db.query(
`SELECT smtp_host, smtp_port, smtp_secure, smtp_user, `SELECT smtp_host, smtp_port, smtp_secure, smtp_user,
smtp_from_name, smtp_from_email, smtp_from_name, smtp_from_email,
@@ -41,10 +44,10 @@ router.get('/smtp', async (req: AuthRequest, res: Response): Promise<void> => {
[req.user!.id] [req.user!.id]
); );
res.json(result.rows[0]); res.json(result.rows[0]);
}); }));
/** PUT /api/settings/smtp — Sauvegarde la config SMTP */ /** PUT /api/settings/smtp — Sauvegarde la config SMTP */
router.put('/smtp', validate(smtpSchema), async (req: AuthRequest, res: Response): Promise<void> => { router.put('/smtp', validate(smtpSchema), wrap(async (req: AuthRequest, res: Response): Promise<void> => {
const { smtp_host, smtp_port, smtp_secure, smtp_user, smtp_pass, smtp_from_name, smtp_from_email } = req.body; const { smtp_host, smtp_port, smtp_secure, smtp_user, smtp_pass, smtp_from_name, smtp_from_email } = req.body;
if (smtp_pass) { if (smtp_pass) {
@@ -66,10 +69,10 @@ router.put('/smtp', validate(smtpSchema), async (req: AuthRequest, res: Response
} }
res.json({ success: true }); res.json({ success: true });
}); }));
/** POST /api/settings/smtp/test — Envoie un email de test */ /** POST /api/settings/smtp/test — Envoie un email de test */
router.post('/smtp/test', async (req: AuthRequest, res: Response): Promise<void> => { router.post('/smtp/test', wrap(async (req: AuthRequest, res: Response): Promise<void> => {
const userResult = await db.query('SELECT * FROM users WHERE id=$1', [req.user!.id]); const userResult = await db.query('SELECT * FROM users WHERE id=$1', [req.user!.id]);
const user = userResult.rows[0]; const user = userResult.rows[0];
@@ -85,12 +88,12 @@ router.post('/smtp/test', async (req: AuthRequest, res: Response): Promise<void>
} catch (err: any) { } catch (err: any) {
res.status(500).json({ error: `Échec SMTP : ${err.message}` }); res.status(500).json({ error: `Échec SMTP : ${err.message}` });
} }
}); }));
// ─── Routes paramètres application (Microsoft Graph) ───────── // ─── Routes paramètres application (Microsoft Graph) ─────────
/** GET /api/settings/app — Config Graph + SharePoint (sans secret) */ /** GET /api/settings/app — Config Graph + SharePoint (sans secret) */
router.get('/app', async (_req: AuthRequest, res: Response): Promise<void> => { router.get('/app', wrap(async (_req: AuthRequest, res: Response): Promise<void> => {
const result = await db.query( const result = await db.query(
`SELECT key, value FROM app_settings `SELECT key, value FROM app_settings
WHERE key IN ( WHERE key IN (
@@ -106,10 +109,10 @@ router.get('/app', async (_req: AuthRequest, res: Response): Promise<void> => {
); );
settings.has_secret = secretResult.rows[0] ? 'true' : 'false'; settings.has_secret = secretResult.rows[0] ? 'true' : 'false';
res.json(settings); res.json(settings);
}); }));
/** PUT /api/settings/app — Sauvegarde la config Microsoft Graph + SharePoint */ /** PUT /api/settings/app — Sauvegarde la config Microsoft Graph + SharePoint */
router.put('/app', validate(appSettingsSchema), async (req: AuthRequest, res: Response): Promise<void> => { router.put('/app', validate(appSettingsSchema), wrap(async (req: AuthRequest, res: Response): Promise<void> => {
const { const {
graph_tenant_id, graph_client_id, graph_client_secret, graph_tenant_id, graph_client_id, graph_client_secret,
sharepoint_site_id, sharepoint_item_id, sharepoint_sheet_name, sharepoint_site_id, sharepoint_item_id, sharepoint_sheet_name,
@@ -135,6 +138,6 @@ router.put('/app', validate(appSettingsSchema), async (req: AuthRequest, res: Re
} }
res.json({ success: true }); res.json({ success: true });
}); }));
export default router; export default router;