From f01e8dc4e5939de86b3e692459abe42648126b35 Mon Sep 17 00:00:00 2001 From: Anass Bouassaba Date: Sat, 14 Dec 2024 10:50:25 +0100 Subject: [PATCH 1/9] wip(idp): adding Hono and Zod packages --- idp/deno.json | 16 ++-------------- idp/deno.lock | 4 +++- idp/src/app.ts | 31 ++++++++----------------------- idp/src/health/router.ts | 12 ++++++------ 4 files changed, 19 insertions(+), 44 deletions(-) diff --git a/idp/deno.json b/idp/deno.json index fec650819..aaef4f11a 100644 --- a/idp/deno.json +++ b/idp/deno.json @@ -7,34 +7,22 @@ }, "imports": { "@/": "./src/", + "hono": "npm:hono@4.6.13", + "zod": "npm:zod@3.24.1", "camelize": "npm:camelize@1.0.1", - "cors": "npm:cors@2.8.5", "dotenv": "npm:dotenv@16.4.5", - "express": "npm:express@5.0.0", - "express-validator": "npm:express-validator@7.2.0", "handlebars": "npm:handlebars@4.7.8", "hashids": "npm:hashids@2.3.0", "jose": "npm:jose@5.9.6", "js-yaml": "npm:js-yaml@4.1.0", "meilisearch": "npm:meilisearch@0.45.0", "mime-types": "npm:mime-types@2.1.35", - "morgan": "npm:morgan@1.10.0", - "multer": "npm:multer@1.4.5-lts.1", "nodemailer": "npm:nodemailer@6.9.15", - "passport": "npm:passport@0.7.0", - "passport-jwt": "npm:passport-jwt@4.0.1", "uuid": "npm:uuid@10.0.0", - "@types/body-parser": "npm:@types/body-parser@1.19.5", - "@types/cors": "npm:@types/cors@2.8.17", - "@types/express": "npm:@types/express@5.0.0", "@types/js-yaml": "npm:@types/js-yaml@4.0.9", "@types/mime-types": "npm:@types/mime-types@2.1.4", - "@types/morgan": "npm:@types/morgan@1.9.9", - "@types/multer": "npm:@types/multer@1.4.12", "@types/node": "npm:@types/node@22.7.7", "@types/nodemailer": "npm:@types/nodemailer@6.4.16", - "@types/passport": "npm:@types/passport@1.0.16", - "@types/passport-jwt": "npm:@types/passport-jwt@4.0.1", "@types/uuid": "npm:@types/uuid@10.0.0", "globals": "npm:globals@15.11.0" }, diff --git a/idp/deno.lock b/idp/deno.lock index 454a48b9c..83ebee3b2 100644 --- a/idp/deno.lock +++ b/idp/deno.lock @@ -907,6 +907,7 @@ "npm:globals@15.11.0", "npm:handlebars@4.7.8", "npm:hashids@2.3.0", + "npm:hono@4.6.13", "npm:jose@5.9.6", "npm:js-yaml@4.1.0", "npm:meilisearch@0.45.0", @@ -916,7 +917,8 @@ "npm:nodemailer@6.9.15", "npm:passport-jwt@4.0.1", "npm:passport@0.7.0", - "npm:uuid@10.0.0" + "npm:uuid@10.0.0", + "npm:zod@3.24.1" ] } } diff --git a/idp/src/app.ts b/idp/src/app.ts index 1882418d9..0810cd3df 100644 --- a/idp/src/app.ts +++ b/idp/src/app.ts @@ -8,11 +8,7 @@ // by the GNU Affero General Public License v3.0 only, included in the file // AGPL-3.0-only in the root of this repository. import '@/infra/env.ts' -import cors from 'cors' -import express from 'express' -import logger from 'morgan' -import passport from 'passport' -import { ExtractJwt, Strategy as JwtStrategy } from 'passport-jwt' +import { Hono } from 'hono' import accountRouter from '@/account/router.ts' import { getConfig } from '@/config/config.ts' import healthRouter from '@/health/router.ts' @@ -29,12 +25,7 @@ import versionRouter from '@/version/router.ts' import { client as postgres } from '@/infra/postgres.ts' import process from 'node:process' -const app = express() - -app.use(cors()) -app.use(logger('dev')) -app.use(express.json({ limit: '3mb' })) -app.use(express.urlencoded({ extended: true })) +const app = new Hono() const { jwtSigningKey: secretOrKey, issuer, audience } = getConfig().token passport.use( @@ -59,23 +50,17 @@ passport.use( ), ) -app.use('/v3/health', healthRouter) -app.use('/v3/users', userRouter) -app.use('/v3/accounts', accountRouter) -app.use('/v3/token', tokenRouter) -app.use('/version', versionRouter) - -app.use(errorHandler) +app.route('/v3/health', healthRouter) +app.route('/v3/users', userRouter) +app.route('/v3/accounts', accountRouter) +app.route('/v3/token', tokenRouter) +app.route('/version', versionRouter) const port = getConfig().port postgres .connect() - .then(() => { - app.listen(port, () => { - console.log(`Listening on port ${port}`) - }) - }) + .then(() => Deno.serve({ port }, app.fetch)) .catch((err) => { console.error(err) process.exit(1) diff --git a/idp/src/health/router.ts b/idp/src/health/router.ts index 8c9d8a6a2..84976a6e0 100644 --- a/idp/src/health/router.ts +++ b/idp/src/health/router.ts @@ -7,22 +7,22 @@ // the Business Source License, use of this software will be governed // by the GNU Affero General Public License v3.0 only, included in the file // AGPL-3.0-only in the root of this repository. -import { Request, Response, Router } from 'express' +import { Hono } from 'hono' import { client as postgres } from '@/infra/postgres.ts' import { client as meilisearch } from '@/infra/meilisearch.ts' -const router = Router() +const router = new Hono() -router.get('', async (_: Request, res: Response) => { +router.get('', async (c) => { if (!postgres.connected) { - res.sendStatus(503) + c.status(503) return } if (!(await meilisearch.isHealthy())) { - res.sendStatus(503) + c.status(503) return } - res.send('OK') + return c.text('OK') }) export default router From 184ef65466a10330379908c491d8851920db5952 Mon Sep 17 00:00:00 2001 From: Anass Bouassaba Date: Sat, 14 Dec 2024 15:28:02 +0100 Subject: [PATCH 2/9] wip(idp): Hono and Zod implementation --- idp/deno.json | 2 +- idp/deno.lock | 724 +----------------------------- idp/src/account/router.ts | 113 ++--- idp/src/app.ts | 89 ++-- idp/src/health/router.ts | 4 +- idp/src/infra/base64.ts | 9 + idp/src/infra/error/core.ts | 32 +- idp/src/infra/error/creators.ts | 9 + idp/src/infra/error/validation.ts | 81 +++- idp/src/infra/passport-request.ts | 13 - idp/src/token/router.ts | 10 +- idp/src/token/service.ts | 24 +- idp/src/user/router.ts | 323 ++++++------- idp/src/version/router.ts | 8 +- 14 files changed, 422 insertions(+), 1019 deletions(-) delete mode 100644 idp/src/infra/passport-request.ts diff --git a/idp/deno.json b/idp/deno.json index aaef4f11a..9a044f279 100644 --- a/idp/deno.json +++ b/idp/deno.json @@ -9,11 +9,11 @@ "@/": "./src/", "hono": "npm:hono@4.6.13", "zod": "npm:zod@3.24.1", + "@hono/zod-validator": "npm:@hono/zod-validator@0.4.2", "camelize": "npm:camelize@1.0.1", "dotenv": "npm:dotenv@16.4.5", "handlebars": "npm:handlebars@4.7.8", "hashids": "npm:hashids@2.3.0", - "jose": "npm:jose@5.9.6", "js-yaml": "npm:js-yaml@4.1.0", "meilisearch": "npm:meilisearch@0.45.0", "mime-types": "npm:mime-types@2.1.35", diff --git a/idp/deno.lock b/idp/deno.lock index 83ebee3b2..5a2569cc3 100644 --- a/idp/deno.lock +++ b/idp/deno.lock @@ -1,294 +1,31 @@ { "version": "4", "specifiers": { - "npm:cors@2.8.5": "2.8.5", + "npm:@hono/zod-validator@0.4.2": "0.4.2_hono@4.6.13_zod@3.24.1", "npm:dotenv@16.4.5": "16.4.5", - "npm:express-validator@7.2.0": "7.2.0", - "npm:express@5.0.0": "5.0.0", "npm:handlebars@4.7.8": "4.7.8", "npm:hashids@2.3.0": "2.3.0", - "npm:jose@5.9.6": "5.9.6", + "npm:hono@4.6.13": "4.6.13", "npm:js-yaml@4.1.0": "4.1.0", "npm:meilisearch@0.45.0": "0.45.0", - "npm:morgan@1.10.0": "1.10.0", - "npm:multer@1.4.5-lts.1": "1.4.5-lts.1", "npm:nodemailer@6.9.15": "6.9.15", - "npm:passport-jwt@4.0.1": "4.0.1", - "npm:passport@0.7.0": "0.7.0", - "npm:uuid@10.0.0": "10.0.0" + "npm:uuid@10.0.0": "10.0.0", + "npm:zod@3.24.1": "3.24.1" }, "npm": { - "accepts@2.0.0": { - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "@hono/zod-validator@0.4.2_hono@4.6.13_zod@3.24.1": { + "integrity": "sha512-1rrlBg+EpDPhzOV4hT9pxr5+xDVmKuz6YJl+la7VCwK6ass5ldyKm5fD+umJdV2zhHD6jROoCCv8NbTwyfhT0g==", "dependencies": [ - "mime-types@3.0.0", - "negotiator" + "hono", + "zod" ] }, - "append-field@1.0.0": { - "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==" - }, "argparse@2.0.1": { "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, - "array-flatten@3.0.0": { - "integrity": "sha512-zPMVc3ZYlGLNk4mpK1NzP2wg0ml9t7fUgDsayR5Y5rSzxQilzR9FGu/EH2jQOcKSAeAfWeylyW8juy3OkWRvNA==" - }, - "basic-auth@2.0.1": { - "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", - "dependencies": [ - "safe-buffer@5.1.2" - ] - }, - "body-parser@2.0.2": { - "integrity": "sha512-SNMk0OONlQ01uk8EPeiBvTW7W4ovpL5b1O3t1sjpPgfxOQ6BqQJ6XjxinDPR79Z6HdcD5zBBwr5ssiTlgdNztQ==", - "dependencies": [ - "bytes", - "content-type", - "debug@3.1.0", - "destroy", - "http-errors", - "iconv-lite@0.5.2", - "on-finished@2.4.1", - "qs", - "raw-body", - "type-is@1.6.18" - ] - }, - "buffer-equal-constant-time@1.0.1": { - "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" - }, - "buffer-from@1.1.2": { - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" - }, - "busboy@1.6.0": { - "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", - "dependencies": [ - "streamsearch" - ] - }, - "bytes@3.1.2": { - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" - }, - "call-bind-apply-helpers@1.0.1": { - "integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==", - "dependencies": [ - "es-errors", - "function-bind" - ] - }, - "call-bind@1.0.8": { - "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", - "dependencies": [ - "call-bind-apply-helpers", - "es-define-property", - "get-intrinsic", - "set-function-length" - ] - }, - "call-bound@1.0.2": { - "integrity": "sha512-0lk0PHFe/uz0vl527fG9CgdE9WdafjDbCXvBbs+LUv000TVt2Jjhqbs4Jwm8gz070w8xXyEAxrPOMullsxXeGg==", - "dependencies": [ - "call-bind", - "get-intrinsic" - ] - }, - "concat-stream@1.6.2": { - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", - "dependencies": [ - "buffer-from", - "inherits", - "readable-stream", - "typedarray" - ] - }, - "content-disposition@1.0.0": { - "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", - "dependencies": [ - "safe-buffer@5.2.1" - ] - }, - "content-type@1.0.5": { - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==" - }, - "cookie-signature@1.2.2": { - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==" - }, - "cookie@0.6.0": { - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==" - }, - "core-util-is@1.0.3": { - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" - }, - "cors@2.8.5": { - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", - "dependencies": [ - "object-assign", - "vary" - ] - }, - "debug@2.6.9": { - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": [ - "ms@2.0.0" - ] - }, - "debug@3.1.0": { - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dependencies": [ - "ms@2.0.0" - ] - }, - "debug@4.3.6": { - "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", - "dependencies": [ - "ms@2.1.2" - ] - }, - "define-data-property@1.1.4": { - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dependencies": [ - "es-define-property", - "es-errors", - "gopd" - ] - }, - "depd@2.0.0": { - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" - }, - "destroy@1.2.0": { - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" - }, "dotenv@16.4.5": { "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==" }, - "dunder-proto@1.0.0": { - "integrity": "sha512-9+Sj30DIu+4KvHqMfLUGLFYL2PkURSYMVXJyXe92nFRvlYq5hBjLEhblKB+vkd/WVlUYMWigiY07T91Fkk0+4A==", - "dependencies": [ - "call-bind-apply-helpers", - "es-errors", - "gopd" - ] - }, - "ecdsa-sig-formatter@1.0.11": { - "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", - "dependencies": [ - "safe-buffer@5.2.1" - ] - }, - "ee-first@1.1.1": { - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" - }, - "encodeurl@1.0.2": { - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" - }, - "encodeurl@2.0.0": { - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==" - }, - "es-define-property@1.0.1": { - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==" - }, - "es-errors@1.3.0": { - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==" - }, - "es-object-atoms@1.0.0": { - "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", - "dependencies": [ - "es-errors" - ] - }, - "escape-html@1.0.3": { - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" - }, - "etag@1.8.1": { - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" - }, - "express-validator@7.2.0": { - "integrity": "sha512-I2ByKD8panjtr8Y05l21Wph9xk7kk64UMyvJCl/fFM/3CTJq8isXYPLeKW/aZBCdb/LYNv63PwhY8khw8VWocA==", - "dependencies": [ - "lodash", - "validator" - ] - }, - "express@5.0.0": { - "integrity": "sha512-V4UkHQc+B7ldh1YC84HCXHwf60M4BOMvp9rkvTUWCK5apqDC1Esnbid4wm6nFyVuDy8XMfETsJw5lsIGBWyo0A==", - "dependencies": [ - "accepts", - "body-parser", - "content-disposition", - "content-type", - "cookie", - "cookie-signature", - "debug@4.3.6", - "depd", - "encodeurl@2.0.0", - "escape-html", - "etag", - "finalhandler", - "fresh@2.0.0", - "http-errors", - "merge-descriptors", - "methods", - "mime-types@3.0.0", - "on-finished@2.4.1", - "once", - "parseurl", - "proxy-addr", - "qs", - "range-parser", - "router", - "safe-buffer@5.2.1", - "send", - "serve-static", - "setprototypeof", - "statuses", - "type-is@2.0.0", - "utils-merge", - "vary" - ] - }, - "finalhandler@2.0.0": { - "integrity": "sha512-MX6Zo2adDViYh+GcxxB1dpO43eypOGUOL12rLCOTMQv/DfIbpSJUy4oQIIZhVZkH9e+bZWKMon0XHFEju16tkQ==", - "dependencies": [ - "debug@2.6.9", - "encodeurl@1.0.2", - "escape-html", - "on-finished@2.4.1", - "parseurl", - "statuses", - "unpipe" - ] - }, - "forwarded@0.2.0": { - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" - }, - "fresh@0.5.2": { - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==" - }, - "fresh@2.0.0": { - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==" - }, - "function-bind@1.1.2": { - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" - }, - "get-intrinsic@1.2.6": { - "integrity": "sha512-qxsEs+9A+u85HhllWJJFicJfPDhRmjzoYdl64aMWW9yRIJmSyxdn8IEkuIM530/7T+lv0TIHd8L6Q/ra0tEoeA==", - "dependencies": [ - "call-bind-apply-helpers", - "dunder-proto", - "es-define-property", - "es-errors", - "es-object-atoms", - "function-bind", - "gopd", - "has-symbols", - "hasown", - "math-intrinsics" - ] - }, - "gopd@1.2.0": { - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==" - }, "handlebars@4.7.8": { "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", "dependencies": [ @@ -299,60 +36,11 @@ "wordwrap" ] }, - "has-property-descriptors@1.0.2": { - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dependencies": [ - "es-define-property" - ] - }, - "has-symbols@1.1.0": { - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==" - }, "hashids@2.3.0": { "integrity": "sha512-ljM73TE/avEhNnazxaj0Dw3BbEUuLC5yYCQ9RSkSUcT4ZSU6ZebdKCIBJ+xT/DnSYW36E9k82GH1Q6MydSIosQ==" }, - "hasown@2.0.2": { - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dependencies": [ - "function-bind" - ] - }, - "http-errors@2.0.0": { - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "dependencies": [ - "depd", - "inherits", - "setprototypeof", - "statuses", - "toidentifier" - ] - }, - "iconv-lite@0.5.2": { - "integrity": "sha512-kERHXvpSaB4aU3eANwidg79K8FlrN77m8G9V+0vOR3HYaRifrlwMEpT7ZBJqLSEIHnEgJTHcWK82wwLwwKwtag==", - "dependencies": [ - "safer-buffer" - ] - }, - "iconv-lite@0.6.3": { - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dependencies": [ - "safer-buffer" - ] - }, - "inherits@2.0.4": { - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "ipaddr.js@1.9.1": { - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" - }, - "is-promise@4.0.0": { - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==" - }, - "isarray@1.0.0": { - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" - }, - "jose@5.9.6": { - "integrity": "sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ==" + "hono@4.6.13": { + "integrity": "sha512-haV0gaMdSjy9URCRN9hxBPlqHa7fMm/T72kAImIxvw4eQLbNz1rgjN4hHElLJSieDiNuiIAXC//cC6YGz2KCbg==" }, "js-yaml@4.1.0": { "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", @@ -360,404 +48,32 @@ "argparse" ] }, - "jsonwebtoken@9.0.2": { - "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", - "dependencies": [ - "jws", - "lodash.includes", - "lodash.isboolean", - "lodash.isinteger", - "lodash.isnumber", - "lodash.isplainobject", - "lodash.isstring", - "lodash.once", - "ms@2.1.3", - "semver" - ] - }, - "jwa@1.4.1": { - "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", - "dependencies": [ - "buffer-equal-constant-time", - "ecdsa-sig-formatter", - "safe-buffer@5.2.1" - ] - }, - "jws@3.2.2": { - "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", - "dependencies": [ - "jwa", - "safe-buffer@5.2.1" - ] - }, - "lodash.includes@4.3.0": { - "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" - }, - "lodash.isboolean@3.0.3": { - "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" - }, - "lodash.isinteger@4.0.4": { - "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" - }, - "lodash.isnumber@3.0.3": { - "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" - }, - "lodash.isplainobject@4.0.6": { - "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" - }, - "lodash.isstring@4.0.1": { - "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" - }, - "lodash.once@4.1.1": { - "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" - }, - "lodash@4.17.21": { - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" - }, - "math-intrinsics@1.0.0": { - "integrity": "sha512-4MqMiKP90ybymYvsut0CH2g4XWbfLtmlCkXmtmdcDCxNB+mQcu1w/1+L/VD7vi/PSv7X2JYV7SCcR+jiPXnQtA==" - }, - "media-typer@0.3.0": { - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==" - }, - "media-typer@1.1.0": { - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==" - }, "meilisearch@0.45.0": { "integrity": "sha512-+zCzEqE+CumY4icB0Vox180adZqaNtnr60hJWGiEdmol5eWmksfY8rYsTcz87styXC2ZOg+2yF56gdH6oyIBTA==" }, - "merge-descriptors@2.0.0": { - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==" - }, - "methods@1.1.2": { - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==" - }, - "mime-db@1.52.0": { - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" - }, - "mime-db@1.53.0": { - "integrity": "sha512-oHlN/w+3MQ3rba9rqFr6V/ypF10LSkdwUysQL7GkXoTgIWeV+tcXGA852TBxH+gsh8UWoyhR1hKcoMJTuWflpg==" - }, - "mime-types@2.1.35": { - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dependencies": [ - "mime-db@1.52.0" - ] - }, - "mime-types@3.0.0": { - "integrity": "sha512-XqoSHeCGjVClAmoGFG3lVFqQFRIrTVw2OH3axRqAcfaw+gHWIfnASS92AV+Rl/mk0MupgZTRHQOjxY6YVnzK5w==", - "dependencies": [ - "mime-db@1.53.0" - ] - }, "minimist@1.2.8": { "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==" }, - "mkdirp@0.5.6": { - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dependencies": [ - "minimist" - ] - }, - "morgan@1.10.0": { - "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==", - "dependencies": [ - "basic-auth", - "debug@2.6.9", - "depd", - "on-finished@2.3.0", - "on-headers" - ] - }, - "ms@2.0.0": { - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "ms@2.1.2": { - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "ms@2.1.3": { - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "multer@1.4.5-lts.1": { - "integrity": "sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==", - "dependencies": [ - "append-field", - "busboy", - "concat-stream", - "mkdirp", - "object-assign", - "type-is@1.6.18", - "xtend" - ] - }, - "negotiator@1.0.0": { - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==" - }, "neo-async@2.6.2": { "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" }, "nodemailer@6.9.15": { "integrity": "sha512-AHf04ySLC6CIfuRtRiEYtGEXgRfa6INgWGluDhnxTZhHSKvrBu7lc1VVchQ0d8nPc4cFaZoPq8vkyNoZr0TpGQ==" }, - "object-assign@4.1.1": { - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" - }, - "object-inspect@1.13.3": { - "integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==" - }, - "on-finished@2.3.0": { - "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", - "dependencies": [ - "ee-first" - ] - }, - "on-finished@2.4.1": { - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dependencies": [ - "ee-first" - ] - }, - "on-headers@1.0.2": { - "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==" - }, - "once@1.4.0": { - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dependencies": [ - "wrappy" - ] - }, - "parseurl@1.3.3": { - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" - }, - "passport-jwt@4.0.1": { - "integrity": "sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ==", - "dependencies": [ - "jsonwebtoken", - "passport-strategy" - ] - }, - "passport-strategy@1.0.0": { - "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==" - }, - "passport@0.7.0": { - "integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==", - "dependencies": [ - "passport-strategy", - "pause", - "utils-merge" - ] - }, - "path-to-regexp@8.2.0": { - "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==" - }, - "pause@0.0.1": { - "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" - }, - "process-nextick-args@2.0.1": { - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" - }, - "proxy-addr@2.0.7": { - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "dependencies": [ - "forwarded", - "ipaddr.js" - ] - }, - "qs@6.13.0": { - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", - "dependencies": [ - "side-channel" - ] - }, - "range-parser@1.2.1": { - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" - }, - "raw-body@3.0.0": { - "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", - "dependencies": [ - "bytes", - "http-errors", - "iconv-lite@0.6.3", - "unpipe" - ] - }, - "readable-stream@2.3.8": { - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dependencies": [ - "core-util-is", - "inherits", - "isarray", - "process-nextick-args", - "safe-buffer@5.1.2", - "string_decoder", - "util-deprecate" - ] - }, - "router@2.0.0": { - "integrity": "sha512-dIM5zVoG8xhC6rnSN8uoAgFARwTE7BQs8YwHEvK0VCmfxQXMaOuA1uiR1IPwsW7JyK5iTt7Od/TC9StasS2NPQ==", - "dependencies": [ - "array-flatten", - "is-promise", - "methods", - "parseurl", - "path-to-regexp", - "setprototypeof", - "utils-merge" - ] - }, - "safe-buffer@5.1.2": { - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "safe-buffer@5.2.1": { - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" - }, - "safer-buffer@2.1.2": { - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "semver@7.6.3": { - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==" - }, - "send@1.1.0": { - "integrity": "sha512-v67WcEouB5GxbTWL/4NeToqcZiAWEq90N888fczVArY8A79J0L4FD7vj5hm3eUMua5EpoQ59wa/oovY6TLvRUA==", - "dependencies": [ - "debug@4.3.6", - "destroy", - "encodeurl@2.0.0", - "escape-html", - "etag", - "fresh@0.5.2", - "http-errors", - "mime-types@2.1.35", - "ms@2.1.3", - "on-finished@2.4.1", - "range-parser", - "statuses" - ] - }, - "serve-static@2.1.0": { - "integrity": "sha512-A3We5UfEjG8Z7VkDv6uItWw6HY2bBSBJT1KtVESn6EOoOr2jAxNhxWCLY3jDE2WcuHXByWju74ck3ZgLwL8xmA==", - "dependencies": [ - "encodeurl@2.0.0", - "escape-html", - "parseurl", - "send" - ] - }, - "set-function-length@1.2.2": { - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "dependencies": [ - "define-data-property", - "es-errors", - "function-bind", - "get-intrinsic", - "gopd", - "has-property-descriptors" - ] - }, - "setprototypeof@1.2.0": { - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" - }, - "side-channel-list@1.0.0": { - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "dependencies": [ - "es-errors", - "object-inspect" - ] - }, - "side-channel-map@1.0.1": { - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "dependencies": [ - "call-bound", - "es-errors", - "get-intrinsic", - "object-inspect" - ] - }, - "side-channel-weakmap@1.0.2": { - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "dependencies": [ - "call-bound", - "es-errors", - "get-intrinsic", - "object-inspect", - "side-channel-map" - ] - }, - "side-channel@1.1.0": { - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "dependencies": [ - "es-errors", - "object-inspect", - "side-channel-list", - "side-channel-map", - "side-channel-weakmap" - ] - }, "source-map@0.6.1": { "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" }, - "statuses@2.0.1": { - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" - }, - "streamsearch@1.1.0": { - "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==" - }, - "string_decoder@1.1.1": { - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dependencies": [ - "safe-buffer@5.1.2" - ] - }, - "toidentifier@1.0.1": { - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" - }, - "type-is@1.6.18": { - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "dependencies": [ - "media-typer@0.3.0", - "mime-types@2.1.35" - ] - }, - "type-is@2.0.0": { - "integrity": "sha512-gd0sGezQYCbWSbkZr75mln4YBidWUN60+devscpLF5mtRDUpiaTvKpBNrdaCvel1NdR2k6vclXybU5fBd2i+nw==", - "dependencies": [ - "content-type", - "media-typer@1.1.0", - "mime-types@3.0.0" - ] - }, - "typedarray@0.0.6": { - "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" - }, "uglify-js@3.19.3": { "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==" }, - "unpipe@1.0.0": { - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" - }, - "util-deprecate@1.0.2": { - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" - }, - "utils-merge@1.0.1": { - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==" - }, "uuid@10.0.0": { "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==" }, - "validator@13.12.0": { - "integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==" - }, - "vary@1.1.2": { - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" - }, "wordwrap@1.0.0": { "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==" }, - "wrappy@1.0.2": { - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" - }, - "xtend@4.0.2": { - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" + "zod@3.24.1": { + "integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==" } }, "remote": { @@ -887,36 +203,22 @@ }, "workspace": { "dependencies": [ - "npm:@types/body-parser@1.19.5", - "npm:@types/cors@2.8.17", - "npm:@types/express@5.0.0", + "npm:@hono/zod-validator@0.4.2", "npm:@types/js-yaml@4.0.9", "npm:@types/mime-types@2.1.4", - "npm:@types/morgan@1.9.9", - "npm:@types/multer@1.4.12", "npm:@types/node@22.7.7", "npm:@types/nodemailer@6.4.16", - "npm:@types/passport-jwt@4.0.1", - "npm:@types/passport@1.0.16", "npm:@types/uuid@10.0.0", "npm:camelize@1.0.1", - "npm:cors@2.8.5", "npm:dotenv@16.4.5", - "npm:express-validator@7.2.0", - "npm:express@5.0.0", "npm:globals@15.11.0", "npm:handlebars@4.7.8", "npm:hashids@2.3.0", "npm:hono@4.6.13", - "npm:jose@5.9.6", "npm:js-yaml@4.1.0", "npm:meilisearch@0.45.0", "npm:mime-types@2.1.35", - "npm:morgan@1.10.0", - "npm:multer@1.4.5-lts.1", "npm:nodemailer@6.9.15", - "npm:passport-jwt@4.0.1", - "npm:passport@0.7.0", "npm:uuid@10.0.0", "npm:zod@3.24.1" ] diff --git a/idp/src/account/router.ts b/idp/src/account/router.ts index f9f9d6293..e64ea5a00 100644 --- a/idp/src/account/router.ts +++ b/idp/src/account/router.ts @@ -7,10 +7,17 @@ // the Business Source License, use of this software will be governed // by the GNU Affero General Public License v3.0 only, included in the file // AGPL-3.0-only in the root of this repository. -import { Request, Response, Router } from 'express' -import { body, validationResult } from 'express-validator' -import { getConfig } from '@/config/config.ts' -import { parseValidationError } from '@/infra/error/validation.ts' +import { Hono } from 'hono' +import { z } from 'zod' +import { zValidator } from '@hono/zod-validator' +import { + email, + fullName, + handleValidationError, + password, + picture, + token, +} from '@/infra/error/validation.ts' import { AccountConfirmEmailOptions, AccountCreateOptions, @@ -23,75 +30,75 @@ import { sendResetPasswordEmail, } from '@/account/service.ts' -const router = Router() +const router = new Hono() router.post( '/', - body('email').isEmail().isLength({ max: 255 }), - body('password') - .isStrongPassword({ - minLength: getConfig().password.minLength, - minLowercase: getConfig().password.minLowercase, - minUppercase: getConfig().password.minUppercase, - minNumbers: getConfig().password.minNumbers, - minSymbols: getConfig().password.minSymbols, - }) - .isLength({ max: 10000 }), - body('fullName').isString().notEmpty().trim().escape().isLength({ max: 255 }), - body('picture').optional().isBase64().isByteLength({ max: 3000000 }), - async (req: Request, res: Response) => { - const result = validationResult(req) - if (!result.isEmpty()) { - throw parseValidationError(result) - } - res.json(await createUser(req.body as AccountCreateOptions)) + zValidator( + 'json', + z.object({ + email, + password, + fullName, + picture, + }), + handleValidationError, + ), + async (c) => { + const body = c.req.valid('json') as AccountCreateOptions + return c.json(await createUser(body)) }, ) -router.get('/password_requirements', (_: Request, res: Response) => { - res.json(getPasswordRequirements()) +router.get('/password_requirements', (c) => { + return c.json(getPasswordRequirements()) }) router.post( '/reset_password', - body('token').isString().notEmpty().trim(), - body('newPassword').isStrongPassword(), - async (req: Request, res: Response) => { - const result = validationResult(req) - if (!result.isEmpty()) { - throw parseValidationError(result) - } - await resetPassword(req.body as AccountResetPasswordOptions) - res.sendStatus(200) + zValidator( + 'json', + z.object({ + token, + newPassword: password, + }), + handleValidationError, + ), + async (c) => { + const body = c.req.valid('json') as AccountResetPasswordOptions + await resetPassword(body) + c.status(200) + return c.body(null) }, ) router.post( '/confirm_email', - body('token').isString().notEmpty().trim(), - async (req: Request, res: Response) => { - const result = validationResult(req) - if (!result.isEmpty()) { - throw parseValidationError(result) - } - await confirmEmail(req.body as AccountConfirmEmailOptions) - res.sendStatus(200) + zValidator( + 'json', + z.object({ token }), + handleValidationError, + ), + async (c) => { + const body = c.req.valid('json') as AccountConfirmEmailOptions + await confirmEmail(body) + c.status(200) + return c.body(null) }, ) router.post( '/send_reset_password_email', - body('email').isEmail().isLength({ max: 255 }), - async (req: Request, res: Response) => { - const result = validationResult(req) - if (!result.isEmpty()) { - throw parseValidationError(result) - } - res.json( - await sendResetPasswordEmail( - req.body as AccountSendResetPasswordEmailOptions, - ), - ) + zValidator( + 'json', + z.object({ email }), + handleValidationError, + ), + async (c) => { + const body = c.req.valid('json') as AccountSendResetPasswordEmailOptions + await sendResetPasswordEmail(body) + c.status(204) + return c.body(null) }, ) diff --git a/idp/src/app.ts b/idp/src/app.ts index 0810cd3df..4ccff9ac8 100644 --- a/idp/src/app.ts +++ b/idp/src/app.ts @@ -9,12 +9,13 @@ // AGPL-3.0-only in the root of this repository. import '@/infra/env.ts' import { Hono } from 'hono' +import { jwt } from 'hono/jwt' import accountRouter from '@/account/router.ts' import { getConfig } from '@/config/config.ts' import healthRouter from '@/health/router.ts' import { ErrorCode, - errorHandler, + ErrorData, newError, newResponse, } from '@/infra/error/core.ts' @@ -24,43 +25,77 @@ import userRouter from '@/user/router.ts' import versionRouter from '@/version/router.ts' import { client as postgres } from '@/infra/postgres.ts' import process from 'node:process' +import { User } from '@/user/model.ts' const app = new Hono() -const { jwtSigningKey: secretOrKey, issuer, audience } = getConfig().token -passport.use( - new JwtStrategy( - { - jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), - secretOrKey, - issuer, - audience, - }, - async (payload: any, done: any) => { - try { - const user = await userRepo.findById(payload.sub) - return done(null, user) - } catch { - return done( - newResponse(newError({ code: ErrorCode.InvalidCredentials })), - false, - ) +app.onError((error, c) => { + if (error.name === 'UnauthorizedError') { + return c.json( + newResponse(newError({ code: ErrorCode.InvalidCredentials })), + 401, + ) + } else { + const genericError = error as any + if ( + genericError.code && Object.values(ErrorCode).includes(genericError.code) + ) { + const data = genericError as ErrorData + if (data.error) { + console.error(data.error) } - }, - ), -) + return c.json(newResponse(data), data.status) + } else { + console.error(error) + return c.json( + newResponse(newError({ code: ErrorCode.InternalServerError })), + 500, + ) + } + } +}) + +app.route('/version', versionRouter) +app.route('/v3/accounts', accountRouter) + +app.use('/v3/*', (c, next) => { + const jwtMiddleware = jwt({ secret: getConfig().token.jwtSigningKey }) + switch (c.req.path) { + case '/v3/users/me/picture:extension': + return next() + default: + return jwtMiddleware(c, next) + } +}) + +declare module 'hono' { + interface ContextVariableMap { + user: User + } +} + +app.use('/v3/*', async (c, next) => { + const payload = c.get('jwtPayload') + try { + const user = await userRepo.findById(payload.sub) + c.set('user', user) + } catch { + return c.json( + newResponse(newError({ code: ErrorCode.InvalidCredentials })), + 401, + ) + } finally { + await next() + } +}) app.route('/v3/health', healthRouter) app.route('/v3/users', userRouter) -app.route('/v3/accounts', accountRouter) app.route('/v3/token', tokenRouter) -app.route('/version', versionRouter) - -const port = getConfig().port postgres .connect() - .then(() => Deno.serve({ port }, app.fetch)) + .then(() => Deno.serve({ port: getConfig().port }, app.fetch)) .catch((err) => { console.error(err) process.exit(1) diff --git a/idp/src/health/router.ts b/idp/src/health/router.ts index 84976a6e0..d03acb6c5 100644 --- a/idp/src/health/router.ts +++ b/idp/src/health/router.ts @@ -16,11 +16,11 @@ const router = new Hono() router.get('', async (c) => { if (!postgres.connected) { c.status(503) - return + return c.body(null) } if (!(await meilisearch.isHealthy())) { c.status(503) - return + return c.body(null) } return c.text('OK') }) diff --git a/idp/src/infra/base64.ts b/idp/src/infra/base64.ts index a52f26dec..06be952ef 100644 --- a/idp/src/infra/base64.ts +++ b/idp/src/infra/base64.ts @@ -1,3 +1,12 @@ +// Copyright (c) 2023 Anass Bouassaba. +// +// Use of this software is governed by the Business Source License +// included in the file LICENSE in the root of this repository. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the GNU Affero General Public License v3.0 only, included in the file +// AGPL-3.0-only in the root of this repository. import { Buffer } from 'node:buffer' export function base64ToBuffer(value: string): Buffer | null { diff --git a/idp/src/infra/error/core.ts b/idp/src/infra/error/core.ts index 1a249aa9c..2905dc24c 100644 --- a/idp/src/infra/error/core.ts +++ b/idp/src/infra/error/core.ts @@ -7,7 +7,6 @@ // the Business Source License, use of this software will be governed // by the GNU Affero General Public License v3.0 only, included in the file // AGPL-3.0-only in the root of this repository. -import { NextFunction, Request, Response } from 'express' export enum ErrorCode { InternalServerError = 'internal_server_error', @@ -31,7 +30,9 @@ export enum ErrorCode { SearchError = 'search_error', } -const statuses: { [key: string]: number } = { +type StatusCode = 500 | 400 | 401 | 403 | 404 | 409 | 429 + +const statuses: { [key: string]: StatusCode } = { [ErrorCode.InternalServerError]: 500, [ErrorCode.RequestValidationError]: 400, [ErrorCode.UsernameUnavailable]: 409, @@ -55,7 +56,7 @@ const statuses: { [key: string]: number } = { export type ErrorData = { code: string - status: number + status: StatusCode message: string userMessage: string moreInfo: string @@ -65,7 +66,7 @@ export type ErrorData = { export type ErrorResponse = { code: string - status: number + status: StatusCode message: string userMessage: string moreInfo: string @@ -100,26 +101,3 @@ export function newResponse(data: ErrorData): ErrorResponse { moreInfo: data.moreInfo, } } - -export function errorHandler( - /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ - error: any, - _: Request, - res: Response, - next: NextFunction, -) { - if (error.code && Object.values(ErrorCode).includes(error.code)) { - const data = error as ErrorData - if (data.error) { - console.error(data.error) - } - res.status(data.status).json(newResponse(data)) - } else { - console.error(error) - res - .status(500) - .json(newResponse(newError({ code: ErrorCode.InternalServerError }))) - } - next(error) - return -} diff --git a/idp/src/infra/error/creators.ts b/idp/src/infra/error/creators.ts index 433ebfd7c..b95e075f3 100644 --- a/idp/src/infra/error/creators.ts +++ b/idp/src/infra/error/creators.ts @@ -1,3 +1,12 @@ +// Copyright (c) 2023 Anass Bouassaba. +// +// Use of this software is governed by the Business Source License +// included in the file LICENSE in the root of this repository. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the GNU Affero General Public License v3.0 only, included in the file +// AGPL-3.0-only in the root of this repository. import { ErrorCode, newError } from '@/infra/error/core.ts' export function newInternalServerError(error?: unknown) { diff --git a/idp/src/infra/error/validation.ts b/idp/src/infra/error/validation.ts index 87e68b3d8..1f06f2675 100644 --- a/idp/src/infra/error/validation.ts +++ b/idp/src/infra/error/validation.ts @@ -1,17 +1,26 @@ +// Copyright (c) 2023 Anass Bouassaba. +// +// Use of this software is governed by the Business Source License +// included in the file LICENSE in the root of this repository. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the GNU Affero General Public License v3.0 only, included in the file +// AGPL-3.0-only in the root of this repository. +import { z, ZodError } from 'zod' import { ErrorCode, ErrorData, newError } from '@/infra/error/core.ts' +import { getConfig } from '@/config/config.ts' +import { Buffer } from 'node:buffer' -/* eslint-disable-next-line @typescript-eslint/no-explicit-any */ -export function parseValidationError(result: any): ErrorData { +export function parseValidationError(result: ZodError): ErrorData { let message: string | undefined let userMessage: string | undefined if (result.errors) { message = result.errors - /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ - .map((e: any) => `${e.msg} for ${e.type} '${e.path}' in ${e.location}.`) + .map((e) => `${e.message} for ${e.path.join('.')}.`) .join(' ') userMessage = result.errors - /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ - .map((e: any) => `${e.msg} for ${e.type} '${e.path}'.`) + .map((e) => `${e.message} for ${e.path.join('.')}.`) .join(' ') } return newError({ @@ -20,3 +29,63 @@ export function parseValidationError(result: any): ErrorData { userMessage, }) } + +type ZodValidationResult = { + success: true + data: T +} | { + success: false + error: ZodError + data: T +} + +export function handleValidationError(result: ZodValidationResult) { + if (!result.success) { + throw parseValidationError(result.error) + } +} + +export const password = z + .string() + .min( + getConfig().password.minLength, + `Password must be at least ${getConfig().password.minLength} characters long.`, + ) + .regex( + new RegExp(`(?=(.*[a-z]){${getConfig().password.minLowercase},})`), + `Password must contain at least ${getConfig().password.minLowercase} lowercase character(s).`, + ) + .regex( + new RegExp(`(?=(.*[A-Z]){${getConfig().password.minUppercase},})`), + `Password must contain at least ${getConfig().password.minUppercase} uppercase character(s).`, + ) + .regex( + new RegExp(`(?=(.*[0-9]){${getConfig().password.minNumbers},})`), + `Password must contain at least ${getConfig().password.minNumbers} number(s).`, + ) + .regex( + new RegExp( + `(?=(.*[!@#$%^&*()_+\\-=\\[\\]{};':"\\|,.<>\\/?]){${getConfig().password.minSymbols},})`, + ), + `Password must contain at least ${getConfig().password.minSymbols} symbol(s).`, + ) + +export const picture = z + .string() + .optional() + .refine((value) => { + if (!value) { + return true + } + try { + return Buffer.from(value, 'base64').length <= 3000000 + } catch { + return false + } + }, { message: 'Picture must be a valid Base64 string and <= 3MB.' }) + +export const email = z.string().email().trim().max(255) + +export const fullName = z.string().nonempty().trim().max(255) + +export const token = z.string().nonempty() diff --git a/idp/src/infra/passport-request.ts b/idp/src/infra/passport-request.ts deleted file mode 100644 index 57e3e5bce..000000000 --- a/idp/src/infra/passport-request.ts +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright (c) 2023 Anass Bouassaba. -// -// Use of this software is governed by the Business Source License -// included in the file LICENSE in the root of this repository. -// -// As of the Change Date specified in that file, in accordance with -// the Business Source License, use of this software will be governed -// by the GNU Affero General Public License v3.0 only, included in the file -// AGPL-3.0-only in the root of this repository. -import { Request } from 'express' -import { User } from '@/user/model.ts' - -export type PassportRequest = Request & { user: User } diff --git a/idp/src/token/router.ts b/idp/src/token/router.ts index 1332ae2c0..d7362cd86 100644 --- a/idp/src/token/router.ts +++ b/idp/src/token/router.ts @@ -7,14 +7,14 @@ // the Business Source License, use of this software will be governed // by the GNU Affero General Public License v3.0 only, included in the file // AGPL-3.0-only in the root of this repository. -import { Request, Response, Router } from 'express' +import { Hono } from 'hono' import { exchange, TokenExchangeOptions } from '@/token/service.ts' -const router = Router() +const router = new Hono() -router.post('/', async (req: Request, res: Response) => { - const options = req.body as TokenExchangeOptions - res.json(await exchange(options)) +router.post('/', async (c) => { + const options = (await c.req.json()) as TokenExchangeOptions + return c.json(await exchange(options)) }) export default router diff --git a/idp/src/token/service.ts b/idp/src/token/service.ts index 78c0d4286..1d3d531ea 100644 --- a/idp/src/token/service.ts +++ b/idp/src/token/service.ts @@ -7,7 +7,7 @@ // the Business Source License, use of this software will be governed // by the GNU Affero General Public License v3.0 only, included in the file // AGPL-3.0-only in the root of this repository. -import { decodeJwt, SignJWT } from 'jose' +import { decode, sign } from 'hono/jwt' import { getConfig } from '@/config/config.ts' import { newEmailNotConfirmedError, @@ -93,7 +93,8 @@ export async function exchange(options: TokenExchangeOptions): Promise { } export function checkAdmin(jwt: string) { - if (!decodeJwt(jwt).is_admin) { + const { payload } = decode(jwt) + if (!payload.is_admin) { throw newUserIsNotAdminError() } } @@ -124,13 +125,18 @@ function validateParameters(options: TokenExchangeOptions) { async function newToken(userId: string, isAdmin: boolean): Promise { const config = getConfig().token const expiry = newAccessTokenExpiry() - const jwt = await new SignJWT({ sub: userId, is_admin: isAdmin }) - .setProtectedHeader({ alg: 'HS256' }) - .setIssuedAt() - .setIssuer(config.issuer) - .setAudience(config.audience) - .setExpirationTime(expiry) - .sign(new TextEncoder().encode(config.jwtSigningKey)) + const jwt = await sign( + { + sub: userId, + is_admin: isAdmin, + exp: expiry, + aud: config.audience, + iss: config.issuer, + iat: Math.floor(new Date().getTime() / 1000), + }, + config.jwtSigningKey, + 'HS256', + ) const token: Token = { access_token: jwt, expires_in: expiry, diff --git a/idp/src/user/router.ts b/idp/src/user/router.ts index d1009c4ed..9eb23d49b 100644 --- a/idp/src/user/router.ts +++ b/idp/src/user/router.ts @@ -7,21 +7,25 @@ // the Business Source License, use of this software will be governed // by the GNU Affero General Public License v3.0 only, included in the file // AGPL-3.0-only in the root of this repository. -import { Response, Router } from 'express' -import { body, query, validationResult } from 'express-validator' -import fs from 'node:fs/promises' -import { jwtVerify } from 'jose' -import multer from 'multer' -import os from 'node:os' -import passport from 'passport' +import { Hono } from 'hono' +import { verify } from 'hono/jwt' +import { z } from 'zod' +import { zValidator } from '@hono/zod-validator' +import fs, { writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' import { getConfig } from '@/config/config.ts' import { newInvalidJwtError, newMissingQueryParamError, newPictureNotFoundError, } from '@/infra/error/creators.ts' -import { parseValidationError } from '@/infra/error/validation.ts' -import { PassportRequest } from '@/infra/passport-request.ts' +import { + email, + fullName, + handleValidationError, + password, + token, +} from '@/infra/error/validation.ts' import { checkAdmin } from '@/token/service.ts' import { deletePicture, @@ -45,214 +49,211 @@ import { UserUpdateFullNameOptions, UserUpdatePasswordOptions, } from '@/user/service.ts' +import { extname, join } from 'node:path' +import { Buffer } from 'node:buffer' +import { UserListOptions } from '@/user/service.ts' -const router = Router() +const router = new Hono() -router.get( - '/me', - passport.authenticate('jwt', { session: false }), - async (req: PassportRequest, res: Response) => { - res.json(await getUser(req.user.id)) - }, -) +router.get('/me', async (c) => { + c.json(await getUser(c.get('user').id)) +}) -router.get( - '/me/picture:extension', - async (req: PassportRequest, res: Response) => { - if (!req.query.access_token) { - throw newMissingQueryParamError('access_token') - } - const userId = await getUserIdFromAccessToken( - req.query.access_token as string, - ) - const { buffer, extension, mime } = await getUserPicture(userId) - if (extension !== req.params.extension) { - throw newPictureNotFoundError() - } - res.setHeader( - 'Content-Disposition', - `attachment; filename=picture.${extension}`, - ) - res.setHeader('Content-Type', mime) - res.send(buffer) - }, -) +router.get('/me/picture:extension', async (c) => { + const accessToken = c.req.query('access_token') + if (!accessToken) { + throw newMissingQueryParamError('access_token') + } + const userId = await getUserIdFromAccessToken(accessToken) + const { buffer, extension, mime } = await getUserPicture(userId) + if (extension !== c.req.param('extension')) { + throw newPictureNotFoundError() + } + c.res.headers.append( + 'Content-Disposition', + `attachment; filename=picture.${extension}`, + ) + c.res.headers.append('Content-Type', mime) + return c.body(buffer) +}) router.post( '/me/update_full_name', - passport.authenticate('jwt', { session: false }), - body('fullName').isString().notEmpty().trim().escape().isLength({ max: 255 }), - async (req: PassportRequest, res: Response) => { - const result = validationResult(req) - if (!result.isEmpty()) { - throw parseValidationError(result) - } - res.json( - await updateFullName(req.user.id, req.body as UserUpdateFullNameOptions), - ) + zValidator( + 'json', + z.object({ fullName }), + handleValidationError, + ), + async (c) => { + const body = c.req.valid('json') as UserUpdateFullNameOptions + return c.json(await updateFullName(c.get('user').id, body)) }, ) router.post( '/me/update_email_request', - passport.authenticate('jwt', { session: false }), - body('email').isEmail().isLength({ max: 255 }), - async (req: PassportRequest, res: Response) => { - const result = validationResult(req) - if (!result.isEmpty()) { - throw parseValidationError(result) - } - res.json( - await updateEmailRequest( - req.user.id, - req.body as UserUpdateEmailRequestOptions, - ), - ) + zValidator( + 'json', + z.object({ email }), + handleValidationError, + ), + async (c) => { + const body = c.req.valid('json') as UserUpdateEmailRequestOptions + return c.json(await updateEmailRequest(c.get('user').id, body)) }, ) router.post( '/me/update_email_confirmation', - passport.authenticate('jwt', { session: false }), - body('token').isString().notEmpty().trim(), - async (req: PassportRequest, res: Response) => { - const result = validationResult(req) - if (!result.isEmpty()) { - throw parseValidationError(result) - } - res.json( - await updateEmailConfirmation( - req.body as UserUpdateEmailConfirmationOptions, - ), - ) + zValidator( + 'json', + z.object({ token }), + handleValidationError, + ), + async (c) => { + const body = c.req.valid('json') as UserUpdateEmailConfirmationOptions + c.json(await updateEmailConfirmation(body)) }, ) router.post( '/me/update_password', - passport.authenticate('jwt', { session: false }), - body('currentPassword').notEmpty(), - body('newPassword').isStrongPassword(), - async (req: PassportRequest, res: Response) => { - const result = validationResult(req) - if (!result.isEmpty()) { - throw parseValidationError(result) - } - res.json( - await updatePassword(req.user.id, req.body as UserUpdatePasswordOptions), - ) + zValidator( + 'json', + z.object({ + currentPassword: password, + newPassword: password, + }), + handleValidationError, + ), + async (c) => { + const body = c.req.valid('json') as UserUpdatePasswordOptions + return c.json(await updatePassword(c.get('user').id, body)) }, ) router.post( '/me/update_picture', - passport.authenticate('jwt', { session: false }), - multer({ - dest: os.tmpdir(), - limits: { fileSize: 3000000, fields: 0, files: 1 }, - }).single('file'), - async (req: PassportRequest, res: Response) => { - const user = await updatePicture( - req.user.id, - req.file.path, - req.file.mimetype, - ) - await fs.rm(req.file.path) - res.json(user) - }, -) + zValidator( + 'form', + z.object({ + file: z.instanceof(File) + .refine((file) => file.size <= 3_000_000, 'File too large.') + .refine((file) => + [ + 'image/jpeg', + 'image/png', + 'image/gif', + 'image/webp', + 'image/bmp', + 'image/tiff', + 'image/svg+xml', + 'image/x-icon', + ].includes(file.type), 'File is not an image.'), + }), + handleValidationError, + ), + async (c) => { + const { file }: { file: File } = c.req.valid('form') -router.post( - '/me/delete_picture', - passport.authenticate('jwt', { session: false }), - async (req: PassportRequest, res: Response) => { - res.json(await deletePicture(req.user.id)) + const path = join(tmpdir(), `${extname(file.name)}`) + const arrayBuffer = await file.arrayBuffer() + await writeFile(path, Buffer.from(arrayBuffer)) + + try { + return c.json(await updatePicture(c.get('user').id, path, file.type)) + } finally { + await fs.rm(path) + } }, ) +router.post('/me/delete_picture', async (c) => { + return c.json(await deletePicture(c.get('user').id)) +}) + router.delete( '/me', - passport.authenticate('jwt', { session: false }), - body('password').isString().notEmpty(), - async (req: PassportRequest, res: Response) => { - const result = validationResult(req) - if (!result.isEmpty()) { - throw parseValidationError(result) - } - await deleteUser(req.user.id, req.body as UserDeleteOptions) - res.sendStatus(204) + zValidator( + 'json', + z.object({ password }), + handleValidationError, + ), + async (c) => { + const body = c.req.valid('json') as UserDeleteOptions + await deleteUser(c.get('user').id, body) + c.status(204) + return c.body(null) }, ) router.get( '/', - passport.authenticate('jwt', { session: false }), - query('query').isString().optional(), - query('page').isInt(), - query('size').isInt(), - async (req: PassportRequest, res: Response) => { - checkAdmin(req.header('Authorization')) - const result = validationResult(req) - if (!result.isEmpty()) { - throw parseValidationError(result) - } - res.json( - await list({ - query: req.query.query as string, - size: parseInt(req.query.size as string), - page: parseInt(req.query.page as string), - }), - ) + zValidator( + 'query', + z.object({ + query: z.string().optional(), + page: z.number().int(), + size: z.number().int(), + }), + handleValidationError, + ), + async (c) => { + checkAdmin(c.req.header('Authorization')!) + const { query, size, page } = c.req.valid('query') as UserListOptions + return c.json(await list({ query, size, page })) }, ) router.post( '/:id/suspend', - passport.authenticate('jwt', { session: false }), - body('suspend').isBoolean(), - async (req: PassportRequest, res: Response) => { - checkAdmin(req.header('Authorization')) - const result = validationResult(req) - if (!result.isEmpty()) { - throw parseValidationError(result) - } - await suspendUser(req.params.id, req.body as UserSuspendOptions) - res.sendStatus(200) + zValidator( + 'json', + z.object({ suspend: z.boolean() }), + handleValidationError, + ), + async (c) => { + checkAdmin(c.req.header('Authorization')!) + const { id } = c.req.param() + const body = c.req.valid('json') as UserSuspendOptions + await suspendUser(id, body) + c.status(200) + return c.body(null) }, ) router.post( '/:id/make_admin', - passport.authenticate('jwt', { session: false }), - body('makeAdmin').isBoolean(), - async (req: PassportRequest, res: Response) => { - checkAdmin(req.header('Authorization')) - const result = validationResult(req) - if (!result.isEmpty()) { - throw parseValidationError(result) - } - await makeAdminUser(req.params.id, req.body as UserMakeAdminOptions) - res.sendStatus(200) + zValidator( + 'json', + z.object({ makeAdmin: z.boolean() }), + handleValidationError, + ), + async (c) => { + checkAdmin(c.req.header('Authorization')!) + const { id } = c.req.param() + const body = c.req.valid('json') as UserMakeAdminOptions + await makeAdminUser(id, body) + c.status(200) + return c.body(null) }, ) -router.get( - '/:id', - passport.authenticate('jwt', { session: false }), - async (req: PassportRequest, res: Response) => { - checkAdmin(req.header('Authorization')) - res.json(await getUserByAdmin(req.params.id)) - }, -) +router.get('/:id', async (c) => { + checkAdmin(c.req.header('Authorization')!) + const { id } = c.req.param() + return c.json(await getUserByAdmin(id)) +}) async function getUserIdFromAccessToken(accessToken: string): Promise { try { - const { payload } = await jwtVerify( + const payload = await verify( accessToken, - new TextEncoder().encode(getConfig().token.jwtSigningKey), + getConfig().token.jwtSigningKey, + 'HS256', ) if (payload.sub) { - return payload.sub + return payload.sub as string } else { throw newInvalidJwtError() } diff --git a/idp/src/version/router.ts b/idp/src/version/router.ts index e39224d02..e86b9a0d1 100644 --- a/idp/src/version/router.ts +++ b/idp/src/version/router.ts @@ -7,12 +7,12 @@ // the Business Source License, use of this software will be governed // by the GNU Affero General Public License v3.0 only, included in the file // AGPL-3.0-only in the root of this repository. -import { Request, Response, Router } from 'express' +import { Hono } from 'hono' -const router = Router() +const router = new Hono() -router.get('/', (_: Request, res: Response) => { - res.json({ version: '3.0.0' }) +router.get('/', (c) => { + return c.json({ version: '3.0.0' }) }) export default router From 03df7fa6cf05309a4fc9c0059e409eae8c399c9d Mon Sep 17 00:00:00 2001 From: Anass Bouassaba Date: Sat, 14 Dec 2024 16:07:31 +0100 Subject: [PATCH 3/9] wip(idp): fixing various issues --- idp/README.md | 8 +------- idp/deno.json | 1 - idp/src/app.ts | 36 ++++++++++++++++++++++++------------ idp/src/token/router.ts | 24 ++++++++++++++++++++---- idp/src/user/router.ts | 8 ++++++-- 5 files changed, 51 insertions(+), 26 deletions(-) diff --git a/idp/README.md b/idp/README.md index 804d5e334..7419a7c87 100644 --- a/idp/README.md +++ b/idp/README.md @@ -2,13 +2,7 @@ ## Getting Started -Run for development: - -```shell -deno task dev -``` - -Run for production: +Run: ```shell deno task start diff --git a/idp/deno.json b/idp/deno.json index 9a044f279..e0e69f384 100644 --- a/idp/deno.json +++ b/idp/deno.json @@ -1,7 +1,6 @@ { "tasks": { "start": "deno run --allow-read --allow-write --allow-env --allow-sys --allow-net src/app.ts", - "dev": "deno run --allow-read --allow-write --allow-env --allow-sys --allow-net --reload src/app.ts", "compile": "deno compile --allow-read --allow-write --allow-env --allow-sys --allow-net --output voltaserve-idp src/app.ts", "check": "deno check src/app.ts" }, diff --git a/idp/src/app.ts b/idp/src/app.ts index 4ccff9ac8..86ac0a35b 100644 --- a/idp/src/app.ts +++ b/idp/src/app.ts @@ -30,17 +30,26 @@ import { User } from '@/user/model.ts' const app = new Hono() app.onError((error, c) => { - if (error.name === 'UnauthorizedError') { + if (error.message === 'Unauthorized') { return c.json( newResponse(newError({ code: ErrorCode.InvalidCredentials })), 401, ) } else { - const genericError = error as any - if ( - genericError.code && Object.values(ErrorCode).includes(genericError.code) - ) { - const data = genericError as ErrorData + console.error(error) + return c.json( + newResponse(newError({ code: ErrorCode.InternalServerError })), + 500, + ) + } +}) + +app.use('*', async (c, next) => { + try { + return await next() + } catch (error: any) { + if (error.code && Object.values(ErrorCode).includes(error.code)) { + const data = error as ErrorData if (data.error) { console.error(data.error) } @@ -55,16 +64,17 @@ app.onError((error, c) => { } }) -app.route('/version', versionRouter) -app.route('/v3/accounts', accountRouter) - -app.use('/v3/*', (c, next) => { +app.use('/v3/*', async (c, next) => { const jwtMiddleware = jwt({ secret: getConfig().token.jwtSigningKey }) switch (c.req.path) { + case '/v3/token': case '/v3/users/me/picture:extension': - return next() + case '/v3/health': + case '/v3/accounts': + case '/version': + return await next() default: - return jwtMiddleware(c, next) + return await jwtMiddleware(c, next) } }) @@ -91,7 +101,9 @@ app.use('/v3/*', async (c, next) => { app.route('/v3/health', healthRouter) app.route('/v3/users', userRouter) +app.route('/v3/accounts', accountRouter) app.route('/v3/token', tokenRouter) +app.route('/version', versionRouter) postgres .connect() diff --git a/idp/src/token/router.ts b/idp/src/token/router.ts index d7362cd86..9329c47c5 100644 --- a/idp/src/token/router.ts +++ b/idp/src/token/router.ts @@ -8,13 +8,29 @@ // by the GNU Affero General Public License v3.0 only, included in the file // AGPL-3.0-only in the root of this repository. import { Hono } from 'hono' +import { z } from 'zod' +import { zValidator } from '@hono/zod-validator' import { exchange, TokenExchangeOptions } from '@/token/service.ts' +import { handleValidationError } from '@/infra/error/validation.ts' const router = new Hono() -router.post('/', async (c) => { - const options = (await c.req.json()) as TokenExchangeOptions - return c.json(await exchange(options)) -}) +router.post( + '/', + zValidator( + 'form', + z.object({ + grant_type: z.union([z.literal('password'), z.literal('refresh_token')]), + username: z.string().optional(), + password: z.string().optional(), + refresh_token: z.string().optional(), + }), + handleValidationError, + ), + async (c) => { + const options = (await c.req.valid('form')) as TokenExchangeOptions + return c.json(await exchange(options)) + }, +) export default router diff --git a/idp/src/user/router.ts b/idp/src/user/router.ts index 9eb23d49b..2e9658ced 100644 --- a/idp/src/user/router.ts +++ b/idp/src/user/router.ts @@ -193,8 +193,12 @@ router.get( 'query', z.object({ query: z.string().optional(), - page: z.number().int(), - size: z.number().int(), + page: z.string().regex(/^\d+$/, 'Must be a numeric value.').transform( + Number, + ), + size: z.string().regex(/^\d+$/, 'Must be a numeric value.').transform( + Number, + ), }), handleValidationError, ), From 976523328005d9c4fa8be2c3ba4a9250f65016d3 Mon Sep 17 00:00:00 2001 From: Anass Bouassaba Date: Sat, 14 Dec 2024 16:59:08 +0100 Subject: [PATCH 4/9] wip(idp): fixing various issues --- idp/src/account/router.ts | 9 +++------ idp/src/app.ts | 22 +++++++++++++--------- idp/src/health/router.ts | 6 ++---- idp/src/user/router.ts | 31 +++++++++++++++---------------- 4 files changed, 33 insertions(+), 35 deletions(-) diff --git a/idp/src/account/router.ts b/idp/src/account/router.ts index e64ea5a00..2613bcb99 100644 --- a/idp/src/account/router.ts +++ b/idp/src/account/router.ts @@ -67,8 +67,7 @@ router.post( async (c) => { const body = c.req.valid('json') as AccountResetPasswordOptions await resetPassword(body) - c.status(200) - return c.body(null) + return c.body(null, 200) }, ) @@ -82,8 +81,7 @@ router.post( async (c) => { const body = c.req.valid('json') as AccountConfirmEmailOptions await confirmEmail(body) - c.status(200) - return c.body(null) + return c.body(null, 200) }, ) @@ -97,8 +95,7 @@ router.post( async (c) => { const body = c.req.valid('json') as AccountSendResetPasswordEmailOptions await sendResetPasswordEmail(body) - c.status(204) - return c.body(null) + return c.body(null, 204) }, ) diff --git a/idp/src/app.ts b/idp/src/app.ts index 86ac0a35b..40ea8c21d 100644 --- a/idp/src/app.ts +++ b/idp/src/app.ts @@ -10,6 +10,7 @@ import '@/infra/env.ts' import { Hono } from 'hono' import { jwt } from 'hono/jwt' +import { logger } from 'hono/logger' import accountRouter from '@/account/router.ts' import { getConfig } from '@/config/config.ts' import healthRouter from '@/health/router.ts' @@ -29,6 +30,8 @@ import { User } from '@/user/model.ts' const app = new Hono() +app.use(logger()) + app.onError((error, c) => { if (error.message === 'Unauthorized') { return c.json( @@ -66,15 +69,16 @@ app.use('*', async (c, next) => { app.use('/v3/*', async (c, next) => { const jwtMiddleware = jwt({ secret: getConfig().token.jwtSigningKey }) - switch (c.req.path) { - case '/v3/token': - case '/v3/users/me/picture:extension': - case '/v3/health': - case '/v3/accounts': - case '/version': - return await next() - default: - return await jwtMiddleware(c, next) + if ( + c.req.path.startsWith('/v3/users/me/picture') || + c.req.path === '/v3/token' || + c.req.path === '/v3/health' || + c.req.path === '/v3/accounts' || + c.req.path === '/version' + ) { + return await next() + } else { + return await jwtMiddleware(c, next) } }) diff --git a/idp/src/health/router.ts b/idp/src/health/router.ts index d03acb6c5..efa73a715 100644 --- a/idp/src/health/router.ts +++ b/idp/src/health/router.ts @@ -15,12 +15,10 @@ const router = new Hono() router.get('', async (c) => { if (!postgres.connected) { - c.status(503) - return c.body(null) + return c.body(null, 503) } if (!(await meilisearch.isHealthy())) { - c.status(503) - return c.body(null) + return c.body(null, 503) } return c.text('OK') }) diff --git a/idp/src/user/router.ts b/idp/src/user/router.ts index 2e9658ced..daa51b21b 100644 --- a/idp/src/user/router.ts +++ b/idp/src/user/router.ts @@ -49,32 +49,34 @@ import { UserUpdateFullNameOptions, UserUpdatePasswordOptions, } from '@/user/service.ts' -import { extname, join } from 'node:path' +import { basename, extname, join } from 'node:path' import { Buffer } from 'node:buffer' import { UserListOptions } from '@/user/service.ts' const router = new Hono() router.get('/me', async (c) => { - c.json(await getUser(c.get('user').id)) + return c.json(await getUser(c.get('user').id)) }) -router.get('/me/picture:extension', async (c) => { +router.get('/me/:filename', async (c) => { + const { filename } = c.req.param() + if (basename(filename, extname(filename)) !== 'picture') { + return c.body(null, 404) + } const accessToken = c.req.query('access_token') if (!accessToken) { throw newMissingQueryParamError('access_token') } const userId = await getUserIdFromAccessToken(accessToken) const { buffer, extension, mime } = await getUserPicture(userId) - if (extension !== c.req.param('extension')) { + if (extension !== extname(c.req.param('filename'))) { throw newPictureNotFoundError() } - c.res.headers.append( - 'Content-Disposition', - `attachment; filename=picture.${extension}`, - ) - c.res.headers.append('Content-Type', mime) - return c.body(buffer) + return c.body(buffer, 200, { + 'Content-Type': mime, + 'Content-Disposition': `attachment; filename=picture${extension}`, + }) }) router.post( @@ -182,8 +184,7 @@ router.delete( async (c) => { const body = c.req.valid('json') as UserDeleteOptions await deleteUser(c.get('user').id, body) - c.status(204) - return c.body(null) + return c.body(null, 204) }, ) @@ -221,8 +222,7 @@ router.post( const { id } = c.req.param() const body = c.req.valid('json') as UserSuspendOptions await suspendUser(id, body) - c.status(200) - return c.body(null) + return c.body(null, 200) }, ) @@ -238,8 +238,7 @@ router.post( const { id } = c.req.param() const body = c.req.valid('json') as UserMakeAdminOptions await makeAdminUser(id, body) - c.status(200) - return c.body(null) + return c.body(null, 200) }, ) From b29abc5eaaa76308aee7b7c77349fde9cb62ec63 Mon Sep 17 00:00:00 2001 From: Anass Bouassaba Date: Sat, 14 Dec 2024 18:14:41 +0100 Subject: [PATCH 5/9] fix(idp): stability --- idp/src/app.ts | 2 +- idp/src/user/repo.ts | 40 ++++++++++++++++++++-------------------- idp/src/user/router.ts | 2 +- idp/src/user/service.ts | 1 + 4 files changed, 23 insertions(+), 22 deletions(-) diff --git a/idp/src/app.ts b/idp/src/app.ts index 40ea8c21d..dbf6b1617 100644 --- a/idp/src/app.ts +++ b/idp/src/app.ts @@ -70,10 +70,10 @@ app.use('*', async (c, next) => { app.use('/v3/*', async (c, next) => { const jwtMiddleware = jwt({ secret: getConfig().token.jwtSigningKey }) if ( + c.req.path.startsWith('/v3/accounts') || c.req.path.startsWith('/v3/users/me/picture') || c.req.path === '/v3/token' || c.req.path === '/v3/health' || - c.req.path === '/v3/accounts' || c.req.path === '/version' ) { return await next() diff --git a/idp/src/user/repo.ts b/idp/src/user/repo.ts index 7f1110544..6d5f8d35d 100644 --- a/idp/src/user/repo.ts +++ b/idp/src/user/repo.ts @@ -16,55 +16,55 @@ import { InsertOptions, UpdateOptions, User } from '@/user/model.ts' class UserRepoImpl { async findById(id: string): Promise { - const { rowCount, rows } = await client.queryObject( + const { rows } = await client.queryObject( `SELECT * FROM "user" WHERE id = $1`, [id], ) - if (rowCount && rowCount < 1) { + if (rows.length === 0) { throw newUserNotFoundError() } return this.mapRow(rows[0]) } async findByUsername(username: string): Promise { - const { rowCount, rows } = await client.queryObject( + const { rows } = await client.queryObject( `SELECT * FROM "user" WHERE username = $1`, [username], ) - if (rowCount && rowCount < 1) { + if (rows.length === 0) { throw newUserNotFoundError() } return this.mapRow(rows[0]) } async findByEmail(email: string): Promise { - const { rowCount, rows } = await client.queryObject( + const { rows } = await client.queryObject( `SELECT * FROM "user" WHERE email = $1`, [email], ) - if (rowCount && rowCount < 1) { + if (rows.length === 0) { throw newUserNotFoundError() } return this.mapRow(rows[0]) } async findByRefreshTokenValue(refreshTokenValue: string): Promise { - const { rowCount, rows } = await client.queryObject( + const { rows } = await client.queryObject( `SELECT * FROM "user" WHERE refresh_token_value = $1`, [refreshTokenValue], ) - if (rowCount && rowCount < 1) { + if (rows.length === 0) { throw newUserNotFoundError() } return this.mapRow(rows[0]) } async findByResetPasswordToken(resetPasswordToken: string): Promise { - const { rowCount, rows } = await client.queryObject( + const { rows } = await client.queryObject( `SELECT * FROM "user" WHERE reset_password_token = $1`, [resetPasswordToken], ) - if (rowCount && rowCount < 1) { + if (rows.length === 0) { throw newUserNotFoundError() } return this.mapRow(rows[0]) @@ -73,22 +73,22 @@ class UserRepoImpl { async findByEmailConfirmationToken( emailConfirmationToken: string, ): Promise { - const { rowCount, rows } = await client.queryObject( + const { rows } = await client.queryObject( `SELECT * FROM "user" WHERE email_confirmation_token = $1`, [emailConfirmationToken], ) - if (rowCount && rowCount < 1) { + if (rows.length === 0) { throw newUserNotFoundError() } return this.mapRow(rows[0]) } async findByEmailUpdateToken(emailUpdateToken: string): Promise { - const { rowCount, rows } = await client.queryObject( + const { rows } = await client.queryObject( `SELECT * FROM "user" WHERE email_update_token = $1`, [emailUpdateToken], ) - if (rowCount && rowCount < 1) { + if (rows.length === 0) { throw newUserNotFoundError() } return this.mapRow(rows[0]) @@ -133,7 +133,7 @@ class UserRepoImpl { } async insert(data: InsertOptions): Promise { - const { rowCount, rows } = await client.queryObject( + const { rowCount } = await client.queryObject( `INSERT INTO "user" ( id, full_name, @@ -167,10 +167,10 @@ class UserRepoImpl { new Date().toISOString(), ], ) - if (rowCount && rowCount < 1) { + if (!rowCount || rowCount === 0) { throw newInternalServerError() } - return this.mapRow(rows[0]) + return await this.findById(data.id) } async update(data: UpdateOptions): Promise { @@ -180,7 +180,7 @@ class UserRepoImpl { } Object.assign(entity, data) entity.updateTime = new Date().toISOString() - const { rowCount, rows } = await client.queryObject( + const { rowCount } = await client.queryObject( `UPDATE "user" SET full_name = $1, @@ -223,10 +223,10 @@ class UserRepoImpl { entity.id, ], ) - if (rowCount && rowCount < 1) { + if (!rowCount || rowCount === 0) { throw newInternalServerError() } - return this.mapRow(rows[0]) + return await this.findById(data.id) } async delete(id: string): Promise { diff --git a/idp/src/user/router.ts b/idp/src/user/router.ts index daa51b21b..001457ad3 100644 --- a/idp/src/user/router.ts +++ b/idp/src/user/router.ts @@ -114,7 +114,7 @@ router.post( ), async (c) => { const body = c.req.valid('json') as UserUpdateEmailConfirmationOptions - c.json(await updateEmailConfirmation(body)) + return c.json(await updateEmailConfirmation(body)) }, ) diff --git a/idp/src/user/service.ts b/idp/src/user/service.ts index 63c6066ca..74159e86e 100644 --- a/idp/src/user/service.ts +++ b/idp/src/user/service.ts @@ -197,6 +197,7 @@ export async function updateFullName( ): Promise { let user = await userRepo.findById(id) user = await userRepo.update({ id: user.id, fullName: options.fullName }) + console.log('>>>>>>>>>>>>', user) await meilisearch.index(USER_SEARCH_INDEX).updateDocuments([ { id: user.id, From 83a4babd1d0e5141bd5989b0f97048e56737f83c Mon Sep 17 00:00:00 2001 From: Anass Bouassaba Date: Sat, 14 Dec 2024 18:22:00 +0100 Subject: [PATCH 6/9] chore(idp): remove unused NPM package --- idp/deno.json | 3 +-- idp/deno.lock | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/idp/deno.json b/idp/deno.json index e0e69f384..29083519a 100644 --- a/idp/deno.json +++ b/idp/deno.json @@ -22,8 +22,7 @@ "@types/mime-types": "npm:@types/mime-types@2.1.4", "@types/node": "npm:@types/node@22.7.7", "@types/nodemailer": "npm:@types/nodemailer@6.4.16", - "@types/uuid": "npm:@types/uuid@10.0.0", - "globals": "npm:globals@15.11.0" + "@types/uuid": "npm:@types/uuid@10.0.0" }, "lint": { "rules": { diff --git a/idp/deno.lock b/idp/deno.lock index 5a2569cc3..212dbfb6d 100644 --- a/idp/deno.lock +++ b/idp/deno.lock @@ -211,7 +211,6 @@ "npm:@types/uuid@10.0.0", "npm:camelize@1.0.1", "npm:dotenv@16.4.5", - "npm:globals@15.11.0", "npm:handlebars@4.7.8", "npm:hashids@2.3.0", "npm:hono@4.6.13", From 4c2491681c128f7230e9137ad1cffe921de2b6c2 Mon Sep 17 00:00:00 2001 From: Anass Bouassaba Date: Sat, 14 Dec 2024 18:23:38 +0100 Subject: [PATCH 7/9] wip(idp): remove console.log --- idp/src/user/service.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/idp/src/user/service.ts b/idp/src/user/service.ts index 74159e86e..63c6066ca 100644 --- a/idp/src/user/service.ts +++ b/idp/src/user/service.ts @@ -197,7 +197,6 @@ export async function updateFullName( ): Promise { let user = await userRepo.findById(id) user = await userRepo.update({ id: user.id, fullName: options.fullName }) - console.log('>>>>>>>>>>>>', user) await meilisearch.index(USER_SEARCH_INDEX).updateDocuments([ { id: user.id, From 60d65128bd5f0ab3e0a9b57c3d7e9ae360526bc6 Mon Sep 17 00:00:00 2001 From: Anass Bouassaba Date: Sat, 14 Dec 2024 18:25:53 +0100 Subject: [PATCH 8/9] fix(idp): exclude falsy values from user DTO --- idp/src/user/service.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/idp/src/user/service.ts b/idp/src/user/service.ts index 63c6066ca..dcbbd662f 100644 --- a/idp/src/user/service.ts +++ b/idp/src/user/service.ts @@ -416,7 +416,9 @@ export function mapEntity(entity: User): UserDTO { email: entity.email, username: entity.username, fullName: entity.fullName, - pendingEmail: entity.emailUpdateValue, + } + if (entity.emailUpdateValue) { + user.pendingEmail = entity.emailUpdateValue } if (entity.picture) { user.picture = { From 3cbe38d84a506af3975f95655cec380bd3a713f3 Mon Sep 17 00:00:00 2001 From: Anass Bouassaba Date: Sat, 14 Dec 2024 18:45:08 +0100 Subject: [PATCH 9/9] fix(idp): better validation error messages --- idp/src/infra/error/validation.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/idp/src/infra/error/validation.ts b/idp/src/infra/error/validation.ts index 1f06f2675..e86eb265c 100644 --- a/idp/src/infra/error/validation.ts +++ b/idp/src/infra/error/validation.ts @@ -17,10 +17,10 @@ export function parseValidationError(result: ZodError): ErrorData { let userMessage: string | undefined if (result.errors) { message = result.errors - .map((e) => `${e.message} for ${e.path.join('.')}.`) + .map((e) => e.message ? `${e.message} (${e.path.join('.')}).` : undefined) .join(' ') userMessage = result.errors - .map((e) => `${e.message} for ${e.path.join('.')}.`) + .map((e) => e.message ? `${e.message} (${e.path.join('.')}).` : undefined) .join(' ') } return newError({ @@ -49,25 +49,25 @@ export const password = z .string() .min( getConfig().password.minLength, - `Password must be at least ${getConfig().password.minLength} characters long.`, + `Password must be at least ${getConfig().password.minLength} characters long`, ) .regex( new RegExp(`(?=(.*[a-z]){${getConfig().password.minLowercase},})`), - `Password must contain at least ${getConfig().password.minLowercase} lowercase character(s).`, + `Password must contain at least ${getConfig().password.minLowercase} lowercase character(s)`, ) .regex( new RegExp(`(?=(.*[A-Z]){${getConfig().password.minUppercase},})`), - `Password must contain at least ${getConfig().password.minUppercase} uppercase character(s).`, + `Password must contain at least ${getConfig().password.minUppercase} uppercase character(s)`, ) .regex( new RegExp(`(?=(.*[0-9]){${getConfig().password.minNumbers},})`), - `Password must contain at least ${getConfig().password.minNumbers} number(s).`, + `Password must contain at least ${getConfig().password.minNumbers} number(s)`, ) .regex( new RegExp( `(?=(.*[!@#$%^&*()_+\\-=\\[\\]{};':"\\|,.<>\\/?]){${getConfig().password.minSymbols},})`, ), - `Password must contain at least ${getConfig().password.minSymbols} symbol(s).`, + `Password must contain at least ${getConfig().password.minSymbols} symbol(s)`, ) export const picture = z @@ -82,7 +82,7 @@ export const picture = z } catch { return false } - }, { message: 'Picture must be a valid Base64 string and <= 3MB.' }) + }, { message: 'Picture must be a valid Base64 string and <= 3MB' }) export const email = z.string().email().trim().max(255)