Skip to content
This repository was archived by the owner on Jul 20, 2026. It is now read-only.

Commit 33cc08c

Browse files
Merge pull request #115 from TheRefraction/feature-admin-middleware
Feature admin middleware
2 parents 092c7cb + 3ee0510 commit 33cc08c

23 files changed

Lines changed: 327 additions & 131 deletions

backend/db/mongo/migrations/01-invoice.js

Lines changed: 15 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -3,131 +3,53 @@
33
*/
44

55
const migrate = (db, helpers) => {
6-
const { createOrModify, ensureIndex } = helpers;
6+
const { createOrModify } = helpers;
77

88
const invoiceOptionSchema = {
99
bsonType: "object",
1010
required: ["name", "item"],
1111
description: "An option slot for a product",
1212
properties: {
1313
name: {
14-
bsonType: "string",
15-
description: "Name of the current option slot"
14+
bsonType: "string"
1615
},
1716
item: {
1817
bsonType: "object",
1918
required: ["name", "delta", "quantity"],
20-
description: "The chosen option",
2119
properties: {
22-
name: {
23-
bsonType: "string",
24-
description: "Name of product"
25-
},
26-
delta: {
27-
bsonType: "double",
28-
description: "Price increase or decrease on the product"
29-
},
30-
quantity: {
31-
bsonType: "int",
32-
description: "Number of options selected (between min_select and max_select)",
33-
minimum: 0
34-
},
20+
name: { bsonType: "string" },
21+
delta: { bsonType: "double" },
22+
quantity: { bsonType: "int", minimum: 0 }
3523
}
3624
}
3725
}
3826
};
3927

40-
const invoiceOptionsSchema = {
41-
bsonType: "array",
42-
description: "Array of options on a given product (optional)",
43-
items: invoiceOptionSchema
44-
};
45-
46-
const invoiceMenuSlotSchema = {
47-
bsonType: "object",
48-
required: ["name", "item"],
49-
description: "A menu slot",
50-
properties: {
51-
name: {
52-
bsonType: "string",
53-
description: "Name of the current slot in the menu"
54-
},
55-
item: {
56-
bsonType: "object",
57-
required: ["name", "delta", "quantity"],
58-
description: "What product has been chosen for this slot",
59-
properties: {
60-
name: {
61-
bsonType: "string",
62-
description: "Name of product"
63-
},
64-
delta: {
65-
bsonType: "double",
66-
description: "Price increase or decrease on the product"
67-
},
68-
quantity: {
69-
bsonType: "int",
70-
description: "Number of products selected (between min_select and max_select)",
71-
minimum: 0 // For instance, a dessert can be optional whence the threshold
72-
},
73-
74-
// Selected options if any (optional)
75-
options: invoiceOptionsSchema
76-
}
77-
}
78-
}
79-
};
80-
81-
const invoiceMenuSlotsSchema = {
82-
bsonType: "array",
83-
description: "Used when the item is a menu. Array of menu slots (optional)",
84-
items: invoiceMenuSlotSchema
85-
};
86-
8728
const invoiceItemSchema = {
8829
bsonType: "object",
8930
required: ["type", "name", "price", "quantity"],
90-
description: "A given line in the invoice which corresponds to a given item",
9131
properties: {
9232
type: {
93-
bsonType: "string",
94-
enum: ["product", "menu"],
95-
description: "Current item is either a product or a menu"
96-
},
97-
name: {
98-
bsonType: "string",
99-
description: "This is the name of the item"
33+
enum: ["product"], // Restreint au type "product" uniquement
34+
bsonType: "string"
10035
},
101-
price: {
102-
bsonType: "double",
103-
description: "The unit price, must be non negative", // For a menu, the prices of products within are not taken into account
104-
minimum: 0
105-
},
106-
quantity: {
107-
bsonType: "int",
108-
description: "Quantity of the ordered item, at least 1",
109-
minimum: 1
110-
},
111-
112-
// Present when type=product
113-
options: invoiceOptionsSchema,
114-
115-
// Present when type=menu
116-
slots: invoiceMenuSlotsSchema
36+
name: { bsonType: "string" },
37+
price: { bsonType: "double", minimum: 0 },
38+
quantity: { bsonType: "int", minimum: 1 },
39+
options: {
40+
bsonType: "array",
41+
items: invoiceOptionSchema
42+
}
11743
}
11844
};
11945

12046
const invoiceSchema = {
12147
bsonType: "object",
12248
required: ["_id", "items"],
12349
properties: {
124-
_id: {
125-
bsonType: "int",
126-
description: "Mirrors the Postgres invoice.id"
127-
},
50+
_id: { bsonType: "int" },
12851
items: {
12952
bsonType: "array",
130-
description: "An array of invoices items (at least one line)",
13153
minItems: 1,
13254
items: invoiceItemSchema
13355
}

backend/package-lock.json

Lines changed: 18 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

backend/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"jsonwebtoken": "^9.0.2",
1717
"mongodb": "^6.8.0",
1818
"morgan": "^1.10.0",
19+
"node-cron": "^4.4.1",
1920
"pg": "^8.12.0"
2021
},
2122
"devDependencies": {
@@ -25,6 +26,7 @@
2526
"@types/jsonwebtoken": "^9.0.4",
2627
"@types/morgan": "^1.9.8",
2728
"@types/node": "^20.19.42",
29+
"@types/node-cron": "^3.0.11",
2830
"@types/pg": "^8.11.6",
2931
"tsx": "^4.16.0",
3032
"typescript": "^5.5.0"

backend/src/app.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import morgan from 'morgan';
55
import routes from './routes';
66

77
import { errorMiddleware, AppError } from './middlewares/error.middleware';
8+
import { requestLogger } from './middlewares/logger.middleware';
89
import { connectMongoDB } from './config/mongo';
910
import { pgPool } from './config/postgres';
1011
import { env } from './config/env';
@@ -45,6 +46,7 @@ class App {
4546
this.app.use(express.json());
4647
this.app.use(express.urlencoded({ extended: true }));
4748
this.app.use(morgan('combined'));
49+
this.app.use(requestLogger);
4850
}
4951

5052
private initializeRoutes(): void {

backend/src/controllers/invoice.controller.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,16 @@ import { InvoiceStatus } from '../models/invoice.model';
99
export class InvoiceController extends BaseController {
1010
constructor(private invoiceSvc: InvoiceService, private paymentSvc: PaymentService) { super(); }
1111

12+
getAll = async(req: Request, res: Response, next: NextFunction): Promise<void> => {
13+
try {
14+
const fullInvoice = await this.invoiceSvc.getAll();
15+
16+
this.sendResponse(res, HttpStatus.OK, 'Invoices retrieved', fullInvoice);
17+
} catch (error) {
18+
next(error);
19+
}
20+
}
21+
1222
getById = async(req: Request, res: Response, next: NextFunction): Promise<void> => {
1323
try {
1424
const invoiceId = parseInt(req.params.id);

backend/src/controllers/product.controller.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ export class ProductController extends BaseController {
1414

1515
getAll = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
1616
try {
17-
const products = await this.productFcd.getAllFullProducts();
17+
const showHidden = req.query.showHidden !== 'false';
18+
19+
const products = await this.productFcd.getAllFullProducts(showHidden);
1820

1921
this.sendResponse(res, HttpStatus.OK, 'Products retrieved successfully', products);
2022
} catch (error) {

backend/src/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
import app from './app';
22
import { env } from './config/env';
3+
import { initAccountCleaner } from './jobs/accountCleaner';
34

45
const PORT = 3000;
56

67
(async () => {
78
await app.init();
89

10+
initAccountCleaner();
11+
912
const server = app.getApp().listen(PORT, () => {
1013
console.log(`Server running on port ${PORT}`);
1114
console.log(`Environment: ${env.NODE_ENV}`);
12-
console.log(`Health check: http://localhost:${PORT}/health`);
15+
console.log(`Health check: http://localhost:${PORT}/api/health`);
1316
});
1417

1518
const gracefulShutdown = () => {

backend/src/jobs/accountCleaner.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import cron from 'node-cron';
2+
import { pgPool } from '../config/postgres';
3+
4+
export const initAccountCleaner = () => {
5+
// Planifié tous les jours à 03h00 du matin
6+
cron.schedule('0 3 * * *', async () => {
7+
console.log('[AccountCleaner] Démarrage de la purge des comptes inactifs...');
8+
9+
try {
10+
// Note: Assurez-vous que vos FK ont 'ON DELETE CASCADE'
11+
// pour supprimer les factures/données liées automatiquement.
12+
const query = `
13+
DELETE FROM account
14+
WHERE last_login < NOW() - INTERVAL '2 years'
15+
OR (last_login IS NULL AND created_at < NOW() - INTERVAL '2 years');
16+
`;
17+
18+
const result = await pgPool.query(query);
19+
20+
console.log(`[AccountCleaner] Succès : ${result.rowCount} comptes supprimés.`);
21+
} catch (error) {
22+
console.error('[AccountCleaner] Erreur lors de la suppression :', error);
23+
}
24+
});
25+
};

backend/src/middlewares/auth.middleware.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,4 +49,16 @@ export const adminMiddleware = async (
4949
return;
5050
}
5151
next();
52-
};
52+
};
53+
54+
/*
55+
UNUSED
56+
export const authorize = (roles: Role[]) => {
57+
return (req: AuthRequest, res: Response, next: NextFunction) => {
58+
if (!req.user || !roles.includes(req.user.role)) {
59+
res.status(HttpStatus.FORBIDDEN).json({ success: false, message: 'Forbidden' });
60+
return;
61+
}
62+
next();
63+
};
64+
};*/
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { Request, Response, NextFunction } from 'express';
2+
import { getCollection } from '../config/mongo';
3+
import { AuthRequest } from './auth.middleware'; // Réutilisation de votre type
4+
5+
export const requestLogger = async (
6+
req: AuthRequest,
7+
res: Response,
8+
next: NextFunction
9+
) => {
10+
const start = Date.now();
11+
12+
res.on('finish', async () => {
13+
const duration = Date.now() - start;
14+
15+
try {
16+
const logsCollection = getCollection<any>('logs');
17+
18+
// Création de l'objet avec des valeurs par défaut pour éviter les "undefined"
19+
const logEntry = {
20+
timestamp: new Date(),
21+
method: req.method || 'GET',
22+
path: req.originalUrl || req.path || '/',
23+
status: res.statusCode || 0,
24+
duration: Math.round(duration),
25+
// userId est optionnel, donc on ne le met que s'il existe
26+
...(req.user?.userId && { userId: String(req.user.userId) }),
27+
ip: req.ip || '127.0.0.1'
28+
};
29+
30+
await logsCollection.insertOne(logEntry);
31+
} catch (error) {
32+
// Loguer l'erreur réelle pour comprendre quel champ manque
33+
console.error('Failed to log request:', error);
34+
}
35+
});
36+
37+
next();
38+
};

0 commit comments

Comments
 (0)