@gatwech/auth 1.0.0-beta.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -0
- package/dist/access-BO2g7QVe.mjs +45 -0
- package/dist/access-C6O_osEz.mjs +79 -0
- package/dist/access-DT1f6l5k.mjs +51 -0
- package/dist/adapter-factory-BZ2Qiqpn.mjs +640 -0
- package/dist/adapters/drizzle-adapter/index.d.mts +47 -0
- package/dist/adapters/drizzle-adapter/index.mjs +208 -0
- package/dist/adapters/index.d.mts +132 -0
- package/dist/adapters/index.mjs +18 -0
- package/dist/adapters/kysely-adapter/index.d.mts +47 -0
- package/dist/adapters/kysely-adapter/index.mjs +11 -0
- package/dist/adapters/memory-adapter/index.d.mts +14 -0
- package/dist/adapters/memory-adapter/index.mjs +154 -0
- package/dist/adapters/mongodb-adapter/index.d.mts +35 -0
- package/dist/adapters/mongodb-adapter/index.mjs +315 -0
- package/dist/adapters/prisma-adapter/index.d.mts +34 -0
- package/dist/adapters/prisma-adapter/index.mjs +159 -0
- package/dist/adapters/test.d.mts +81 -0
- package/dist/adapters/test.mjs +793 -0
- package/dist/admin-Bcq53Nwj.mjs +743 -0
- package/dist/admin-DtfO1PLg.d.mts +1689 -0
- package/dist/anonymous-CRt1imBW.mjs +107 -0
- package/dist/api/index.d.mts +6 -0
- package/dist/api/index.mjs +17 -0
- package/dist/api-0pR_I-Nx.mjs +3139 -0
- package/dist/auth-CBUoBX6o.mjs +233 -0
- package/dist/bearer-Djf_awfo.mjs +63 -0
- package/dist/bun-sqlite-dialect-CeNvMaZz.mjs +155 -0
- package/dist/captcha-BqaMxlKC.mjs +178 -0
- package/dist/chunk-BK_LfMhE.mjs +40 -0
- package/dist/client/index.d.mts +186 -0
- package/dist/client/index.mjs +7 -0
- package/dist/client/plugins/index.d.mts +949 -0
- package/dist/client/plugins/index.mjs +539 -0
- package/dist/client/react/index.d.mts +179 -0
- package/dist/client/react/index.mjs +69 -0
- package/dist/client-BOX93C3d.d.mts +1250 -0
- package/dist/client-CNY0tn9g.d.mts +628 -0
- package/dist/client-CygJhqeB.mjs +29 -0
- package/dist/client-WiN43tAS.mjs +17 -0
- package/dist/client-aExCkfwm.mjs +30 -0
- package/dist/cookies/index.d.mts +7 -0
- package/dist/cookies/index.mjs +7 -0
- package/dist/cookies-cJObLwn1.mjs +597 -0
- package/dist/crypto/index.d.mts +44 -0
- package/dist/crypto/index.mjs +4 -0
- package/dist/crypto-Bb42eMrH.mjs +70 -0
- package/dist/custom-session-L3t7ecsd.mjs +67 -0
- package/dist/date-Cx2yCiC4.mjs +115 -0
- package/dist/db/index.d.mts +7 -0
- package/dist/db/index.mjs +15 -0
- package/dist/db-BrstmDhc.mjs +1124 -0
- package/dist/device-authorization-V4s3h4Mj.mjs +557 -0
- package/dist/dialect-CeddeGHf.mjs +72 -0
- package/dist/email-otp-BtDHSw14.mjs +592 -0
- package/dist/esm-C-QicDNz.mjs +4090 -0
- package/dist/generic-oauth-CL5X27ZI.mjs +445 -0
- package/dist/get-request-ip-DSjrEpGo.mjs +27 -0
- package/dist/get-tables-BQP7YW9c.mjs +249 -0
- package/dist/has-permission-BpAr7uCn.mjs +14 -0
- package/dist/haveibeenpwned-cMpJvmzI.mjs +43 -0
- package/dist/helper-tmxW3BHj.d.mts +21 -0
- package/dist/index-34ONrP8l.d.mts +503 -0
- package/dist/index-65G5U5DB-DSdjzreF.d.mts +2543 -0
- package/dist/index-B-XKqFvY.d.mts +30 -0
- package/dist/index-B3n8e37d.d.mts +165 -0
- package/dist/index-BBm8nQ9Q.d.mts +42 -0
- package/dist/index-BHoQlf7a.d.mts +7699 -0
- package/dist/index-BYMI9r_d.d.mts +257 -0
- package/dist/index-BqhKCbPH.d.mts +36 -0
- package/dist/index-Buhn3hkR.d.mts +433 -0
- package/dist/index-C-u1h_WX.d.mts +174 -0
- package/dist/index-C7fQuk-V.d.mts +41 -0
- package/dist/index-CM-CFWwr.d.mts +1919 -0
- package/dist/index-CuFzhoS6.d.mts +359 -0
- package/dist/index-Cxi7TbLE.d.mts +11347 -0
- package/dist/index-DB7t8whN.d.mts +1 -0
- package/dist/index-DKNNV1kb.d.mts +626 -0
- package/dist/index-DR0xXno9.d.mts +113 -0
- package/dist/index-D_k662QG.d.mts +833 -0
- package/dist/index-DaBcikv9.d.mts +118 -0
- package/dist/index-DlaMq1nb.d.mts +955 -0
- package/dist/index-DoXZYQpq.d.mts +290 -0
- package/dist/index-G6V0Z_Mn.d.mts +125 -0
- package/dist/index-z0KSvE5i.d.mts +246 -0
- package/dist/index-zjjNBRY2.d.mts +95 -0
- package/dist/index.d.mts +12 -0
- package/dist/index.mjs +33 -0
- package/dist/integrations/next-js.d.mts +21 -0
- package/dist/integrations/next-js.mjs +62 -0
- package/dist/integrations/node.d.mts +15 -0
- package/dist/integrations/node.mjs +15 -0
- package/dist/integrations/react-start.d.mts +15 -0
- package/dist/integrations/react-start.mjs +46 -0
- package/dist/json-D0TBLy4q.mjs +24 -0
- package/dist/jwt-CmxZAGe2.mjs +94 -0
- package/dist/jwt-lHV4QHpD.mjs +387 -0
- package/dist/kysely-adapter-DpzYO8Yf.mjs +181 -0
- package/dist/magic-link-Bk-ueorR.mjs +176 -0
- package/dist/misc-beA1w3XV.mjs +7 -0
- package/dist/multi-session-DRrLKrg9.mjs +144 -0
- package/dist/node-sqlite-dialect-BcKxBQnm.mjs +155 -0
- package/dist/oauth-proxy-D1sWh9B_.mjs +133 -0
- package/dist/oauth2/index.d.mts +60 -0
- package/dist/oauth2/index.mjs +19 -0
- package/dist/one-tap-DMswkTEc.mjs +113 -0
- package/dist/organization-BArHO_5v.mjs +3857 -0
- package/dist/parser-DcrIEuXE.mjs +73 -0
- package/dist/passkey-CcD4CnhM.mjs +558 -0
- package/dist/password-sVC4nePO.mjs +25 -0
- package/dist/permission-DLYUF3cW.mjs +15 -0
- package/dist/phone-number-rHDIlh-2.mjs +485 -0
- package/dist/plugin-helper-4L-lzsjZ.mjs +16 -0
- package/dist/plugins/access/index.d.mts +3 -0
- package/dist/plugins/access/index.mjs +3 -0
- package/dist/plugins/admin/access/index.d.mts +116 -0
- package/dist/plugins/admin/access/index.mjs +4 -0
- package/dist/plugins/admin/index.d.mts +9 -0
- package/dist/plugins/admin/index.mjs +22 -0
- package/dist/plugins/anonymous/index.d.mts +7 -0
- package/dist/plugins/anonymous/index.mjs +18 -0
- package/dist/plugins/bearer/index.d.mts +2 -0
- package/dist/plugins/bearer/index.mjs +8 -0
- package/dist/plugins/captcha/index.d.mts +2 -0
- package/dist/plugins/captcha/index.mjs +4 -0
- package/dist/plugins/custom-session/index.d.mts +7 -0
- package/dist/plugins/custom-session/index.mjs +19 -0
- package/dist/plugins/device-authorization/index.d.mts +4 -0
- package/dist/plugins/device-authorization/index.mjs +18 -0
- package/dist/plugins/email-otp/index.d.mts +3 -0
- package/dist/plugins/email-otp/index.mjs +19 -0
- package/dist/plugins/generic-oauth/index.d.mts +7 -0
- package/dist/plugins/generic-oauth/index.mjs +18 -0
- package/dist/plugins/haveibeenpwned/index.d.mts +2 -0
- package/dist/plugins/haveibeenpwned/index.mjs +18 -0
- package/dist/plugins/index.d.mts +139 -0
- package/dist/plugins/index.mjs +47 -0
- package/dist/plugins/jwt/index.d.mts +7 -0
- package/dist/plugins/jwt/index.mjs +18 -0
- package/dist/plugins/magic-link/index.d.mts +2 -0
- package/dist/plugins/magic-link/index.mjs +18 -0
- package/dist/plugins/multi-session/index.d.mts +3 -0
- package/dist/plugins/multi-session/index.mjs +18 -0
- package/dist/plugins/oauth-proxy/index.d.mts +2 -0
- package/dist/plugins/oauth-proxy/index.mjs +18 -0
- package/dist/plugins/one-tap/index.d.mts +2 -0
- package/dist/plugins/one-tap/index.mjs +18 -0
- package/dist/plugins/one-time-token/index.d.mts +7 -0
- package/dist/plugins/one-time-token/index.mjs +47 -0
- package/dist/plugins/organization/access/index.d.mts +4 -0
- package/dist/plugins/organization/access/index.mjs +4 -0
- package/dist/plugins/organization/index.d.mts +9 -0
- package/dist/plugins/organization/index.mjs +22 -0
- package/dist/plugins/passkey/index.d.mts +7 -0
- package/dist/plugins/passkey/index.mjs +18 -0
- package/dist/plugins/phone-number/index.d.mts +7 -0
- package/dist/plugins/phone-number/index.mjs +18 -0
- package/dist/plugins/two-factor/index.d.mts +7 -0
- package/dist/plugins/two-factor/index.mjs +20 -0
- package/dist/plugins/username/index.d.mts +4 -0
- package/dist/plugins/username/index.mjs +18 -0
- package/dist/plugins-3jfNste3.mjs +1544 -0
- package/dist/plugins-hFZT3hmK.d.mts +14 -0
- package/dist/promise-8rrEHRXd.mjs +23154 -0
- package/dist/proxy-CL9PZXJA.mjs +441 -0
- package/dist/session-Dgp133Yt.mjs +413 -0
- package/dist/social-providers/index.d.mts +1 -0
- package/dist/social-providers/index.mjs +3 -0
- package/dist/src/adapters/adapter-factory/index.d.ts +10 -0
- package/dist/src/adapters/adapter-factory/types.d.ts +124 -0
- package/dist/src/adapters/create-test-suite.d.ts +97 -0
- package/dist/src/adapters/drizzle-adapter/drizzle-adapter.d.ts +43 -0
- package/dist/src/adapters/drizzle-adapter/index.d.ts +1 -0
- package/dist/src/adapters/drizzle-adapter/test/generate-schema.d.ts +12 -0
- package/dist/src/adapters/index.d.ts +21 -0
- package/dist/src/adapters/kysely-adapter/bun-sqlite-dialect.d.ts +58 -0
- package/dist/src/adapters/kysely-adapter/dialect.d.ts +13 -0
- package/dist/src/adapters/kysely-adapter/index.d.ts +3 -0
- package/dist/src/adapters/kysely-adapter/kysely-adapter.d.ts +32 -0
- package/dist/src/adapters/kysely-adapter/node-sqlite-dialect.d.ts +58 -0
- package/dist/src/adapters/kysely-adapter/types.d.ts +1 -0
- package/dist/src/adapters/memory-adapter/index.d.ts +1 -0
- package/dist/src/adapters/memory-adapter/memory-adapter.d.ts +9 -0
- package/dist/src/adapters/mongodb-adapter/index.d.ts +1 -0
- package/dist/src/adapters/mongodb-adapter/mongodb-adapter.d.ts +31 -0
- package/dist/src/adapters/prisma-adapter/index.d.ts +1 -0
- package/dist/src/adapters/prisma-adapter/prisma-adapter.d.ts +32 -0
- package/dist/src/adapters/prisma-adapter/test/auth.d.ts +3 -0
- package/dist/src/adapters/prisma-adapter/test/generate-auth-config.d.ts +2 -0
- package/dist/src/adapters/prisma-adapter/test/generate-prisma-schema.d.ts +3 -0
- package/dist/src/adapters/prisma-adapter/test/get-prisma-client.d.ts +7 -0
- package/dist/src/adapters/prisma-adapter/test/push-prisma-schema.d.ts +1 -0
- package/dist/src/adapters/test-adapter.d.ts +61 -0
- package/dist/src/adapters/test.d.ts +78 -0
- package/dist/src/adapters/tests/auth-flow.d.ts +22 -0
- package/dist/src/adapters/tests/index.d.ts +5 -0
- package/dist/src/adapters/tests/normal.d.ts +87 -0
- package/dist/src/adapters/tests/number-id.d.ts +17 -0
- package/dist/src/adapters/tests/performance.d.ts +24 -0
- package/dist/src/adapters/tests/transactions.d.ts +20 -0
- package/dist/src/adapters/utils.d.ts +3 -0
- package/dist/src/api/index.d.ts +7150 -0
- package/dist/src/api/middlewares/index.d.ts +2 -0
- package/dist/src/api/middlewares/oauth.d.ts +15 -0
- package/dist/src/api/middlewares/origin-check.d.ts +8 -0
- package/dist/src/api/rate-limiter/index.d.ts +13 -0
- package/dist/src/api/routes/account.d.ts +623 -0
- package/dist/src/api/routes/callback.d.ts +68 -0
- package/dist/src/api/routes/email-verification.d.ts +266 -0
- package/dist/src/api/routes/error.d.ts +51 -0
- package/dist/src/api/routes/index.d.ts +11 -0
- package/dist/src/api/routes/ok.d.ts +61 -0
- package/dist/src/api/routes/reset-password.d.ts +203 -0
- package/dist/src/api/routes/session.d.ts +586 -0
- package/dist/src/api/routes/sign-in.d.ts +298 -0
- package/dist/src/api/routes/sign-out.d.ts +59 -0
- package/dist/src/api/routes/sign-up.d.ts +625 -0
- package/dist/src/api/routes/update-user.d.ts +862 -0
- package/dist/src/api/to-auth-endpoints.d.ts +3 -0
- package/dist/src/auth.d.ts +25 -0
- package/dist/src/client/broadcast-channel.d.ts +12 -0
- package/dist/src/client/config.d.ts +85 -0
- package/dist/src/client/fetch-plugins.d.ts +14 -0
- package/dist/src/client/index.d.ts +15 -0
- package/dist/src/client/parser.d.ts +12 -0
- package/dist/src/client/path-to-object.d.ts +63 -0
- package/dist/src/client/plugins/index.d.ts +21 -0
- package/dist/src/client/plugins/infer-plugin.d.ts +11 -0
- package/dist/src/client/proxy.d.ts +8 -0
- package/dist/src/client/query.d.ts +16 -0
- package/dist/src/client/react/index.d.ts +130 -0
- package/dist/src/client/react/react-store.d.ts +43 -0
- package/dist/src/client/session-atom.d.ts +18 -0
- package/dist/src/client/session-refresh.d.ts +17 -0
- package/dist/src/client/test-plugin.d.ts +147 -0
- package/dist/src/client/types.d.ts +49 -0
- package/dist/src/client/vanilla.d.ts +124 -0
- package/dist/src/cookies/check-cookies.d.ts +4 -0
- package/dist/src/cookies/cookie-utils.d.ts +16 -0
- package/dist/src/cookies/index.d.ts +56 -0
- package/dist/src/cookies/session-store.d.ts +43 -0
- package/dist/src/crypto/buffer.d.ts +4 -0
- package/dist/src/crypto/index.d.ts +14 -0
- package/dist/src/crypto/jwt.d.ts +4 -0
- package/dist/src/crypto/password.d.ts +5 -0
- package/dist/src/crypto/random.d.ts +1 -0
- package/dist/src/db/field.d.ts +97 -0
- package/dist/src/db/get-migration.d.ts +18 -0
- package/dist/src/db/get-schema.d.ts +6 -0
- package/dist/src/db/get-tables.d.ts +3 -0
- package/dist/src/db/index.d.ts +10 -0
- package/dist/src/db/internal-adapter.d.ts +9 -0
- package/dist/src/db/schema.d.ts +76 -0
- package/dist/src/db/to-zod.d.ts +31 -0
- package/dist/src/db/utils.d.ts +6 -0
- package/dist/src/db/with-hooks.d.ts +27 -0
- package/dist/src/index.d.ts +14 -0
- package/dist/src/init.d.ts +2 -0
- package/dist/src/integrations/next-js.d.ts +15 -0
- package/dist/src/integrations/node.d.ts +6 -0
- package/dist/src/integrations/react-start.d.ts +9 -0
- package/dist/src/oauth2/index.d.ts +4 -0
- package/dist/src/oauth2/link-account.d.ts +41 -0
- package/dist/src/oauth2/state.d.ts +21 -0
- package/dist/src/oauth2/utils.d.ts +3 -0
- package/dist/src/plugins/access/access.d.ts +25 -0
- package/dist/src/plugins/access/index.d.ts +2 -0
- package/dist/src/plugins/access/types.d.ts +14 -0
- package/dist/src/plugins/additional-fields/client.d.ts +91 -0
- package/dist/src/plugins/admin/access/index.d.ts +1 -0
- package/dist/src/plugins/admin/access/statement.d.ts +110 -0
- package/dist/src/plugins/admin/admin.d.ts +1570 -0
- package/dist/src/plugins/admin/client.d.ts +62 -0
- package/dist/src/plugins/admin/error-codes.d.ts +21 -0
- package/dist/src/plugins/admin/has-permission.d.ts +21 -0
- package/dist/src/plugins/admin/index.d.ts +2 -0
- package/dist/src/plugins/admin/schema.d.ts +36 -0
- package/dist/src/plugins/admin/types.d.ts +77 -0
- package/dist/src/plugins/anonymous/client.d.ts +8 -0
- package/dist/src/plugins/anonymous/index.d.ts +159 -0
- package/dist/src/plugins/api-key/client.d.ts +11 -0
- package/dist/src/plugins/api-key/index.d.ts +1431 -0
- package/dist/src/plugins/api-key/rate-limit.d.ts +20 -0
- package/dist/src/plugins/api-key/routes/create-api-key.d.ts +244 -0
- package/dist/src/plugins/api-key/routes/delete-all-expired-api-keys.d.ts +45 -0
- package/dist/src/plugins/api-key/routes/delete-api-key.d.ts +112 -0
- package/dist/src/plugins/api-key/routes/get-api-key.d.ts +234 -0
- package/dist/src/plugins/api-key/routes/index.d.ts +1136 -0
- package/dist/src/plugins/api-key/routes/list-api-keys.d.ts +231 -0
- package/dist/src/plugins/api-key/routes/update-api-key.d.ts +235 -0
- package/dist/src/plugins/api-key/routes/verify-api-key.d.ts +80 -0
- package/dist/src/plugins/api-key/schema.d.ts +188 -0
- package/dist/src/plugins/api-key/types.d.ts +285 -0
- package/dist/src/plugins/bearer/index.d.ts +31 -0
- package/dist/src/plugins/captcha/constants.d.ts +9 -0
- package/dist/src/plugins/captcha/error-codes.d.ts +9 -0
- package/dist/src/plugins/captcha/index.d.ts +7 -0
- package/dist/src/plugins/captcha/types.d.ts +23 -0
- package/dist/src/plugins/captcha/utils.d.ts +1 -0
- package/dist/src/plugins/captcha/verify-handlers/captchafox.d.ts +11 -0
- package/dist/src/plugins/captcha/verify-handlers/cloudflare-turnstile.d.ts +10 -0
- package/dist/src/plugins/captcha/verify-handlers/google-recaptcha.d.ts +11 -0
- package/dist/src/plugins/captcha/verify-handlers/h-captcha.d.ts +11 -0
- package/dist/src/plugins/captcha/verify-handlers/index.d.ts +4 -0
- package/dist/src/plugins/custom-session/client.d.ts +12 -0
- package/dist/src/plugins/custom-session/index.d.ts +89 -0
- package/dist/src/plugins/device-authorization/client.d.ts +12 -0
- package/dist/src/plugins/device-authorization/index.d.ts +568 -0
- package/dist/src/plugins/device-authorization/schema.d.ts +56 -0
- package/dist/src/plugins/email-otp/client.d.ts +9 -0
- package/dist/src/plugins/email-otp/index.d.ts +826 -0
- package/dist/src/plugins/email-otp/utils.d.ts +2 -0
- package/dist/src/plugins/generic-oauth/client.d.ts +5 -0
- package/dist/src/plugins/generic-oauth/index.d.ts +428 -0
- package/dist/src/plugins/haveibeenpwned/index.d.ts +25 -0
- package/dist/src/plugins/index.d.ts +28 -0
- package/dist/src/plugins/jwt/adapter.d.ts +8 -0
- package/dist/src/plugins/jwt/client.d.ts +5 -0
- package/dist/src/plugins/jwt/index.d.ts +276 -0
- package/dist/src/plugins/jwt/schema.d.ts +18 -0
- package/dist/src/plugins/jwt/sign.d.ts +8 -0
- package/dist/src/plugins/jwt/types.d.ts +175 -0
- package/dist/src/plugins/jwt/utils.d.ts +46 -0
- package/dist/src/plugins/last-login-method/client.d.ts +34 -0
- package/dist/src/plugins/last-login-method/index.d.ts +103 -0
- package/dist/src/plugins/magic-link/client.d.ts +5 -0
- package/dist/src/plugins/magic-link/index.d.ts +253 -0
- package/dist/src/plugins/magic-link/utils.d.ts +1 -0
- package/dist/src/plugins/multi-session/client.d.ts +9 -0
- package/dist/src/plugins/multi-session/index.d.ts +284 -0
- package/dist/src/plugins/oauth-proxy/index.d.ts +119 -0
- package/dist/src/plugins/one-tap/client.d.ts +81 -0
- package/dist/src/plugins/one-tap/index.d.ts +114 -0
- package/dist/src/plugins/one-time-token/client.d.ts +5 -0
- package/dist/src/plugins/one-time-token/index.d.ts +170 -0
- package/dist/src/plugins/one-time-token/utils.d.ts +1 -0
- package/dist/src/plugins/organization/access/index.d.ts +1 -0
- package/dist/src/plugins/organization/access/statement.d.ts +241 -0
- package/dist/src/plugins/organization/adapter.d.ts +685 -0
- package/dist/src/plugins/organization/call.d.ts +55 -0
- package/dist/src/plugins/organization/client.d.ts +344 -0
- package/dist/src/plugins/organization/error-codes.d.ts +58 -0
- package/dist/src/plugins/organization/has-permission.d.ts +13 -0
- package/dist/src/plugins/organization/index.d.ts +4 -0
- package/dist/src/plugins/organization/organization.d.ts +412 -0
- package/dist/src/plugins/organization/permission.d.ts +27 -0
- package/dist/src/plugins/organization/routes/crud-access-control.d.ts +701 -0
- package/dist/src/plugins/organization/routes/crud-invites.d.ts +1522 -0
- package/dist/src/plugins/organization/routes/crud-members.d.ts +1220 -0
- package/dist/src/plugins/organization/routes/crud-org.d.ts +1399 -0
- package/dist/src/plugins/organization/routes/crud-team.d.ts +1323 -0
- package/dist/src/plugins/organization/schema.d.ts +366 -0
- package/dist/src/plugins/organization/types.d.ts +737 -0
- package/dist/src/plugins/passkey/client.d.ts +190 -0
- package/dist/src/plugins/passkey/error-codes.d.ts +9 -0
- package/dist/src/plugins/passkey/index.d.ts +838 -0
- package/dist/src/plugins/passkey/schema.d.ts +50 -0
- package/dist/src/plugins/passkey/types.d.ts +69 -0
- package/dist/src/plugins/passkey/utils.d.ts +2 -0
- package/dist/src/plugins/phone-number/client.d.ts +9 -0
- package/dist/src/plugins/phone-number/index.d.ts +622 -0
- package/dist/src/plugins/phone-number/phone-number-error.d.ts +10 -0
- package/dist/src/plugins/two-factor/backup-codes/index.d.ts +387 -0
- package/dist/src/plugins/two-factor/client.d.ts +28 -0
- package/dist/src/plugins/two-factor/constant.d.ts +2 -0
- package/dist/src/plugins/two-factor/error-code.d.ts +11 -0
- package/dist/src/plugins/two-factor/index.d.ts +1014 -0
- package/dist/src/plugins/two-factor/otp/index.d.ts +282 -0
- package/dist/src/plugins/two-factor/schema.d.ts +35 -0
- package/dist/src/plugins/two-factor/totp/index.d.ts +298 -0
- package/dist/src/plugins/two-factor/types.d.ts +50 -0
- package/dist/src/plugins/two-factor/utils.d.ts +1 -0
- package/dist/src/plugins/two-factor/verify-two-factor.d.ts +59 -0
- package/dist/src/plugins/username/client.d.ts +5 -0
- package/dist/src/plugins/username/error-codes.d.ts +10 -0
- package/dist/src/plugins/username/index.d.ts +313 -0
- package/dist/src/plugins/username/schema.d.ts +27 -0
- package/dist/src/social-providers/index.d.ts +1 -0
- package/dist/src/test-utils/headers.d.ts +5 -0
- package/dist/src/test-utils/index.d.ts +2 -0
- package/dist/src/test-utils/test-instance.d.ts +17849 -0
- package/dist/src/types/adapter.d.ts +21 -0
- package/dist/src/types/api.d.ts +29 -0
- package/dist/src/types/helper.d.ts +32 -0
- package/dist/src/types/index.d.ts +7 -0
- package/dist/src/types/models.d.ts +13 -0
- package/dist/src/types/plugins.d.ts +14 -0
- package/dist/src/utils/await-object.d.ts +3 -0
- package/dist/src/utils/boolean.d.ts +1 -0
- package/dist/src/utils/clone.d.ts +1 -0
- package/dist/src/utils/constants.d.ts +1 -0
- package/dist/src/utils/date.d.ts +1 -0
- package/dist/src/utils/ensure-utc.d.ts +1 -0
- package/dist/src/utils/get-request-ip.d.ts +2 -0
- package/dist/src/utils/hashing.d.ts +6 -0
- package/dist/src/utils/hide-metadata.d.ts +3 -0
- package/dist/src/utils/id.d.ts +1 -0
- package/dist/src/utils/import-util.d.ts +1 -0
- package/dist/src/utils/index.d.ts +5 -0
- package/dist/src/utils/is-atom.d.ts +2 -0
- package/dist/src/utils/is-promise.d.ts +1 -0
- package/dist/src/utils/json.d.ts +1 -0
- package/dist/src/utils/merger.d.ts +1 -0
- package/dist/src/utils/middleware-response.d.ts +8 -0
- package/dist/src/utils/misc.d.ts +1 -0
- package/dist/src/utils/password.d.ts +6 -0
- package/dist/src/utils/plugin-helper.d.ts +5 -0
- package/dist/src/utils/shim.d.ts +3 -0
- package/dist/src/utils/time.d.ts +4 -0
- package/dist/src/utils/url.d.ts +4 -0
- package/dist/src/utils/wildcard.d.ts +45 -0
- package/dist/test-utils/index.d.mts +17870 -0
- package/dist/test-utils/index.mjs +272 -0
- package/dist/two-factor-ClUXxom2.mjs +914 -0
- package/dist/types/index.d.mts +6 -0
- package/dist/types/index.mjs +3 -0
- package/dist/types-B1OL7hyf.d.mts +4 -0
- package/dist/types-Bjn15Uy6.mjs +1 -0
- package/dist/url-VB2gAbTI.mjs +59 -0
- package/dist/username-DiGklOGl.mjs +278 -0
- package/dist/utils-SOMB4Px5.mjs +132 -0
- package/package.json +447 -0
|
@@ -0,0 +1,3139 @@
|
|
|
1
|
+
import { i as parseState, n as HIDE_METADATA, r as generateState, t as generateId } from "./utils-SOMB4Px5.mjs";
|
|
2
|
+
import { i as getProtocol, n as getHost, r as getOrigin } from "./url-VB2gAbTI.mjs";
|
|
3
|
+
import { t as getIp } from "./get-request-ip-DSjrEpGo.mjs";
|
|
4
|
+
import { t as safeJSONParse } from "./json-D0TBLy4q.mjs";
|
|
5
|
+
import { n as symmetricEncrypt, r as generateRandomString, t as symmetricDecrypt } from "./crypto-Bb42eMrH.mjs";
|
|
6
|
+
import { t as signJWT } from "./jwt-CmxZAGe2.mjs";
|
|
7
|
+
import { c as setSessionCookie, n as deleteSessionCookie } from "./cookies-cJObLwn1.mjs";
|
|
8
|
+
import { l as parseUserInput, t as getDate } from "./date-Cx2yCiC4.mjs";
|
|
9
|
+
import { c as revokeSessions, i as listSessions, l as sensitiveSessionMiddleware, n as getSession, o as revokeOtherSessions, r as getSessionFromCtx, s as revokeSession, t as freshSessionMiddleware, u as sessionMiddleware } from "./session-Dgp133Yt.mjs";
|
|
10
|
+
import { hasRequestState, runWithEndpointContext, runWithRequestState, runWithTransaction } from "@gatwech/core/context";
|
|
11
|
+
import { isDevelopment, isProduction, logger, shouldPublishLog } from "@gatwech/core/env";
|
|
12
|
+
import { BASE_ERROR_CODES } from "@gatwech/core/error";
|
|
13
|
+
import { APIError, APIError as APIError$1, createRouter, toResponse } from "better-call";
|
|
14
|
+
import { createAuthEndpoint, createAuthEndpoint as createAuthEndpoint$1, createAuthMiddleware, createAuthMiddleware as createAuthMiddleware$1, optionsMiddleware } from "@gatwech/core/api";
|
|
15
|
+
import * as z$1 from "zod";
|
|
16
|
+
import { SocialProviderListEnum } from "@gatwech/core/social-providers";
|
|
17
|
+
import { jwtVerify } from "jose";
|
|
18
|
+
import { JWTExpired } from "jose/errors";
|
|
19
|
+
import { createDefu } from "defu";
|
|
20
|
+
|
|
21
|
+
//#region src/utils/wildcard.ts
|
|
22
|
+
/**
|
|
23
|
+
* Escapes a character if it has a special meaning in regular expressions
|
|
24
|
+
* and returns the character as is if it doesn't
|
|
25
|
+
*/
|
|
26
|
+
function escapeRegExpChar(char) {
|
|
27
|
+
if (char === "-" || char === "^" || char === "$" || char === "+" || char === "." || char === "(" || char === ")" || char === "|" || char === "[" || char === "]" || char === "{" || char === "}" || char === "*" || char === "?" || char === "\\") return `\\${char}`;
|
|
28
|
+
else return char;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Escapes all characters in a given string that have a special meaning in regular expressions
|
|
32
|
+
*/
|
|
33
|
+
function escapeRegExpString(str) {
|
|
34
|
+
let result = "";
|
|
35
|
+
for (let i = 0; i < str.length; i++) result += escapeRegExpChar(str[i]);
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Transforms one or more glob patterns into a RegExp pattern
|
|
40
|
+
*/
|
|
41
|
+
function transform(pattern, separator = true) {
|
|
42
|
+
if (Array.isArray(pattern)) return `(?:${pattern.map((p) => `^${transform(p, separator)}$`).join("|")})`;
|
|
43
|
+
let separatorSplitter = "";
|
|
44
|
+
let separatorMatcher = "";
|
|
45
|
+
let wildcard = ".";
|
|
46
|
+
if (separator === true) {
|
|
47
|
+
separatorSplitter = "/";
|
|
48
|
+
separatorMatcher = "[/\\\\]";
|
|
49
|
+
wildcard = "[^/\\\\]";
|
|
50
|
+
} else if (separator) {
|
|
51
|
+
separatorSplitter = separator;
|
|
52
|
+
separatorMatcher = escapeRegExpString(separatorSplitter);
|
|
53
|
+
if (separatorMatcher.length > 1) {
|
|
54
|
+
separatorMatcher = `(?:${separatorMatcher})`;
|
|
55
|
+
wildcard = `((?!${separatorMatcher}).)`;
|
|
56
|
+
} else wildcard = `[^${separatorMatcher}]`;
|
|
57
|
+
}
|
|
58
|
+
let requiredSeparator = separator ? `${separatorMatcher}+?` : "";
|
|
59
|
+
let optionalSeparator = separator ? `${separatorMatcher}*?` : "";
|
|
60
|
+
let segments = separator ? pattern.split(separatorSplitter) : [pattern];
|
|
61
|
+
let result = "";
|
|
62
|
+
for (let s = 0; s < segments.length; s++) {
|
|
63
|
+
let segment = segments[s];
|
|
64
|
+
let nextSegment = segments[s + 1];
|
|
65
|
+
let currentSeparator = "";
|
|
66
|
+
if (!segment && s > 0) continue;
|
|
67
|
+
if (separator) if (s === segments.length - 1) currentSeparator = optionalSeparator;
|
|
68
|
+
else if (nextSegment !== "**") currentSeparator = requiredSeparator;
|
|
69
|
+
else currentSeparator = "";
|
|
70
|
+
if (separator && segment === "**") {
|
|
71
|
+
if (currentSeparator) {
|
|
72
|
+
result += s === 0 ? "" : currentSeparator;
|
|
73
|
+
result += `(?:${wildcard}*?${currentSeparator})*?`;
|
|
74
|
+
}
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
for (let c = 0; c < segment.length; c++) {
|
|
78
|
+
let char = segment[c];
|
|
79
|
+
if (char === "\\") {
|
|
80
|
+
if (c < segment.length - 1) {
|
|
81
|
+
result += escapeRegExpChar(segment[c + 1]);
|
|
82
|
+
c++;
|
|
83
|
+
}
|
|
84
|
+
} else if (char === "?") result += wildcard;
|
|
85
|
+
else if (char === "*") result += `${wildcard}*?`;
|
|
86
|
+
else result += escapeRegExpChar(char);
|
|
87
|
+
}
|
|
88
|
+
result += currentSeparator;
|
|
89
|
+
}
|
|
90
|
+
return result;
|
|
91
|
+
}
|
|
92
|
+
function isMatch(regexp, sample) {
|
|
93
|
+
if (typeof sample !== "string") throw new TypeError(`Sample must be a string, but ${typeof sample} given`);
|
|
94
|
+
return regexp.test(sample);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Compiles one or more glob patterns into a RegExp and returns an isMatch function.
|
|
98
|
+
* The isMatch function takes a sample string as its only argument and returns `true`
|
|
99
|
+
* if the string matches the pattern(s).
|
|
100
|
+
*
|
|
101
|
+
* ```js
|
|
102
|
+
* wildcardMatch('src/*.js')('src/index.js') //=> true
|
|
103
|
+
* ```
|
|
104
|
+
*
|
|
105
|
+
* ```js
|
|
106
|
+
* const isMatch = wildcardMatch('*.example.com', '.')
|
|
107
|
+
* isMatch('foo.example.com') //=> true
|
|
108
|
+
* isMatch('foo.bar.com') //=> false
|
|
109
|
+
* ```
|
|
110
|
+
*/
|
|
111
|
+
function wildcardMatch(pattern, options) {
|
|
112
|
+
if (typeof pattern !== "string" && !Array.isArray(pattern)) throw new TypeError(`The first argument must be a single pattern string or an array of patterns, but ${typeof pattern} given`);
|
|
113
|
+
if (typeof options === "string" || typeof options === "boolean") options = { separator: options };
|
|
114
|
+
if (arguments.length === 2 && !(typeof options === "undefined" || typeof options === "object" && options !== null && !Array.isArray(options))) throw new TypeError(`The second argument must be an options object or a string/boolean separator, but ${typeof options} given`);
|
|
115
|
+
options = options || {};
|
|
116
|
+
if (options.separator === "\\") throw new Error("\\ is not a valid separator because it is used for escaping. Try setting the separator to `true` instead");
|
|
117
|
+
let regexpPattern = transform(pattern, options.separator);
|
|
118
|
+
let regexp = new RegExp(`^${regexpPattern}$`, options.flags);
|
|
119
|
+
let fn = isMatch.bind(null, regexp);
|
|
120
|
+
fn.options = options;
|
|
121
|
+
fn.pattern = pattern;
|
|
122
|
+
fn.regexp = regexp;
|
|
123
|
+
return fn;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
//#endregion
|
|
127
|
+
//#region src/api/middlewares/origin-check.ts
|
|
128
|
+
/**
|
|
129
|
+
* A middleware to validate callbackURL and origin against
|
|
130
|
+
* trustedOrigins.
|
|
131
|
+
*/
|
|
132
|
+
const originCheckMiddleware = createAuthMiddleware(async (ctx) => {
|
|
133
|
+
if (ctx.request?.method !== "POST" || !ctx.request) return;
|
|
134
|
+
const headers = ctx.request?.headers;
|
|
135
|
+
const request = ctx.request;
|
|
136
|
+
const { body, query, context } = ctx;
|
|
137
|
+
/**
|
|
138
|
+
* We only allow requests with the x-auth-request header set to
|
|
139
|
+
* true or application/json content type. This is to prevent
|
|
140
|
+
* simple requests from being processed
|
|
141
|
+
*/
|
|
142
|
+
if (isSimpleRequest(headers) && !ctx.context.skipCSRFCheck) throw new APIError("FORBIDDEN", { message: "Invalid request" });
|
|
143
|
+
const originHeader = headers?.get("origin") || headers?.get("referer") || "";
|
|
144
|
+
const callbackURL = body?.callbackURL || query?.callbackURL;
|
|
145
|
+
const redirectURL = body?.redirectTo;
|
|
146
|
+
const errorCallbackURL = body?.errorCallbackURL;
|
|
147
|
+
const newUserCallbackURL = body?.newUserCallbackURL;
|
|
148
|
+
const trustedOrigins = Array.isArray(context.options.trustedOrigins) ? context.trustedOrigins : [...context.trustedOrigins, ...await context.options.trustedOrigins?.(request) || []];
|
|
149
|
+
const useCookies = headers?.has("cookie");
|
|
150
|
+
const matchesPattern = (url, pattern) => {
|
|
151
|
+
if (url.startsWith("/")) return false;
|
|
152
|
+
if (pattern.includes("*")) {
|
|
153
|
+
if (pattern.includes("://")) return wildcardMatch(pattern)(getOrigin(url) || url);
|
|
154
|
+
const host = getHost(url);
|
|
155
|
+
if (!host) return false;
|
|
156
|
+
return wildcardMatch(pattern)(host);
|
|
157
|
+
}
|
|
158
|
+
const protocol = getProtocol(url);
|
|
159
|
+
return protocol === "http:" || protocol === "https:" || !protocol ? pattern === getOrigin(url) : url.startsWith(pattern);
|
|
160
|
+
};
|
|
161
|
+
const validateURL = (url, label) => {
|
|
162
|
+
if (!url) return;
|
|
163
|
+
if (!trustedOrigins.some((origin) => matchesPattern(url, origin) || url?.startsWith("/") && label !== "origin" && /^\/(?!\/|\\|%2f|%5c)[\w\-.\+/@]*(?:\?[\w\-.\+/=&%@]*)?$/.test(url))) {
|
|
164
|
+
ctx.context.logger.error(`Invalid ${label}: ${url}`);
|
|
165
|
+
ctx.context.logger.info(`If it's a valid URL, please add ${url} to trustedOrigins in your auth config\n`, `Current list of trustedOrigins: ${trustedOrigins}`);
|
|
166
|
+
throw new APIError("FORBIDDEN", { message: `Invalid ${label}` });
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
if (useCookies && !ctx.context.skipCSRFCheck && !ctx.context.skipOriginCheck) {
|
|
170
|
+
if (!originHeader || originHeader === "null") throw new APIError("FORBIDDEN", { message: "Missing or null Origin" });
|
|
171
|
+
validateURL(originHeader, "origin");
|
|
172
|
+
}
|
|
173
|
+
callbackURL && validateURL(callbackURL, "callbackURL");
|
|
174
|
+
redirectURL && validateURL(redirectURL, "redirectURL");
|
|
175
|
+
errorCallbackURL && validateURL(errorCallbackURL, "errorCallbackURL");
|
|
176
|
+
newUserCallbackURL && validateURL(newUserCallbackURL, "newUserCallbackURL");
|
|
177
|
+
});
|
|
178
|
+
const originCheck = (getValue) => createAuthMiddleware(async (ctx) => {
|
|
179
|
+
if (!ctx.request) return;
|
|
180
|
+
const { context } = ctx;
|
|
181
|
+
const callbackURL = getValue(ctx);
|
|
182
|
+
const trustedOrigins = Array.isArray(context.options.trustedOrigins) ? context.trustedOrigins : [...context.trustedOrigins, ...await context.options.trustedOrigins?.(ctx.request) || []];
|
|
183
|
+
const matchesPattern = (url, pattern) => {
|
|
184
|
+
if (url.startsWith("/")) return false;
|
|
185
|
+
if (pattern.includes("*")) {
|
|
186
|
+
if (pattern.includes("://")) return wildcardMatch(pattern)(getOrigin(url) || url);
|
|
187
|
+
const host = getHost(url);
|
|
188
|
+
if (!host) return false;
|
|
189
|
+
return wildcardMatch(pattern)(host);
|
|
190
|
+
}
|
|
191
|
+
const protocol = getProtocol(url);
|
|
192
|
+
return protocol === "http:" || protocol === "https:" || !protocol ? pattern === getOrigin(url) : url.startsWith(pattern);
|
|
193
|
+
};
|
|
194
|
+
const validateURL = (url, label) => {
|
|
195
|
+
if (!url) return;
|
|
196
|
+
if (!trustedOrigins.some((origin) => matchesPattern(url, origin) || url?.startsWith("/") && label !== "origin" && /^\/(?!\/|\\|%2f|%5c)[\w\-.\+/@]*(?:\?[\w\-.\+/=&%@]*)?$/.test(url))) {
|
|
197
|
+
ctx.context.logger.error(`Invalid ${label}: ${url}`);
|
|
198
|
+
ctx.context.logger.info(`If it's a valid URL, please add ${url} to trustedOrigins in your auth config\n`, `Current list of trustedOrigins: ${trustedOrigins}`);
|
|
199
|
+
throw new APIError("FORBIDDEN", { message: `Invalid ${label}` });
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
const callbacks = Array.isArray(callbackURL) ? callbackURL : [callbackURL];
|
|
203
|
+
for (const url of callbacks) validateURL(url, "callbackURL");
|
|
204
|
+
});
|
|
205
|
+
function isSimpleRequest(headers) {
|
|
206
|
+
const SIMPLE_HEADERS = [
|
|
207
|
+
"accept",
|
|
208
|
+
"accept-language",
|
|
209
|
+
"content-language",
|
|
210
|
+
"content-type"
|
|
211
|
+
];
|
|
212
|
+
const SIMPLE_CONTENT_TYPES = [
|
|
213
|
+
"application/x-www-form-urlencoded",
|
|
214
|
+
"multipart/form-data",
|
|
215
|
+
"text/plain"
|
|
216
|
+
];
|
|
217
|
+
for (const [key, value] of headers.entries()) {
|
|
218
|
+
if (!SIMPLE_HEADERS.includes(key.toLowerCase())) return false;
|
|
219
|
+
if (key.toLowerCase() === "content-type" && !SIMPLE_CONTENT_TYPES.includes(value?.split(";")[0]?.trim()?.toLowerCase() || "")) return false;
|
|
220
|
+
}
|
|
221
|
+
return true;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
//#endregion
|
|
225
|
+
//#region src/api/rate-limiter/index.ts
|
|
226
|
+
function shouldRateLimit(max, window, rateLimitData) {
|
|
227
|
+
const now = Date.now();
|
|
228
|
+
const windowInMs = window * 1e3;
|
|
229
|
+
return now - rateLimitData.lastRequest < windowInMs && rateLimitData.count >= max;
|
|
230
|
+
}
|
|
231
|
+
function rateLimitResponse(retryAfter) {
|
|
232
|
+
return new Response(JSON.stringify({ message: "Too many requests. Please try again later." }), {
|
|
233
|
+
status: 429,
|
|
234
|
+
statusText: "Too Many Requests",
|
|
235
|
+
headers: { "X-Retry-After": retryAfter.toString() }
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
function getRetryAfter(lastRequest, window) {
|
|
239
|
+
const now = Date.now();
|
|
240
|
+
const windowInMs = window * 1e3;
|
|
241
|
+
return Math.ceil((lastRequest + windowInMs - now) / 1e3);
|
|
242
|
+
}
|
|
243
|
+
function createDBStorage(ctx) {
|
|
244
|
+
const model = "rateLimit";
|
|
245
|
+
const db = ctx.adapter;
|
|
246
|
+
return {
|
|
247
|
+
get: async (key) => {
|
|
248
|
+
const data = (await db.findMany({
|
|
249
|
+
model,
|
|
250
|
+
where: [{
|
|
251
|
+
field: "key",
|
|
252
|
+
value: key
|
|
253
|
+
}]
|
|
254
|
+
}))[0];
|
|
255
|
+
if (typeof data?.lastRequest === "bigint") data.lastRequest = Number(data.lastRequest);
|
|
256
|
+
return data;
|
|
257
|
+
},
|
|
258
|
+
set: async (key, value, _update) => {
|
|
259
|
+
try {
|
|
260
|
+
if (_update) await db.updateMany({
|
|
261
|
+
model,
|
|
262
|
+
where: [{
|
|
263
|
+
field: "key",
|
|
264
|
+
value: key
|
|
265
|
+
}],
|
|
266
|
+
update: {
|
|
267
|
+
count: value.count,
|
|
268
|
+
lastRequest: value.lastRequest
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
else await db.create({
|
|
272
|
+
model,
|
|
273
|
+
data: {
|
|
274
|
+
key,
|
|
275
|
+
count: value.count,
|
|
276
|
+
lastRequest: value.lastRequest
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
} catch (e) {
|
|
280
|
+
ctx.logger.error("Error setting rate limit", e);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
const memory = /* @__PURE__ */ new Map();
|
|
286
|
+
function getRateLimitStorage(ctx, rateLimitSettings) {
|
|
287
|
+
if (ctx.options.rateLimit?.customStorage) return ctx.options.rateLimit.customStorage;
|
|
288
|
+
const storage = ctx.rateLimit.storage;
|
|
289
|
+
if (storage === "secondary-storage") return {
|
|
290
|
+
get: async (key) => {
|
|
291
|
+
const data = await ctx.options.secondaryStorage?.get(key);
|
|
292
|
+
return data ? safeJSONParse(data) : void 0;
|
|
293
|
+
},
|
|
294
|
+
set: async (key, value, _update) => {
|
|
295
|
+
const ttl = rateLimitSettings?.window ?? ctx.options.rateLimit?.window ?? 10;
|
|
296
|
+
await ctx.options.secondaryStorage?.set?.(key, JSON.stringify(value), ttl);
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
else if (storage === "memory") return {
|
|
300
|
+
async get(key) {
|
|
301
|
+
return memory.get(key);
|
|
302
|
+
},
|
|
303
|
+
async set(key, value, _update) {
|
|
304
|
+
memory.set(key, value);
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
return createDBStorage(ctx);
|
|
308
|
+
}
|
|
309
|
+
async function onRequestRateLimit(req, ctx) {
|
|
310
|
+
if (!ctx.rateLimit.enabled) return;
|
|
311
|
+
const path = new URL(req.url).pathname.replace(ctx.options.basePath || "/api/auth", "");
|
|
312
|
+
let window = ctx.rateLimit.window;
|
|
313
|
+
let max = ctx.rateLimit.max;
|
|
314
|
+
const ip = getIp(req, ctx.options);
|
|
315
|
+
if (!ip) return;
|
|
316
|
+
const key = ip + path;
|
|
317
|
+
const specialRule = getDefaultSpecialRules().find((rule) => rule.pathMatcher(path));
|
|
318
|
+
if (specialRule) {
|
|
319
|
+
window = specialRule.window;
|
|
320
|
+
max = specialRule.max;
|
|
321
|
+
}
|
|
322
|
+
for (const plugin of ctx.options.plugins || []) if (plugin.rateLimit) {
|
|
323
|
+
const matchedRule = plugin.rateLimit.find((rule) => rule.pathMatcher(path));
|
|
324
|
+
if (matchedRule) {
|
|
325
|
+
window = matchedRule.window;
|
|
326
|
+
max = matchedRule.max;
|
|
327
|
+
break;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
if (ctx.rateLimit.customRules) {
|
|
331
|
+
const _path = Object.keys(ctx.rateLimit.customRules).find((p) => {
|
|
332
|
+
if (p.includes("*")) return wildcardMatch(p)(path);
|
|
333
|
+
return p === path;
|
|
334
|
+
});
|
|
335
|
+
if (_path) {
|
|
336
|
+
const customRule = ctx.rateLimit.customRules[_path];
|
|
337
|
+
const resolved = typeof customRule === "function" ? await customRule(req) : customRule;
|
|
338
|
+
if (resolved) {
|
|
339
|
+
window = resolved.window;
|
|
340
|
+
max = resolved.max;
|
|
341
|
+
}
|
|
342
|
+
if (resolved === false) return;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
const storage = getRateLimitStorage(ctx, { window });
|
|
346
|
+
const data = await storage.get(key);
|
|
347
|
+
const now = Date.now();
|
|
348
|
+
if (!data) await storage.set(key, {
|
|
349
|
+
key,
|
|
350
|
+
count: 1,
|
|
351
|
+
lastRequest: now
|
|
352
|
+
});
|
|
353
|
+
else {
|
|
354
|
+
const timeSinceLastRequest = now - data.lastRequest;
|
|
355
|
+
if (shouldRateLimit(max, window, data)) return rateLimitResponse(getRetryAfter(data.lastRequest, window));
|
|
356
|
+
else if (timeSinceLastRequest > window * 1e3) await storage.set(key, {
|
|
357
|
+
...data,
|
|
358
|
+
count: 1,
|
|
359
|
+
lastRequest: now
|
|
360
|
+
}, true);
|
|
361
|
+
else await storage.set(key, {
|
|
362
|
+
...data,
|
|
363
|
+
count: data.count + 1,
|
|
364
|
+
lastRequest: now
|
|
365
|
+
}, true);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
function getDefaultSpecialRules() {
|
|
369
|
+
return [{
|
|
370
|
+
pathMatcher(path) {
|
|
371
|
+
return path.startsWith("/sign-in") || path.startsWith("/sign-up") || path.startsWith("/change-password") || path.startsWith("/change-email");
|
|
372
|
+
},
|
|
373
|
+
window: 10,
|
|
374
|
+
max: 3
|
|
375
|
+
}];
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
//#endregion
|
|
379
|
+
//#region src/oauth2/utils.ts
|
|
380
|
+
function decryptOAuthToken(token, ctx) {
|
|
381
|
+
if (!token) return token;
|
|
382
|
+
if (ctx.options.account?.encryptOAuthTokens) return symmetricDecrypt({
|
|
383
|
+
key: ctx.secret,
|
|
384
|
+
data: token
|
|
385
|
+
});
|
|
386
|
+
return token;
|
|
387
|
+
}
|
|
388
|
+
function setTokenUtil(token, ctx) {
|
|
389
|
+
if (ctx.options.account?.encryptOAuthTokens && token) return symmetricEncrypt({
|
|
390
|
+
key: ctx.secret,
|
|
391
|
+
data: token
|
|
392
|
+
});
|
|
393
|
+
return token;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
//#endregion
|
|
397
|
+
//#region src/api/routes/account.ts
|
|
398
|
+
const listUserAccounts = createAuthEndpoint("/list-accounts", {
|
|
399
|
+
method: "GET",
|
|
400
|
+
use: [sessionMiddleware],
|
|
401
|
+
metadata: { openapi: {
|
|
402
|
+
description: "List all accounts linked to the user",
|
|
403
|
+
responses: { "200": {
|
|
404
|
+
description: "Success",
|
|
405
|
+
content: { "application/json": { schema: {
|
|
406
|
+
type: "array",
|
|
407
|
+
items: {
|
|
408
|
+
type: "object",
|
|
409
|
+
properties: {
|
|
410
|
+
id: { type: "string" },
|
|
411
|
+
providerId: { type: "string" },
|
|
412
|
+
createdAt: {
|
|
413
|
+
type: "string",
|
|
414
|
+
format: "date-time"
|
|
415
|
+
},
|
|
416
|
+
updatedAt: {
|
|
417
|
+
type: "string",
|
|
418
|
+
format: "date-time"
|
|
419
|
+
},
|
|
420
|
+
accountId: { type: "string" },
|
|
421
|
+
userId: { type: "string" },
|
|
422
|
+
scopes: {
|
|
423
|
+
type: "array",
|
|
424
|
+
items: { type: "string" }
|
|
425
|
+
}
|
|
426
|
+
},
|
|
427
|
+
required: [
|
|
428
|
+
"id",
|
|
429
|
+
"providerId",
|
|
430
|
+
"createdAt",
|
|
431
|
+
"updatedAt",
|
|
432
|
+
"accountId",
|
|
433
|
+
"userId",
|
|
434
|
+
"scopes"
|
|
435
|
+
]
|
|
436
|
+
}
|
|
437
|
+
} } }
|
|
438
|
+
} }
|
|
439
|
+
} }
|
|
440
|
+
}, async (c) => {
|
|
441
|
+
const session = c.context.session;
|
|
442
|
+
const accounts = await c.context.internalAdapter.findAccounts(session.user.id);
|
|
443
|
+
return c.json(accounts.map((a) => ({
|
|
444
|
+
id: a.id,
|
|
445
|
+
providerId: a.providerId,
|
|
446
|
+
createdAt: a.createdAt,
|
|
447
|
+
updatedAt: a.updatedAt,
|
|
448
|
+
accountId: a.accountId,
|
|
449
|
+
userId: a.userId,
|
|
450
|
+
scopes: a.scope?.split(",") || []
|
|
451
|
+
})));
|
|
452
|
+
});
|
|
453
|
+
const linkSocialAccount = createAuthEndpoint("/link-social", {
|
|
454
|
+
method: "POST",
|
|
455
|
+
requireHeaders: true,
|
|
456
|
+
body: z$1.object({
|
|
457
|
+
callbackURL: z$1.string().meta({ description: "The URL to redirect to after the user has signed in" }).optional(),
|
|
458
|
+
provider: SocialProviderListEnum,
|
|
459
|
+
idToken: z$1.object({
|
|
460
|
+
token: z$1.string(),
|
|
461
|
+
nonce: z$1.string().optional(),
|
|
462
|
+
accessToken: z$1.string().optional(),
|
|
463
|
+
refreshToken: z$1.string().optional(),
|
|
464
|
+
scopes: z$1.array(z$1.string()).optional()
|
|
465
|
+
}).optional(),
|
|
466
|
+
requestSignUp: z$1.boolean().optional(),
|
|
467
|
+
scopes: z$1.array(z$1.string()).meta({ description: "Additional scopes to request from the provider" }).optional(),
|
|
468
|
+
errorCallbackURL: z$1.string().meta({ description: "The URL to redirect to if there is an error during the link process" }).optional(),
|
|
469
|
+
disableRedirect: z$1.boolean().meta({ description: "Disable automatic redirection to the provider. Useful for handling the redirection yourself" }).optional(),
|
|
470
|
+
additionalData: z$1.record(z$1.string(), z$1.any()).optional()
|
|
471
|
+
}),
|
|
472
|
+
use: [sessionMiddleware],
|
|
473
|
+
metadata: { openapi: {
|
|
474
|
+
description: "Link a social account to the user",
|
|
475
|
+
responses: { "200": {
|
|
476
|
+
description: "Success",
|
|
477
|
+
content: { "application/json": { schema: {
|
|
478
|
+
type: "object",
|
|
479
|
+
properties: {
|
|
480
|
+
url: {
|
|
481
|
+
type: "string",
|
|
482
|
+
description: "The authorization URL to redirect the user to"
|
|
483
|
+
},
|
|
484
|
+
redirect: {
|
|
485
|
+
type: "boolean",
|
|
486
|
+
description: "Indicates if the user should be redirected to the authorization URL"
|
|
487
|
+
},
|
|
488
|
+
status: { type: "boolean" }
|
|
489
|
+
},
|
|
490
|
+
required: ["redirect"]
|
|
491
|
+
} } }
|
|
492
|
+
} }
|
|
493
|
+
} }
|
|
494
|
+
}, async (c) => {
|
|
495
|
+
const session = c.context.session;
|
|
496
|
+
const provider = c.context.socialProviders.find((p) => p.id === c.body.provider);
|
|
497
|
+
if (!provider) {
|
|
498
|
+
c.context.logger.error("Provider not found. Make sure to add the provider in your auth config", { provider: c.body.provider });
|
|
499
|
+
throw new APIError("NOT_FOUND", { message: BASE_ERROR_CODES.PROVIDER_NOT_FOUND });
|
|
500
|
+
}
|
|
501
|
+
if (c.body.idToken) {
|
|
502
|
+
if (!provider.verifyIdToken) {
|
|
503
|
+
c.context.logger.error("Provider does not support id token verification", { provider: c.body.provider });
|
|
504
|
+
throw new APIError("NOT_FOUND", { message: BASE_ERROR_CODES.ID_TOKEN_NOT_SUPPORTED });
|
|
505
|
+
}
|
|
506
|
+
const { token, nonce } = c.body.idToken;
|
|
507
|
+
if (!await provider.verifyIdToken(token, nonce)) {
|
|
508
|
+
c.context.logger.error("Invalid id token", { provider: c.body.provider });
|
|
509
|
+
throw new APIError("UNAUTHORIZED", { message: BASE_ERROR_CODES.INVALID_TOKEN });
|
|
510
|
+
}
|
|
511
|
+
const linkingUserInfo = await provider.getUserInfo({
|
|
512
|
+
idToken: token,
|
|
513
|
+
accessToken: c.body.idToken.accessToken,
|
|
514
|
+
refreshToken: c.body.idToken.refreshToken
|
|
515
|
+
});
|
|
516
|
+
if (!linkingUserInfo || !linkingUserInfo?.user) {
|
|
517
|
+
c.context.logger.error("Failed to get user info", { provider: c.body.provider });
|
|
518
|
+
throw new APIError("UNAUTHORIZED", { message: BASE_ERROR_CODES.FAILED_TO_GET_USER_INFO });
|
|
519
|
+
}
|
|
520
|
+
const linkingUserId = String(linkingUserInfo.user.id);
|
|
521
|
+
if (!linkingUserInfo.user.email) {
|
|
522
|
+
c.context.logger.error("User email not found", { provider: c.body.provider });
|
|
523
|
+
throw new APIError("UNAUTHORIZED", { message: BASE_ERROR_CODES.USER_EMAIL_NOT_FOUND });
|
|
524
|
+
}
|
|
525
|
+
if ((await c.context.internalAdapter.findAccounts(session.user.id)).find((a) => a.providerId === provider.id && a.accountId === linkingUserId)) return c.json({
|
|
526
|
+
url: "",
|
|
527
|
+
status: true,
|
|
528
|
+
redirect: false
|
|
529
|
+
});
|
|
530
|
+
if (!(c.context.options.account?.accountLinking?.trustedProviders)?.includes(provider.id) && !linkingUserInfo.user.emailVerified || c.context.options.account?.accountLinking?.enabled === false) throw new APIError("UNAUTHORIZED", { message: "Account not linked - linking not allowed" });
|
|
531
|
+
if (linkingUserInfo.user.email !== session.user.email && c.context.options.account?.accountLinking?.allowDifferentEmails !== true) throw new APIError("UNAUTHORIZED", { message: "Account not linked - different emails not allowed" });
|
|
532
|
+
try {
|
|
533
|
+
await c.context.internalAdapter.createAccount({
|
|
534
|
+
userId: session.user.id,
|
|
535
|
+
providerId: provider.id,
|
|
536
|
+
accountId: linkingUserId,
|
|
537
|
+
accessToken: c.body.idToken.accessToken,
|
|
538
|
+
idToken: token,
|
|
539
|
+
refreshToken: c.body.idToken.refreshToken,
|
|
540
|
+
scope: c.body.idToken.scopes?.join(",")
|
|
541
|
+
});
|
|
542
|
+
} catch (e) {
|
|
543
|
+
throw new APIError("EXPECTATION_FAILED", { message: "Account not linked - unable to create account" });
|
|
544
|
+
}
|
|
545
|
+
if (c.context.options.account?.accountLinking?.updateUserInfoOnLink === true) try {
|
|
546
|
+
await c.context.internalAdapter.updateUser(session.user.id, {
|
|
547
|
+
name: linkingUserInfo.user?.name,
|
|
548
|
+
image: linkingUserInfo.user?.image
|
|
549
|
+
});
|
|
550
|
+
} catch (e) {
|
|
551
|
+
console.warn("Could not update user - " + e.toString());
|
|
552
|
+
}
|
|
553
|
+
return c.json({
|
|
554
|
+
url: "",
|
|
555
|
+
status: true,
|
|
556
|
+
redirect: false
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
const state = await generateState(c, {
|
|
560
|
+
userId: session.user.id,
|
|
561
|
+
email: session.user.email
|
|
562
|
+
}, c.body.additionalData);
|
|
563
|
+
const url = await provider.createAuthorizationURL({
|
|
564
|
+
state: state.state,
|
|
565
|
+
codeVerifier: state.codeVerifier,
|
|
566
|
+
redirectURI: `${c.context.baseURL}/callback/${provider.id}`,
|
|
567
|
+
scopes: c.body.scopes
|
|
568
|
+
});
|
|
569
|
+
return c.json({
|
|
570
|
+
url: url.toString(),
|
|
571
|
+
redirect: !c.body.disableRedirect
|
|
572
|
+
});
|
|
573
|
+
});
|
|
574
|
+
const unlinkAccount = createAuthEndpoint("/unlink-account", {
|
|
575
|
+
method: "POST",
|
|
576
|
+
body: z$1.object({
|
|
577
|
+
providerId: z$1.string(),
|
|
578
|
+
accountId: z$1.string().optional()
|
|
579
|
+
}),
|
|
580
|
+
use: [freshSessionMiddleware],
|
|
581
|
+
metadata: { openapi: {
|
|
582
|
+
description: "Unlink an account",
|
|
583
|
+
responses: { "200": {
|
|
584
|
+
description: "Success",
|
|
585
|
+
content: { "application/json": { schema: {
|
|
586
|
+
type: "object",
|
|
587
|
+
properties: { status: { type: "boolean" } }
|
|
588
|
+
} } }
|
|
589
|
+
} }
|
|
590
|
+
} }
|
|
591
|
+
}, async (ctx) => {
|
|
592
|
+
const { providerId, accountId } = ctx.body;
|
|
593
|
+
const accounts = await ctx.context.internalAdapter.findAccounts(ctx.context.session.user.id);
|
|
594
|
+
if (accounts.length === 1 && !ctx.context.options.account?.accountLinking?.allowUnlinkingAll) throw new APIError("BAD_REQUEST", { message: BASE_ERROR_CODES.FAILED_TO_UNLINK_LAST_ACCOUNT });
|
|
595
|
+
const accountExist = accounts.find((account) => accountId ? account.accountId === accountId && account.providerId === providerId : account.providerId === providerId);
|
|
596
|
+
if (!accountExist) throw new APIError("BAD_REQUEST", { message: BASE_ERROR_CODES.ACCOUNT_NOT_FOUND });
|
|
597
|
+
await ctx.context.internalAdapter.deleteAccount(accountExist.id);
|
|
598
|
+
return ctx.json({ status: true });
|
|
599
|
+
});
|
|
600
|
+
const getAccessToken = createAuthEndpoint("/get-access-token", {
|
|
601
|
+
method: "POST",
|
|
602
|
+
body: z$1.object({
|
|
603
|
+
providerId: z$1.string().meta({ description: "The provider ID for the OAuth provider" }),
|
|
604
|
+
accountId: z$1.string().meta({ description: "The account ID associated with the refresh token" }).optional(),
|
|
605
|
+
userId: z$1.string().meta({ description: "The user ID associated with the account" }).optional()
|
|
606
|
+
}),
|
|
607
|
+
metadata: { openapi: {
|
|
608
|
+
description: "Get a valid access token, doing a refresh if needed",
|
|
609
|
+
responses: {
|
|
610
|
+
200: {
|
|
611
|
+
description: "A Valid access token",
|
|
612
|
+
content: { "application/json": { schema: {
|
|
613
|
+
type: "object",
|
|
614
|
+
properties: {
|
|
615
|
+
tokenType: { type: "string" },
|
|
616
|
+
idToken: { type: "string" },
|
|
617
|
+
accessToken: { type: "string" },
|
|
618
|
+
refreshToken: { type: "string" },
|
|
619
|
+
accessTokenExpiresAt: {
|
|
620
|
+
type: "string",
|
|
621
|
+
format: "date-time"
|
|
622
|
+
},
|
|
623
|
+
refreshTokenExpiresAt: {
|
|
624
|
+
type: "string",
|
|
625
|
+
format: "date-time"
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
} } }
|
|
629
|
+
},
|
|
630
|
+
400: { description: "Invalid refresh token or provider configuration" }
|
|
631
|
+
}
|
|
632
|
+
} }
|
|
633
|
+
}, async (ctx) => {
|
|
634
|
+
const { providerId, accountId, userId } = ctx.body;
|
|
635
|
+
const req = ctx.request;
|
|
636
|
+
const session = await getSessionFromCtx(ctx);
|
|
637
|
+
if (req && !session) throw ctx.error("UNAUTHORIZED");
|
|
638
|
+
let resolvedUserId = session?.user?.id || userId;
|
|
639
|
+
if (!resolvedUserId) throw new APIError("BAD_REQUEST", { message: `Either userId or session is required` });
|
|
640
|
+
if (!ctx.context.socialProviders.find((p) => p.id === providerId)) throw new APIError("BAD_REQUEST", { message: `Provider ${providerId} is not supported.` });
|
|
641
|
+
const account = (await ctx.context.internalAdapter.findAccounts(resolvedUserId)).find((acc) => accountId ? acc.id === accountId && acc.providerId === providerId : acc.providerId === providerId);
|
|
642
|
+
if (!account) throw new APIError("BAD_REQUEST", { message: "Account not found" });
|
|
643
|
+
const provider = ctx.context.socialProviders.find((p) => p.id === providerId);
|
|
644
|
+
if (!provider) throw new APIError("BAD_REQUEST", { message: `Provider ${providerId} not found.` });
|
|
645
|
+
try {
|
|
646
|
+
let newTokens = null;
|
|
647
|
+
const accessTokenExpired = account.accessTokenExpiresAt && new Date(account.accessTokenExpiresAt).getTime() - Date.now() < 5e3;
|
|
648
|
+
if (account.refreshToken && accessTokenExpired && provider.refreshAccessToken) {
|
|
649
|
+
const refreshToken$1 = await decryptOAuthToken(account.refreshToken, ctx.context);
|
|
650
|
+
newTokens = await provider.refreshAccessToken(refreshToken$1);
|
|
651
|
+
await ctx.context.internalAdapter.updateAccount(account.id, {
|
|
652
|
+
accessToken: await setTokenUtil(newTokens.accessToken, ctx.context),
|
|
653
|
+
accessTokenExpiresAt: newTokens.accessTokenExpiresAt,
|
|
654
|
+
refreshToken: await setTokenUtil(newTokens.refreshToken, ctx.context),
|
|
655
|
+
refreshTokenExpiresAt: newTokens.refreshTokenExpiresAt
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
const tokens = {
|
|
659
|
+
accessToken: newTokens?.accessToken ?? await decryptOAuthToken(account.accessToken ?? "", ctx.context),
|
|
660
|
+
accessTokenExpiresAt: newTokens?.accessTokenExpiresAt ?? account.accessTokenExpiresAt ?? void 0,
|
|
661
|
+
scopes: account.scope?.split(",") ?? [],
|
|
662
|
+
idToken: newTokens?.idToken ?? account.idToken ?? void 0
|
|
663
|
+
};
|
|
664
|
+
return ctx.json(tokens);
|
|
665
|
+
} catch (error$1) {
|
|
666
|
+
throw new APIError("BAD_REQUEST", {
|
|
667
|
+
message: "Failed to get a valid access token",
|
|
668
|
+
cause: error$1
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
});
|
|
672
|
+
const refreshToken = createAuthEndpoint("/refresh-token", {
|
|
673
|
+
method: "POST",
|
|
674
|
+
body: z$1.object({
|
|
675
|
+
providerId: z$1.string().meta({ description: "The provider ID for the OAuth provider" }),
|
|
676
|
+
accountId: z$1.string().meta({ description: "The account ID associated with the refresh token" }).optional(),
|
|
677
|
+
userId: z$1.string().meta({ description: "The user ID associated with the account" }).optional()
|
|
678
|
+
}),
|
|
679
|
+
metadata: { openapi: {
|
|
680
|
+
description: "Refresh the access token using a refresh token",
|
|
681
|
+
responses: {
|
|
682
|
+
200: {
|
|
683
|
+
description: "Access token refreshed successfully",
|
|
684
|
+
content: { "application/json": { schema: {
|
|
685
|
+
type: "object",
|
|
686
|
+
properties: {
|
|
687
|
+
tokenType: { type: "string" },
|
|
688
|
+
idToken: { type: "string" },
|
|
689
|
+
accessToken: { type: "string" },
|
|
690
|
+
refreshToken: { type: "string" },
|
|
691
|
+
accessTokenExpiresAt: {
|
|
692
|
+
type: "string",
|
|
693
|
+
format: "date-time"
|
|
694
|
+
},
|
|
695
|
+
refreshTokenExpiresAt: {
|
|
696
|
+
type: "string",
|
|
697
|
+
format: "date-time"
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
} } }
|
|
701
|
+
},
|
|
702
|
+
400: { description: "Invalid refresh token or provider configuration" }
|
|
703
|
+
}
|
|
704
|
+
} }
|
|
705
|
+
}, async (ctx) => {
|
|
706
|
+
const { providerId, accountId, userId } = ctx.body;
|
|
707
|
+
const req = ctx.request;
|
|
708
|
+
const session = await getSessionFromCtx(ctx);
|
|
709
|
+
if (req && !session) throw ctx.error("UNAUTHORIZED");
|
|
710
|
+
let resolvedUserId = session?.user?.id || userId;
|
|
711
|
+
if (!resolvedUserId) throw new APIError("BAD_REQUEST", { message: `Either userId or session is required` });
|
|
712
|
+
const account = (await ctx.context.internalAdapter.findAccounts(resolvedUserId)).find((acc) => accountId ? acc.id === accountId && acc.providerId === providerId : acc.providerId === providerId);
|
|
713
|
+
if (!account) throw new APIError("BAD_REQUEST", { message: "Account not found" });
|
|
714
|
+
const provider = ctx.context.socialProviders.find((p) => p.id === providerId);
|
|
715
|
+
if (!provider) throw new APIError("BAD_REQUEST", { message: `Provider ${providerId} not found.` });
|
|
716
|
+
if (!provider.refreshAccessToken) throw new APIError("BAD_REQUEST", { message: `Provider ${providerId} does not support token refreshing.` });
|
|
717
|
+
try {
|
|
718
|
+
const tokens = await provider.refreshAccessToken(account.refreshToken);
|
|
719
|
+
await ctx.context.internalAdapter.updateAccount(account.id, {
|
|
720
|
+
accessToken: await setTokenUtil(tokens.accessToken, ctx.context),
|
|
721
|
+
refreshToken: await setTokenUtil(tokens.refreshToken, ctx.context),
|
|
722
|
+
accessTokenExpiresAt: tokens.accessTokenExpiresAt,
|
|
723
|
+
refreshTokenExpiresAt: tokens.refreshTokenExpiresAt
|
|
724
|
+
});
|
|
725
|
+
return ctx.json(tokens);
|
|
726
|
+
} catch (error$1) {
|
|
727
|
+
throw new APIError("BAD_REQUEST", {
|
|
728
|
+
message: "Failed to refresh access token",
|
|
729
|
+
cause: error$1
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
});
|
|
733
|
+
const accountInfo = createAuthEndpoint("/account-info", {
|
|
734
|
+
method: "POST",
|
|
735
|
+
use: [sessionMiddleware],
|
|
736
|
+
metadata: { openapi: {
|
|
737
|
+
description: "Get the account info provided by the provider",
|
|
738
|
+
responses: { "200": {
|
|
739
|
+
description: "Success",
|
|
740
|
+
content: { "application/json": { schema: {
|
|
741
|
+
type: "object",
|
|
742
|
+
properties: {
|
|
743
|
+
user: {
|
|
744
|
+
type: "object",
|
|
745
|
+
properties: {
|
|
746
|
+
id: { type: "string" },
|
|
747
|
+
name: { type: "string" },
|
|
748
|
+
email: { type: "string" },
|
|
749
|
+
image: { type: "string" },
|
|
750
|
+
emailVerified: { type: "boolean" }
|
|
751
|
+
},
|
|
752
|
+
required: ["id", "emailVerified"]
|
|
753
|
+
},
|
|
754
|
+
data: {
|
|
755
|
+
type: "object",
|
|
756
|
+
properties: {},
|
|
757
|
+
additionalProperties: true
|
|
758
|
+
}
|
|
759
|
+
},
|
|
760
|
+
required: ["user", "data"],
|
|
761
|
+
additionalProperties: false
|
|
762
|
+
} } }
|
|
763
|
+
} }
|
|
764
|
+
} },
|
|
765
|
+
body: z$1.object({ accountId: z$1.string().meta({ description: "The provider given account id for which to get the account info" }) })
|
|
766
|
+
}, async (ctx) => {
|
|
767
|
+
const account = await ctx.context.internalAdapter.findAccount(ctx.body.accountId);
|
|
768
|
+
if (!account || account.userId !== ctx.context.session.user.id) throw new APIError("BAD_REQUEST", { message: "Account not found" });
|
|
769
|
+
const provider = ctx.context.socialProviders.find((p) => p.id === account.providerId);
|
|
770
|
+
if (!provider) throw new APIError("INTERNAL_SERVER_ERROR", { message: `Provider account provider is ${account.providerId} but it is not configured` });
|
|
771
|
+
const tokens = await getAccessToken({
|
|
772
|
+
...ctx,
|
|
773
|
+
body: {
|
|
774
|
+
accountId: account.id,
|
|
775
|
+
providerId: account.providerId
|
|
776
|
+
},
|
|
777
|
+
returnHeaders: false
|
|
778
|
+
});
|
|
779
|
+
if (!tokens.accessToken) throw new APIError("BAD_REQUEST", { message: "Access token not found" });
|
|
780
|
+
const info = await provider.getUserInfo({
|
|
781
|
+
...tokens,
|
|
782
|
+
accessToken: tokens.accessToken
|
|
783
|
+
});
|
|
784
|
+
return ctx.json(info);
|
|
785
|
+
});
|
|
786
|
+
|
|
787
|
+
//#endregion
|
|
788
|
+
//#region src/oauth2/link-account.ts
|
|
789
|
+
async function handleOAuthUserInfo(c, { userInfo, account, callbackURL, disableSignUp, overrideUserInfo }) {
|
|
790
|
+
const dbUser = await c.context.internalAdapter.findOAuthUser(userInfo.email.toLowerCase(), account.accountId, account.providerId).catch((e) => {
|
|
791
|
+
logger.error("Payvo Auth was unable to query your database.\nError: ", e);
|
|
792
|
+
const errorURL = c.context.options.onAPIError?.errorURL || `${c.context.baseURL}/error`;
|
|
793
|
+
throw c.redirect(`${errorURL}?error=internal_server_error`);
|
|
794
|
+
});
|
|
795
|
+
let user = dbUser?.user;
|
|
796
|
+
let isRegister = !user;
|
|
797
|
+
if (dbUser) {
|
|
798
|
+
const hasBeenLinked = dbUser.accounts.find((a) => a.providerId === account.providerId && a.accountId === account.accountId);
|
|
799
|
+
if (!hasBeenLinked) {
|
|
800
|
+
if (!(c.context.options.account?.accountLinking?.trustedProviders)?.includes(account.providerId) && !userInfo.emailVerified || c.context.options.account?.accountLinking?.enabled === false) {
|
|
801
|
+
if (isDevelopment()) logger.warn(`User already exist but account isn't linked to ${account.providerId}. To read more about how account linking works in Payvo Auth see https://www.payvopayvo.com/docs/concepts/users-accounts#account-linking.`);
|
|
802
|
+
return {
|
|
803
|
+
error: "account not linked",
|
|
804
|
+
data: null
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
try {
|
|
808
|
+
await c.context.internalAdapter.linkAccount({
|
|
809
|
+
providerId: account.providerId,
|
|
810
|
+
accountId: userInfo.id.toString(),
|
|
811
|
+
userId: dbUser.user.id,
|
|
812
|
+
accessToken: await setTokenUtil(account.accessToken, c.context),
|
|
813
|
+
refreshToken: await setTokenUtil(account.refreshToken, c.context),
|
|
814
|
+
idToken: account.idToken,
|
|
815
|
+
accessTokenExpiresAt: account.accessTokenExpiresAt,
|
|
816
|
+
refreshTokenExpiresAt: account.refreshTokenExpiresAt,
|
|
817
|
+
scope: account.scope
|
|
818
|
+
});
|
|
819
|
+
} catch (e) {
|
|
820
|
+
logger.error("Unable to link account", e);
|
|
821
|
+
return {
|
|
822
|
+
error: "unable to link account",
|
|
823
|
+
data: null
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
if (userInfo.emailVerified && !dbUser.user.emailVerified && userInfo.email.toLowerCase() === dbUser.user.email) await c.context.internalAdapter.updateUser(dbUser.user.id, { emailVerified: true });
|
|
827
|
+
} else {
|
|
828
|
+
if (c.context.options.account?.updateAccountOnSignIn !== false) {
|
|
829
|
+
const updateData = Object.fromEntries(Object.entries({
|
|
830
|
+
idToken: account.idToken,
|
|
831
|
+
accessToken: await setTokenUtil(account.accessToken, c.context),
|
|
832
|
+
refreshToken: await setTokenUtil(account.refreshToken, c.context),
|
|
833
|
+
accessTokenExpiresAt: account.accessTokenExpiresAt,
|
|
834
|
+
refreshTokenExpiresAt: account.refreshTokenExpiresAt,
|
|
835
|
+
scope: account.scope
|
|
836
|
+
}).filter(([_, value]) => value !== void 0));
|
|
837
|
+
if (Object.keys(updateData).length > 0) await c.context.internalAdapter.updateAccount(hasBeenLinked.id, updateData);
|
|
838
|
+
}
|
|
839
|
+
if (userInfo.emailVerified && !dbUser.user.emailVerified && userInfo.email.toLowerCase() === dbUser.user.email) await c.context.internalAdapter.updateUser(dbUser.user.id, { emailVerified: true });
|
|
840
|
+
}
|
|
841
|
+
if (overrideUserInfo) {
|
|
842
|
+
const { id: _,...restUserInfo } = userInfo;
|
|
843
|
+
await c.context.internalAdapter.updateUser(dbUser.user.id, {
|
|
844
|
+
...restUserInfo,
|
|
845
|
+
email: userInfo.email.toLowerCase(),
|
|
846
|
+
emailVerified: userInfo.email.toLowerCase() === dbUser.user.email ? dbUser.user.emailVerified || userInfo.emailVerified : userInfo.emailVerified
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
} else {
|
|
850
|
+
if (disableSignUp) return {
|
|
851
|
+
error: "signup disabled",
|
|
852
|
+
data: null,
|
|
853
|
+
isRegister: false
|
|
854
|
+
};
|
|
855
|
+
try {
|
|
856
|
+
const { id: _,...restUserInfo } = userInfo;
|
|
857
|
+
user = await c.context.internalAdapter.createOAuthUser({
|
|
858
|
+
...restUserInfo,
|
|
859
|
+
email: userInfo.email.toLowerCase()
|
|
860
|
+
}, {
|
|
861
|
+
accessToken: await setTokenUtil(account.accessToken, c.context),
|
|
862
|
+
refreshToken: await setTokenUtil(account.refreshToken, c.context),
|
|
863
|
+
idToken: account.idToken,
|
|
864
|
+
accessTokenExpiresAt: account.accessTokenExpiresAt,
|
|
865
|
+
refreshTokenExpiresAt: account.refreshTokenExpiresAt,
|
|
866
|
+
scope: account.scope,
|
|
867
|
+
providerId: account.providerId,
|
|
868
|
+
accountId: userInfo.id.toString()
|
|
869
|
+
}).then((res) => res?.user);
|
|
870
|
+
if (!userInfo.emailVerified && user && c.context.options.emailVerification?.sendOnSignUp) {
|
|
871
|
+
const token = await createEmailVerificationToken(c.context.secret, user.email, void 0, c.context.options.emailVerification?.expiresIn);
|
|
872
|
+
const url = `${c.context.baseURL}/verify-email?token=${token}&callbackURL=${callbackURL}`;
|
|
873
|
+
await c.context.options.emailVerification?.sendVerificationEmail?.({
|
|
874
|
+
user,
|
|
875
|
+
url,
|
|
876
|
+
token
|
|
877
|
+
}, c.request);
|
|
878
|
+
}
|
|
879
|
+
} catch (e) {
|
|
880
|
+
logger.error(e);
|
|
881
|
+
if (e instanceof APIError$1) return {
|
|
882
|
+
error: e.message,
|
|
883
|
+
data: null,
|
|
884
|
+
isRegister: false
|
|
885
|
+
};
|
|
886
|
+
return {
|
|
887
|
+
error: "unable to create user",
|
|
888
|
+
data: null,
|
|
889
|
+
isRegister: false
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
if (!user) return {
|
|
894
|
+
error: "unable to create user",
|
|
895
|
+
data: null,
|
|
896
|
+
isRegister: false
|
|
897
|
+
};
|
|
898
|
+
const session = await c.context.internalAdapter.createSession(user.id);
|
|
899
|
+
if (!session) return {
|
|
900
|
+
error: "unable to create session",
|
|
901
|
+
data: null,
|
|
902
|
+
isRegister: false
|
|
903
|
+
};
|
|
904
|
+
return {
|
|
905
|
+
data: {
|
|
906
|
+
session,
|
|
907
|
+
user
|
|
908
|
+
},
|
|
909
|
+
error: null,
|
|
910
|
+
isRegister
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
//#endregion
|
|
915
|
+
//#region src/api/routes/callback.ts
|
|
916
|
+
const schema = z$1.object({
|
|
917
|
+
code: z$1.string().optional(),
|
|
918
|
+
error: z$1.string().optional(),
|
|
919
|
+
device_id: z$1.string().optional(),
|
|
920
|
+
error_description: z$1.string().optional(),
|
|
921
|
+
state: z$1.string().optional(),
|
|
922
|
+
user: z$1.string().optional()
|
|
923
|
+
});
|
|
924
|
+
const callbackOAuth = createAuthEndpoint("/callback/:id", {
|
|
925
|
+
method: ["GET", "POST"],
|
|
926
|
+
body: schema.optional(),
|
|
927
|
+
query: schema.optional(),
|
|
928
|
+
metadata: HIDE_METADATA
|
|
929
|
+
}, async (c) => {
|
|
930
|
+
let queryOrBody;
|
|
931
|
+
const defaultErrorURL = c.context.options.onAPIError?.errorURL || `${c.context.baseURL}/error`;
|
|
932
|
+
if (c.method === "POST") {
|
|
933
|
+
const postData = c.body ? schema.parse(c.body) : {};
|
|
934
|
+
const queryData = c.query ? schema.parse(c.query) : {};
|
|
935
|
+
const mergedData = schema.parse({
|
|
936
|
+
...postData,
|
|
937
|
+
...queryData
|
|
938
|
+
});
|
|
939
|
+
const params = new URLSearchParams();
|
|
940
|
+
for (const [key, value] of Object.entries(mergedData)) if (value !== void 0 && value !== null) params.set(key, String(value));
|
|
941
|
+
const redirectURL = `${c.context.baseURL}/callback/${c.params.id}?${params.toString()}`;
|
|
942
|
+
throw c.redirect(redirectURL);
|
|
943
|
+
}
|
|
944
|
+
try {
|
|
945
|
+
if (c.method === "GET") queryOrBody = schema.parse(c.query);
|
|
946
|
+
else if (c.method === "POST") queryOrBody = schema.parse(c.body);
|
|
947
|
+
else throw new Error("Unsupported method");
|
|
948
|
+
} catch (e) {
|
|
949
|
+
c.context.logger.error("INVALID_CALLBACK_REQUEST", e);
|
|
950
|
+
throw c.redirect(`${defaultErrorURL}?error=invalid_callback_request`);
|
|
951
|
+
}
|
|
952
|
+
const { code, error: error$1, state, error_description, device_id } = queryOrBody;
|
|
953
|
+
if (!state) {
|
|
954
|
+
c.context.logger.error("State not found", error$1);
|
|
955
|
+
const url = `${defaultErrorURL}${defaultErrorURL.includes("?") ? "&" : "?"}state=state_not_found`;
|
|
956
|
+
throw c.redirect(url);
|
|
957
|
+
}
|
|
958
|
+
const { codeVerifier, callbackURL, link, errorURL, newUserURL, requestSignUp } = await parseState(c);
|
|
959
|
+
function redirectOnError(error$2, description) {
|
|
960
|
+
const baseURL = errorURL ?? defaultErrorURL;
|
|
961
|
+
const params = new URLSearchParams({ error: error$2 });
|
|
962
|
+
if (description) params.set("error_description", description);
|
|
963
|
+
const url = `${baseURL}${baseURL.includes("?") ? "&" : "?"}${params.toString()}`;
|
|
964
|
+
throw c.redirect(url);
|
|
965
|
+
}
|
|
966
|
+
if (error$1) redirectOnError(error$1, error_description);
|
|
967
|
+
if (!code) {
|
|
968
|
+
c.context.logger.error("Code not found");
|
|
969
|
+
throw redirectOnError("no_code");
|
|
970
|
+
}
|
|
971
|
+
const provider = c.context.socialProviders.find((p) => p.id === c.params.id);
|
|
972
|
+
if (!provider) {
|
|
973
|
+
c.context.logger.error("Oauth provider with id", c.params.id, "not found");
|
|
974
|
+
throw redirectOnError("oauth_provider_not_found");
|
|
975
|
+
}
|
|
976
|
+
let tokens;
|
|
977
|
+
try {
|
|
978
|
+
tokens = await provider.validateAuthorizationCode({
|
|
979
|
+
code,
|
|
980
|
+
codeVerifier,
|
|
981
|
+
deviceId: device_id,
|
|
982
|
+
redirectURI: `${c.context.baseURL}/callback/${provider.id}`
|
|
983
|
+
});
|
|
984
|
+
} catch (e) {
|
|
985
|
+
c.context.logger.error("", e);
|
|
986
|
+
throw redirectOnError("invalid_code");
|
|
987
|
+
}
|
|
988
|
+
const userInfo = await provider.getUserInfo({
|
|
989
|
+
...tokens,
|
|
990
|
+
user: c.body?.user ? safeJSONParse(c.body.user) : void 0
|
|
991
|
+
}).then((res) => res?.user);
|
|
992
|
+
if (!userInfo) {
|
|
993
|
+
c.context.logger.error("Unable to get user info");
|
|
994
|
+
return redirectOnError("unable_to_get_user_info");
|
|
995
|
+
}
|
|
996
|
+
if (!callbackURL) {
|
|
997
|
+
c.context.logger.error("No callback URL found");
|
|
998
|
+
throw redirectOnError("no_callback_url");
|
|
999
|
+
}
|
|
1000
|
+
if (link) {
|
|
1001
|
+
if (!(c.context.options.account?.accountLinking?.trustedProviders)?.includes(provider.id) && !userInfo.emailVerified || c.context.options.account?.accountLinking?.enabled === false) {
|
|
1002
|
+
c.context.logger.error("Unable to link account - untrusted provider");
|
|
1003
|
+
return redirectOnError("unable_to_link_account");
|
|
1004
|
+
}
|
|
1005
|
+
if (userInfo.email !== link.email && c.context.options.account?.accountLinking?.allowDifferentEmails !== true) return redirectOnError("email_doesn't_match");
|
|
1006
|
+
const existingAccount = await c.context.internalAdapter.findAccount(String(userInfo.id));
|
|
1007
|
+
if (existingAccount) {
|
|
1008
|
+
if (existingAccount.userId.toString() !== link.userId.toString()) return redirectOnError("account_already_linked_to_different_user");
|
|
1009
|
+
const updateData = Object.fromEntries(Object.entries({
|
|
1010
|
+
accessToken: await setTokenUtil(tokens.accessToken, c.context),
|
|
1011
|
+
refreshToken: await setTokenUtil(tokens.refreshToken, c.context),
|
|
1012
|
+
idToken: tokens.idToken,
|
|
1013
|
+
accessTokenExpiresAt: tokens.accessTokenExpiresAt,
|
|
1014
|
+
refreshTokenExpiresAt: tokens.refreshTokenExpiresAt,
|
|
1015
|
+
scope: tokens.scopes?.join(",")
|
|
1016
|
+
}).filter(([_, value]) => value !== void 0));
|
|
1017
|
+
await c.context.internalAdapter.updateAccount(existingAccount.id, updateData);
|
|
1018
|
+
} else if (!await c.context.internalAdapter.createAccount({
|
|
1019
|
+
userId: link.userId,
|
|
1020
|
+
providerId: provider.id,
|
|
1021
|
+
accountId: String(userInfo.id),
|
|
1022
|
+
...tokens,
|
|
1023
|
+
accessToken: await setTokenUtil(tokens.accessToken, c.context),
|
|
1024
|
+
refreshToken: await setTokenUtil(tokens.refreshToken, c.context),
|
|
1025
|
+
scope: tokens.scopes?.join(",")
|
|
1026
|
+
})) return redirectOnError("unable_to_link_account");
|
|
1027
|
+
let toRedirectTo$1;
|
|
1028
|
+
try {
|
|
1029
|
+
toRedirectTo$1 = callbackURL.toString();
|
|
1030
|
+
} catch {
|
|
1031
|
+
toRedirectTo$1 = callbackURL;
|
|
1032
|
+
}
|
|
1033
|
+
throw c.redirect(toRedirectTo$1);
|
|
1034
|
+
}
|
|
1035
|
+
if (!userInfo.email) {
|
|
1036
|
+
c.context.logger.error("Provider did not return email. This could be due to misconfiguration in the provider settings.");
|
|
1037
|
+
return redirectOnError("email_not_found");
|
|
1038
|
+
}
|
|
1039
|
+
const result = await handleOAuthUserInfo(c, {
|
|
1040
|
+
userInfo: {
|
|
1041
|
+
...userInfo,
|
|
1042
|
+
id: String(userInfo.id),
|
|
1043
|
+
email: userInfo.email,
|
|
1044
|
+
name: userInfo.name || userInfo.email
|
|
1045
|
+
},
|
|
1046
|
+
account: {
|
|
1047
|
+
providerId: provider.id,
|
|
1048
|
+
accountId: String(userInfo.id),
|
|
1049
|
+
...tokens,
|
|
1050
|
+
scope: tokens.scopes?.join(",")
|
|
1051
|
+
},
|
|
1052
|
+
callbackURL,
|
|
1053
|
+
disableSignUp: provider.disableImplicitSignUp && !requestSignUp || provider.options?.disableSignUp,
|
|
1054
|
+
overrideUserInfo: provider.options?.overrideUserInfoOnSignIn
|
|
1055
|
+
});
|
|
1056
|
+
if (result.error) {
|
|
1057
|
+
c.context.logger.error(result.error.split(" ").join("_"));
|
|
1058
|
+
return redirectOnError(result.error.split(" ").join("_"));
|
|
1059
|
+
}
|
|
1060
|
+
const { session, user } = result.data;
|
|
1061
|
+
await setSessionCookie(c, {
|
|
1062
|
+
session,
|
|
1063
|
+
user
|
|
1064
|
+
});
|
|
1065
|
+
let toRedirectTo;
|
|
1066
|
+
try {
|
|
1067
|
+
toRedirectTo = (result.isRegister ? newUserURL || callbackURL : callbackURL).toString();
|
|
1068
|
+
} catch {
|
|
1069
|
+
toRedirectTo = result.isRegister ? newUserURL || callbackURL : callbackURL;
|
|
1070
|
+
}
|
|
1071
|
+
throw c.redirect(toRedirectTo);
|
|
1072
|
+
});
|
|
1073
|
+
|
|
1074
|
+
//#endregion
|
|
1075
|
+
//#region src/api/routes/email-verification.ts
|
|
1076
|
+
async function createEmailVerificationToken(secret, email, updateTo, expiresIn = 3600) {
|
|
1077
|
+
return await signJWT({
|
|
1078
|
+
email: email.toLowerCase(),
|
|
1079
|
+
updateTo
|
|
1080
|
+
}, secret, expiresIn);
|
|
1081
|
+
}
|
|
1082
|
+
/**
|
|
1083
|
+
* A function to send a verification email to the user
|
|
1084
|
+
*/
|
|
1085
|
+
async function sendVerificationEmailFn(ctx, user) {
|
|
1086
|
+
if (!ctx.context.options.emailVerification?.sendVerificationEmail) {
|
|
1087
|
+
ctx.context.logger.error("Verification email isn't enabled.");
|
|
1088
|
+
throw new APIError("BAD_REQUEST", { message: "Verification email isn't enabled" });
|
|
1089
|
+
}
|
|
1090
|
+
const token = await createEmailVerificationToken(ctx.context.secret, user.email, void 0, ctx.context.options.emailVerification?.expiresIn);
|
|
1091
|
+
const callbackURL = ctx.body.callbackURL ? encodeURIComponent(ctx.body.callbackURL) : encodeURIComponent("/");
|
|
1092
|
+
const url = `${ctx.context.baseURL}/verify-email?token=${token}&callbackURL=${callbackURL}`;
|
|
1093
|
+
await ctx.context.options.emailVerification.sendVerificationEmail({
|
|
1094
|
+
user,
|
|
1095
|
+
url,
|
|
1096
|
+
token
|
|
1097
|
+
}, ctx.request);
|
|
1098
|
+
}
|
|
1099
|
+
const sendVerificationEmail = createAuthEndpoint("/send-verification-email", {
|
|
1100
|
+
method: "POST",
|
|
1101
|
+
body: z$1.object({
|
|
1102
|
+
email: z$1.email().meta({ description: "The email to send the verification email to" }),
|
|
1103
|
+
callbackURL: z$1.string().meta({ description: "The URL to use for email verification callback" }).optional()
|
|
1104
|
+
}),
|
|
1105
|
+
metadata: { openapi: {
|
|
1106
|
+
description: "Send a verification email to the user",
|
|
1107
|
+
requestBody: { content: { "application/json": { schema: {
|
|
1108
|
+
type: "object",
|
|
1109
|
+
properties: {
|
|
1110
|
+
email: {
|
|
1111
|
+
type: "string",
|
|
1112
|
+
description: "The email to send the verification email to",
|
|
1113
|
+
example: "user@example.com"
|
|
1114
|
+
},
|
|
1115
|
+
callbackURL: {
|
|
1116
|
+
type: "string",
|
|
1117
|
+
description: "The URL to use for email verification callback",
|
|
1118
|
+
example: "https://example.com/callback",
|
|
1119
|
+
nullable: true
|
|
1120
|
+
}
|
|
1121
|
+
},
|
|
1122
|
+
required: ["email"]
|
|
1123
|
+
} } } },
|
|
1124
|
+
responses: {
|
|
1125
|
+
"200": {
|
|
1126
|
+
description: "Success",
|
|
1127
|
+
content: { "application/json": { schema: {
|
|
1128
|
+
type: "object",
|
|
1129
|
+
properties: { status: {
|
|
1130
|
+
type: "boolean",
|
|
1131
|
+
description: "Indicates if the email was sent successfully",
|
|
1132
|
+
example: true
|
|
1133
|
+
} }
|
|
1134
|
+
} } }
|
|
1135
|
+
},
|
|
1136
|
+
"400": {
|
|
1137
|
+
description: "Bad Request",
|
|
1138
|
+
content: { "application/json": { schema: {
|
|
1139
|
+
type: "object",
|
|
1140
|
+
properties: { message: {
|
|
1141
|
+
type: "string",
|
|
1142
|
+
description: "Error message",
|
|
1143
|
+
example: "Verification email isn't enabled"
|
|
1144
|
+
} }
|
|
1145
|
+
} } }
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
} }
|
|
1149
|
+
}, async (ctx) => {
|
|
1150
|
+
if (!ctx.context.options.emailVerification?.sendVerificationEmail) {
|
|
1151
|
+
ctx.context.logger.error("Verification email isn't enabled.");
|
|
1152
|
+
throw new APIError("BAD_REQUEST", { message: "Verification email isn't enabled" });
|
|
1153
|
+
}
|
|
1154
|
+
const { email } = ctx.body;
|
|
1155
|
+
const session = await getSessionFromCtx(ctx);
|
|
1156
|
+
if (!session) {
|
|
1157
|
+
const user = await ctx.context.internalAdapter.findUserByEmail(email);
|
|
1158
|
+
if (!user) return ctx.json({ status: true });
|
|
1159
|
+
await sendVerificationEmailFn(ctx, user.user);
|
|
1160
|
+
return ctx.json({ status: true });
|
|
1161
|
+
}
|
|
1162
|
+
if (session?.user.emailVerified) throw new APIError("BAD_REQUEST", { message: "You can only send a verification email to an unverified email" });
|
|
1163
|
+
if (session?.user.email !== email) throw new APIError("BAD_REQUEST", { message: "You can only send a verification email to your own email" });
|
|
1164
|
+
await sendVerificationEmailFn(ctx, session.user);
|
|
1165
|
+
return ctx.json({ status: true });
|
|
1166
|
+
});
|
|
1167
|
+
const verifyEmail = createAuthEndpoint("/verify-email", {
|
|
1168
|
+
method: "GET",
|
|
1169
|
+
query: z$1.object({
|
|
1170
|
+
token: z$1.string().meta({ description: "The token to verify the email" }),
|
|
1171
|
+
callbackURL: z$1.string().meta({ description: "The URL to redirect to after email verification" }).optional()
|
|
1172
|
+
}),
|
|
1173
|
+
use: [originCheck((ctx) => ctx.query.callbackURL)],
|
|
1174
|
+
metadata: { openapi: {
|
|
1175
|
+
description: "Verify the email of the user",
|
|
1176
|
+
parameters: [{
|
|
1177
|
+
name: "token",
|
|
1178
|
+
in: "query",
|
|
1179
|
+
description: "The token to verify the email",
|
|
1180
|
+
required: true,
|
|
1181
|
+
schema: { type: "string" }
|
|
1182
|
+
}, {
|
|
1183
|
+
name: "callbackURL",
|
|
1184
|
+
in: "query",
|
|
1185
|
+
description: "The URL to redirect to after email verification",
|
|
1186
|
+
required: false,
|
|
1187
|
+
schema: { type: "string" }
|
|
1188
|
+
}],
|
|
1189
|
+
responses: { "200": {
|
|
1190
|
+
description: "Success",
|
|
1191
|
+
content: { "application/json": { schema: {
|
|
1192
|
+
type: "object",
|
|
1193
|
+
properties: {
|
|
1194
|
+
user: {
|
|
1195
|
+
type: "object",
|
|
1196
|
+
properties: {
|
|
1197
|
+
id: {
|
|
1198
|
+
type: "string",
|
|
1199
|
+
description: "User ID"
|
|
1200
|
+
},
|
|
1201
|
+
email: {
|
|
1202
|
+
type: "string",
|
|
1203
|
+
description: "User email"
|
|
1204
|
+
},
|
|
1205
|
+
name: {
|
|
1206
|
+
type: "string",
|
|
1207
|
+
description: "User name"
|
|
1208
|
+
},
|
|
1209
|
+
image: {
|
|
1210
|
+
type: "string",
|
|
1211
|
+
description: "User image URL"
|
|
1212
|
+
},
|
|
1213
|
+
emailVerified: {
|
|
1214
|
+
type: "boolean",
|
|
1215
|
+
description: "Indicates if the user email is verified"
|
|
1216
|
+
},
|
|
1217
|
+
createdAt: {
|
|
1218
|
+
type: "string",
|
|
1219
|
+
description: "User creation date"
|
|
1220
|
+
},
|
|
1221
|
+
updatedAt: {
|
|
1222
|
+
type: "string",
|
|
1223
|
+
description: "User update date"
|
|
1224
|
+
}
|
|
1225
|
+
},
|
|
1226
|
+
required: [
|
|
1227
|
+
"id",
|
|
1228
|
+
"email",
|
|
1229
|
+
"name",
|
|
1230
|
+
"image",
|
|
1231
|
+
"emailVerified",
|
|
1232
|
+
"createdAt",
|
|
1233
|
+
"updatedAt"
|
|
1234
|
+
]
|
|
1235
|
+
},
|
|
1236
|
+
status: {
|
|
1237
|
+
type: "boolean",
|
|
1238
|
+
description: "Indicates if the email was verified successfully"
|
|
1239
|
+
}
|
|
1240
|
+
},
|
|
1241
|
+
required: ["user", "status"]
|
|
1242
|
+
} } }
|
|
1243
|
+
} }
|
|
1244
|
+
} }
|
|
1245
|
+
}, async (ctx) => {
|
|
1246
|
+
function redirectOnError(error$1) {
|
|
1247
|
+
if (ctx.query.callbackURL) {
|
|
1248
|
+
if (ctx.query.callbackURL.includes("?")) throw ctx.redirect(`${ctx.query.callbackURL}&error=${error$1}`);
|
|
1249
|
+
throw ctx.redirect(`${ctx.query.callbackURL}?error=${error$1}`);
|
|
1250
|
+
}
|
|
1251
|
+
throw new APIError("UNAUTHORIZED", { message: error$1 });
|
|
1252
|
+
}
|
|
1253
|
+
const { token } = ctx.query;
|
|
1254
|
+
let jwt;
|
|
1255
|
+
try {
|
|
1256
|
+
jwt = await jwtVerify(token, new TextEncoder().encode(ctx.context.secret), { algorithms: ["HS256"] });
|
|
1257
|
+
} catch (e) {
|
|
1258
|
+
if (e instanceof JWTExpired) return redirectOnError("token_expired");
|
|
1259
|
+
return redirectOnError("invalid_token");
|
|
1260
|
+
}
|
|
1261
|
+
const parsed = z$1.object({
|
|
1262
|
+
email: z$1.string().email(),
|
|
1263
|
+
updateTo: z$1.string().optional()
|
|
1264
|
+
}).parse(jwt.payload);
|
|
1265
|
+
const user = await ctx.context.internalAdapter.findUserByEmail(parsed.email);
|
|
1266
|
+
if (!user) return redirectOnError("user_not_found");
|
|
1267
|
+
if (parsed.updateTo) {
|
|
1268
|
+
const session = await getSessionFromCtx(ctx);
|
|
1269
|
+
if (!session) {
|
|
1270
|
+
if (ctx.query.callbackURL) throw ctx.redirect(`${ctx.query.callbackURL}?error=unauthorized`);
|
|
1271
|
+
return redirectOnError("unauthorized");
|
|
1272
|
+
}
|
|
1273
|
+
if (session.user.email !== parsed.email) {
|
|
1274
|
+
if (ctx.query.callbackURL) throw ctx.redirect(`${ctx.query.callbackURL}?error=unauthorized`);
|
|
1275
|
+
return redirectOnError("unauthorized");
|
|
1276
|
+
}
|
|
1277
|
+
const updatedUser$1 = await ctx.context.internalAdapter.updateUserByEmail(parsed.email, {
|
|
1278
|
+
email: parsed.updateTo,
|
|
1279
|
+
emailVerified: false
|
|
1280
|
+
});
|
|
1281
|
+
const newToken = await createEmailVerificationToken(ctx.context.secret, parsed.updateTo);
|
|
1282
|
+
const updateCallbackURL = ctx.query.callbackURL ? encodeURIComponent(ctx.query.callbackURL) : encodeURIComponent("/");
|
|
1283
|
+
await ctx.context.options.emailVerification?.sendVerificationEmail?.({
|
|
1284
|
+
user: updatedUser$1,
|
|
1285
|
+
url: `${ctx.context.baseURL}/verify-email?token=${newToken}&callbackURL=${updateCallbackURL}`,
|
|
1286
|
+
token: newToken
|
|
1287
|
+
}, ctx.request);
|
|
1288
|
+
await setSessionCookie(ctx, {
|
|
1289
|
+
session: session.session,
|
|
1290
|
+
user: {
|
|
1291
|
+
...session.user,
|
|
1292
|
+
email: parsed.updateTo,
|
|
1293
|
+
emailVerified: false
|
|
1294
|
+
}
|
|
1295
|
+
});
|
|
1296
|
+
if (ctx.query.callbackURL) throw ctx.redirect(ctx.query.callbackURL);
|
|
1297
|
+
return ctx.json({
|
|
1298
|
+
status: true,
|
|
1299
|
+
user: {
|
|
1300
|
+
id: updatedUser$1.id,
|
|
1301
|
+
email: updatedUser$1.email,
|
|
1302
|
+
name: updatedUser$1.name,
|
|
1303
|
+
image: updatedUser$1.image,
|
|
1304
|
+
emailVerified: updatedUser$1.emailVerified,
|
|
1305
|
+
createdAt: updatedUser$1.createdAt,
|
|
1306
|
+
updatedAt: updatedUser$1.updatedAt
|
|
1307
|
+
}
|
|
1308
|
+
});
|
|
1309
|
+
}
|
|
1310
|
+
if (ctx.context.options.emailVerification?.onEmailVerification) await ctx.context.options.emailVerification.onEmailVerification(user.user, ctx.request);
|
|
1311
|
+
const updatedUser = await ctx.context.internalAdapter.updateUserByEmail(parsed.email, { emailVerified: true });
|
|
1312
|
+
if (ctx.context.options.emailVerification?.afterEmailVerification) await ctx.context.options.emailVerification.afterEmailVerification(updatedUser, ctx.request);
|
|
1313
|
+
if (ctx.context.options.emailVerification?.autoSignInAfterVerification) {
|
|
1314
|
+
const currentSession = await getSessionFromCtx(ctx);
|
|
1315
|
+
if (!currentSession || currentSession.user.email !== parsed.email) {
|
|
1316
|
+
const session = await ctx.context.internalAdapter.createSession(user.user.id);
|
|
1317
|
+
if (!session) throw new APIError("INTERNAL_SERVER_ERROR", { message: "Failed to create session" });
|
|
1318
|
+
await setSessionCookie(ctx, {
|
|
1319
|
+
session,
|
|
1320
|
+
user: {
|
|
1321
|
+
...user.user,
|
|
1322
|
+
emailVerified: true
|
|
1323
|
+
}
|
|
1324
|
+
});
|
|
1325
|
+
} else await setSessionCookie(ctx, {
|
|
1326
|
+
session: currentSession.session,
|
|
1327
|
+
user: {
|
|
1328
|
+
...currentSession.user,
|
|
1329
|
+
emailVerified: true
|
|
1330
|
+
}
|
|
1331
|
+
});
|
|
1332
|
+
}
|
|
1333
|
+
if (ctx.query.callbackURL) throw ctx.redirect(ctx.query.callbackURL);
|
|
1334
|
+
return ctx.json({
|
|
1335
|
+
status: true,
|
|
1336
|
+
user: null
|
|
1337
|
+
});
|
|
1338
|
+
});
|
|
1339
|
+
|
|
1340
|
+
//#endregion
|
|
1341
|
+
//#region src/api/routes/error.ts
|
|
1342
|
+
function sanitize(input) {
|
|
1343
|
+
return input.replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'").replace(/&(?!amp;|lt;|gt;|quot;|#39;|#x[0-9a-fA-F]+;|#[0-9]+;)/g, "&");
|
|
1344
|
+
}
|
|
1345
|
+
const html = (options, code = "Unknown", description = null) => {
|
|
1346
|
+
const custom = options.onAPIError?.customizeDefaultErrorPage;
|
|
1347
|
+
return `<!DOCTYPE html>
|
|
1348
|
+
<html lang="en">
|
|
1349
|
+
<head>
|
|
1350
|
+
<meta charset="UTF-8" />
|
|
1351
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
1352
|
+
<title>Error</title>
|
|
1353
|
+
<style>
|
|
1354
|
+
body {
|
|
1355
|
+
font-family: ${custom?.font?.defaultFamily || "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif"};
|
|
1356
|
+
background: ${custom?.colors?.background || "var(--background)"};
|
|
1357
|
+
color: var(--foreground);
|
|
1358
|
+
margin: 0;
|
|
1359
|
+
}
|
|
1360
|
+
:root,
|
|
1361
|
+
:host {
|
|
1362
|
+
--spacing: 0.25rem;
|
|
1363
|
+
--container-md: 28rem;
|
|
1364
|
+
--text-sm: ${custom?.size?.textSm || "0.875rem"};
|
|
1365
|
+
--text-sm--line-height: calc(1.25 / 0.875);
|
|
1366
|
+
--text-2xl: ${custom?.size?.text2xl || "1.5rem"};
|
|
1367
|
+
--text-2xl--line-height: calc(2 / 1.5);
|
|
1368
|
+
--text-4xl: ${custom?.size?.text4xl || "2.25rem"};
|
|
1369
|
+
--text-4xl--line-height: calc(2.5 / 2.25);
|
|
1370
|
+
--text-6xl: ${custom?.size?.text6xl || "3.75rem"};
|
|
1371
|
+
--text-6xl--line-height: 1;
|
|
1372
|
+
--font-weight-medium: 500;
|
|
1373
|
+
--font-weight-semibold: 600;
|
|
1374
|
+
--font-weight-bold: 700;
|
|
1375
|
+
--default-transition-duration: 150ms;
|
|
1376
|
+
--default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
|
1377
|
+
--radius: ${custom?.size?.radiusSm || "0.625rem"};
|
|
1378
|
+
--default-mono-font-family: ${custom?.font?.monoFamily || "var(--font-geist-mono)"};
|
|
1379
|
+
--primary: ${custom?.colors?.primary || "black"};
|
|
1380
|
+
--primary-foreground: ${custom?.colors?.primaryForeground || "white"};
|
|
1381
|
+
--background: ${custom?.colors?.background || "white"};
|
|
1382
|
+
--foreground: ${custom?.colors?.foreground || "oklch(0.271 0 0)"};
|
|
1383
|
+
--border: ${custom?.colors?.border || "oklch(0.89 0 0)"};
|
|
1384
|
+
--destructive: ${custom?.colors?.destructive || "oklch(0.55 0.15 25.723)"};
|
|
1385
|
+
--muted-foreground: ${custom?.colors?.mutedForeground || "oklch(0.545 0 0)"};
|
|
1386
|
+
--corner-border: ${custom?.colors?.cornerBorder || "#404040"};
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
button, .btn {
|
|
1390
|
+
cursor: pointer;
|
|
1391
|
+
background: none;
|
|
1392
|
+
border: none;
|
|
1393
|
+
color: inherit;
|
|
1394
|
+
font: inherit;
|
|
1395
|
+
transition: all var(--default-transition-duration)
|
|
1396
|
+
var(--default-transition-timing-function);
|
|
1397
|
+
}
|
|
1398
|
+
button:hover, .btn:hover {
|
|
1399
|
+
opacity: 0.8;
|
|
1400
|
+
}
|
|
1401
|
+
</style>
|
|
1402
|
+
</head>
|
|
1403
|
+
<body style="width: 100vw; height: 100vh; overflow: hidden;">
|
|
1404
|
+
<div
|
|
1405
|
+
style="
|
|
1406
|
+
display: flex;
|
|
1407
|
+
flex-direction: column;
|
|
1408
|
+
align-items: center;
|
|
1409
|
+
justify-content: center;
|
|
1410
|
+
gap: 1.5rem;
|
|
1411
|
+
position: relative;
|
|
1412
|
+
width: 100vw;
|
|
1413
|
+
height: 100vh;
|
|
1414
|
+
"
|
|
1415
|
+
>
|
|
1416
|
+
${custom?.disableBackgroundGrid ? "" : `
|
|
1417
|
+
<div
|
|
1418
|
+
style="
|
|
1419
|
+
position: absolute;
|
|
1420
|
+
inset: 0;
|
|
1421
|
+
background-image: linear-gradient(to right, ${custom?.colors?.gridColor || "var(--border)"} 1px, transparent 1px),
|
|
1422
|
+
linear-gradient(to bottom, ${custom?.colors?.gridColor || "var(--border)"} 1px, transparent 1px);
|
|
1423
|
+
opacity: 0.5;
|
|
1424
|
+
background-size: 24px 24px;
|
|
1425
|
+
pointer-events: none;
|
|
1426
|
+
width: 100vw;
|
|
1427
|
+
height: 100vh;
|
|
1428
|
+
"
|
|
1429
|
+
></div>
|
|
1430
|
+
`}
|
|
1431
|
+
|
|
1432
|
+
<div
|
|
1433
|
+
style="
|
|
1434
|
+
position: relative;
|
|
1435
|
+
z-index: 10;
|
|
1436
|
+
border: 2px solid var(--border);
|
|
1437
|
+
background: ${custom?.colors?.cardBackground || "var(--background)"};
|
|
1438
|
+
padding: 3rem;
|
|
1439
|
+
max-width: 42rem;
|
|
1440
|
+
min-width: 297px;
|
|
1441
|
+
width: 100%;
|
|
1442
|
+
"
|
|
1443
|
+
>
|
|
1444
|
+
${custom?.disableCornerDecorations ? "" : `
|
|
1445
|
+
<!-- Corner decorations -->
|
|
1446
|
+
<div
|
|
1447
|
+
style="
|
|
1448
|
+
position: absolute;
|
|
1449
|
+
top: -2px;
|
|
1450
|
+
left: -2px;
|
|
1451
|
+
width: 3rem;
|
|
1452
|
+
height: 3rem;
|
|
1453
|
+
border-top: 4px solid var(--corner-border);
|
|
1454
|
+
border-left: 4px solid var(--corner-border);
|
|
1455
|
+
"
|
|
1456
|
+
></div>
|
|
1457
|
+
<div
|
|
1458
|
+
style="
|
|
1459
|
+
position: absolute;
|
|
1460
|
+
top: -2px;
|
|
1461
|
+
right: -2px;
|
|
1462
|
+
width: 3rem;
|
|
1463
|
+
height: 3rem;
|
|
1464
|
+
border-top: 4px solid var(--corner-border);
|
|
1465
|
+
border-right: 4px solid var(--corner-border);
|
|
1466
|
+
"
|
|
1467
|
+
></div>
|
|
1468
|
+
|
|
1469
|
+
<div
|
|
1470
|
+
style="
|
|
1471
|
+
position: absolute;
|
|
1472
|
+
bottom: -2px;
|
|
1473
|
+
left: -2px;
|
|
1474
|
+
width: 3rem;
|
|
1475
|
+
height: 3rem;
|
|
1476
|
+
border-bottom: 4px solid var(--corner-border);
|
|
1477
|
+
border-left: 4px solid var(--corner-border);
|
|
1478
|
+
"
|
|
1479
|
+
></div>
|
|
1480
|
+
<div
|
|
1481
|
+
style="
|
|
1482
|
+
position: absolute;
|
|
1483
|
+
bottom: -2px;
|
|
1484
|
+
right: -2px;
|
|
1485
|
+
width: 3rem;
|
|
1486
|
+
height: 3rem;
|
|
1487
|
+
border-bottom: 4px solid var(--corner-border);
|
|
1488
|
+
border-right: 4px solid var(--corner-border);
|
|
1489
|
+
"
|
|
1490
|
+
></div>`}
|
|
1491
|
+
|
|
1492
|
+
<div style="text-align: center; margin-bottom: 2rem;">
|
|
1493
|
+
<div style="margin-bottom: 1.5rem;">
|
|
1494
|
+
<div
|
|
1495
|
+
style="
|
|
1496
|
+
display: inline-block;
|
|
1497
|
+
border: 2px solid ${custom?.disableTitleBorder ? "transparent" : custom?.colors?.titleBorder || "var(--destructive)"};
|
|
1498
|
+
padding: 0.5rem 1.5rem;
|
|
1499
|
+
"
|
|
1500
|
+
>
|
|
1501
|
+
<h1
|
|
1502
|
+
style="
|
|
1503
|
+
font-size: var(--text-6xl);
|
|
1504
|
+
font-weight: var(--font-weight-bold);
|
|
1505
|
+
color: ${custom?.colors?.titleColor || "var(--foreground)"};
|
|
1506
|
+
letter-spacing: -0.02em;
|
|
1507
|
+
margin: 0;
|
|
1508
|
+
"
|
|
1509
|
+
>
|
|
1510
|
+
ERROR
|
|
1511
|
+
</h1>
|
|
1512
|
+
</div>
|
|
1513
|
+
<div
|
|
1514
|
+
style="
|
|
1515
|
+
height: 1px;
|
|
1516
|
+
background-color: var(--border);
|
|
1517
|
+
width: 100%;
|
|
1518
|
+
margin-top: 1rem;
|
|
1519
|
+
"
|
|
1520
|
+
></div>
|
|
1521
|
+
</div>
|
|
1522
|
+
|
|
1523
|
+
<h2
|
|
1524
|
+
style="
|
|
1525
|
+
font-size: var(--text-2xl);
|
|
1526
|
+
font-weight: var(--font-weight-semibold);
|
|
1527
|
+
color: var(--foreground);
|
|
1528
|
+
margin: 0 0 1.5rem;
|
|
1529
|
+
"
|
|
1530
|
+
>
|
|
1531
|
+
Something went wrong
|
|
1532
|
+
</h2>
|
|
1533
|
+
|
|
1534
|
+
<div
|
|
1535
|
+
style="
|
|
1536
|
+
display: inline-flex;
|
|
1537
|
+
align-items: center;
|
|
1538
|
+
gap: 0.5rem;
|
|
1539
|
+
border: 2px solid var(--border);
|
|
1540
|
+
background-color: var(--muted);
|
|
1541
|
+
padding: 0.5rem 1rem;
|
|
1542
|
+
margin: 0 0 1.5rem;
|
|
1543
|
+
"
|
|
1544
|
+
>
|
|
1545
|
+
<span
|
|
1546
|
+
style="
|
|
1547
|
+
font-size: 0.75rem;
|
|
1548
|
+
color: var(--muted-foreground);
|
|
1549
|
+
font-weight: var(--font-weight-semibold);
|
|
1550
|
+
"
|
|
1551
|
+
>
|
|
1552
|
+
CODE:
|
|
1553
|
+
</span>
|
|
1554
|
+
<span
|
|
1555
|
+
style="
|
|
1556
|
+
font-size: var(--text-sm);
|
|
1557
|
+
font-family: var(--default-mono-font-family, monospace);
|
|
1558
|
+
color: var(--foreground);
|
|
1559
|
+
"
|
|
1560
|
+
>
|
|
1561
|
+
${sanitize(code)}
|
|
1562
|
+
</span>
|
|
1563
|
+
</div>
|
|
1564
|
+
|
|
1565
|
+
<p
|
|
1566
|
+
style="
|
|
1567
|
+
color: var(--muted-foreground);
|
|
1568
|
+
max-width: 28rem;
|
|
1569
|
+
margin: 0 auto;
|
|
1570
|
+
border-left: 2px solid var(--border);
|
|
1571
|
+
padding-left: 1rem;
|
|
1572
|
+
"
|
|
1573
|
+
>
|
|
1574
|
+
${!description ? `We encountered an unexpected error. Please try again or return to the home page. If you're a developer, you can find more information about the error <a href='https://payvopayvo.com/docs/errors/${encodeURIComponent(code)}' target='_blank' rel="noopener noreferrer" style='color: var(--foreground); text-decoration: underline;'>here</a>.` : description}
|
|
1575
|
+
</p>
|
|
1576
|
+
</div>
|
|
1577
|
+
|
|
1578
|
+
<div
|
|
1579
|
+
style="
|
|
1580
|
+
display: flex;
|
|
1581
|
+
gap: 1rem;
|
|
1582
|
+
margin-top: 2rem;
|
|
1583
|
+
justify-content: center;
|
|
1584
|
+
"
|
|
1585
|
+
>
|
|
1586
|
+
<a
|
|
1587
|
+
href="/"
|
|
1588
|
+
style="
|
|
1589
|
+
text-decoration: none;
|
|
1590
|
+
"
|
|
1591
|
+
>
|
|
1592
|
+
<div
|
|
1593
|
+
style="
|
|
1594
|
+
border: 2px solid var(--border);
|
|
1595
|
+
background: var(--primary);
|
|
1596
|
+
color: var(--primary-foreground);
|
|
1597
|
+
padding: 0.5rem 1.25rem;
|
|
1598
|
+
border-radius: 0;
|
|
1599
|
+
"
|
|
1600
|
+
class="btn"
|
|
1601
|
+
>
|
|
1602
|
+
Go Home
|
|
1603
|
+
</div>
|
|
1604
|
+
</a>
|
|
1605
|
+
<a
|
|
1606
|
+
href="https://payvopayvo.com/docs/errors/${encodeURIComponent(code)}?askai=${encodeURIComponent(`What does the error code ${code} mean?`)}"
|
|
1607
|
+
target="_blank"
|
|
1608
|
+
rel="noopener noreferrer"
|
|
1609
|
+
style="
|
|
1610
|
+
text-decoration: none;
|
|
1611
|
+
"
|
|
1612
|
+
>
|
|
1613
|
+
<div
|
|
1614
|
+
style="
|
|
1615
|
+
border: 2px solid var(--border);
|
|
1616
|
+
background: transparent;
|
|
1617
|
+
color: var(--foreground);
|
|
1618
|
+
padding: 0.5rem 1.25rem;
|
|
1619
|
+
border-radius: 0;
|
|
1620
|
+
"
|
|
1621
|
+
class="btn"
|
|
1622
|
+
>
|
|
1623
|
+
Ask AI
|
|
1624
|
+
</div>
|
|
1625
|
+
</a>
|
|
1626
|
+
</div>
|
|
1627
|
+
</div>
|
|
1628
|
+
</div>
|
|
1629
|
+
</body>
|
|
1630
|
+
</html>`;
|
|
1631
|
+
};
|
|
1632
|
+
const error = createAuthEndpoint("/error", {
|
|
1633
|
+
method: "GET",
|
|
1634
|
+
metadata: {
|
|
1635
|
+
...HIDE_METADATA,
|
|
1636
|
+
openapi: {
|
|
1637
|
+
description: "Displays an error page",
|
|
1638
|
+
responses: { "200": {
|
|
1639
|
+
description: "Success",
|
|
1640
|
+
content: { "text/html": { schema: {
|
|
1641
|
+
type: "string",
|
|
1642
|
+
description: "The HTML content of the error page"
|
|
1643
|
+
} } }
|
|
1644
|
+
} }
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
}, async (c) => {
|
|
1648
|
+
const url = new URL(c.request?.url || "");
|
|
1649
|
+
const unsanitizedCode = url.searchParams.get("error") || "UNKNOWN";
|
|
1650
|
+
const unsanitizedDescription = url.searchParams.get("error_description") || null;
|
|
1651
|
+
const safeCode = /^[\'A-Za-z0-9_-]+$/.test(unsanitizedCode || "") ? unsanitizedCode : "UNKNOWN";
|
|
1652
|
+
const safeDescription = unsanitizedDescription ? sanitize(unsanitizedDescription) : null;
|
|
1653
|
+
const queryParams = new URLSearchParams();
|
|
1654
|
+
queryParams.set("error", safeCode);
|
|
1655
|
+
if (unsanitizedDescription) queryParams.set("error_description", unsanitizedDescription);
|
|
1656
|
+
const options = c.context.options;
|
|
1657
|
+
const errorURL = options.onAPIError?.errorURL;
|
|
1658
|
+
if (errorURL) return new Response(null, {
|
|
1659
|
+
status: 302,
|
|
1660
|
+
headers: { Location: `${errorURL}${errorURL.includes("?") ? "&" : "?"}${queryParams.toString()}` }
|
|
1661
|
+
});
|
|
1662
|
+
if (isProduction && !options.onAPIError?.customizeDefaultErrorPage) return new Response(null, {
|
|
1663
|
+
status: 302,
|
|
1664
|
+
headers: { Location: `/?${queryParams.toString()}` }
|
|
1665
|
+
});
|
|
1666
|
+
return new Response(html(c.context.options, safeCode, safeDescription), { headers: { "Content-Type": "text/html" } });
|
|
1667
|
+
});
|
|
1668
|
+
|
|
1669
|
+
//#endregion
|
|
1670
|
+
//#region src/api/routes/ok.ts
|
|
1671
|
+
const ok = createAuthEndpoint("/ok", {
|
|
1672
|
+
method: "GET",
|
|
1673
|
+
metadata: {
|
|
1674
|
+
...HIDE_METADATA,
|
|
1675
|
+
openapi: {
|
|
1676
|
+
description: "Check if the API is working",
|
|
1677
|
+
responses: { "200": {
|
|
1678
|
+
description: "API is working",
|
|
1679
|
+
content: { "application/json": { schema: {
|
|
1680
|
+
type: "object",
|
|
1681
|
+
properties: { ok: {
|
|
1682
|
+
type: "boolean",
|
|
1683
|
+
description: "Indicates if the API is working"
|
|
1684
|
+
} },
|
|
1685
|
+
required: ["ok"]
|
|
1686
|
+
} } }
|
|
1687
|
+
} }
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
}, async (ctx) => {
|
|
1691
|
+
return ctx.json({ ok: true });
|
|
1692
|
+
});
|
|
1693
|
+
|
|
1694
|
+
//#endregion
|
|
1695
|
+
//#region src/api/routes/reset-password.ts
|
|
1696
|
+
function redirectError(ctx, callbackURL, query) {
|
|
1697
|
+
const url = callbackURL ? new URL(callbackURL, ctx.baseURL) : new URL(`${ctx.baseURL}/error`);
|
|
1698
|
+
if (query) Object.entries(query).forEach(([k, v]) => url.searchParams.set(k, v));
|
|
1699
|
+
return url.href;
|
|
1700
|
+
}
|
|
1701
|
+
function redirectCallback(ctx, callbackURL, query) {
|
|
1702
|
+
const url = new URL(callbackURL, ctx.baseURL);
|
|
1703
|
+
if (query) Object.entries(query).forEach(([k, v]) => url.searchParams.set(k, v));
|
|
1704
|
+
return url.href;
|
|
1705
|
+
}
|
|
1706
|
+
const requestPasswordReset = createAuthEndpoint("/request-password-reset", {
|
|
1707
|
+
method: "POST",
|
|
1708
|
+
body: z$1.object({
|
|
1709
|
+
email: z$1.email().meta({ description: "The email address of the user to send a password reset email to" }),
|
|
1710
|
+
redirectTo: z$1.string().meta({ description: "The URL to redirect the user to reset their password. If the token isn't valid or expired, it'll be redirected with a query parameter `?error=INVALID_TOKEN`. If the token is valid, it'll be redirected with a query parameter `?token=VALID_TOKEN" }).optional()
|
|
1711
|
+
}),
|
|
1712
|
+
metadata: { openapi: {
|
|
1713
|
+
description: "Send a password reset email to the user",
|
|
1714
|
+
responses: { "200": {
|
|
1715
|
+
description: "Success",
|
|
1716
|
+
content: { "application/json": { schema: {
|
|
1717
|
+
type: "object",
|
|
1718
|
+
properties: {
|
|
1719
|
+
status: { type: "boolean" },
|
|
1720
|
+
message: { type: "string" }
|
|
1721
|
+
}
|
|
1722
|
+
} } }
|
|
1723
|
+
} }
|
|
1724
|
+
} }
|
|
1725
|
+
}, async (ctx) => {
|
|
1726
|
+
if (!ctx.context.options.emailAndPassword?.sendResetPassword) {
|
|
1727
|
+
ctx.context.logger.error("Reset password isn't enabled.Please pass an emailAndPassword.sendResetPassword function in your auth config!");
|
|
1728
|
+
throw new APIError("BAD_REQUEST", { message: "Reset password isn't enabled" });
|
|
1729
|
+
}
|
|
1730
|
+
const { email, redirectTo } = ctx.body;
|
|
1731
|
+
const user = await ctx.context.internalAdapter.findUserByEmail(email, { includeAccounts: true });
|
|
1732
|
+
if (!user) {
|
|
1733
|
+
ctx.context.logger.error("Reset Password: User not found", { email });
|
|
1734
|
+
return ctx.json({
|
|
1735
|
+
status: true,
|
|
1736
|
+
message: "If this email exists in our system, check your email for the reset link"
|
|
1737
|
+
});
|
|
1738
|
+
}
|
|
1739
|
+
const expiresAt = getDate(ctx.context.options.emailAndPassword.resetPasswordTokenExpiresIn || 3600 * 1, "sec");
|
|
1740
|
+
const verificationToken = generateId(24);
|
|
1741
|
+
await ctx.context.internalAdapter.createVerificationValue({
|
|
1742
|
+
value: user.user.id,
|
|
1743
|
+
identifier: `reset-password:${verificationToken}`,
|
|
1744
|
+
expiresAt
|
|
1745
|
+
});
|
|
1746
|
+
const callbackURL = redirectTo ? encodeURIComponent(redirectTo) : "";
|
|
1747
|
+
const url = `${ctx.context.baseURL}/reset-password/${verificationToken}?callbackURL=${callbackURL}`;
|
|
1748
|
+
await ctx.context.options.emailAndPassword.sendResetPassword({
|
|
1749
|
+
user: user.user,
|
|
1750
|
+
url,
|
|
1751
|
+
token: verificationToken
|
|
1752
|
+
}, ctx.request);
|
|
1753
|
+
return ctx.json({
|
|
1754
|
+
status: true,
|
|
1755
|
+
message: "If this email exists in our system, check your email for the reset link"
|
|
1756
|
+
});
|
|
1757
|
+
});
|
|
1758
|
+
const requestPasswordResetCallback = createAuthEndpoint("/reset-password/:token", {
|
|
1759
|
+
method: "GET",
|
|
1760
|
+
query: z$1.object({ callbackURL: z$1.string().meta({ description: "The URL to redirect the user to reset their password" }) }),
|
|
1761
|
+
use: [originCheck((ctx) => ctx.query.callbackURL)],
|
|
1762
|
+
metadata: { openapi: {
|
|
1763
|
+
description: "Redirects the user to the callback URL with the token",
|
|
1764
|
+
responses: { "200": {
|
|
1765
|
+
description: "Success",
|
|
1766
|
+
content: { "application/json": { schema: {
|
|
1767
|
+
type: "object",
|
|
1768
|
+
properties: { token: { type: "string" } }
|
|
1769
|
+
} } }
|
|
1770
|
+
} }
|
|
1771
|
+
} }
|
|
1772
|
+
}, async (ctx) => {
|
|
1773
|
+
const { token } = ctx.params;
|
|
1774
|
+
const { callbackURL } = ctx.query;
|
|
1775
|
+
if (!token || !callbackURL) throw ctx.redirect(redirectError(ctx.context, callbackURL, { error: "INVALID_TOKEN" }));
|
|
1776
|
+
const verification = await ctx.context.internalAdapter.findVerificationValue(`reset-password:${token}`);
|
|
1777
|
+
if (!verification || verification.expiresAt < /* @__PURE__ */ new Date()) throw ctx.redirect(redirectError(ctx.context, callbackURL, { error: "INVALID_TOKEN" }));
|
|
1778
|
+
throw ctx.redirect(redirectCallback(ctx.context, callbackURL, { token }));
|
|
1779
|
+
});
|
|
1780
|
+
const resetPassword = createAuthEndpoint("/reset-password", {
|
|
1781
|
+
method: "POST",
|
|
1782
|
+
query: z$1.object({ token: z$1.string().optional() }).optional(),
|
|
1783
|
+
body: z$1.object({
|
|
1784
|
+
newPassword: z$1.string().meta({ description: "The new password to set" }),
|
|
1785
|
+
token: z$1.string().meta({ description: "The token to reset the password" }).optional()
|
|
1786
|
+
}),
|
|
1787
|
+
metadata: { openapi: {
|
|
1788
|
+
description: "Reset the password for a user",
|
|
1789
|
+
responses: { "200": {
|
|
1790
|
+
description: "Success",
|
|
1791
|
+
content: { "application/json": { schema: {
|
|
1792
|
+
type: "object",
|
|
1793
|
+
properties: { status: { type: "boolean" } }
|
|
1794
|
+
} } }
|
|
1795
|
+
} }
|
|
1796
|
+
} }
|
|
1797
|
+
}, async (ctx) => {
|
|
1798
|
+
const token = ctx.body.token || ctx.query?.token;
|
|
1799
|
+
if (!token) throw new APIError("BAD_REQUEST", { message: BASE_ERROR_CODES.INVALID_TOKEN });
|
|
1800
|
+
const { newPassword } = ctx.body;
|
|
1801
|
+
const minLength = ctx.context.password?.config.minPasswordLength;
|
|
1802
|
+
const maxLength = ctx.context.password?.config.maxPasswordLength;
|
|
1803
|
+
if (newPassword.length < minLength) throw new APIError("BAD_REQUEST", { message: BASE_ERROR_CODES.PASSWORD_TOO_SHORT });
|
|
1804
|
+
if (newPassword.length > maxLength) throw new APIError("BAD_REQUEST", { message: BASE_ERROR_CODES.PASSWORD_TOO_LONG });
|
|
1805
|
+
const id = `reset-password:${token}`;
|
|
1806
|
+
const verification = await ctx.context.internalAdapter.findVerificationValue(id);
|
|
1807
|
+
if (!verification || verification.expiresAt < /* @__PURE__ */ new Date()) throw new APIError("BAD_REQUEST", { message: BASE_ERROR_CODES.INVALID_TOKEN });
|
|
1808
|
+
const userId = verification.value;
|
|
1809
|
+
const hashedPassword = await ctx.context.password.hash(newPassword);
|
|
1810
|
+
if (!(await ctx.context.internalAdapter.findAccounts(userId)).find((ac) => ac.providerId === "credential")) await ctx.context.internalAdapter.createAccount({
|
|
1811
|
+
userId,
|
|
1812
|
+
providerId: "credential",
|
|
1813
|
+
password: hashedPassword,
|
|
1814
|
+
accountId: userId
|
|
1815
|
+
});
|
|
1816
|
+
else await ctx.context.internalAdapter.updatePassword(userId, hashedPassword);
|
|
1817
|
+
await ctx.context.internalAdapter.deleteVerificationValue(verification.id);
|
|
1818
|
+
if (ctx.context.options.emailAndPassword?.onPasswordReset) {
|
|
1819
|
+
const user = await ctx.context.internalAdapter.findUserById(userId);
|
|
1820
|
+
if (user) await ctx.context.options.emailAndPassword.onPasswordReset({ user }, ctx.request);
|
|
1821
|
+
}
|
|
1822
|
+
if (ctx.context.options.emailAndPassword?.revokeSessionsOnPasswordReset) await ctx.context.internalAdapter.deleteSessions(userId);
|
|
1823
|
+
return ctx.json({ status: true });
|
|
1824
|
+
});
|
|
1825
|
+
|
|
1826
|
+
//#endregion
|
|
1827
|
+
//#region src/api/routes/sign-in.ts
|
|
1828
|
+
const signInSocial = createAuthEndpoint("/sign-in/social", {
|
|
1829
|
+
method: "POST",
|
|
1830
|
+
body: z$1.object({
|
|
1831
|
+
callbackURL: z$1.string().meta({ description: "Callback URL to redirect to after the user has signed in" }).optional(),
|
|
1832
|
+
newUserCallbackURL: z$1.string().optional(),
|
|
1833
|
+
errorCallbackURL: z$1.string().meta({ description: "Callback URL to redirect to if an error happens" }).optional(),
|
|
1834
|
+
provider: SocialProviderListEnum,
|
|
1835
|
+
disableRedirect: z$1.boolean().meta({ description: "Disable automatic redirection to the provider. Useful for handling the redirection yourself" }).optional(),
|
|
1836
|
+
idToken: z$1.optional(z$1.object({
|
|
1837
|
+
token: z$1.string().meta({ description: "ID token from the provider" }),
|
|
1838
|
+
nonce: z$1.string().meta({ description: "Nonce used to generate the token" }).optional(),
|
|
1839
|
+
accessToken: z$1.string().meta({ description: "Access token from the provider" }).optional(),
|
|
1840
|
+
refreshToken: z$1.string().meta({ description: "Refresh token from the provider" }).optional(),
|
|
1841
|
+
expiresAt: z$1.number().meta({ description: "Expiry date of the token" }).optional()
|
|
1842
|
+
})),
|
|
1843
|
+
scopes: z$1.array(z$1.string()).meta({ description: "Array of scopes to request from the provider. This will override the default scopes passed." }).optional(),
|
|
1844
|
+
requestSignUp: z$1.boolean().meta({ description: "Explicitly request sign-up. Useful when disableImplicitSignUp is true for this provider" }).optional(),
|
|
1845
|
+
loginHint: z$1.string().meta({ description: "The login hint to use for the authorization code request" }).optional(),
|
|
1846
|
+
additionalData: z$1.record(z$1.string(), z$1.any()).optional().meta({ description: "Additional data to be passed through the OAuth flow" })
|
|
1847
|
+
}),
|
|
1848
|
+
metadata: { openapi: {
|
|
1849
|
+
description: "Sign in with a social provider",
|
|
1850
|
+
operationId: "socialSignIn",
|
|
1851
|
+
responses: { "200": {
|
|
1852
|
+
description: "Success - Returns either session details or redirect URL",
|
|
1853
|
+
content: { "application/json": { schema: {
|
|
1854
|
+
type: "object",
|
|
1855
|
+
description: "Session response when idToken is provided",
|
|
1856
|
+
properties: {
|
|
1857
|
+
redirect: {
|
|
1858
|
+
type: "boolean",
|
|
1859
|
+
enum: [false]
|
|
1860
|
+
},
|
|
1861
|
+
token: {
|
|
1862
|
+
type: "string",
|
|
1863
|
+
description: "Session token",
|
|
1864
|
+
url: {
|
|
1865
|
+
type: "null",
|
|
1866
|
+
nullable: true
|
|
1867
|
+
},
|
|
1868
|
+
user: {
|
|
1869
|
+
type: "object",
|
|
1870
|
+
properties: {
|
|
1871
|
+
id: { type: "string" },
|
|
1872
|
+
email: { type: "string" },
|
|
1873
|
+
name: {
|
|
1874
|
+
type: "string",
|
|
1875
|
+
nullable: true
|
|
1876
|
+
},
|
|
1877
|
+
image: {
|
|
1878
|
+
type: "string",
|
|
1879
|
+
nullable: true
|
|
1880
|
+
},
|
|
1881
|
+
emailVerified: { type: "boolean" },
|
|
1882
|
+
createdAt: {
|
|
1883
|
+
type: "string",
|
|
1884
|
+
format: "date-time"
|
|
1885
|
+
},
|
|
1886
|
+
updatedAt: {
|
|
1887
|
+
type: "string",
|
|
1888
|
+
format: "date-time"
|
|
1889
|
+
}
|
|
1890
|
+
},
|
|
1891
|
+
required: [
|
|
1892
|
+
"id",
|
|
1893
|
+
"email",
|
|
1894
|
+
"emailVerified",
|
|
1895
|
+
"createdAt",
|
|
1896
|
+
"updatedAt"
|
|
1897
|
+
]
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
},
|
|
1901
|
+
required: [
|
|
1902
|
+
"redirect",
|
|
1903
|
+
"token",
|
|
1904
|
+
"user"
|
|
1905
|
+
]
|
|
1906
|
+
} } }
|
|
1907
|
+
} }
|
|
1908
|
+
} }
|
|
1909
|
+
}, async (c) => {
|
|
1910
|
+
const provider = c.context.socialProviders.find((p) => p.id === c.body.provider);
|
|
1911
|
+
if (!provider) {
|
|
1912
|
+
c.context.logger.error("Provider not found. Make sure to add the provider in your auth config", { provider: c.body.provider });
|
|
1913
|
+
throw new APIError("NOT_FOUND", { message: BASE_ERROR_CODES.PROVIDER_NOT_FOUND });
|
|
1914
|
+
}
|
|
1915
|
+
if (c.body.idToken) {
|
|
1916
|
+
if (!provider.verifyIdToken) {
|
|
1917
|
+
c.context.logger.error("Provider does not support id token verification", { provider: c.body.provider });
|
|
1918
|
+
throw new APIError("NOT_FOUND", { message: BASE_ERROR_CODES.ID_TOKEN_NOT_SUPPORTED });
|
|
1919
|
+
}
|
|
1920
|
+
const { token, nonce } = c.body.idToken;
|
|
1921
|
+
if (!await provider.verifyIdToken(token, nonce)) {
|
|
1922
|
+
c.context.logger.error("Invalid id token", { provider: c.body.provider });
|
|
1923
|
+
throw new APIError("UNAUTHORIZED", { message: BASE_ERROR_CODES.INVALID_TOKEN });
|
|
1924
|
+
}
|
|
1925
|
+
const userInfo = await provider.getUserInfo({
|
|
1926
|
+
idToken: token,
|
|
1927
|
+
accessToken: c.body.idToken.accessToken,
|
|
1928
|
+
refreshToken: c.body.idToken.refreshToken
|
|
1929
|
+
});
|
|
1930
|
+
if (!userInfo || !userInfo?.user) {
|
|
1931
|
+
c.context.logger.error("Failed to get user info", { provider: c.body.provider });
|
|
1932
|
+
throw new APIError("UNAUTHORIZED", { message: BASE_ERROR_CODES.FAILED_TO_GET_USER_INFO });
|
|
1933
|
+
}
|
|
1934
|
+
if (!userInfo.user.email) {
|
|
1935
|
+
c.context.logger.error("User email not found", { provider: c.body.provider });
|
|
1936
|
+
throw new APIError("UNAUTHORIZED", { message: BASE_ERROR_CODES.USER_EMAIL_NOT_FOUND });
|
|
1937
|
+
}
|
|
1938
|
+
const data = await handleOAuthUserInfo(c, {
|
|
1939
|
+
userInfo: {
|
|
1940
|
+
...userInfo.user,
|
|
1941
|
+
email: userInfo.user.email,
|
|
1942
|
+
id: String(userInfo.user.id),
|
|
1943
|
+
name: userInfo.user.name || "",
|
|
1944
|
+
image: userInfo.user.image,
|
|
1945
|
+
emailVerified: userInfo.user.emailVerified || false
|
|
1946
|
+
},
|
|
1947
|
+
account: {
|
|
1948
|
+
providerId: provider.id,
|
|
1949
|
+
accountId: String(userInfo.user.id),
|
|
1950
|
+
accessToken: c.body.idToken.accessToken
|
|
1951
|
+
},
|
|
1952
|
+
callbackURL: c.body.callbackURL,
|
|
1953
|
+
disableSignUp: provider.disableImplicitSignUp && !c.body.requestSignUp || provider.disableSignUp
|
|
1954
|
+
});
|
|
1955
|
+
if (data.error) throw new APIError("UNAUTHORIZED", { message: data.error });
|
|
1956
|
+
await setSessionCookie(c, data.data);
|
|
1957
|
+
return c.json({
|
|
1958
|
+
redirect: false,
|
|
1959
|
+
token: data.data.session.token,
|
|
1960
|
+
url: void 0,
|
|
1961
|
+
user: {
|
|
1962
|
+
id: data.data.user.id,
|
|
1963
|
+
email: data.data.user.email,
|
|
1964
|
+
name: data.data.user.name,
|
|
1965
|
+
image: data.data.user.image,
|
|
1966
|
+
emailVerified: data.data.user.emailVerified,
|
|
1967
|
+
createdAt: data.data.user.createdAt,
|
|
1968
|
+
updatedAt: data.data.user.updatedAt
|
|
1969
|
+
}
|
|
1970
|
+
});
|
|
1971
|
+
}
|
|
1972
|
+
const { codeVerifier, state } = await generateState(c, void 0, c.body.additionalData);
|
|
1973
|
+
const url = await provider.createAuthorizationURL({
|
|
1974
|
+
state,
|
|
1975
|
+
codeVerifier,
|
|
1976
|
+
redirectURI: `${c.context.baseURL}/callback/${provider.id}`,
|
|
1977
|
+
scopes: c.body.scopes,
|
|
1978
|
+
loginHint: c.body.loginHint
|
|
1979
|
+
});
|
|
1980
|
+
return c.json({
|
|
1981
|
+
url: url.toString(),
|
|
1982
|
+
redirect: !c.body.disableRedirect
|
|
1983
|
+
});
|
|
1984
|
+
});
|
|
1985
|
+
const signInEmail = createAuthEndpoint("/sign-in/email", {
|
|
1986
|
+
method: "POST",
|
|
1987
|
+
body: z$1.object({
|
|
1988
|
+
email: z$1.string().meta({ description: "Email of the user" }),
|
|
1989
|
+
password: z$1.string().meta({ description: "Password of the user" }),
|
|
1990
|
+
callbackURL: z$1.string().meta({ description: "Callback URL to use as a redirect for email verification" }).optional(),
|
|
1991
|
+
rememberMe: z$1.boolean().meta({ description: "If this is false, the session will not be remembered. Default is `true`." }).default(true).optional()
|
|
1992
|
+
}),
|
|
1993
|
+
metadata: { openapi: {
|
|
1994
|
+
description: "Sign in with email and password",
|
|
1995
|
+
responses: { "200": {
|
|
1996
|
+
description: "Success - Returns either session details or redirect URL",
|
|
1997
|
+
content: { "application/json": { schema: {
|
|
1998
|
+
type: "object",
|
|
1999
|
+
description: "Session response when idToken is provided",
|
|
2000
|
+
properties: {
|
|
2001
|
+
redirect: {
|
|
2002
|
+
type: "boolean",
|
|
2003
|
+
enum: [false]
|
|
2004
|
+
},
|
|
2005
|
+
token: {
|
|
2006
|
+
type: "string",
|
|
2007
|
+
description: "Session token"
|
|
2008
|
+
},
|
|
2009
|
+
url: {
|
|
2010
|
+
type: "null",
|
|
2011
|
+
nullable: true
|
|
2012
|
+
},
|
|
2013
|
+
user: {
|
|
2014
|
+
type: "object",
|
|
2015
|
+
properties: {
|
|
2016
|
+
id: { type: "string" },
|
|
2017
|
+
email: { type: "string" },
|
|
2018
|
+
name: {
|
|
2019
|
+
type: "string",
|
|
2020
|
+
nullable: true
|
|
2021
|
+
},
|
|
2022
|
+
image: {
|
|
2023
|
+
type: "string",
|
|
2024
|
+
nullable: true
|
|
2025
|
+
},
|
|
2026
|
+
emailVerified: { type: "boolean" },
|
|
2027
|
+
createdAt: {
|
|
2028
|
+
type: "string",
|
|
2029
|
+
format: "date-time"
|
|
2030
|
+
},
|
|
2031
|
+
updatedAt: {
|
|
2032
|
+
type: "string",
|
|
2033
|
+
format: "date-time"
|
|
2034
|
+
}
|
|
2035
|
+
},
|
|
2036
|
+
required: [
|
|
2037
|
+
"id",
|
|
2038
|
+
"email",
|
|
2039
|
+
"emailVerified",
|
|
2040
|
+
"createdAt",
|
|
2041
|
+
"updatedAt"
|
|
2042
|
+
]
|
|
2043
|
+
}
|
|
2044
|
+
},
|
|
2045
|
+
required: [
|
|
2046
|
+
"redirect",
|
|
2047
|
+
"token",
|
|
2048
|
+
"user"
|
|
2049
|
+
]
|
|
2050
|
+
} } }
|
|
2051
|
+
} }
|
|
2052
|
+
} }
|
|
2053
|
+
}, async (ctx) => {
|
|
2054
|
+
if (!ctx.context.options?.emailAndPassword?.enabled) {
|
|
2055
|
+
ctx.context.logger.error("Email and password is not enabled. Make sure to enable it in the options on you `auth.ts` file. Check `https://payvopayvo.com/docs/authentication/email-password` for more!");
|
|
2056
|
+
throw new APIError("BAD_REQUEST", { message: "Email and password is not enabled" });
|
|
2057
|
+
}
|
|
2058
|
+
const { email, password } = ctx.body;
|
|
2059
|
+
if (!z$1.string().email().safeParse(email).success) throw new APIError("BAD_REQUEST", { message: BASE_ERROR_CODES.INVALID_EMAIL });
|
|
2060
|
+
const user = await ctx.context.internalAdapter.findUserByEmail(email, { includeAccounts: true });
|
|
2061
|
+
if (!user) {
|
|
2062
|
+
await ctx.context.password.hash(password);
|
|
2063
|
+
ctx.context.logger.error("User not found", { email });
|
|
2064
|
+
throw new APIError("UNAUTHORIZED", { message: BASE_ERROR_CODES.INVALID_EMAIL_OR_PASSWORD });
|
|
2065
|
+
}
|
|
2066
|
+
const credentialAccount = user.accounts.find((a) => a.providerId === "credential");
|
|
2067
|
+
if (!credentialAccount) {
|
|
2068
|
+
ctx.context.logger.error("Credential account not found", { email });
|
|
2069
|
+
throw new APIError("UNAUTHORIZED", { message: BASE_ERROR_CODES.INVALID_EMAIL_OR_PASSWORD });
|
|
2070
|
+
}
|
|
2071
|
+
const currentPassword = credentialAccount?.password;
|
|
2072
|
+
if (!currentPassword) {
|
|
2073
|
+
ctx.context.logger.error("Password not found", { email });
|
|
2074
|
+
throw new APIError("UNAUTHORIZED", { message: BASE_ERROR_CODES.INVALID_EMAIL_OR_PASSWORD });
|
|
2075
|
+
}
|
|
2076
|
+
if (!await ctx.context.password.verify({
|
|
2077
|
+
hash: currentPassword,
|
|
2078
|
+
password
|
|
2079
|
+
})) {
|
|
2080
|
+
ctx.context.logger.error("Invalid password");
|
|
2081
|
+
throw new APIError("UNAUTHORIZED", { message: BASE_ERROR_CODES.INVALID_EMAIL_OR_PASSWORD });
|
|
2082
|
+
}
|
|
2083
|
+
if (ctx.context.options?.emailAndPassword?.requireEmailVerification && !user.user.emailVerified) {
|
|
2084
|
+
if (!ctx.context.options?.emailVerification?.sendVerificationEmail) throw new APIError("FORBIDDEN", { message: BASE_ERROR_CODES.EMAIL_NOT_VERIFIED });
|
|
2085
|
+
if (ctx.context.options?.emailVerification?.sendOnSignIn) {
|
|
2086
|
+
const token = await createEmailVerificationToken(ctx.context.secret, user.user.email, void 0, ctx.context.options.emailVerification?.expiresIn);
|
|
2087
|
+
const callbackURL = ctx.body.callbackURL ? encodeURIComponent(ctx.body.callbackURL) : encodeURIComponent("/");
|
|
2088
|
+
const url = `${ctx.context.baseURL}/verify-email?token=${token}&callbackURL=${callbackURL}`;
|
|
2089
|
+
await ctx.context.options.emailVerification.sendVerificationEmail({
|
|
2090
|
+
user: user.user,
|
|
2091
|
+
url,
|
|
2092
|
+
token
|
|
2093
|
+
}, ctx.request);
|
|
2094
|
+
}
|
|
2095
|
+
throw new APIError("FORBIDDEN", { message: BASE_ERROR_CODES.EMAIL_NOT_VERIFIED });
|
|
2096
|
+
}
|
|
2097
|
+
const session = await ctx.context.internalAdapter.createSession(user.user.id, ctx.body.rememberMe === false);
|
|
2098
|
+
if (!session) {
|
|
2099
|
+
ctx.context.logger.error("Failed to create session");
|
|
2100
|
+
throw new APIError("UNAUTHORIZED", { message: BASE_ERROR_CODES.FAILED_TO_CREATE_SESSION });
|
|
2101
|
+
}
|
|
2102
|
+
await setSessionCookie(ctx, {
|
|
2103
|
+
session,
|
|
2104
|
+
user: user.user
|
|
2105
|
+
}, ctx.body.rememberMe === false);
|
|
2106
|
+
return ctx.json({
|
|
2107
|
+
redirect: !!ctx.body.callbackURL,
|
|
2108
|
+
token: session.token,
|
|
2109
|
+
url: ctx.body.callbackURL,
|
|
2110
|
+
user: {
|
|
2111
|
+
id: user.user.id,
|
|
2112
|
+
email: user.user.email,
|
|
2113
|
+
name: user.user.name,
|
|
2114
|
+
image: user.user.image,
|
|
2115
|
+
emailVerified: user.user.emailVerified,
|
|
2116
|
+
createdAt: user.user.createdAt,
|
|
2117
|
+
updatedAt: user.user.updatedAt
|
|
2118
|
+
}
|
|
2119
|
+
});
|
|
2120
|
+
});
|
|
2121
|
+
|
|
2122
|
+
//#endregion
|
|
2123
|
+
//#region src/api/routes/sign-out.ts
|
|
2124
|
+
const signOut = createAuthEndpoint("/sign-out", {
|
|
2125
|
+
method: "POST",
|
|
2126
|
+
requireHeaders: true,
|
|
2127
|
+
metadata: { openapi: {
|
|
2128
|
+
description: "Sign out the current user",
|
|
2129
|
+
responses: { "200": {
|
|
2130
|
+
description: "Success",
|
|
2131
|
+
content: { "application/json": { schema: {
|
|
2132
|
+
type: "object",
|
|
2133
|
+
properties: { success: { type: "boolean" } }
|
|
2134
|
+
} } }
|
|
2135
|
+
} }
|
|
2136
|
+
} }
|
|
2137
|
+
}, async (ctx) => {
|
|
2138
|
+
const sessionCookieToken = await ctx.getSignedCookie(ctx.context.authCookies.sessionToken.name, ctx.context.secret);
|
|
2139
|
+
if (!sessionCookieToken) {
|
|
2140
|
+
deleteSessionCookie(ctx);
|
|
2141
|
+
throw new APIError("BAD_REQUEST", { message: BASE_ERROR_CODES.FAILED_TO_GET_SESSION });
|
|
2142
|
+
}
|
|
2143
|
+
await ctx.context.internalAdapter.deleteSession(sessionCookieToken);
|
|
2144
|
+
deleteSessionCookie(ctx);
|
|
2145
|
+
return ctx.json({ success: true });
|
|
2146
|
+
});
|
|
2147
|
+
|
|
2148
|
+
//#endregion
|
|
2149
|
+
//#region src/api/routes/sign-up.ts
|
|
2150
|
+
const signUpEmail = () => createAuthEndpoint("/sign-up/email", {
|
|
2151
|
+
method: "POST",
|
|
2152
|
+
body: z$1.record(z$1.string(), z$1.any()),
|
|
2153
|
+
metadata: {
|
|
2154
|
+
$Infer: { body: {} },
|
|
2155
|
+
openapi: {
|
|
2156
|
+
description: "Sign up a user using email and password",
|
|
2157
|
+
requestBody: { content: { "application/json": { schema: {
|
|
2158
|
+
type: "object",
|
|
2159
|
+
properties: {
|
|
2160
|
+
name: {
|
|
2161
|
+
type: "string",
|
|
2162
|
+
description: "The name of the user"
|
|
2163
|
+
},
|
|
2164
|
+
email: {
|
|
2165
|
+
type: "string",
|
|
2166
|
+
description: "The email of the user"
|
|
2167
|
+
},
|
|
2168
|
+
password: {
|
|
2169
|
+
type: "string",
|
|
2170
|
+
description: "The password of the user"
|
|
2171
|
+
},
|
|
2172
|
+
image: {
|
|
2173
|
+
type: "string",
|
|
2174
|
+
description: "The profile image URL of the user"
|
|
2175
|
+
},
|
|
2176
|
+
callbackURL: {
|
|
2177
|
+
type: "string",
|
|
2178
|
+
description: "The URL to use for email verification callback"
|
|
2179
|
+
},
|
|
2180
|
+
rememberMe: {
|
|
2181
|
+
type: "boolean",
|
|
2182
|
+
description: "If this is false, the session will not be remembered. Default is `true`."
|
|
2183
|
+
}
|
|
2184
|
+
},
|
|
2185
|
+
required: [
|
|
2186
|
+
"name",
|
|
2187
|
+
"email",
|
|
2188
|
+
"password"
|
|
2189
|
+
]
|
|
2190
|
+
} } } },
|
|
2191
|
+
responses: {
|
|
2192
|
+
"200": {
|
|
2193
|
+
description: "Successfully created user",
|
|
2194
|
+
content: { "application/json": { schema: {
|
|
2195
|
+
type: "object",
|
|
2196
|
+
properties: {
|
|
2197
|
+
token: {
|
|
2198
|
+
type: "string",
|
|
2199
|
+
nullable: true,
|
|
2200
|
+
description: "Authentication token for the session"
|
|
2201
|
+
},
|
|
2202
|
+
user: {
|
|
2203
|
+
type: "object",
|
|
2204
|
+
properties: {
|
|
2205
|
+
id: {
|
|
2206
|
+
type: "string",
|
|
2207
|
+
description: "The unique identifier of the user"
|
|
2208
|
+
},
|
|
2209
|
+
email: {
|
|
2210
|
+
type: "string",
|
|
2211
|
+
format: "email",
|
|
2212
|
+
description: "The email address of the user"
|
|
2213
|
+
},
|
|
2214
|
+
name: {
|
|
2215
|
+
type: "string",
|
|
2216
|
+
description: "The name of the user"
|
|
2217
|
+
},
|
|
2218
|
+
image: {
|
|
2219
|
+
type: "string",
|
|
2220
|
+
format: "uri",
|
|
2221
|
+
nullable: true,
|
|
2222
|
+
description: "The profile image URL of the user"
|
|
2223
|
+
},
|
|
2224
|
+
emailVerified: {
|
|
2225
|
+
type: "boolean",
|
|
2226
|
+
description: "Whether the email has been verified"
|
|
2227
|
+
},
|
|
2228
|
+
createdAt: {
|
|
2229
|
+
type: "string",
|
|
2230
|
+
format: "date-time",
|
|
2231
|
+
description: "When the user was created"
|
|
2232
|
+
},
|
|
2233
|
+
updatedAt: {
|
|
2234
|
+
type: "string",
|
|
2235
|
+
format: "date-time",
|
|
2236
|
+
description: "When the user was last updated"
|
|
2237
|
+
}
|
|
2238
|
+
},
|
|
2239
|
+
required: [
|
|
2240
|
+
"id",
|
|
2241
|
+
"email",
|
|
2242
|
+
"name",
|
|
2243
|
+
"emailVerified",
|
|
2244
|
+
"createdAt",
|
|
2245
|
+
"updatedAt"
|
|
2246
|
+
]
|
|
2247
|
+
}
|
|
2248
|
+
},
|
|
2249
|
+
required: ["user"]
|
|
2250
|
+
} } }
|
|
2251
|
+
},
|
|
2252
|
+
"422": {
|
|
2253
|
+
description: "Unprocessable Entity. User already exists or failed to create user.",
|
|
2254
|
+
content: { "application/json": { schema: {
|
|
2255
|
+
type: "object",
|
|
2256
|
+
properties: { message: { type: "string" } }
|
|
2257
|
+
} } }
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
}, async (ctx) => {
|
|
2263
|
+
return runWithTransaction(ctx.context.adapter, async () => {
|
|
2264
|
+
if (!ctx.context.options.emailAndPassword?.enabled || ctx.context.options.emailAndPassword?.disableSignUp) throw new APIError("BAD_REQUEST", { message: "Email and password sign up is not enabled" });
|
|
2265
|
+
const body = ctx.body;
|
|
2266
|
+
const { name, email, password, image, callbackURL, rememberMe,...rest } = body;
|
|
2267
|
+
if (!z$1.email().safeParse(email).success) throw new APIError("BAD_REQUEST", { message: BASE_ERROR_CODES.INVALID_EMAIL });
|
|
2268
|
+
const minPasswordLength = ctx.context.password.config.minPasswordLength;
|
|
2269
|
+
if (password.length < minPasswordLength) {
|
|
2270
|
+
ctx.context.logger.error("Password is too short");
|
|
2271
|
+
throw new APIError("BAD_REQUEST", { message: BASE_ERROR_CODES.PASSWORD_TOO_SHORT });
|
|
2272
|
+
}
|
|
2273
|
+
const maxPasswordLength = ctx.context.password.config.maxPasswordLength;
|
|
2274
|
+
if (password.length > maxPasswordLength) {
|
|
2275
|
+
ctx.context.logger.error("Password is too long");
|
|
2276
|
+
throw new APIError("BAD_REQUEST", { message: BASE_ERROR_CODES.PASSWORD_TOO_LONG });
|
|
2277
|
+
}
|
|
2278
|
+
if ((await ctx.context.internalAdapter.findUserByEmail(email))?.user) {
|
|
2279
|
+
ctx.context.logger.info(`Sign-up attempt for existing email: ${email}`);
|
|
2280
|
+
throw new APIError("UNPROCESSABLE_ENTITY", { message: BASE_ERROR_CODES.USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL });
|
|
2281
|
+
}
|
|
2282
|
+
/**
|
|
2283
|
+
* Hash the password
|
|
2284
|
+
*
|
|
2285
|
+
* This is done prior to creating the user
|
|
2286
|
+
* to ensure that any plugin that
|
|
2287
|
+
* may break the hashing should break
|
|
2288
|
+
* before the user is created.
|
|
2289
|
+
*/
|
|
2290
|
+
const hash = await ctx.context.password.hash(password);
|
|
2291
|
+
let createdUser;
|
|
2292
|
+
try {
|
|
2293
|
+
const data = parseUserInput(ctx.context.options, rest, "create");
|
|
2294
|
+
createdUser = await ctx.context.internalAdapter.createUser({
|
|
2295
|
+
email: email.toLowerCase(),
|
|
2296
|
+
name,
|
|
2297
|
+
image,
|
|
2298
|
+
...data,
|
|
2299
|
+
emailVerified: false
|
|
2300
|
+
});
|
|
2301
|
+
if (!createdUser) throw new APIError("BAD_REQUEST", { message: BASE_ERROR_CODES.FAILED_TO_CREATE_USER });
|
|
2302
|
+
} catch (e) {
|
|
2303
|
+
if (isDevelopment()) ctx.context.logger.error("Failed to create user", e);
|
|
2304
|
+
if (e instanceof APIError) throw e;
|
|
2305
|
+
ctx.context.logger?.error("Failed to create user", e);
|
|
2306
|
+
throw new APIError("UNPROCESSABLE_ENTITY", {
|
|
2307
|
+
message: BASE_ERROR_CODES.FAILED_TO_CREATE_USER,
|
|
2308
|
+
details: e
|
|
2309
|
+
});
|
|
2310
|
+
}
|
|
2311
|
+
if (!createdUser) throw new APIError("UNPROCESSABLE_ENTITY", { message: BASE_ERROR_CODES.FAILED_TO_CREATE_USER });
|
|
2312
|
+
await ctx.context.internalAdapter.linkAccount({
|
|
2313
|
+
userId: createdUser.id,
|
|
2314
|
+
providerId: "credential",
|
|
2315
|
+
accountId: createdUser.id,
|
|
2316
|
+
password: hash
|
|
2317
|
+
});
|
|
2318
|
+
if (ctx.context.options.emailVerification?.sendOnSignUp || ctx.context.options.emailAndPassword.requireEmailVerification) {
|
|
2319
|
+
const token = await createEmailVerificationToken(ctx.context.secret, createdUser.email, void 0, ctx.context.options.emailVerification?.expiresIn);
|
|
2320
|
+
const callbackURL$1 = body.callbackURL ? encodeURIComponent(body.callbackURL) : encodeURIComponent("/");
|
|
2321
|
+
const url = `${ctx.context.baseURL}/verify-email?token=${token}&callbackURL=${callbackURL$1}`;
|
|
2322
|
+
const args = ctx.request ? [{
|
|
2323
|
+
user: createdUser,
|
|
2324
|
+
url,
|
|
2325
|
+
token
|
|
2326
|
+
}, ctx.request] : [{
|
|
2327
|
+
user: createdUser,
|
|
2328
|
+
url,
|
|
2329
|
+
token
|
|
2330
|
+
}];
|
|
2331
|
+
await ctx.context.options.emailVerification?.sendVerificationEmail?.(...args);
|
|
2332
|
+
}
|
|
2333
|
+
if (ctx.context.options.emailAndPassword.autoSignIn === false || ctx.context.options.emailAndPassword.requireEmailVerification) return ctx.json({
|
|
2334
|
+
token: null,
|
|
2335
|
+
user: {
|
|
2336
|
+
id: createdUser.id,
|
|
2337
|
+
email: createdUser.email,
|
|
2338
|
+
name: createdUser.name,
|
|
2339
|
+
image: createdUser.image,
|
|
2340
|
+
emailVerified: createdUser.emailVerified,
|
|
2341
|
+
createdAt: createdUser.createdAt,
|
|
2342
|
+
updatedAt: createdUser.updatedAt
|
|
2343
|
+
}
|
|
2344
|
+
});
|
|
2345
|
+
const session = await ctx.context.internalAdapter.createSession(createdUser.id, rememberMe === false);
|
|
2346
|
+
if (!session) throw new APIError("BAD_REQUEST", { message: BASE_ERROR_CODES.FAILED_TO_CREATE_SESSION });
|
|
2347
|
+
await setSessionCookie(ctx, {
|
|
2348
|
+
session,
|
|
2349
|
+
user: createdUser
|
|
2350
|
+
}, rememberMe === false);
|
|
2351
|
+
return ctx.json({
|
|
2352
|
+
token: session.token,
|
|
2353
|
+
user: {
|
|
2354
|
+
id: createdUser.id,
|
|
2355
|
+
email: createdUser.email,
|
|
2356
|
+
name: createdUser.name,
|
|
2357
|
+
image: createdUser.image,
|
|
2358
|
+
emailVerified: createdUser.emailVerified,
|
|
2359
|
+
createdAt: createdUser.createdAt,
|
|
2360
|
+
updatedAt: createdUser.updatedAt
|
|
2361
|
+
}
|
|
2362
|
+
});
|
|
2363
|
+
});
|
|
2364
|
+
});
|
|
2365
|
+
|
|
2366
|
+
//#endregion
|
|
2367
|
+
//#region src/api/routes/update-user.ts
|
|
2368
|
+
const updateUser = () => createAuthEndpoint("/update-user", {
|
|
2369
|
+
method: "POST",
|
|
2370
|
+
body: z$1.record(z$1.string().meta({ description: "Field name must be a string" }), z$1.any()),
|
|
2371
|
+
use: [sessionMiddleware],
|
|
2372
|
+
metadata: {
|
|
2373
|
+
$Infer: { body: {} },
|
|
2374
|
+
openapi: {
|
|
2375
|
+
description: "Update the current user",
|
|
2376
|
+
requestBody: { content: { "application/json": { schema: {
|
|
2377
|
+
type: "object",
|
|
2378
|
+
properties: {
|
|
2379
|
+
name: {
|
|
2380
|
+
type: "string",
|
|
2381
|
+
description: "The name of the user"
|
|
2382
|
+
},
|
|
2383
|
+
image: {
|
|
2384
|
+
type: "string",
|
|
2385
|
+
description: "The image of the user"
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2388
|
+
} } } },
|
|
2389
|
+
responses: { "200": {
|
|
2390
|
+
description: "Success",
|
|
2391
|
+
content: { "application/json": { schema: {
|
|
2392
|
+
type: "object",
|
|
2393
|
+
properties: { status: {
|
|
2394
|
+
type: "boolean",
|
|
2395
|
+
description: "Indicates if the update was successful"
|
|
2396
|
+
} }
|
|
2397
|
+
} } }
|
|
2398
|
+
} }
|
|
2399
|
+
}
|
|
2400
|
+
}
|
|
2401
|
+
}, async (ctx) => {
|
|
2402
|
+
const body = ctx.body;
|
|
2403
|
+
if (body.email) throw new APIError("BAD_REQUEST", { message: BASE_ERROR_CODES.EMAIL_CAN_NOT_BE_UPDATED });
|
|
2404
|
+
const { name, image,...rest } = body;
|
|
2405
|
+
const session = ctx.context.session;
|
|
2406
|
+
const additionalFields = parseUserInput(ctx.context.options, rest, "update");
|
|
2407
|
+
if (image === void 0 && name === void 0 && Object.keys(additionalFields).length === 0) throw new APIError("BAD_REQUEST", { message: "No fields to update" });
|
|
2408
|
+
const user = await ctx.context.internalAdapter.updateUser(session.user.id, {
|
|
2409
|
+
name,
|
|
2410
|
+
image,
|
|
2411
|
+
...additionalFields
|
|
2412
|
+
});
|
|
2413
|
+
/**
|
|
2414
|
+
* Update the session cookie with the new user data
|
|
2415
|
+
*/
|
|
2416
|
+
await setSessionCookie(ctx, {
|
|
2417
|
+
session: session.session,
|
|
2418
|
+
user
|
|
2419
|
+
});
|
|
2420
|
+
return ctx.json({ status: true });
|
|
2421
|
+
});
|
|
2422
|
+
const changePassword = createAuthEndpoint("/change-password", {
|
|
2423
|
+
method: "POST",
|
|
2424
|
+
body: z$1.object({
|
|
2425
|
+
newPassword: z$1.string().meta({ description: "The new password to set" }),
|
|
2426
|
+
currentPassword: z$1.string().meta({ description: "The current password is required" }),
|
|
2427
|
+
revokeOtherSessions: z$1.boolean().meta({ description: "Must be a boolean value" }).optional()
|
|
2428
|
+
}),
|
|
2429
|
+
use: [sensitiveSessionMiddleware],
|
|
2430
|
+
metadata: { openapi: {
|
|
2431
|
+
description: "Change the password of the user",
|
|
2432
|
+
responses: { "200": {
|
|
2433
|
+
description: "Password successfully changed",
|
|
2434
|
+
content: { "application/json": { schema: {
|
|
2435
|
+
type: "object",
|
|
2436
|
+
properties: {
|
|
2437
|
+
token: {
|
|
2438
|
+
type: "string",
|
|
2439
|
+
nullable: true,
|
|
2440
|
+
description: "New session token if other sessions were revoked"
|
|
2441
|
+
},
|
|
2442
|
+
user: {
|
|
2443
|
+
type: "object",
|
|
2444
|
+
properties: {
|
|
2445
|
+
id: {
|
|
2446
|
+
type: "string",
|
|
2447
|
+
description: "The unique identifier of the user"
|
|
2448
|
+
},
|
|
2449
|
+
email: {
|
|
2450
|
+
type: "string",
|
|
2451
|
+
format: "email",
|
|
2452
|
+
description: "The email address of the user"
|
|
2453
|
+
},
|
|
2454
|
+
name: {
|
|
2455
|
+
type: "string",
|
|
2456
|
+
description: "The name of the user"
|
|
2457
|
+
},
|
|
2458
|
+
image: {
|
|
2459
|
+
type: "string",
|
|
2460
|
+
format: "uri",
|
|
2461
|
+
nullable: true,
|
|
2462
|
+
description: "The profile image URL of the user"
|
|
2463
|
+
},
|
|
2464
|
+
emailVerified: {
|
|
2465
|
+
type: "boolean",
|
|
2466
|
+
description: "Whether the email has been verified"
|
|
2467
|
+
},
|
|
2468
|
+
createdAt: {
|
|
2469
|
+
type: "string",
|
|
2470
|
+
format: "date-time",
|
|
2471
|
+
description: "When the user was created"
|
|
2472
|
+
},
|
|
2473
|
+
updatedAt: {
|
|
2474
|
+
type: "string",
|
|
2475
|
+
format: "date-time",
|
|
2476
|
+
description: "When the user was last updated"
|
|
2477
|
+
}
|
|
2478
|
+
},
|
|
2479
|
+
required: [
|
|
2480
|
+
"id",
|
|
2481
|
+
"email",
|
|
2482
|
+
"name",
|
|
2483
|
+
"emailVerified",
|
|
2484
|
+
"createdAt",
|
|
2485
|
+
"updatedAt"
|
|
2486
|
+
]
|
|
2487
|
+
}
|
|
2488
|
+
},
|
|
2489
|
+
required: ["user"]
|
|
2490
|
+
} } }
|
|
2491
|
+
} }
|
|
2492
|
+
} }
|
|
2493
|
+
}, async (ctx) => {
|
|
2494
|
+
const { newPassword, currentPassword, revokeOtherSessions: revokeOtherSessions$1 } = ctx.body;
|
|
2495
|
+
const session = ctx.context.session;
|
|
2496
|
+
const minPasswordLength = ctx.context.password.config.minPasswordLength;
|
|
2497
|
+
if (newPassword.length < minPasswordLength) {
|
|
2498
|
+
ctx.context.logger.error("Password is too short");
|
|
2499
|
+
throw new APIError("BAD_REQUEST", { message: BASE_ERROR_CODES.PASSWORD_TOO_SHORT });
|
|
2500
|
+
}
|
|
2501
|
+
const maxPasswordLength = ctx.context.password.config.maxPasswordLength;
|
|
2502
|
+
if (newPassword.length > maxPasswordLength) {
|
|
2503
|
+
ctx.context.logger.error("Password is too long");
|
|
2504
|
+
throw new APIError("BAD_REQUEST", { message: BASE_ERROR_CODES.PASSWORD_TOO_LONG });
|
|
2505
|
+
}
|
|
2506
|
+
const account = (await ctx.context.internalAdapter.findAccounts(session.user.id)).find((account$1) => account$1.providerId === "credential" && account$1.password);
|
|
2507
|
+
if (!account || !account.password) throw new APIError("BAD_REQUEST", { message: BASE_ERROR_CODES.CREDENTIAL_ACCOUNT_NOT_FOUND });
|
|
2508
|
+
const passwordHash = await ctx.context.password.hash(newPassword);
|
|
2509
|
+
if (!await ctx.context.password.verify({
|
|
2510
|
+
hash: account.password,
|
|
2511
|
+
password: currentPassword
|
|
2512
|
+
})) throw new APIError("BAD_REQUEST", { message: BASE_ERROR_CODES.INVALID_PASSWORD });
|
|
2513
|
+
await ctx.context.internalAdapter.updateAccount(account.id, { password: passwordHash });
|
|
2514
|
+
let token = null;
|
|
2515
|
+
if (revokeOtherSessions$1) {
|
|
2516
|
+
await ctx.context.internalAdapter.deleteSessions(session.user.id);
|
|
2517
|
+
const newSession = await ctx.context.internalAdapter.createSession(session.user.id);
|
|
2518
|
+
if (!newSession) throw new APIError("INTERNAL_SERVER_ERROR", { message: BASE_ERROR_CODES.FAILED_TO_GET_SESSION });
|
|
2519
|
+
await setSessionCookie(ctx, {
|
|
2520
|
+
session: newSession,
|
|
2521
|
+
user: session.user
|
|
2522
|
+
});
|
|
2523
|
+
token = newSession.token;
|
|
2524
|
+
}
|
|
2525
|
+
return ctx.json({
|
|
2526
|
+
token,
|
|
2527
|
+
user: {
|
|
2528
|
+
id: session.user.id,
|
|
2529
|
+
email: session.user.email,
|
|
2530
|
+
name: session.user.name,
|
|
2531
|
+
image: session.user.image,
|
|
2532
|
+
emailVerified: session.user.emailVerified,
|
|
2533
|
+
createdAt: session.user.createdAt,
|
|
2534
|
+
updatedAt: session.user.updatedAt
|
|
2535
|
+
}
|
|
2536
|
+
});
|
|
2537
|
+
});
|
|
2538
|
+
const setPassword = createAuthEndpoint("/set-password", {
|
|
2539
|
+
method: "POST",
|
|
2540
|
+
body: z$1.object({ newPassword: z$1.string().meta({ description: "The new password to set is required" }) }),
|
|
2541
|
+
metadata: { SERVER_ONLY: true },
|
|
2542
|
+
use: [sensitiveSessionMiddleware]
|
|
2543
|
+
}, async (ctx) => {
|
|
2544
|
+
const { newPassword } = ctx.body;
|
|
2545
|
+
const session = ctx.context.session;
|
|
2546
|
+
const minPasswordLength = ctx.context.password.config.minPasswordLength;
|
|
2547
|
+
if (newPassword.length < minPasswordLength) {
|
|
2548
|
+
ctx.context.logger.error("Password is too short");
|
|
2549
|
+
throw new APIError("BAD_REQUEST", { message: BASE_ERROR_CODES.PASSWORD_TOO_SHORT });
|
|
2550
|
+
}
|
|
2551
|
+
const maxPasswordLength = ctx.context.password.config.maxPasswordLength;
|
|
2552
|
+
if (newPassword.length > maxPasswordLength) {
|
|
2553
|
+
ctx.context.logger.error("Password is too long");
|
|
2554
|
+
throw new APIError("BAD_REQUEST", { message: BASE_ERROR_CODES.PASSWORD_TOO_LONG });
|
|
2555
|
+
}
|
|
2556
|
+
const account = (await ctx.context.internalAdapter.findAccounts(session.user.id)).find((account$1) => account$1.providerId === "credential" && account$1.password);
|
|
2557
|
+
const passwordHash = await ctx.context.password.hash(newPassword);
|
|
2558
|
+
if (!account) {
|
|
2559
|
+
await ctx.context.internalAdapter.linkAccount({
|
|
2560
|
+
userId: session.user.id,
|
|
2561
|
+
providerId: "credential",
|
|
2562
|
+
accountId: session.user.id,
|
|
2563
|
+
password: passwordHash
|
|
2564
|
+
});
|
|
2565
|
+
return ctx.json({ status: true });
|
|
2566
|
+
}
|
|
2567
|
+
throw new APIError("BAD_REQUEST", { message: "user already has a password" });
|
|
2568
|
+
});
|
|
2569
|
+
const deleteUser = createAuthEndpoint("/delete-user", {
|
|
2570
|
+
method: "POST",
|
|
2571
|
+
use: [sensitiveSessionMiddleware],
|
|
2572
|
+
body: z$1.object({
|
|
2573
|
+
callbackURL: z$1.string().meta({ description: "The callback URL to redirect to after the user is deleted" }).optional(),
|
|
2574
|
+
password: z$1.string().meta({ description: "The password of the user is required to delete the user" }).optional(),
|
|
2575
|
+
token: z$1.string().meta({ description: "The token to delete the user is required" }).optional()
|
|
2576
|
+
}),
|
|
2577
|
+
metadata: { openapi: {
|
|
2578
|
+
description: "Delete the user",
|
|
2579
|
+
responses: { "200": {
|
|
2580
|
+
description: "User deletion processed successfully",
|
|
2581
|
+
content: { "application/json": { schema: {
|
|
2582
|
+
type: "object",
|
|
2583
|
+
properties: {
|
|
2584
|
+
success: {
|
|
2585
|
+
type: "boolean",
|
|
2586
|
+
description: "Indicates if the operation was successful"
|
|
2587
|
+
},
|
|
2588
|
+
message: {
|
|
2589
|
+
type: "string",
|
|
2590
|
+
enum: ["User deleted", "Verification email sent"],
|
|
2591
|
+
description: "Status message of the deletion process"
|
|
2592
|
+
}
|
|
2593
|
+
},
|
|
2594
|
+
required: ["success", "message"]
|
|
2595
|
+
} } }
|
|
2596
|
+
} }
|
|
2597
|
+
} }
|
|
2598
|
+
}, async (ctx) => {
|
|
2599
|
+
if (!ctx.context.options.user?.deleteUser?.enabled) {
|
|
2600
|
+
ctx.context.logger.error("Delete user is disabled. Enable it in the options");
|
|
2601
|
+
throw new APIError("NOT_FOUND");
|
|
2602
|
+
}
|
|
2603
|
+
const session = ctx.context.session;
|
|
2604
|
+
if (ctx.body.password) {
|
|
2605
|
+
const account = (await ctx.context.internalAdapter.findAccounts(session.user.id)).find((account$1) => account$1.providerId === "credential" && account$1.password);
|
|
2606
|
+
if (!account || !account.password) throw new APIError("BAD_REQUEST", { message: BASE_ERROR_CODES.CREDENTIAL_ACCOUNT_NOT_FOUND });
|
|
2607
|
+
if (!await ctx.context.password.verify({
|
|
2608
|
+
hash: account.password,
|
|
2609
|
+
password: ctx.body.password
|
|
2610
|
+
})) throw new APIError("BAD_REQUEST", { message: BASE_ERROR_CODES.INVALID_PASSWORD });
|
|
2611
|
+
}
|
|
2612
|
+
if (ctx.body.token) {
|
|
2613
|
+
await deleteUserCallback({
|
|
2614
|
+
...ctx,
|
|
2615
|
+
query: { token: ctx.body.token }
|
|
2616
|
+
});
|
|
2617
|
+
return ctx.json({
|
|
2618
|
+
success: true,
|
|
2619
|
+
message: "User deleted"
|
|
2620
|
+
});
|
|
2621
|
+
}
|
|
2622
|
+
if (ctx.context.options.user.deleteUser?.sendDeleteAccountVerification) {
|
|
2623
|
+
const token = generateRandomString(32, "0-9", "a-z");
|
|
2624
|
+
await ctx.context.internalAdapter.createVerificationValue({
|
|
2625
|
+
value: session.user.id,
|
|
2626
|
+
identifier: `delete-account-${token}`,
|
|
2627
|
+
expiresAt: new Date(Date.now() + (ctx.context.options.user.deleteUser?.deleteTokenExpiresIn || 3600 * 24) * 1e3)
|
|
2628
|
+
});
|
|
2629
|
+
const url = `${ctx.context.baseURL}/delete-user/callback?token=${token}&callbackURL=${ctx.body.callbackURL || "/"}`;
|
|
2630
|
+
await ctx.context.options.user.deleteUser.sendDeleteAccountVerification({
|
|
2631
|
+
user: session.user,
|
|
2632
|
+
url,
|
|
2633
|
+
token
|
|
2634
|
+
}, ctx.request);
|
|
2635
|
+
return ctx.json({
|
|
2636
|
+
success: true,
|
|
2637
|
+
message: "Verification email sent"
|
|
2638
|
+
});
|
|
2639
|
+
}
|
|
2640
|
+
if (!ctx.body.password && ctx.context.sessionConfig.freshAge !== 0) {
|
|
2641
|
+
const currentAge = new Date(session.session.createdAt).getTime();
|
|
2642
|
+
const freshAge = ctx.context.sessionConfig.freshAge * 1e3;
|
|
2643
|
+
if (Date.now() - currentAge > freshAge * 1e3) throw new APIError("BAD_REQUEST", { message: BASE_ERROR_CODES.SESSION_EXPIRED });
|
|
2644
|
+
}
|
|
2645
|
+
const beforeDelete = ctx.context.options.user.deleteUser?.beforeDelete;
|
|
2646
|
+
if (beforeDelete) await beforeDelete(session.user, ctx.request);
|
|
2647
|
+
await ctx.context.internalAdapter.deleteUser(session.user.id);
|
|
2648
|
+
await ctx.context.internalAdapter.deleteSessions(session.user.id);
|
|
2649
|
+
deleteSessionCookie(ctx);
|
|
2650
|
+
const afterDelete = ctx.context.options.user.deleteUser?.afterDelete;
|
|
2651
|
+
if (afterDelete) await afterDelete(session.user, ctx.request);
|
|
2652
|
+
return ctx.json({
|
|
2653
|
+
success: true,
|
|
2654
|
+
message: "User deleted"
|
|
2655
|
+
});
|
|
2656
|
+
});
|
|
2657
|
+
const deleteUserCallback = createAuthEndpoint("/delete-user/callback", {
|
|
2658
|
+
method: "GET",
|
|
2659
|
+
query: z$1.object({
|
|
2660
|
+
token: z$1.string().meta({ description: "The token to verify the deletion request" }),
|
|
2661
|
+
callbackURL: z$1.string().meta({ description: "The URL to redirect to after deletion" }).optional()
|
|
2662
|
+
}),
|
|
2663
|
+
use: [originCheck((ctx) => ctx.query.callbackURL)],
|
|
2664
|
+
metadata: { openapi: {
|
|
2665
|
+
description: "Callback to complete user deletion with verification token",
|
|
2666
|
+
responses: { "200": {
|
|
2667
|
+
description: "User successfully deleted",
|
|
2668
|
+
content: { "application/json": { schema: {
|
|
2669
|
+
type: "object",
|
|
2670
|
+
properties: {
|
|
2671
|
+
success: {
|
|
2672
|
+
type: "boolean",
|
|
2673
|
+
description: "Indicates if the deletion was successful"
|
|
2674
|
+
},
|
|
2675
|
+
message: {
|
|
2676
|
+
type: "string",
|
|
2677
|
+
enum: ["User deleted"],
|
|
2678
|
+
description: "Confirmation message"
|
|
2679
|
+
}
|
|
2680
|
+
},
|
|
2681
|
+
required: ["success", "message"]
|
|
2682
|
+
} } }
|
|
2683
|
+
} }
|
|
2684
|
+
} }
|
|
2685
|
+
}, async (ctx) => {
|
|
2686
|
+
if (!ctx.context.options.user?.deleteUser?.enabled) {
|
|
2687
|
+
ctx.context.logger.error("Delete user is disabled. Enable it in the options");
|
|
2688
|
+
throw new APIError("NOT_FOUND");
|
|
2689
|
+
}
|
|
2690
|
+
const session = await getSessionFromCtx(ctx);
|
|
2691
|
+
if (!session) throw new APIError("NOT_FOUND", { message: BASE_ERROR_CODES.FAILED_TO_GET_USER_INFO });
|
|
2692
|
+
const token = await ctx.context.internalAdapter.findVerificationValue(`delete-account-${ctx.query.token}`);
|
|
2693
|
+
if (!token || token.expiresAt < /* @__PURE__ */ new Date()) throw new APIError("NOT_FOUND", { message: BASE_ERROR_CODES.INVALID_TOKEN });
|
|
2694
|
+
if (token.value !== session.user.id) throw new APIError("NOT_FOUND", { message: BASE_ERROR_CODES.INVALID_TOKEN });
|
|
2695
|
+
const beforeDelete = ctx.context.options.user.deleteUser?.beforeDelete;
|
|
2696
|
+
if (beforeDelete) await beforeDelete(session.user, ctx.request);
|
|
2697
|
+
await ctx.context.internalAdapter.deleteUser(session.user.id);
|
|
2698
|
+
await ctx.context.internalAdapter.deleteSessions(session.user.id);
|
|
2699
|
+
await ctx.context.internalAdapter.deleteAccounts(session.user.id);
|
|
2700
|
+
await ctx.context.internalAdapter.deleteVerificationValue(token.id);
|
|
2701
|
+
deleteSessionCookie(ctx);
|
|
2702
|
+
const afterDelete = ctx.context.options.user.deleteUser?.afterDelete;
|
|
2703
|
+
if (afterDelete) await afterDelete(session.user, ctx.request);
|
|
2704
|
+
if (ctx.query.callbackURL) throw ctx.redirect(ctx.query.callbackURL || "/");
|
|
2705
|
+
return ctx.json({
|
|
2706
|
+
success: true,
|
|
2707
|
+
message: "User deleted"
|
|
2708
|
+
});
|
|
2709
|
+
});
|
|
2710
|
+
const changeEmail = createAuthEndpoint("/change-email", {
|
|
2711
|
+
method: "POST",
|
|
2712
|
+
body: z$1.object({
|
|
2713
|
+
newEmail: z$1.email().meta({ description: "The new email address to set must be a valid email address" }),
|
|
2714
|
+
callbackURL: z$1.string().meta({ description: "The URL to redirect to after email verification" }).optional()
|
|
2715
|
+
}),
|
|
2716
|
+
use: [sensitiveSessionMiddleware],
|
|
2717
|
+
metadata: { openapi: { responses: {
|
|
2718
|
+
"200": {
|
|
2719
|
+
description: "Email change request processed successfully",
|
|
2720
|
+
content: { "application/json": { schema: {
|
|
2721
|
+
type: "object",
|
|
2722
|
+
properties: {
|
|
2723
|
+
status: {
|
|
2724
|
+
type: "boolean",
|
|
2725
|
+
description: "Indicates if the request was successful"
|
|
2726
|
+
},
|
|
2727
|
+
message: {
|
|
2728
|
+
type: "string",
|
|
2729
|
+
enum: ["Email updated", "Verification email sent"],
|
|
2730
|
+
description: "Status message of the email change process",
|
|
2731
|
+
nullable: true
|
|
2732
|
+
}
|
|
2733
|
+
},
|
|
2734
|
+
required: ["status"]
|
|
2735
|
+
} } }
|
|
2736
|
+
},
|
|
2737
|
+
"422": {
|
|
2738
|
+
description: "Unprocessable Entity. Email already exists",
|
|
2739
|
+
content: { "application/json": { schema: {
|
|
2740
|
+
type: "object",
|
|
2741
|
+
properties: { message: { type: "string" } }
|
|
2742
|
+
} } }
|
|
2743
|
+
}
|
|
2744
|
+
} } }
|
|
2745
|
+
}, async (ctx) => {
|
|
2746
|
+
if (!ctx.context.options.user?.changeEmail?.enabled) {
|
|
2747
|
+
ctx.context.logger.error("Change email is disabled.");
|
|
2748
|
+
throw new APIError("BAD_REQUEST", { message: "Change email is disabled" });
|
|
2749
|
+
}
|
|
2750
|
+
const newEmail = ctx.body.newEmail.toLowerCase();
|
|
2751
|
+
if (newEmail === ctx.context.session.user.email) {
|
|
2752
|
+
ctx.context.logger.error("Email is the same");
|
|
2753
|
+
throw new APIError("BAD_REQUEST", { message: "Email is the same" });
|
|
2754
|
+
}
|
|
2755
|
+
if (await ctx.context.internalAdapter.findUserByEmail(newEmail)) {
|
|
2756
|
+
ctx.context.logger.error("Email already exists");
|
|
2757
|
+
throw new APIError("UNPROCESSABLE_ENTITY", { message: BASE_ERROR_CODES.USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL });
|
|
2758
|
+
}
|
|
2759
|
+
/**
|
|
2760
|
+
* If the email is not verified, we can update the email
|
|
2761
|
+
*/
|
|
2762
|
+
if (ctx.context.session.user.emailVerified !== true) {
|
|
2763
|
+
await ctx.context.internalAdapter.updateUserByEmail(ctx.context.session.user.email, { email: newEmail });
|
|
2764
|
+
await setSessionCookie(ctx, {
|
|
2765
|
+
session: ctx.context.session.session,
|
|
2766
|
+
user: {
|
|
2767
|
+
...ctx.context.session.user,
|
|
2768
|
+
email: newEmail
|
|
2769
|
+
}
|
|
2770
|
+
});
|
|
2771
|
+
if (ctx.context.options.emailVerification?.sendVerificationEmail) {
|
|
2772
|
+
const token$1 = await createEmailVerificationToken(ctx.context.secret, newEmail, void 0, ctx.context.options.emailVerification?.expiresIn);
|
|
2773
|
+
const url$1 = `${ctx.context.baseURL}/verify-email?token=${token$1}&callbackURL=${ctx.body.callbackURL || "/"}`;
|
|
2774
|
+
await ctx.context.options.emailVerification.sendVerificationEmail({
|
|
2775
|
+
user: {
|
|
2776
|
+
...ctx.context.session.user,
|
|
2777
|
+
email: newEmail
|
|
2778
|
+
},
|
|
2779
|
+
url: url$1,
|
|
2780
|
+
token: token$1
|
|
2781
|
+
}, ctx.request);
|
|
2782
|
+
}
|
|
2783
|
+
return ctx.json({ status: true });
|
|
2784
|
+
}
|
|
2785
|
+
/**
|
|
2786
|
+
* If the email is verified, we need to send a verification email
|
|
2787
|
+
*/
|
|
2788
|
+
if (!ctx.context.options.user.changeEmail.sendChangeEmailVerification) {
|
|
2789
|
+
ctx.context.logger.error("Verification email isn't enabled.");
|
|
2790
|
+
throw new APIError("BAD_REQUEST", { message: "Verification email isn't enabled" });
|
|
2791
|
+
}
|
|
2792
|
+
const token = await createEmailVerificationToken(ctx.context.secret, ctx.context.session.user.email, newEmail, ctx.context.options.emailVerification?.expiresIn);
|
|
2793
|
+
const url = `${ctx.context.baseURL}/verify-email?token=${token}&callbackURL=${ctx.body.callbackURL || "/"}`;
|
|
2794
|
+
await ctx.context.options.user.changeEmail.sendChangeEmailVerification({
|
|
2795
|
+
user: ctx.context.session.user,
|
|
2796
|
+
newEmail,
|
|
2797
|
+
url,
|
|
2798
|
+
token
|
|
2799
|
+
}, ctx.request);
|
|
2800
|
+
return ctx.json({ status: true });
|
|
2801
|
+
});
|
|
2802
|
+
|
|
2803
|
+
//#endregion
|
|
2804
|
+
//#region src/api/to-auth-endpoints.ts
|
|
2805
|
+
const defuReplaceArrays = createDefu((obj, key, value) => {
|
|
2806
|
+
if (Array.isArray(obj[key]) && Array.isArray(value)) {
|
|
2807
|
+
obj[key] = value;
|
|
2808
|
+
return true;
|
|
2809
|
+
}
|
|
2810
|
+
});
|
|
2811
|
+
function toAuthEndpoints(endpoints, ctx) {
|
|
2812
|
+
const api = {};
|
|
2813
|
+
for (const [key, endpoint] of Object.entries(endpoints)) {
|
|
2814
|
+
api[key] = async (context) => {
|
|
2815
|
+
const run = async () => {
|
|
2816
|
+
const authContext = await ctx;
|
|
2817
|
+
let internalContext = {
|
|
2818
|
+
...context,
|
|
2819
|
+
context: {
|
|
2820
|
+
...authContext,
|
|
2821
|
+
returned: void 0,
|
|
2822
|
+
responseHeaders: void 0,
|
|
2823
|
+
session: null
|
|
2824
|
+
},
|
|
2825
|
+
path: endpoint.path,
|
|
2826
|
+
headers: context?.headers ? new Headers(context?.headers) : void 0
|
|
2827
|
+
};
|
|
2828
|
+
return runWithEndpointContext(internalContext, async () => {
|
|
2829
|
+
const { beforeHooks, afterHooks } = getHooks(authContext);
|
|
2830
|
+
const before = await runBeforeHooks(internalContext, beforeHooks);
|
|
2831
|
+
/**
|
|
2832
|
+
* If `before.context` is returned, it should
|
|
2833
|
+
* get merged with the original context
|
|
2834
|
+
*/
|
|
2835
|
+
if ("context" in before && before.context && typeof before.context === "object") {
|
|
2836
|
+
const { headers,...rest } = before.context;
|
|
2837
|
+
/**
|
|
2838
|
+
* Headers should be merged differently
|
|
2839
|
+
* so the hook doesn't override the whole
|
|
2840
|
+
* header
|
|
2841
|
+
*/
|
|
2842
|
+
if (headers) headers.forEach((value, key$1) => {
|
|
2843
|
+
internalContext.headers.set(key$1, value);
|
|
2844
|
+
});
|
|
2845
|
+
internalContext = defuReplaceArrays(rest, internalContext);
|
|
2846
|
+
} else if (before) return context?.asResponse ? toResponse(before, { headers: context?.headers }) : context?.returnHeaders ? {
|
|
2847
|
+
headers: context?.headers,
|
|
2848
|
+
response: before
|
|
2849
|
+
} : before;
|
|
2850
|
+
internalContext.asResponse = false;
|
|
2851
|
+
internalContext.returnHeaders = true;
|
|
2852
|
+
const result = await runWithEndpointContext(internalContext, () => endpoint(internalContext)).catch((e) => {
|
|
2853
|
+
if (e instanceof APIError)
|
|
2854
|
+
/**
|
|
2855
|
+
* API Errors from response are caught
|
|
2856
|
+
* and returned to hooks
|
|
2857
|
+
*/
|
|
2858
|
+
return {
|
|
2859
|
+
response: e,
|
|
2860
|
+
headers: e.headers ? new Headers(e.headers) : null
|
|
2861
|
+
};
|
|
2862
|
+
throw e;
|
|
2863
|
+
});
|
|
2864
|
+
if (result && result instanceof Response) return result;
|
|
2865
|
+
internalContext.context.returned = result.response;
|
|
2866
|
+
internalContext.context.responseHeaders = result.headers;
|
|
2867
|
+
const after = await runAfterHooks(internalContext, afterHooks);
|
|
2868
|
+
if (after.response) result.response = after.response;
|
|
2869
|
+
if (result.response instanceof APIError && shouldPublishLog(authContext.logger.level, "debug")) result.response.stack = result.response.errorStack;
|
|
2870
|
+
if (result.response instanceof APIError && !context?.asResponse) throw result.response;
|
|
2871
|
+
return context?.asResponse ? toResponse(result.response, { headers: result.headers }) : context?.returnHeaders ? {
|
|
2872
|
+
headers: result.headers,
|
|
2873
|
+
response: result.response
|
|
2874
|
+
} : result.response;
|
|
2875
|
+
});
|
|
2876
|
+
};
|
|
2877
|
+
if (await hasRequestState()) return run();
|
|
2878
|
+
else return runWithRequestState(/* @__PURE__ */ new WeakMap(), run);
|
|
2879
|
+
};
|
|
2880
|
+
api[key].path = endpoint.path;
|
|
2881
|
+
api[key].options = endpoint.options;
|
|
2882
|
+
}
|
|
2883
|
+
return api;
|
|
2884
|
+
}
|
|
2885
|
+
async function runBeforeHooks(context, hooks) {
|
|
2886
|
+
let modifiedContext = {};
|
|
2887
|
+
for (const hook of hooks) if (hook.matcher(context)) {
|
|
2888
|
+
const result = await hook.handler({
|
|
2889
|
+
...context,
|
|
2890
|
+
returnHeaders: false
|
|
2891
|
+
}).catch((e) => {
|
|
2892
|
+
if (e instanceof APIError && shouldPublishLog(context.context.logger.level, "debug")) e.stack = e.errorStack;
|
|
2893
|
+
throw e;
|
|
2894
|
+
});
|
|
2895
|
+
if (result && typeof result === "object") {
|
|
2896
|
+
if ("context" in result && typeof result.context === "object") {
|
|
2897
|
+
const { headers,...rest } = result.context;
|
|
2898
|
+
if (headers instanceof Headers) if (modifiedContext.headers) headers.forEach((value, key) => {
|
|
2899
|
+
modifiedContext.headers?.set(key, value);
|
|
2900
|
+
});
|
|
2901
|
+
else modifiedContext.headers = headers;
|
|
2902
|
+
modifiedContext = defuReplaceArrays(rest, modifiedContext);
|
|
2903
|
+
continue;
|
|
2904
|
+
}
|
|
2905
|
+
return result;
|
|
2906
|
+
}
|
|
2907
|
+
}
|
|
2908
|
+
return { context: modifiedContext };
|
|
2909
|
+
}
|
|
2910
|
+
async function runAfterHooks(context, hooks) {
|
|
2911
|
+
for (const hook of hooks) if (hook.matcher(context)) {
|
|
2912
|
+
const result = await hook.handler(context).catch((e) => {
|
|
2913
|
+
if (e instanceof APIError) {
|
|
2914
|
+
if (shouldPublishLog(context.context.logger.level, "debug")) e.stack = e.errorStack;
|
|
2915
|
+
return {
|
|
2916
|
+
response: e,
|
|
2917
|
+
headers: e.headers ? new Headers(e.headers) : null
|
|
2918
|
+
};
|
|
2919
|
+
}
|
|
2920
|
+
throw e;
|
|
2921
|
+
});
|
|
2922
|
+
if (result.headers) result.headers.forEach((value, key) => {
|
|
2923
|
+
if (!context.context.responseHeaders) context.context.responseHeaders = new Headers({ [key]: value });
|
|
2924
|
+
else if (key.toLowerCase() === "set-cookie") context.context.responseHeaders.append(key, value);
|
|
2925
|
+
else context.context.responseHeaders.set(key, value);
|
|
2926
|
+
});
|
|
2927
|
+
if (result.response) context.context.returned = result.response;
|
|
2928
|
+
}
|
|
2929
|
+
return {
|
|
2930
|
+
response: context.context.returned,
|
|
2931
|
+
headers: context.context.responseHeaders
|
|
2932
|
+
};
|
|
2933
|
+
}
|
|
2934
|
+
function getHooks(authContext) {
|
|
2935
|
+
const plugins = authContext.options.plugins || [];
|
|
2936
|
+
const beforeHooks = [];
|
|
2937
|
+
const afterHooks = [];
|
|
2938
|
+
if (authContext.options.hooks?.before) beforeHooks.push({
|
|
2939
|
+
matcher: () => true,
|
|
2940
|
+
handler: authContext.options.hooks.before
|
|
2941
|
+
});
|
|
2942
|
+
if (authContext.options.hooks?.after) afterHooks.push({
|
|
2943
|
+
matcher: () => true,
|
|
2944
|
+
handler: authContext.options.hooks.after
|
|
2945
|
+
});
|
|
2946
|
+
const pluginBeforeHooks = plugins.map((plugin) => {
|
|
2947
|
+
if (plugin.hooks?.before) return plugin.hooks.before;
|
|
2948
|
+
}).filter((plugin) => plugin !== void 0).flat();
|
|
2949
|
+
const pluginAfterHooks = plugins.map((plugin) => {
|
|
2950
|
+
if (plugin.hooks?.after) return plugin.hooks.after;
|
|
2951
|
+
}).filter((plugin) => plugin !== void 0).flat();
|
|
2952
|
+
/**
|
|
2953
|
+
* Add plugin added hooks at last
|
|
2954
|
+
*/
|
|
2955
|
+
if (pluginBeforeHooks.length) beforeHooks.push(...pluginBeforeHooks);
|
|
2956
|
+
if (pluginAfterHooks.length) afterHooks.push(...pluginAfterHooks);
|
|
2957
|
+
return {
|
|
2958
|
+
beforeHooks,
|
|
2959
|
+
afterHooks
|
|
2960
|
+
};
|
|
2961
|
+
}
|
|
2962
|
+
|
|
2963
|
+
//#endregion
|
|
2964
|
+
//#region src/api/index.ts
|
|
2965
|
+
function checkEndpointConflicts(options, logger$1) {
|
|
2966
|
+
const endpointRegistry = /* @__PURE__ */ new Map();
|
|
2967
|
+
options.plugins?.forEach((plugin) => {
|
|
2968
|
+
if (plugin.endpoints) {
|
|
2969
|
+
for (const [key, endpoint] of Object.entries(plugin.endpoints)) if (endpoint && "path" in endpoint) {
|
|
2970
|
+
const path = endpoint.path;
|
|
2971
|
+
let methods = [];
|
|
2972
|
+
if (endpoint.options && "method" in endpoint.options) {
|
|
2973
|
+
if (Array.isArray(endpoint.options.method)) methods = endpoint.options.method;
|
|
2974
|
+
else if (typeof endpoint.options.method === "string") methods = [endpoint.options.method];
|
|
2975
|
+
}
|
|
2976
|
+
if (methods.length === 0) methods = ["*"];
|
|
2977
|
+
if (!endpointRegistry.has(path)) endpointRegistry.set(path, []);
|
|
2978
|
+
endpointRegistry.get(path).push({
|
|
2979
|
+
pluginId: plugin.id,
|
|
2980
|
+
endpointKey: key,
|
|
2981
|
+
methods
|
|
2982
|
+
});
|
|
2983
|
+
}
|
|
2984
|
+
}
|
|
2985
|
+
});
|
|
2986
|
+
const conflicts = [];
|
|
2987
|
+
for (const [path, entries] of endpointRegistry.entries()) if (entries.length > 1) {
|
|
2988
|
+
const methodMap = /* @__PURE__ */ new Map();
|
|
2989
|
+
let hasConflict = false;
|
|
2990
|
+
for (const entry of entries) for (const method of entry.methods) {
|
|
2991
|
+
if (!methodMap.has(method)) methodMap.set(method, []);
|
|
2992
|
+
methodMap.get(method).push(entry.pluginId);
|
|
2993
|
+
if (methodMap.get(method).length > 1) hasConflict = true;
|
|
2994
|
+
if (method === "*" && entries.length > 1) hasConflict = true;
|
|
2995
|
+
else if (method !== "*" && methodMap.has("*")) hasConflict = true;
|
|
2996
|
+
}
|
|
2997
|
+
if (hasConflict) {
|
|
2998
|
+
const uniquePlugins = [...new Set(entries.map((e) => e.pluginId))];
|
|
2999
|
+
const conflictingMethods = [];
|
|
3000
|
+
for (const [method, plugins] of methodMap.entries()) if (plugins.length > 1 || method === "*" && entries.length > 1 || method !== "*" && methodMap.has("*")) conflictingMethods.push(method);
|
|
3001
|
+
conflicts.push({
|
|
3002
|
+
path,
|
|
3003
|
+
plugins: uniquePlugins,
|
|
3004
|
+
conflictingMethods
|
|
3005
|
+
});
|
|
3006
|
+
}
|
|
3007
|
+
}
|
|
3008
|
+
if (conflicts.length > 0) {
|
|
3009
|
+
const conflictMessages = conflicts.map((conflict) => ` - "${conflict.path}" [${conflict.conflictingMethods.join(", ")}] used by plugins: ${conflict.plugins.join(", ")}`).join("\n");
|
|
3010
|
+
logger$1.error(`Endpoint path conflicts detected! Multiple plugins are trying to use the same endpoint paths with conflicting HTTP methods:
|
|
3011
|
+
${conflictMessages}
|
|
3012
|
+
|
|
3013
|
+
To resolve this, you can:
|
|
3014
|
+
1. Use only one of the conflicting plugins
|
|
3015
|
+
2. Configure the plugins to use different paths (if supported)
|
|
3016
|
+
3. Ensure plugins use different HTTP methods for the same path
|
|
3017
|
+
`);
|
|
3018
|
+
}
|
|
3019
|
+
}
|
|
3020
|
+
function getEndpoints(ctx, options) {
|
|
3021
|
+
const pluginEndpoints = options.plugins?.reduce((acc, plugin) => {
|
|
3022
|
+
return {
|
|
3023
|
+
...acc,
|
|
3024
|
+
...plugin.endpoints
|
|
3025
|
+
};
|
|
3026
|
+
}, {}) ?? {};
|
|
3027
|
+
const middlewares = options.plugins?.map((plugin) => plugin.middlewares?.map((m) => {
|
|
3028
|
+
const middleware = (async (context) => {
|
|
3029
|
+
const authContext = await ctx;
|
|
3030
|
+
return m.middleware({
|
|
3031
|
+
...context,
|
|
3032
|
+
context: {
|
|
3033
|
+
...authContext,
|
|
3034
|
+
...context.context
|
|
3035
|
+
}
|
|
3036
|
+
});
|
|
3037
|
+
});
|
|
3038
|
+
middleware.options = m.middleware.options;
|
|
3039
|
+
return {
|
|
3040
|
+
path: m.path,
|
|
3041
|
+
middleware
|
|
3042
|
+
};
|
|
3043
|
+
})).filter((plugin) => plugin !== void 0).flat() || [];
|
|
3044
|
+
return {
|
|
3045
|
+
api: toAuthEndpoints({
|
|
3046
|
+
signInSocial,
|
|
3047
|
+
callbackOAuth,
|
|
3048
|
+
getSession: getSession(),
|
|
3049
|
+
signOut,
|
|
3050
|
+
signUpEmail: signUpEmail(),
|
|
3051
|
+
signInEmail,
|
|
3052
|
+
resetPassword,
|
|
3053
|
+
verifyEmail,
|
|
3054
|
+
sendVerificationEmail,
|
|
3055
|
+
changeEmail,
|
|
3056
|
+
changePassword,
|
|
3057
|
+
setPassword,
|
|
3058
|
+
updateUser: updateUser(),
|
|
3059
|
+
deleteUser,
|
|
3060
|
+
requestPasswordReset,
|
|
3061
|
+
requestPasswordResetCallback,
|
|
3062
|
+
listSessions: listSessions(),
|
|
3063
|
+
revokeSession,
|
|
3064
|
+
revokeSessions,
|
|
3065
|
+
revokeOtherSessions,
|
|
3066
|
+
linkSocialAccount,
|
|
3067
|
+
listUserAccounts,
|
|
3068
|
+
deleteUserCallback,
|
|
3069
|
+
unlinkAccount,
|
|
3070
|
+
refreshToken,
|
|
3071
|
+
getAccessToken,
|
|
3072
|
+
accountInfo,
|
|
3073
|
+
...pluginEndpoints,
|
|
3074
|
+
ok,
|
|
3075
|
+
error
|
|
3076
|
+
}, ctx),
|
|
3077
|
+
middlewares
|
|
3078
|
+
};
|
|
3079
|
+
}
|
|
3080
|
+
const router = (ctx, options) => {
|
|
3081
|
+
const { api, middlewares } = getEndpoints(ctx, options);
|
|
3082
|
+
const basePath = new URL(ctx.baseURL).pathname;
|
|
3083
|
+
return createRouter(api, {
|
|
3084
|
+
routerContext: ctx,
|
|
3085
|
+
openapi: { disabled: true },
|
|
3086
|
+
basePath,
|
|
3087
|
+
routerMiddleware: [{
|
|
3088
|
+
path: "/**",
|
|
3089
|
+
middleware: originCheckMiddleware
|
|
3090
|
+
}, ...middlewares],
|
|
3091
|
+
async onRequest(req) {
|
|
3092
|
+
const disabledPaths = ctx.options.disabledPaths || [];
|
|
3093
|
+
const path = new URL(req.url).pathname.replace(basePath, "");
|
|
3094
|
+
if (disabledPaths.includes(path)) return new Response("Not Found", { status: 404 });
|
|
3095
|
+
for (const plugin of ctx.options.plugins || []) if (plugin.onRequest) {
|
|
3096
|
+
const response = await plugin.onRequest(req, ctx);
|
|
3097
|
+
if (response && "response" in response) return response.response;
|
|
3098
|
+
if (response && "request" in response) {
|
|
3099
|
+
const rateLimitResponse$1 = await onRequestRateLimit(response.request, ctx);
|
|
3100
|
+
if (rateLimitResponse$1) return rateLimitResponse$1;
|
|
3101
|
+
return response.request;
|
|
3102
|
+
}
|
|
3103
|
+
}
|
|
3104
|
+
return onRequestRateLimit(req, ctx);
|
|
3105
|
+
},
|
|
3106
|
+
async onResponse(res) {
|
|
3107
|
+
for (const plugin of ctx.options.plugins || []) if (plugin.onResponse) {
|
|
3108
|
+
const response = await plugin.onResponse(res, ctx);
|
|
3109
|
+
if (response) return response.response;
|
|
3110
|
+
}
|
|
3111
|
+
return res;
|
|
3112
|
+
},
|
|
3113
|
+
onError(e) {
|
|
3114
|
+
if (e instanceof APIError && e.status === "FOUND") return;
|
|
3115
|
+
if (options.onAPIError?.throw) throw e;
|
|
3116
|
+
if (options.onAPIError?.onError) {
|
|
3117
|
+
options.onAPIError.onError(e, ctx);
|
|
3118
|
+
return;
|
|
3119
|
+
}
|
|
3120
|
+
const optLogLevel = options.logger?.level;
|
|
3121
|
+
const log = optLogLevel === "error" || optLogLevel === "warn" || optLogLevel === "debug" ? logger : void 0;
|
|
3122
|
+
if (options.logger?.disabled !== true) {
|
|
3123
|
+
if (e && typeof e === "object" && "message" in e && typeof e.message === "string") {
|
|
3124
|
+
if (e.message.includes("no column") || e.message.includes("column") || e.message.includes("relation") || e.message.includes("table") || e.message.includes("does not exist")) {
|
|
3125
|
+
ctx.logger?.error(e.message);
|
|
3126
|
+
return;
|
|
3127
|
+
}
|
|
3128
|
+
}
|
|
3129
|
+
if (e instanceof APIError) {
|
|
3130
|
+
if (e.status === "INTERNAL_SERVER_ERROR") ctx.logger.error(e.status, e);
|
|
3131
|
+
log?.error(e.message);
|
|
3132
|
+
} else ctx.logger?.error(e && typeof e === "object" && "name" in e ? e.name : "", e);
|
|
3133
|
+
}
|
|
3134
|
+
}
|
|
3135
|
+
});
|
|
3136
|
+
};
|
|
3137
|
+
|
|
3138
|
+
//#endregion
|
|
3139
|
+
export { getAccessToken as A, createEmailVerificationToken as C, callbackOAuth as D, verifyEmail as E, decryptOAuthToken as F, setTokenUtil as I, isSimpleRequest as L, listUserAccounts as M, refreshToken as N, handleOAuthUserInfo as O, unlinkAccount as P, originCheck as R, error as S, sendVerificationEmailFn as T, signInSocial as _, getEndpoints as a, resetPassword as b, changeEmail as c, deleteUserCallback as d, setPassword as f, signInEmail as g, signOut as h, createAuthMiddleware$1 as i, linkSocialAccount as j, accountInfo as k, changePassword as l, signUpEmail as m, checkEndpointConflicts as n, optionsMiddleware as o, updateUser as p, createAuthEndpoint$1 as r, router as s, APIError$1 as t, deleteUser as u, requestPasswordReset as v, sendVerificationEmail as w, ok as x, requestPasswordResetCallback as y, originCheckMiddleware as z };
|