diff --git a/.vscode/settings.json b/.vscode/settings.json index ba6e793db..1460e3fd8 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,6 +2,7 @@ "github.copilot.chat.codeGeneration.useInstructionFiles": true, "github.copilot.enable": { "javascript": true, - "markdown": true + "markdown": true, + "json": true } } diff --git a/MIGRATIONS.md b/MIGRATIONS.md new file mode 100644 index 000000000..63dfaaca8 --- /dev/null +++ b/MIGRATIONS.md @@ -0,0 +1,160 @@ +# Migrations + +Breaking changes and upgrade notes for downstream projects. + +--- + +## `acl` → `@casl/ability` (2026-02-20) + +`acl@0.4.11` (unmaintained since 2018) has been replaced by `@casl/ability`. + +### What changed + +- `lib/middlewares/policy.js` no longer exports `Acl`. +- Policy files now call `policy.registerRules([...])` instead of `policy.Acl.allow([...])`. +- `isAllowed` and `isOwner` middleware signatures are **unchanged** — routes do not need to be updated. + +### HTTP method → CASL action mapping + +| HTTP method | CASL action | +|-----------------|-------------| +| `GET` | `read` | +| `POST` | `create` | +| `PUT` / `PATCH` | `update` | +| `DELETE` | `delete` | +| `*` (all) | `manage` | + +### Migration example + +**Before (`acl`):** + +```js +import policy from '../../../lib/middlewares/policy.js'; + +const invokeRolesPolicies = () => { + policy.Acl.allow([ + { + roles: ['user'], + allows: [ + { resources: '/api/tasks', permissions: '*' }, + { resources: '/api/tasks/:taskId', permissions: '*' }, + ], + }, + { + roles: ['guest'], + allows: [ + { resources: '/api/tasks/stats', permissions: ['get'] }, + { resources: '/api/tasks', permissions: ['get'] }, + { resources: '/api/tasks/:taskId', permissions: ['get'] }, + ], + }, + ]); +}; + +export default { invokeRolesPolicies }; +``` + +**After (`@casl/ability`):** + +```js +import policy from '../../../lib/middlewares/policy.js'; + +const invokeRolesPolicies = () => { + policy.registerRules([ + { roles: ['user'], actions: 'manage', subject: '/api/tasks' }, + { roles: ['user'], actions: 'manage', subject: '/api/tasks/:taskId' }, + { roles: ['guest'], actions: ['read'], subject: '/api/tasks/stats' }, + { roles: ['guest'], actions: ['read'], subject: '/api/tasks' }, + { roles: ['guest'], actions: ['read'], subject: '/api/tasks/:taskId' }, + ]); +}; + +export default { invokeRolesPolicies }; +``` + +### `defineAbilityFor` is now async + +`policy.defineAbilityFor(user)` returns a `Promise` (lazy-loads `@casl/ability` on first call). Express `isAllowed` middleware is `async` and works unchanged. If you test `defineAbilityFor` directly, `await` it: + +```js +// Unit test +const ability = await policy.defineAbilityFor(null); +expect(ability.can('read', '/api/tasks')).toBe(true); +``` + +> **Jest note**: `policy.js` must be a static top-level import in the test file (not only reached via dynamic `import()`). This pre-loads the module in Jest's VM registry before policy files are dynamically imported in `beforeAll`. +> ```js +> import policy from '../../../lib/middlewares/policy.js'; // required at top level +> ``` + +### Steps for downstream projects + +1. `npm remove acl && npm install @casl/ability` +2. Update every `modules/*/policies/*.policy.js` following the pattern above. +3. Remove any direct use of `policy.Acl` (it is no longer exported). +4. If you have unit tests that call `defineAbilityFor`, add `import policy from '...policy.js'` as a top-level static import and `await` the call. +5. Run `npm run lint && npm test` — all existing 403/200 assertions should pass unchanged. + +--- + +## `@hapi/joi` → `zod@3` + `body-parser` / `swig` removed (2026-02-21) + +`@hapi/joi` (abandoned), `body-parser` (built into Express 4.16+), `swig` and `consolidate` (template engine, unused in API-only mode) have been removed. + +### What changed + +- `lib/helpers/joi.js` deleted → `lib/helpers/zod.js` (zxcvbn `superRefine` helper). +- `lib/middlewares/model.js`: `getResultFromJoi(body, schema, options)` → `getResultFromZod(body, schema)` (no options arg). +- `model.isValid(schema)` middleware interface is **unchanged** — routes do not need updating. +- `config.joi` renamed to `config.validation`; `validationOptions` key removed (Zod handles stripping and defaults internally). +- PUT routes should use a `.partial()` schema (`TaskUpdate`, `UserUpdate`) for partial updates. + +### Migration example + +**Before (`@hapi/joi`):** + +```js +import Joi from '@hapi/joi'; + +const TaskSchema = Joi.object().keys({ + title: Joi.string().trim().default('').required(), + description: Joi.string().allow('').default('').required(), +}); + +export default { Task: TaskSchema }; +``` + +**After (`zod@3`):** + +```js +import { z } from 'zod'; + +const Task = z.object({ + title: z.string().trim().min(1), + description: z.string().default(''), +}).strip(); + +const TaskUpdate = Task.partial(); + +export default { Task, TaskUpdate }; +``` + +### Unit tests + +Replace `schema.Task.validate(data, options)` with `schema.Task.safeParse(data)`. The result shape changes: + +| | Joi | Zod | +|---|---|---| +| Success | `{ value: T, error: undefined }` | `{ success: true, data: T }` | +| Failure | `{ value: T, error: ValidationError }` | `{ success: false, error: ZodError }` | + +Assertions like `expect(result.error).toBeFalsy()` / `.toBeDefined()` work unchanged. To verify field stripping, check `result.data?.unknownField` (not `result.unknownField`). + +### Steps for downstream projects + +1. `npm remove @hapi/joi body-parser swig consolidate && npm install zod@3` +2. Rewrite `modules/*/models/*.schema.js` using the Zod pattern above. +3. If you call `model.getResultFromJoi(body, schema, options)` directly, replace with `model.getResultFromZod(body, schema)`. +4. Rename `config.joi` → `config.validation` in all `config/defaults/*.js`; remove `validationOptions`. +5. Update unit tests from `.validate()` to `.safeParse()`. +6. Run `npm run lint && npm test` — all existing 422/200 assertions should pass unchanged. diff --git a/config/defaults/development.js b/config/defaults/development.js index fd3140d98..83a039de1 100644 --- a/config/defaults/development.js +++ b/config/defaults/development.js @@ -159,6 +159,19 @@ const config = { maxSize: 126, // max password size minimumScore: 3, // min password complexity score }, + cookie: { + secure: false, // false in dev (HTTP localhost) + sameSite: 'strict', + }, + rateLimit: { + auth: { + windowMs: 15 * 60 * 1000, // 15 min + max: 20, // 20 requests per window in dev (more lenient) + message: { message: 'Too many requests, please try again later.' }, + standardHeaders: true, + legacyHeaders: false, + }, + }, // jwt is for token authentification jwt: { secret: 'WaosSecretKeyExampleToChnageAbsolutely', // secret for hash @@ -189,17 +202,10 @@ const config = { privateKeyLocation: null, }, }, - // joi is used to manage schema restrictions, on the top of mongo / orm - joi: { + // validation is used to manage schema restrictions, on the top of mongo / orm + validation: { // enabled HTTP methods for request data validation supportedMethods: ['post', 'put'], - // Joi validation options - validationOptions: { - abortEarly: false, // abort after the last validation error - allowUnknown: true, // allow unknown keys that will be ignored - stripUnknown: true, // remove unknown keys from the validated data - noDefaults: false, // automatically set to true for put method (update) - }, }, seedDB: { seed: true, diff --git a/config/defaults/production.js b/config/defaults/production.js index 78a1fe889..689aa7a9f 100644 --- a/config/defaults/production.js +++ b/config/defaults/production.js @@ -20,6 +20,19 @@ export default _.merge(config.default, { certificate: './config/sslcerts/cert.pem', caBundle: './config/sslcerts/cabundle.crt', }, + cookie: { + secure: true, // HTTPS only in prod + sameSite: 'strict', + }, + rateLimit: { + auth: { + windowMs: 15 * 60 * 1000, + max: 10, // stricter in prod + message: { message: 'Too many requests, please try again later.' }, + standardHeaders: true, + legacyHeaders: false, + }, + }, log: { format: 'custom', pattern: diff --git a/config/defaults/test.js b/config/defaults/test.js index e4d776d45..cfd7ca0d8 100644 --- a/config/defaults/test.js +++ b/config/defaults/test.js @@ -20,4 +20,9 @@ export default _.merge(config.default, { }, }, }, + rateLimit: { + auth: { + max: Number.MAX_SAFE_INTEGER, // disable rate limiting in tests + }, + }, }); diff --git a/lib/helpers/zod.js b/lib/helpers/zod.js new file mode 100644 index 000000000..fe6dc9852 --- /dev/null +++ b/lib/helpers/zod.js @@ -0,0 +1,19 @@ +/** + * Module dependencies + */ +import zxcvbn from 'zxcvbn'; +import config from '../../config/index.js'; +import { z } from 'zod'; + +/** + * @desc Zod superRefine for zxcvbn password strength + */ +const passwordRefinement = (val, ctx) => { + if (config.zxcvbn.forbiddenPasswords.includes(val)) { + ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'password is too common' }); + } else if (zxcvbn(val).score < config.zxcvbn.minimumScore) { + ctx.addIssue({ code: z.ZodIssueCode.custom, message: `password must have a strength of at least ${config.zxcvbn.minimumScore}` }); + } +}; + +export default { passwordRefinement }; diff --git a/lib/middlewares/model.js b/lib/middlewares/model.js index 05f3106c4..5cfd50cd9 100644 --- a/lib/middlewares/model.js +++ b/lib/middlewares/model.js @@ -15,64 +15,53 @@ const cleanError = (string) => .trim(); /** - * get Joi result + * get Zod result */ -const getResultFromJoi = (body, schema, options) => - schema.validate(body, options, (err, data) => { - if (err) { - const output = { - status: 'failed', - error: { - original: err._object, - // fetch only message and type from each error - details: _.map(err.details, ({ message, type }) => ({ - message: message.replace(/['"]/g, ''), - type, - })), - }, - }; - return output; - } - return data; - }); +const getResultFromZod = (body, schema) => { + const result = schema.safeParse(body); + if (!result.success) { + return { + error: { + original: body, + _original: body, + details: result.error.issues.map(({ message, code }) => ({ + message: message.replace(/['"]/g, ''), + type: code, + })), + }, + }; + } + return { value: result.data }; +}; /** * check error and return if needed */ const checkError = (result) => { if (result && result.error) { - if (result.error.original && (result.error.original.password || result.error.original.firstname)) + if (result.error.original && (result.error.original.password || result.error.original.firstName)) result.error.original = _.pick(result.error.original, config.whitelists.users.default); + if (result.error._original && (result.error._original.password || result.error._original.firstName)) + result.error._original = _.pick(result.error._original, config.whitelists.users.default); let description = ''; result.error.details.forEach((err) => { const message = cleanError(err.message); description += `${message.charAt(0).toUpperCase() + message.slice(1).toLowerCase()}. `; }); - - if (result.error._original && (result.error._original.password || result.error._original.firstname)) - result.error._original = _.pick(result.error._original, config.whitelists.users.default); return description; } return false; }; /** - * Check model is Valid with Joi schema + * Check model is Valid with Zod schema */ const isValid = (schema) => (req, res, next) => { const method = req.method.toLowerCase(); - const options = _.clone(config.joi.validationOptions); - if (_.includes(config.joi.supportedMethods, method)) { - if (method === 'put') { - options.noDefaults = true; - } - // Validate req.body using the schema and validation options - const result = getResultFromJoi(req.body, schema, options); - // check error + if (_.includes(config.validation.supportedMethods, method)) { + const result = getResultFromZod(req.body, schema); const error = checkError(result); if (error) return responses.error(res, 422, 'Schema validation error', error)(result.error); - - // else return req.body with the data after Joi validation req.body = result.value; return next(); } @@ -81,7 +70,7 @@ const isValid = (schema) => (req, res, next) => { export default { cleanError, - getResultFromJoi, + getResultFromZod, checkError, isValid, }; diff --git a/lib/middlewares/policy.js b/lib/middlewares/policy.js index 8d82d0b6e..b6adc7e3c 100644 --- a/lib/middlewares/policy.js +++ b/lib/middlewares/policy.js @@ -1,11 +1,35 @@ /** * Module dependencies */ -import ACL from 'acl'; import responses from '../helpers/responses.js'; -/* eslint new-cap: 0 */ -const Acl = new ACL(new ACL.memoryBackend()); // Using the memory backend +const methodToAction = { + get: 'read', post: 'create', put: 'update', patch: 'update', delete: 'delete', +}; + +// Global rules registry — populated at startup by each policy file +const rulesRegistry = []; + +const registerRules = (rules) => rulesRegistry.push(...rules); + +// Lazy CASL loader — dynamic import avoids Jest's experimental VM module linker +let _casl = null; +const loadCasl = async () => { + if (!_casl) _casl = await import('@casl/ability'); + return _casl; +}; + +const defineAbilityFor = async (user) => { + const { AbilityBuilder, Ability } = await loadCasl(); + const { can, build } = new AbilityBuilder(Ability); + const roles = user ? user.roles : ['guest']; + for (const rule of rulesRegistry) { + if (rule.roles.some((r) => roles.includes(r))) { + can(rule.actions, rule.subject); + } + } + return build(); +}; /** * @desc MiddleWare to check if user is allowed @@ -13,13 +37,11 @@ const Acl = new ACL(new ACL.memoryBackend()); // Using the memory backend * @param {Object} res - Express response object * @param {Function} next - Express next middleware function */ -const isAllowed = (req, res, next) => { - const roles = req.user ? req.user.roles : ['guest']; - Acl.areAnyRolesAllowed(roles, req.route.path, req.method.toLowerCase(), (err, isAllowed) => { - if (err) return responses.error(res, 500, 'Server Error', 'Unexpected authorization error')(err); // An authorization error occurred - if (isAllowed) return next(); // Access granted! Invoke next middleware - return responses.error(res, 403, 'Unauthorized', 'User is not authorized')(); - }); +const isAllowed = async (req, res, next) => { + const ability = await defineAbilityFor(req.user); + const action = methodToAction[req.method.toLowerCase()]; + if (ability.can(action, req.route.path)) return next(); + return responses.error(res, 403, 'Unauthorized', 'User is not authorized')(); }; /** @@ -36,7 +58,8 @@ const isOwner = (req, res, next) => { }; export default { - Acl, + registerRules, + defineAbilityFor, isAllowed, isOwner, }; diff --git a/lib/services/express.js b/lib/services/express.js index d7c0335f3..b1a745030 100644 --- a/lib/services/express.js +++ b/lib/services/express.js @@ -4,7 +4,6 @@ * Module dependencies. */ import express from 'express'; -import bodyParser from 'body-parser'; import compress from 'compression'; import methodOverride from 'method-override'; import cookieParser from 'cookie-parser'; @@ -13,7 +12,6 @@ import path from 'path'; import _ from 'lodash'; import lusca from 'lusca'; import cors from 'cors'; -import cons from 'consolidate'; import morgan from 'morgan'; import fs from 'fs'; import YAML from 'js-yaml'; @@ -85,11 +83,11 @@ const initMiddleware = (app) => { } // Request body parsing middleware should be above methodOverride app.use( - bodyParser.urlencoded({ + express.urlencoded({ extended: true, }), ); - app.use(bodyParser.json(config.bodyParser)); + app.use(express.json(config.bodyParser)); app.use(methodOverride()); // Add the cookie parser and flash middleware app.use(cookieParser()); @@ -175,15 +173,6 @@ const initErrorRoutes = (app) => { }); }; -/** - * set rendering Engine - */ -const setEngine = (app) => { - app.engine('html', cons.swig); - app.set('view engine', 'html'); - app.set('views', path.resolve('config/templates')); -}; - /** * Initialize the Express application */ @@ -210,8 +199,6 @@ const init = async () => { await initModulesServerRoutes(app); // Initialize error routes initErrorRoutes(app); - // Set engine - setEngine(app); return app; }; @@ -225,6 +212,5 @@ export default { initModulesServerPolicies, initModulesServerRoutes, initErrorRoutes, - setEngine, init, }; diff --git a/modules/auth/controllers/auth.controller.js b/modules/auth/controllers/auth.controller.js index baf573f09..7373fbeef 100644 --- a/modules/auth/controllers/auth.controller.js +++ b/modules/auth/controllers/auth.controller.js @@ -3,7 +3,6 @@ */ import passport from 'passport'; import jwt from 'jsonwebtoken'; -import _ from 'lodash'; import UserService from '../../users/services/users.service.js'; import config from '../../../config/index.js'; @@ -13,6 +12,12 @@ import errors from '../../../lib/helpers/errors.js'; import AppError from '../../../lib/helpers/AppError.js'; import UsersSchema from '../../users/models/user.schema.js'; +const tokenCookieOptions = { + httpOnly: true, + secure: config.cookie.secure, + sameSite: config.cookie.sameSite, +}; + /** * @desc Endpoint to ask the service to create a user * @param {Object} req - Express request object @@ -27,7 +32,7 @@ const signup = async (req, res) => { }); return res .status(200) - .cookie('TOKEN', token, { httpOnly: true }) + .cookie('TOKEN', token, tokenCookieOptions) .json({ user, tokenExpiresIn: Date.now() + config.jwt.expiresIn * 1000, @@ -53,7 +58,7 @@ const signin = async (req, res) => { }); return res .status(200) - .cookie('TOKEN', token, { httpOnly: true }) + .cookie('TOKEN', token, tokenCookieOptions) .json({ user, tokenExpiresIn: Date.now() + config.jwt.expiresIn * 1000, @@ -87,7 +92,7 @@ const token = async (req, res) => { }); return res .status(200) - .cookie('TOKEN', token, { httpOnly: true }) + .cookie('TOKEN', token, tokenCookieOptions) .json({ user, tokenExpiresIn: Date.now() + config.jwt.expiresIn * 1000 }); }; @@ -129,11 +134,11 @@ const checkOAuthUserProfile = async (profil, key, provider, res) => { provider, providerData: profil.providerData || null, }; - const result = model.getResultFromJoi(user, UsersSchema.User, _.clone(config.joi.validationOptions)); + const result = model.getResultFromZod(user, UsersSchema.User); // check error const error = model.checkError(result); if (error) return responses.error(res, 422, 'Schema validation error', error)(result.error); - // else return req.body with the data after Joi validation + // else return req.body with the data after Zod validation return await UserService.create(result.value); } catch (err) { throw new AppError('oAuth', { code: 'CONTROLLER_ERROR', details: err.details || err }); @@ -164,7 +169,7 @@ const oauthCallback = async (req, res, next) => { }); return res .status(200) - .cookie('TOKEN', token, { httpOnly: true }) + .cookie('TOKEN', token, tokenCookieOptions) .json({ user, tokenExpiresIn: Date.now() + config.jwt.expiresIn * 1000, @@ -190,7 +195,7 @@ const oauthCallback = async (req, res, next) => { const token = jwt.sign({ userId: user.id }, config.jwt.secret, { expiresIn: config.jwt.expiresIn, }); - res.cookie('TOKEN', token, { httpOnly: true }); + res.cookie('TOKEN', token, tokenCookieOptions); res.redirect(302, `${config.cors.origin[0]}/token`); } })(req, res, next); diff --git a/modules/auth/controllers/auth.password.controller.js b/modules/auth/controllers/auth.password.controller.js index a4bfd9c6f..bc9e45996 100644 --- a/modules/auth/controllers/auth.password.controller.js +++ b/modules/auth/controllers/auth.password.controller.js @@ -10,6 +10,12 @@ import errors from '../../../lib/helpers/errors.js'; import responses from '../../../lib/helpers/responses.js'; import config from '../../../config/index.js'; +const tokenCookieOptions = { + httpOnly: true, + secure: config.cookie.secure, + sameSite: config.cookie.sameSite, +}; + /** * @desc Endpoint to init password reset mail * @param {Object} req - Express request object @@ -85,7 +91,7 @@ const reset = async (req, res) => { user = await UserService.update(user, edit, 'recover'); return res .status(200) - .cookie('TOKEN', jwt.sign({ userId: user.id }, config.jwt.secret, { expiresIn: config.jwt.expiresIn }), { httpOnly: true }) + .cookie('TOKEN', jwt.sign({ userId: user.id }, config.jwt.secret, { expiresIn: config.jwt.expiresIn }), tokenCookieOptions) .json({ user, tokenExpiresIn: Date.now() + config.jwt.expiresIn * 1000, @@ -129,7 +135,7 @@ const updatePassword = async (req, res) => { user = await UserService.update(user, { password }, 'recover'); return res .status(200) - .cookie('TOKEN', jwt.sign({ userId: user.id }, config.jwt.secret, { expiresIn: config.jwt.expiresIn }), { httpOnly: true }) + .cookie('TOKEN', jwt.sign({ userId: user.id }, config.jwt.secret, { expiresIn: config.jwt.expiresIn }), tokenCookieOptions) .json({ user, tokenExpiresIn: Date.now() + config.jwt.expiresIn * 1000, diff --git a/modules/auth/routes/auth.routes.js b/modules/auth/routes/auth.routes.js index 29285312c..73fde70a1 100644 --- a/modules/auth/routes/auth.routes.js +++ b/modules/auth/routes/auth.routes.js @@ -2,21 +2,25 @@ * Module dependencies */ import passport from 'passport'; +import rateLimit from 'express-rate-limit'; +import config from '../../../config/index.js'; import model from '../../../lib/middlewares/model.js'; import UsersSchema from '../../users/models/user.schema.js'; import auth from '../controllers/auth.controller.js'; import authPassword from '../controllers/auth.password.controller.js'; export default (app) => { + const authLimiter = rateLimit(config.rateLimit.auth); + // Setting up the users password api - app.route('/api/auth/forgot').post(authPassword.forgot); - app.route('/api/auth/reset/:token').get(authPassword.validateResetToken); - app.route('/api/auth/reset').post(authPassword.reset); + app.route('/api/auth/forgot').post(authLimiter, authPassword.forgot); + app.route('/api/auth/reset/:token').get(authLimiter, authPassword.validateResetToken); + app.route('/api/auth/reset').post(authLimiter, authPassword.reset); // Setting up the users authentication api - app.route('/api/auth/signup').post(model.isValid(UsersSchema.User), auth.signup); - app.route('/api/auth/signin').post(passport.authenticate('local', { session: false }), auth.signin); + app.route('/api/auth/signup').post(authLimiter, model.isValid(UsersSchema.User), auth.signup); + app.route('/api/auth/signin').post(authLimiter, passport.authenticate('local', { session: false }), auth.signin); // Jwt reset token app.route('/api/auth/token').get(passport.authenticate('jwt', { session: false }), auth.token); diff --git a/modules/auth/tests/auth.integration.tests.js b/modules/auth/tests/auth.integration.tests.js index 3e1cb0a93..b19061d9c 100644 --- a/modules/auth/tests/auth.integration.tests.js +++ b/modules/auth/tests/auth.integration.tests.js @@ -5,6 +5,7 @@ import request from 'supertest'; import path from 'path'; import _ from 'lodash'; import { jest } from '@jest/globals'; +import passport from 'passport'; import { bootstrap } from '../../../lib/app.js'; import mongooseService from '../../../lib/services/mongoose.js'; @@ -493,6 +494,84 @@ describe('Auth integration tests:', () => { oauthUsers.push(result); }); + test('should return 422 when checkOAuthUserProfile receives an invalid profile', async () => { + const invalidProfil = { + firstName: '', // invalid — fails min(1) + lastName: 'Test', + email: 'invalid-oauth@test.com', + avatar: '', + providerData: { id: 'google-invalid-999' }, + }; + const errors = []; + const mockRes = { status() { return this; }, json(body) { errors.push(body); }, cookie() { return this; } }; + const result = await AuthController.checkOAuthUserProfile(invalidProfil, 'id', 'google', mockRes); + expect(result).toBeDefined(); + expect(result.type).toBe('error'); + expect(errors[0]?.message).toBe('Schema validation error'); + }); + + test('should throw AppError when create fails inside checkOAuthUserProfile', async () => { + const profil = { + firstName: 'OAuth', + lastName: 'Err', + email: 'oautherr@test.com', + avatar: '', + providerData: { id: 'google-err-000' }, + }; + const mockRes = { status() { return this; }, json() {}, cookie() { return this; } }; + const createSpy = jest.spyOn(UserService, 'create').mockRejectedValueOnce(new Error('DB error')); + await expect( + AuthController.checkOAuthUserProfile(profil, 'id', 'google', mockRes), + ).rejects.toThrow('oAuth'); + createSpy.mockRestore(); + }); + + test('should authenticate via client-side OAuth and set tokenCookieOptions on response', async () => { + const oauthEmail = 'oauthcb-appauth@test.com'; + try { + const result = await agent + .post('/api/auth/google/callback') + .send({ strategy: false, key: 'id', value: 'cb-app-auth-id-999', firstName: 'OAuth', lastName: 'Callback', email: oauthEmail }) + .expect(200); + const tokenCookie = result.headers['set-cookie']?.find((c) => c.startsWith('TOKEN=')); + expect(tokenCookie).toBeDefined(); + expect(tokenCookie).toMatch(/HttpOnly/i); + expect(tokenCookie).toMatch(/SameSite=Strict/i); + expect(result.body.message).toBe('oAuth Ok'); + } catch (err) { + console.log(err); + expect(err).toBeFalsy(); + } finally { + try { + const u = await UserService.getBrut({ email: oauthEmail }); + if (u) await UserService.remove(u); + } catch (_) { /* cleanup */ } + } + }); + + test('should set tokenCookieOptions and redirect on classic web oAuth success', async () => { + const mockUserId = 'mock-oauth-user-id-123'; + const authenticateSpy = jest.spyOn(passport, 'authenticate').mockImplementationOnce( + (strategy, callback) => () => callback(null, { id: mockUserId }), + ); + const cookies = {}; + const redirectCalls = []; + const mockReq = { params: { strategy: 'google' }, body: {} }; + const mockRes = { + cookie(name, val, opts) { cookies[name] = { val, opts }; return this; }, + redirect(code, url) { redirectCalls.push({ code, url }); }, + }; + + await AuthController.oauthCallback(mockReq, mockRes, () => {}); + + expect(cookies.TOKEN).toBeDefined(); + expect(cookies.TOKEN.opts.httpOnly).toBe(true); + expect(cookies.TOKEN.opts.sameSite).toBe(config.cookie.sameSite); + expect(redirectCalls[0]).toMatchObject({ code: 302 }); + expect(redirectCalls[0].url).toMatch(/\/token$/); + authenticateSpy.mockRestore(); + }); + test('should find an existing OAuth user via checkOAuthUserProfile', async () => { // Create an OAuth user directly first const createdUser = await UserService.create({ @@ -617,6 +696,80 @@ describe('Auth integration tests:', () => { }); }); + describe('Security', () => { + const secEmail = 'security@test.com'; + const secPassword = 'W@os.jsI$Aw3$0m3'; + let secUser; + + beforeEach(async () => { + try { + const result = await agent.post('/api/auth/signup').send({ + firstName: 'Sec', + lastName: 'Test', + email: secEmail, + password: secPassword, + provider: 'local', + }).expect(200); + secUser = result.body.user; + } catch (err) { + console.log(err); + expect(err).toBeFalsy(); + } + }); + + test('signup cookie should have HttpOnly and SameSite=Strict flags', async () => { + try { + const result = await agent.post('/api/auth/signup').send({ + firstName: 'Cookie', + lastName: 'Test', + email: 'cookieflag@test.com', + password: secPassword, + provider: 'local', + }).expect(200); + const tokenCookie = result.headers['set-cookie']?.find((c) => c.startsWith('TOKEN=')); + expect(tokenCookie).toBeDefined(); + expect(tokenCookie).toMatch(/HttpOnly/i); + expect(tokenCookie).toMatch(/SameSite=Strict/i); + } catch (err) { + console.log(err); + expect(err).toBeFalsy(); + } + }); + + test('signin cookie should have HttpOnly and SameSite=Strict flags', async () => { + try { + const result = await agent.post('/api/auth/signin').send({ email: secEmail, password: secPassword }).expect(200); + const tokenCookie = result.headers['set-cookie']?.find((c) => c.startsWith('TOKEN=')); + expect(tokenCookie).toBeDefined(); + expect(tokenCookie).toMatch(/HttpOnly/i); + expect(tokenCookie).toMatch(/SameSite=Strict/i); + } catch (err) { + console.log(err); + expect(err).toBeFalsy(); + } + }); + + test('rate-limited auth routes should include RateLimit response headers', async () => { + try { + const result = await agent.post('/api/auth/signin').send({ email: secEmail, password: secPassword }).expect(200); + expect(result.headers['ratelimit-limit']).toBeDefined(); + expect(result.headers['ratelimit-remaining']).toBeDefined(); + } catch (err) { + console.log(err); + expect(err).toBeFalsy(); + } + }); + + afterEach(async () => { + try { if (secUser) await UserService.remove(secUser); } catch (_) { /* cleanup */ } + try { + const cookieUser = await UserService.getBrut({ email: 'cookieflag@test.com' }); + if (cookieUser) await UserService.remove(cookieUser); + } catch (_) { /* cleanup */ } + secUser = null; + }); + }); + describe('Error paths', () => { test('should redirect to invalid when validateResetToken getBrut throws', async () => { jest.spyOn(UserService, 'getBrut').mockRejectedValueOnce(new Error('DB error')); diff --git a/modules/core/tests/core.unit.tests.js b/modules/core/tests/core.unit.tests.js index 18dd77040..07f048e3c 100644 --- a/modules/core/tests/core.unit.tests.js +++ b/modules/core/tests/core.unit.tests.js @@ -12,6 +12,7 @@ import logger from '../../../lib/services/logger.js'; import mongooseService from '../../../lib/services/mongoose.js'; import expressService from '../../../lib/services/express.js'; import errors from '../../../lib/helpers/errors.js'; +import policy from '../../../lib/middlewares/policy.js'; /** * Unit tests @@ -309,6 +310,48 @@ describe('Core unit tests:', () => { }); }); + describe('Policy', () => { + beforeAll(async () => { + const [homePolicy, tasksPolicy, uploadsPolicy, usersAccountPolicy, usersAdminPolicy] = await Promise.all([ + import('../../../modules/home/policies/home.policy.js'), + import('../../../modules/tasks/policies/tasks.policy.js'), + import('../../../modules/uploads/policies/uploads.policy.js'), + import('../../../modules/users/policies/users.account.policy.js'), + import('../../../modules/users/policies/users.admin.policy.js'), + ]); + homePolicy.default.invokeRolesPolicies(); + tasksPolicy.default.invokeRolesPolicies(); + uploadsPolicy.default.invokeRolesPolicies(); + usersAccountPolicy.default.invokeRolesPolicies(); + usersAdminPolicy.default.invokeRolesPolicies(); + }); + + it('guest can read public task routes', async () => { + const ability = await policy.defineAbilityFor(null); + expect(ability.can('read', '/api/tasks')).toBe(true); + }); + + it('guest cannot create tasks', async () => { + const ability = await policy.defineAbilityFor(null); + expect(ability.can('create', '/api/tasks')).toBe(false); + }); + + it('user can manage tasks', async () => { + const ability = await policy.defineAbilityFor({ roles: ['user'] }); + expect(ability.can('create', '/api/tasks')).toBe(true); + }); + + it('user cannot access admin routes', async () => { + const ability = await policy.defineAbilityFor({ roles: ['user'] }); + expect(ability.can('read', '/api/users')).toBe(false); + }); + + it('admin can access admin routes', async () => { + const ability = await policy.defineAbilityFor({ roles: ['admin'] }); + expect(ability.can('read', '/api/users')).toBe(true); + }); + }); + describe('Mongoose service', () => { it('should invoke callback after loading models', async () => { const callback = jest.fn(); diff --git a/modules/home/policies/home.policy.js b/modules/home/policies/home.policy.js index 2cd4a8843..ec5c1b7b6 100644 --- a/modules/home/policies/home.policy.js +++ b/modules/home/policies/home.policy.js @@ -4,31 +4,14 @@ import policy from '../../../lib/middlewares/policy.js'; /** - * Invoke Tasks Permissions + * Invoke Home Permissions */ const invokeRolesPolicies = () => { - policy.Acl.allow([ - { - roles: ['guest'], - allows: [ - { - resources: '/api/home/releases', - permissions: ['get'], - }, - { - resources: '/api/home/changelogs', - permissions: ['get'], - }, - { - resources: '/api/home/team', - permissions: ['get'], - }, - { - resources: '/api/home/pages/:name', - permissions: ['get'], - }, - ], - }, + policy.registerRules([ + { roles: ['guest'], actions: ['read'], subject: '/api/home/releases' }, + { roles: ['guest'], actions: ['read'], subject: '/api/home/changelogs' }, + { roles: ['guest'], actions: ['read'], subject: '/api/home/team' }, + { roles: ['guest'], actions: ['read'], subject: '/api/home/pages/:name' }, ]); }; diff --git a/modules/tasks/models/tasks.schema.js b/modules/tasks/models/tasks.schema.js index 977b8585c..dae0235f6 100644 --- a/modules/tasks/models/tasks.schema.js +++ b/modules/tasks/models/tasks.schema.js @@ -1,17 +1,20 @@ /** * Module dependencies */ -import Joi from '@hapi/joi'; +import { z } from 'zod'; /** * Data Schema */ -const TaskSchema = Joi.object().keys({ - title: Joi.string().trim().default('').required(), - description: Joi.string().allow('').default('').required(), - user: Joi.string().trim().default(''), -}); +const Task = z.object({ + title: z.string({ invalid_type_error: 'title must be a string' }).trim().min(1), + description: z.string().default(''), + user: z.string().trim().default(''), +}).strip(); + +const TaskUpdate = Task.partial(); export default { - Task: TaskSchema, + Task, + TaskUpdate, }; diff --git a/modules/tasks/policies/tasks.policy.js b/modules/tasks/policies/tasks.policy.js index cf67d7d56..75ea9d1d7 100644 --- a/modules/tasks/policies/tasks.policy.js +++ b/modules/tasks/policies/tasks.policy.js @@ -7,37 +7,12 @@ import policy from '../../../lib/middlewares/policy.js'; * Invoke Tasks Permissions */ const invokeRolesPolicies = () => { - policy.Acl.allow([ - { - roles: ['user'], - allows: [ - { - resources: '/api/tasks', - permissions: '*', - }, - { - resources: '/api/tasks/:taskId', - permissions: '*', - }, - ], - }, - { - roles: ['guest'], - allows: [ - { - resources: '/api/tasks/stats', - permissions: ['get'], - }, - { - resources: '/api/tasks', - permissions: ['get'], - }, - { - resources: '/api/tasks/:taskId', - permissions: ['get'], - }, - ], - }, + policy.registerRules([ + { roles: ['user'], actions: 'manage', subject: '/api/tasks' }, + { roles: ['user'], actions: 'manage', subject: '/api/tasks/:taskId' }, + { roles: ['guest'], actions: ['read'], subject: '/api/tasks/stats' }, + { roles: ['guest'], actions: ['read'], subject: '/api/tasks' }, + { roles: ['guest'], actions: ['read'], subject: '/api/tasks/:taskId' }, ]); }; diff --git a/modules/tasks/routes/tasks.routes.js b/modules/tasks/routes/tasks.routes.js index 725b7fa10..80511d774 100644 --- a/modules/tasks/routes/tasks.routes.js +++ b/modules/tasks/routes/tasks.routes.js @@ -26,8 +26,8 @@ export default (app) => { .route('/api/tasks/:taskId') .all(passport.authenticate('jwt', { session: false }), policy.isAllowed) // policy.isOwner (require set in middleWare) .get(tasks.get) // get - .put(model.isValid(tasksSchema.Task), policy.isOwner, tasks.update) // update - .delete(model.isValid(tasksSchema.Task), policy.isOwner, tasks.remove); // delete + .put(model.isValid(tasksSchema.TaskUpdate), policy.isOwner, tasks.update) // update + .delete(policy.isOwner, tasks.remove); // delete // Finish by binding the task middleware app.param('taskId', tasks.taskByID); diff --git a/modules/tasks/tests/tasks.unit.tests.js b/modules/tasks/tests/tasks.unit.tests.js index 3c30b853a..de2f2f5f0 100644 --- a/modules/tasks/tests/tasks.unit.tests.js +++ b/modules/tasks/tests/tasks.unit.tests.js @@ -1,13 +1,8 @@ /** * Module dependencies. */ -import _ from 'lodash'; - -import config from '../../../config/index.js'; import schema from '../models/tasks.schema.js'; -const options = _.clone(config.joi.validationOptions); - /** * Unit tests */ @@ -22,7 +17,7 @@ describe('Tasks unit tests:', () => { }); test('should be valid a task example without problems', (done) => { - const result = schema.Task.validate(task, options); + const result = schema.Task.safeParse(task); expect(typeof result).toBe('object'); expect(result.error).toBeFalsy(); done(); @@ -31,7 +26,7 @@ describe('Tasks unit tests:', () => { test('should be able to show an error when trying a schema without title', (done) => { task.title = ''; - const result = schema.Task.validate(task, options); + const result = schema.Task.safeParse(task); expect(typeof result).toBe('object'); expect(result.error).toBeDefined(); done(); @@ -40,7 +35,7 @@ describe('Tasks unit tests:', () => { test('should be able to show an error when trying a schema without description', (done) => { task.description = null; - const result = schema.Task.validate(task, options); + const result = schema.Task.safeParse(task); expect(typeof result).toBe('object'); expect(result.error).toBeDefined(); done(); @@ -49,7 +44,7 @@ describe('Tasks unit tests:', () => { test('should not show an error when trying a schema with user', (done) => { task.user = '507f1f77bcf86cd799439011'; - const result = schema.Task.validate(task, options); + const result = schema.Task.safeParse(task); expect(typeof result).toBe('object'); expect(result.error).toBeFalsy(); done(); @@ -58,8 +53,8 @@ describe('Tasks unit tests:', () => { test('should be able remove unknown when trying a different schema', (done) => { task.toto = ''; - const result = schema.Task.validate(task, options); - expect(result.toto).toBeUndefined(); + const result = schema.Task.safeParse(task); + expect(result.data?.toto).toBeUndefined(); done(); }); }); diff --git a/modules/uploads/policies/uploads.policy.js b/modules/uploads/policies/uploads.policy.js index 064037ff3..b66a9a1c5 100644 --- a/modules/uploads/policies/uploads.policy.js +++ b/modules/uploads/policies/uploads.policy.js @@ -7,20 +7,9 @@ import policy from '../../../lib/middlewares/policy.js'; * Invoke Uploads Permissions */ const invokeRolesPolicies = () => { - policy.Acl.allow([ - { - roles: ['user', 'admin'], - allows: [ - { - resources: '/api/uploads/:uploadName', - permissions: ['get', 'delete'], - }, - { - resources: '/api/uploads/images/:imageName', - permissions: ['get'], - }, - ], - }, + policy.registerRules([ + { roles: ['user', 'admin'], actions: ['read', 'delete'], subject: '/api/uploads/:uploadName' }, + { roles: ['user', 'admin'], actions: ['read'], subject: '/api/uploads/images/:imageName' }, ]); }; diff --git a/modules/users/models/user.schema.js b/modules/users/models/user.schema.js index eb6769486..6c9217195 100644 --- a/modules/users/models/user.schema.js +++ b/modules/users/models/user.schema.js @@ -1,41 +1,49 @@ /** * Module dependencies */ -import PlainJoi from '@hapi/joi'; +import { z } from 'zod'; import config from '../../../config/index.js'; -import joiHelpers from '../../../lib/helpers/joi.js'; +import zodHelpers from '../../../lib/helpers/zod.js'; -const Joi = PlainJoi.extend(joiHelpers.joiZxcvbn(PlainJoi)); const names = /^[a-zA-ZàáâäãåąčćęèéêëėįìíîïłńòóôöõøùúûüųūÿýżźñçčšžÀÁÂÄÃÅĄĆČĖĘÈÉÊËÌÍÎÏĮŁŃÒÓÔÖÕØÙÚÛÜŲŪŸÝŻŹÑßÇŒÆČŠŽ∂ð ,.'-]+$/u; /** * User Data Schema */ -const UserSchema = Joi.object().keys({ - firstName: Joi.string().regex(names).min(1).max(50).trim().required(), - lastName: Joi.string().regex(names).min(1).max(50).trim().required(), - bio: Joi.string().max(200).trim().allow('').optional(), - position: Joi.string().max(50).trim().allow('').optional(), - email: Joi.string().email(), - avatar: Joi.string().trim().default('').allow(''), - roles: Joi.array() - .items(Joi.string().valid(...config.whitelists.users.roles)) - .min(1) - .default(['user']), +const User = z.object({ + firstName: z.string().regex(names).min(1).max(50).trim(), + lastName: z.string().regex(names).min(1).max(50).trim(), + bio: z.string().max(200).trim().optional().default(''), + position: z.string().max(50).trim().optional().default(''), + email: z.string().email().optional(), + avatar: z.string().trim().default(''), + roles: z.array(z.enum(config.whitelists.users.roles)).min(1).default(['user']), /* Provider */ - provider: Joi.string(), - providerData: Joi.object(), + provider: z.string().optional(), + providerData: z.record(z.unknown()).optional(), /* Password */ - password: Joi.zxcvbn().strength(config.zxcvbn.minimumScore).min(config.zxcvbn.minSize).max(config.zxcvbn.maxSize).default(''), - resetPasswordToken: Joi.string().allow(null), - resetPasswordExpires: Joi.date().allow(null), + password: z.string() + .max(config.zxcvbn.maxSize) + .default('') + .superRefine((val, ctx) => { + if (val === '') return; // allow empty (OAuth users / no password set) + zodHelpers.passwordRefinement(val, ctx); + if (val.length < config.zxcvbn.minSize) { + ctx.addIssue({ code: z.ZodIssueCode.custom, message: `Password length must be at least ${config.zxcvbn.minSize} characters long` }); + } + }), + resetPasswordToken: z.string().nullable().optional(), + resetPasswordExpires: z.coerce.date().nullable().optional(), // startup requirement - terms: Joi.date().default(null).optional(), // last check + terms: z.coerce.date().nullable().optional(), // others - complementary: Joi.object({}).unknown().allow(null).optional(), -}); + complementary: z.record(z.unknown()).nullable().optional(), +}).strip(); + +const UserUpdate = User.partial(); export default { - User: UserSchema, + User, + UserUpdate, }; diff --git a/modules/users/policies/users.account.policy.js b/modules/users/policies/users.account.policy.js index 036446025..fe00ca93f 100644 --- a/modules/users/policies/users.account.policy.js +++ b/modules/users/policies/users.account.policy.js @@ -4,56 +4,19 @@ import policy from '../../../lib/middlewares/policy.js'; /** - * Invoke Tasks Permissions + * Invoke Users Account Permissions */ const invokeRolesPolicies = () => { - policy.Acl.allow([ - { - roles: ['user'], - allows: [ - { - resources: '/api/users/me', - permissions: ['get'], - }, - { - resources: '/api/users/terms', - permissions: ['get'], - }, - { - resources: '/api/users', - permissions: ['put', 'delete'], - }, - { - resources: '/api/users/password', - permissions: ['post'], - }, - { - resources: '/api/users/avatar', - permissions: ['post', 'delete'], - }, - { - resources: '/api/users/accounts', - permissions: ['post', 'delete'], - }, - { - resources: '/api/users/data', - permissions: ['get', 'delete'], - }, - { - resources: '/api/users/data/mail', - permissions: ['get'], - }, - ], - }, - { - roles: ['guest'], - allows: [ - { - resources: '/api/users/stats', - permissions: ['get'], - }, - ], - }, + policy.registerRules([ + { roles: ['user'], actions: ['read'], subject: '/api/users/me' }, + { roles: ['user'], actions: ['read'], subject: '/api/users/terms' }, + { roles: ['user'], actions: ['update', 'delete'], subject: '/api/users' }, + { roles: ['user'], actions: ['create'], subject: '/api/users/password' }, + { roles: ['user'], actions: ['create', 'delete'], subject: '/api/users/avatar' }, + { roles: ['user'], actions: ['create', 'delete'], subject: '/api/users/accounts' }, + { roles: ['user'], actions: ['read', 'delete'], subject: '/api/users/data' }, + { roles: ['user'], actions: ['read'], subject: '/api/users/data/mail' }, + { roles: ['guest'], actions: ['read'], subject: '/api/users/stats' }, ]); }; diff --git a/modules/users/policies/users.admin.policy.js b/modules/users/policies/users.admin.policy.js index 63ca45aca..3d5159413 100644 --- a/modules/users/policies/users.admin.policy.js +++ b/modules/users/policies/users.admin.policy.js @@ -4,27 +4,13 @@ import policy from '../../../lib/middlewares/policy.js'; /** - * Invoke Tasks Permissions + * Invoke Users Admin Permissions */ const invokeRolesPolicies = () => { - policy.Acl.allow([ - { - roles: ['admin'], - allows: [ - { - resources: '/api/users', - permissions: ['get'], - }, - { - resources: '/api/users/page/:userPage', - permissions: ['get'], - }, - { - resources: '/api/users/:userId', - permissions: ['get', 'put', 'delete'], - }, - ], - }, + policy.registerRules([ + { roles: ['admin'], actions: ['read'], subject: '/api/users' }, + { roles: ['admin'], actions: ['read'], subject: '/api/users/page/:userPage' }, + { roles: ['admin'], actions: ['read', 'update', 'delete'], subject: '/api/users/:userId' }, ]); }; diff --git a/modules/users/routes/users.routes.js b/modules/users/routes/users.routes.js index a2e040f83..db895ea1a 100644 --- a/modules/users/routes/users.routes.js +++ b/modules/users/routes/users.routes.js @@ -28,7 +28,7 @@ export default (app) => { app .route('/api/users') .all(passport.authenticate('jwt', { session: false }), policy.isAllowed) - .put(model.isValid(usersSchema.User), users.update) + .put(model.isValid(usersSchema.UserUpdate), users.update) .delete(users.remove); app.route('/api/users/password').post(passport.authenticate('jwt', { session: false }), policy.isAllowed, authPassword.updatePassword); diff --git a/modules/users/tests/user.unit.tests.js b/modules/users/tests/user.unit.tests.js index 309a8df5c..d20b28d67 100644 --- a/modules/users/tests/user.unit.tests.js +++ b/modules/users/tests/user.unit.tests.js @@ -1,13 +1,8 @@ /** * Module dependencies. */ -import _ from 'lodash'; - -import config from '../../../config/index.js'; import schema from '../models/user.schema.js'; -const options = _.clone(config.joi.validationOptions); - /** * Unit tests */ @@ -26,7 +21,7 @@ describe('User unit tests:', () => { describe('Schema', () => { test('should be valid a user example without problems', (done) => { - const result = schema.User.validate(user, options); + const result = schema.User.safeParse(user); expect(typeof result).toBe('object'); expect(result.error).toBeFalsy(); done(); @@ -35,7 +30,7 @@ describe('User unit tests:', () => { test('should be able to show an error when trying a schema without first name', (done) => { user.firstName = ''; - const result = schema.User.validate(user, options); + const result = schema.User.safeParse(user); expect(typeof result).toBe('object'); expect(result.error).toBeDefined(); done(); @@ -44,7 +39,7 @@ describe('User unit tests:', () => { test('should be able to accept a user with valid roles without problems', (done) => { user.roles = ['user', 'admin']; - const result = schema.User.validate(user, options); + const result = schema.User.safeParse(user); expect(typeof result).toBe('object'); expect(result.error).toBeFalsy(); done(); @@ -53,7 +48,7 @@ describe('User unit tests:', () => { test('should be able to show an error when trying a user without a role', (done) => { user.roles = []; - const result = schema.User.validate(user, options); + const result = schema.User.safeParse(user); expect(typeof result).toBe('object'); expect(result.error).toBeDefined(); done(); @@ -62,7 +57,7 @@ describe('User unit tests:', () => { test('should be able to show an error when trying to update an existing user with a invalid role', (done) => { user.roles = ['invalid-user-role-enum']; - const result = schema.User.validate(user, options); + const result = schema.User.safeParse(user); expect(typeof result).toBe('object'); expect(result.error).toBeDefined(); done(); @@ -73,7 +68,7 @@ describe('User unit tests:', () => { test('should validate when the password strength passes - "P-@-$-$-w-0-r-d-!"', (done) => { user.password = 'P-@-$-$-w-0-r-d-!'; - const result = schema.User.validate(user, options); + const result = schema.User.safeParse(user); expect(typeof result).toBe('object'); expect(result.error).toBeFalsy(); done(); @@ -82,7 +77,7 @@ describe('User unit tests:', () => { test('should validate when the password is undefined', (done) => { user.password = undefined; - const result = schema.User.validate(user, options); + const result = schema.User.safeParse(user); expect(typeof result).toBe('object'); expect(result.error).toBeFalsy(); done(); @@ -91,7 +86,7 @@ describe('User unit tests:', () => { test('should allow a difficult password with a score of 4 with zxcvbn- "WeAreOpenSource"', (done) => { user.password = 'Open-Source Stack Solution For WeAreOpenSource Applications'; - const result = schema.User.validate(user, options); + const result = schema.User.safeParse(user); expect(typeof result).toBe('object'); expect(result.error).toBeFalsy(); done(); @@ -100,7 +95,7 @@ describe('User unit tests:', () => { test('should allow a password with a score of 3 with zxcvbn- "AreOpenSource"', (done) => { user.password = 'AreOpenSource'; - const result = schema.User.validate(user, options); + const result = schema.User.safeParse(user); expect(typeof result).toBe('object'); expect(result.error).toBeFalsy(); done(); @@ -109,7 +104,7 @@ describe('User unit tests:', () => { test('should not allow a password with a score of 2 with zxcvbn- "OpenSource"', (done) => { user.password = 'OpenSource'; - const result = schema.User.validate(user, options); + const result = schema.User.safeParse(user); expect(typeof result).toBe('object'); expect(result.error).toBeDefined(); done(); @@ -118,7 +113,7 @@ describe('User unit tests:', () => { test('should not allow a simple password with a score of 1 with zxcvbn- "Source"', (done) => { user.password = 'Source'; - const result = schema.User.validate(user, options); + const result = schema.User.safeParse(user); expect(typeof result).toBe('object'); expect(result.error).toBeDefined(); done(); @@ -127,7 +122,7 @@ describe('User unit tests:', () => { test('should not allow this simple password - "P@$$w0rd!"', (done) => { user.password = 'P@$$w0rd!'; - const result = schema.User.validate(user, options); + const result = schema.User.safeParse(user); expect(typeof result).toBe('object'); expect(result.error).toBeDefined(); done(); @@ -136,7 +131,7 @@ describe('User unit tests:', () => { test('should not allow a password smaller than 8 characters long.', (done) => { user.password = ')!/uLT'; - const result = schema.User.validate(user, options); + const result = schema.User.safeParse(user); expect(typeof result).toBe('object'); expect(result.error).toBeDefined(); done(); @@ -146,16 +141,16 @@ describe('User unit tests:', () => { user.password = ')!/uLT="lh&:`6X!]|15o!$!TJf,.13l?vG].-j],lFPe/QhwN#{Z<[*1nX@n1^?WW-%_.*D)m$toB+N7z}kcN#B_d(f41h%w@0F!]igtSQ1gl~6sEV&r~}~1ub>If1c+'; - const result = schema.User.validate(user, options); + const result = schema.User.safeParse(user); expect(typeof result).toBe('object'); expect(result.error).toBeDefined(); done(); }); test('should not allow a forbidden password.', (done) => { - user.password = 'azerty'; + user.password = 'azertyui'; // in config.zxcvbn.forbiddenPasswords - const result = schema.User.validate(user, options); + const result = schema.User.safeParse(user); expect(typeof result).toBe('object'); expect(result.error).toBeDefined(); done(); @@ -164,7 +159,7 @@ describe('User unit tests:', () => { test('should not allow a password with 3 or more repeating characters - "P@$$w0rd!!!"', (done) => { user.password = 'P@$$w0rd!!!'; - const result = schema.User.validate(user, options); + const result = schema.User.safeParse(user); expect(typeof result).toBe('object'); expect(result.error).toBeDefined(); done(); @@ -175,7 +170,7 @@ describe('User unit tests:', () => { test('should not allow invalid email address - "123"', (done) => { user.email = '123'; - const result = schema.User.validate(user, options); + const result = schema.User.safeParse(user); expect(typeof result).toBe('object'); expect(result.error).toBeDefined(); done(); @@ -184,7 +179,7 @@ describe('User unit tests:', () => { test('should not allow invalid email address - "123@123@123"', (done) => { user.email = '123@123@123'; - const result = schema.User.validate(user, options); + const result = schema.User.safeParse(user); expect(typeof result).toBe('object'); expect(result.error).toBeDefined(); done(); @@ -193,7 +188,7 @@ describe('User unit tests:', () => { test('should not allow invalid email address - "123.com"', (done) => { user.email = '123.com'; - const result = schema.User.validate(user, options); + const result = schema.User.safeParse(user); expect(typeof result).toBe('object'); expect(result.error).toBeDefined(); done(); @@ -202,7 +197,7 @@ describe('User unit tests:', () => { test('should not allow invalid email address - "@123.com"', (done) => { user.email = '@123.com'; - const result = schema.User.validate(user, options); + const result = schema.User.safeParse(user); expect(typeof result).toBe('object'); expect(result.error).toBeDefined(); done(); @@ -211,7 +206,7 @@ describe('User unit tests:', () => { test('should not allow invalid email address - "abc@abc@abc.com"', (done) => { user.email = 'abc@abc@abc.com'; - const result = schema.User.validate(user, options); + const result = schema.User.safeParse(user); expect(typeof result).toBe('object'); expect(result.error).toBeDefined(); done(); @@ -220,7 +215,7 @@ describe('User unit tests:', () => { test('should not allow invalid characters in email address - "abc~@#$%^&*()ef=@abc.com"', (done) => { user.email = 'abc~@#$%^&*()ef=@abc.com'; - const result = schema.User.validate(user, options); + const result = schema.User.safeParse(user); expect(typeof result).toBe('object'); expect(result.error).toBeDefined(); done(); @@ -229,7 +224,7 @@ describe('User unit tests:', () => { test('should not allow space characters in email address - "abc def@abc.com"', (done) => { user.email = 'abc def@abc.com'; - const result = schema.User.validate(user, options); + const result = schema.User.safeParse(user); expect(typeof result).toBe('object'); expect(result.error).toBeDefined(); done(); @@ -239,7 +234,7 @@ describe('User unit tests:', () => { test('should not allow doudble quote characters in email address - "abc"def@abc.com"', (done) => { user.email = 'abc"def@abc.com'; - const result = schema.User.validate(user, options); + const result = schema.User.safeParse(user); expect(typeof result).toBe('object'); expect(result.error).toBeDefined(); done(); @@ -248,7 +243,7 @@ describe('User unit tests:', () => { test('should not allow double dotted characters in email address - "abcdef@abc..com"', (done) => { user.email = 'abcdef@abc..com'; - const result = schema.User.validate(user, options); + const result = schema.User.safeParse(user); expect(typeof result).toBe('object'); expect(result.error).toBeDefined(); done(); @@ -257,7 +252,7 @@ describe('User unit tests:', () => { test('should allow single quote characters in email address - "abc\'def@abc.com"', (done) => { user.email = "abc'def@abc.com"; - const result = schema.User.validate(user, options); + const result = schema.User.safeParse(user); expect(typeof result).toBe('object'); expect(result.error).toBeFalsy(); done(); @@ -266,7 +261,7 @@ describe('User unit tests:', () => { test('should allow valid email address - "abc@abc.com"', (done) => { user.email = 'abc@abc.com'; - const result = schema.User.validate(user, options); + const result = schema.User.safeParse(user); expect(typeof result).toBe('object'); expect(result.error).toBeFalsy(); done(); @@ -275,7 +270,7 @@ describe('User unit tests:', () => { test('should allow valid email address - "abc+def@abc.com"', (done) => { user.email = 'abc+def@abc.com'; - const result = schema.User.validate(user, options); + const result = schema.User.safeParse(user); expect(typeof result).toBe('object'); expect(result.error).toBeFalsy(); done(); @@ -284,7 +279,7 @@ describe('User unit tests:', () => { test('should allow valid email address - "abc.def@abc.com"', (done) => { user.email = 'abc.def@abc.com'; - const result = schema.User.validate(user, options); + const result = schema.User.safeParse(user); expect(typeof result).toBe('object'); expect(result.error).toBeFalsy(); done(); @@ -293,7 +288,7 @@ describe('User unit tests:', () => { test('should allow valid email address - "abc.def@abc.def.com"', (done) => { user.email = 'abc.def@abc.def.com'; - const result = schema.User.validate(user, options); + const result = schema.User.safeParse(user); expect(typeof result).toBe('object'); expect(result.error).toBeFalsy(); done(); @@ -302,7 +297,7 @@ describe('User unit tests:', () => { test('should allow valid email address - "abc-def@abc.com"', (done) => { user.email = 'abc-def@abc.com'; - const result = schema.User.validate(user, options); + const result = schema.User.safeParse(user); expect(typeof result).toBe('object'); expect(result.error).toBeFalsy(); done(); diff --git a/package-lock.json b/package-lock.json index afd2770f1..ebff290a1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,21 +9,19 @@ "version": "0.4.0", "license": "MIT", "dependencies": { - "@hapi/joi": "^17.1.1", + "@casl/ability": "^6.8.0", "@jest/globals": "^30.2.0", - "acl": "~0.4.11", "axios": "^1.13.5", "bcrypt": "^6.0.0", - "body-parser": "^2.2.2", "bson": "^7.2.0", "chalk": "^5.6.2", "compression": "^1.8.1", - "consolidate": "^1.0.4", "cookie-parser": "^1.4.7", "cors": "^2.8.6", "cross-env": "^10.1.0", "enhanced-resolve": "^5.19.0", "express": "^5.2.1", + "express-rate-limit": "^8.2.1", "generate-password": "^1.7.1", "glob": "^13.0.6", "handlebars": "^4.7.8", @@ -52,9 +50,9 @@ "snyk": "^1.1302.1", "supertest": "^7.2.2", "swagger-ui-express": "^5.0.1", - "swig": "^1.4.2", "ts-jest": "^29.4.6", "winston": "^3.19.0", + "zod": "^3.25.76", "zxcvbn": "^4.4.2" }, "devDependencies": { @@ -174,6 +172,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -625,6 +624,18 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "license": "MIT" }, + "node_modules/@casl/ability": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/@casl/ability/-/ability-6.8.0.tgz", + "integrity": "sha512-Ipt4mzI4gSgnomFdaPjaLgY2MWuXqAEZLrU6qqWBB7khGiBBuuEp6ytYDnq09bRXqcjaeeHiaCvCGFbBA2SpvA==", + "license": "MIT", + "dependencies": { + "@ucast/mongo2js": "^1.3.0" + }, + "funding": { + "url": "https://github.com/stalniy/casl/blob/master/BACKERS.md" + } + }, "node_modules/@colors/colors": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", @@ -976,6 +987,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -998,6 +1010,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -1178,58 +1191,6 @@ "node": "^20.19.0 || ^22.13.0 || >=24" } }, - "node_modules/@hapi/address": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@hapi/address/-/address-4.1.0.tgz", - "integrity": "sha512-SkszZf13HVgGmChdHo/PxchnSaCJ6cetVqLzyciudzZRT0jcOouIF/Q93mgjw8cce+D+4F4C1Z/WrfFN+O3VHQ==", - "deprecated": "Moved to 'npm install @sideway/address'", - "license": "BSD-3-Clause", - "dependencies": { - "@hapi/hoek": "^9.0.0" - } - }, - "node_modules/@hapi/formula": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@hapi/formula/-/formula-2.0.0.tgz", - "integrity": "sha512-V87P8fv7PI0LH7LiVi8Lkf3x+KCO7pQozXRssAHNXXL9L1K+uyu4XypLXwxqVDKgyQai6qj3/KteNlrqDx4W5A==", - "deprecated": "Moved to 'npm install @sideway/formula'", - "license": "BSD-3-Clause" - }, - "node_modules/@hapi/hoek": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", - "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", - "license": "BSD-3-Clause" - }, - "node_modules/@hapi/joi": { - "version": "17.1.1", - "resolved": "https://registry.npmjs.org/@hapi/joi/-/joi-17.1.1.tgz", - "integrity": "sha512-p4DKeZAoeZW4g3u7ZeRo+vCDuSDgSvtsB/NpfjXEHTUjSeINAi/RrVOWiVQ1isaoLzMvFEhe8n5065mQq1AdQg==", - "deprecated": "Switch to 'npm install joi'", - "license": "BSD-3-Clause", - "dependencies": { - "@hapi/address": "^4.0.1", - "@hapi/formula": "^2.0.0", - "@hapi/hoek": "^9.0.0", - "@hapi/pinpoint": "^2.0.0", - "@hapi/topo": "^5.0.0" - } - }, - "node_modules/@hapi/pinpoint": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@hapi/pinpoint/-/pinpoint-2.0.1.tgz", - "integrity": "sha512-EKQmr16tM8s16vTT3cA5L0kZZcTMU5DUOZTuvpnY738m+jyP3JIUj+Mm1xc1rsLkGBQ/gVnfKYPwOmPg1tUR4Q==", - "license": "BSD-3-Clause" - }, - "node_modules/@hapi/topo": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", - "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", - "license": "BSD-3-Clause", - "dependencies": { - "@hapi/hoek": "^9.0.0" - } - }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -2891,6 +2852,7 @@ "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.2.0.tgz", "integrity": "sha512-XsauDV82o5qXbhalKxD7p4TZYYdwcaEXC77PPD2HixEFF+6YGppjrAAQurTl2ECWcEomHBMMNS9AH3kcCFx8jA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/core": "^7.27.4", "@jest/types": "30.2.0", @@ -3122,6 +3084,7 @@ "integrity": "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@octokit/auth-token": "^6.0.0", "@octokit/graphql": "^9.0.3", @@ -4191,6 +4154,7 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.3.tgz", "integrity": "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ==", "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -4256,6 +4220,41 @@ "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", "license": "MIT" }, + "node_modules/@ucast/core": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/@ucast/core/-/core-1.10.2.tgz", + "integrity": "sha512-ons5CwXZ/51wrUPfoduC+cO7AS1/wRb0ybpQJ9RrssossDxVy4t49QxWoWgfBDvVKsz9VXzBk9z0wqTdZ+Cq8g==", + "license": "Apache-2.0" + }, + "node_modules/@ucast/js": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@ucast/js/-/js-3.1.0.tgz", + "integrity": "sha512-eJ7yQeYtMK85UZjxoxBEbTWx6UMxEXKbjVyp+NlzrT5oMKV5Gpo/9bjTl3r7msaXTVC8iD9NJacqJ8yp7joX+Q==", + "license": "Apache-2.0", + "dependencies": { + "@ucast/core": "1.10.2" + } + }, + "node_modules/@ucast/mongo": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/@ucast/mongo/-/mongo-2.4.3.tgz", + "integrity": "sha512-XcI8LclrHWP83H+7H2anGCEeDq0n+12FU2mXCTz6/Tva9/9ddK/iacvvhCyW6cijAAOILmt0tWplRyRhVyZLsA==", + "license": "Apache-2.0", + "dependencies": { + "@ucast/core": "^1.4.1" + } + }, + "node_modules/@ucast/mongo2js": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@ucast/mongo2js/-/mongo2js-1.4.1.tgz", + "integrity": "sha512-9aeg5cmqwRQnKCXHN6I17wk83Rcm487bHelaG8T4vfpWneAI469wSI3Srnbu+PuZ5znWRbnwtVq9RgPL+bN6CA==", + "license": "Apache-2.0", + "dependencies": { + "@ucast/core": "1.10.2", + "@ucast/js": "3.1.0", + "@ucast/mongo": "2.4.3" + } + }, "node_modules/@ungap/structured-clone": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", @@ -4602,27 +4601,13 @@ "node": ">= 0.6" } }, - "node_modules/acl": { - "version": "0.4.11", - "resolved": "https://registry.npmjs.org/acl/-/acl-0.4.11.tgz", - "integrity": "sha512-4AGXAEZ80JEGbB99qxvaBj0Gkba9fFivf4GMhYZdMlIV7eQpBWud6PaPFfksWDb0xO584tCChhOIeV0JR3kXIA==", - "dependencies": { - "async": "^2.1.4", - "bluebird": "^3.0.2", - "lodash": "^4.17.3", - "mongodb": "^2.0.47", - "redis": "^2.2.5" - }, - "engines": { - "node": ">= 0.10" - } - }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4687,15 +4672,6 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/amdefine": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", - "integrity": "sha512-S2Hw0TtNkMJhIabBwIojKL9YHO5T0n5eNqWJ7Lrlel/zDbftQpxpapi8tZs3X1HWa+u+QeydGmzzNU0m09+Rcg==", - "license": "BSD-3-Clause OR MIT", - "engines": { - "node": ">=0.4.2" - } - }, "node_modules/ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -4797,15 +4773,6 @@ "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", "license": "MIT" }, - "node_modules/async": { - "version": "2.6.4", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", - "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", - "license": "MIT", - "dependencies": { - "lodash": "^4.17.14" - } - }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -4838,6 +4805,7 @@ "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.2.0.tgz", "integrity": "sha512-0YiBEOxWqKkSQWL9nNGGEgndoeL0ZpWrbLMNL5u/Kaxrli3Eaxlt3ZtIDktEvXt4L/R9r3ODr2zKwGM/2BjxVw==", "license": "MIT", + "peer": true, "dependencies": { "@jest/transform": "30.2.0", "@types/babel__core": "^7.20.5", @@ -5115,12 +5083,6 @@ "safe-buffer": "~5.2.0" } }, - "node_modules/bluebird": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", - "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", - "license": "MIT" - }, "node_modules/body-parser": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", @@ -5202,6 +5164,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -5287,6 +5250,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/buffer-shims/-/buffer-shims-1.0.0.tgz", "integrity": "sha512-Zy8ZXMyxIT6RMTeY7OP/bDndfj6bwCan7SS98CEndS6deHwWPpseeHlwarNcBim+etXnF9HBc1non5JgDaJU1g==", + "dev": true, "license": "MIT" }, "node_modules/busboy": { @@ -6147,211 +6111,6 @@ "dev": true, "license": "ISC" }, - "node_modules/consolidate": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/consolidate/-/consolidate-1.0.4.tgz", - "integrity": "sha512-RuZ3xnqEDsxiwaoIkqVeeK3gg9qxw7+YKYX2tKhLs1eukVKMgSr4VYI3iYFsRHi4TloHYDlugrz3kvkjs3nynA==", - "license": "MIT", - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@babel/core": "^7.22.5", - "arc-templates": "^0.5.3", - "atpl": ">=0.7.6", - "bracket-template": "^1.1.5", - "coffee-script": "^1.12.7", - "dot": "^1.1.3", - "dust": "^0.3.0", - "dustjs-helpers": "^1.7.4", - "dustjs-linkedin": "^2.7.5", - "eco": "^1.1.0-rc-3", - "ect": "^0.5.9", - "ejs": "^3.1.5", - "haml-coffee": "^1.14.1", - "hamlet": "^0.3.3", - "hamljs": "^0.6.2", - "handlebars": "^4.7.6", - "hogan.js": "^3.0.2", - "htmling": "^0.0.8", - "jazz": "^0.0.18", - "jqtpl": "~1.1.0", - "just": "^0.1.8", - "liquid-node": "^3.0.1", - "liquor": "^0.0.5", - "lodash": "^4.17.20", - "mote": "^0.2.0", - "mustache": "^4.0.1", - "nunjucks": "^3.2.2", - "plates": "~0.4.11", - "pug": "^3.0.0", - "qejs": "^3.0.5", - "ractive": "^1.3.12", - "react": ">=16.13.1", - "react-dom": ">=16.13.1", - "slm": "^2.0.0", - "swig": "^1.4.2", - "swig-templates": "^2.0.3", - "teacup": "^2.0.0", - "templayed": ">=0.2.3", - "then-pug": "*", - "tinyliquid": "^0.2.34", - "toffee": "^0.3.6", - "twig": "^1.15.2", - "twing": "^5.0.2", - "underscore": "^1.11.0", - "vash": "^0.13.0", - "velocityjs": "^2.0.1", - "walrus": "^0.10.1", - "whiskers": "^0.4.0" - }, - "peerDependenciesMeta": { - "@babel/core": { - "optional": true - }, - "arc-templates": { - "optional": true - }, - "atpl": { - "optional": true - }, - "bracket-template": { - "optional": true - }, - "coffee-script": { - "optional": true - }, - "dot": { - "optional": true - }, - "dust": { - "optional": true - }, - "dustjs-helpers": { - "optional": true - }, - "dustjs-linkedin": { - "optional": true - }, - "eco": { - "optional": true - }, - "ect": { - "optional": true - }, - "ejs": { - "optional": true - }, - "haml-coffee": { - "optional": true - }, - "hamlet": { - "optional": true - }, - "hamljs": { - "optional": true - }, - "handlebars": { - "optional": true - }, - "hogan.js": { - "optional": true - }, - "htmling": { - "optional": true - }, - "jazz": { - "optional": true - }, - "jqtpl": { - "optional": true - }, - "just": { - "optional": true - }, - "liquid-node": { - "optional": true - }, - "liquor": { - "optional": true - }, - "lodash": { - "optional": true - }, - "mote": { - "optional": true - }, - "mustache": { - "optional": true - }, - "nunjucks": { - "optional": true - }, - "plates": { - "optional": true - }, - "pug": { - "optional": true - }, - "qejs": { - "optional": true - }, - "ractive": { - "optional": true - }, - "react": { - "optional": true - }, - "react-dom": { - "optional": true - }, - "slm": { - "optional": true - }, - "swig": { - "optional": true - }, - "swig-templates": { - "optional": true - }, - "teacup": { - "optional": true - }, - "templayed": { - "optional": true - }, - "then-pug": { - "optional": true - }, - "tinyliquid": { - "optional": true - }, - "toffee": { - "optional": true - }, - "twig": { - "optional": true - }, - "twing": { - "optional": true - }, - "underscore": { - "optional": true - }, - "vash": { - "optional": true - }, - "velocityjs": { - "optional": true - }, - "walrus": { - "optional": true - }, - "whiskers": { - "optional": true - } - } - }, "node_modules/content-disposition": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", @@ -7507,6 +7266,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, "license": "MIT" }, "node_modules/cors": { @@ -7532,6 +7292,7 @@ "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", @@ -7800,6 +7561,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -8133,12 +7895,6 @@ "integrity": "sha512-iGCHkfUc5kFekGiqhe8B/mdaurD+lakO9txNnTvKtA6PISrw86LgqHvRzWYPyoE2Ph5aMIrCw9/uko6XHTKCwA==", "license": "MIT" }, - "node_modules/double-ended-queue": { - "version": "2.1.0-0", - "resolved": "https://registry.npmjs.org/double-ended-queue/-/double-ended-queue-2.1.0-0.tgz", - "integrity": "sha512-+BNfZ+deCo8hMNpDqDnvT+c0XpJ5cUa6mqYq89bho2Ifze4URTqRkcwR399hWoTrTkbZ/XJYDgP6rc7pRgffEQ==", - "license": "MIT" - }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -8496,12 +8252,6 @@ "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", "license": "MIT" }, - "node_modules/es6-promise": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.2.1.tgz", - "integrity": "sha512-oj4jOSXvWglTsc3wrw86iom3LDPOx1nbipQk+jaG3dy+sMRM6ReSgVr/VlmBuF6lXUrflN9DCcQHeSbAwGUl4g==", - "license": "MIT" - }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -8535,6 +8285,7 @@ "integrity": "sha512-O0piBKY36YSJhlFSG8p9VUdPV/SxxS4FYDWVpr/9GJuMaepzwlf4J8I4ov1b+ySQfDTPhc3DtLaxcT1fN0yqCg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", @@ -8810,6 +8561,7 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", + "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -8848,6 +8600,24 @@ "url": "https://opencollective.com/express" } }, + "node_modules/express-rate-limit": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.2.1.tgz", + "integrity": "sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==", + "license": "MIT", + "dependencies": { + "ip-address": "10.0.1" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, "node_modules/express/node_modules/cookie-signature": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", @@ -10563,6 +10333,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/ip-address": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", + "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -10758,6 +10537,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, "license": "MIT" }, "node_modules/isexe": { @@ -12547,6 +12327,7 @@ "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", "license": "MIT", + "peer": true, "dependencies": { "cssstyle": "^4.2.1", "data-urls": "^5.0.0", @@ -13223,6 +13004,7 @@ "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==", "dev": true, "license": "MIT", + "peer": true, "bin": { "marked": "bin/marked.js" }, @@ -13555,20 +13337,6 @@ "node": "*" } }, - "node_modules/mongodb": { - "version": "2.2.36", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-2.2.36.tgz", - "integrity": "sha512-P2SBLQ8Z0PVx71ngoXwo12+FiSfbNfGOClAao03/bant5DgLNkOPAck5IaJcEk4gKlQhDEURzfR3xuBG1/B+IA==", - "license": "Apache-2.0", - "dependencies": { - "es6-promise": "3.2.1", - "mongodb-core": "2.1.20", - "readable-stream": "2.2.7" - }, - "engines": { - "node": ">=0.10.3" - } - }, "node_modules/mongodb-connection-string-url": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-7.0.1.tgz", @@ -13582,26 +13350,6 @@ "node": ">=20.19.0" } }, - "node_modules/mongodb-core": { - "version": "2.1.20", - "resolved": "https://registry.npmjs.org/mongodb-core/-/mongodb-core-2.1.20.tgz", - "integrity": "sha512-IN57CX5/Q1bhDq6ShAR6gIv4koFsZP7L8WOK1S0lR0pVDQaScffSMV5jxubLsmZ7J+UdqmykKw4r9hG3XQEGgQ==", - "license": "Apache-2.0", - "dependencies": { - "bson": "~1.0.4", - "require_optional": "~1.0.0" - } - }, - "node_modules/mongodb-core/node_modules/bson": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/bson/-/bson-1.0.9.tgz", - "integrity": "sha512-IQX9/h7WdMBIW/q/++tGd+emQr0XMdeZ6icnT/74Xk9fnabWn+gZgpE+9V+gujL3hhJOoNrnDVY7tWdzc7NUTg==", - "deprecated": "Fixed a critical issue with BSON serialization documented in CVE-2019-2391, see https://bit.ly/2KcpXdo for more details", - "license": "Apache-2.0", - "engines": { - "node": ">=0.6.19" - } - }, "node_modules/mongoose": { "version": "9.2.1", "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-9.2.1.tgz", @@ -15974,6 +15722,7 @@ "dev": true, "inBundle": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -16211,31 +15960,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/optimist": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", - "integrity": "sha512-snN4O4TkigujZphWLN0E//nQmm7790RYaE53DdL7ZYwee2D8DDo9/EyYiKUfN3rneWUjhJnueija3G9I2i0h3g==", - "license": "MIT/X11", - "dependencies": { - "minimist": "~0.0.1", - "wordwrap": "~0.0.2" - } - }, - "node_modules/optimist/node_modules/minimist": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", - "integrity": "sha512-iotkTvxc+TwOm5Ieim8VnSNvCDjCK9S8G3scJ50ZthspSxa7jx50jkhYduuAtAjvfDUwSgOwf8+If99AlOEhyw==", - "license": "MIT" - }, - "node_modules/optimist/node_modules/wordwrap": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", - "integrity": "sha512-1tMA907+V4QmxV7dbRvb4/8MaRALK6q9Abid3ndMYnbyo8piisCmeONVqVSXqQA3KaP4SLt5b7ud6E2sqP8TFw==", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -17010,6 +16734,7 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", "integrity": "sha512-yN0WQmuCX63LP/TMvAg31nvT6m4vDqJEiiv2CAZqWOGNWutc9DfDk1NPYYmKUFmaVM2UwDowH4u5AHWYP/jxKw==", + "dev": true, "license": "MIT" }, "node_modules/proto-list": { @@ -17419,6 +17144,7 @@ "version": "2.2.7", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.2.7.tgz", "integrity": "sha512-a6ibcfWFhgihuTw/chl+u3fB5ykBZFmnvpyZHebY0MCQE4vvYcsCLpCeaQ1BkH7HdJYavNSqF0WDLeo4IPHQaQ==", + "dev": true, "license": "MIT", "dependencies": { "buffer-shims": "~1.0.0", @@ -17456,35 +17182,6 @@ "node": ">=8" } }, - "node_modules/redis": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/redis/-/redis-2.8.0.tgz", - "integrity": "sha512-M1OkonEQwtRmZv4tEWF2VgpG0JWJ8Fv1PhlgT5+B+uNq2cA3Rt1Yt/ryoR+vQNOQcIEgdCdfH0jr3bDpihAw1A==", - "license": "MIT", - "dependencies": { - "double-ended-queue": "^2.1.0-0", - "redis-commands": "^1.2.0", - "redis-parser": "^2.6.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/redis-commands": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.7.0.tgz", - "integrity": "sha512-nJWqw3bTFy21hX/CPKHth6sfhZbdiHP6bTawSgQBlKOVRG7EZkfHbbHwQJnrE4vsQf0CMNE+3gJ4Fmm16vdVlQ==", - "license": "MIT" - }, - "node_modules/redis-parser": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-2.6.0.tgz", - "integrity": "sha512-9Hdw19gwXFBJdN8ENUoNVJFRyMDFrE/ZBClPicKYDPwNPJ4ST1TedAHYNSiGKElwh2vrmRGMoJYbVdJd+WQXIw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/registry-auth-token": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-5.1.1.tgz", @@ -17498,34 +17195,6 @@ "node": ">=14" } }, - "node_modules/require_optional": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/require_optional/-/require_optional-1.0.1.tgz", - "integrity": "sha512-qhM/y57enGWHAe3v/NcwML6a3/vfESLe/sGM2dII+gEO0BpKRUkWZow/tyloNqJyN6kXSl3RyyM8Ll5D/sJP8g==", - "license": "Apache-2.0", - "dependencies": { - "resolve-from": "^2.0.0", - "semver": "^5.1.0" - } - }, - "node_modules/require_optional/node_modules/resolve-from": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz", - "integrity": "sha512-qpFcKaXsq8+oRoLilkwyc7zHGF5i9Q2/25NIgLQQ/+VVv9rU4qvr6nXVAw1DsnXJyQkZsR4Ytfbtg5ehfcUssQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/require_optional/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "license": "ISC", - "bin": { - "semver": "bin/semver" - } - }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -17742,6 +17411,7 @@ "integrity": "sha512-WRgl5GcypwramYX4HV+eQGzUbD7UUbljVmS+5G1uMwX/wLgYuJAxGeerXJDMO2xshng4+FXqCgyB5QfClV6WjA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@semantic-release/commit-analyzer": "^13.0.1", "@semantic-release/error": "^4.0.0", @@ -18948,6 +18618,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", + "dev": true, "license": "MIT", "dependencies": { "safe-buffer": "~5.1.0" @@ -18957,6 +18628,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, "license": "MIT" }, "node_modules/string-length": { @@ -19234,87 +18906,6 @@ "express": ">=4.0.0 || >=5.0.0-beta" } }, - "node_modules/swig": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/swig/-/swig-1.4.2.tgz", - "integrity": "sha512-23eN2Cmm6XmSc9j//g7J/PlYBdm60eznA/snxYZLVpoy4diL2wzCqEsf6ThVwRhhYIngwSNSztvIdrdH9sTCGA==", - "deprecated": "This package is no longer maintained", - "license": "MIT", - "dependencies": { - "optimist": "~0.6", - "uglify-js": "~2.4" - }, - "bin": { - "swig": "bin/swig.js" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/swig/node_modules/async": { - "version": "0.2.10", - "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz", - "integrity": "sha512-eAkdoKxU6/LkKDBzLpT+t6Ff5EtfSF4wx1WfJiPEEV7WNLnDaRXk0oVysiEPm262roaachGexwUv94WhSgN5TQ==" - }, - "node_modules/swig/node_modules/camelcase": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", - "integrity": "sha512-wzLkDa4K/mzI1OSITC+DUyjgIl/ETNHE9QvYgy6J6Jvqyyz4C0Xfd+lQhb19sX2jMpZV4IssUn0VDVmglV+s4g==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/swig/node_modules/source-map": { - "version": "0.1.34", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.34.tgz", - "integrity": "sha512-yfCwDj0vR9RTwt3pEzglgb3ZgmcXHt6DjG3bjJvzPwTL+5zDQ2MhmSzAcTy0GTiQuCiriSWXvWM1/NhKdXuoQA==", - "dependencies": { - "amdefine": ">=0.0.4" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/swig/node_modules/uglify-js": { - "version": "2.4.24", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.4.24.tgz", - "integrity": "sha512-tktIjwackfZLd893KGJmXc1hrRHH1vH9Po3xFh1XBjjeGAnN02xJ3SuoA+n1L29/ZaCA18KzCFlckS+vfPugiA==", - "license": "BSD", - "dependencies": { - "async": "~0.2.6", - "source-map": "0.1.34", - "uglify-to-browserify": "~1.0.0", - "yargs": "~3.5.4" - }, - "bin": { - "uglifyjs": "bin/uglifyjs" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/swig/node_modules/wordwrap": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", - "integrity": "sha512-xSBsCeh+g+dinoBv3GAOWM4LcVVO68wLXRanibtBSdUvkGWQRGeE9P7IwU9EmDDi4jA6L44lz15CGMwdw9N5+Q==", - "license": "MIT/X11", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/swig/node_modules/yargs": { - "version": "3.5.4", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.5.4.tgz", - "integrity": "sha512-5j382E4xQSs71p/xZQsU1PtRA2HXPAjX0E0DkoGLxwNASMOKX6A9doV1NrZmj85u2Pjquz402qonBzz/yLPbPA==", - "license": "MIT/X11", - "dependencies": { - "camelcase": "^1.0.2", - "decamelize": "^1.0.0", - "window-size": "0.1.0", - "wordwrap": "0.0.2" - } - }, "node_modules/symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", @@ -19644,6 +19235,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -19950,12 +19542,6 @@ "node": ">=0.8.0" } }, - "node_modules/uglify-to-browserify": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", - "integrity": "sha512-vb2s1lYx2xBtUgy+ta+b2J/GLVUR+wmpINwHePmPRhOsIVCG2wDzKJ0n14GslH1BifsqVzSOwQhRaCAsZ/nI4Q==", - "license": "MIT" - }, "node_modules/uid2": { "version": "0.0.4", "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.4.tgz", @@ -20300,14 +19886,6 @@ "node": ">= 8" } }, - "node_modules/window-size": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", - "integrity": "sha512-1pTPQDKTdd61ozlKGNCjhNRd+KPmgLSGa3mZTHoOliaGcESD8G1PXhh7c1fgiPjVbNVfgy2Faw4BI8/m0cC8Mg==", - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/winston": { "version": "3.19.0", "resolved": "https://registry.npmjs.org/winston/-/winston-3.19.0.tgz", @@ -20674,6 +20252,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/zxcvbn": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/zxcvbn/-/zxcvbn-4.4.2.tgz", diff --git a/package.json b/package.json index 495aa2bbc..c6deb5796 100644 --- a/package.json +++ b/package.json @@ -44,21 +44,19 @@ "release:auto": "npx semantic-release" }, "dependencies": { - "@hapi/joi": "^17.1.1", + "@casl/ability": "^6.8.0", "@jest/globals": "^30.2.0", - "acl": "~0.4.11", "axios": "^1.13.5", "bcrypt": "^6.0.0", - "body-parser": "^2.2.2", "bson": "^7.2.0", "chalk": "^5.6.2", "compression": "^1.8.1", - "consolidate": "^1.0.4", "cookie-parser": "^1.4.7", "cors": "^2.8.6", "cross-env": "^10.1.0", "enhanced-resolve": "^5.19.0", "express": "^5.2.1", + "express-rate-limit": "^8.2.1", "generate-password": "^1.7.1", "glob": "^13.0.6", "handlebars": "^4.7.8", @@ -87,20 +85,20 @@ "snyk": "^1.1302.1", "supertest": "^7.2.2", "swagger-ui-express": "^5.0.1", - "swig": "^1.4.2", "ts-jest": "^29.4.6", "winston": "^3.19.0", + "zod": "^3.25.76", "zxcvbn": "^4.4.2" }, "devDependencies": { "@commitlint/cli": "^20.4.2", "@commitlint/config-conventional": "^20.4.2", + "@eslint/js": "^10.0.0", "@semantic-release/changelog": "^6.0.3", "@semantic-release/git": "^10.0.1", "@weareopensource/conventional-changelog": "^1.7.0", "commitizen": "^4.3.1", "eslint": "10.0.0", - "@eslint/js": "^10.0.0", "eslint-config-prettier": "^10.1.8", "globals": "^17.3.0", "husky": "^9.1.7",