diff --git a/package.json b/package.json index c4fd78e..0977944 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "devDependencies": { "@biomejs/biome": "1.9.4", "@cloudflare/vitest-pool-workers": "^0.5.41", - "@cloudflare/workers-types": "^4.20241218.0", + "@cloudflare/workers-types": "^4.20250204.0", "@commitlint/cli": "^19.6.1", "@commitlint/config-conventional": "^19.6.0", "@types/bcryptjs": "^2.4.6", @@ -29,6 +29,6 @@ "drizzle-kit": "^0.28.1", "husky": "^9.1.7", "vitest": "^2.1.9", - "wrangler": "^3.98.0" + "wrangler": "^3.108.1" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2181eb6..319a406 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13,7 +13,7 @@ importers: version: 2.4.3 drizzle-orm: specifier: ^0.36.4 - version: 0.36.4(@cloudflare/workers-types@4.20241218.0) + version: 0.36.4(@cloudflare/workers-types@4.20250204.0) hono: specifier: ^4.7.0 version: 4.7.0 @@ -26,10 +26,10 @@ importers: version: 1.9.4 '@cloudflare/vitest-pool-workers': specifier: ^0.5.41 - version: 0.5.41(@cloudflare/workers-types@4.20241218.0)(@vitest/runner@2.1.9)(@vitest/snapshot@2.1.9)(vitest@2.1.9(@types/node@22.10.2)) + version: 0.5.41(@cloudflare/workers-types@4.20250204.0)(@vitest/runner@2.1.9)(@vitest/snapshot@2.1.9)(vitest@2.1.9(@types/node@22.10.2)) '@cloudflare/workers-types': - specifier: ^4.20241218.0 - version: 4.20241218.0 + specifier: ^4.20250204.0 + version: 4.20250204.0 '@commitlint/cli': specifier: ^19.6.1 version: 19.6.1(@types/node@22.10.2)(typescript@5.7.2) @@ -52,8 +52,8 @@ importers: specifier: ^2.1.9 version: 2.1.9(@types/node@22.10.2) wrangler: - specifier: ^3.98.0 - version: 3.98.0(@cloudflare/workers-types@4.20241218.0) + specifier: ^3.108.1 + version: 3.108.1(@cloudflare/workers-types@4.20250204.0) packages: @@ -129,22 +129,16 @@ packages: '@vitest/snapshot': 2.0.x - 2.1.x vitest: 2.0.x - 2.1.x - '@cloudflare/workerd-darwin-64@1.20241205.0': - resolution: {integrity: sha512-TArEZkSZkHJyEwnlWWkSpCI99cF6lJ14OVeEoI9Um/+cD9CKZLM9vCmsLeKglKheJ0KcdCnkA+DbeD15t3VaWg==} - engines: {node: '>=16'} - cpu: [x64] - os: [darwin] - '@cloudflare/workerd-darwin-64@1.20241230.0': resolution: {integrity: sha512-BZHLg4bbhNQoaY1Uan81O3FV/zcmWueC55juhnaI7NAobiQth9RppadPNpxNAmS9fK2mR5z8xrwMQSQrHmztyQ==} engines: {node: '>=16'} cpu: [x64] os: [darwin] - '@cloudflare/workerd-darwin-arm64@1.20241205.0': - resolution: {integrity: sha512-u5eqKa9QRdA8MugfgCoD+ADDjY6EpKbv3hSYJETmmUh17l7WXjWBzv4pUvOKIX67C0UzMUy4jZYwC53MymhX3w==} + '@cloudflare/workerd-darwin-64@1.20250204.0': + resolution: {integrity: sha512-HpsgbWEfvdcwuZ8WAZhi1TlSCyyHC3tbghpKsOqGDaQNltyAFAWqa278TPNfcitYf/FmV4961v3eqUE+RFdHNQ==} engines: {node: '>=16'} - cpu: [arm64] + cpu: [x64] os: [darwin] '@cloudflare/workerd-darwin-arm64@1.20241230.0': @@ -153,11 +147,11 @@ packages: cpu: [arm64] os: [darwin] - '@cloudflare/workerd-linux-64@1.20241205.0': - resolution: {integrity: sha512-OYA7S5zpumMamWEW+IhhBU6YojIEocyE5X/YFPiTOCrDE3dsfr9t6oqNE7hxGm1VAAu+Irtl+a/5LwmBOU681w==} + '@cloudflare/workerd-darwin-arm64@1.20250204.0': + resolution: {integrity: sha512-AJ8Tk7KMJqePlch3SH8oL41ROtsrb07hKRHD6M+FvGC3tLtf26rpteAAMNYKMDYKzFNFUIKZNijYDFZjBFndXQ==} engines: {node: '>=16'} - cpu: [x64] - os: [linux] + cpu: [arm64] + os: [darwin] '@cloudflare/workerd-linux-64@1.20241230.0': resolution: {integrity: sha512-Y3mHcW0KghOmWdNZyHYpEOG4Ba/ga8tht5vj1a+WXfagEjMO8Y98XhZUlCaYa9yB7Wh5jVcK5LM2jlO/BLgqpA==} @@ -165,10 +159,10 @@ packages: cpu: [x64] os: [linux] - '@cloudflare/workerd-linux-arm64@1.20241205.0': - resolution: {integrity: sha512-qAzecONjFJGIAVJZKExQ5dlbic0f3d4A+GdKa+H6SoUJtPaWiE3K6WuePo4JOT7W3/Zfh25McmX+MmpMUUcM5Q==} + '@cloudflare/workerd-linux-64@1.20250204.0': + resolution: {integrity: sha512-RIUfUSnDC8h73zAa+u1K2Frc7nc+eeQoBBP7SaqsRe6JdX8jfIv/GtWjQWCoj8xQFgLvhpJKZ4sTTTV+AilQbw==} engines: {node: '>=16'} - cpu: [arm64] + cpu: [x64] os: [linux] '@cloudflare/workerd-linux-arm64@1.20241230.0': @@ -177,11 +171,11 @@ packages: cpu: [arm64] os: [linux] - '@cloudflare/workerd-windows-64@1.20241205.0': - resolution: {integrity: sha512-BEab+HiUgCdl6GXAT7EI2yaRtDPiRJlB94XLvRvXi1ZcmQqsrq6awGo6apctFo4WUL29V7c09LxmN4HQ3X2Tvg==} + '@cloudflare/workerd-linux-arm64@1.20250204.0': + resolution: {integrity: sha512-8Ql8jDjoIgr2J7oBD01kd9kduUz60njofrBpAOkjCPed15He8e8XHkYaYow3g0xpae4S2ryrPOeoD3M64sRxeg==} engines: {node: '>=16'} - cpu: [x64] - os: [win32] + cpu: [arm64] + os: [linux] '@cloudflare/workerd-windows-64@1.20241230.0': resolution: {integrity: sha512-y5SPIk9iOb2gz+yWtHxoeMnjPnkYQswiCJ480oHC6zexnJLlKTpcmBCjDH1nWCT4pQi8F25gaH8thgElf4NvXQ==} @@ -189,8 +183,14 @@ packages: cpu: [x64] os: [win32] - '@cloudflare/workers-types@4.20241218.0': - resolution: {integrity: sha512-Y0brjmJHcAZBXOPI7lU5hbiXglQWniA1kQjot2ata+HFimyjPPcz+4QWBRrmWcMPo0OadR2Vmac7WStDLpvz0w==} + '@cloudflare/workerd-windows-64@1.20250204.0': + resolution: {integrity: sha512-RpDJO3+to+e17X3EWfRCagboZYwBz2fowc+jL53+fd7uD19v3F59H48lw2BDpHJMRyhg6ouWcpM94OhsHv8ecA==} + engines: {node: '>=16'} + cpu: [x64] + os: [win32] + + '@cloudflare/workers-types@4.20250204.0': + resolution: {integrity: sha512-mWoQbYaP+nYztx9I7q9sgaiNlT54Cypszz0RfzMxYnT5W3NXDuwGcjGB+5B5H5VB8tEC2dYnBRpa70lX94ueaQ==} '@commitlint/cli@19.6.1': resolution: {integrity: sha512-8hcyA6ZoHwWXC76BoC8qVOSr8xHy00LZhZpauiD0iO0VYbVhMnED0da85lTfIULxl7Lj4c6vZgF0Wu/ed1+jlQ==} @@ -268,6 +268,9 @@ packages: '@drizzle-team/brocli@0.10.2': resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==} + '@emnapi/runtime@1.3.1': + resolution: {integrity: sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==} + '@esbuild-kit/core-utils@3.3.2': resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==} deprecated: 'Merged into tsx: https://tsx.is' @@ -830,6 +833,111 @@ packages: resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==} engines: {node: '>=14'} + '@img/sharp-darwin-arm64@0.33.5': + resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.33.5': + resolution: {integrity: sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.0.4': + resolution: {integrity: sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.0.4': + resolution: {integrity: sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.0.4': + resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linux-arm@1.0.5': + resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==} + cpu: [arm] + os: [linux] + + '@img/sharp-libvips-linux-s390x@1.0.4': + resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==} + cpu: [s390x] + os: [linux] + + '@img/sharp-libvips-linux-x64@1.0.4': + resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==} + cpu: [x64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-arm64@1.0.4': + resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-x64@1.0.4': + resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==} + cpu: [x64] + os: [linux] + + '@img/sharp-linux-arm64@0.33.5': + resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linux-arm@0.33.5': + resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + + '@img/sharp-linux-s390x@0.33.5': + resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + + '@img/sharp-linux-x64@0.33.5': + resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-linuxmusl-arm64@0.33.5': + resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linuxmusl-x64@0.33.5': + resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-wasm32@0.33.5': + resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-ia32@0.33.5': + resolution: {integrity: sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.33.5': + resolution: {integrity: sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + '@jridgewell/resolve-uri@3.1.2': resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} @@ -983,6 +1091,10 @@ packages: resolution: {integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==} hasBin: true + acorn-walk@8.3.2: + resolution: {integrity: sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==} + engines: {node: '>=0.4.0'} + acorn-walk@8.3.4: resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==} engines: {node: '>=0.4.0'} @@ -1069,6 +1181,13 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + color-string@1.9.1: + resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + + color@4.2.3: + resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} + engines: {node: '>=12.5.0'} + compare-func@2.0.0: resolution: {integrity: sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==} @@ -1088,6 +1207,10 @@ packages: engines: {node: '>=16'} hasBin: true + cookie@0.5.0: + resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} + engines: {node: '>= 0.6'} + cookie@0.7.2: resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} engines: {node: '>= 0.6'} @@ -1135,6 +1258,10 @@ packages: defu@6.1.4: resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + detect-libc@2.0.3: + resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} + engines: {node: '>=8'} + devalue@4.3.3: resolution: {integrity: sha512-UH8EL6H2ifcY8TbD2QsxwCC/pr5xSwPvv85LrLXVihmHVC3T3YqTCIwnR5ak0yO1KYqlxrPVOA/JVZJYPy2ATg==} @@ -1369,6 +1496,9 @@ packages: is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + is-arrayish@0.3.2: + resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} + is-core-module@2.16.0: resolution: {integrity: sha512-urTSINYfAYgcbLb0yDQ6egFm6h3Mo1DcF9EkyXSRjjzdHbsulg01qhwWuXdOoUBuTkbQ80KDboXa0vFJ+BDH+g==} engines: {node: '>= 0.4'} @@ -1464,13 +1594,13 @@ packages: engines: {node: '>=10.0.0'} hasBin: true - miniflare@3.20241205.0: - resolution: {integrity: sha512-Z0cTtIf6ZrcAJ3SrOI9EUM3s4dkGhNeU6Ubl8sroYhsPVD+rtz3m5+p6McHFWCkcMff1o60X5XEKVTmkz0gbpA==} + miniflare@3.20241230.0: + resolution: {integrity: sha512-ZtWNoNAIj5Q0Vb3B4SPEKr7DDmVG8a0Stsp/AuRkYXoJniA5hsbKjFNIGhTXGMIHVP5bvDrKJWt/POIDGfpiKg==} engines: {node: '>=16.13'} hasBin: true - miniflare@3.20241230.0: - resolution: {integrity: sha512-ZtWNoNAIj5Q0Vb3B4SPEKr7DDmVG8a0Stsp/AuRkYXoJniA5hsbKjFNIGhTXGMIHVP5bvDrKJWt/POIDGfpiKg==} + miniflare@3.20250204.0: + resolution: {integrity: sha512-f7tezEkOvVRVHIVul2EbTyKvWJCXpTDRAOxTxtD4N92+YI8PC2P8AvO4Z30vlN61r5Pje33fTBG8G1fEwSZIqQ==} engines: {node: '>=16.13'} hasBin: true @@ -1600,9 +1730,21 @@ packages: engines: {node: '>=10'} hasBin: true + semver@7.7.1: + resolution: {integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==} + engines: {node: '>=10'} + hasBin: true + + sharp@0.33.5: + resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + simple-swizzle@0.2.2: + resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -1694,12 +1836,12 @@ packages: resolution: {integrity: sha512-zICwjrDrcrUE0pyyJc1I2QzBkLM8FINsgOrt6WjA+BgajVq9Nxu2PbFFXUrAggLfDXlZGZBVZYw7WNV5KiBiBA==} engines: {node: '>=14.0'} - unenv-nightly@2.0.0-20241204-140205-a5d5190: - resolution: {integrity: sha512-jpmAytLeiiW01pl5bhVn9wYJ4vtiLdhGe10oXlJBuQEX8mxjxO8BlEXGHU4vr4yEikjFP1wsomTHt/CLU8kUwg==} - unenv-nightly@2.0.0-20241218-183400-5d6aec3: resolution: {integrity: sha512-7Xpi29CJRbOV1/IrC03DawMJ0hloklDLq/cigSe+J2jkcC+iDres2Cy0r4ltj5f0x7DqsaGaB4/dLuCPPFZnZA==} + unenv@2.0.0-rc.1: + resolution: {integrity: sha512-PU5fb40H8X149s117aB4ytbORcCvlASdtF97tfls4BPIyj4PeVxvpSuy1jAptqYHqB0vb2w2sHvzM0XWcp2OKg==} + unicorn-magic@0.1.0: resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==} engines: {node: '>=18'} @@ -1770,13 +1912,13 @@ packages: engines: {node: '>=8'} hasBin: true - workerd@1.20241205.0: - resolution: {integrity: sha512-vso/2n0c5SdBDWiD+Sx5gM7unA6SiZXRVUHDqH1euoP/9mFVHZF8icoYsNLB87b/TX8zNgpae+I5N/xFpd9v0g==} + workerd@1.20241230.0: + resolution: {integrity: sha512-EgixXP0JGXGq6J9lz17TKIZtfNDUvJNG+cl9paPMfZuYWT920fFpBx+K04YmnbQRLnglsivF1GT9pxh1yrlWhg==} engines: {node: '>=16'} hasBin: true - workerd@1.20241230.0: - resolution: {integrity: sha512-EgixXP0JGXGq6J9lz17TKIZtfNDUvJNG+cl9paPMfZuYWT920fFpBx+K04YmnbQRLnglsivF1GT9pxh1yrlWhg==} + workerd@1.20250204.0: + resolution: {integrity: sha512-zcKufjVFsQMiD3/acg1Ix00HIMCkXCrDxQXYRDn/1AIz3QQGkmbVDwcUk1Ki2jBUoXmBCMsJdycRucgMVEypWg==} engines: {node: '>=16'} hasBin: true @@ -1791,12 +1933,12 @@ packages: '@cloudflare/workers-types': optional: true - wrangler@3.98.0: - resolution: {integrity: sha512-s3R2Jdai+sIAQ1Fd+WzEK5fVxYHxAN7qbjYPXGx75dxM9/O2p+CT666PYLROGIk4sfAeLU4eVp9iqfVDuiQESw==} + wrangler@3.108.1: + resolution: {integrity: sha512-SuiMv/ys52Cu7r6CVRXJT/GvneXHoB0ef5nGBWghSWyHxUSIm4KavGO6F/hTphn+WmSpHYQt3xNl5hdxk6rJlA==} engines: {node: '>=16.17.0'} hasBin: true peerDependencies: - '@cloudflare/workers-types': ^4.20241205.0 + '@cloudflare/workers-types': ^4.20250204.0 peerDependenciesMeta: '@cloudflare/workers-types': optional: true @@ -1836,9 +1978,15 @@ packages: resolution: {integrity: sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==} engines: {node: '>=12.20'} + youch@3.2.3: + resolution: {integrity: sha512-ZBcWz/uzZaQVdCvfV4uk616Bbpf2ee+F/AvuKDR5EwX/Y4v06xWdtMluqTD7+KlZdM93lLm9gMZYo0sKBS0pgw==} + youch@3.3.4: resolution: {integrity: sha512-UeVBXie8cA35DS6+nBkls68xaBBXCye0CNznrhszZjTbRVnJKQuNsyLKBTTL4ln1o1rh2PKtv35twV7irj5SEg==} + zod@3.22.3: + resolution: {integrity: sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==} + zod@3.24.1: resolution: {integrity: sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==} @@ -1891,7 +2039,7 @@ snapshots: dependencies: mime: 3.0.0 - '@cloudflare/vitest-pool-workers@0.5.41(@cloudflare/workers-types@4.20241218.0)(@vitest/runner@2.1.9)(@vitest/snapshot@2.1.9)(vitest@2.1.9(@types/node@22.10.2))': + '@cloudflare/vitest-pool-workers@0.5.41(@cloudflare/workers-types@4.20250204.0)(@vitest/runner@2.1.9)(@vitest/snapshot@2.1.9)(vitest@2.1.9(@types/node@22.10.2))': dependencies: '@vitest/runner': 2.1.9 '@vitest/snapshot': 2.1.9 @@ -1902,7 +2050,7 @@ snapshots: miniflare: 3.20241230.0 semver: 7.6.3 vitest: 2.1.9(@types/node@22.10.2) - wrangler: 3.100.0(@cloudflare/workers-types@4.20241218.0) + wrangler: 3.100.0(@cloudflare/workers-types@4.20250204.0) zod: 3.24.1 transitivePeerDependencies: - '@cloudflare/workers-types' @@ -1910,37 +2058,37 @@ snapshots: - supports-color - utf-8-validate - '@cloudflare/workerd-darwin-64@1.20241205.0': - optional: true - '@cloudflare/workerd-darwin-64@1.20241230.0': optional: true - '@cloudflare/workerd-darwin-arm64@1.20241205.0': + '@cloudflare/workerd-darwin-64@1.20250204.0': optional: true '@cloudflare/workerd-darwin-arm64@1.20241230.0': optional: true - '@cloudflare/workerd-linux-64@1.20241205.0': + '@cloudflare/workerd-darwin-arm64@1.20250204.0': optional: true '@cloudflare/workerd-linux-64@1.20241230.0': optional: true - '@cloudflare/workerd-linux-arm64@1.20241205.0': + '@cloudflare/workerd-linux-64@1.20250204.0': optional: true '@cloudflare/workerd-linux-arm64@1.20241230.0': optional: true - '@cloudflare/workerd-windows-64@1.20241205.0': + '@cloudflare/workerd-linux-arm64@1.20250204.0': optional: true '@cloudflare/workerd-windows-64@1.20241230.0': optional: true - '@cloudflare/workers-types@4.20241218.0': {} + '@cloudflare/workerd-windows-64@1.20250204.0': + optional: true + + '@cloudflare/workers-types@4.20250204.0': {} '@commitlint/cli@19.6.1(@types/node@22.10.2)(typescript@5.7.2)': dependencies: @@ -2058,6 +2206,11 @@ snapshots: '@drizzle-team/brocli@0.10.2': {} + '@emnapi/runtime@1.3.1': + dependencies: + tslib: 2.8.1 + optional: true + '@esbuild-kit/core-utils@3.3.2': dependencies: esbuild: 0.18.20 @@ -2350,6 +2503,81 @@ snapshots: '@fastify/busboy@2.1.1': {} + '@img/sharp-darwin-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.0.4 + optional: true + + '@img/sharp-darwin-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.0.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.0.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.0.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.0.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.0.5': + optional: true + + '@img/sharp-libvips-linux-s390x@1.0.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.0.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.0.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.0.4': + optional: true + + '@img/sharp-linux-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.0.4 + optional: true + + '@img/sharp-linux-arm@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.0.5 + optional: true + + '@img/sharp-linux-s390x@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.0.4 + optional: true + + '@img/sharp-linux-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.0.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.0.4 + optional: true + + '@img/sharp-wasm32@0.33.5': + dependencies: + '@emnapi/runtime': 1.3.1 + optional: true + + '@img/sharp-win32-ia32@0.33.5': + optional: true + + '@img/sharp-win32-x64@0.33.5': + optional: true + '@jridgewell/resolve-uri@3.1.2': {} '@jridgewell/sourcemap-codec@1.5.0': {} @@ -2477,6 +2705,8 @@ snapshots: jsonparse: 1.3.1 through: 2.3.8 + acorn-walk@8.3.2: {} + acorn-walk@8.3.4: dependencies: acorn: 8.14.0 @@ -2555,6 +2785,18 @@ snapshots: color-name@1.1.4: {} + color-string@1.9.1: + dependencies: + color-name: 1.1.4 + simple-swizzle: 0.2.2 + optional: true + + color@4.2.3: + dependencies: + color-convert: 2.0.1 + color-string: 1.9.1 + optional: true + compare-func@2.0.0: dependencies: array-ify: 1.0.0 @@ -2577,6 +2819,8 @@ snapshots: meow: 12.1.1 split2: 4.2.0 + cookie@0.5.0: {} + cookie@0.7.2: {} cosmiconfig-typescript-loader@6.1.0(@types/node@22.10.2)(cosmiconfig@9.0.0(typescript@5.7.2))(typescript@5.7.2): @@ -2609,6 +2853,9 @@ snapshots: defu@6.1.4: {} + detect-libc@2.0.3: + optional: true + devalue@4.3.3: {} dot-prop@5.3.0: @@ -2626,9 +2873,9 @@ snapshots: transitivePeerDependencies: - supports-color - drizzle-orm@0.36.4(@cloudflare/workers-types@4.20241218.0): + drizzle-orm@0.36.4(@cloudflare/workers-types@4.20250204.0): optionalDependencies: - '@cloudflare/workers-types': 4.20241218.0 + '@cloudflare/workers-types': 4.20250204.0 emoji-regex@8.0.0: {} @@ -2820,6 +3067,9 @@ snapshots: is-arrayish@0.2.1: {} + is-arrayish@0.3.2: + optional: true + is-core-module@2.16.0: dependencies: hasown: 2.0.2 @@ -2888,7 +3138,7 @@ snapshots: mime@3.0.0: {} - miniflare@3.20241205.0: + miniflare@3.20241230.0: dependencies: '@cspotcode/source-map-support': 0.8.1 acorn: 8.14.0 @@ -2898,7 +3148,7 @@ snapshots: glob-to-regexp: 0.4.1 stoppable: 1.1.0 undici: 5.28.5 - workerd: 1.20241205.0 + workerd: 1.20241230.0 ws: 8.18.0 youch: 3.3.4 zod: 3.24.1 @@ -2907,23 +3157,21 @@ snapshots: - supports-color - utf-8-validate - miniflare@3.20241230.0: + miniflare@3.20250204.0: dependencies: '@cspotcode/source-map-support': 0.8.1 acorn: 8.14.0 - acorn-walk: 8.3.4 - capnp-ts: 0.7.0 + acorn-walk: 8.3.2 exit-hook: 2.2.1 glob-to-regexp: 0.4.1 stoppable: 1.1.0 undici: 5.28.5 - workerd: 1.20241230.0 + workerd: 1.20250204.0 ws: 8.18.0 - youch: 3.3.4 - zod: 3.24.1 + youch: 3.2.3 + zod: 3.22.3 transitivePeerDependencies: - bufferutil - - supports-color - utf-8-validate minimist@1.2.8: {} @@ -3056,8 +3304,43 @@ snapshots: semver@7.6.3: {} + semver@7.7.1: + optional: true + + sharp@0.33.5: + dependencies: + color: 4.2.3 + detect-libc: 2.0.3 + semver: 7.7.1 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.33.5 + '@img/sharp-darwin-x64': 0.33.5 + '@img/sharp-libvips-darwin-arm64': 1.0.4 + '@img/sharp-libvips-darwin-x64': 1.0.4 + '@img/sharp-libvips-linux-arm': 1.0.5 + '@img/sharp-libvips-linux-arm64': 1.0.4 + '@img/sharp-libvips-linux-s390x': 1.0.4 + '@img/sharp-libvips-linux-x64': 1.0.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 + '@img/sharp-libvips-linuxmusl-x64': 1.0.4 + '@img/sharp-linux-arm': 0.33.5 + '@img/sharp-linux-arm64': 0.33.5 + '@img/sharp-linux-s390x': 0.33.5 + '@img/sharp-linux-x64': 0.33.5 + '@img/sharp-linuxmusl-arm64': 0.33.5 + '@img/sharp-linuxmusl-x64': 0.33.5 + '@img/sharp-wasm32': 0.33.5 + '@img/sharp-win32-ia32': 0.33.5 + '@img/sharp-win32-x64': 0.33.5 + optional: true + siginfo@2.0.0: {} + simple-swizzle@0.2.2: + dependencies: + is-arrayish: 0.3.2 + optional: true + source-map-js@1.2.1: {} source-map-support@0.5.21: @@ -3122,14 +3405,15 @@ snapshots: dependencies: '@fastify/busboy': 2.1.1 - unenv-nightly@2.0.0-20241204-140205-a5d5190: + unenv-nightly@2.0.0-20241218-183400-5d6aec3: dependencies: defu: 6.1.4 + mlly: 1.7.4 ohash: 1.1.4 pathe: 1.1.2 ufo: 1.5.4 - unenv-nightly@2.0.0-20241218-183400-5d6aec3: + unenv@2.0.0-rc.1: dependencies: defu: 6.1.4 mlly: 1.7.4 @@ -3206,14 +3490,6 @@ snapshots: siginfo: 2.0.0 stackback: 0.0.2 - workerd@1.20241205.0: - optionalDependencies: - '@cloudflare/workerd-darwin-64': 1.20241205.0 - '@cloudflare/workerd-darwin-arm64': 1.20241205.0 - '@cloudflare/workerd-linux-64': 1.20241205.0 - '@cloudflare/workerd-linux-arm64': 1.20241205.0 - '@cloudflare/workerd-windows-64': 1.20241205.0 - workerd@1.20241230.0: optionalDependencies: '@cloudflare/workerd-darwin-64': 1.20241230.0 @@ -3222,7 +3498,15 @@ snapshots: '@cloudflare/workerd-linux-arm64': 1.20241230.0 '@cloudflare/workerd-windows-64': 1.20241230.0 - wrangler@3.100.0(@cloudflare/workers-types@4.20241218.0): + workerd@1.20250204.0: + optionalDependencies: + '@cloudflare/workerd-darwin-64': 1.20250204.0 + '@cloudflare/workerd-darwin-arm64': 1.20250204.0 + '@cloudflare/workerd-linux-64': 1.20250204.0 + '@cloudflare/workerd-linux-arm64': 1.20250204.0 + '@cloudflare/workerd-windows-64': 1.20250204.0 + + wrangler@3.100.0(@cloudflare/workers-types@4.20250204.0): dependencies: '@cloudflare/kv-asset-handler': 0.3.4 '@esbuild-plugins/node-globals-polyfill': 0.2.3(esbuild@0.17.19) @@ -3242,38 +3526,30 @@ snapshots: workerd: 1.20241230.0 xxhash-wasm: 1.1.0 optionalDependencies: - '@cloudflare/workers-types': 4.20241218.0 + '@cloudflare/workers-types': 4.20250204.0 fsevents: 2.3.3 transitivePeerDependencies: - bufferutil - supports-color - utf-8-validate - wrangler@3.98.0(@cloudflare/workers-types@4.20241218.0): + wrangler@3.108.1(@cloudflare/workers-types@4.20250204.0): dependencies: '@cloudflare/kv-asset-handler': 0.3.4 '@esbuild-plugins/node-globals-polyfill': 0.2.3(esbuild@0.17.19) '@esbuild-plugins/node-modules-polyfill': 0.2.2(esbuild@0.17.19) blake3-wasm: 2.1.5 - chokidar: 4.0.3 - date-fns: 4.1.0 esbuild: 0.17.19 - itty-time: 1.0.6 - miniflare: 3.20241205.0 - nanoid: 3.3.8 + miniflare: 3.20250204.0 path-to-regexp: 6.3.0 - resolve: 1.22.10 - selfsigned: 2.4.1 - source-map: 0.6.1 - unenv: unenv-nightly@2.0.0-20241204-140205-a5d5190 - workerd: 1.20241205.0 - xxhash-wasm: 1.1.0 + unenv: 2.0.0-rc.1 + workerd: 1.20250204.0 optionalDependencies: - '@cloudflare/workers-types': 4.20241218.0 + '@cloudflare/workers-types': 4.20250204.0 fsevents: 2.3.3 + sharp: 0.33.5 transitivePeerDependencies: - bufferutil - - supports-color - utf-8-validate wrap-ansi@7.0.0: @@ -3302,10 +3578,18 @@ snapshots: yocto-queue@1.1.1: {} + youch@3.2.3: + dependencies: + cookie: 0.5.0 + mustache: 4.2.0 + stacktracey: 2.1.8 + youch@3.3.4: dependencies: cookie: 0.7.2 mustache: 4.2.0 stacktracey: 2.1.8 + zod@3.22.3: {} + zod@3.24.1: {} diff --git a/src/handlers/auth/users/RefreshToken.Handler.ts b/src/handlers/auth/users/RefreshToken.Handler.ts new file mode 100644 index 0000000..11fffee --- /dev/null +++ b/src/handlers/auth/users/RefreshToken.Handler.ts @@ -0,0 +1,78 @@ +import type { ISanitizedAuthDTO } from '@src/dtos/Auth.DTO' +import { AppError } from '@src/errors/AppErrors.Error' +import { WebCryptoAES } from '@src/lib/webCryptoAES' +import { RefreshTokenRepository } from '@src/repositories/auth/RefreshToken.Repository' +import { JWTManager } from '@src/services/auth/jwtManager/JWTManager.Service' +import { RefreshTokenService } from '@src/services/auth/refreshToken/RefreshToken.Service' +import type { Presenter } from '@src/types/presenter' +import { factory } from '@src/utils/factory' +import type { TypedResponse } from 'hono' +import { getCookie, setCookie } from 'hono/cookie' +import { logger } from 'hono/logger' +import type { StatusCode } from 'hono/utils/http-status' + +export const RefreshTokenHandler = factory.createHandlers( + logger(), + async ( + c + ): Promise, StatusCode>> => { + const refreshTokenRepository = new RefreshTokenRepository(c.env.DB) + const jwtManager = new JWTManager({ + USER_SECRET_KEY: c.env.USER_SECRET_KEY, + REFRESH_SECRET_KEY: c.env.REFRESH_SECRET_KEY, + AUTH_ISSUER: c.env.AUTH_ISSUER + }) + const webCryptoAES = new WebCryptoAES({ + secret: c.env.REFRESH_AES_KEY + }) + const refreshTokenService = new RefreshTokenService( + refreshTokenRepository, + c.env.AUTH_ISSUER, + c.env.USER_SECRET_KEY, + c.env.REFRESH_SECRET_KEY, + jwtManager, + webCryptoAES + ) + + const accessToken = await c.req.json().catch(() => { + throw new AppError({ + name: 'Bad Request', + message: 'Invalid JSON format in request body' + }) + }) + + const userAgent = c.req.header('User-Agent') + + const refreshToken = getCookie(c, 'refreshToken') + + const data = { refreshToken, userAgent, ...accessToken } + + const user = await refreshTokenService.execute(data) + + setCookie(c, 'refreshToken', user.token.refreshToken, { + secure: true, + httpOnly: true, + sameSite: 'Strict', + maxAge: 60 * 60 * 24 * 27, + path: '/users/auth/refresh' + }) + + return c.json( + { + success: true, + message: 'Token refreshed successfully', + data: { + id: user.id, + name: user.name, + username: user.username, + email: user.email ? user.email : undefined, + token: { + accessToken: user.token.accessToken, + expiresIn: user.token.expiresIn + } + } + }, + 200 + ) + } +) diff --git a/src/routers/auth.router.ts b/src/routers/auth.router.ts index 83185bd..fbc34c9 100644 --- a/src/routers/auth.router.ts +++ b/src/routers/auth.router.ts @@ -1,6 +1,7 @@ import { EnableOtpHandler } from '@src/handlers/auth/users/EnableOTP.Handler' import { GenerateOtpAuthUrlHandler } from '@src/handlers/auth/users/GenerateOtpAuth.Handler' import { OtpAuthHandler } from '@src/handlers/auth/users/OTPAuth.Handler' +import { RefreshTokenHandler } from '@src/handlers/auth/users/RefreshToken.Handler' import { UserAuthHandler } from '@src/handlers/auth/users/UserAuth.Handler' import { authenticateToken } from '@src/middleware/Auth.middleware' import { Hono } from 'hono' @@ -9,6 +10,7 @@ const authRouters = new Hono() authRouters.post('/users/login', ...UserAuthHandler) authRouters.post('/users/otp/login', ...OtpAuthHandler) +authRouters.post('/users/auth/refresh', ...RefreshTokenHandler) authRouters.put('/users/:id/otp', ...EnableOtpHandler) authRouters.post( '/users/:id/otp/secret', diff --git a/src/services/auth/refreshToken/RefreshToken.Service.ts b/src/services/auth/refreshToken/RefreshToken.Service.ts new file mode 100644 index 0000000..d6f6411 --- /dev/null +++ b/src/services/auth/refreshToken/RefreshToken.Service.ts @@ -0,0 +1,237 @@ +import type { IAuthReturnDTO, StoreRefreshTokenInput } from '@src/dtos/Auth.DTO' +import type { + IRefreshTokenDTO, + IRefreshTokenParamsDTO +} from '@src/dtos/RefreshToken.DTO' +import type { IUsersDTO } from '@src/dtos/User.DTO' +import { RefreshToken } from '@src/entities/RefreshToken.Entity' +import { AppError } from '@src/errors/AppErrors.Error' +import type { WebCryptoAES } from '@src/lib/webCryptoAES' +import type { RefreshTokenRepository } from '@src/repositories/auth/RefreshToken.Repository' +import { refreshTokenSchema } from '@src/validations/auth/RefreshToken.Validation' +import { decode, verify } from 'hono/jwt' +import type { TokenHeader } from 'hono/utils/jwt/jwt' +import { + type JWTPayload, + JwtTokenSignatureMismatched +} from 'hono/utils/jwt/types' +import type { JWTManager } from '../jwtManager/JWTManager.Service' + +export class RefreshTokenService { + public constructor( + private readonly refreshTokenRepository: RefreshTokenRepository, + private readonly AUTH_ISSUER: string, + private readonly USER_SECRET_KEY: string, + private readonly REFRESH_SECRET_KEY: string, + private readonly jwtManager: JWTManager, + private readonly webCryptoAES: WebCryptoAES + ) {} + + public async execute(data: IRefreshTokenParamsDTO): Promise { + this.validationInput(data) + + await this.verifySignature(data.accessToken, 'accessToken') + await this.verifySignature(data.refreshToken, 'refreshToken') + + const { refreshToken, accessToken } = this.decodeTokens(data) + + this.validateToken(refreshToken) + this.validateToken(accessToken) + + this.checkTokensSubMatch(accessToken.payload.sub, refreshToken.payload.sub) + + this.checkTokenExp(refreshToken.payload.exp, accessToken.payload.exp) + + const user = await this.getRefreshToken(refreshToken.payload.sub) + + await this.checkTokensMatch(data.refreshToken, user.refreshToken[0].token) + + const newToken = await this.jwtManager.generateToken({ + id: user.id, + name: user.name, + username: user.username + }) + + const hashedNewToken = await this.hashNewToken(newToken.refreshToken) + + await this.refreshTokenRepository.update({ + ...user.refreshToken[0], + revoked: true + }) + + await this.storeRefreshToken({ + expiresAt: newToken.accessTokenExp, + refreshToken: hashedNewToken, + userAgent: data.userAgent, + userId: user.id + }) + + return { + id: user.id, + name: user.name, + username: user.username, + email: user.email ? user.email : undefined, + token: { + accessToken: newToken.accessToken, + refreshToken: newToken.refreshToken, + expiresIn: newToken.accessTokenExp + } + } + } + + private checkTokensSubMatch( + accessTokenSub: unknown, + refreshTokenSub: unknown + ): asserts refreshTokenSub is string { + if ( + typeof accessTokenSub !== 'string' || + typeof refreshTokenSub !== 'string' || + refreshTokenSub !== accessTokenSub + ) { + throw new AppError({ + name: 'Forbidden', + message: 'Action Denied!' + }) + } + } + + private async verifySignature( + token: string, + type: 'refreshToken' | 'accessToken' + ): Promise { + try { + const secret = + type === 'refreshToken' ? this.REFRESH_SECRET_KEY : this.USER_SECRET_KEY + + await verify(token, secret) + } catch (error) { + if (error instanceof JwtTokenSignatureMismatched) { + throw new AppError({ + name: 'Unauthorized', + message: 'Access Denied!' + }) + } + } + } + + private async storeRefreshToken(data: StoreRefreshTokenInput): Promise { + const refreshTokenData = { + revoked: false, + token: data.refreshToken, + userAgent: data.userAgent, + userId: data.userId, + expiresAt: new Date(data.expiresAt * 1000).toISOString() + } + const refreshToken = new RefreshToken(refreshTokenData) + + await this.refreshTokenRepository.create(refreshToken) + } + + private async checkTokensMatch( + clientToken: string, + dbToken: string + ): Promise { + const decryptedToken = await this.webCryptoAES.decryptSymetric(dbToken) + + const isValid = clientToken === decryptedToken.plainText + + if (!isValid) { + throw new AppError({ + name: 'Unauthorized', + message: 'Access Denied!' + }) + } + } + + private async hashNewToken(refreshToken: string): Promise { + const encryptedClientToken = + await this.webCryptoAES.encryptSymetric(refreshToken) + + if (!encryptedClientToken.cipherText || encryptedClientToken.error) { + throw new AppError({ + name: 'Internal Server Error', + message: 'Internal Server Error' + }) + } + + return encryptedClientToken.cipherText + } + + private async getRefreshToken( + userId: string + ): Promise { + const user = await this.refreshTokenRepository.findManyByOr({ + id: userId, + revoked: false, + noExpired: true + }) + + if (user === null) { + throw new AppError({ + name: 'Forbidden', + message: 'Action Denied!' + }) + } + + return user + } + + private checkTokenExp( + refreshTokenExp?: number, + accessTokenExp?: number + ): void { + const now = Math.floor(Date.now() / 1000) + + if ( + !refreshTokenExp || + !accessTokenExp || + refreshTokenExp < now || + accessTokenExp > now + ) { + throw new AppError({ + name: 'Unauthorized', + message: 'Invalid Token!' + }) + } + } + + private validateToken(data: { + header: TokenHeader + payload: JWTPayload + }): void { + if ( + !data.payload.iss || + !data.payload.sub || + data.payload.iss !== this.AUTH_ISSUER + ) { + throw new AppError({ + name: 'Forbidden', + message: 'Action Denied!' + }) + } + } + + private decodeTokens(data: IRefreshTokenParamsDTO): { + refreshToken: { header: TokenHeader; payload: JWTPayload } + accessToken: { header: TokenHeader; payload: JWTPayload } + } { + const refreshToken = decode(data.refreshToken) + const accessToken = decode(data.accessToken) + return { refreshToken, accessToken } + } + + private validationInput(data: IRefreshTokenParamsDTO): void { + const isValidData = refreshTokenSchema.check(data) + + if (!isValidData.success) { + throw new AppError({ + name: 'Bad Request', + message: 'Validation failed', + cause: { + field: isValidData.field, + cause: `is not ${isValidData.failedValidator}` + } + }) + } + } +} diff --git a/src/validations/auth/RefreshToken.Validation.ts b/src/validations/auth/RefreshToken.Validation.ts new file mode 100644 index 0000000..0ed138d --- /dev/null +++ b/src/validations/auth/RefreshToken.Validation.ts @@ -0,0 +1,12 @@ +import { validator } from '@src/lib/validator' + +export const refreshTokenSchema = validator.schema( + { + accessToken: validator.string().max(300).min(1), + refreshToken: validator.string().max(256).min(8), + userAgent: validator.string().max(256).nullable() + }, + { + strict: true + } +) diff --git a/tests/handlers/auth/refreshToken/NotFound.failure.test.ts b/tests/handlers/auth/refreshToken/NotFound.failure.test.ts new file mode 100644 index 0000000..3aa4fd1 --- /dev/null +++ b/tests/handlers/auth/refreshToken/NotFound.failure.test.ts @@ -0,0 +1,69 @@ +import { applyD1Migrations, env } from 'cloudflare:test' +import { users } from '@src/db/user.schema' +import app from '@src/index' +import { drizzle } from 'drizzle-orm/d1' +import { sign } from 'hono/jwt' +import { afterEach, beforeAll, describe, expect, test } from 'vitest' + +describe('Refresh Token not found failure cases E2E', () => { + const db = drizzle(env.DB) + + beforeAll(async () => { + await applyD1Migrations(env.DB, env.TEST_MIGRATIONS) + }) + + afterEach(async () => { + await db.delete(users) + }) + + test('should fail with not found refresh token on database', async () => { + const payloadAccessToken = { + sub: '01JHBDWAXFPAKAFK38E1MAM01W', + name: 'John Doe', + username: 'johndoe123', + iss: env.AUTH_ISSUER, + iat: Math.floor(Date.now() / 1000), + exp: Math.floor(Date.now() / 1000) - 60 + } + + const payloadRefreshToken = { + sub: '01JHBDWAXFPAKAFK38E1MAM01W', + iss: env.AUTH_ISSUER, + iat: Math.floor(Date.now() / 1000), + exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 30 + } + + const accessTokenGen = await sign(payloadAccessToken, env.USER_SECRET_KEY) + const refreshTokenGen = await sign( + payloadRefreshToken, + env.REFRESH_SECRET_KEY + ) + + const payload = JSON.stringify({ + accessToken: accessTokenGen + }) + + const res = await app.request( + '/users/auth/refresh', + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': 'mock-csrf-token', + 'User-Agent': 'Vitest', + Cookie: `refreshToken=${refreshTokenGen}; Max-Age=2332800; Path=/users/auth/refresh; HttpOnly; Secure; SameSite=Strict` + }, + body: payload + }, + env + ) + + const result = await res.json() + + expect(res.status).toBe(403) + expect(result).toStrictEqual({ + success: false, + message: 'Action Denied!' + }) + }) +}) diff --git a/tests/handlers/auth/refreshToken/Signature.failure.test.ts b/tests/handlers/auth/refreshToken/Signature.failure.test.ts new file mode 100644 index 0000000..91c565e --- /dev/null +++ b/tests/handlers/auth/refreshToken/Signature.failure.test.ts @@ -0,0 +1,117 @@ +import { applyD1Migrations, env } from 'cloudflare:test' +import { users } from '@src/db/user.schema' +import app from '@src/index' +import { drizzle } from 'drizzle-orm/d1' +import { sign } from 'hono/jwt' +import { afterEach, beforeAll, describe, expect, test } from 'vitest' + +describe('Refresh Token signature failure cases E2E', () => { + const db = drizzle(env.DB) + + beforeAll(async () => { + await applyD1Migrations(env.DB, env.TEST_MIGRATIONS) + }) + + afterEach(async () => { + await db.delete(users) + }) + + test('should fail with accessToken wrong invalid access token signature', async () => { + const payloadAccessToken = { + sub: '01JHBDWAXFPAKAFK38E1MAM01W', + name: 'John Doe', + username: 'johndoe123', + iss: env.AUTH_ISSUER, + iat: Math.floor(Date.now() / 1000), + exp: Math.floor(Date.now() / 1000) + 60 + } + + const payloadRefreshToken = { + sub: '01JHBDWAXFPAKAFK38E1MAM01W', + iss: env.AUTH_ISSUER, + iat: Math.floor(Date.now() / 1000), + exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 30 + } + + const accessTokenGen = await sign(payloadAccessToken, 'invalid-signature') + const refreshTokenGen = await sign( + payloadRefreshToken, + env.REFRESH_SECRET_KEY + ) + + const payload = JSON.stringify({ + accessToken: accessTokenGen + }) + + const res = await app.request( + '/users/auth/refresh', + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': 'mock-csrf-token', + 'User-Agent': 'Vitest', + Cookie: `refreshToken=${refreshTokenGen}; Max-Age=2332800; Path=/users/auth/refresh; HttpOnly; Secure; SameSite=Strict` + }, + body: payload + }, + env + ) + + const result = await res.json() + + expect(res.status).toBe(401) + expect(result).toStrictEqual({ + success: false, + message: 'Access Denied!' + }) + }) + + test('should fail with accessToken wrong invalid refresh token signature', async () => { + const payloadAccessToken = { + sub: '01JHBDWAXFPAKAFK38E1MAM01W', + name: 'John Doe', + username: 'johndoe123', + iss: env.AUTH_ISSUER, + iat: Math.floor(Date.now() / 1000), + exp: Math.floor(Date.now() / 1000) + 60 + } + + const payloadRefreshToken = { + sub: '01JHBDWAXFPAKAFK38E1MAM01W', + iss: env.AUTH_ISSUER, + iat: Math.floor(Date.now() / 1000), + exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 30 + } + + const accessTokenGen = await sign(payloadAccessToken, env.USER_SECRET_KEY) + const refreshTokenGen = await sign(payloadRefreshToken, 'invalid-signature') + + const payload = JSON.stringify({ + accessToken: accessTokenGen + }) + + const res = await app.request( + '/users/auth/refresh', + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': 'mock-csrf-token', + 'User-Agent': 'Vitest', + Cookie: `refreshToken=${refreshTokenGen}; Max-Age=2332800; Path=/users/auth/refresh; HttpOnly; Secure; SameSite=Strict` + }, + body: payload + }, + env + ) + + const result = await res.json() + + expect(res.status).toBe(401) + expect(result).toStrictEqual({ + success: false, + message: 'Access Denied!' + }) + }) +}) diff --git a/tests/handlers/auth/refreshToken/SubMatch.failure.test.ts b/tests/handlers/auth/refreshToken/SubMatch.failure.test.ts new file mode 100644 index 0000000..9549485 --- /dev/null +++ b/tests/handlers/auth/refreshToken/SubMatch.failure.test.ts @@ -0,0 +1,69 @@ +import { applyD1Migrations, env } from 'cloudflare:test' +import { users } from '@src/db/user.schema' +import app from '@src/index' +import { drizzle } from 'drizzle-orm/d1' +import { sign } from 'hono/jwt' +import { afterEach, beforeAll, describe, expect, test } from 'vitest' + +describe('Refresh Token Sub Match failure cases E2E', () => { + const db = drizzle(env.DB) + + beforeAll(async () => { + await applyD1Migrations(env.DB, env.TEST_MIGRATIONS) + }) + + afterEach(async () => { + await db.delete(users) + }) + + test('should fail with not matched sub tokens', async () => { + const payloadAccessToken = { + sub: '01JHBDWAXFPAKAFK38E1MAM01W', + name: 'John Doe', + username: 'johndoe123', + iss: env.AUTH_ISSUER, + iat: Math.floor(Date.now() / 1000), + exp: Math.floor(Date.now() / 1000) + 60 + } + + const payloadRefreshToken = { + sub: 'other-sub', + iss: env.AUTH_ISSUER, + iat: Math.floor(Date.now() / 1000), + exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 30 + } + + const accessTokenGen = await sign(payloadAccessToken, env.USER_SECRET_KEY) + const refreshTokenGen = await sign( + payloadRefreshToken, + env.REFRESH_SECRET_KEY + ) + + const payload = JSON.stringify({ + accessToken: accessTokenGen + }) + + const res = await app.request( + '/users/auth/refresh', + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': 'mock-csrf-token', + 'User-Agent': 'Vitest', + Cookie: `refreshToken=${refreshTokenGen}; Max-Age=2332800; Path=/users/auth/refresh; HttpOnly; Secure; SameSite=Strict` + }, + body: payload + }, + env + ) + + const result = await res.json() + + expect(res.status).toBe(403) + expect(result).toStrictEqual({ + success: false, + message: 'Action Denied!' + }) + }) +}) diff --git a/tests/handlers/auth/refreshToken/Success.test.ts b/tests/handlers/auth/refreshToken/Success.test.ts new file mode 100644 index 0000000..33cfaaf --- /dev/null +++ b/tests/handlers/auth/refreshToken/Success.test.ts @@ -0,0 +1,164 @@ +import { applyD1Migrations, env } from 'cloudflare:test' +import { refreshToken } from '@src/db/refreshToken.schema' +import { users } from '@src/db/user.schema' +import { RefreshToken } from '@src/entities/RefreshToken.Entity' +import app from '@src/index' +import { WebCryptoAES } from '@src/lib/webCryptoAES' +import { drizzle } from 'drizzle-orm/d1' +import { sign } from 'hono/jwt' +import { afterEach, beforeAll, describe, expect, test } from 'vitest' + +describe('Refresh Token success cases handler E2E', () => { + const db = drizzle(env.DB) + + beforeAll(async () => { + await applyD1Migrations(env.DB, env.TEST_MIGRATIONS) + }) + + afterEach(async () => { + await db.delete(users) + }) + + test('should refresh token successfully', async () => { + const webCryptoAES = new WebCryptoAES({ secret: env.REFRESH_AES_KEY }) + + const payloadAccessToken = { + sub: '01JHBDWAXFPAKAFK38E1MAM01W', + name: 'John Doe', + username: 'johndoe123', + iss: env.AUTH_ISSUER, + iat: Math.floor(Date.now() / 1000), + exp: Math.floor(Date.now() / 1000) - 60 + } + + const payloadRequestRefreshToken = { + sub: '01JHBDWAXFPAKAFK38E1MAM01W', + iss: env.AUTH_ISSUER, + iat: Math.floor(Date.now() / 1000), + exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 30 + } + + const generatedAccessToken = await sign( + payloadAccessToken, + env.USER_SECRET_KEY + ) + + const refreshTokenInstances: RefreshToken[] = [] + let generatedRefreshToken = '' + + const totalRefreshTokens = 3 + + for (let i = 0; i < totalRefreshTokens; i++) { + const payloadRefreshTokena = + i === totalRefreshTokens - 1 + ? payloadRequestRefreshToken + : { + sub: '01JHBDWAXFPAKAFK38E1MAM01W', + iss: env.AUTH_ISSUER, + iat: Math.floor(Date.now() / 1000), + exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 30 + (i + 1) + } + + const refreshTokenGen = await sign( + payloadRefreshTokena, + env.REFRESH_SECRET_KEY + ) + + const encryptedRefreshToken = + (await webCryptoAES.encryptSymetric(refreshTokenGen)).cipherText ?? '' + + if (i === totalRefreshTokens - 1) { + generatedRefreshToken = refreshTokenGen + } + const expirationDate = new Date() + expirationDate.setMonth(expirationDate.getMonth() + 1) + expirationDate.setSeconds(expirationDate.getSeconds() + (1 + i)) + + const refreshTokenData = { + token: encryptedRefreshToken, + revoked: false, + userAgent: 'Vitest', + userId: '01JHBDWAXFPAKAFK38E1MAM01W', + expiresAt: expirationDate.toISOString(), + createdAt: expirationDate.toISOString() + } + + const refreshTokenInstance = new RefreshToken(refreshTokenData) + + refreshTokenInstances.push(refreshTokenInstance) + } + + const date = new Date().toISOString() + await db.insert(users).values({ + id: '01JHBDWAXFPAKAFK38E1MAM01W', + name: 'John Doe', + username: 'johndoe123', + email: 'johndoe@example.com', + isActive: true, + deletedAt: null, + kats: 0, + rank: 0, + createdAt: date + }) + + await db.insert(refreshToken).values(refreshTokenInstances) + + const payload = JSON.stringify({ + accessToken: generatedAccessToken + }) + + const res = await app.request( + '/users/auth/refresh', + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': 'mock-csrf-token', + 'User-Agent': 'Vitest', + Cookie: `refreshToken=${generatedRefreshToken}; Max-Age=2332800; Path=/users/auth/refresh; HttpOnly; Secure; SameSite=Strict` + }, + body: payload + }, + env + ) + + const result = await res.json() + + const cookies = res.headers.getSetCookie()[0] + const token = cookies.split('refreshToken=')[1]?.split(';')[0] || null + + const tokenDB = await db.select().from(refreshToken) + + const decryptedTokenDb = await webCryptoAES.decryptSymetric( + tokenDB[refreshTokenInstances.length - 1].token + ) + + const refreshTokenRegex = cookies.match(/refreshToken=([^;]+)/)?.[1] + const jwtRegex = /^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+$/ + + expect(tokenDB[refreshTokenInstances.length - 1].revoked).toBe(true) + expect(token).toBe(decryptedTokenDb.plainText) + expect(res.status).toBe(200) + expect(cookies).toMatch(/refreshToken=/) + expect(cookies).toMatch(/HttpOnly/) + expect(cookies).toMatch(/Path=\/users\/auth\/refresh/) + expect(cookies).toMatch(/Secure/) + expect(cookies).toMatch(/SameSite=Strict/) + expect(cookies).toMatch(/Max-Age=2332800/) + expect(refreshTokenRegex).toMatch(jwtRegex) + expect(result).toStrictEqual({ + success: true, + message: 'Token refreshed successfully', + data: { + id: '01JHBDWAXFPAKAFK38E1MAM01W', + name: 'John Doe', + username: 'johndoe123', + email: 'johndoe@example.com', + token: { + accessToken: expect.stringMatching(jwtRegex), + expiresIn: expect.any(Number) + } + } + }) + }) +}) diff --git a/tests/handlers/auth/refreshToken/TokenExp.failure.test.ts b/tests/handlers/auth/refreshToken/TokenExp.failure.test.ts new file mode 100644 index 0000000..4b6cc97 --- /dev/null +++ b/tests/handlers/auth/refreshToken/TokenExp.failure.test.ts @@ -0,0 +1,120 @@ +import { applyD1Migrations, env } from 'cloudflare:test' +import { users } from '@src/db/user.schema' +import app from '@src/index' +import { drizzle } from 'drizzle-orm/d1' +import { sign } from 'hono/jwt' +import { afterEach, beforeAll, describe, expect, test } from 'vitest' + +describe('Refresh Token expired failure cases E2E', () => { + const db = drizzle(env.DB) + + beforeAll(async () => { + await applyD1Migrations(env.DB, env.TEST_MIGRATIONS) + }) + + afterEach(async () => { + await db.delete(users) + }) + + test('should fail with accessToken not expired', async () => { + const payloadAccessToken = { + sub: '01JHBDWAXFPAKAFK38E1MAM01W', + name: 'John Doe', + username: 'johndoe123', + iss: env.AUTH_ISSUER, + iat: Math.floor(Date.now() / 1000), + exp: Math.floor(Date.now() / 1000) + 60 * 60 + } + + const payloadRefreshToken = { + sub: '01JHBDWAXFPAKAFK38E1MAM01W', + iss: env.AUTH_ISSUER, + iat: Math.floor(Date.now() / 1000), + exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 30 + } + + const accessTokenGen = await sign(payloadAccessToken, env.USER_SECRET_KEY) + const refreshTokenGen = await sign( + payloadRefreshToken, + env.REFRESH_SECRET_KEY + ) + + const payload = JSON.stringify({ + accessToken: accessTokenGen + }) + + const res = await app.request( + '/users/auth/refresh', + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': 'mock-csrf-token', + 'User-Agent': 'Vitest', + Cookie: `refreshToken=${refreshTokenGen}; Max-Age=2332800; Path=/users/auth/refresh; HttpOnly; Secure; SameSite=Strict` + }, + body: payload + }, + env + ) + + const result = await res.json() + + expect(res.status).toBe(401) + expect(result).toStrictEqual({ + success: false, + message: 'Invalid Token!' + }) + }) + + test('should fail with refreshToken expired', async () => { + const payloadAccessToken = { + sub: '01JHBDWAXFPAKAFK38E1MAM01W', + name: 'John Doe', + username: 'johndoe123', + iss: env.AUTH_ISSUER, + iat: Math.floor(Date.now() / 1000), + exp: Math.floor(Date.now() / 1000) - 60 + } + + const payloadRefreshToken = { + sub: '01JHBDWAXFPAKAFK38E1MAM01W', + iss: env.AUTH_ISSUER, + iat: Math.floor(Date.now() / 1000), + exp: Math.floor(Date.now() / 1000) - 60 + } + + const accessTokenGen = await sign(payloadAccessToken, env.USER_SECRET_KEY) + const refreshTokenGen = await sign( + payloadRefreshToken, + env.REFRESH_SECRET_KEY + ) + + const payload = JSON.stringify({ + accessToken: accessTokenGen + }) + + const res = await app.request( + '/users/auth/refresh', + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': 'mock-csrf-token', + 'User-Agent': 'Vitest', + Cookie: `refreshToken=${refreshTokenGen}; Max-Age=2332800; Path=/users/auth/refresh; HttpOnly; Secure; SameSite=Strict` + }, + body: payload + }, + env + ) + + const result = await res.json() + + expect(res.status).toBe(401) + expect(result).toStrictEqual({ + success: false, + message: 'Invalid Token!' + }) + }) +}) diff --git a/tests/handlers/auth/refreshToken/TokenStatus.failure.test.ts b/tests/handlers/auth/refreshToken/TokenStatus.failure.test.ts new file mode 100644 index 0000000..3408327 --- /dev/null +++ b/tests/handlers/auth/refreshToken/TokenStatus.failure.test.ts @@ -0,0 +1,120 @@ +import { applyD1Migrations, env } from 'cloudflare:test' +import { refreshToken } from '@src/db/refreshToken.schema' +import { users } from '@src/db/user.schema' +import { RefreshToken } from '@src/entities/RefreshToken.Entity' +import app from '@src/index' +import { WebCryptoAES } from '@src/lib/webCryptoAES' +import { drizzle } from 'drizzle-orm/d1' +import { sign } from 'hono/jwt' +import { afterEach, beforeAll, describe, expect, test } from 'vitest' + +describe('Refresh Token status failure cases E2E', () => { + const db = drizzle(env.DB) + + beforeAll(async () => { + await applyD1Migrations(env.DB, env.TEST_MIGRATIONS) + }) + + afterEach(async () => { + await db.delete(users) + }) + + test('should fail with not match refresh token', async () => { + const webCryptoAES = new WebCryptoAES({ secret: env.REFRESH_AES_KEY }) + + const payloadAccessToken = { + sub: '01JHBDWAXFPAKAFK38E1MAM01W', + name: 'John Doe', + username: 'johndoe123', + iss: env.AUTH_ISSUER, + iat: Math.floor(Date.now() / 1000), + exp: Math.floor(Date.now() / 1000) - 60 + } + + const payloadRefreshToken = { + sub: '01JHBDWAXFPAKAFK38E1MAM01W', + iss: env.AUTH_ISSUER, + iat: Math.floor(Date.now() / 1000), + exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 30 + } + + const payloadRefreshDatabase = { + sub: '01JHBDWAXFPAKAFK38E1MAM01W', + iss: 'env.AUTH_ISSUER', + iat: Math.floor(Date.now() / 1000), + exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24 + } + + const accessTokenGen = await sign(payloadAccessToken, env.USER_SECRET_KEY) + const refreshTokenGen = await sign( + payloadRefreshToken, + env.REFRESH_SECRET_KEY + ) + + const refreshTokenDatabase = await sign( + payloadRefreshDatabase, + env.REFRESH_SECRET_KEY + ) + + const now = new Date() + const expiresTomorrow = new Date(now) + + expiresTomorrow.setDate(expiresTomorrow.getDate() + 1) + + const expiresAt = expiresTomorrow.toISOString() + + const data = { + token: + (await webCryptoAES.encryptSymetric(refreshTokenDatabase)).cipherText ?? + '', + revoked: false, + userAgent: 'Vitest', + userId: '01JHBDWAXFPAKAFK38E1MAM01W', + expiresAt: expiresAt, + createdAt: new Date().toISOString() + } + + const refreshTokenInstance = new RefreshToken(data) + + await db.insert(users).values({ + id: '01JHBDWAXFPAKAFK38E1MAM01W', + name: 'John Doe', + username: 'johndoe123', + email: 'johndoe@example.com', + isActive: true, + deletedAt: null, + kats: 0, + rank: 0, + createdAt: now.toISOString() + }) + + await db.insert(refreshToken).values(refreshTokenInstance) + + const payload = JSON.stringify({ + accessToken: accessTokenGen + }) + + const res = await app.request( + '/users/auth/refresh', + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': 'mock-csrf-token', + 'User-Agent': 'Vitest', + Cookie: `refreshToken=${refreshTokenGen}; Max-Age=2332800; Path=/users/auth/refresh; HttpOnly; Secure; SameSite=Strict` + }, + body: payload + }, + env + ) + + const result = await res.json() + + expect(res.status).toBe(401) + expect(result).toStrictEqual({ + success: false, + message: 'Access Denied!' + }) + }) +}) diff --git a/tests/handlers/auth/refreshToken/ValidateToken.failure.test.ts b/tests/handlers/auth/refreshToken/ValidateToken.failure.test.ts new file mode 100644 index 0000000..782985e --- /dev/null +++ b/tests/handlers/auth/refreshToken/ValidateToken.failure.test.ts @@ -0,0 +1,324 @@ +import { applyD1Migrations, env } from 'cloudflare:test' +import { users } from '@src/db/user.schema' +import app from '@src/index' +import { drizzle } from 'drizzle-orm/d1' +import { sign } from 'hono/jwt' +import { afterEach, beforeAll, describe, expect, test } from 'vitest' + +describe('Refresh Token Validate Token failure cases E2E', () => { + const db = drizzle(env.DB) + + beforeAll(async () => { + await applyD1Migrations(env.DB, env.TEST_MIGRATIONS) + }) + + afterEach(async () => { + await db.delete(users) + }) + + test('should fail with undefined sub at accessToken', async () => { + const payloadAccessToken = { + sub: undefined, + name: 'John Doe', + username: 'johndoe123', + iss: env.AUTH_ISSUER, + iat: Math.floor(Date.now() / 1000), + exp: Math.floor(Date.now() / 1000) + 60 + } + + const payloadRefreshToken = { + sub: '01JHBDWAXFPAKAFK38E1MAM01W', + iss: env.AUTH_ISSUER, + iat: Math.floor(Date.now() / 1000), + exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 30 + } + + const accessTokenGen = await sign(payloadAccessToken, env.USER_SECRET_KEY) + const refreshTokenGen = await sign( + payloadRefreshToken, + env.REFRESH_SECRET_KEY + ) + + const payload = JSON.stringify({ + accessToken: accessTokenGen + }) + + const res = await app.request( + '/users/auth/refresh', + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': 'mock-csrf-token', + 'User-Agent': 'Vitest', + Cookie: `refreshToken=${refreshTokenGen}; Max-Age=2332800; Path=/users/auth/refresh; HttpOnly; Secure; SameSite=Strict` + }, + body: payload + }, + env + ) + + const result = await res.json() + + expect(res.status).toBe(403) + expect(result).toStrictEqual({ + success: false, + message: 'Action Denied!' + }) + }) + + test('should fail with undefined iss at accessToken', async () => { + const payloadAccessToken = { + sub: '01JHBDWAXFPAKAFK38E1MAM01W', + name: 'John Doe', + username: 'johndoe123', + iss: undefined, + iat: Math.floor(Date.now() / 1000), + exp: Math.floor(Date.now() / 1000) + 60 + } + + const payloadRefreshToken = { + sub: '01JHBDWAXFPAKAFK38E1MAM01W', + iss: env.AUTH_ISSUER, + iat: Math.floor(Date.now() / 1000), + exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 30 + } + + const accessTokenGen = await sign(payloadAccessToken, env.USER_SECRET_KEY) + const refreshTokenGen = await sign( + payloadRefreshToken, + env.REFRESH_SECRET_KEY + ) + + const payload = JSON.stringify({ + accessToken: accessTokenGen + }) + + const res = await app.request( + '/users/auth/refresh', + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': 'mock-csrf-token', + 'User-Agent': 'Vitest', + Cookie: `refreshToken=${refreshTokenGen}; Max-Age=2332800; Path=/users/auth/refresh; HttpOnly; Secure; SameSite=Strict` + }, + body: payload + }, + env + ) + + const result = await res.json() + + expect(res.status).toBe(403) + expect(result).toStrictEqual({ + success: false, + message: 'Action Denied!' + }) + }) + + test('should fail with undefined iss is not equal AUTH_ISSUER at accessToken', async () => { + const payloadAccessToken = { + sub: '01JHBDWAXFPAKAFK38E1MAM01W', + name: 'John Doe', + username: 'johndoe123', + iss: 'not-Auth_issuer', + iat: Math.floor(Date.now() / 1000), + exp: Math.floor(Date.now() / 1000) + 60 + } + + const payloadRefreshToken = { + sub: '01JHBDWAXFPAKAFK38E1MAM01W', + iss: env.AUTH_ISSUER, + iat: Math.floor(Date.now() / 1000), + exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 30 + } + + const accessTokenGen = await sign(payloadAccessToken, env.USER_SECRET_KEY) + const refreshTokenGen = await sign( + payloadRefreshToken, + env.REFRESH_SECRET_KEY + ) + + const payload = JSON.stringify({ + accessToken: accessTokenGen + }) + + const res = await app.request( + '/users/auth/refresh', + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': 'mock-csrf-token', + 'User-Agent': 'Vitest', + Cookie: `refreshToken=${refreshTokenGen}; Max-Age=2332800; Path=/users/auth/refresh; HttpOnly; Secure; SameSite=Strict` + }, + body: payload + }, + env + ) + + const result = await res.json() + + expect(res.status).toBe(403) + expect(result).toStrictEqual({ + success: false, + message: 'Action Denied!' + }) + }) + + test('should fail with undefined sub at refreshToken', async () => { + const payloadAccessToken = { + sub: '01JHBDWAXFPAKAFK38E1MAM01W', + name: 'John Doe', + username: 'johndoe123', + iss: env.AUTH_ISSUER, + iat: Math.floor(Date.now() / 1000), + exp: Math.floor(Date.now() / 1000) + 60 + } + + const payloadRefreshToken = { + sub: undefined, + iss: env.AUTH_ISSUER, + iat: Math.floor(Date.now() / 1000), + exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 30 + } + + const accessTokenGen = await sign(payloadAccessToken, env.USER_SECRET_KEY) + const refreshTokenGen = await sign( + payloadRefreshToken, + env.REFRESH_SECRET_KEY + ) + + const payload = JSON.stringify({ + accessToken: accessTokenGen + }) + + const res = await app.request( + '/users/auth/refresh', + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': 'mock-csrf-token', + 'User-Agent': 'Vitest', + Cookie: `refreshToken=${refreshTokenGen}; Max-Age=2332800; Path=/users/auth/refresh; HttpOnly; Secure; SameSite=Strict` + }, + body: payload + }, + env + ) + + const result = await res.json() + + expect(res.status).toBe(403) + expect(result).toStrictEqual({ + success: false, + message: 'Action Denied!' + }) + }) + + test('should fail with undefined iss at refreshToken', async () => { + const payloadAccessToken = { + sub: '01JHBDWAXFPAKAFK38E1MAM01W', + name: 'John Doe', + username: 'johndoe123', + iss: env.AUTH_ISSUER, + iat: Math.floor(Date.now() / 1000), + exp: Math.floor(Date.now() / 1000) + 60 + } + + const payloadRefreshToken = { + sub: '01JHBDWAXFPAKAFK38E1MAM01W', + iss: undefined, + iat: Math.floor(Date.now() / 1000), + exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 30 + } + + const accessTokenGen = await sign(payloadAccessToken, env.USER_SECRET_KEY) + const refreshTokenGen = await sign( + payloadRefreshToken, + env.REFRESH_SECRET_KEY + ) + + const payload = JSON.stringify({ + accessToken: accessTokenGen + }) + + const res = await app.request( + '/users/auth/refresh', + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': 'mock-csrf-token', + 'User-Agent': 'Vitest', + Cookie: `refreshToken=${refreshTokenGen}; Max-Age=2332800; Path=/users/auth/refresh; HttpOnly; Secure; SameSite=Strict` + }, + body: payload + }, + env + ) + + const result = await res.json() + + expect(res.status).toBe(403) + expect(result).toStrictEqual({ + success: false, + message: 'Action Denied!' + }) + }) + + test('should fail with undefined iss is not equal AUTH_ISSUER at accessToken', async () => { + const payloadAccessToken = { + sub: '01JHBDWAXFPAKAFK38E1MAM01W', + name: 'John Doe', + username: 'johndoe123', + iss: env.AUTH_ISSUER, + iat: Math.floor(Date.now() / 1000), + exp: Math.floor(Date.now() / 1000) + 60 + } + + const payloadRefreshToken = { + sub: '01JHBDWAXFPAKAFK38E1MAM01W', + iss: 'not-Auth_issuer', + iat: Math.floor(Date.now() / 1000), + exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 30 + } + + const accessTokenGen = await sign(payloadAccessToken, env.USER_SECRET_KEY) + const refreshTokenGen = await sign( + payloadRefreshToken, + env.REFRESH_SECRET_KEY + ) + + const payload = JSON.stringify({ + accessToken: accessTokenGen + }) + + const res = await app.request( + '/users/auth/refresh', + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': 'mock-csrf-token', + 'User-Agent': 'Vitest', + Cookie: `refreshToken=${refreshTokenGen}; Max-Age=2332800; Path=/users/auth/refresh; HttpOnly; Secure; SameSite=Strict` + }, + body: payload + }, + env + ) + + const result = await res.json() + + expect(res.status).toBe(403) + expect(result).toStrictEqual({ + success: false, + message: 'Action Denied!' + }) + }) +}) diff --git a/tests/handlers/auth/refreshToken/Validation.failure.test.ts b/tests/handlers/auth/refreshToken/Validation.failure.test.ts new file mode 100644 index 0000000..487dac1 --- /dev/null +++ b/tests/handlers/auth/refreshToken/Validation.failure.test.ts @@ -0,0 +1,149 @@ +import { applyD1Migrations, env } from 'cloudflare:test' +import { users } from '@src/db/user.schema' +import app from '@src/index' +import { drizzle } from 'drizzle-orm/d1' +import { afterEach, beforeAll, describe, expect, test } from 'vitest' + +describe('Refresh Token Input Validation E2E', () => { + const db = drizzle(env.DB) + + beforeAll(async () => { + await applyD1Migrations(env.DB, env.TEST_MIGRATIONS) + }) + + afterEach(async () => { + await db.delete(users) + }) + + test('should fail with undefined accessToken', async () => { + const payload = JSON.stringify({ + accessToken: 123 + }) + + const res = await app.request( + '/users/auth/refresh', + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': 'mock-csrf-token', + 'User-Agent': 'Vitest', + Cookie: + 'refreshToken=324234234; Max-Age=2332800; Path=/users/auth/refresh; HttpOnly; Secure; SameSite=Strict' + }, + body: payload + }, + env + ) + + const result = await res.json() + + expect(res.status).toBe(400) + expect(result).toStrictEqual({ + success: false, + message: 'Validation failed', + cause: { + cause: 'is not string', + field: 'accessToken' + } + }) + }) + + test('should fail with accessToken not string', async () => { + const payload = JSON.stringify({}) + + const res = await app.request( + '/users/auth/refresh', + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': 'mock-csrf-token', + 'User-Agent': 'Vitest', + Cookie: + 'refreshToken=324234234; Max-Age=2332800; Path=/users/auth/refresh; HttpOnly; Secure; SameSite=Strict' + }, + body: payload + }, + env + ) + + const result = await res.json() + + expect(res.status).toBe(400) + expect(result).toStrictEqual({ + success: false, + message: 'Validation failed', + cause: { + cause: 'is not nullable', + field: 'accessToken' + } + }) + }) + + test('should fail with undefined refreshToken', async () => { + const payload = JSON.stringify({ + accessToken: '123' + }) + + const res = await app.request( + '/users/auth/refresh', + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': 'mock-csrf-token', + 'User-Agent': 'Vitest' + }, + body: payload + }, + env + ) + + const result = await res.json() + + expect(res.status).toBe(400) + expect(result).toStrictEqual({ + success: false, + message: 'Validation failed', + cause: { + cause: 'is not nullable', + field: 'refreshToken' + } + }) + }) + + test('should fail with less them min refreshToken', async () => { + const payload = JSON.stringify({ + accessToken: '123' + }) + + const res = await app.request( + '/users/auth/refresh', + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': 'mock-csrf-token', + 'User-Agent': 'Vitest', + Cookie: + 'refreshToken=; Max-Age=2332800; Path=/users/auth/refresh; HttpOnly; Secure; SameSite=Strict' + }, + body: payload + }, + env + ) + + const result = await res.json() + + expect(res.status).toBe(400) + expect(result).toStrictEqual({ + success: false, + message: 'Validation failed', + cause: { + cause: 'is not min', + field: 'refreshToken' + } + }) + }) +})