From ddbed41c253946c23a45b675c6506117b9482f3f Mon Sep 17 00:00:00 2001 From: Anders Rasmussen Date: Wed, 27 Sep 2023 14:12:06 +0200 Subject: [PATCH] #1 - Added i18n to backend --- package.json | 3 + pnpm-lock.yaml | 545 ++++++++++++++++++++- src/index.ts | 31 ++ src/locales/da.json | 27 + src/locales/en.json | 27 + src/modules/auth/__test__/login.test.ts | 19 +- src/modules/auth/__test__/register.test.ts | 50 +- src/modules/auth/auth.controller.ts | 6 +- src/modules/auth/auth.route.ts | 15 +- src/modules/auth/auth.schema.ts | 187 ++++--- src/modules/auth/auth.service.ts | 4 +- src/modules/auth/user.service.ts | 6 +- src/plugins/i18n.ts | 16 + src/plugins/index.ts | 7 +- tsconfig.json | 2 +- 15 files changed, 828 insertions(+), 117 deletions(-) create mode 100644 src/locales/da.json create mode 100644 src/locales/en.json create mode 100644 src/plugins/i18n.ts diff --git a/package.json b/package.json index c1852d9..7678dfc 100644 --- a/package.json +++ b/package.json @@ -26,9 +26,11 @@ "@fastify/swagger": "^8.10.0", "@fastify/swagger-ui": "^1.9.3", "@prisma/client": "^5.3.1", + "ajv-errors": "^3.0.0", "bcrypt": "^5.1.1", "dotenv": "^16.3.1", "fastify": "^4.23.2", + "fastify-i18n": "^1.1.1", "fastify-plugin": "^4.5.1", "fastify-zod": "^1.4.0", "zod": "^3.22.2" @@ -45,6 +47,7 @@ "eslint": "^8.49.0", "eslint-config-prettier": "^9.0.0", "jest": "^29.7.0", + "json-schema-to-ts": "^2.9.2", "pino-pretty": "^10.2.0", "prettier": "^3.0.3", "prisma": "^5.3.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3e8b6bf..561a704 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,6 +32,9 @@ dependencies: '@prisma/client': specifier: ^5.3.1 version: 5.3.1(prisma@5.3.1) + ajv-errors: + specifier: ^3.0.0 + version: 3.0.0(ajv@8.12.0) bcrypt: specifier: ^5.1.1 version: 5.1.1 @@ -41,6 +44,9 @@ dependencies: fastify: specifier: ^4.23.2 version: 4.23.2 + fastify-i18n: + specifier: ^1.1.1 + version: 1.1.1 fastify-plugin: specifier: ^4.5.1 version: 4.5.1 @@ -85,6 +91,9 @@ devDependencies: jest: specifier: ^29.7.0 version: 29.7.0(@types/node@20.6.2)(ts-node@10.9.1) + json-schema-to-ts: + specifier: ^2.9.2 + version: 2.9.2 pino-pretty: specifier: ^10.2.0 version: 10.2.0 @@ -414,6 +423,13 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/runtime@7.23.1: + resolution: {integrity: sha512-hC2v6p8ZSI/W0HUzh3V8C5g+NwSKzKPtJwSpTjwl0o297GP9+ZLQSkdvHz46CM3LqyoXxq+5G9komY+eSqSO0g==} + engines: {node: '>=6.9.0'} + dependencies: + regenerator-runtime: 0.14.0 + dev: true + /@babel/template@7.22.15: resolution: {integrity: sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==} engines: {node: '>=6.9.0'} @@ -1441,6 +1457,14 @@ packages: - supports-color dev: false + /ajv-errors@3.0.0(ajv@8.12.0): + resolution: {integrity: sha512-V3wD15YHfHz6y0KdhYFjyy9vWtEVALT9UrxfN3zqlI6dMioHnJrqOYfyPKol3oqrnCM9uwkcdCwkJ0WUcbLMTQ==} + peerDependencies: + ajv: ^8.0.1 + dependencies: + ajv: 8.12.0 + dev: false + /ajv-formats@2.1.1(ajv@8.12.0): resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} peerDependencies: @@ -1537,11 +1561,43 @@ packages: /argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + /array-buffer-byte-length@1.0.0: + resolution: {integrity: sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==} + dependencies: + call-bind: 1.0.2 + is-array-buffer: 3.0.2 + dev: false + /array-union@2.1.0: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} dev: true + /array.prototype.foreach@1.0.5: + resolution: {integrity: sha512-FSk2BdZDQVdxGeh63usPldJo5xtkdBp3iYBqEGlGnId5TV0xtrKOnz9kXzfFL5L/81EIuVkxtiYtJSE2IjKoPA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.1 + es-abstract: 1.22.2 + es-array-method-boxes-properly: 1.0.0 + get-intrinsic: 1.2.1 + is-string: 1.0.7 + dev: false + + /arraybuffer.prototype.slice@1.0.2: + resolution: {integrity: sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==} + engines: {node: '>= 0.4'} + dependencies: + array-buffer-byte-length: 1.0.0 + call-bind: 1.0.2 + define-properties: 1.2.1 + es-abstract: 1.22.2 + get-intrinsic: 1.2.1 + is-array-buffer: 3.0.2 + is-shared-array-buffer: 1.0.2 + dev: false + /asn1.js@5.4.1: resolution: {integrity: sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==} dependencies: @@ -1555,6 +1611,11 @@ packages: resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} engines: {node: '>=8.0.0'} + /available-typed-arrays@1.0.5: + resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==} + engines: {node: '>= 0.4'} + dev: false + /avvio@8.2.1: resolution: {integrity: sha512-TAlMYvOuwGyLK3PfBb5WKBXZmXz2fVCgv23d6zZFdle/q3gPjmxBaeuC0pY0Dzs5PWMSgfqqEZkrye19GlDTgw==} dependencies: @@ -1716,6 +1777,13 @@ packages: base64-js: 1.5.1 ieee754: 1.2.1 + /call-bind@1.0.2: + resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==} + dependencies: + function-bind: 1.1.1 + get-intrinsic: 1.2.1 + dev: false + /callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} @@ -1971,6 +2039,24 @@ packages: engines: {node: '>=0.10.0'} dev: true + /define-data-property@1.1.0: + resolution: {integrity: sha512-UzGwzcjyv3OtAvolTj1GoyNYzfFR+iqbGjcnBEENZVCpM4/Ng1yhGNvS3lR/xDS74Tb2wGG9WzNSNIOS9UVb2g==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.1 + gopd: 1.0.1 + has-property-descriptors: 1.0.0 + dev: false + + /define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.0 + has-property-descriptors: 1.0.0 + object-keys: 1.1.1 + dev: false + /delegates@1.0.0: resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} dev: false @@ -2080,6 +2166,73 @@ packages: is-arrayish: 0.2.1 dev: true + /es-abstract@1.22.2: + resolution: {integrity: sha512-YoxfFcDmhjOgWPWsV13+2RNjq1F6UQnfs+8TftwNqtzlmFzEXvlUwdrNrYeaizfjQzRMxkZ6ElWMOJIFKdVqwA==} + engines: {node: '>= 0.4'} + dependencies: + array-buffer-byte-length: 1.0.0 + arraybuffer.prototype.slice: 1.0.2 + available-typed-arrays: 1.0.5 + call-bind: 1.0.2 + es-set-tostringtag: 2.0.1 + es-to-primitive: 1.2.1 + function.prototype.name: 1.1.6 + get-intrinsic: 1.2.1 + get-symbol-description: 1.0.0 + globalthis: 1.0.3 + gopd: 1.0.1 + has: 1.0.3 + has-property-descriptors: 1.0.0 + has-proto: 1.0.1 + has-symbols: 1.0.3 + internal-slot: 1.0.5 + is-array-buffer: 3.0.2 + is-callable: 1.2.7 + is-negative-zero: 2.0.2 + is-regex: 1.1.4 + is-shared-array-buffer: 1.0.2 + is-string: 1.0.7 + is-typed-array: 1.1.12 + is-weakref: 1.0.2 + object-inspect: 1.12.3 + object-keys: 1.1.1 + object.assign: 4.1.4 + regexp.prototype.flags: 1.5.1 + safe-array-concat: 1.0.1 + safe-regex-test: 1.0.0 + string.prototype.trim: 1.2.8 + string.prototype.trimend: 1.0.7 + string.prototype.trimstart: 1.0.7 + typed-array-buffer: 1.0.0 + typed-array-byte-length: 1.0.0 + typed-array-byte-offset: 1.0.0 + typed-array-length: 1.0.4 + unbox-primitive: 1.0.2 + which-typed-array: 1.1.11 + dev: false + + /es-array-method-boxes-properly@1.0.0: + resolution: {integrity: sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==} + dev: false + + /es-set-tostringtag@2.0.1: + resolution: {integrity: sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.1 + has: 1.0.3 + has-tostringtag: 1.0.0 + dev: false + + /es-to-primitive@1.2.1: + resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==} + engines: {node: '>= 0.4'} + dependencies: + is-callable: 1.2.7 + is-date-object: 1.0.5 + is-symbol: 1.0.4 + dev: false + /escalade@3.1.1: resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} engines: {node: '>=6'} @@ -2331,6 +2484,13 @@ packages: reusify: 1.0.4 dev: false + /fastify-i18n@1.1.1: + resolution: {integrity: sha512-avkTM3SMv7CPGwVmR+OCe5QoJ16jiR8fPPLDvDFkYQiOFosYIgkyuenQ/fnfEiMVWfL2SbUc7Hdpt3ubtFQCxg==} + dependencies: + fastify-plugin: 4.5.1 + node-polyglot: 2.5.0 + dev: false + /fastify-plugin@4.5.1: resolution: {integrity: sha512-stRHYGeuqpEZTL1Ef0Ovr2ltazUT9g844X5z/zEBFLG8RYlpDiOCIG+ATvYEp+/zmc7sN29mcIMp8gvYplYPIQ==} dev: false @@ -2454,6 +2614,12 @@ packages: resolution: {integrity: sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==} dev: true + /for-each@0.3.3: + resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} + dependencies: + is-callable: 1.2.7 + dev: false + /forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} @@ -2479,7 +2645,20 @@ packages: /function-bind@1.1.1: resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} - dev: true + + /function.prototype.name@1.1.6: + resolution: {integrity: sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.1 + es-abstract: 1.22.2 + functions-have-names: 1.2.3 + dev: false + + /functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + dev: false /gauge@3.0.2: resolution: {integrity: sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==} @@ -2506,6 +2685,15 @@ packages: engines: {node: 6.* || 8.* || >= 10.*} dev: true + /get-intrinsic@1.2.1: + resolution: {integrity: sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==} + dependencies: + function-bind: 1.1.1 + has: 1.0.3 + has-proto: 1.0.1 + has-symbols: 1.0.3 + dev: false + /get-package-type@0.1.0: resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} engines: {node: '>=8.0.0'} @@ -2516,6 +2704,14 @@ packages: engines: {node: '>=10'} dev: true + /get-symbol-description@1.0.0: + resolution: {integrity: sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.1 + dev: false + /glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -2562,6 +2758,13 @@ packages: type-fest: 0.20.2 dev: true + /globalthis@1.0.3: + resolution: {integrity: sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==} + engines: {node: '>= 0.4'} + dependencies: + define-properties: 1.2.1 + dev: false + /globby@11.1.0: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} engines: {node: '>=10'} @@ -2574,6 +2777,12 @@ packages: slash: 3.0.0 dev: true + /gopd@1.0.1: + resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} + dependencies: + get-intrinsic: 1.2.1 + dev: false + /graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} dev: true @@ -2582,6 +2791,10 @@ packages: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} dev: true + /has-bigints@1.0.2: + resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} + dev: false + /has-flag@3.0.0: resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} engines: {node: '>=4'} @@ -2592,6 +2805,29 @@ packages: engines: {node: '>=8'} dev: true + /has-property-descriptors@1.0.0: + resolution: {integrity: sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==} + dependencies: + get-intrinsic: 1.2.1 + dev: false + + /has-proto@1.0.1: + resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==} + engines: {node: '>= 0.4'} + dev: false + + /has-symbols@1.0.3: + resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} + engines: {node: '>= 0.4'} + dev: false + + /has-tostringtag@1.0.0: + resolution: {integrity: sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==} + engines: {node: '>= 0.4'} + dependencies: + has-symbols: 1.0.3 + dev: false + /has-unicode@2.0.1: resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==} dev: false @@ -2601,7 +2837,6 @@ packages: engines: {node: '>= 0.4.0'} dependencies: function-bind: 1.1.1 - dev: true /header-case@2.0.4: resolution: {integrity: sha512-H/vuk5TEEVZwrR0lp2zed9OCo1uAILMlx0JEMgC26rzyJJ3N1v6XkwHHXJQdR2doSjcGPM6OKPYoJgf0plJ11Q==} @@ -2686,6 +2921,15 @@ packages: /inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + /internal-slot@1.0.5: + resolution: {integrity: sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.1 + has: 1.0.3 + side-channel: 1.0.4 + dev: false + /ioredis@5.3.2: resolution: {integrity: sha512-1DKMMzlIHM02eBBVOFQ1+AolGjs6+xEcM4PDL7NqOS6szq7H9jSaEkIUH6/a5Hl241LzW6JLSiAbNvTQjUupUA==} engines: {node: '>=12.22.0'} @@ -2708,10 +2952,24 @@ packages: engines: {node: '>= 0.10'} dev: false + /is-array-buffer@3.0.2: + resolution: {integrity: sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.1 + is-typed-array: 1.1.12 + dev: false + /is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} dev: true + /is-bigint@1.0.4: + resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==} + dependencies: + has-bigints: 1.0.2 + dev: false + /is-binary-path@2.1.0: resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} engines: {node: '>=8'} @@ -2719,12 +2977,32 @@ packages: binary-extensions: 2.2.0 dev: true + /is-boolean-object@1.1.2: + resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + has-tostringtag: 1.0.0 + dev: false + + /is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + dev: false + /is-core-module@2.13.0: resolution: {integrity: sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==} dependencies: has: 1.0.3 dev: true + /is-date-object@1.0.5: + resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: false + /is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -2746,6 +3024,18 @@ packages: is-extglob: 2.1.1 dev: true + /is-negative-zero@2.0.2: + resolution: {integrity: sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==} + engines: {node: '>= 0.4'} + dev: false + + /is-number-object@1.0.7: + resolution: {integrity: sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: false + /is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} @@ -2756,11 +3046,56 @@ packages: engines: {node: '>=8'} dev: true + /is-regex@1.1.4: + resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + has-tostringtag: 1.0.0 + dev: false + + /is-shared-array-buffer@1.0.2: + resolution: {integrity: sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==} + dependencies: + call-bind: 1.0.2 + dev: false + /is-stream@2.0.1: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} dev: true + /is-string@1.0.7: + resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: false + + /is-symbol@1.0.4: + resolution: {integrity: sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==} + engines: {node: '>= 0.4'} + dependencies: + has-symbols: 1.0.3 + dev: false + + /is-typed-array@1.1.12: + resolution: {integrity: sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==} + engines: {node: '>= 0.4'} + dependencies: + which-typed-array: 1.1.11 + dev: false + + /is-weakref@1.0.2: + resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} + dependencies: + call-bind: 1.0.2 + dev: false + + /isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + dev: false + /isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} dev: true @@ -3242,7 +3577,6 @@ packages: /js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - dev: true /js-yaml@3.14.1: resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} @@ -3283,6 +3617,15 @@ packages: - supports-color dev: false + /json-schema-to-ts@2.9.2: + resolution: {integrity: sha512-h9WqLkTVpBbiaPb5OmeUpz/FBLS/kvIJw4oRCPiEisIu2WjMh+aai0QIY2LoOhRFx5r92taGLcerIrzxKBAP6g==} + engines: {node: '>=16'} + dependencies: + '@babel/runtime': 7.23.1 + '@types/json-schema': 7.0.13 + ts-algebra: 1.2.2 + dev: true + /json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} dev: true @@ -3371,6 +3714,13 @@ packages: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} dev: true + /loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + dependencies: + js-tokens: 4.0.0 + dev: false + /lower-case@2.0.2: resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} dependencies: @@ -3542,6 +3892,16 @@ packages: resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} dev: true + /node-polyglot@2.5.0: + resolution: {integrity: sha512-zXVwHNhFsG3mls+LKHxoHF70GQOL3FTDT3jH7ldkb95kG76RdU7F/NbvxV7D2hNIL9VpWXW6y78Fz+3KZkatRg==} + dependencies: + array.prototype.foreach: 1.0.5 + has: 1.0.3 + object.entries: 1.1.7 + string.prototype.trim: 1.2.8 + warning: 4.0.3 + dev: false + /node-releases@2.0.13: resolution: {integrity: sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==} dev: true @@ -3580,6 +3940,34 @@ packages: engines: {node: '>=0.10.0'} dev: false + /object-inspect@1.12.3: + resolution: {integrity: sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==} + dev: false + + /object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + dev: false + + /object.assign@4.1.4: + resolution: {integrity: sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.1 + has-symbols: 1.0.3 + object-keys: 1.1.1 + dev: false + + /object.entries@1.1.7: + resolution: {integrity: sha512-jCBs/0plmPsOnrKAfFQXRG2NFjlhZgjjcBLSmTnEhU8U6vVTsVe8ANeQJCHTl3gSsI4J+0emOoCgoKlmQPMgmA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.1 + es-abstract: 1.22.2 + dev: false + /obliterator@2.0.4: resolution: {integrity: sha512-lgHwxlxV1qIg1Eap7LgIeoBWIMFibOjbrYPIPJZcI1mmGAI2m3lNYpK12Y+GBdPQ0U1hRwSord7GIaawz962qQ==} dev: false @@ -3897,6 +4285,19 @@ packages: redis-errors: 1.2.0 dev: false + /regenerator-runtime@0.14.0: + resolution: {integrity: sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==} + dev: true + + /regexp.prototype.flags@1.5.1: + resolution: {integrity: sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.1 + set-function-name: 2.0.1 + dev: false + /require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} @@ -3970,9 +4371,27 @@ packages: queue-microtask: 1.2.3 dev: true + /safe-array-concat@1.0.1: + resolution: {integrity: sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==} + engines: {node: '>=0.4'} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.1 + has-symbols: 1.0.3 + isarray: 2.0.5 + dev: false + /safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + /safe-regex-test@1.0.0: + resolution: {integrity: sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.1 + is-regex: 1.1.4 + dev: false + /safe-regex2@2.0.0: resolution: {integrity: sha512-PaUSFsUaNNuKwkBijoAPHAK6/eM6VirvyPWlZ7BAQy4D+hCvh4B6lIG+nPdhbFfIbP+gTGBcrdsOaUs0F+ZBOQ==} dependencies: @@ -4018,6 +4437,15 @@ packages: resolution: {integrity: sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==} dev: false + /set-function-name@2.0.1: + resolution: {integrity: sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.0 + dev: false + /setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} dev: false @@ -4034,6 +4462,14 @@ packages: engines: {node: '>=8'} dev: true + /side-channel@1.0.4: + resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.1 + object-inspect: 1.12.3 + dev: false + /signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} @@ -4127,6 +4563,31 @@ packages: is-fullwidth-code-point: 3.0.0 strip-ansi: 6.0.1 + /string.prototype.trim@1.2.8: + resolution: {integrity: sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.1 + es-abstract: 1.22.2 + dev: false + + /string.prototype.trimend@1.0.7: + resolution: {integrity: sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.1 + es-abstract: 1.22.2 + dev: false + + /string.prototype.trimstart@1.0.7: + resolution: {integrity: sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.1 + es-abstract: 1.22.2 + dev: false + /string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} dependencies: @@ -4255,6 +4716,10 @@ packages: hasBin: true dev: true + /ts-algebra@1.2.2: + resolution: {integrity: sha512-kloPhf1hq3JbCPOTYoOWDKxebWjNb2o/LKnNfkWhxVVisFFmMJPPdJeGoGmM+iRLyoXAR61e08Pb+vUXINg8aA==} + dev: true + /ts-api-utils@1.0.3(typescript@5.2.2): resolution: {integrity: sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==} engines: {node: '>=16.13.0'} @@ -4401,12 +4866,59 @@ packages: mime-types: 2.1.35 dev: false + /typed-array-buffer@1.0.0: + resolution: {integrity: sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.1 + is-typed-array: 1.1.12 + dev: false + + /typed-array-byte-length@1.0.0: + resolution: {integrity: sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + for-each: 0.3.3 + has-proto: 1.0.1 + is-typed-array: 1.1.12 + dev: false + + /typed-array-byte-offset@1.0.0: + resolution: {integrity: sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.5 + call-bind: 1.0.2 + for-each: 0.3.3 + has-proto: 1.0.1 + is-typed-array: 1.1.12 + dev: false + + /typed-array-length@1.0.4: + resolution: {integrity: sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==} + dependencies: + call-bind: 1.0.2 + for-each: 0.3.3 + is-typed-array: 1.1.12 + dev: false + /typescript@5.2.2: resolution: {integrity: sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==} engines: {node: '>=14.17'} hasBin: true dev: true + /unbox-primitive@1.0.2: + resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} + dependencies: + call-bind: 1.0.2 + has-bigints: 1.0.2 + has-symbols: 1.0.3 + which-boxed-primitive: 1.0.2 + dev: false + /update-browserslist-db@1.0.13(browserslist@4.21.11): resolution: {integrity: sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==} hasBin: true @@ -4467,6 +4979,12 @@ packages: makeerror: 1.0.12 dev: true + /warning@4.0.3: + resolution: {integrity: sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==} + dependencies: + loose-envify: 1.4.0 + dev: false + /webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} dev: false @@ -4478,6 +4996,27 @@ packages: webidl-conversions: 3.0.1 dev: false + /which-boxed-primitive@1.0.2: + resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} + dependencies: + is-bigint: 1.0.4 + is-boolean-object: 1.1.2 + is-number-object: 1.0.7 + is-string: 1.0.7 + is-symbol: 1.0.4 + dev: false + + /which-typed-array@1.1.11: + resolution: {integrity: sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.5 + call-bind: 1.0.2 + for-each: 0.3.3 + gopd: 1.0.1 + has-tostringtag: 1.0.0 + dev: false + /which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} diff --git a/src/index.ts b/src/index.ts index d65a8ad..de0c77a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -26,6 +26,37 @@ const getLoggerConfig = () => { export async function build() { const fastify = Fastify({ logger: getLoggerConfig(), + ajv: { + customOptions: { + allErrors: true, + }, + plugins: [ + require('ajv-errors') + ] + } + }); + + fastify.setErrorHandler(function (error, request, reply) { + if (error.validation) { + const errors: { [key: string]: Array } = {}; + error.validation.forEach(err => { + let pathName = err.instancePath.substring(1).toString(); + + if (pathName === '') { + pathName = "_"; + } + + if (!errors[pathName]) { + errors[pathName] = []; + } + + errors[pathName].push(request.i18n.t(err.message)); + }); + + return reply.status(400).send(errors); + } + + return reply.send(error); }); const startPlugins = performance.now(); diff --git a/src/locales/da.json b/src/locales/da.json new file mode 100644 index 0000000..ee445d0 --- /dev/null +++ b/src/locales/da.json @@ -0,0 +1,27 @@ +{ + "email": { + "required": "Email er påkrævet", + "format": "Email skal være af korrekt format", + "type": "Email skal være en tekst", + "inUse": "Email er allerede i brug" + }, + "name": { + "required": "Navn er påkrævet", + "type": "Navn skal være en tekst", + "minLength": "Du skal vælge et navn" + }, + "password": { + "required": "Password er påkrævet", + "type": "Password skal være en tekst", + "minLength": "Password skal minimum være 8 karakterer", + "incorrect": "password er forkert" + }, + "user": { + "notFound": "Brugeren blev ikke fundet", + "emailOrPasswordIncorrect": "Email og/eller Password er forkert" + }, + "refreshToken": { + "expired": "Refresh token er udløbet", + "used": "Refresh token er allerede blevet brugt" + } +} \ No newline at end of file diff --git a/src/locales/en.json b/src/locales/en.json new file mode 100644 index 0000000..055f798 --- /dev/null +++ b/src/locales/en.json @@ -0,0 +1,27 @@ +{ + "email": { + "required": "Email is required", + "format": "Email must be of correct format", + "type": "Email must be a string", + "inUse": "Email is already in use" + }, + "name": { + "required": "Name is required", + "type": "Name must be a string", + "minLength": "You must choose a name" + }, + "password": { + "required": "Password is required", + "type": "Password must be a string", + "minLength": "Password must be atleast 8 characters", + "incorrect": "password incorrect" + }, + "user": { + "notFound": "User not found", + "emailOrPasswordIncorrect": "Email and/or Password incorrect" + }, + "refreshToken": { + "expired": "Refresh token has reached absolute expiry", + "used": "Refresh token has already been used" + } +} \ No newline at end of file diff --git a/src/modules/auth/__test__/login.test.ts b/src/modules/auth/__test__/login.test.ts index eaac7fa..c7a056b 100644 --- a/src/modules/auth/__test__/login.test.ts +++ b/src/modules/auth/__test__/login.test.ts @@ -57,7 +57,7 @@ describe('POST /api/auth/login', () => { expect(response.statusCode).toBe(401); expect(response.json()).toMatchObject({ error: 'Unauthorized', - message: 'email and/or password incorrect', + message: 'Email and/or Password incorrect', statusCode: 401, }); }); @@ -75,7 +75,7 @@ describe('POST /api/auth/login', () => { expect(response.statusCode).toBe(401); expect(response.json()).toMatchObject({ error: 'Unauthorized', - message: 'email and/or password incorrect', + message: 'Email and/or Password incorrect', statusCode: 401, }); }); @@ -89,9 +89,10 @@ describe('POST /api/auth/login', () => { expect(response.statusCode).toBe(400); expect(response.json()).toMatchObject({ - error: 'Bad Request', - message: "body must have required property 'email'", - statusCode: 400, + _: [ + "Email is required", + "Password is required" + ] }); }); @@ -106,9 +107,7 @@ describe('POST /api/auth/login', () => { expect(response.statusCode).toBe(400); expect(response.json()).toMatchObject({ - error: 'Bad Request', - message: "body must have required property 'email'", - statusCode: 400, + _: ["Email is required"] }); }); @@ -123,9 +122,7 @@ describe('POST /api/auth/login', () => { expect(response.statusCode).toBe(400); expect(response.json()).toMatchObject({ - error: 'Bad Request', - message: "body must have required property 'password'", - statusCode: 400, + _: ["Password is required"] }); }); }); diff --git a/src/modules/auth/__test__/register.test.ts b/src/modules/auth/__test__/register.test.ts index f524460..59d2a0e 100644 --- a/src/modules/auth/__test__/register.test.ts +++ b/src/modules/auth/__test__/register.test.ts @@ -68,9 +68,7 @@ describe('POST /api/auth/register', () => { expect(response.statusCode).toBe(400); expect(response.json()).toMatchObject({ - error: 'Bad Request', - message: 'body/email must match format "email"', - statusCode: 400, + email: ['Email must be of correct format'] }); }); @@ -86,9 +84,7 @@ describe('POST /api/auth/register', () => { expect(response.statusCode).toBe(400); expect(response.json()).toMatchObject({ - error: 'Bad Request', - message: "body must have required property 'email'", - statusCode: 400, + _: ['Email is required'] }); }); @@ -105,9 +101,7 @@ describe('POST /api/auth/register', () => { expect(response.statusCode).toBe(400); expect(response.json()).toMatchObject({ - error: 'Bad Request', - message: 'body/email must NOT have fewer than 1 characters', - statusCode: 400, + email: ['Email must be of correct format'] }); }); @@ -123,9 +117,7 @@ describe('POST /api/auth/register', () => { expect(response.statusCode).toBe(400); expect(response.json()).toMatchObject({ - error: 'Bad Request', - message: "body must have required property 'password'", - statusCode: 400, + _: ["Password is required"] }); }); @@ -142,9 +134,7 @@ describe('POST /api/auth/register', () => { expect(response.statusCode).toBe(400); expect(response.json()).toMatchObject({ - error: 'Bad Request', - message: 'body/password must NOT have fewer than 8 characters', - statusCode: 400, + password: ['Password must be atleast 8 characters'] }); }); @@ -161,9 +151,7 @@ describe('POST /api/auth/register', () => { expect(response.statusCode).toBe(400); expect(response.json()).toMatchObject({ - error: 'Bad Request', - message: 'body/password must NOT have fewer than 8 characters', - statusCode: 400, + password: ['Password must be atleast 8 characters'] }); }); @@ -179,9 +167,7 @@ describe('POST /api/auth/register', () => { expect(response.statusCode).toBe(400); expect(response.json()).toMatchObject({ - error: 'Bad Request', - message: "body must have required property 'name'", - statusCode: 400, + _: ["Name is required"] }); }); @@ -198,9 +184,25 @@ describe('POST /api/auth/register', () => { expect(response.statusCode).toBe(400); expect(response.json()).toMatchObject({ - error: 'Bad Request', - message: 'body/name must NOT have fewer than 1 characters', - statusCode: 400, + name: ['You must choose a name'] + }); + }); + + it('should return status 400, when name, email and password is not provided', async () => { + const response = await global.fastify.inject({ + method: 'POST', + url: '/api/auth/register', + payload: { + }, + }); + + expect(response.statusCode).toBe(400); + expect(response.json()).toMatchObject({ + _: [ + 'Email is required', + 'Password is required', + 'Name is required' + ] }); }); }); diff --git a/src/modules/auth/auth.controller.ts b/src/modules/auth/auth.controller.ts index cc41ed6..a6d4e9d 100644 --- a/src/modules/auth/auth.controller.ts +++ b/src/modules/auth/auth.controller.ts @@ -28,7 +28,7 @@ export default class AuthController { return reply.code(201).send(user); } catch (e) { if (e instanceof Error) { - return reply.badRequest(e.message); + return reply.badRequest(request.i18n.t(e.message)); } /* istanbul ignore next */ @@ -46,7 +46,7 @@ export default class AuthController { const user = await this.userService.getUserByEmail(request.body.email); if (!this.authService.verifyPassword(user.password, request.body.password)) { - throw new Error('password incorrect'); + throw new Error(request.i18n.t('password.incorrect')); } const { refreshToken, refreshTokenPayload, accessToken } = @@ -65,7 +65,7 @@ export default class AuthController { accessToken: accessToken, }); } catch (e) { - return reply.unauthorized('email and/or password incorrect'); + return reply.unauthorized(request.i18n.t('user.emailOrPasswordIncorrect')); } } diff --git a/src/modules/auth/auth.route.ts b/src/modules/auth/auth.route.ts index dabafd6..5c7267b 100644 --- a/src/modules/auth/auth.route.ts +++ b/src/modules/auth/auth.route.ts @@ -1,6 +1,5 @@ import { FastifyInstance } from 'fastify'; import AuthController from './auth.controller'; -import { $ref } from './auth.schema'; import AuthService from './auth.service'; import UserService from './user.service'; @@ -12,9 +11,9 @@ export default async (fastify: FastifyInstance) => { { schema: { tags: ['Auth'], - body: $ref('createUserSchema'), + body: { $ref: 'createUserSchema' }, response: { - 201: $ref('createUserResponseSchema'), + 201: { $ref: 'createUserResponseSchema' }, }, }, }, @@ -26,9 +25,9 @@ export default async (fastify: FastifyInstance) => { { schema: { tags: ['Auth'], - body: $ref('loginSchema'), + body: { $ref: 'loginSchema' }, response: { - 200: $ref('loginResponseSchema'), + 200: { $ref: 'loginResponseSchema' }, }, }, }, @@ -41,7 +40,7 @@ export default async (fastify: FastifyInstance) => { schema: { tags: ['Auth'], response: { - 200: $ref('refreshResponseSchema'), + 200: { $ref: 'refreshResponseSchema' }, }, description: 'The `refreshToken` cookie is required', }, @@ -55,7 +54,7 @@ export default async (fastify: FastifyInstance) => { schema: { tags: ['Auth'], response: { - 200: $ref('logoutResponseSchema'), + 200: { $ref: 'logoutResponseSchema' }, }, }, onRequest: [fastify.authenticate], @@ -72,7 +71,7 @@ export default async (fastify: FastifyInstance) => { }, tags: ['Auth'], response: { - 200: $ref('userResponseSchema'), + 200: { $ref: 'userResponseSchema' }, }, }, onRequest: [fastify.authenticate], diff --git a/src/modules/auth/auth.schema.ts b/src/modules/auth/auth.schema.ts index 65c9448..dba4ded 100644 --- a/src/modules/auth/auth.schema.ts +++ b/src/modules/auth/auth.schema.ts @@ -1,70 +1,135 @@ -import { z } from 'zod'; -import { buildJsonSchemas } from 'fastify-zod'; +import { FromSchema } from "json-schema-to-ts"; -const userCore = { - name: z.string().min(1), - email: z - .string({ - required_error: 'Email is required', - invalid_type_error: 'Email must be a string', - }) - .min(1) - .email(), -}; - -const createUserSchema = z.object({ - ...userCore, - password: z - .string({ - required_error: 'Password is required', - invalid_type_error: 'Password must be a string', - }) - .min(8), -}); - -const createUserResponseSchema = z.object({ - ...userCore, -}); +const createUserSchema = { + $id: 'createUserSchema', + type: 'object', + properties: { + name: { + type: "string", + minLength: 1, + errorMessage: { + type: 'name.type', + minLength: 'name.minLength' + }, + }, + email: { + type: "string", + format: 'email', + errorMessage: { + type: 'email.type', + format: 'email.format', + }, + }, + password: { + type: 'string', + minLength: 8, + errorMessage: { + type: 'password.type', + minLength: 'password.minLength', + } + } + }, + required: ['email', 'password', 'name'], + errorMessage: { + required: { + email: "email.required", + password: 'password.required', + name: 'name.required', + } + } +} as const; -const loginSchema = z.object({ - email: userCore.email, - password: z - .string({ - required_error: 'Password is required', - invalid_type_error: 'Password must be a string', - }) - .min(1), -}); +const createUserResponseSchema = { + $id: 'createUserResponseSchema', + type: 'object', + properties: { + name: { + type: "string", + }, + email: { + type: "string", + format: 'email', + } + } +} -const loginResponseSchema = z.object({ - accessToken: z.string(), -}); +const loginSchema = { + $id: 'loginSchema', + type: 'object', + properties: { + email: { + type: "string", + format: 'email', + errorMessage: { + type: 'email.type', + format: 'email.format', + }, + }, + password: { + type: 'string', + errorMessage: { + type: 'password.type', + } + } + }, + required: ['email', 'password'], + errorMessage: { + required: { + email: "email.required", + password: 'password.required', + } + } +} as const; -const refreshResponseSchema = z.object({ - accessToken: z.string(), -}); +const loginResponseSchema = { + $id: 'loginResponseSchema', + type: 'object', + properties: { + accessToken: { + type: 'string' + } + } +}; -const logoutResponseSchema = z.object({}); +const refreshResponseSchema = { + $id: 'refreshResponseSchema', + type: 'object', + properties: { + accessToken: { + type: 'string' + } + } +}; -const userResponseSchema = z.object({ - ...userCore, -}); +const logoutResponseSchema = { + $id: 'logoutResponseSchema', + type: 'object', + properties: {} +}; -export type CreateUserInput = z.infer; +const userResponseSchema = { + $id: 'userResponseSchema', + type: 'object', + properties: { + name: { + type: "string", + }, + email: { + type: "string", + format: 'email', + }, + } +}; -export type LoginInput = z.infer; +export type LoginInput = FromSchema +export type CreateUserInput = FromSchema -export const { schemas: authSchemas, $ref } = buildJsonSchemas( - { - createUserSchema, - createUserResponseSchema, - loginSchema, - loginResponseSchema, - refreshResponseSchema, - logoutResponseSchema, - userResponseSchema, - }, - { - $id: 'authSchema', - }, -); +export const authSchemas = [ + createUserSchema, + createUserResponseSchema, + loginSchema, + loginResponseSchema, + refreshResponseSchema, + logoutResponseSchema, + userResponseSchema +] diff --git a/src/modules/auth/auth.service.ts b/src/modules/auth/auth.service.ts index 0097a69..5a13caa 100644 --- a/src/modules/auth/auth.service.ts +++ b/src/modules/auth/auth.service.ts @@ -116,7 +116,7 @@ export default class AuthService { }, }); - throw new Error('Refresh token has reached absolute expiry'); + throw new Error('refreshToken.expired'); } const userSession = await prisma.userSession.findFirst({ @@ -133,7 +133,7 @@ export default class AuthService { }, }); - throw new Error('Refresh token has already been used'); + throw new Error('refreshToken.used'); } const { refreshToken, refreshTokenPayload } = diff --git a/src/modules/auth/user.service.ts b/src/modules/auth/user.service.ts index babde1f..cf2472c 100644 --- a/src/modules/auth/user.service.ts +++ b/src/modules/auth/user.service.ts @@ -6,7 +6,7 @@ import { CreateUserInput } from './auth.schema'; export default class UserService { public async createUser(input: CreateUserInput): Promise { if (await this.isEmailInUse(input.email)) { - throw new Error('Email is already in use'); + throw new Error('email.inUse'); } return await prisma.user.create({ @@ -34,7 +34,7 @@ export default class UserService { }); if (!user) { - throw Error('User not found'); + throw Error('user.notFound'); } return user; @@ -46,7 +46,7 @@ export default class UserService { }); if (!user) { - throw Error('User not found'); + throw Error('user.notFound'); } return user; diff --git a/src/plugins/i18n.ts b/src/plugins/i18n.ts new file mode 100644 index 0000000..055b059 --- /dev/null +++ b/src/plugins/i18n.ts @@ -0,0 +1,16 @@ +import { FastifyInstance } from 'fastify'; + +import fastifyPlugin from 'fastify-plugin'; +import i18n from 'fastify-i18n'; +import en from '../locales/en.json'; +import da from '../locales/da.json'; + +export default fastifyPlugin(async (fastify: FastifyInstance) => { + fastify.register(i18n, { + fallbackLocale: 'en', + messages: { + en: en, + da: da, + } + }); +}); diff --git a/src/plugins/index.ts b/src/plugins/index.ts index 65d3897..668cd0e 100644 --- a/src/plugins/index.ts +++ b/src/plugins/index.ts @@ -9,9 +9,14 @@ import swagger from './swagger'; import cookie from './cookie'; import cors from './cors'; import jwt from './jwt'; +import i18n from './i18n'; export default fastifyPlugin(async (fastify: FastifyInstance) => { - await Promise.all([fastify.register(config), fastify.register(sensible)]); + await Promise.all([ + fastify.register(config), + fastify.register(sensible), + fastify.register(i18n) + ]); await Promise.all([ fastify.register(prisma), diff --git a/tsconfig.json b/tsconfig.json index ef6a749..d174a9e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -35,7 +35,7 @@ // "types": [], /* Specify type package names to be included without being referenced in a source file. */ // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ - // "resolveJsonModule": true, /* Enable importing .json files. */ + "resolveJsonModule": true, /* Enable importing .json files. */ // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ /* JavaScript Support */