From a0f6087d3e48cb1670a8c0a856ab6776fc61f4b8 Mon Sep 17 00:00:00 2001 From: Noa Virellia Date: Thu, 29 Jan 2026 11:43:46 +0800 Subject: [PATCH] refactor(client): use generated API client and hooks Signed-off-by: Noa Virellia --- client/cms/openapi-ts.config.ts | 19 + client/cms/package.json | 4 +- client/cms/pnpm-lock.yaml | 327 +++++++- .../src/client/@tanstack/react-query.gen.ts | 353 +++++++++ client/cms/src/client/client.gen.ts | 16 + client/cms/src/client/client/client.gen.ts | 311 ++++++++ client/cms/src/client/client/index.ts | 25 + client/cms/src/client/client/types.gen.ts | 241 ++++++ client/cms/src/client/client/utils.gen.ts | 332 ++++++++ client/cms/src/client/core/auth.gen.ts | 42 ++ .../cms/src/client/core/bodySerializer.gen.ts | 100 +++ client/cms/src/client/core/params.gen.ts | 176 +++++ .../cms/src/client/core/pathSerializer.gen.ts | 181 +++++ .../src/client/core/queryKeySerializer.gen.ts | 136 ++++ .../src/client/core/serverSentEvents.gen.ts | 266 +++++++ client/cms/src/client/core/types.gen.ts | 118 +++ client/cms/src/client/core/utils.gen.ts | 143 ++++ client/cms/src/client/index.ts | 4 + client/cms/src/client/sdk.gen.ts | 153 ++++ client/cms/src/client/types.gen.ts | 713 ++++++++++++++++++ client/cms/src/client/zod.gen.ts | 280 +++++++ .../cms/src/components/checkin/qr-dialog.tsx | 28 +- client/cms/src/components/login-form.tsx | 2 +- .../profile/edit-profile-dialog.tsx | 5 +- .../src/components/profile/main-profile.tsx | 7 +- .../cms/src/components/sidebar/nav-user.tsx | 3 +- client/cms/src/hooks/data/useExchangeToken.ts | 16 + .../cms/src/hooks/data/useGetCheckInCode.ts | 18 - client/cms/src/hooks/data/useGetMagicLink.ts | 14 +- client/cms/src/hooks/data/useUpdateUser.ts | 18 +- client/cms/src/hooks/data/useUserInfo.ts | 20 +- .../src/hooks/data/useValidateMagicLink.ts | 11 - client/cms/src/lib/axios.ts | 67 -- client/cms/src/lib/client.ts | 63 ++ client/cms/src/lib/token.ts | 21 +- client/cms/src/main.tsx | 3 + client/cms/src/routes/authorize.tsx | 28 +- client/cms/src/routes/token.tsx | 29 +- 38 files changed, 4076 insertions(+), 217 deletions(-) create mode 100644 client/cms/openapi-ts.config.ts create mode 100644 client/cms/src/client/@tanstack/react-query.gen.ts create mode 100644 client/cms/src/client/client.gen.ts create mode 100644 client/cms/src/client/client/client.gen.ts create mode 100644 client/cms/src/client/client/index.ts create mode 100644 client/cms/src/client/client/types.gen.ts create mode 100644 client/cms/src/client/client/utils.gen.ts create mode 100644 client/cms/src/client/core/auth.gen.ts create mode 100644 client/cms/src/client/core/bodySerializer.gen.ts create mode 100644 client/cms/src/client/core/params.gen.ts create mode 100644 client/cms/src/client/core/pathSerializer.gen.ts create mode 100644 client/cms/src/client/core/queryKeySerializer.gen.ts create mode 100644 client/cms/src/client/core/serverSentEvents.gen.ts create mode 100644 client/cms/src/client/core/types.gen.ts create mode 100644 client/cms/src/client/core/utils.gen.ts create mode 100644 client/cms/src/client/index.ts create mode 100644 client/cms/src/client/sdk.gen.ts create mode 100644 client/cms/src/client/types.gen.ts create mode 100644 client/cms/src/client/zod.gen.ts create mode 100644 client/cms/src/hooks/data/useExchangeToken.ts delete mode 100644 client/cms/src/hooks/data/useGetCheckInCode.ts delete mode 100644 client/cms/src/hooks/data/useValidateMagicLink.ts delete mode 100644 client/cms/src/lib/axios.ts create mode 100644 client/cms/src/lib/client.ts diff --git a/client/cms/openapi-ts.config.ts b/client/cms/openapi-ts.config.ts new file mode 100644 index 0000000..c0d6eef --- /dev/null +++ b/client/cms/openapi-ts.config.ts @@ -0,0 +1,19 @@ +import { defineConfig } from '@hey-api/openapi-ts'; + +export default defineConfig({ + input: 'http://10.0.0.10:8000/swagger/doc.json', // sign up at app.heyapi.dev + output: 'src/client', + plugins: [ + '@hey-api/typescript', + '@tanstack/react-query', + 'zod', + { + name: '@hey-api/transformers', + dates: true, + }, + { + name: '@hey-api/sdk', + transformer: true, + }, + ], +}); diff --git a/client/cms/package.json b/client/cms/package.json index 266c7ff..b01e804 100644 --- a/client/cms/package.json +++ b/client/cms/package.json @@ -7,7 +7,8 @@ "dev": "vite", "build": "tsc -b && vite build", "lint": "eslint .", - "preview": "vite preview" + "preview": "vite preview", + "gen": "openapi-ts" }, "dependencies": { "@dnd-kit/core": "^6.3.1", @@ -66,6 +67,7 @@ "@antfu/eslint-config": "^6.7.1", "@eslint-react/eslint-plugin": "^2.3.13", "@eslint/js": "^9.39.1", + "@hey-api/openapi-ts": "0.91.0", "@tailwindcss/typography": "^0.5.19", "@tanstack/eslint-plugin-query": "^5.91.2", "@tanstack/router-plugin": "^1.141.7", diff --git a/client/cms/pnpm-lock.yaml b/client/cms/pnpm-lock.yaml index 7bbc249..1f4dcea 100644 --- a/client/cms/pnpm-lock.yaml +++ b/client/cms/pnpm-lock.yaml @@ -171,6 +171,9 @@ importers: '@eslint/js': specifier: ^9.39.1 version: 9.39.2 + '@hey-api/openapi-ts': + specifier: 0.91.0 + version: 0.91.0(typescript@5.9.3) '@tailwindcss/typography': specifier: ^0.5.19 version: 0.5.19(tailwindcss@4.1.18) @@ -712,6 +715,34 @@ packages: '@floating-ui/utils@0.2.10': resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} + '@hey-api/codegen-core@0.6.0': + resolution: {integrity: sha512-O6TRP3g1188gdpZC9yio64KkvWD97QpLjaCFnwTgykTZtzQXZbkyw+lYPVfZ7Ee+m7eLau1/jEJmQcUY9G0sLw==} + engines: {node: '>=20.19.0'} + peerDependencies: + typescript: '>=5.5.3' + + '@hey-api/json-schema-ref-parser@1.2.3': + resolution: {integrity: sha512-gRbyyTjzpFVNmbD+Upn3w4dWV+bCXGJbff3A7leDO/tfNxSz1xIb6Ad/5UKtvEW9kDt/2Uyc3XkFZ6rpafvbfQ==} + engines: {node: '>= 16'} + + '@hey-api/openapi-ts@0.91.0': + resolution: {integrity: sha512-AHkd982HsPz1XpqRm59URwJyJqTzyzzC30EAp07b/0M9KojjneCPxm8FnvFnXLRTMyKgcOymMsYXuLzJ9mpMHA==} + engines: {node: '>=20.19.0'} + hasBin: true + peerDependencies: + typescript: '>=5.5.3' + + '@hey-api/shared@0.1.0': + resolution: {integrity: sha512-qEDMSBWEEWxcBU5XHacjCCnFOVq1YWPPR3owURVep60I7ejfSG5OINxM4eF+p3KJGMcZduzzfq9pd1grStHZBg==} + engines: {node: '>=20.19.0'} + peerDependencies: + typescript: '>=5.5.3' + + '@hey-api/types@0.1.3': + resolution: {integrity: sha512-mZaiPOWH761yD4GjDQvtjS2ZYLu5o5pI1TVSvV/u7cmbybv51/FVtinFBeaE1kFQCKZ8OQpn2ezjLBJrKsGATw==} + peerDependencies: + typescript: '>=5.5.3' + '@hookform/resolvers@5.2.2': resolution: {integrity: sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA==} peerDependencies: @@ -749,6 +780,9 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@jsdevtools/ono@7.1.3': + resolution: {integrity: sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==} + '@marsidev/react-turnstile@1.4.1': resolution: {integrity: sha512-1jE0IjvB8z+q1NFRs3149gXzXwIzXQWqQjn9fmAr13BiE3RYLWck5Me6flHYE90shW5L12Jkm6R1peS1OnA9oQ==} peerDependencies: @@ -1266,79 +1300,66 @@ packages: resolution: {integrity: sha512-AEXMESUDWWGqD6LwO/HkqCZgUE1VCJ1OhbvYGsfqX2Y6w5quSXuyoy/Fg3nRqiwro+cJYFxiw5v4kB2ZDLhxrw==} cpu: [arm] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.55.2': resolution: {integrity: sha512-ZV7EljjBDwBBBSv570VWj0hiNTdHt9uGznDtznBB4Caj3ch5rgD4I2K1GQrtbvJ/QiB+663lLgOdcADMNVC29Q==} cpu: [arm] os: [linux] - libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.55.2': resolution: {integrity: sha512-uvjwc8NtQVPAJtq4Tt7Q49FOodjfbf6NpqXyW/rjXoV+iZ3EJAHLNAnKT5UJBc6ffQVgmXTUL2ifYiLABlGFqA==} cpu: [arm64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.55.2': resolution: {integrity: sha512-s3KoWVNnye9mm/2WpOZ3JeUiediUVw6AvY/H7jNA6qgKA2V2aM25lMkVarTDfiicn/DLq3O0a81jncXszoyCFA==} cpu: [arm64] os: [linux] - libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.55.2': resolution: {integrity: sha512-gi21faacK+J8aVSyAUptML9VQN26JRxe484IbF+h3hpG+sNVoMXPduhREz2CcYr5my0NE3MjVvQ5bMKX71pfVA==} cpu: [loong64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.55.2': resolution: {integrity: sha512-qSlWiXnVaS/ceqXNfnoFZh4IiCA0EwvCivivTGbEu1qv2o+WTHpn1zNmCTAoOG5QaVr2/yhCoLScQtc/7RxshA==} cpu: [loong64] os: [linux] - libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.55.2': resolution: {integrity: sha512-rPyuLFNoF1B0+wolH277E780NUKf+KoEDb3OyoLbAO18BbeKi++YN6gC/zuJoPPDlQRL3fIxHxCxVEWiem2yXw==} cpu: [ppc64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.55.2': resolution: {integrity: sha512-g+0ZLMook31iWV4PvqKU0i9E78gaZgYpSrYPed/4Bu+nGTgfOPtfs1h11tSSRPXSjC5EzLTjV/1A7L2Vr8pJoQ==} cpu: [ppc64] os: [linux] - libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.55.2': resolution: {integrity: sha512-i+sGeRGsjKZcQRh3BRfpLsM3LX3bi4AoEVqmGDyc50L6KfYsN45wVCSz70iQMwPWr3E5opSiLOwsC9WB4/1pqg==} cpu: [riscv64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.55.2': resolution: {integrity: sha512-C1vLcKc4MfFV6I0aWsC7B2Y9QcsiEcvKkfxprwkPfLaN8hQf0/fKHwSF2lcYzA9g4imqnhic729VB9Fo70HO3Q==} cpu: [riscv64] os: [linux] - libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.55.2': resolution: {integrity: sha512-68gHUK/howpQjh7g7hlD9DvTTt4sNLp1Bb+Yzw2Ki0xvscm2cOdCLZNJNhd2jW8lsTPrHAHuF751BygifW4bkQ==} cpu: [s390x] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.55.2': resolution: {integrity: sha512-1e30XAuaBP1MAizaOBApsgeGZge2/Byd6wV4a8oa6jPdHELbRHBiw7wvo4dp7Ie2PE8TZT4pj9RLGZv9N4qwlw==} cpu: [x64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-musl@4.55.2': resolution: {integrity: sha512-4BJucJBGbuGnH6q7kpPqGJGzZnYrpAzRd60HQSt3OpX/6/YVgSsJnNzR8Ot74io50SeVT4CtCWe/RYIAymFPwA==} cpu: [x64] os: [linux] - libc: [musl] '@rollup/rollup-openbsd-x64@4.55.2': resolution: {integrity: sha512-cT2MmXySMo58ENv8p6/O6wI/h/gLnD3D6JoajwXFZH6X9jz4hARqUhWpGuQhOgLNXscfZYRQMJvZDtWNzMAIDw==} @@ -1497,28 +1518,24 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.1.18': resolution: {integrity: sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.1.18': resolution: {integrity: sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.1.18': resolution: {integrity: sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [musl] '@tailwindcss/oxide-wasm32-wasi@4.1.18': resolution: {integrity: sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==} @@ -1924,6 +1941,10 @@ packages: ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + ansi-colors@4.1.3: + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} + ansi-escapes@7.2.0: resolution: {integrity: sha512-g6LhBsl+GBPRWGWsBtutpzBYuIIdBkLEvad5C/va/74Db018+5TZiyA26cZJAr3Rft5lprVqOIPxf5Vid6tqAw==} engines: {node: '>=18'} @@ -2027,6 +2048,18 @@ packages: resolution: {integrity: sha512-bkXY9WsVpY7CvMhKSR6pZilZu9Ln5WDrKVBUXf2S443etkmEO4V58heTecXcUIsNsi4Rx8JUO4NfX1IcQl4deg==} engines: {node: '>=18.20'} + bundle-name@4.1.0: + resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} + engines: {node: '>=18'} + + c12@3.3.3: + resolution: {integrity: sha512-750hTRvgBy5kcMNPdh95Qo+XUBeGo8C7nsKSmedDmaQI+E0r82DwHeM6vBewDe4rGFbnxoa4V9pw+sPh5+Iz8Q==} + peerDependencies: + magicast: '*' + peerDependenciesMeta: + magicast: + optional: true + cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} @@ -2076,10 +2109,20 @@ packages: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} + chokidar@5.0.0: + resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} + engines: {node: '>= 20.19.0'} + ci-info@4.3.1: resolution: {integrity: sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==} engines: {node: '>=8'} + citty@0.1.6: + resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==} + + citty@0.2.0: + resolution: {integrity: sha512-8csy5IBFI2ex2hTVpaHN2j+LNE199AgiI7y4dMintrr8i0lQiFn+0AWMZrWdHKIgMOer65f8IThysYhoReqjWA==} + class-variance-authority@0.7.1: resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} @@ -2109,6 +2152,10 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + color-support@1.1.3: + resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==} + hasBin: true + colorette@2.0.20: resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} @@ -2143,6 +2190,10 @@ packages: confbox@0.2.2: resolution: {integrity: sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==} + consola@3.4.2: + resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} + engines: {node: ^14.18.0 || >=16.10.0} + convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} @@ -2246,6 +2297,21 @@ packages: deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + default-browser-id@5.0.1: + resolution: {integrity: sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==} + engines: {node: '>=18'} + + default-browser@5.4.0: + resolution: {integrity: sha512-XDuvSq38Hr1MdN47EDvYtx3U0MTqpCEn+F6ft8z2vYDzMrvQhVp0ui9oQdqW3MvK3vqUETglt1tVGgjLuJ5izg==} + engines: {node: '>=18'} + + define-lazy-prop@3.0.0: + resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} + engines: {node: '>=12'} + + defu@6.1.4: + resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} @@ -2254,6 +2320,9 @@ packages: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} + destr@2.0.5: + resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} + detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} @@ -2285,6 +2354,10 @@ packages: dot-case@3.0.4: resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==} + dotenv@17.2.3: + resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==} + engines: {node: '>=12'} + dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -2737,6 +2810,10 @@ packages: get-tsconfig@4.13.0: resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==} + giget@2.0.0: + resolution: {integrity: sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==} + hasBin: true + github-slugger@2.0.0: resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==} @@ -2910,6 +2987,11 @@ packages: is-decimal@2.0.1: resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} + is-docker@3.0.0: + resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + hasBin: true + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -2935,6 +3017,15 @@ packages: eslint: '*' typescript: '>=4.7.4' + is-in-ssh@1.0.0: + resolution: {integrity: sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==} + engines: {node: '>=20'} + + is-inside-container@1.0.0: + resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} + engines: {node: '>=14.16'} + hasBin: true + is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} @@ -2943,6 +3034,10 @@ packages: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} + is-wsl@3.1.0: + resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==} + engines: {node: '>=16'} + isbot@5.1.33: resolution: {integrity: sha512-P4Hgb5NqswjkI0J1CM6XKXon/sxKY1SuowE7Qx2hrBhIwICFyXy54mfgB5eMHXsbe/eStzzpbIGNOvGmz+dlKg==} engines: {node: '>=18'} @@ -3037,28 +3132,24 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [glibc] lightningcss-linux-arm64-musl@1.30.2: resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [musl] lightningcss-linux-x64-gnu@1.30.2: resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [glibc] lightningcss-linux-x64-musl@1.30.2: resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [musl] lightningcss-win32-arm64-msvc@1.30.2: resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==} @@ -3330,6 +3421,9 @@ packages: no-case@3.0.4: resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} + node-fetch-native@1.6.7: + resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==} + node-releases@2.0.27: resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} @@ -3340,6 +3434,11 @@ packages: nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + nypm@0.6.4: + resolution: {integrity: sha512-1TvCKjZyyklN+JJj2TS3P4uSQEInrM/HkkuSXsEzm1ApPgBffOn8gFguNnZf07r/1X6vlryfIqMUkJKQMzlZiw==} + engines: {node: '>=18'} + hasBin: true + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -3347,10 +3446,17 @@ packages: object-deep-merge@2.0.0: resolution: {integrity: sha512-3DC3UMpeffLTHiuXSy/UG4NOIYTLlY9u3V82+djSCLYClWobZiS4ivYzpIUWrRY/nfsJ8cWsKyG3QfyLePmhvg==} + ohash@2.0.11: + resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} + onetime@7.0.0: resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} engines: {node: '>=18'} + open@11.0.0: + resolution: {integrity: sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==} + engines: {node: '>=20'} + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -3420,6 +3526,9 @@ packages: pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + perfect-debounce@2.1.0: + resolution: {integrity: sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -3465,6 +3574,10 @@ packages: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} + powershell-utils@0.1.0: + resolution: {integrity: sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==} + engines: {node: '>=20'} + prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -3498,6 +3611,9 @@ packages: quansync@0.2.11: resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} + rc9@2.1.2: + resolution: {integrity: sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==} + react-dom@19.2.3: resolution: {integrity: sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==} peerDependencies: @@ -3581,6 +3697,10 @@ packages: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} + readdirp@5.0.0: + resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} + engines: {node: '>= 20.19.0'} + recast@0.23.11: resolution: {integrity: sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==} engines: {node: '>= 4'} @@ -3696,6 +3816,10 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + run-applescript@7.1.0: + resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==} + engines: {node: '>=18'} + scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} @@ -4093,6 +4217,10 @@ packages: resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} engines: {node: '>=18'} + wsl-utils@0.3.1: + resolution: {integrity: sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==} + engines: {node: '>=20'} + xml-name-validator@4.0.0: resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} engines: {node: '>=12'} @@ -4635,6 +4763,53 @@ snapshots: '@floating-ui/utils@0.2.10': {} + '@hey-api/codegen-core@0.6.0(typescript@5.9.3)': + dependencies: + '@hey-api/types': 0.1.3(typescript@5.9.3) + ansi-colors: 4.1.3 + c12: 3.3.3 + color-support: 1.1.3 + typescript: 5.9.3 + transitivePeerDependencies: + - magicast + + '@hey-api/json-schema-ref-parser@1.2.3': + dependencies: + '@jsdevtools/ono': 7.1.3 + '@types/json-schema': 7.0.15 + js-yaml: 4.1.1 + lodash: 4.17.21 + + '@hey-api/openapi-ts@0.91.0(typescript@5.9.3)': + dependencies: + '@hey-api/codegen-core': 0.6.0(typescript@5.9.3) + '@hey-api/json-schema-ref-parser': 1.2.3 + '@hey-api/shared': 0.1.0(typescript@5.9.3) + '@hey-api/types': 0.1.3(typescript@5.9.3) + ansi-colors: 4.1.3 + color-support: 1.1.3 + commander: 14.0.2 + typescript: 5.9.3 + transitivePeerDependencies: + - magicast + + '@hey-api/shared@0.1.0(typescript@5.9.3)': + dependencies: + '@hey-api/codegen-core': 0.6.0(typescript@5.9.3) + '@hey-api/json-schema-ref-parser': 1.2.3 + '@hey-api/types': 0.1.3(typescript@5.9.3) + ansi-colors: 4.1.3 + cross-spawn: 7.0.6 + open: 11.0.0 + semver: 7.7.3 + typescript: 5.9.3 + transitivePeerDependencies: + - magicast + + '@hey-api/types@0.1.3(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + '@hookform/resolvers@5.2.2(react-hook-form@7.71.1(react@19.2.3))': dependencies: '@standard-schema/utils': 0.3.0 @@ -4670,6 +4845,8 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@jsdevtools/ono@7.1.3': {} + '@marsidev/react-turnstile@1.4.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: react: 19.2.3 @@ -5830,6 +6007,8 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 + ansi-colors@4.1.3: {} + ansi-escapes@7.2.0: dependencies: environment: 1.1.0 @@ -5928,6 +6107,25 @@ snapshots: builtin-modules@5.0.0: {} + bundle-name@4.1.0: + dependencies: + run-applescript: 7.1.0 + + c12@3.3.3: + dependencies: + chokidar: 5.0.0 + confbox: 0.2.2 + defu: 6.1.4 + dotenv: 17.2.3 + exsolve: 1.0.8 + giget: 2.0.0 + jiti: 2.6.1 + ohash: 2.0.11 + pathe: 2.0.3 + perfect-debounce: 2.1.0 + pkg-types: 2.3.0 + rc9: 2.1.2 + cac@6.7.14: {} call-bind-apply-helpers@1.0.2: @@ -5972,8 +6170,18 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + chokidar@5.0.0: + dependencies: + readdirp: 5.0.0 + ci-info@4.3.1: {} + citty@0.1.6: + dependencies: + consola: 3.4.2 + + citty@0.2.0: {} + class-variance-authority@0.7.1: dependencies: clsx: 2.1.1 @@ -6005,6 +6213,8 @@ snapshots: color-name@1.1.4: {} + color-support@1.1.3: {} + colorette@2.0.20: {} combined-stream@1.0.8: @@ -6027,6 +6237,8 @@ snapshots: confbox@0.2.2: {} + consola@3.4.2: {} + convert-source-map@2.0.0: {} cookie-es@2.0.0: {} @@ -6110,10 +6322,23 @@ snapshots: deep-is@0.1.4: {} + default-browser-id@5.0.1: {} + + default-browser@5.4.0: + dependencies: + bundle-name: 4.1.0 + default-browser-id: 5.0.1 + + define-lazy-prop@3.0.0: {} + + defu@6.1.4: {} + delayed-stream@1.0.0: {} dequal@2.0.3: {} + destr@2.0.5: {} + detect-libc@2.1.2: {} detect-node-es@1.1.0: {} @@ -6140,6 +6365,8 @@ snapshots: no-case: 3.0.4 tslib: 2.8.1 + dotenv@17.2.3: {} + dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -6721,6 +6948,15 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 + giget@2.0.0: + dependencies: + citty: 0.1.6 + consola: 3.4.2 + defu: 6.1.4 + node-fetch-native: 1.6.7 + nypm: 0.6.4 + pathe: 2.0.3 + github-slugger@2.0.0: {} glob-parent@5.1.2: @@ -6955,6 +7191,8 @@ snapshots: is-decimal@2.0.1: {} + is-docker@3.0.0: {} + is-extglob@2.1.1: {} is-fullwidth-code-point@3.0.0: {} @@ -6979,10 +7217,20 @@ snapshots: transitivePeerDependencies: - supports-color + is-in-ssh@1.0.0: {} + + is-inside-container@1.0.0: + dependencies: + is-docker: 3.0.0 + is-number@7.0.0: {} is-plain-obj@4.1.0: {} + is-wsl@3.1.0: + dependencies: + is-inside-container: 1.0.0 + isbot@5.1.33: {} isexe@2.0.0: {} @@ -7561,6 +7809,8 @@ snapshots: lower-case: 2.0.2 tslib: 2.8.1 + node-fetch-native@1.6.7: {} + node-releases@2.0.27: {} normalize-path@3.0.0: {} @@ -7569,14 +7819,31 @@ snapshots: dependencies: boolbase: 1.0.0 + nypm@0.6.4: + dependencies: + citty: 0.2.0 + pathe: 2.0.3 + tinyexec: 1.0.2 + object-assign@4.1.1: {} object-deep-merge@2.0.0: {} + ohash@2.0.11: {} + onetime@7.0.0: dependencies: mimic-function: 5.0.1 + open@11.0.0: + dependencies: + default-browser: 5.4.0 + define-lazy-prop: 3.0.0 + is-in-ssh: 1.0.0 + is-inside-container: 1.0.0 + powershell-utils: 0.1.0 + wsl-utils: 0.3.1 + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -7649,6 +7916,8 @@ snapshots: pathe@2.0.3: {} + perfect-debounce@2.1.0: {} + picocolors@1.1.1: {} picomatch@2.3.1: {} @@ -7693,6 +7962,8 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + powershell-utils@0.1.0: {} + prelude-ls@1.2.1: {} prettier@3.8.0: {} @@ -7719,6 +7990,11 @@ snapshots: quansync@0.2.11: {} + rc9@2.1.2: + dependencies: + defu: 6.1.4 + destr: 2.0.5 + react-dom@19.2.3(react@19.2.3): dependencies: react: 19.2.3 @@ -7819,6 +8095,8 @@ snapshots: dependencies: picomatch: 2.3.1 + readdirp@5.0.0: {} + recast@0.23.11: dependencies: ast-types: 0.16.1 @@ -8029,6 +8307,8 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.55.2 fsevents: 2.3.3 + run-applescript@7.1.0: {} + scheduler@0.27.0: {} scslre@0.3.0: @@ -8415,6 +8695,11 @@ snapshots: string-width: 7.2.0 strip-ansi: 7.1.2 + wsl-utils@0.3.1: + dependencies: + is-wsl: 3.1.0 + powershell-utils: 0.1.0 + xml-name-validator@4.0.0: {} y18n@4.0.3: {} diff --git a/client/cms/src/client/@tanstack/react-query.gen.ts b/client/cms/src/client/@tanstack/react-query.gen.ts new file mode 100644 index 0000000..6c77868 --- /dev/null +++ b/client/cms/src/client/@tanstack/react-query.gen.ts @@ -0,0 +1,353 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { type InfiniteData, infiniteQueryOptions, queryOptions, type UseMutationOptions } from '@tanstack/react-query'; + +import { client } from '../client.gen'; +import { getAuthRedirect, getEventCheckin, getEventCheckinQuery, getEventInfo, getUserFull, getUserInfo, getUserList, type Options, patchUserUpdate, postAuthExchange, postAuthMagic, postAuthRefresh, postAuthToken, postEventCheckinSubmit } from '../sdk.gen'; +import type { GetAuthRedirectData, GetAuthRedirectError, GetEventCheckinData, GetEventCheckinError, GetEventCheckinQueryData, GetEventCheckinQueryError, GetEventCheckinQueryResponse, GetEventCheckinResponse, GetEventInfoData, GetEventInfoError, GetEventInfoResponse, GetUserFullData, GetUserFullError, GetUserFullResponse, GetUserInfoData, GetUserInfoError, GetUserInfoResponse, GetUserListData, GetUserListError, GetUserListResponse, PatchUserUpdateData, PatchUserUpdateError, PatchUserUpdateResponse, PostAuthExchangeData, PostAuthExchangeError, PostAuthExchangeResponse, PostAuthMagicData, PostAuthMagicError, PostAuthMagicResponse, PostAuthRefreshData, PostAuthRefreshError, PostAuthRefreshResponse, PostAuthTokenData, PostAuthTokenError, PostAuthTokenResponse, PostEventCheckinSubmitData, PostEventCheckinSubmitError, PostEventCheckinSubmitResponse } from '../types.gen'; + +/** + * Exchange Auth Code + * + * Exchanges client credentials and user session for a specific redirect authorization code. + */ +export const postAuthExchangeMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await postAuthExchange({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; +}; + +/** + * Request Magic Link + * + * Verifies Turnstile token and sends an authentication link via email. Returns the URI directly if debug mode is enabled. + */ +export const postAuthMagicMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await postAuthMagic({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; +}; + +export type QueryKey = [ + Pick & { + _id: string; + _infinite?: boolean; + tags?: ReadonlyArray; + } +]; + +const createQueryKey = (id: string, options?: TOptions, infinite?: boolean, tags?: ReadonlyArray): [ + QueryKey[0] +] => { + const params: QueryKey[0] = { _id: id, baseUrl: options?.baseUrl || (options?.client ?? client).getConfig().baseUrl } as QueryKey[0]; + if (infinite) { + params._infinite = infinite; + } + if (tags) { + params.tags = tags; + } + if (options?.body) { + params.body = options.body; + } + if (options?.headers) { + params.headers = options.headers; + } + if (options?.path) { + params.path = options.path; + } + if (options?.query) { + params.query = options.query; + } + return [params]; +}; + +export const getAuthRedirectQueryKey = (options: Options) => createQueryKey('getAuthRedirect', options); + +/** + * Handle Auth Callback and Redirect + * + * Verifies the temporary email code, ensures the user exists (or creates one), validates the client's redirect URI, and finally performs a 302 redirect with a new authorization code. + */ +export const getAuthRedirectOptions = (options: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getAuthRedirect({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: getAuthRedirectQueryKey(options) +}); + +/** + * Refresh Access Token + * + * Accepts a valid refresh token to issue a new access token and a rotated refresh token. + */ +export const postAuthRefreshMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await postAuthRefresh({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; +}; + +/** + * Exchange Code for Token + * + * Verifies the provided authorization code and issues a pair of JWT tokens (Access and Refresh). + */ +export const postAuthTokenMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await postAuthToken({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; +}; + +export const getEventCheckinQueryKey = (options: Options) => createQueryKey('getEventCheckin', options); + +/** + * Generate Check-in Code + * + * Creates a temporary check-in code for the authenticated user and event. + */ +export const getEventCheckinOptions = (options: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getEventCheckin({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: getEventCheckinQueryKey(options) +}); + +export const getEventCheckinQueryQueryKey = (options: Options) => createQueryKey('getEventCheckinQuery', options); + +/** + * Query Check-in Status + * + * Returns the timestamp of when the user checked in, or null if not yet checked in. + */ +export const getEventCheckinQueryOptions = (options: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getEventCheckinQuery({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: getEventCheckinQueryQueryKey(options) +}); + +/** + * Submit Check-in Code + * + * Submits the generated code to mark the user as attended. + */ +export const postEventCheckinSubmitMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await postEventCheckinSubmit({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; +}; + +export const getEventInfoQueryKey = (options: Options) => createQueryKey('getEventInfo', options); + +/** + * Get Event Information + * + * Fetches the name, start time, and end time of an event using its UUID. + */ +export const getEventInfoOptions = (options: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getEventInfo({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: getEventInfoQueryKey(options) +}); + +export const getUserFullQueryKey = (options?: Options) => createQueryKey('getUserFull', options); + +/** + * Get Full User Table + * + * Fetches all user records without pagination. This is typically used for administrative overview or data export. + */ +export const getUserFullOptions = (options?: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getUserFull({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: getUserFullQueryKey(options) +}); + +export const getUserInfoQueryKey = (options?: Options) => createQueryKey('getUserInfo', options); + +/** + * Get My User Information + * + * Fetches the complete profile data for the user associated with the provided session/token. + */ +export const getUserInfoOptions = (options?: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getUserInfo({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: getUserInfoQueryKey(options) +}); + +export const getUserListQueryKey = (options: Options) => createQueryKey('getUserList', options); + +/** + * List Users + * + * Fetches a list of users with support for pagination via limit and offset. Data is sourced from the search engine for high performance. + */ +export const getUserListOptions = (options: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getUserList({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: getUserListQueryKey(options) +}); + +const createInfiniteParams = [0], 'body' | 'headers' | 'path' | 'query'>>(queryKey: QueryKey, page: K) => { + const params = { ...queryKey[0] }; + if (page.body) { + params.body = { + ...queryKey[0].body as any, + ...page.body as any + }; + } + if (page.headers) { + params.headers = { + ...queryKey[0].headers, + ...page.headers + }; + } + if (page.path) { + params.path = { + ...queryKey[0].path as any, + ...page.path as any + }; + } + if (page.query) { + params.query = { + ...queryKey[0].query as any, + ...page.query as any + }; + } + return params as unknown as typeof page; +}; + +export const getUserListInfiniteQueryKey = (options: Options): QueryKey> => createQueryKey('getUserList', options, true); + +/** + * List Users + * + * Fetches a list of users with support for pagination via limit and offset. Data is sourced from the search engine for high performance. + */ +export const getUserListInfiniteOptions = (options: Options) => infiniteQueryOptions, QueryKey>, string | Pick>[0], 'body' | 'headers' | 'path' | 'query'>>( +// @ts-ignore +{ + queryFn: async ({ pageParam, queryKey, signal }) => { + // @ts-ignore + const page: Pick>[0], 'body' | 'headers' | 'path' | 'query'> = typeof pageParam === 'object' ? pageParam : { + query: { + offset: pageParam + } + }; + const params = createInfiniteParams(queryKey, page); + const { data } = await getUserList({ + ...options, + ...params, + signal, + throwOnError: true + }); + return data; + }, + queryKey: getUserListInfiniteQueryKey(options) +}); + +/** + * Update User Information + * + * Updates specific profile fields such as username, nickname, subtitle, avatar (URL), and bio (Base64). + * Validation: Username (5-255 chars), Nickname (max 24 chars), Subtitle (max 32 chars). + */ +export const patchUserUpdateMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await patchUserUpdate({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; +}; diff --git a/client/cms/src/client/client.gen.ts b/client/cms/src/client/client.gen.ts new file mode 100644 index 0000000..36ba69f --- /dev/null +++ b/client/cms/src/client/client.gen.ts @@ -0,0 +1,16 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { type ClientOptions, type Config, createClient, createConfig } from './client'; +import type { ClientOptions as ClientOptions2 } from './types.gen'; + +/** + * The `createClientConfig()` function will be called on client initialization + * and the returned object will become the client's initial configuration. + * + * You may want to initialize your client this way instead of calling + * `setConfig()`. This is useful for example if you're using Next.js + * to ensure your client always has the correct values. + */ +export type CreateClientConfig = (override?: Config) => Config & T>; + +export const client = createClient(createConfig({ baseUrl: 'http://localhost:8000/api/v1' })); diff --git a/client/cms/src/client/client/client.gen.ts b/client/cms/src/client/client/client.gen.ts new file mode 100644 index 0000000..d4cbcce --- /dev/null +++ b/client/cms/src/client/client/client.gen.ts @@ -0,0 +1,311 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { createSseClient } from '../core/serverSentEvents.gen'; +import type { HttpMethod } from '../core/types.gen'; +import { getValidRequestBody } from '../core/utils.gen'; +import type { + Client, + Config, + RequestOptions, + ResolvedRequestOptions, +} from './types.gen'; +import { + buildUrl, + createConfig, + createInterceptors, + getParseAs, + mergeConfigs, + mergeHeaders, + setAuthParams, +} from './utils.gen'; + +type ReqInit = Omit & { + body?: any; + headers: ReturnType; +}; + +export const createClient = (config: Config = {}): Client => { + let _config = mergeConfigs(createConfig(), config); + + const getConfig = (): Config => ({ ..._config }); + + const setConfig = (config: Config): Config => { + _config = mergeConfigs(_config, config); + return getConfig(); + }; + + const interceptors = createInterceptors< + Request, + Response, + unknown, + ResolvedRequestOptions + >(); + + const beforeRequest = async (options: RequestOptions) => { + const opts = { + ..._config, + ...options, + fetch: options.fetch ?? _config.fetch ?? globalThis.fetch, + headers: mergeHeaders(_config.headers, options.headers), + serializedBody: undefined, + }; + + if (opts.security) { + await setAuthParams({ + ...opts, + security: opts.security, + }); + } + + if (opts.requestValidator) { + await opts.requestValidator(opts); + } + + if (opts.body !== undefined && opts.bodySerializer) { + opts.serializedBody = opts.bodySerializer(opts.body); + } + + // remove Content-Type header if body is empty to avoid sending invalid requests + if (opts.body === undefined || opts.serializedBody === '') { + opts.headers.delete('Content-Type'); + } + + const url = buildUrl(opts); + + return { opts, url }; + }; + + const request: Client['request'] = async (options) => { + // @ts-expect-error + const { opts, url } = await beforeRequest(options); + const requestInit: ReqInit = { + redirect: 'follow', + ...opts, + body: getValidRequestBody(opts), + }; + + let request = new Request(url, requestInit); + + for (const fn of interceptors.request.fns) { + if (fn) { + request = await fn(request, opts); + } + } + + // fetch must be assigned here, otherwise it would throw the error: + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation + const _fetch = opts.fetch!; + let response: Response; + + try { + response = await _fetch(request); + } catch (error) { + // Handle fetch exceptions (AbortError, network errors, etc.) + let finalError = error; + + for (const fn of interceptors.error.fns) { + if (fn) { + finalError = (await fn( + error, + undefined as any, + request, + opts, + )) as unknown; + } + } + + finalError = finalError || ({} as unknown); + + if (opts.throwOnError) { + throw finalError; + } + + // Return error response + return opts.responseStyle === 'data' + ? undefined + : { + error: finalError, + request, + response: undefined as any, + }; + } + + for (const fn of interceptors.response.fns) { + if (fn) { + response = await fn(response, request, opts); + } + } + + const result = { + request, + response, + }; + + if (response.ok) { + const parseAs = + (opts.parseAs === 'auto' + ? getParseAs(response.headers.get('Content-Type')) + : opts.parseAs) ?? 'json'; + + if ( + response.status === 204 || + response.headers.get('Content-Length') === '0' + ) { + let emptyData: any; + switch (parseAs) { + case 'arrayBuffer': + case 'blob': + case 'text': + emptyData = await response[parseAs](); + break; + case 'formData': + emptyData = new FormData(); + break; + case 'stream': + emptyData = response.body; + break; + case 'json': + default: + emptyData = {}; + break; + } + return opts.responseStyle === 'data' + ? emptyData + : { + data: emptyData, + ...result, + }; + } + + let data: any; + switch (parseAs) { + case 'arrayBuffer': + case 'blob': + case 'formData': + case 'text': + data = await response[parseAs](); + break; + case 'json': { + // Some servers return 200 with no Content-Length and empty body. + // response.json() would throw; read as text and parse if non-empty. + const text = await response.text(); + data = text ? JSON.parse(text) : {}; + break; + } + case 'stream': + return opts.responseStyle === 'data' + ? response.body + : { + data: response.body, + ...result, + }; + } + + if (parseAs === 'json') { + if (opts.responseValidator) { + await opts.responseValidator(data); + } + + if (opts.responseTransformer) { + data = await opts.responseTransformer(data); + } + } + + return opts.responseStyle === 'data' + ? data + : { + data, + ...result, + }; + } + + const textError = await response.text(); + let jsonError: unknown; + + try { + jsonError = JSON.parse(textError); + } catch { + // noop + } + + const error = jsonError ?? textError; + let finalError = error; + + for (const fn of interceptors.error.fns) { + if (fn) { + finalError = (await fn(error, response, request, opts)) as string; + } + } + + finalError = finalError || ({} as string); + + if (opts.throwOnError) { + throw finalError; + } + + // TODO: we probably want to return error and improve types + return opts.responseStyle === 'data' + ? undefined + : { + error: finalError, + ...result, + }; + }; + + const makeMethodFn = + (method: Uppercase) => (options: RequestOptions) => + request({ ...options, method }); + + const makeSseFn = + (method: Uppercase) => async (options: RequestOptions) => { + const { opts, url } = await beforeRequest(options); + return createSseClient({ + ...opts, + body: opts.body as BodyInit | null | undefined, + headers: opts.headers as unknown as Record, + method, + onRequest: async (url, init) => { + let request = new Request(url, init); + for (const fn of interceptors.request.fns) { + if (fn) { + request = await fn(request, opts); + } + } + return request; + }, + serializedBody: getValidRequestBody(opts) as + | BodyInit + | null + | undefined, + url, + }); + }; + + return { + buildUrl, + connect: makeMethodFn('CONNECT'), + delete: makeMethodFn('DELETE'), + get: makeMethodFn('GET'), + getConfig, + head: makeMethodFn('HEAD'), + interceptors, + options: makeMethodFn('OPTIONS'), + patch: makeMethodFn('PATCH'), + post: makeMethodFn('POST'), + put: makeMethodFn('PUT'), + request, + setConfig, + sse: { + connect: makeSseFn('CONNECT'), + delete: makeSseFn('DELETE'), + get: makeSseFn('GET'), + head: makeSseFn('HEAD'), + options: makeSseFn('OPTIONS'), + patch: makeSseFn('PATCH'), + post: makeSseFn('POST'), + put: makeSseFn('PUT'), + trace: makeSseFn('TRACE'), + }, + trace: makeMethodFn('TRACE'), + } as Client; +}; diff --git a/client/cms/src/client/client/index.ts b/client/cms/src/client/client/index.ts new file mode 100644 index 0000000..b295ede --- /dev/null +++ b/client/cms/src/client/client/index.ts @@ -0,0 +1,25 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type { Auth } from '../core/auth.gen'; +export type { QuerySerializerOptions } from '../core/bodySerializer.gen'; +export { + formDataBodySerializer, + jsonBodySerializer, + urlSearchParamsBodySerializer, +} from '../core/bodySerializer.gen'; +export { buildClientParams } from '../core/params.gen'; +export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen'; +export { createClient } from './client.gen'; +export type { + Client, + ClientOptions, + Config, + CreateClientConfig, + Options, + RequestOptions, + RequestResult, + ResolvedRequestOptions, + ResponseStyle, + TDataShape, +} from './types.gen'; +export { createConfig, mergeHeaders } from './utils.gen'; diff --git a/client/cms/src/client/client/types.gen.ts b/client/cms/src/client/client/types.gen.ts new file mode 100644 index 0000000..b4a499c --- /dev/null +++ b/client/cms/src/client/client/types.gen.ts @@ -0,0 +1,241 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Auth } from '../core/auth.gen'; +import type { + ServerSentEventsOptions, + ServerSentEventsResult, +} from '../core/serverSentEvents.gen'; +import type { + Client as CoreClient, + Config as CoreConfig, +} from '../core/types.gen'; +import type { Middleware } from './utils.gen'; + +export type ResponseStyle = 'data' | 'fields'; + +export interface Config + extends Omit, + CoreConfig { + /** + * Base URL for all requests made by this client. + */ + baseUrl?: T['baseUrl']; + /** + * Fetch API implementation. You can use this option to provide a custom + * fetch instance. + * + * @default globalThis.fetch + */ + fetch?: typeof fetch; + /** + * Please don't use the Fetch client for Next.js applications. The `next` + * options won't have any effect. + * + * Install {@link https://www.npmjs.com/package/@hey-api/client-next `@hey-api/client-next`} instead. + */ + next?: never; + /** + * Return the response data parsed in a specified format. By default, `auto` + * will infer the appropriate method from the `Content-Type` response header. + * You can override this behavior with any of the {@link Body} methods. + * Select `stream` if you don't want to parse response data at all. + * + * @default 'auto' + */ + parseAs?: + | 'arrayBuffer' + | 'auto' + | 'blob' + | 'formData' + | 'json' + | 'stream' + | 'text'; + /** + * Should we return only data or multiple fields (data, error, response, etc.)? + * + * @default 'fields' + */ + responseStyle?: ResponseStyle; + /** + * Throw an error instead of returning it in the response? + * + * @default false + */ + throwOnError?: T['throwOnError']; +} + +export interface RequestOptions< + TData = unknown, + TResponseStyle extends ResponseStyle = 'fields', + ThrowOnError extends boolean = boolean, + Url extends string = string, +> extends Config<{ + responseStyle: TResponseStyle; + throwOnError: ThrowOnError; + }>, + Pick< + ServerSentEventsOptions, + | 'onSseError' + | 'onSseEvent' + | 'sseDefaultRetryDelay' + | 'sseMaxRetryAttempts' + | 'sseMaxRetryDelay' + > { + /** + * Any body that you want to add to your request. + * + * {@link https://developer.mozilla.org/docs/Web/API/fetch#body} + */ + body?: unknown; + path?: Record; + query?: Record; + /** + * Security mechanism(s) to use for the request. + */ + security?: ReadonlyArray; + url: Url; +} + +export interface ResolvedRequestOptions< + TResponseStyle extends ResponseStyle = 'fields', + ThrowOnError extends boolean = boolean, + Url extends string = string, +> extends RequestOptions { + serializedBody?: string; +} + +export type RequestResult< + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = boolean, + TResponseStyle extends ResponseStyle = 'fields', +> = ThrowOnError extends true + ? Promise< + TResponseStyle extends 'data' + ? TData extends Record + ? TData[keyof TData] + : TData + : { + data: TData extends Record + ? TData[keyof TData] + : TData; + request: Request; + response: Response; + } + > + : Promise< + TResponseStyle extends 'data' + ? + | (TData extends Record + ? TData[keyof TData] + : TData) + | undefined + : ( + | { + data: TData extends Record + ? TData[keyof TData] + : TData; + error: undefined; + } + | { + data: undefined; + error: TError extends Record + ? TError[keyof TError] + : TError; + } + ) & { + request: Request; + response: Response; + } + >; + +export interface ClientOptions { + baseUrl?: string; + responseStyle?: ResponseStyle; + throwOnError?: boolean; +} + +type MethodFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = 'fields', +>( + options: Omit, 'method'>, +) => RequestResult; + +type SseFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = 'fields', +>( + options: Omit, 'method'>, +) => Promise>; + +type RequestFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = 'fields', +>( + options: Omit, 'method'> & + Pick< + Required>, + 'method' + >, +) => RequestResult; + +type BuildUrlFn = < + TData extends { + body?: unknown; + path?: Record; + query?: Record; + url: string; + }, +>( + options: TData & Options, +) => string; + +export type Client = CoreClient< + RequestFn, + Config, + MethodFn, + BuildUrlFn, + SseFn +> & { + interceptors: Middleware; +}; + +/** + * The `createClientConfig()` function will be called on client initialization + * and the returned object will become the client's initial configuration. + * + * You may want to initialize your client this way instead of calling + * `setConfig()`. This is useful for example if you're using Next.js + * to ensure your client always has the correct values. + */ +export type CreateClientConfig = ( + override?: Config, +) => Config & T>; + +export interface TDataShape { + body?: unknown; + headers?: unknown; + path?: unknown; + query?: unknown; + url: string; +} + +type OmitKeys = Pick>; + +export type Options< + TData extends TDataShape = TDataShape, + ThrowOnError extends boolean = boolean, + TResponse = unknown, + TResponseStyle extends ResponseStyle = 'fields', +> = OmitKeys< + RequestOptions, + 'body' | 'path' | 'query' | 'url' +> & + ([TData] extends [never] ? unknown : Omit); diff --git a/client/cms/src/client/client/utils.gen.ts b/client/cms/src/client/client/utils.gen.ts new file mode 100644 index 0000000..4c48a9e --- /dev/null +++ b/client/cms/src/client/client/utils.gen.ts @@ -0,0 +1,332 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { getAuthToken } from '../core/auth.gen'; +import type { QuerySerializerOptions } from '../core/bodySerializer.gen'; +import { jsonBodySerializer } from '../core/bodySerializer.gen'; +import { + serializeArrayParam, + serializeObjectParam, + serializePrimitiveParam, +} from '../core/pathSerializer.gen'; +import { getUrl } from '../core/utils.gen'; +import type { Client, ClientOptions, Config, RequestOptions } from './types.gen'; + +export const createQuerySerializer = ({ + parameters = {}, + ...args +}: QuerySerializerOptions = {}) => { + const querySerializer = (queryParams: T) => { + const search: string[] = []; + if (queryParams && typeof queryParams === 'object') { + for (const name in queryParams) { + const value = queryParams[name]; + + if (value === undefined || value === null) { + continue; + } + + const options = parameters[name] || args; + + if (Array.isArray(value)) { + const serializedArray = serializeArrayParam({ + allowReserved: options.allowReserved, + explode: true, + name, + style: 'form', + value, + ...options.array, + }); + if (serializedArray) search.push(serializedArray); + } else if (typeof value === 'object') { + const serializedObject = serializeObjectParam({ + allowReserved: options.allowReserved, + explode: true, + name, + style: 'deepObject', + value: value as Record, + ...options.object, + }); + if (serializedObject) search.push(serializedObject); + } else { + const serializedPrimitive = serializePrimitiveParam({ + allowReserved: options.allowReserved, + name, + value: value as string, + }); + if (serializedPrimitive) search.push(serializedPrimitive); + } + } + } + return search.join('&'); + }; + return querySerializer; +}; + +/** + * Infers parseAs value from provided Content-Type header. + */ +export const getParseAs = ( + contentType: string | null, +): Exclude => { + if (!contentType) { + // If no Content-Type header is provided, the best we can do is return the raw response body, + // which is effectively the same as the 'stream' option. + return 'stream'; + } + + const cleanContent = contentType.split(';')[0]?.trim(); + + if (!cleanContent) { + return; + } + + if ( + cleanContent.startsWith('application/json') || + cleanContent.endsWith('+json') + ) { + return 'json'; + } + + if (cleanContent === 'multipart/form-data') { + return 'formData'; + } + + if ( + ['application/', 'audio/', 'image/', 'video/'].some((type) => + cleanContent.startsWith(type), + ) + ) { + return 'blob'; + } + + if (cleanContent.startsWith('text/')) { + return 'text'; + } + + return; +}; + +const checkForExistence = ( + options: Pick & { + headers: Headers; + }, + name?: string, +): boolean => { + if (!name) { + return false; + } + if ( + options.headers.has(name) || + options.query?.[name] || + options.headers.get('Cookie')?.includes(`${name}=`) + ) { + return true; + } + return false; +}; + +export const setAuthParams = async ({ + security, + ...options +}: Pick, 'security'> & + Pick & { + headers: Headers; + }) => { + for (const auth of security) { + if (checkForExistence(options, auth.name)) { + continue; + } + + const token = await getAuthToken(auth, options.auth); + + if (!token) { + continue; + } + + const name = auth.name ?? 'Authorization'; + + switch (auth.in) { + case 'query': + if (!options.query) { + options.query = {}; + } + options.query[name] = token; + break; + case 'cookie': + options.headers.append('Cookie', `${name}=${token}`); + break; + case 'header': + default: + options.headers.set(name, token); + break; + } + } +}; + +export const buildUrl: Client['buildUrl'] = (options) => + getUrl({ + baseUrl: options.baseUrl as string, + path: options.path, + query: options.query, + querySerializer: + typeof options.querySerializer === 'function' + ? options.querySerializer + : createQuerySerializer(options.querySerializer), + url: options.url, + }); + +export const mergeConfigs = (a: Config, b: Config): Config => { + const config = { ...a, ...b }; + if (config.baseUrl?.endsWith('/')) { + config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1); + } + config.headers = mergeHeaders(a.headers, b.headers); + return config; +}; + +const headersEntries = (headers: Headers): Array<[string, string]> => { + const entries: Array<[string, string]> = []; + headers.forEach((value, key) => { + entries.push([key, value]); + }); + return entries; +}; + +export const mergeHeaders = ( + ...headers: Array['headers'] | undefined> +): Headers => { + const mergedHeaders = new Headers(); + for (const header of headers) { + if (!header) { + continue; + } + + const iterator = + header instanceof Headers + ? headersEntries(header) + : Object.entries(header); + + for (const [key, value] of iterator) { + if (value === null) { + mergedHeaders.delete(key); + } else if (Array.isArray(value)) { + for (const v of value) { + mergedHeaders.append(key, v as string); + } + } else if (value !== undefined) { + // assume object headers are meant to be JSON stringified, i.e. their + // content value in OpenAPI specification is 'application/json' + mergedHeaders.set( + key, + typeof value === 'object' ? JSON.stringify(value) : (value as string), + ); + } + } + } + return mergedHeaders; +}; + +type ErrInterceptor = ( + error: Err, + response: Res, + request: Req, + options: Options, +) => Err | Promise; + +type ReqInterceptor = ( + request: Req, + options: Options, +) => Req | Promise; + +type ResInterceptor = ( + response: Res, + request: Req, + options: Options, +) => Res | Promise; + +class Interceptors { + fns: Array = []; + + clear(): void { + this.fns = []; + } + + eject(id: number | Interceptor): void { + const index = this.getInterceptorIndex(id); + if (this.fns[index]) { + this.fns[index] = null; + } + } + + exists(id: number | Interceptor): boolean { + const index = this.getInterceptorIndex(id); + return Boolean(this.fns[index]); + } + + getInterceptorIndex(id: number | Interceptor): number { + if (typeof id === 'number') { + return this.fns[id] ? id : -1; + } + return this.fns.indexOf(id); + } + + update( + id: number | Interceptor, + fn: Interceptor, + ): number | Interceptor | false { + const index = this.getInterceptorIndex(id); + if (this.fns[index]) { + this.fns[index] = fn; + return id; + } + return false; + } + + use(fn: Interceptor): number { + this.fns.push(fn); + return this.fns.length - 1; + } +} + +export interface Middleware { + error: Interceptors>; + request: Interceptors>; + response: Interceptors>; +} + +export const createInterceptors = (): Middleware< + Req, + Res, + Err, + Options +> => ({ + error: new Interceptors>(), + request: new Interceptors>(), + response: new Interceptors>(), +}); + +const defaultQuerySerializer = createQuerySerializer({ + allowReserved: false, + array: { + explode: true, + style: 'form', + }, + object: { + explode: true, + style: 'deepObject', + }, +}); + +const defaultHeaders = { + 'Content-Type': 'application/json', +}; + +export const createConfig = ( + override: Config & T> = {}, +): Config & T> => ({ + ...jsonBodySerializer, + headers: defaultHeaders, + parseAs: 'auto', + querySerializer: defaultQuerySerializer, + ...override, +}); diff --git a/client/cms/src/client/core/auth.gen.ts b/client/cms/src/client/core/auth.gen.ts new file mode 100644 index 0000000..f8a7326 --- /dev/null +++ b/client/cms/src/client/core/auth.gen.ts @@ -0,0 +1,42 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type AuthToken = string | undefined; + +export interface Auth { + /** + * Which part of the request do we use to send the auth? + * + * @default 'header' + */ + in?: 'header' | 'query' | 'cookie'; + /** + * Header or query parameter name. + * + * @default 'Authorization' + */ + name?: string; + scheme?: 'basic' | 'bearer'; + type: 'apiKey' | 'http'; +} + +export const getAuthToken = async ( + auth: Auth, + callback: ((auth: Auth) => Promise | AuthToken) | AuthToken, +): Promise => { + const token = + typeof callback === 'function' ? await callback(auth) : callback; + + if (!token) { + return; + } + + if (auth.scheme === 'bearer') { + return `Bearer ${token}`; + } + + if (auth.scheme === 'basic') { + return `Basic ${btoa(token)}`; + } + + return token; +}; diff --git a/client/cms/src/client/core/bodySerializer.gen.ts b/client/cms/src/client/core/bodySerializer.gen.ts new file mode 100644 index 0000000..552b50f --- /dev/null +++ b/client/cms/src/client/core/bodySerializer.gen.ts @@ -0,0 +1,100 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { + ArrayStyle, + ObjectStyle, + SerializerOptions, +} from './pathSerializer.gen'; + +export type QuerySerializer = (query: Record) => string; + +export type BodySerializer = (body: any) => any; + +type QuerySerializerOptionsObject = { + allowReserved?: boolean; + array?: Partial>; + object?: Partial>; +}; + +export type QuerySerializerOptions = QuerySerializerOptionsObject & { + /** + * Per-parameter serialization overrides. When provided, these settings + * override the global array/object settings for specific parameter names. + */ + parameters?: Record; +}; + +const serializeFormDataPair = ( + data: FormData, + key: string, + value: unknown, +): void => { + if (typeof value === 'string' || value instanceof Blob) { + data.append(key, value); + } else if (value instanceof Date) { + data.append(key, value.toISOString()); + } else { + data.append(key, JSON.stringify(value)); + } +}; + +const serializeUrlSearchParamsPair = ( + data: URLSearchParams, + key: string, + value: unknown, +): void => { + if (typeof value === 'string') { + data.append(key, value); + } else { + data.append(key, JSON.stringify(value)); + } +}; + +export const formDataBodySerializer = { + bodySerializer: | Array>>( + body: T, + ): FormData => { + const data = new FormData(); + + Object.entries(body).forEach(([key, value]) => { + if (value === undefined || value === null) { + return; + } + if (Array.isArray(value)) { + value.forEach((v) => serializeFormDataPair(data, key, v)); + } else { + serializeFormDataPair(data, key, value); + } + }); + + return data; + }, +}; + +export const jsonBodySerializer = { + bodySerializer: (body: T): string => + JSON.stringify(body, (_key, value) => + typeof value === 'bigint' ? value.toString() : value, + ), +}; + +export const urlSearchParamsBodySerializer = { + bodySerializer: | Array>>( + body: T, + ): string => { + const data = new URLSearchParams(); + + Object.entries(body).forEach(([key, value]) => { + if (value === undefined || value === null) { + return; + } + if (Array.isArray(value)) { + value.forEach((v) => serializeUrlSearchParamsPair(data, key, v)); + } else { + serializeUrlSearchParamsPair(data, key, value); + } + }); + + return data.toString(); + }, +}; diff --git a/client/cms/src/client/core/params.gen.ts b/client/cms/src/client/core/params.gen.ts new file mode 100644 index 0000000..602715c --- /dev/null +++ b/client/cms/src/client/core/params.gen.ts @@ -0,0 +1,176 @@ +// This file is auto-generated by @hey-api/openapi-ts + +type Slot = 'body' | 'headers' | 'path' | 'query'; + +export type Field = + | { + in: Exclude; + /** + * Field name. This is the name we want the user to see and use. + */ + key: string; + /** + * Field mapped name. This is the name we want to use in the request. + * If omitted, we use the same value as `key`. + */ + map?: string; + } + | { + in: Extract; + /** + * Key isn't required for bodies. + */ + key?: string; + map?: string; + } + | { + /** + * Field name. This is the name we want the user to see and use. + */ + key: string; + /** + * Field mapped name. This is the name we want to use in the request. + * If `in` is omitted, `map` aliases `key` to the transport layer. + */ + map: Slot; + }; + +export interface Fields { + allowExtra?: Partial>; + args?: ReadonlyArray; +} + +export type FieldsConfig = ReadonlyArray; + +const extraPrefixesMap: Record = { + $body_: 'body', + $headers_: 'headers', + $path_: 'path', + $query_: 'query', +}; +const extraPrefixes = Object.entries(extraPrefixesMap); + +type KeyMap = Map< + string, + | { + in: Slot; + map?: string; + } + | { + in?: never; + map: Slot; + } +>; + +const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => { + if (!map) { + map = new Map(); + } + + for (const config of fields) { + if ('in' in config) { + if (config.key) { + map.set(config.key, { + in: config.in, + map: config.map, + }); + } + } else if ('key' in config) { + map.set(config.key, { + map: config.map, + }); + } else if (config.args) { + buildKeyMap(config.args, map); + } + } + + return map; +}; + +interface Params { + body: unknown; + headers: Record; + path: Record; + query: Record; +} + +const stripEmptySlots = (params: Params) => { + for (const [slot, value] of Object.entries(params)) { + if (value && typeof value === 'object' && !Object.keys(value).length) { + delete params[slot as Slot]; + } + } +}; + +export const buildClientParams = ( + args: ReadonlyArray, + fields: FieldsConfig, +) => { + const params: Params = { + body: {}, + headers: {}, + path: {}, + query: {}, + }; + + const map = buildKeyMap(fields); + + let config: FieldsConfig[number] | undefined; + + for (const [index, arg] of args.entries()) { + if (fields[index]) { + config = fields[index]; + } + + if (!config) { + continue; + } + + if ('in' in config) { + if (config.key) { + const field = map.get(config.key)!; + const name = field.map || config.key; + if (field.in) { + (params[field.in] as Record)[name] = arg; + } + } else { + params.body = arg; + } + } else { + for (const [key, value] of Object.entries(arg ?? {})) { + const field = map.get(key); + + if (field) { + if (field.in) { + const name = field.map || key; + (params[field.in] as Record)[name] = value; + } else { + params[field.map] = value; + } + } else { + const extra = extraPrefixes.find(([prefix]) => + key.startsWith(prefix), + ); + + if (extra) { + const [prefix, slot] = extra; + (params[slot] as Record)[ + key.slice(prefix.length) + ] = value; + } else if ('allowExtra' in config && config.allowExtra) { + for (const [slot, allowed] of Object.entries(config.allowExtra)) { + if (allowed) { + (params[slot as Slot] as Record)[key] = value; + break; + } + } + } + } + } + } + } + + stripEmptySlots(params); + + return params; +}; diff --git a/client/cms/src/client/core/pathSerializer.gen.ts b/client/cms/src/client/core/pathSerializer.gen.ts new file mode 100644 index 0000000..8d99931 --- /dev/null +++ b/client/cms/src/client/core/pathSerializer.gen.ts @@ -0,0 +1,181 @@ +// This file is auto-generated by @hey-api/openapi-ts + +interface SerializeOptions + extends SerializePrimitiveOptions, + SerializerOptions {} + +interface SerializePrimitiveOptions { + allowReserved?: boolean; + name: string; +} + +export interface SerializerOptions { + /** + * @default true + */ + explode: boolean; + style: T; +} + +export type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited'; +export type ArraySeparatorStyle = ArrayStyle | MatrixStyle; +type MatrixStyle = 'label' | 'matrix' | 'simple'; +export type ObjectStyle = 'form' | 'deepObject'; +type ObjectSeparatorStyle = ObjectStyle | MatrixStyle; + +interface SerializePrimitiveParam extends SerializePrimitiveOptions { + value: string; +} + +export const separatorArrayExplode = (style: ArraySeparatorStyle) => { + switch (style) { + case 'label': + return '.'; + case 'matrix': + return ';'; + case 'simple': + return ','; + default: + return '&'; + } +}; + +export const separatorArrayNoExplode = (style: ArraySeparatorStyle) => { + switch (style) { + case 'form': + return ','; + case 'pipeDelimited': + return '|'; + case 'spaceDelimited': + return '%20'; + default: + return ','; + } +}; + +export const separatorObjectExplode = (style: ObjectSeparatorStyle) => { + switch (style) { + case 'label': + return '.'; + case 'matrix': + return ';'; + case 'simple': + return ','; + default: + return '&'; + } +}; + +export const serializeArrayParam = ({ + allowReserved, + explode, + name, + style, + value, +}: SerializeOptions & { + value: unknown[]; +}) => { + if (!explode) { + const joinedValues = ( + allowReserved ? value : value.map((v) => encodeURIComponent(v as string)) + ).join(separatorArrayNoExplode(style)); + switch (style) { + case 'label': + return `.${joinedValues}`; + case 'matrix': + return `;${name}=${joinedValues}`; + case 'simple': + return joinedValues; + default: + return `${name}=${joinedValues}`; + } + } + + const separator = separatorArrayExplode(style); + const joinedValues = value + .map((v) => { + if (style === 'label' || style === 'simple') { + return allowReserved ? v : encodeURIComponent(v as string); + } + + return serializePrimitiveParam({ + allowReserved, + name, + value: v as string, + }); + }) + .join(separator); + return style === 'label' || style === 'matrix' + ? separator + joinedValues + : joinedValues; +}; + +export const serializePrimitiveParam = ({ + allowReserved, + name, + value, +}: SerializePrimitiveParam) => { + if (value === undefined || value === null) { + return ''; + } + + if (typeof value === 'object') { + throw new Error( + 'Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.', + ); + } + + return `${name}=${allowReserved ? value : encodeURIComponent(value)}`; +}; + +export const serializeObjectParam = ({ + allowReserved, + explode, + name, + style, + value, + valueOnly, +}: SerializeOptions & { + value: Record | Date; + valueOnly?: boolean; +}) => { + if (value instanceof Date) { + return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`; + } + + if (style !== 'deepObject' && !explode) { + let values: string[] = []; + Object.entries(value).forEach(([key, v]) => { + values = [ + ...values, + key, + allowReserved ? (v as string) : encodeURIComponent(v as string), + ]; + }); + const joinedValues = values.join(','); + switch (style) { + case 'form': + return `${name}=${joinedValues}`; + case 'label': + return `.${joinedValues}`; + case 'matrix': + return `;${name}=${joinedValues}`; + default: + return joinedValues; + } + } + + const separator = separatorObjectExplode(style); + const joinedValues = Object.entries(value) + .map(([key, v]) => + serializePrimitiveParam({ + allowReserved, + name: style === 'deepObject' ? `${name}[${key}]` : key, + value: v as string, + }), + ) + .join(separator); + return style === 'label' || style === 'matrix' + ? separator + joinedValues + : joinedValues; +}; diff --git a/client/cms/src/client/core/queryKeySerializer.gen.ts b/client/cms/src/client/core/queryKeySerializer.gen.ts new file mode 100644 index 0000000..d3bb683 --- /dev/null +++ b/client/cms/src/client/core/queryKeySerializer.gen.ts @@ -0,0 +1,136 @@ +// This file is auto-generated by @hey-api/openapi-ts + +/** + * JSON-friendly union that mirrors what Pinia Colada can hash. + */ +export type JsonValue = + | null + | string + | number + | boolean + | JsonValue[] + | { [key: string]: JsonValue }; + +/** + * Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes. + */ +export const queryKeyJsonReplacer = (_key: string, value: unknown) => { + if ( + value === undefined || + typeof value === 'function' || + typeof value === 'symbol' + ) { + return undefined; + } + if (typeof value === 'bigint') { + return value.toString(); + } + if (value instanceof Date) { + return value.toISOString(); + } + return value; +}; + +/** + * Safely stringifies a value and parses it back into a JsonValue. + */ +export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => { + try { + const json = JSON.stringify(input, queryKeyJsonReplacer); + if (json === undefined) { + return undefined; + } + return JSON.parse(json) as JsonValue; + } catch { + return undefined; + } +}; + +/** + * Detects plain objects (including objects with a null prototype). + */ +const isPlainObject = (value: unknown): value is Record => { + if (value === null || typeof value !== 'object') { + return false; + } + const prototype = Object.getPrototypeOf(value as object); + return prototype === Object.prototype || prototype === null; +}; + +/** + * Turns URLSearchParams into a sorted JSON object for deterministic keys. + */ +const serializeSearchParams = (params: URLSearchParams): JsonValue => { + const entries = Array.from(params.entries()).sort(([a], [b]) => + a.localeCompare(b), + ); + const result: Record = {}; + + for (const [key, value] of entries) { + const existing = result[key]; + if (existing === undefined) { + result[key] = value; + continue; + } + + if (Array.isArray(existing)) { + (existing as string[]).push(value); + } else { + result[key] = [existing, value]; + } + } + + return result; +}; + +/** + * Normalizes any accepted value into a JSON-friendly shape for query keys. + */ +export const serializeQueryKeyValue = ( + value: unknown, +): JsonValue | undefined => { + if (value === null) { + return null; + } + + if ( + typeof value === 'string' || + typeof value === 'number' || + typeof value === 'boolean' + ) { + return value; + } + + if ( + value === undefined || + typeof value === 'function' || + typeof value === 'symbol' + ) { + return undefined; + } + + if (typeof value === 'bigint') { + return value.toString(); + } + + if (value instanceof Date) { + return value.toISOString(); + } + + if (Array.isArray(value)) { + return stringifyToJsonValue(value); + } + + if ( + typeof URLSearchParams !== 'undefined' && + value instanceof URLSearchParams + ) { + return serializeSearchParams(value); + } + + if (isPlainObject(value)) { + return stringifyToJsonValue(value); + } + + return undefined; +}; diff --git a/client/cms/src/client/core/serverSentEvents.gen.ts b/client/cms/src/client/core/serverSentEvents.gen.ts new file mode 100644 index 0000000..343d25a --- /dev/null +++ b/client/cms/src/client/core/serverSentEvents.gen.ts @@ -0,0 +1,266 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Config } from './types.gen'; + +export type ServerSentEventsOptions = Omit< + RequestInit, + 'method' +> & + Pick & { + /** + * Fetch API implementation. You can use this option to provide a custom + * fetch instance. + * + * @default globalThis.fetch + */ + fetch?: typeof fetch; + /** + * Implementing clients can call request interceptors inside this hook. + */ + onRequest?: (url: string, init: RequestInit) => Promise; + /** + * Callback invoked when a network or parsing error occurs during streaming. + * + * This option applies only if the endpoint returns a stream of events. + * + * @param error The error that occurred. + */ + onSseError?: (error: unknown) => void; + /** + * Callback invoked when an event is streamed from the server. + * + * This option applies only if the endpoint returns a stream of events. + * + * @param event Event streamed from the server. + * @returns Nothing (void). + */ + onSseEvent?: (event: StreamEvent) => void; + serializedBody?: RequestInit['body']; + /** + * Default retry delay in milliseconds. + * + * This option applies only if the endpoint returns a stream of events. + * + * @default 3000 + */ + sseDefaultRetryDelay?: number; + /** + * Maximum number of retry attempts before giving up. + */ + sseMaxRetryAttempts?: number; + /** + * Maximum retry delay in milliseconds. + * + * Applies only when exponential backoff is used. + * + * This option applies only if the endpoint returns a stream of events. + * + * @default 30000 + */ + sseMaxRetryDelay?: number; + /** + * Optional sleep function for retry backoff. + * + * Defaults to using `setTimeout`. + */ + sseSleepFn?: (ms: number) => Promise; + url: string; + }; + +export interface StreamEvent { + data: TData; + event?: string; + id?: string; + retry?: number; +} + +export type ServerSentEventsResult< + TData = unknown, + TReturn = void, + TNext = unknown, +> = { + stream: AsyncGenerator< + TData extends Record ? TData[keyof TData] : TData, + TReturn, + TNext + >; +}; + +export const createSseClient = ({ + onRequest, + onSseError, + onSseEvent, + responseTransformer, + responseValidator, + sseDefaultRetryDelay, + sseMaxRetryAttempts, + sseMaxRetryDelay, + sseSleepFn, + url, + ...options +}: ServerSentEventsOptions): ServerSentEventsResult => { + let lastEventId: string | undefined; + + const sleep = + sseSleepFn ?? + ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms))); + + const createStream = async function* () { + let retryDelay: number = sseDefaultRetryDelay ?? 3000; + let attempt = 0; + const signal = options.signal ?? new AbortController().signal; + + while (true) { + if (signal.aborted) break; + + attempt++; + + const headers = + options.headers instanceof Headers + ? options.headers + : new Headers(options.headers as Record | undefined); + + if (lastEventId !== undefined) { + headers.set('Last-Event-ID', lastEventId); + } + + try { + const requestInit: RequestInit = { + redirect: 'follow', + ...options, + body: options.serializedBody, + headers, + signal, + }; + let request = new Request(url, requestInit); + if (onRequest) { + request = await onRequest(url, requestInit); + } + // fetch must be assigned here, otherwise it would throw the error: + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation + const _fetch = options.fetch ?? globalThis.fetch; + const response = await _fetch(request); + + if (!response.ok) + throw new Error( + `SSE failed: ${response.status} ${response.statusText}`, + ); + + if (!response.body) throw new Error('No body in SSE response'); + + const reader = response.body + .pipeThrough(new TextDecoderStream()) + .getReader(); + + let buffer = ''; + + const abortHandler = () => { + try { + reader.cancel(); + } catch { + // noop + } + }; + + signal.addEventListener('abort', abortHandler); + + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + buffer += value; + // Normalize line endings: CRLF -> LF, then CR -> LF + buffer = buffer.replace(/\r\n/g, '\n').replace(/\r/g, '\n'); + + const chunks = buffer.split('\n\n'); + buffer = chunks.pop() ?? ''; + + for (const chunk of chunks) { + const lines = chunk.split('\n'); + const dataLines: Array = []; + let eventName: string | undefined; + + for (const line of lines) { + if (line.startsWith('data:')) { + dataLines.push(line.replace(/^data:\s*/, '')); + } else if (line.startsWith('event:')) { + eventName = line.replace(/^event:\s*/, ''); + } else if (line.startsWith('id:')) { + lastEventId = line.replace(/^id:\s*/, ''); + } else if (line.startsWith('retry:')) { + const parsed = Number.parseInt( + line.replace(/^retry:\s*/, ''), + 10, + ); + if (!Number.isNaN(parsed)) { + retryDelay = parsed; + } + } + } + + let data: unknown; + let parsedJson = false; + + if (dataLines.length) { + const rawData = dataLines.join('\n'); + try { + data = JSON.parse(rawData); + parsedJson = true; + } catch { + data = rawData; + } + } + + if (parsedJson) { + if (responseValidator) { + await responseValidator(data); + } + + if (responseTransformer) { + data = await responseTransformer(data); + } + } + + onSseEvent?.({ + data, + event: eventName, + id: lastEventId, + retry: retryDelay, + }); + + if (dataLines.length) { + yield data as any; + } + } + } + } finally { + signal.removeEventListener('abort', abortHandler); + reader.releaseLock(); + } + + break; // exit loop on normal completion + } catch (error) { + // connection failed or aborted; retry after delay + onSseError?.(error); + + if ( + sseMaxRetryAttempts !== undefined && + attempt >= sseMaxRetryAttempts + ) { + break; // stop after firing error + } + + // exponential backoff: double retry each attempt, cap at 30s + const backoff = Math.min( + retryDelay * 2 ** (attempt - 1), + sseMaxRetryDelay ?? 30000, + ); + await sleep(backoff); + } + } + }; + + const stream = createStream(); + + return { stream }; +}; diff --git a/client/cms/src/client/core/types.gen.ts b/client/cms/src/client/core/types.gen.ts new file mode 100644 index 0000000..643c070 --- /dev/null +++ b/client/cms/src/client/core/types.gen.ts @@ -0,0 +1,118 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Auth, AuthToken } from './auth.gen'; +import type { + BodySerializer, + QuerySerializer, + QuerySerializerOptions, +} from './bodySerializer.gen'; + +export type HttpMethod = + | 'connect' + | 'delete' + | 'get' + | 'head' + | 'options' + | 'patch' + | 'post' + | 'put' + | 'trace'; + +export type Client< + RequestFn = never, + Config = unknown, + MethodFn = never, + BuildUrlFn = never, + SseFn = never, +> = { + /** + * Returns the final request URL. + */ + buildUrl: BuildUrlFn; + getConfig: () => Config; + request: RequestFn; + setConfig: (config: Config) => Config; +} & { + [K in HttpMethod]: MethodFn; +} & ([SseFn] extends [never] + ? { sse?: never } + : { sse: { [K in HttpMethod]: SseFn } }); + +export interface Config { + /** + * Auth token or a function returning auth token. The resolved value will be + * added to the request payload as defined by its `security` array. + */ + auth?: ((auth: Auth) => Promise | AuthToken) | AuthToken; + /** + * A function for serializing request body parameter. By default, + * {@link JSON.stringify()} will be used. + */ + bodySerializer?: BodySerializer | null; + /** + * An object containing any HTTP headers that you want to pre-populate your + * `Headers` object with. + * + * {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more} + */ + headers?: + | RequestInit['headers'] + | Record< + string, + | string + | number + | boolean + | (string | number | boolean)[] + | null + | undefined + | unknown + >; + /** + * The request method. + * + * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more} + */ + method?: Uppercase; + /** + * A function for serializing request query parameters. By default, arrays + * will be exploded in form style, objects will be exploded in deepObject + * style, and reserved characters are percent-encoded. + * + * This method will have no effect if the native `paramsSerializer()` Axios + * API function is used. + * + * {@link https://swagger.io/docs/specification/serialization/#query View examples} + */ + querySerializer?: QuerySerializer | QuerySerializerOptions; + /** + * A function validating request data. This is useful if you want to ensure + * the request conforms to the desired shape, so it can be safely sent to + * the server. + */ + requestValidator?: (data: unknown) => Promise; + /** + * A function transforming response data before it's returned. This is useful + * for post-processing data, e.g. converting ISO strings into Date objects. + */ + responseTransformer?: (data: unknown) => Promise; + /** + * A function validating response data. This is useful if you want to ensure + * the response conforms to the desired shape, so it can be safely passed to + * the transformers and returned to the user. + */ + responseValidator?: (data: unknown) => Promise; +} + +type IsExactlyNeverOrNeverUndefined = [T] extends [never] + ? true + : [T] extends [never | undefined] + ? [undefined] extends [T] + ? false + : true + : false; + +export type OmitNever> = { + [K in keyof T as IsExactlyNeverOrNeverUndefined extends true + ? never + : K]: T[K]; +}; diff --git a/client/cms/src/client/core/utils.gen.ts b/client/cms/src/client/core/utils.gen.ts new file mode 100644 index 0000000..0b5389d --- /dev/null +++ b/client/cms/src/client/core/utils.gen.ts @@ -0,0 +1,143 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { BodySerializer, QuerySerializer } from './bodySerializer.gen'; +import { + type ArraySeparatorStyle, + serializeArrayParam, + serializeObjectParam, + serializePrimitiveParam, +} from './pathSerializer.gen'; + +export interface PathSerializer { + path: Record; + url: string; +} + +export const PATH_PARAM_RE = /\{[^{}]+\}/g; + +export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { + let url = _url; + const matches = _url.match(PATH_PARAM_RE); + if (matches) { + for (const match of matches) { + let explode = false; + let name = match.substring(1, match.length - 1); + let style: ArraySeparatorStyle = 'simple'; + + if (name.endsWith('*')) { + explode = true; + name = name.substring(0, name.length - 1); + } + + if (name.startsWith('.')) { + name = name.substring(1); + style = 'label'; + } else if (name.startsWith(';')) { + name = name.substring(1); + style = 'matrix'; + } + + const value = path[name]; + + if (value === undefined || value === null) { + continue; + } + + if (Array.isArray(value)) { + url = url.replace( + match, + serializeArrayParam({ explode, name, style, value }), + ); + continue; + } + + if (typeof value === 'object') { + url = url.replace( + match, + serializeObjectParam({ + explode, + name, + style, + value: value as Record, + valueOnly: true, + }), + ); + continue; + } + + if (style === 'matrix') { + url = url.replace( + match, + `;${serializePrimitiveParam({ + name, + value: value as string, + })}`, + ); + continue; + } + + const replaceValue = encodeURIComponent( + style === 'label' ? `.${value as string}` : (value as string), + ); + url = url.replace(match, replaceValue); + } + } + return url; +}; + +export const getUrl = ({ + baseUrl, + path, + query, + querySerializer, + url: _url, +}: { + baseUrl?: string; + path?: Record; + query?: Record; + querySerializer: QuerySerializer; + url: string; +}) => { + const pathUrl = _url.startsWith('/') ? _url : `/${_url}`; + let url = (baseUrl ?? '') + pathUrl; + if (path) { + url = defaultPathSerializer({ path, url }); + } + let search = query ? querySerializer(query) : ''; + if (search.startsWith('?')) { + search = search.substring(1); + } + if (search) { + url += `?${search}`; + } + return url; +}; + +export function getValidRequestBody(options: { + body?: unknown; + bodySerializer?: BodySerializer | null; + serializedBody?: unknown; +}) { + const hasBody = options.body !== undefined; + const isSerializedBody = hasBody && options.bodySerializer; + + if (isSerializedBody) { + if ('serializedBody' in options) { + const hasSerializedBody = + options.serializedBody !== undefined && options.serializedBody !== ''; + + return hasSerializedBody ? options.serializedBody : null; + } + + // not all clients implement a serializedBody property (i.e. client-axios) + return options.body !== '' ? options.body : null; + } + + // plain/text body + if (hasBody) { + return options.body; + } + + // no body was provided + return undefined; +} diff --git a/client/cms/src/client/index.ts b/client/cms/src/client/index.ts new file mode 100644 index 0000000..e4e494e --- /dev/null +++ b/client/cms/src/client/index.ts @@ -0,0 +1,4 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export { getAuthRedirect, getEventCheckin, getEventCheckinQuery, getEventInfo, getUserFull, getUserInfo, getUserList, type Options, patchUserUpdate, postAuthExchange, postAuthMagic, postAuthRefresh, postAuthToken, postEventCheckinSubmit } from './sdk.gen'; +export type { ClientOptions, DataUser, DataUserSearchDoc, GetAuthRedirectData, GetAuthRedirectError, GetAuthRedirectErrors, GetEventCheckinData, GetEventCheckinError, GetEventCheckinErrors, GetEventCheckinQueryData, GetEventCheckinQueryError, GetEventCheckinQueryErrors, GetEventCheckinQueryResponse, GetEventCheckinQueryResponses, GetEventCheckinResponse, GetEventCheckinResponses, GetEventInfoData, GetEventInfoError, GetEventInfoErrors, GetEventInfoResponse, GetEventInfoResponses, GetUserFullData, GetUserFullError, GetUserFullErrors, GetUserFullResponse, GetUserFullResponses, GetUserInfoData, GetUserInfoError, GetUserInfoErrors, GetUserInfoResponse, GetUserInfoResponses, GetUserListData, GetUserListError, GetUserListErrors, GetUserListResponse, GetUserListResponses, PatchUserUpdateData, PatchUserUpdateError, PatchUserUpdateErrors, PatchUserUpdateResponse, PatchUserUpdateResponses, PostAuthExchangeData, PostAuthExchangeError, PostAuthExchangeErrors, PostAuthExchangeResponse, PostAuthExchangeResponses, PostAuthMagicData, PostAuthMagicError, PostAuthMagicErrors, PostAuthMagicResponse, PostAuthMagicResponses, PostAuthRefreshData, PostAuthRefreshError, PostAuthRefreshErrors, PostAuthRefreshResponse, PostAuthRefreshResponses, PostAuthTokenData, PostAuthTokenError, PostAuthTokenErrors, PostAuthTokenResponse, PostAuthTokenResponses, PostEventCheckinSubmitData, PostEventCheckinSubmitError, PostEventCheckinSubmitErrors, PostEventCheckinSubmitResponse, PostEventCheckinSubmitResponses, ServiceAuthExchangeData, ServiceAuthExchangeResponse, ServiceAuthMagicData, ServiceAuthMagicResponse, ServiceAuthRefreshData, ServiceAuthTokenData, ServiceAuthTokenResponse, ServiceEventCheckinQueryResponse, ServiceEventCheckinResponse, ServiceEventCheckinSubmitData, ServiceEventInfoResponse, ServiceUserUserInfoData, ServiceUserUserTableResponse, UtilsRespStatus } from './types.gen'; diff --git a/client/cms/src/client/sdk.gen.ts b/client/cms/src/client/sdk.gen.ts new file mode 100644 index 0000000..d9d3e85 --- /dev/null +++ b/client/cms/src/client/sdk.gen.ts @@ -0,0 +1,153 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Client, Options as Options2, TDataShape } from './client'; +import { client } from './client.gen'; +import type { GetAuthRedirectData, GetAuthRedirectErrors, GetEventCheckinData, GetEventCheckinErrors, GetEventCheckinQueryData, GetEventCheckinQueryErrors, GetEventCheckinQueryResponses, GetEventCheckinResponses, GetEventInfoData, GetEventInfoErrors, GetEventInfoResponses, GetUserFullData, GetUserFullErrors, GetUserFullResponses, GetUserInfoData, GetUserInfoErrors, GetUserInfoResponses, GetUserListData, GetUserListErrors, GetUserListResponses, PatchUserUpdateData, PatchUserUpdateErrors, PatchUserUpdateResponses, PostAuthExchangeData, PostAuthExchangeErrors, PostAuthExchangeResponses, PostAuthMagicData, PostAuthMagicErrors, PostAuthMagicResponses, PostAuthRefreshData, PostAuthRefreshErrors, PostAuthRefreshResponses, PostAuthTokenData, PostAuthTokenErrors, PostAuthTokenResponses, PostEventCheckinSubmitData, PostEventCheckinSubmitErrors, PostEventCheckinSubmitResponses } from './types.gen'; + +export type Options = Options2 & { + /** + * You can provide a client instance returned by `createClient()` instead of + * individual options. This might be also useful if you want to implement a + * custom client. + */ + client?: Client; + /** + * You can pass arbitrary values through the `meta` object. This can be + * used to access values that aren't defined as part of the SDK function. + */ + meta?: Record; +}; + +/** + * Exchange Auth Code + * + * Exchanges client credentials and user session for a specific redirect authorization code. + */ +export const postAuthExchange = (options: Options) => (options.client ?? client).post({ + url: '/auth/exchange', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } +}); + +/** + * Request Magic Link + * + * Verifies Turnstile token and sends an authentication link via email. Returns the URI directly if debug mode is enabled. + */ +export const postAuthMagic = (options: Options) => (options.client ?? client).post({ + url: '/auth/magic', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } +}); + +/** + * Handle Auth Callback and Redirect + * + * Verifies the temporary email code, ensures the user exists (or creates one), validates the client's redirect URI, and finally performs a 302 redirect with a new authorization code. + */ +export const getAuthRedirect = (options: Options) => (options.client ?? client).get({ url: '/auth/redirect', ...options }); + +/** + * Refresh Access Token + * + * Accepts a valid refresh token to issue a new access token and a rotated refresh token. + */ +export const postAuthRefresh = (options: Options) => (options.client ?? client).post({ + url: '/auth/refresh', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } +}); + +/** + * Exchange Code for Token + * + * Verifies the provided authorization code and issues a pair of JWT tokens (Access and Refresh). + */ +export const postAuthToken = (options: Options) => (options.client ?? client).post({ + url: '/auth/token', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } +}); + +/** + * Generate Check-in Code + * + * Creates a temporary check-in code for the authenticated user and event. + */ +export const getEventCheckin = (options: Options) => (options.client ?? client).get({ url: '/event/checkin', ...options }); + +/** + * Query Check-in Status + * + * Returns the timestamp of when the user checked in, or null if not yet checked in. + */ +export const getEventCheckinQuery = (options: Options) => (options.client ?? client).get({ url: '/event/checkin/query', ...options }); + +/** + * Submit Check-in Code + * + * Submits the generated code to mark the user as attended. + */ +export const postEventCheckinSubmit = (options: Options) => (options.client ?? client).post({ + url: '/event/checkin/submit', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } +}); + +/** + * Get Event Information + * + * Fetches the name, start time, and end time of an event using its UUID. + */ +export const getEventInfo = (options: Options) => (options.client ?? client).get({ url: '/event/info', ...options }); + +/** + * Get Full User Table + * + * Fetches all user records without pagination. This is typically used for administrative overview or data export. + */ +export const getUserFull = (options?: Options) => (options?.client ?? client).get({ url: '/user/full', ...options }); + +/** + * Get My User Information + * + * Fetches the complete profile data for the user associated with the provided session/token. + */ +export const getUserInfo = (options?: Options) => (options?.client ?? client).get({ url: '/user/info', ...options }); + +/** + * List Users + * + * Fetches a list of users with support for pagination via limit and offset. Data is sourced from the search engine for high performance. + */ +export const getUserList = (options: Options) => (options.client ?? client).get({ url: '/user/list', ...options }); + +/** + * Update User Information + * + * Updates specific profile fields such as username, nickname, subtitle, avatar (URL), and bio (Base64). + * Validation: Username (5-255 chars), Nickname (max 24 chars), Subtitle (max 32 chars). + */ +export const patchUserUpdate = (options: Options) => (options.client ?? client).patch({ + url: '/user/update', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } +}); diff --git a/client/cms/src/client/types.gen.ts b/client/cms/src/client/types.gen.ts new file mode 100644 index 0000000..4e66492 --- /dev/null +++ b/client/cms/src/client/types.gen.ts @@ -0,0 +1,713 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type ClientOptions = { + baseUrl: 'http://localhost:8000/api/v1' | 'https://localhost:8000/api/v1' | (string & {}); +}; + +export type DataUser = { + allow_public?: boolean; + avatar?: string; + bio?: string; + email?: string; + id?: number; + nickname?: string; + permission_level?: number; + subtitle?: string; + user_id?: string; + username?: string; + uuid?: string; +}; + +export type DataUserSearchDoc = { + avatar?: string; + email?: string; + nickname?: string; + subtitle?: string; + type?: string; + user_id?: string; + username?: string; +}; + +export type ServiceAuthExchangeData = { + client_id?: string; + redirect_uri?: string; + state?: string; +}; + +export type ServiceAuthExchangeResponse = { + redirect_uri?: string; +}; + +export type ServiceAuthMagicData = { + client_id?: string; + client_ip?: string; + email?: string; + redirect_uri?: string; + state?: string; + turnstile_token?: string; +}; + +export type ServiceAuthMagicResponse = { + uri?: string; +}; + +export type ServiceAuthRefreshData = { + refresh_token?: string; +}; + +export type ServiceAuthTokenData = { + code?: string; +}; + +export type ServiceAuthTokenResponse = { + access_token?: string; + refresh_token?: string; +}; + +export type ServiceEventCheckinQueryResponse = { + checkin_at?: string; +}; + +export type ServiceEventCheckinResponse = { + checkin_code?: string; +}; + +export type ServiceEventCheckinSubmitData = { + checkin_code?: string; +}; + +export type ServiceEventInfoResponse = { + end_time?: string; + name?: string; + start_time?: string; +}; + +export type ServiceUserUserInfoData = { + allow_public?: boolean; + avatar?: string; + bio?: string; + email?: string; + nickname?: string; + permission_level?: number; + subtitle?: string; + user_id?: string; + username?: string; +}; + +export type ServiceUserUserTableResponse = { + user_table?: Array; +}; + +export type UtilsRespStatus = { + code?: number; + data?: unknown; + error_id?: string; + status?: string; +}; + +export type PostAuthExchangeData = { + /** + * Exchange Request Credentials + */ + body: ServiceAuthExchangeData; + path?: never; + query?: never; + url: '/auth/exchange'; +}; + +export type PostAuthExchangeErrors = { + /** + * Invalid Input + */ + 400: UtilsRespStatus & { + data?: { + [key: string]: unknown; + }; + }; + /** + * Unauthorized + */ + 401: UtilsRespStatus & { + data?: { + [key: string]: unknown; + }; + }; + /** + * Internal Server Error + */ + 500: UtilsRespStatus & { + data?: { + [key: string]: unknown; + }; + }; +}; + +export type PostAuthExchangeError = PostAuthExchangeErrors[keyof PostAuthExchangeErrors]; + +export type PostAuthExchangeResponses = { + /** + * Successful exchange + */ + 200: UtilsRespStatus & { + data?: ServiceAuthExchangeResponse; + }; +}; + +export type PostAuthExchangeResponse = PostAuthExchangeResponses[keyof PostAuthExchangeResponses]; + +export type PostAuthMagicData = { + /** + * Magic Link Request Data + */ + body: ServiceAuthMagicData; + path?: never; + query?: never; + url: '/auth/magic'; +}; + +export type PostAuthMagicErrors = { + /** + * Invalid Input + */ + 400: UtilsRespStatus & { + data?: { + [key: string]: unknown; + }; + }; + /** + * Turnstile Verification Failed + */ + 403: UtilsRespStatus & { + data?: { + [key: string]: unknown; + }; + }; + /** + * Internal Server Error + */ + 500: UtilsRespStatus & { + data?: { + [key: string]: unknown; + }; + }; +}; + +export type PostAuthMagicError = PostAuthMagicErrors[keyof PostAuthMagicErrors]; + +export type PostAuthMagicResponses = { + /** + * Successful request + */ + 200: UtilsRespStatus & { + data?: ServiceAuthMagicResponse; + }; +}; + +export type PostAuthMagicResponse = PostAuthMagicResponses[keyof PostAuthMagicResponses]; + +export type GetAuthRedirectData = { + body?: never; + path?: never; + query: { + /** + * Client Identifier + */ + client_id: string; + /** + * Target Redirect URI + */ + redirect_uri: string; + /** + * Temporary Verification Code + */ + code: string; + /** + * Opaque state used to maintain state between the request and callback + */ + state?: string; + }; + url: '/auth/redirect'; +}; + +export type GetAuthRedirectErrors = { + /** + * Invalid Input / Client Not Found / URI Mismatch + */ + 400: UtilsRespStatus & { + data?: { + [key: string]: unknown; + }; + }; + /** + * Invalid or Expired Verification Code + */ + 403: UtilsRespStatus & { + data?: { + [key: string]: unknown; + }; + }; + /** + * Internal Server Error + */ + 500: UtilsRespStatus & { + data?: { + [key: string]: unknown; + }; + }; +}; + +export type GetAuthRedirectError = GetAuthRedirectErrors[keyof GetAuthRedirectErrors]; + +export type PostAuthRefreshData = { + /** + * Refresh Token Body + */ + body: ServiceAuthRefreshData; + path?: never; + query?: never; + url: '/auth/refresh'; +}; + +export type PostAuthRefreshErrors = { + /** + * Invalid Input + */ + 400: UtilsRespStatus & { + data?: { + [key: string]: unknown; + }; + }; + /** + * Invalid Refresh Token + */ + 401: UtilsRespStatus & { + data?: { + [key: string]: unknown; + }; + }; + /** + * Internal Server Error + */ + 500: UtilsRespStatus & { + data?: { + [key: string]: unknown; + }; + }; +}; + +export type PostAuthRefreshError = PostAuthRefreshErrors[keyof PostAuthRefreshErrors]; + +export type PostAuthRefreshResponses = { + /** + * Successful rotation + */ + 200: UtilsRespStatus & { + data?: ServiceAuthTokenResponse; + }; +}; + +export type PostAuthRefreshResponse = PostAuthRefreshResponses[keyof PostAuthRefreshResponses]; + +export type PostAuthTokenData = { + /** + * Token Request Body + */ + body: ServiceAuthTokenData; + path?: never; + query?: never; + url: '/auth/token'; +}; + +export type PostAuthTokenErrors = { + /** + * Invalid Input + */ + 400: UtilsRespStatus & { + data?: { + [key: string]: unknown; + }; + }; + /** + * Invalid or Expired Code + */ + 403: UtilsRespStatus & { + data?: { + [key: string]: unknown; + }; + }; + /** + * Internal Server Error + */ + 500: UtilsRespStatus & { + data?: { + [key: string]: unknown; + }; + }; +}; + +export type PostAuthTokenError = PostAuthTokenErrors[keyof PostAuthTokenErrors]; + +export type PostAuthTokenResponses = { + /** + * Successful token issuance + */ + 200: UtilsRespStatus & { + data?: ServiceAuthTokenResponse; + }; +}; + +export type PostAuthTokenResponse = PostAuthTokenResponses[keyof PostAuthTokenResponses]; + +export type GetEventCheckinData = { + body?: never; + path?: never; + query: { + /** + * Event UUID + */ + event_id: string; + }; + url: '/event/checkin'; +}; + +export type GetEventCheckinErrors = { + /** + * Invalid Input + */ + 400: UtilsRespStatus & { + data?: { + [key: string]: unknown; + }; + }; + /** + * Internal Server Error + */ + 500: UtilsRespStatus & { + data?: { + [key: string]: unknown; + }; + }; +}; + +export type GetEventCheckinError = GetEventCheckinErrors[keyof GetEventCheckinErrors]; + +export type GetEventCheckinResponses = { + /** + * Successfully generated code + */ + 200: UtilsRespStatus & { + data?: ServiceEventCheckinResponse; + }; +}; + +export type GetEventCheckinResponse = GetEventCheckinResponses[keyof GetEventCheckinResponses]; + +export type GetEventCheckinQueryData = { + body?: never; + path?: never; + query: { + /** + * Event UUID + */ + event_id: string; + }; + url: '/event/checkin/query'; +}; + +export type GetEventCheckinQueryErrors = { + /** + * Invalid Input + */ + 400: UtilsRespStatus & { + data?: { + [key: string]: unknown; + }; + }; + /** + * Record Not Found + */ + 404: UtilsRespStatus & { + data?: { + [key: string]: unknown; + }; + }; +}; + +export type GetEventCheckinQueryError = GetEventCheckinQueryErrors[keyof GetEventCheckinQueryErrors]; + +export type GetEventCheckinQueryResponses = { + /** + * Current attendance status + */ + 200: UtilsRespStatus & { + data?: ServiceEventCheckinQueryResponse; + }; +}; + +export type GetEventCheckinQueryResponse = GetEventCheckinQueryResponses[keyof GetEventCheckinQueryResponses]; + +export type PostEventCheckinSubmitData = { + /** + * Checkin Code Data + */ + body: ServiceEventCheckinSubmitData; + path?: never; + query?: never; + url: '/event/checkin/submit'; +}; + +export type PostEventCheckinSubmitErrors = { + /** + * Invalid Code or Input + */ + 400: UtilsRespStatus & { + data?: { + [key: string]: unknown; + }; + }; +}; + +export type PostEventCheckinSubmitError = PostEventCheckinSubmitErrors[keyof PostEventCheckinSubmitErrors]; + +export type PostEventCheckinSubmitResponses = { + /** + * Attendance marked successfully + */ + 200: UtilsRespStatus & { + data?: { + [key: string]: unknown; + }; + }; +}; + +export type PostEventCheckinSubmitResponse = PostEventCheckinSubmitResponses[keyof PostEventCheckinSubmitResponses]; + +export type GetEventInfoData = { + body?: never; + path?: never; + query: { + /** + * Event UUID + */ + event_id: string; + }; + url: '/event/info'; +}; + +export type GetEventInfoErrors = { + /** + * Invalid Input + */ + 400: UtilsRespStatus & { + data?: { + [key: string]: unknown; + }; + }; + /** + * Event Not Found + */ + 404: UtilsRespStatus & { + data?: { + [key: string]: unknown; + }; + }; + /** + * Internal Server Error + */ + 500: UtilsRespStatus & { + data?: { + [key: string]: unknown; + }; + }; +}; + +export type GetEventInfoError = GetEventInfoErrors[keyof GetEventInfoErrors]; + +export type GetEventInfoResponses = { + /** + * Successful retrieval + */ + 200: UtilsRespStatus & { + data?: ServiceEventInfoResponse; + }; +}; + +export type GetEventInfoResponse = GetEventInfoResponses[keyof GetEventInfoResponses]; + +export type GetUserFullData = { + body?: never; + path?: never; + query?: never; + url: '/user/full'; +}; + +export type GetUserFullErrors = { + /** + * Internal Server Error (Database Error) + */ + 500: UtilsRespStatus & { + data?: { + [key: string]: unknown; + }; + }; +}; + +export type GetUserFullError = GetUserFullErrors[keyof GetUserFullErrors]; + +export type GetUserFullResponses = { + /** + * Successful retrieval of full user table + */ + 200: UtilsRespStatus & { + data?: ServiceUserUserTableResponse; + }; +}; + +export type GetUserFullResponse = GetUserFullResponses[keyof GetUserFullResponses]; + +export type GetUserInfoData = { + body?: never; + path?: never; + query?: never; + url: '/user/info'; +}; + +export type GetUserInfoErrors = { + /** + * Missing User ID / Unauthorized + */ + 403: UtilsRespStatus & { + data?: { + [key: string]: unknown; + }; + }; + /** + * User Not Found + */ + 404: UtilsRespStatus & { + data?: { + [key: string]: unknown; + }; + }; + /** + * Internal Server Error (UUID Parse Failed) + */ + 500: UtilsRespStatus & { + data?: { + [key: string]: unknown; + }; + }; +}; + +export type GetUserInfoError = GetUserInfoErrors[keyof GetUserInfoErrors]; + +export type GetUserInfoResponses = { + /** + * Successful profile retrieval + */ + 200: UtilsRespStatus & { + data?: ServiceUserUserInfoData; + }; +}; + +export type GetUserInfoResponse = GetUserInfoResponses[keyof GetUserInfoResponses]; + +export type GetUserListData = { + body?: never; + path?: never; + query: { + /** + * Maximum number of users to return (default 0) + */ + limit?: string; + /** + * Number of users to skip + */ + offset: string; + }; + url: '/user/list'; +}; + +export type GetUserListErrors = { + /** + * Invalid Input (Format Error) + */ + 400: UtilsRespStatus & { + data?: { + [key: string]: unknown; + }; + }; + /** + * Internal Server Error (Search Engine or Missing Offset) + */ + 500: UtilsRespStatus & { + data?: { + [key: string]: unknown; + }; + }; +}; + +export type GetUserListError = GetUserListErrors[keyof GetUserListErrors]; + +export type GetUserListResponses = { + /** + * Successful paginated list retrieval + */ + 200: UtilsRespStatus & { + data?: Array; + }; +}; + +export type GetUserListResponse = GetUserListResponses[keyof GetUserListResponses]; + +export type PatchUserUpdateData = { + /** + * Updated User Profile Data + */ + body: ServiceUserUserInfoData; + path?: never; + query?: never; + url: '/user/update'; +}; + +export type PatchUserUpdateErrors = { + /** + * Invalid Input (Validation Failed) + */ + 400: UtilsRespStatus & { + data?: { + [key: string]: unknown; + }; + }; + /** + * Missing User ID / Unauthorized + */ + 403: UtilsRespStatus & { + data?: { + [key: string]: unknown; + }; + }; + /** + * Internal Server Error (Database Error / UUID Parse Failed) + */ + 500: UtilsRespStatus & { + data?: { + [key: string]: unknown; + }; + }; +}; + +export type PatchUserUpdateError = PatchUserUpdateErrors[keyof PatchUserUpdateErrors]; + +export type PatchUserUpdateResponses = { + /** + * Successful profile update + */ + 200: UtilsRespStatus & { + data?: { + [key: string]: unknown; + }; + }; +}; + +export type PatchUserUpdateResponse = PatchUserUpdateResponses[keyof PatchUserUpdateResponses]; diff --git a/client/cms/src/client/zod.gen.ts b/client/cms/src/client/zod.gen.ts new file mode 100644 index 0000000..efd557b --- /dev/null +++ b/client/cms/src/client/zod.gen.ts @@ -0,0 +1,280 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { z } from 'zod'; + +export const zDataUser = z.object({ + allow_public: z.optional(z.boolean()), + avatar: z.optional(z.string()), + bio: z.optional(z.string()), + email: z.optional(z.string()), + id: z.optional(z.int()), + nickname: z.optional(z.string()), + permission_level: z.optional(z.int()), + subtitle: z.optional(z.string()), + user_id: z.optional(z.string()), + username: z.optional(z.string()), + uuid: z.optional(z.string()) +}); + +export const zDataUserSearchDoc = z.object({ + avatar: z.optional(z.string()), + email: z.optional(z.string()), + nickname: z.optional(z.string()), + subtitle: z.optional(z.string()), + type: z.optional(z.string()), + user_id: z.optional(z.string()), + username: z.optional(z.string()) +}); + +export const zServiceAuthExchangeData = z.object({ + client_id: z.optional(z.string()), + redirect_uri: z.optional(z.string()), + state: z.optional(z.string()) +}); + +export const zServiceAuthExchangeResponse = z.object({ + redirect_uri: z.optional(z.string()) +}); + +export const zServiceAuthMagicData = z.object({ + client_id: z.optional(z.string()), + client_ip: z.optional(z.string()), + email: z.optional(z.string()), + redirect_uri: z.optional(z.string()), + state: z.optional(z.string()), + turnstile_token: z.optional(z.string()) +}); + +export const zServiceAuthMagicResponse = z.object({ + uri: z.optional(z.string()) +}); + +export const zServiceAuthRefreshData = z.object({ + refresh_token: z.optional(z.string()) +}); + +export const zServiceAuthTokenData = z.object({ + code: z.optional(z.string()) +}); + +export const zServiceAuthTokenResponse = z.object({ + access_token: z.optional(z.string()), + refresh_token: z.optional(z.string()) +}); + +export const zServiceEventCheckinQueryResponse = z.object({ + checkin_at: z.optional(z.string()) +}); + +export const zServiceEventCheckinResponse = z.object({ + checkin_code: z.optional(z.string()) +}); + +export const zServiceEventCheckinSubmitData = z.object({ + checkin_code: z.optional(z.string()) +}); + +export const zServiceEventInfoResponse = z.object({ + end_time: z.optional(z.string()), + name: z.optional(z.string()), + start_time: z.optional(z.string()) +}); + +export const zServiceUserUserInfoData = z.object({ + allow_public: z.optional(z.boolean()), + avatar: z.optional(z.string()), + bio: z.optional(z.string()), + email: z.optional(z.string()), + nickname: z.optional(z.string()), + permission_level: z.optional(z.int()), + subtitle: z.optional(z.string()), + user_id: z.optional(z.string()), + username: z.optional(z.string()) +}); + +export const zServiceUserUserTableResponse = z.object({ + user_table: z.optional(z.array(zDataUser)) +}); + +export const zUtilsRespStatus = z.object({ + code: z.optional(z.int()), + data: z.optional(z.unknown()), + error_id: z.optional(z.string()), + status: z.optional(z.string()) +}); + +export const zPostAuthExchangeData = z.object({ + body: zServiceAuthExchangeData, + path: z.optional(z.never()), + query: z.optional(z.never()) +}); + +/** + * Successful exchange + */ +export const zPostAuthExchangeResponse = zUtilsRespStatus.and(z.object({ + data: z.optional(zServiceAuthExchangeResponse) +})); + +export const zPostAuthMagicData = z.object({ + body: zServiceAuthMagicData, + path: z.optional(z.never()), + query: z.optional(z.never()) +}); + +/** + * Successful request + */ +export const zPostAuthMagicResponse = zUtilsRespStatus.and(z.object({ + data: z.optional(zServiceAuthMagicResponse) +})); + +export const zGetAuthRedirectData = z.object({ + body: z.optional(z.never()), + path: z.optional(z.never()), + query: z.object({ + client_id: z.string(), + redirect_uri: z.string(), + code: z.string(), + state: z.optional(z.string()) + }) +}); + +export const zPostAuthRefreshData = z.object({ + body: zServiceAuthRefreshData, + path: z.optional(z.never()), + query: z.optional(z.never()) +}); + +/** + * Successful rotation + */ +export const zPostAuthRefreshResponse = zUtilsRespStatus.and(z.object({ + data: z.optional(zServiceAuthTokenResponse) +})); + +export const zPostAuthTokenData = z.object({ + body: zServiceAuthTokenData, + path: z.optional(z.never()), + query: z.optional(z.never()) +}); + +/** + * Successful token issuance + */ +export const zPostAuthTokenResponse = zUtilsRespStatus.and(z.object({ + data: z.optional(zServiceAuthTokenResponse) +})); + +export const zGetEventCheckinData = z.object({ + body: z.optional(z.never()), + path: z.optional(z.never()), + query: z.object({ + event_id: z.string() + }) +}); + +/** + * Successfully generated code + */ +export const zGetEventCheckinResponse = zUtilsRespStatus.and(z.object({ + data: z.optional(zServiceEventCheckinResponse) +})); + +export const zGetEventCheckinQueryData = z.object({ + body: z.optional(z.never()), + path: z.optional(z.never()), + query: z.object({ + event_id: z.string() + }) +}); + +/** + * Current attendance status + */ +export const zGetEventCheckinQueryResponse = zUtilsRespStatus.and(z.object({ + data: z.optional(zServiceEventCheckinQueryResponse) +})); + +export const zPostEventCheckinSubmitData = z.object({ + body: zServiceEventCheckinSubmitData, + path: z.optional(z.never()), + query: z.optional(z.never()) +}); + +/** + * Attendance marked successfully + */ +export const zPostEventCheckinSubmitResponse = zUtilsRespStatus.and(z.object({ + data: z.optional(z.record(z.string(), z.unknown())) +})); + +export const zGetEventInfoData = z.object({ + body: z.optional(z.never()), + path: z.optional(z.never()), + query: z.object({ + event_id: z.string() + }) +}); + +/** + * Successful retrieval + */ +export const zGetEventInfoResponse = zUtilsRespStatus.and(z.object({ + data: z.optional(zServiceEventInfoResponse) +})); + +export const zGetUserFullData = z.object({ + body: z.optional(z.never()), + path: z.optional(z.never()), + query: z.optional(z.never()) +}); + +/** + * Successful retrieval of full user table + */ +export const zGetUserFullResponse = zUtilsRespStatus.and(z.object({ + data: z.optional(zServiceUserUserTableResponse) +})); + +export const zGetUserInfoData = z.object({ + body: z.optional(z.never()), + path: z.optional(z.never()), + query: z.optional(z.never()) +}); + +/** + * Successful profile retrieval + */ +export const zGetUserInfoResponse = zUtilsRespStatus.and(z.object({ + data: z.optional(zServiceUserUserInfoData) +})); + +export const zGetUserListData = z.object({ + body: z.optional(z.never()), + path: z.optional(z.never()), + query: z.object({ + limit: z.optional(z.string()), + offset: z.string() + }) +}); + +/** + * Successful paginated list retrieval + */ +export const zGetUserListResponse = zUtilsRespStatus.and(z.object({ + data: z.optional(z.array(zDataUserSearchDoc)) +})); + +export const zPatchUserUpdateData = z.object({ + body: zServiceUserUserInfoData, + path: z.optional(z.never()), + query: z.optional(z.never()) +}); + +/** + * Successful profile update + */ +export const zPatchUserUpdateResponse = zUtilsRespStatus.and(z.object({ + data: z.optional(z.record(z.string(), z.unknown())) +})); diff --git a/client/cms/src/components/checkin/qr-dialog.tsx b/client/cms/src/components/checkin/qr-dialog.tsx index f4bb318..a1fa442 100644 --- a/client/cms/src/components/checkin/qr-dialog.tsx +++ b/client/cms/src/components/checkin/qr-dialog.tsx @@ -9,7 +9,6 @@ import { DialogTrigger, } from '@/components/ui/dialog'; import { QRCode } from '@/components/ui/shadcn-io/qr-code'; -import { useCheckinCode } from '@/hooks/data/useGetCheckInCode'; import { Button } from '../ui/button'; export function QrDialog( @@ -35,23 +34,24 @@ export function QrDialog( } function QrSection({ eventId, enabled }: { eventId: string; enabled: boolean }) { - const { data } = useCheckinCode(eventId, enabled); + // const { data } = useCheckinCode(eventId, enabled); + const data = { data: { checkin_code: `dummy${eventId}${enabled}` } }; return data ? ( - <> -
- + <> +
+ +
+ +
+ {data.data.checkin_code}
- -
- {data.data.checkin_code} -
-
- - ) +
+ + ) : ( - - ); + + ); } function QrSectionSkeleton() { diff --git a/client/cms/src/components/login-form.tsx b/client/cms/src/components/login-form.tsx index 8ef955b..5447440 100644 --- a/client/cms/src/components/login-form.tsx +++ b/client/cms/src/components/login-form.tsx @@ -32,7 +32,7 @@ export function LoginForm({ event.preventDefault(); const formData = new FormData(formRef.current!); const email = formData.get('email')! as string; - mutateAsync({ email, turnstile_token: token!, ...oauthParams }).then(() => { + mutateAsync({ body: { email, turnstile_token: token!, ...oauthParams } }).then(() => { void navigate({ to: '/magicLinkSent', search: { email } }); }).catch((error) => { console.error(error); diff --git a/client/cms/src/components/profile/edit-profile-dialog.tsx b/client/cms/src/components/profile/edit-profile-dialog.tsx index e9efd34..c98d4c7 100644 --- a/client/cms/src/components/profile/edit-profile-dialog.tsx +++ b/client/cms/src/components/profile/edit-profile-dialog.tsx @@ -29,7 +29,8 @@ const formSchema = z.object({ avatar: z.url().min(1), }); export function EditProfileDialog() { - const { data: user } = useUserInfo(); + const { data } = useUserInfo(); + const user = data.data!; const { mutateAsync } = useUpdateUser(); const form = useForm({ @@ -46,7 +47,7 @@ export function EditProfileDialog() { value, }) => { try { - await mutateAsync(value); + await mutateAsync({ body: value }); toast.success('个人资料更新成功'); } catch (error) { diff --git a/client/cms/src/components/profile/main-profile.tsx b/client/cms/src/components/profile/main-profile.tsx index 573f3e5..425d632 100644 --- a/client/cms/src/components/profile/main-profile.tsx +++ b/client/cms/src/components/profile/main-profile.tsx @@ -12,8 +12,9 @@ import { Button } from '../ui/button'; import { EditProfileDialog } from './edit-profile-dialog'; export function MainProfile() { - const { data: user } = useUserInfo(); - const [bio, setBio] = useState(() => base64ToUtf8(user.bio)); + const { data } = useUserInfo(); + const user = data.data!; + const [bio, setBio] = useState(() => base64ToUtf8(user.bio ?? '')); const [enableBioEdit, setEnableBioEdit] = useState(false); const { mutateAsync } = useUpdateUser(); @@ -61,7 +62,7 @@ export function MainProfile() { else { if (!isNil(bio)) { try { - await mutateAsync({ bio: utf8ToBase64(bio) }); + await mutateAsync({ body: { bio: utf8ToBase64(bio) } }); setEnableBioEdit(false); } catch (error) { diff --git a/client/cms/src/components/sidebar/nav-user.tsx b/client/cms/src/components/sidebar/nav-user.tsx index 0917530..3da5d56 100644 --- a/client/cms/src/components/sidebar/nav-user.tsx +++ b/client/cms/src/components/sidebar/nav-user.tsx @@ -29,7 +29,8 @@ import { Skeleton } from '../ui/skeleton'; function NavUser_() { const { isMobile } = useSidebar(); - const { data: user } = useUserInfo(); + const { data } = useUserInfo(); + const user = data.data!; const { logout } = useLogout(); return ( diff --git a/client/cms/src/hooks/data/useExchangeToken.ts b/client/cms/src/hooks/data/useExchangeToken.ts new file mode 100644 index 0000000..99c5df8 --- /dev/null +++ b/client/cms/src/hooks/data/useExchangeToken.ts @@ -0,0 +1,16 @@ +import { postAuthExchangeMutation } from "@/client/@tanstack/react-query.gen"; +import { useMutation } from "@tanstack/react-query"; +import { toast } from "sonner"; + +export function useExchangeToken() { + return useMutation({ + ...postAuthExchangeMutation(), + onSuccess: (data) => { + window.location.href = data.data?.redirect_uri!; + }, + onError: (error) => { + console.error(error); + toast("An error occurred while exchanging the token. Please login manually."); + } + }) +} diff --git a/client/cms/src/hooks/data/useGetCheckInCode.ts b/client/cms/src/hooks/data/useGetCheckInCode.ts deleted file mode 100644 index e4d5f05..0000000 --- a/client/cms/src/hooks/data/useGetCheckInCode.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { useQuery } from '@tanstack/react-query'; -import { axiosClient } from '@/lib/axios'; - -export function useCheckinCode(eventId: string, enabled: boolean) { - return useQuery({ - queryKey: ['getCheckinCode', eventId], - queryFn: async () => { - return axiosClient.get<{ - checkin_code: string; - }>('/user/checkin', { - params: { - event_id: eventId, - }, - }); - }, - enabled, - }); -} diff --git a/client/cms/src/hooks/data/useGetMagicLink.ts b/client/cms/src/hooks/data/useGetMagicLink.ts index 4be1b04..0fc0e61 100644 --- a/client/cms/src/hooks/data/useGetMagicLink.ts +++ b/client/cms/src/hooks/data/useGetMagicLink.ts @@ -1,16 +1,8 @@ -import type { AuthorizeSearchParams } from '@/routes/authorize'; +import { postAuthMagicMutation } from '@/client/@tanstack/react-query.gen'; import { useMutation } from '@tanstack/react-query'; -import { axiosClient } from '@/lib/axios'; - -interface GetMagicLinkPayload extends AuthorizeSearchParams { - email: string; - turnstile_token: string; -} export function useGetMagicLink() { return useMutation({ - mutationFn: async (payload: GetMagicLinkPayload) => { - return axiosClient.post<{ status: string }>('/auth/magic', payload); - }, - }); + ...postAuthMagicMutation() + }) } diff --git a/client/cms/src/hooks/data/useUpdateUser.ts b/client/cms/src/hooks/data/useUpdateUser.ts index 8958939..6988dca 100644 --- a/client/cms/src/hooks/data/useUpdateUser.ts +++ b/client/cms/src/hooks/data/useUpdateUser.ts @@ -1,22 +1,12 @@ +import { getUserInfoQueryKey, patchUserUpdateMutation } from '@/client/@tanstack/react-query.gen'; import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { axiosClient } from '@/lib/axios'; - -interface UpdateUserPayload { - avatar?: string; - bio?: string; - nickname?: string; - subtitle?: string; - username?: string; -} export function useUpdateUser() { const queryClient = useQueryClient(); return useMutation({ - mutationFn: async (payload: UpdateUserPayload) => { - return axiosClient.patch<{ status: string }>('/user/update', payload); - }, + ...patchUserUpdateMutation(), onSuccess: async () => { - await queryClient.invalidateQueries({ queryKey: ['userInfo'] }); + await queryClient.invalidateQueries({ queryKey: getUserInfoQueryKey() }); }, - }); + }) } diff --git a/client/cms/src/hooks/data/useUserInfo.ts b/client/cms/src/hooks/data/useUserInfo.ts index 3f8394c..fde0174 100644 --- a/client/cms/src/hooks/data/useUserInfo.ts +++ b/client/cms/src/hooks/data/useUserInfo.ts @@ -1,23 +1,9 @@ +import { getUserInfoOptions } from '@/client/@tanstack/react-query.gen'; import { useSuspenseQuery } from '@tanstack/react-query'; -import { axiosClient } from '@/lib/axios'; export function useUserInfo() { return useSuspenseQuery({ - queryKey: ['userInfo'], - queryFn: async () => { - const response = await axiosClient.get<{ - username: string; - user_id: string; - email: string; - type: string; - nickname: string; - subtitle: string; - avatar: string; - bio: string; - } - >('/user/info'); - return response.data; - }, - staleTime: 10 * 60 * 1000, + ...getUserInfoOptions(), + staleTime: 10 * 60 * 1000 }); } diff --git a/client/cms/src/hooks/data/useValidateMagicLink.ts b/client/cms/src/hooks/data/useValidateMagicLink.ts deleted file mode 100644 index 7f59477..0000000 --- a/client/cms/src/hooks/data/useValidateMagicLink.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { useSuspenseQuery } from '@tanstack/react-query'; -import { axiosClient } from '@/lib/axios'; - -export function useValidateMagicLink(ticket: string) { - return useSuspenseQuery({ - queryKey: ['validateMagicLink', ticket], - queryFn: async () => { - return axiosClient.get<{ access_token: string; refresh_token: string }>('/auth/magic/verify', { params: { token: ticket } }); - }, - }); -} diff --git a/client/cms/src/lib/axios.ts b/client/cms/src/lib/axios.ts deleted file mode 100644 index f4d8db5..0000000 --- a/client/cms/src/lib/axios.ts +++ /dev/null @@ -1,67 +0,0 @@ -import type { AxiosError, AxiosRequestConfig } from 'axios'; -import type { JsonValue } from 'type-fest'; -import axios from 'axios'; -import { isNil } from 'lodash-es'; -import { router } from '@/lib/router'; -import { clearTokens, doRefreshToken, getRefreshToken, getToken, setRefreshToken, setToken } from './token'; - -export const HEADER_API_VERSION = { - 'X-Api-Version': 'latest', -}; - -export const axiosClient = axios.create({ - baseURL: '/api/v1/', - headers: HEADER_API_VERSION, -}); - -axiosClient.interceptors.request.use((config) => { - const token = getToken(); - if (token !== null) { - config.headers = config.headers ?? {}; - config.headers.Authorization = `Bearer ${token}`; - } - return config; -}); - -type RetryConfig = AxiosRequestConfig & { _retry?: boolean }; - -interface ResponseData { - code: number; - error_id: string; - status: string; - data: JsonValue; -} - -axiosClient.interceptors.response.use(async (response) => { - const data = response.data as ResponseData; - if (data.code !== 200) { - return Promise.reject(data); - } - response.data = data.data; - return response; -}, async (error: AxiosError) => { - const originalRequest = error.config as RetryConfig | undefined; - if (!error.response || error.response.status !== 401 || !originalRequest) { - return Promise.reject(error); - } - if (error.response.status === 401 && originalRequest.url !== '/auth/refresh' && !isNil(getRefreshToken())) { - try { - const maybeRefreshTokenResponse = await doRefreshToken(); - if (maybeRefreshTokenResponse.status !== 200) { - throw new Error('Failed to refresh token'); - } - const { access_token, refresh_token } = maybeRefreshTokenResponse.data; - originalRequest.headers = originalRequest.headers ?? {}; - originalRequest.headers.Authorization = `Bearer ${access_token}`; - setToken(access_token); - setRefreshToken(refresh_token); - return await axiosClient(originalRequest); - } - // eslint-disable-next-line unused-imports/no-unused-vars - catch (e) { - // Should remove token (tokens are out of date) - clearTokens(); - await router.navigate({ to: '/authorize' }); - } - } -}); diff --git a/client/cms/src/lib/client.ts b/client/cms/src/lib/client.ts new file mode 100644 index 0000000..be66d6d --- /dev/null +++ b/client/cms/src/lib/client.ts @@ -0,0 +1,63 @@ +import { + getToken, + getRefreshToken, + setToken, + setRefreshToken, + clearTokens, + doRefreshToken +} from "./token"; +import { router } from "./router"; +import { isEmpty, + isNil } from "lodash-es"; +import { client } from "@/client/client.gen"; + +export function configInternalApiClient() { + client.setConfig({ + baseUrl: '/api/v1/', + headers: { + 'X-Api-Version': 'latest', + }, + }); + + client.interceptors.request.use((request) => { + const token = getToken(); + if (token) { + request.headers.set('Authorization', `Bearer ${token}`); + } + return request; + }); + + client.interceptors.response.use(async (response, request, options) => { + if (response.status === 401) { + const refreshToken = getRefreshToken(); + // Avoid infinite loop if the refresh token request itself fails + if (!request.url.includes('/auth/refresh') && !isNil(refreshToken)) { + try { + const refreshResponse = await doRefreshToken(); + if (!isEmpty(refreshResponse)) { + const { access_token, refresh_token } = refreshResponse; + setToken(access_token!); + setRefreshToken(refresh_token!); + + const fetchFn = options.fetch ?? globalThis.fetch; + const headers = new Headers(request.headers); + headers.set('Authorization', `Bearer ${access_token}`); + + return fetchFn(request.url, { + method: request.method, + headers, + body: (options.serializedBody ?? options.body) as BodyInit | null | undefined, + signal: request.signal, + }); + } + } catch (e) { + clearTokens(); + await router.navigate({ to: '/authorize' }); + return response; + } + } + } + return response; + }); + +} diff --git a/client/cms/src/lib/token.ts b/client/cms/src/lib/token.ts index 9c7c57c..e671f98 100644 --- a/client/cms/src/lib/token.ts +++ b/client/cms/src/lib/token.ts @@ -1,4 +1,4 @@ -import { axiosClient, HEADER_API_VERSION } from './axios'; +import { postAuthRefresh, type ServiceAuthTokenResponse } from '@/client'; export function setToken(token: string) { localStorage.setItem('token', token); @@ -29,18 +29,11 @@ export function clearTokens() { setRefreshToken(''); } -export async function doSetTokenByCode(code: string) { - return new Promise((resolve, reject) => { - axiosClient.post<{ access_token: string; refresh_token: string }>('/auth/token', { code }, { headers: HEADER_API_VERSION }).then(({ data }) => { - setToken(data.access_token); - setRefreshToken(data.refresh_token); - resolve(); - }).catch((error) => { - reject(error); - }); +export async function doRefreshToken(): Promise { + const { data } = await postAuthRefresh({ + body: { + refresh_token: getRefreshToken()! + } }); -} - -export async function doRefreshToken() { - return axiosClient.post<{ access_token: string; refresh_token: string }>('/auth/refresh', { refresh_token: getRefreshToken() }, { headers: HEADER_API_VERSION }); + return data?.data; } diff --git a/client/cms/src/main.tsx b/client/cms/src/main.tsx index 0489bf0..ef930e2 100644 --- a/client/cms/src/main.tsx +++ b/client/cms/src/main.tsx @@ -2,6 +2,9 @@ import { RouterProvider } from '@tanstack/react-router'; import { StrictMode } from 'react'; import ReactDOM from 'react-dom/client'; import { router } from '@/lib/router'; +import { configInternalApiClient } from './lib/client'; + +configInternalApiClient(); // Render the app const rootElement = document.getElementById('root')!; diff --git a/client/cms/src/routes/authorize.tsx b/client/cms/src/routes/authorize.tsx index 7f3c164..273088c 100644 --- a/client/cms/src/routes/authorize.tsx +++ b/client/cms/src/routes/authorize.tsx @@ -3,9 +3,10 @@ import { zodValidator } from '@tanstack/zod-adapter'; import { isNil } from 'lodash-es'; import z from 'zod'; import { LoginForm } from '@/components/login-form'; -import { axiosClient } from '@/lib/axios'; import { generateOAuthState } from '@/lib/random'; import { getToken } from '@/lib/token'; +import { useExchangeToken } from '@/hooks/data/useExchangeToken'; +import { useEffect } from 'react'; const authorizeSchema = z.object({ response_type: z.literal('code').default('code'), @@ -24,22 +25,21 @@ export const Route = createFileRoute('/authorize')({ function RouteComponent() { const token = getToken(); const oauthParams = Route.useSearch(); + const mutation = useExchangeToken(); /** * Auth by Token Flow */ - if (!isNil(token)) { - axiosClient.post<{ redirect_uri: string }>('/auth/exchange', { - client_id: oauthParams.client_id, - redirect_uri: oauthParams.redirect_uri, - state: oauthParams.state, - }).then((res) => { - window.location.href = res.data.redirect_uri; - }).catch((e) => { - console.error(e); - return 'Token exchange failed'; - }); - return 'Redirecting'; - } + useEffect(() => { + if (!isNil(token) && mutation.isIdle) { + mutation.mutate({ + body: { + client_id: oauthParams.client_id, + redirect_uri: oauthParams.redirect_uri, + state: oauthParams.state, + } + }); + } + }, [token, mutation.isIdle]); return (
diff --git a/client/cms/src/routes/token.tsx b/client/cms/src/routes/token.tsx index 78f8bc7..0f6fdb7 100644 --- a/client/cms/src/routes/token.tsx +++ b/client/cms/src/routes/token.tsx @@ -1,7 +1,11 @@ import { createFileRoute, useNavigate } from '@tanstack/react-router'; -import { useEffect, useState } from 'react'; +import { + useEffect, + useState } from 'react'; import z from 'zod'; -import { doSetTokenByCode } from '@/lib/token'; +import { postAuthTokenMutation } from '@/client/@tanstack/react-query.gen'; +import { useMutation } from '@tanstack/react-query'; +import { setRefreshToken, setToken } from '@/lib/token'; const tokenCodeSchema = z.object({ code: z.string().nonempty(), @@ -17,14 +21,23 @@ function RouteComponent() { const [status, setStatus] = useState('Loading...'); const navigate = useNavigate(); - useEffect(() => { - doSetTokenByCode(code).then(() => { + const mutation = useMutation({ + ...postAuthTokenMutation(), + onSuccess: (data) => { + setToken(data.data?.access_token!) + setRefreshToken(data.data?.refresh_token!) void navigate({ to: '/' }); - }).catch((_) => { + }, + onError: () => { setStatus('Error getting token'); - }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + } + }) + + useEffect(() => { + if (mutation.isIdle) { + mutation.mutate({ body: { code } }) + } + }, []) return
{status}
; }