diff --git a/package-lock.json b/package-lock.json index ae55cf0..a1acb2d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,7 +34,8 @@ "socket.io": "^4.7.5", "trivialperms": "^2.0.0-beta.0", "ts-essentials": "^10.0.0", - "zod": "^3.23.8" + "zod": "^3.23.8", + "zod-express-middleware": "^1.4.0" }, "devDependencies": { "@ckpack/vue-color": "^1.3.0", @@ -1459,7 +1460,6 @@ "version": "1.19.5", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", - "dev": true, "dependencies": { "@types/connect": "*", "@types/node": "*" @@ -1469,7 +1469,6 @@ "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", - "dev": true, "dependencies": { "@types/node": "*" } @@ -1497,7 +1496,6 @@ "version": "4.17.21", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", - "dev": true, "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.33", @@ -1509,7 +1507,6 @@ "version": "4.19.0", "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.0.tgz", "integrity": "sha512-bGyep3JqPCRry1wq+O5n7oiBgGWmeIJXPjXXCo8EK0u8duZGSYar7cGqd3ML2JUsLGeB7fmc06KYo9fLGWqPvQ==", - "dev": true, "dependencies": { "@types/node": "*", "@types/qs": "*", @@ -1520,8 +1517,7 @@ "node_modules/@types/http-errors": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", - "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", - "dev": true + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==" }, "node_modules/@types/jquery": { "version": "3.5.30", @@ -1551,8 +1547,7 @@ "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", - "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", - "dev": true + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==" }, "node_modules/@types/node": { "version": "20.12.12", @@ -1574,20 +1569,17 @@ "node_modules/@types/qs": { "version": "6.9.15", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.15.tgz", - "integrity": "sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg==", - "dev": true + "integrity": "sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg==" }, "node_modules/@types/range-parser": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", - "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", - "dev": true + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==" }, "node_modules/@types/send": { "version": "0.17.4", "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", - "dev": true, "dependencies": { "@types/mime": "^1", "@types/node": "*" @@ -1597,7 +1589,6 @@ "version": "1.15.7", "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", - "dev": true, "dependencies": { "@types/http-errors": "*", "@types/node": "*", @@ -7333,6 +7324,16 @@ "funding": { "url": "https://github.com/sponsors/colinhacks" } + }, + "node_modules/zod-express-middleware": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/zod-express-middleware/-/zod-express-middleware-1.4.0.tgz", + "integrity": "sha512-C1pBbwbuotitG1L3I1cr9QD/nuepHAdZEUbVn7y1o2cJq0oaUuS7gTVGby1+DGHL4t2P4eEZKCX0QQDv6hEs3A==", + "peerDependencies": { + "@types/express": "^4.17.12", + "express": "^4.17.1", + "zod": "^3.2.0" + } } } } diff --git a/package.json b/package.json index 0cf6214..5f27cbb 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,8 @@ "socket.io": "^4.7.5", "trivialperms": "^2.0.0-beta.0", "ts-essentials": "^10.0.0", - "zod": "^3.23.8" + "zod": "^3.23.8", + "zod-express-middleware": "^1.4.0" }, "devDependencies": { "@ckpack/vue-color": "^1.3.0", @@ -86,6 +87,11 @@ "vue-codemirror": "^6.1.1", "vue-router": "^4.1.6" }, + "overrides": { + "zod-express-middleware": { + "express": "$express" + } + }, "lint-staged": { "*.{ts,js,vue}": "npm run lint" } diff --git a/src/server/engines/validation/models/account.ts b/src/server/engines/validation/models/account.ts new file mode 100644 index 0000000..d9dc5f9 --- /dev/null +++ b/src/server/engines/validation/models/account.ts @@ -0,0 +1,43 @@ +// --------------------------------------------------------------------------------------------------------------------- +// Account Validation Model +// --------------------------------------------------------------------------------------------------------------------- + +import { z } from 'zod'; + +// --------------------------------------------------------------------------------------------------------------------- + +export const AccountID = z.string().min(4) + .regex(/^[a-zA-Z0-9]+$/); + +export const AccountSettings = z.object({ + colorMode: z.enum([ 'light', 'dark', 'auto' ]).optional() + + // Other settings... +}); + +export const Account = z.object({ + id: z.string(), + email: z.string(), + name: z.string().optional(), + avatar: z.string().optional(), + permissions: z.array(z.string()).optional(), + settings: AccountSettings.passthrough().optional() +}); + +// --------------------------------------------------------------------------------------------------------------------- +// Request Validations +// --------------------------------------------------------------------------------------------------------------------- + +export const UpdateParams = z.object({ + accountID: AccountID +}); + +export const AccountFilter = z.object({ + id: z.union([ AccountID, z.array(AccountID) ]).optional(), + email: z.union([ z.string().email(), z.array(z.string().email()) ]) + .optional(), + name: z.union([ z.string().min(1), z.array(z.string().min(1)) ]) + .optional() +}); + +// --------------------------------------------------------------------------------------------------------------------- diff --git a/src/server/routes/accounts.ts b/src/server/routes/accounts.ts index 60a447b..5dc7acb 100644 --- a/src/server/routes/accounts.ts +++ b/src/server/routes/accounts.ts @@ -3,16 +3,17 @@ //---------------------------------------------------------------------------------------------------------------------- import express from 'express'; +import { processRequest } from 'zod-express-middleware'; // Managers import * as accountMan from '../managers/account'; import * as permsMan from '../managers/permissions'; -// Models -// import { Account } from '../models/account'; +// Validation +import * as AccountValidators from '../engines/validation/models/account'; // Utils -import { convertQueryToRecord, ensureAuthenticated, errorHandler } from './utils'; +import { ensureAuthenticated, errorHandler } from './utils'; // Logger import logging from '@strata-js/util-logging'; @@ -24,23 +25,16 @@ const router = express.Router(); //---------------------------------------------------------------------------------------------------------------------- -router.get('/', async(req, resp) => +router.get('/', processRequest({ query: AccountValidators.AccountFilter }), async(req, resp) => { - const query = convertQueryToRecord(req); - const filters = { - id: query.id, - email: query.email, - name: query.name - }; - - resp.json((await accountMan.list(filters)).map((accountObj) => + resp.json((await accountMan.list(req.query)).map((accountObj) => { const { permissions, settings, groups, ...restAccount } = accountObj; return restAccount; })); }); -router.get('/:accountID', async(req, resp) => +router.get('/:accountID', processRequest({ params: AccountValidators.UpdateParams }), async(req, resp) => { const user = req.user; const account = await accountMan.get(req.params.accountID); @@ -58,12 +52,20 @@ router.get('/:accountID', async(req, resp) => } }); -router.patch('/:accountID', ensureAuthenticated, async(req, resp) => -{ - // Update the account - const newAccount = await accountMan.update(req.params.accountID, req.body); - resp.json(newAccount); -}); +router.patch( + '/:accountID', + ensureAuthenticated, + processRequest({ + params: AccountValidators.UpdateParams, + body: AccountValidators.Account.partial({ id: true }) + }), + async(req, resp) => + { + // Update the account + const newAccount = await accountMan.update(req.params.accountID, req.body); + resp.json(newAccount); + } +); //---------------------------------------------------------------------------------------------------------------------- // Error Handling