"fix-wrap-all-async-handlers"
This commit is contained in:
@@ -104,19 +104,22 @@ 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> => {
|
||||||
|
try {
|
||||||
const { refreshToken } = req.body;
|
const { refreshToken } = req.body;
|
||||||
if (refreshToken) {
|
if (refreshToken) {
|
||||||
await db.query('DELETE FROM refresh_tokens WHERE token_hash = $1', [hashToken(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> => {
|
||||||
|
try {
|
||||||
const result = await db.query(
|
const result = await db.query(
|
||||||
`SELECT id, name, email,
|
`SELECT id, name, email,
|
||||||
smtp_host, smtp_port, smtp_secure, smtp_user,
|
smtp_host, smtp_port, smtp_secure, smtp_user,
|
||||||
@@ -125,6 +128,7 @@ router.get('/me', requireAuth, async (req: AuthRequest, res: Response): Promise<
|
|||||||
[req.user!.id]
|
[req.user!.id]
|
||||||
);
|
);
|
||||||
res.json(result.rows[0]);
|
res.json(result.rows[0]);
|
||||||
|
} catch (err) { next(err); }
|
||||||
});
|
});
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user