faire-auth 1.0.0
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/LICENSE +661 -0
- package/README.md +34 -0
- package/dist/access-B5bBI4uT.mjs +39 -0
- package/dist/access-Crt-IJ2P.cjs +51 -0
- package/dist/adapter-factory-BXei1QRX.mjs +881 -0
- package/dist/adapter-factory-DT-_2aHJ.cjs +893 -0
- package/dist/adapters/drizzle-adapter/index.cjs +208 -0
- package/dist/adapters/drizzle-adapter/index.d.cts +48 -0
- package/dist/adapters/drizzle-adapter/index.d.mts +48 -0
- package/dist/adapters/drizzle-adapter/index.mjs +207 -0
- package/dist/adapters/index.cjs +23 -0
- package/dist/adapters/index.d.cts +157 -0
- package/dist/adapters/index.d.mts +157 -0
- package/dist/adapters/index.mjs +17 -0
- package/dist/adapters/kysely-adapter/index.cjs +11 -0
- package/dist/adapters/kysely-adapter/index.d.cts +52 -0
- package/dist/adapters/kysely-adapter/index.d.mts +52 -0
- package/dist/adapters/kysely-adapter/index.mjs +9 -0
- package/dist/adapters/memory-adapter/index.cjs +9 -0
- package/dist/adapters/memory-adapter/index.d.cts +15 -0
- package/dist/adapters/memory-adapter/index.d.mts +15 -0
- package/dist/adapters/memory-adapter/index.mjs +9 -0
- package/dist/adapters/mongodb-adapter/index.cjs +9 -0
- package/dist/adapters/mongodb-adapter/index.d.cts +4 -0
- package/dist/adapters/mongodb-adapter/index.d.mts +4 -0
- package/dist/adapters/mongodb-adapter/index.mjs +9 -0
- package/dist/adapters/prisma-adapter/index.cjs +156 -0
- package/dist/adapters/prisma-adapter/index.d.cts +35 -0
- package/dist/adapters/prisma-adapter/index.d.mts +35 -0
- package/dist/adapters/prisma-adapter/index.mjs +155 -0
- package/dist/adapters/test.cjs +797 -0
- package/dist/adapters/test.d.cts +82 -0
- package/dist/adapters/test.d.mts +82 -0
- package/dist/adapters/test.mjs +793 -0
- package/dist/api/index.cjs +20 -0
- package/dist/api/index.d.cts +3 -0
- package/dist/api/index.d.mts +3 -0
- package/dist/api/index.mjs +20 -0
- package/dist/api/middleware/index.cjs +16 -0
- package/dist/api/middleware/index.d.cts +16 -0
- package/dist/api/middleware/index.d.mts +16 -0
- package/dist/api/middleware/index.mjs +11 -0
- package/dist/api-Bm89BLgN.cjs +1899 -0
- package/dist/api-_B7qO28y.mjs +1892 -0
- package/dist/auth-Bo9PDt5j.cjs +739 -0
- package/dist/auth-Ds4AG4h4.mjs +709 -0
- package/dist/bearer-0SeEmaS4.mjs +54 -0
- package/dist/bearer-CHeREcGT.cjs +60 -0
- package/dist/bun-sqlite-dialect-CfkBo2v5.mjs +155 -0
- package/dist/bun-sqlite-dialect-wuaC2-gh.cjs +156 -0
- package/dist/chunk-Ceigfy4P.mjs +39 -0
- package/dist/chunk-oakRccI5.cjs +60 -0
- package/dist/client/index.cjs +22 -0
- package/dist/client/index.d.cts +8 -0
- package/dist/client/index.d.mts +8 -0
- package/dist/client/index.mjs +19 -0
- package/dist/client/lynx/index.cjs +79 -0
- package/dist/client/lynx/index.d.cts +156 -0
- package/dist/client/lynx/index.d.mts +156 -0
- package/dist/client/lynx/index.mjs +76 -0
- package/dist/client/plugins/index.cjs +11 -0
- package/dist/client/plugins/index.d.cts +17 -0
- package/dist/client/plugins/index.d.mts +17 -0
- package/dist/client/plugins/index.mjs +10 -0
- package/dist/client/react/index.cjs +79 -0
- package/dist/client/react/index.d.cts +156 -0
- package/dist/client/react/index.d.mts +156 -0
- package/dist/client/react/index.mjs +76 -0
- package/dist/client/solid/index.cjs +51 -0
- package/dist/client/solid/index.d.cts +90 -0
- package/dist/client/solid/index.d.mts +90 -0
- package/dist/client/solid/index.mjs +50 -0
- package/dist/client/svelte/index.cjs +28 -0
- package/dist/client/svelte/index.d.cts +110 -0
- package/dist/client/svelte/index.d.mts +110 -0
- package/dist/client/svelte/index.mjs +28 -0
- package/dist/client/vue/index.cjs +68 -0
- package/dist/client/vue/index.d.cts +123 -0
- package/dist/client/vue/index.d.mts +123 -0
- package/dist/client/vue/index.mjs +67 -0
- package/dist/cookies-B5xeA6i8.cjs +456 -0
- package/dist/cookies-Ch1JfQIz.mjs +367 -0
- package/dist/crypto/index.cjs +11 -0
- package/dist/crypto/index.d.cts +22 -0
- package/dist/crypto/index.d.mts +22 -0
- package/dist/crypto/index.mjs +5 -0
- package/dist/crypto-CcTxYH82.cjs +57 -0
- package/dist/crypto-CyQRFtkq.mjs +41 -0
- package/dist/db/index.cjs +40 -0
- package/dist/db/index.d.cts +4 -0
- package/dist/db/index.d.mts +4 -0
- package/dist/db/index.mjs +16 -0
- package/dist/db-BTKyYu-_.cjs +1180 -0
- package/dist/db-xiJtoDgz.mjs +1119 -0
- package/dist/defu-B0Svx_x4.cjs +34 -0
- package/dist/defu-SZkpHhbR.mjs +28 -0
- package/dist/email-verification-C-AsfJsb.cjs +247 -0
- package/dist/email-verification-CWhj2qfb.mjs +228 -0
- package/dist/hono-BEq1NYVX.cjs +526 -0
- package/dist/hono-Cdukwu5K.cjs +13 -0
- package/dist/hono-CoKYWnXA.mjs +7 -0
- package/dist/hono-Covz10jU.mjs +508 -0
- package/dist/hono-DOM31WQz.d.cts +18 -0
- package/dist/hono-DuE14mmj.d.mts +18 -0
- package/dist/index-B-WeOMzg.d.cts +13350 -0
- package/dist/index-B09P9p5_.d.cts +33 -0
- package/dist/index-B5_0QTLg.d.mts +3 -0
- package/dist/index-B7SKyR_a.d.cts +3 -0
- package/dist/index-BVHscLQl.d.cts +992 -0
- package/dist/index-C2-2QDyS.d.cts +35 -0
- package/dist/index-CTwrVluH.d.mts +41 -0
- package/dist/index-CWAqWfoZ.d.cts +27 -0
- package/dist/index-CYSShrFx.d.mts +27 -0
- package/dist/index-Qu-iJGNr.d.mts +992 -0
- package/dist/index-cs4Sh0En.d.cts +41 -0
- package/dist/index-jlessPI0.d.mts +13350 -0
- package/dist/index-ppgcFe12.d.mts +33 -0
- package/dist/index-to0RO38U.d.mts +35 -0
- package/dist/index.cjs +85 -0
- package/dist/index.d.cts +5 -0
- package/dist/index.d.mts +5 -0
- package/dist/index.mjs +27 -0
- package/dist/integrations/next-js.cjs +50 -0
- package/dist/integrations/next-js.d.cts +23 -0
- package/dist/integrations/next-js.d.mts +23 -0
- package/dist/integrations/next-js.mjs +49 -0
- package/dist/integrations/node.cjs +139 -0
- package/dist/integrations/node.d.cts +22 -0
- package/dist/integrations/node.d.mts +22 -0
- package/dist/integrations/node.mjs +134 -0
- package/dist/integrations/react-start.cjs +37 -0
- package/dist/integrations/react-start.d.cts +17 -0
- package/dist/integrations/react-start.d.mts +17 -0
- package/dist/integrations/react-start.mjs +37 -0
- package/dist/integrations/solid-start.cjs +14 -0
- package/dist/integrations/solid-start.d.cts +13 -0
- package/dist/integrations/solid-start.d.mts +13 -0
- package/dist/integrations/solid-start.mjs +13 -0
- package/dist/integrations/svelte-kit.cjs +52 -0
- package/dist/integrations/svelte-kit.d.cts +42 -0
- package/dist/integrations/svelte-kit.d.mts +42 -0
- package/dist/integrations/svelte-kit.mjs +49 -0
- package/dist/ip-B5SwXaMB.cjs +54 -0
- package/dist/ip-BjNYspCH.mjs +36 -0
- package/dist/json-B1v2qRTQ.mjs +24 -0
- package/dist/json-CcJ2HjGo.cjs +30 -0
- package/dist/kysely-adapter-Bxi7sPdG.cjs +288 -0
- package/dist/kysely-adapter-DRuoT_X8.mjs +270 -0
- package/dist/logged-CyGfp8_H.mjs +187 -0
- package/dist/logged-DQK3C64b.cjs +193 -0
- package/dist/memory-adapter-BlnDIAAX.cjs +154 -0
- package/dist/memory-adapter-Db3tsJTn.mjs +148 -0
- package/dist/middleware-CE6OhDkq.mjs +29 -0
- package/dist/middleware-CWvRH4EW.cjs +47 -0
- package/dist/mongodb-adapter-C_qGQED5.mjs +291 -0
- package/dist/mongodb-adapter-C_qfK87x.cjs +297 -0
- package/dist/node-sqlite-dialect-BfBzyOAg.mjs +155 -0
- package/dist/node-sqlite-dialect-mJWC8LWQ.cjs +156 -0
- package/dist/oauth2/index.cjs +25 -0
- package/dist/oauth2/index.d.cts +42 -0
- package/dist/oauth2/index.d.mts +42 -0
- package/dist/oauth2/index.mjs +16 -0
- package/dist/oauth2-B8M0lW9l.mjs +159 -0
- package/dist/oauth2-BLeOEi7Y.cjs +177 -0
- package/dist/origin-check-8Gajklvh.mjs +228 -0
- package/dist/origin-check-id9lTnqZ.cjs +246 -0
- package/dist/parser-CdncAl9i.cjs +83 -0
- package/dist/parser-CeAer-RM.mjs +71 -0
- package/dist/passkey-B6N20LSS.cjs +512 -0
- package/dist/passkey-DJsR_GX3.mjs +505 -0
- package/dist/plugins/access/index.cjs +4 -0
- package/dist/plugins/access/index.d.cts +2 -0
- package/dist/plugins/access/index.d.mts +2 -0
- package/dist/plugins/access/index.mjs +3 -0
- package/dist/plugins/bearer/index.cjs +7 -0
- package/dist/plugins/bearer/index.d.cts +4 -0
- package/dist/plugins/bearer/index.d.mts +4 -0
- package/dist/plugins/bearer/index.mjs +7 -0
- package/dist/plugins/index.cjs +49 -0
- package/dist/plugins/index.d.cts +9326 -0
- package/dist/plugins/index.d.mts +9326 -0
- package/dist/plugins/index.mjs +26 -0
- package/dist/plugins/passkey/index.cjs +15 -0
- package/dist/plugins/passkey/index.d.cts +4 -0
- package/dist/plugins/passkey/index.d.mts +4 -0
- package/dist/plugins/passkey/index.mjs +15 -0
- package/dist/plugins-C1lm2Wj3.cjs +5254 -0
- package/dist/plugins-Dai1QkRU.mjs +5199 -0
- package/dist/request-1NL_1lyJ.mjs +58 -0
- package/dist/request-CPNlp-DK.cjs +100 -0
- package/dist/schema-D-qjygfE.cjs +179 -0
- package/dist/schema-DTIwLLmf.mjs +125 -0
- package/dist/session-De9POJTW.cjs +641 -0
- package/dist/session-rH8ougDl.mjs +538 -0
- package/dist/social-providers/index.cjs +10 -0
- package/dist/social-providers/index.d.cts +2 -0
- package/dist/social-providers/index.d.mts +2 -0
- package/dist/social-providers/index.mjs +5 -0
- package/dist/social-providers-DcFNUT0S.cjs +13 -0
- package/dist/social-providers-DoYJhBe3.mjs +9 -0
- package/dist/state--f7fwIGh.cjs +99 -0
- package/dist/state-CqJfCIl9.mjs +86 -0
- package/dist/test-utils/index.cjs +27492 -0
- package/dist/test-utils/index.d.cts +4584 -0
- package/dist/test-utils/index.d.mts +4584 -0
- package/dist/test-utils/index.mjs +27490 -0
- package/dist/types/index.cjs +0 -0
- package/dist/types/index.d.cts +3 -0
- package/dist/types/index.d.mts +3 -0
- package/dist/types/index.mjs +1 -0
- package/dist/url-BoUuoIwZ.mjs +88 -0
- package/dist/url-D2yvKaM7.cjs +118 -0
- package/dist/utils/cookies.cjs +51 -0
- package/dist/utils/cookies.d.cts +179 -0
- package/dist/utils/cookies.d.mts +179 -0
- package/dist/utils/cookies.mjs +7 -0
- package/dist/utils-44aDDP8f.cjs +2 -0
- package/dist/utils-D4vZd1-9.mjs +4 -0
- package/dist/vanilla-Bu2PmFzE.cjs +30 -0
- package/dist/vanilla-TfHqeiC7.mjs +25 -0
- package/package.json +470 -0
|
@@ -0,0 +1,1892 @@
|
|
|
1
|
+
import { t as contextStorage } from "./hono-CoKYWnXA.mjs";
|
|
2
|
+
import { r as runWithTransaction, t as getIp } from "./ip-BjNYspCH.mjs";
|
|
3
|
+
import { n as crypto_exports } from "./crypto-CyQRFtkq.mjs";
|
|
4
|
+
import { t as safeJSONParse } from "./json-B1v2qRTQ.mjs";
|
|
5
|
+
import { n as cookies_exports, o as deleteSessionCookie, y as setSessionCookie } from "./cookies-Ch1JfQIz.mjs";
|
|
6
|
+
import { r as createMiddleware } from "./middleware-CE6OhDkq.mjs";
|
|
7
|
+
import { _ as initContextMiddleware, a as requestOnlySessionMiddleware, c as revokeSessions, d as createAPI, f as createEndpoint, g as initHooksMiddleware, h as initErrorHandler, i as listSessions, l as sensitiveSessionMiddleware, m as setDefaultExecutionCtx, n as getSession, o as revokeOtherSessions, p as setRenderer, s as revokeSession, t as freshSessionMiddleware, u as sessionMiddleware } from "./session-rH8ougDl.mjs";
|
|
8
|
+
import { n as originCheck, r as wildcardMatch, t as initOriginCheckMiddleware } from "./origin-check-8Gajklvh.mjs";
|
|
9
|
+
import { n as sendVerificationEmail, r as verifyEmail, t as createEmailVerificationToken } from "./email-verification-CWhj2qfb.mjs";
|
|
10
|
+
import { n as decryptOAuthToken, r as setTokenUtil, t as handleOAuthUserInfo } from "./oauth2-B8M0lW9l.mjs";
|
|
11
|
+
import { n as parseState, t as generateState } from "./state-CqJfCIl9.mjs";
|
|
12
|
+
import { t as social_providers_exports } from "./social-providers-DoYJhBe3.mjs";
|
|
13
|
+
import { r as generateId } from "./utils-D4vZd1-9.mjs";
|
|
14
|
+
import { c as parseUserInput } from "./schema-DTIwLLmf.mjs";
|
|
15
|
+
import { isDevelopment } from "@faire-auth/core/env";
|
|
16
|
+
import { BASE_ERROR_CODES } from "@faire-auth/core/error";
|
|
17
|
+
import { OpenAPIHono, callbackURLSchema, createRoute, emailSchema, nameSchema, passwordSchema, req, res } from "@faire-auth/core/factory";
|
|
18
|
+
import { Definitions, False, Routes, SCHEMAS, True, isPromise } from "@faire-auth/core/static";
|
|
19
|
+
import { buildSchemas, getDate } from "@faire-auth/core/utils";
|
|
20
|
+
import { Hono } from "hono";
|
|
21
|
+
import { RegExpRouter } from "hono/router/reg-exp-router";
|
|
22
|
+
import * as z$1 from "zod";
|
|
23
|
+
import { looseUserSchema } from "@faire-auth/core/db";
|
|
24
|
+
|
|
25
|
+
//#region src/api/schema/account.ts
|
|
26
|
+
const socialProviderListEnum = z$1.enum(social_providers_exports.socialProviderList).or(z$1.string());
|
|
27
|
+
const linkAccountSchema = z$1.object({
|
|
28
|
+
callbackURL: callbackURLSchema(true).openapi({ description: "The URL to redirect to after the user has signed in" }),
|
|
29
|
+
provider: socialProviderListEnum,
|
|
30
|
+
idToken: z$1.object({
|
|
31
|
+
token: z$1.string(),
|
|
32
|
+
nonce: z$1.string().optional(),
|
|
33
|
+
accessToken: z$1.string().optional(),
|
|
34
|
+
refreshToken: z$1.string().optional(),
|
|
35
|
+
scopes: z$1.array(z$1.string().min(1)).optional()
|
|
36
|
+
}).optional(),
|
|
37
|
+
scopes: z$1.array(z$1.string().min(1)).optional().openapi({ description: "Additional scopes to request when linking the account" }),
|
|
38
|
+
errorCallbackURL: callbackURLSchema(true).openapi({ description: "The URL to redirect to if there is an error during the link process" })
|
|
39
|
+
});
|
|
40
|
+
const unlinkAccountSchema = z$1.object({
|
|
41
|
+
providerId: z$1.string(),
|
|
42
|
+
accountId: z$1.string().optional()
|
|
43
|
+
});
|
|
44
|
+
const getAccessTokenSchema = z$1.object({
|
|
45
|
+
providerId: z$1.string().openapi({ description: "The provider ID for the OAuth provider" }),
|
|
46
|
+
accountId: z$1.string().optional().openapi({ description: "The account ID associated with the refresh token" }),
|
|
47
|
+
userId: z$1.string().optional().openapi({ description: "The user ID associated with the account" })
|
|
48
|
+
});
|
|
49
|
+
const refreshAccessTokenSchema = z$1.object({
|
|
50
|
+
providerId: z$1.string().openapi({ description: "The provider ID for the OAuth provider" }),
|
|
51
|
+
accountId: z$1.string().optional().openapi({ description: "The account ID associated with the refresh token" }),
|
|
52
|
+
userId: z$1.string().optional().openapi({ description: "The user ID associated with the account" })
|
|
53
|
+
});
|
|
54
|
+
const getAccountInfoSchema = z$1.object({ accountId: z$1.string().openapi({ description: "The provider given account id for which to get the account info" }) });
|
|
55
|
+
|
|
56
|
+
//#endregion
|
|
57
|
+
//#region src/api/schema/sign-in.ts
|
|
58
|
+
const signInSocialSchema = z$1.object({
|
|
59
|
+
callbackURL: callbackURLSchema(true).openapi({ description: "Callback URL to redirect to after the user has signed in" }),
|
|
60
|
+
newUserCallbackURL: callbackURLSchema(true),
|
|
61
|
+
errorCallbackURL: callbackURLSchema(true).openapi({ description: "Callback URL to redirect to if an error happens" }),
|
|
62
|
+
provider: socialProviderListEnum,
|
|
63
|
+
disableRedirect: z$1.boolean().optional().openapi({ description: "Disable automatic redirection to the provider. Useful for handling the redirection yourself" }),
|
|
64
|
+
idToken: z$1.optional(z$1.object({
|
|
65
|
+
token: z$1.string().openapi({ description: "ID token from the provider" }),
|
|
66
|
+
nonce: z$1.string().optional().openapi({ description: "Nonce used to generate the token" }),
|
|
67
|
+
accessToken: z$1.string().optional().openapi({ description: "Access token from the provider" }),
|
|
68
|
+
refreshToken: z$1.string().optional().openapi({ description: "Refresh token from the provider" }),
|
|
69
|
+
expiresAt: z$1.number().optional().openapi({ description: "Expiry date of the token" })
|
|
70
|
+
})).openapi({ description: "ID token from the provider to sign in the user with id token" }),
|
|
71
|
+
scopes: z$1.array(z$1.string().min(1)).optional().openapi({ description: "Array of scopes to request from the provider. This will override the default scopes passed." }),
|
|
72
|
+
requestSignUp: z$1.boolean().optional().openapi({ description: "Explicitly request sign-up. Useful when disableImplicitSignUp is true for this provider" }),
|
|
73
|
+
loginHint: z$1.string().optional().openapi({ description: "The login hint to use for the authorization code request" })
|
|
74
|
+
});
|
|
75
|
+
const signInEmailSchema = z$1.object({
|
|
76
|
+
email: emailSchema.openapi({ description: "Email of the user" }),
|
|
77
|
+
password: passwordSchema.openapi({ description: "Password of the user" }),
|
|
78
|
+
callbackURL: callbackURLSchema(true).openapi({ description: "Callback URL to use as a redirect for email verification" }),
|
|
79
|
+
rememberMe: z$1.boolean().default(true).openapi({ description: "If this is false, the session will not be remembered. Default is `true`." })
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
//#endregion
|
|
83
|
+
//#region src/api/routes/sign-in.ts
|
|
84
|
+
const signInSocialRoute = createRoute({
|
|
85
|
+
operationId: Routes.SIGN_IN_SOCIAL,
|
|
86
|
+
method: "post",
|
|
87
|
+
path: "/sign-in/social",
|
|
88
|
+
description: "Sign in with a social provider",
|
|
89
|
+
request: req().bdy(signInSocialSchema).bld(),
|
|
90
|
+
responses: res(SCHEMAS[Definitions.SIGN_IN_UP].default, "Returns either session details or redirect URL").rdr().err(401, "Invalid token or user not found").err(404, "Provider or id token not supported").zod().bld()
|
|
91
|
+
});
|
|
92
|
+
const signInSocial = createEndpoint(signInSocialRoute, (options) => async (ctx) => {
|
|
93
|
+
const { provider: requestedProvider, idToken, requestSignUp, callbackURL, scopes, loginHint, disableRedirect } = ctx.req.valid("json");
|
|
94
|
+
const context = ctx.get("context");
|
|
95
|
+
const provider = context.socialProviders.find((p) => p.id === requestedProvider);
|
|
96
|
+
if (!provider) {
|
|
97
|
+
context.logger.error("Provider not found. Make sure to add the provider in your auth config", { provider: requestedProvider });
|
|
98
|
+
return ctx.render({
|
|
99
|
+
success: False,
|
|
100
|
+
message: BASE_ERROR_CODES.PROVIDER_NOT_FOUND
|
|
101
|
+
}, 404);
|
|
102
|
+
}
|
|
103
|
+
if (idToken) {
|
|
104
|
+
if (!provider.verifyIdToken) {
|
|
105
|
+
context.logger.error("Provider does not support id token verification", { provider: requestedProvider });
|
|
106
|
+
return ctx.render({
|
|
107
|
+
success: False,
|
|
108
|
+
message: BASE_ERROR_CODES.ID_TOKEN_NOT_SUPPORTED
|
|
109
|
+
}, 404);
|
|
110
|
+
}
|
|
111
|
+
const { token, nonce } = idToken;
|
|
112
|
+
if (!await provider.verifyIdToken(token, nonce)) {
|
|
113
|
+
context.logger.error("Invalid id token", { provider: requestedProvider });
|
|
114
|
+
return ctx.render({
|
|
115
|
+
success: False,
|
|
116
|
+
message: BASE_ERROR_CODES.INVALID_TOKEN
|
|
117
|
+
}, 401);
|
|
118
|
+
}
|
|
119
|
+
const userInfo = await provider.getUserInfo({
|
|
120
|
+
idToken: token,
|
|
121
|
+
...idToken.accessToken != null && { accessToken: idToken.accessToken },
|
|
122
|
+
...idToken.refreshToken != null && { refreshToken: idToken.refreshToken }
|
|
123
|
+
});
|
|
124
|
+
if (!userInfo?.user) {
|
|
125
|
+
context.logger.error("Failed to get user info", { provider: requestedProvider });
|
|
126
|
+
return ctx.render({
|
|
127
|
+
success: False,
|
|
128
|
+
message: BASE_ERROR_CODES.FAILED_TO_GET_USER_INFO
|
|
129
|
+
}, 401);
|
|
130
|
+
}
|
|
131
|
+
if (userInfo.user.email == null) {
|
|
132
|
+
context.logger.error("User email not found", { provider: requestedProvider });
|
|
133
|
+
return ctx.render({
|
|
134
|
+
success: False,
|
|
135
|
+
message: BASE_ERROR_CODES.USER_EMAIL_NOT_FOUND
|
|
136
|
+
}, 401);
|
|
137
|
+
}
|
|
138
|
+
const data = await handleOAuthUserInfo(ctx, {
|
|
139
|
+
userInfo: {
|
|
140
|
+
...userInfo.user,
|
|
141
|
+
email: userInfo.user.email,
|
|
142
|
+
id: userInfo.user.id.toString(),
|
|
143
|
+
...userInfo.user.name && { name: userInfo.user.name },
|
|
144
|
+
...userInfo.user.image && { image: userInfo.user.image },
|
|
145
|
+
emailVerified: userInfo.user.emailVerified
|
|
146
|
+
},
|
|
147
|
+
account: {
|
|
148
|
+
providerId: provider.id,
|
|
149
|
+
accountId: userInfo.user.id.toString(),
|
|
150
|
+
...idToken.accessToken && { accessToken: idToken.accessToken }
|
|
151
|
+
},
|
|
152
|
+
...callbackURL && { callbackURL },
|
|
153
|
+
disableSignUp: provider.disableImplicitSignUp === true && requestSignUp !== true || provider.disableSignUp === true
|
|
154
|
+
}, options);
|
|
155
|
+
if (data.type === "redirect") return data.response;
|
|
156
|
+
if (data.type === "error") return ctx.render({
|
|
157
|
+
success: False,
|
|
158
|
+
message: data.reason.replaceAll("_", " ")
|
|
159
|
+
}, 401);
|
|
160
|
+
await setSessionCookie(ctx, options, data.data);
|
|
161
|
+
return ctx.render({
|
|
162
|
+
success: True,
|
|
163
|
+
redirect: False,
|
|
164
|
+
token: data.data.session.token,
|
|
165
|
+
user: data.data.user
|
|
166
|
+
}, 200);
|
|
167
|
+
}
|
|
168
|
+
const { codeVerifier, state } = await generateState(ctx, void 0, options);
|
|
169
|
+
const url = await provider.createAuthorizationURL({
|
|
170
|
+
state,
|
|
171
|
+
codeVerifier,
|
|
172
|
+
redirectURI: `${context.baseURL}/callback/${provider.id}`,
|
|
173
|
+
...scopes && scopes.length > 0 && { scopes },
|
|
174
|
+
...loginHint != null && loginHint !== "" && { loginHint }
|
|
175
|
+
});
|
|
176
|
+
if (disableRedirect === true) return ctx.render({
|
|
177
|
+
success: False,
|
|
178
|
+
redirect: False
|
|
179
|
+
}, 200);
|
|
180
|
+
return ctx.render({
|
|
181
|
+
success: False,
|
|
182
|
+
redirect: True,
|
|
183
|
+
url: url.toString()
|
|
184
|
+
}, 200);
|
|
185
|
+
});
|
|
186
|
+
const signInEmailRoute = createRoute({
|
|
187
|
+
operationId: Routes.SIGN_IN_EMAIL,
|
|
188
|
+
method: "post",
|
|
189
|
+
path: "/sign-in/email",
|
|
190
|
+
description: "Sign in with email and password",
|
|
191
|
+
request: req().bdy(signInEmailSchema).bld(),
|
|
192
|
+
responses: res(SCHEMAS[Definitions.SIGN_IN_UP].default, "Returns either session details or redirect URL").err(400, "Email and password login not enabled in options").err(401, "Invalid email or password").err(403, "Email not verified").zod().err(500, "Failed to create session").bld()
|
|
193
|
+
});
|
|
194
|
+
const signInEmail = createEndpoint(signInEmailRoute, (options) => async (ctx) => {
|
|
195
|
+
const context = ctx.get("context");
|
|
196
|
+
if (!(options.emailAndPassword?.enabled ?? false)) {
|
|
197
|
+
context.logger.error("Email and password is not enabled. Make sure to enable it in the options on you `auth.ts` file. Check `https://faire-auth.com/docs/authentication/email-password` for more!");
|
|
198
|
+
return ctx.render({
|
|
199
|
+
success: False,
|
|
200
|
+
message: "Email and password is not enabled"
|
|
201
|
+
}, 400);
|
|
202
|
+
}
|
|
203
|
+
const { email, password, rememberMe, callbackURL } = ctx.req.valid("json");
|
|
204
|
+
const user = await context.internalAdapter.findUserByEmail(email, { includeAccounts: true });
|
|
205
|
+
if (!user) {
|
|
206
|
+
await context.password.hash(password);
|
|
207
|
+
context.logger.error("User not found", { email });
|
|
208
|
+
return ctx.render({
|
|
209
|
+
success: False,
|
|
210
|
+
message: BASE_ERROR_CODES.INVALID_EMAIL_OR_PASSWORD
|
|
211
|
+
}, 401);
|
|
212
|
+
}
|
|
213
|
+
const credentialAccount = user.accounts.find((a) => a.providerId === "credential");
|
|
214
|
+
if (!credentialAccount) {
|
|
215
|
+
context.logger.error("Credential account not found", { email });
|
|
216
|
+
return ctx.render({
|
|
217
|
+
success: False,
|
|
218
|
+
message: BASE_ERROR_CODES.INVALID_EMAIL_OR_PASSWORD
|
|
219
|
+
}, 401);
|
|
220
|
+
}
|
|
221
|
+
const currentPassword = credentialAccount.password;
|
|
222
|
+
if (currentPassword == null) {
|
|
223
|
+
context.logger.error("Password not found", { email });
|
|
224
|
+
return ctx.render({
|
|
225
|
+
success: False,
|
|
226
|
+
message: BASE_ERROR_CODES.INVALID_EMAIL_OR_PASSWORD
|
|
227
|
+
}, 401);
|
|
228
|
+
}
|
|
229
|
+
if (!await context.password.verify({
|
|
230
|
+
hash: currentPassword,
|
|
231
|
+
password
|
|
232
|
+
})) {
|
|
233
|
+
context.logger.error("Invalid password");
|
|
234
|
+
return ctx.render({
|
|
235
|
+
success: False,
|
|
236
|
+
message: BASE_ERROR_CODES.INVALID_EMAIL_OR_PASSWORD
|
|
237
|
+
}, 401);
|
|
238
|
+
}
|
|
239
|
+
if (options.emailAndPassword?.requireEmailVerification === true && !user.user.emailVerified) {
|
|
240
|
+
if (!options.emailVerification?.sendVerificationEmail) return ctx.render({
|
|
241
|
+
success: False,
|
|
242
|
+
message: BASE_ERROR_CODES.EMAIL_NOT_VERIFIED
|
|
243
|
+
}, 403);
|
|
244
|
+
const token = await createEmailVerificationToken(context.secret, user.user.email, void 0, options.emailVerification.expiresIn);
|
|
245
|
+
const url = `${context.baseURL}/verify-email?token=${token}&callbackURL=${callbackURL ?? "/"}`;
|
|
246
|
+
await options.emailVerification.sendVerificationEmail({
|
|
247
|
+
user: user.user,
|
|
248
|
+
url,
|
|
249
|
+
token
|
|
250
|
+
}, ctx);
|
|
251
|
+
return ctx.render({
|
|
252
|
+
success: False,
|
|
253
|
+
message: BASE_ERROR_CODES.EMAIL_NOT_VERIFIED
|
|
254
|
+
}, 403);
|
|
255
|
+
}
|
|
256
|
+
const session = await context.internalAdapter.createSession(user.user.id, !rememberMe);
|
|
257
|
+
if (session == null) {
|
|
258
|
+
context.logger.error("Failed to create session");
|
|
259
|
+
return ctx.render({
|
|
260
|
+
success: False,
|
|
261
|
+
message: BASE_ERROR_CODES.FAILED_TO_CREATE_SESSION
|
|
262
|
+
}, 500);
|
|
263
|
+
}
|
|
264
|
+
await setSessionCookie(ctx, options, {
|
|
265
|
+
session,
|
|
266
|
+
user: user.user
|
|
267
|
+
}, !rememberMe);
|
|
268
|
+
if (callbackURL != null) return ctx.render({
|
|
269
|
+
success: True,
|
|
270
|
+
redirect: True,
|
|
271
|
+
url: callbackURL,
|
|
272
|
+
token: session.token,
|
|
273
|
+
user: user.user
|
|
274
|
+
}, 200);
|
|
275
|
+
return ctx.render({
|
|
276
|
+
success: True,
|
|
277
|
+
redirect: False,
|
|
278
|
+
token: session.token,
|
|
279
|
+
user: user.user
|
|
280
|
+
}, 200);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
//#endregion
|
|
284
|
+
//#region src/api/schema/callback.ts
|
|
285
|
+
const callbackSchema = z$1.object({
|
|
286
|
+
code: z$1.string(),
|
|
287
|
+
error: z$1.string(),
|
|
288
|
+
device_id: z$1.string(),
|
|
289
|
+
error_description: z$1.string(),
|
|
290
|
+
state: z$1.string(),
|
|
291
|
+
user: z$1.string()
|
|
292
|
+
}).partial();
|
|
293
|
+
const oauthCallbackParamsSchema = z$1.object({ id: z$1.string().openapi({
|
|
294
|
+
param: {
|
|
295
|
+
name: "id",
|
|
296
|
+
in: "path"
|
|
297
|
+
},
|
|
298
|
+
description: "OAuth provider ID"
|
|
299
|
+
}) });
|
|
300
|
+
const oauthCallbackQuerySchema = callbackSchema;
|
|
301
|
+
|
|
302
|
+
//#endregion
|
|
303
|
+
//#region src/api/routes/callback.ts
|
|
304
|
+
const handleCallbackOAuth = async (ctx, paramsId, { code, error: error$1, device_id, error_description, state, user } = {}, options) => {
|
|
305
|
+
const context = ctx.get("context");
|
|
306
|
+
const defaultErrorURL = options.onAPIError?.errorURL ?? `${context.baseURL}/error`;
|
|
307
|
+
if (error$1 != null) return ctx.redirect(`${defaultErrorURL}?error=${error$1}&error_description=${error_description}`, 302);
|
|
308
|
+
if (state == null) {
|
|
309
|
+
context.logger.error("State not found", error$1);
|
|
310
|
+
return ctx.redirect(`${defaultErrorURL}?error=state_not_found`, 302);
|
|
311
|
+
}
|
|
312
|
+
const res$1 = await parseState(ctx, options);
|
|
313
|
+
if (res$1.type === "redirect") return res$1.response;
|
|
314
|
+
const { codeVerifier, callbackURL, link, errorURL, newUserURL, requestSignUp } = res$1.data;
|
|
315
|
+
const redirectOnError = (error$2) => {
|
|
316
|
+
let url = errorURL ?? defaultErrorURL;
|
|
317
|
+
if (url.includes("?")) url = `${url}&error=${error$2}`;
|
|
318
|
+
else url = `${url}?error=${error$2}`;
|
|
319
|
+
return ctx.redirect(url, 302);
|
|
320
|
+
};
|
|
321
|
+
if (code == null) {
|
|
322
|
+
context.logger.error("Code not found");
|
|
323
|
+
return redirectOnError("no_code");
|
|
324
|
+
}
|
|
325
|
+
const provider = context.socialProviders.find((p) => p.id === paramsId);
|
|
326
|
+
if (!provider) {
|
|
327
|
+
context.logger.error("Oauth provider with id", paramsId, "not found");
|
|
328
|
+
return redirectOnError("oauth_provider_not_found");
|
|
329
|
+
}
|
|
330
|
+
let tokens;
|
|
331
|
+
try {
|
|
332
|
+
tokens = await provider.validateAuthorizationCode({
|
|
333
|
+
code,
|
|
334
|
+
codeVerifier,
|
|
335
|
+
...device_id != null ? { deviceId: device_id } : {},
|
|
336
|
+
redirectURI: `${context.baseURL}/callback/${provider.id}`
|
|
337
|
+
});
|
|
338
|
+
} catch (e) {
|
|
339
|
+
context.logger.error("", e);
|
|
340
|
+
return redirectOnError("invalid_code");
|
|
341
|
+
}
|
|
342
|
+
const userInfo = await provider.getUserInfo({
|
|
343
|
+
...tokens,
|
|
344
|
+
...user && { user: safeJSONParse(user) }
|
|
345
|
+
}).then((res$2) => res$2?.user);
|
|
346
|
+
if (!userInfo) {
|
|
347
|
+
context.logger.error("Unable to get user info");
|
|
348
|
+
return redirectOnError("unable_to_get_user_info");
|
|
349
|
+
}
|
|
350
|
+
if (!callbackURL) {
|
|
351
|
+
context.logger.error("No callback URL found");
|
|
352
|
+
return redirectOnError("no_callback_url");
|
|
353
|
+
}
|
|
354
|
+
if (link) {
|
|
355
|
+
if (!(options.account?.accountLinking?.trustedProviders)?.includes(provider.id) && !userInfo.emailVerified || options.account?.accountLinking?.enabled === false) {
|
|
356
|
+
context.logger.error("Unable to link account - untrusted provider");
|
|
357
|
+
return redirectOnError("unable_to_link_account");
|
|
358
|
+
}
|
|
359
|
+
const existingAccount = await context.internalAdapter.findAccount(userInfo.id.toString());
|
|
360
|
+
if (existingAccount) {
|
|
361
|
+
if (existingAccount.userId.toString() !== link.userId.toString()) return redirectOnError("account_already_linked_to_different_user");
|
|
362
|
+
const updateData = Object.fromEntries(Object.entries({
|
|
363
|
+
idToken: tokens.idToken,
|
|
364
|
+
accessToken: await setTokenUtil(tokens.accessToken, ctx, options),
|
|
365
|
+
refreshToken: await setTokenUtil(tokens.refreshToken, ctx, options),
|
|
366
|
+
accessTokenExpiresAt: tokens.accessTokenExpiresAt,
|
|
367
|
+
refreshTokenExpiresAt: tokens.refreshTokenExpiresAt,
|
|
368
|
+
...tokens.scopes && tokens.scopes.length > 0 && { scope: tokens.scopes.join(",") }
|
|
369
|
+
}).filter(([_, value]) => value !== void 0));
|
|
370
|
+
await context.internalAdapter.updateAccount(existingAccount.id, updateData);
|
|
371
|
+
} else if (await context.internalAdapter.createAccount({
|
|
372
|
+
userId: link.userId,
|
|
373
|
+
providerId: provider.id,
|
|
374
|
+
accountId: userInfo.id,
|
|
375
|
+
...tokens,
|
|
376
|
+
accessToken: await setTokenUtil(tokens.accessToken, ctx, options),
|
|
377
|
+
refreshToken: await setTokenUtil(tokens.refreshToken, ctx, options),
|
|
378
|
+
...tokens.scopes && tokens.scopes.length > 0 && { scope: tokens.scopes.join(",") }
|
|
379
|
+
}) == null) return redirectOnError("unable_to_link_account");
|
|
380
|
+
let toRedirectTo$1;
|
|
381
|
+
try {
|
|
382
|
+
toRedirectTo$1 = callbackURL.toString();
|
|
383
|
+
} catch {
|
|
384
|
+
toRedirectTo$1 = callbackURL;
|
|
385
|
+
}
|
|
386
|
+
return ctx.redirect(toRedirectTo$1, 302);
|
|
387
|
+
}
|
|
388
|
+
if (userInfo.email == null) {
|
|
389
|
+
context.logger.error("Provider did not return email. This could be due to misconfiguration in the provider settings.");
|
|
390
|
+
return redirectOnError("email_not_found");
|
|
391
|
+
}
|
|
392
|
+
const result = await handleOAuthUserInfo(ctx, {
|
|
393
|
+
userInfo: {
|
|
394
|
+
...userInfo,
|
|
395
|
+
id: userInfo.id.toString(),
|
|
396
|
+
email: userInfo.email,
|
|
397
|
+
name: userInfo.name ?? userInfo.email
|
|
398
|
+
},
|
|
399
|
+
account: {
|
|
400
|
+
providerId: provider.id,
|
|
401
|
+
accountId: userInfo.id.toString(),
|
|
402
|
+
...tokens,
|
|
403
|
+
...tokens.scopes && tokens.scopes.length > 0 && { scope: tokens.scopes.join(",") }
|
|
404
|
+
},
|
|
405
|
+
callbackURL,
|
|
406
|
+
disableSignUp: provider.disableImplicitSignUp === true && !(requestSignUp ?? false) || provider.options?.disableSignUp === true,
|
|
407
|
+
overrideUserInfo: provider.options?.overrideUserInfoOnSignIn ?? false
|
|
408
|
+
}, options);
|
|
409
|
+
if (result.type === "redirect") return result.response;
|
|
410
|
+
if (result.type === "error") {
|
|
411
|
+
context.logger.error(result.reason);
|
|
412
|
+
return redirectOnError(result.reason);
|
|
413
|
+
}
|
|
414
|
+
const { session, user: resultUser } = result.data;
|
|
415
|
+
await setSessionCookie(ctx, options, {
|
|
416
|
+
session,
|
|
417
|
+
user: resultUser
|
|
418
|
+
});
|
|
419
|
+
let toRedirectTo;
|
|
420
|
+
try {
|
|
421
|
+
toRedirectTo = (result.isRegister === true ? newUserURL ?? callbackURL : callbackURL).toString();
|
|
422
|
+
} catch {
|
|
423
|
+
toRedirectTo = result.isRegister === true ? newUserURL ?? callbackURL : callbackURL;
|
|
424
|
+
}
|
|
425
|
+
return ctx.redirect(toRedirectTo, 302);
|
|
426
|
+
};
|
|
427
|
+
const getOAuthCallbackRoute = createRoute({
|
|
428
|
+
operationId: Routes.OAUTH_CALLBACK_GET,
|
|
429
|
+
isAction: false,
|
|
430
|
+
method: "get",
|
|
431
|
+
path: "/callback/:id",
|
|
432
|
+
description: "Handle OAuth callback",
|
|
433
|
+
request: req().qry(oauthCallbackQuerySchema).prm(oauthCallbackParamsSchema).bld(),
|
|
434
|
+
responses: res().rdr("Redirect to success or error URL").err(400).bld()
|
|
435
|
+
});
|
|
436
|
+
const getOAuthCallback = createEndpoint(getOAuthCallbackRoute, (options) => async (ctx) => {
|
|
437
|
+
const query = ctx.req.valid("query");
|
|
438
|
+
const { id: paramsId } = ctx.req.valid("param");
|
|
439
|
+
return handleCallbackOAuth(ctx, paramsId, query, options);
|
|
440
|
+
});
|
|
441
|
+
const postOAuthCallbackRoute = createRoute({
|
|
442
|
+
operationId: Routes.OAUTH_CALLBACK_POST,
|
|
443
|
+
isAction: false,
|
|
444
|
+
method: "post",
|
|
445
|
+
path: "/callback/:id",
|
|
446
|
+
description: "Handle OAuth callback (POST)",
|
|
447
|
+
request: req().bdy(callbackSchema.optional()).prm(oauthCallbackParamsSchema).bld(),
|
|
448
|
+
responses: res().rdr("Redirect to success or error URL").err(400).zod().bld()
|
|
449
|
+
});
|
|
450
|
+
const postOAuthCallback = createEndpoint(postOAuthCallbackRoute, (options) => async (ctx) => {
|
|
451
|
+
const body = ctx.req.valid("json");
|
|
452
|
+
const { id: paramsId } = ctx.req.valid("param");
|
|
453
|
+
return handleCallbackOAuth(ctx, paramsId, body, options);
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
//#endregion
|
|
457
|
+
//#region src/api/routes/sign-out.ts
|
|
458
|
+
const signOutRoute = createRoute({
|
|
459
|
+
operationId: Routes.SIGN_OUT,
|
|
460
|
+
method: "post",
|
|
461
|
+
path: "/sign-out",
|
|
462
|
+
description: "Sign out the current user",
|
|
463
|
+
responses: res(SCHEMAS[Definitions.SUCCESS].default).err(400).bld()
|
|
464
|
+
});
|
|
465
|
+
const signOut = createEndpoint(signOutRoute, (_options) => async (ctx) => {
|
|
466
|
+
const context = ctx.get("context");
|
|
467
|
+
const sessionCookieToken = await (0, cookies_exports.getSignedCookie)(ctx, context.secret, context.authCookies.sessionToken.name);
|
|
468
|
+
if (sessionCookieToken == null || sessionCookieToken === false) {
|
|
469
|
+
deleteSessionCookie(ctx);
|
|
470
|
+
return ctx.render({
|
|
471
|
+
success: False,
|
|
472
|
+
message: BASE_ERROR_CODES.FAILED_TO_GET_SESSION
|
|
473
|
+
}, 400);
|
|
474
|
+
}
|
|
475
|
+
await context.internalAdapter.deleteSession(sessionCookieToken);
|
|
476
|
+
deleteSessionCookie(ctx);
|
|
477
|
+
return ctx.render({ success: True }, 200);
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
//#endregion
|
|
481
|
+
//#region src/api/schema/reset-password.ts
|
|
482
|
+
const requestPasswordResetSchema = z$1.object({
|
|
483
|
+
email: emailSchema.openapi({ description: "The email address of the user to send the password reset email to" }),
|
|
484
|
+
redirectTo: callbackURLSchema(true).openapi({ 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`" })
|
|
485
|
+
});
|
|
486
|
+
const requestPasswordResetCallbackParamsSchema = z$1.object({ token: z$1.string().openapi({ param: {
|
|
487
|
+
in: "path",
|
|
488
|
+
name: "token"
|
|
489
|
+
} }) });
|
|
490
|
+
const requestPasswordResetCallbackQuerySchema = z$1.object({ callbackURL: callbackURLSchema().openapi({
|
|
491
|
+
param: {
|
|
492
|
+
in: "query",
|
|
493
|
+
name: "callbackURL"
|
|
494
|
+
},
|
|
495
|
+
description: "The URL to redirect the user to reset their password"
|
|
496
|
+
}) });
|
|
497
|
+
const resetPasswordSchema = z$1.object({
|
|
498
|
+
newPassword: passwordSchema.openapi({ description: "The new password to set" }),
|
|
499
|
+
token: z$1.string().optional().openapi({ description: "The token to reset the password" })
|
|
500
|
+
});
|
|
501
|
+
const resetPasswordQuerySchema = z$1.object({ token: z$1.string().optional().openapi({ param: {
|
|
502
|
+
in: "query",
|
|
503
|
+
name: "token"
|
|
504
|
+
} }) });
|
|
505
|
+
|
|
506
|
+
//#endregion
|
|
507
|
+
//#region src/api/routes/reset-password.ts
|
|
508
|
+
const redirectError = (ctx, callbackURL, query) => {
|
|
509
|
+
const url = callbackURL != null ? new URL(callbackURL, ctx.get("context").baseURL) : new URL(`${ctx.get("context").baseURL}/error`);
|
|
510
|
+
if (query) Object.entries(query).forEach(([k, v]) => url.searchParams.set(k, v));
|
|
511
|
+
return url.href;
|
|
512
|
+
};
|
|
513
|
+
const redirectCallback = (ctx, callbackURL, query) => {
|
|
514
|
+
const url = new URL(callbackURL, ctx.get("context").baseURL);
|
|
515
|
+
if (query) Object.entries(query).forEach(([k, v]) => url.searchParams.set(k, v));
|
|
516
|
+
return url.href;
|
|
517
|
+
};
|
|
518
|
+
const requestPasswordResetRoute = createRoute({
|
|
519
|
+
operationId: Routes.REQUEST_PASSWORD_RESET,
|
|
520
|
+
method: "post",
|
|
521
|
+
path: "/request-password-reset",
|
|
522
|
+
description: "Send a password reset email to the user",
|
|
523
|
+
request: req().bdy(requestPasswordResetSchema).bld(),
|
|
524
|
+
responses: res(SCHEMAS[Definitions.SUCCESS].default).err(400, "Reset password not enabled in options").zod().bld()
|
|
525
|
+
});
|
|
526
|
+
const requestPasswordReset = createEndpoint(requestPasswordResetRoute, (options) => async (ctx) => {
|
|
527
|
+
const context = ctx.get("context");
|
|
528
|
+
if (!options.emailAndPassword?.sendResetPassword) {
|
|
529
|
+
context.logger.error("Reset password isn't enabled. Please add an emailAndPassword and sendResetPassword function to your auth config!");
|
|
530
|
+
return ctx.render({
|
|
531
|
+
success: False,
|
|
532
|
+
message: "Reset password isn't enabled"
|
|
533
|
+
}, 400);
|
|
534
|
+
}
|
|
535
|
+
const { email, redirectTo } = ctx.req.valid("json");
|
|
536
|
+
const user = await context.internalAdapter.findUserByEmail(email, { includeAccounts: true });
|
|
537
|
+
if (!user) {
|
|
538
|
+
context.logger.error("Reset Password: User not found", { email });
|
|
539
|
+
return ctx.render({
|
|
540
|
+
success: True,
|
|
541
|
+
message: "If this email exists in our system, check your email for the reset link"
|
|
542
|
+
}, 200);
|
|
543
|
+
}
|
|
544
|
+
const expiresAt = getDate(options.emailAndPassword.resetPasswordTokenExpiresIn ?? 3600 * 1, "sec");
|
|
545
|
+
const verificationToken = generateId(24);
|
|
546
|
+
await context.internalAdapter.createVerificationValue({
|
|
547
|
+
value: user.user.id,
|
|
548
|
+
identifier: `reset-password:${verificationToken}`,
|
|
549
|
+
expiresAt
|
|
550
|
+
});
|
|
551
|
+
const callbackURL = redirectTo != null ? encodeURIComponent(redirectTo) : void 0;
|
|
552
|
+
const url = `${context.baseURL}/reset-password/${verificationToken}${callbackURL != null ? `?callbackURL=${callbackURL}` : ""}`;
|
|
553
|
+
await options.emailAndPassword.sendResetPassword({
|
|
554
|
+
user: user.user,
|
|
555
|
+
url,
|
|
556
|
+
token: verificationToken
|
|
557
|
+
}, ctx);
|
|
558
|
+
return ctx.render({ success: True }, 200);
|
|
559
|
+
});
|
|
560
|
+
const requestPasswordResetCallbackRoute = createRoute({
|
|
561
|
+
operationId: Routes.REQUEST_PASSWORD_RESET_CALLBACK,
|
|
562
|
+
method: "get",
|
|
563
|
+
path: "/reset-password/:token",
|
|
564
|
+
description: "Redirects the user to the callback URL with the token",
|
|
565
|
+
middleware: [originCheck((ctx) => ctx.req.query("callbackURL"))],
|
|
566
|
+
request: req().prm(requestPasswordResetCallbackParamsSchema).qry(requestPasswordResetCallbackQuerySchema).bld(),
|
|
567
|
+
responses: res().rdr().bld()
|
|
568
|
+
});
|
|
569
|
+
const requestPasswordResetCallback = createEndpoint(requestPasswordResetCallbackRoute, (_options) => async (ctx) => {
|
|
570
|
+
const { token } = ctx.req.valid("param");
|
|
571
|
+
const { callbackURL } = ctx.req.valid("query");
|
|
572
|
+
if (!token || !callbackURL) return ctx.redirect(redirectError(ctx, callbackURL, { error: "INVALID_TOKEN" }), 302);
|
|
573
|
+
const verification = await ctx.get("context").internalAdapter.findVerificationValue(`reset-password:${token}`);
|
|
574
|
+
if (!verification || verification.expiresAt < /* @__PURE__ */ new Date()) return ctx.redirect(redirectError(ctx, callbackURL, { error: "INVALID_TOKEN" }), 302);
|
|
575
|
+
return ctx.redirect(redirectCallback(ctx, callbackURL, { token }), 302);
|
|
576
|
+
});
|
|
577
|
+
const resetPasswordRoute = createRoute({
|
|
578
|
+
operationId: Routes.RESET_PASSWORD,
|
|
579
|
+
method: "post",
|
|
580
|
+
path: "/reset-password",
|
|
581
|
+
description: "Reset the password for a user",
|
|
582
|
+
request: req().qry(resetPasswordQuerySchema).bdy(resetPasswordSchema).bld(),
|
|
583
|
+
responses: res(SCHEMAS[Definitions.SUCCESS].default).err(400, "Invalid token or password").zod().bld()
|
|
584
|
+
});
|
|
585
|
+
const resetPassword = createEndpoint(resetPasswordRoute, (options) => async (ctx) => {
|
|
586
|
+
const token = ctx.req.valid("json").token || ctx.req.valid("query").token;
|
|
587
|
+
if (token == null || token === "") return ctx.render({
|
|
588
|
+
success: False,
|
|
589
|
+
message: BASE_ERROR_CODES.INVALID_TOKEN
|
|
590
|
+
}, 400);
|
|
591
|
+
const { newPassword } = ctx.req.valid("json");
|
|
592
|
+
const context = ctx.get("context");
|
|
593
|
+
const minLength = context.password.config.minPasswordLength;
|
|
594
|
+
const maxLength = context.password.config.maxPasswordLength;
|
|
595
|
+
if (newPassword.length < minLength) return ctx.render({
|
|
596
|
+
success: False,
|
|
597
|
+
message: BASE_ERROR_CODES.PASSWORD_TOO_SHORT
|
|
598
|
+
}, 400);
|
|
599
|
+
if (newPassword.length > maxLength) return ctx.render({
|
|
600
|
+
success: False,
|
|
601
|
+
message: BASE_ERROR_CODES.PASSWORD_TOO_LONG
|
|
602
|
+
}, 400);
|
|
603
|
+
const id = `reset-password:${token}`;
|
|
604
|
+
const verification = await context.internalAdapter.findVerificationValue(id);
|
|
605
|
+
if (!verification || verification.expiresAt < /* @__PURE__ */ new Date()) return ctx.render({
|
|
606
|
+
success: False,
|
|
607
|
+
message: BASE_ERROR_CODES.INVALID_TOKEN
|
|
608
|
+
}, 400);
|
|
609
|
+
const userId = verification.value;
|
|
610
|
+
const hashedPassword = await context.password.hash(newPassword);
|
|
611
|
+
if (!(await context.internalAdapter.findAccounts(userId)).find((ac) => ac.providerId === "credential")) await context.internalAdapter.createAccount({
|
|
612
|
+
userId,
|
|
613
|
+
providerId: "credential",
|
|
614
|
+
password: hashedPassword,
|
|
615
|
+
accountId: userId
|
|
616
|
+
});
|
|
617
|
+
else await context.internalAdapter.updatePassword(userId, hashedPassword);
|
|
618
|
+
await context.internalAdapter.deleteVerificationValue(verification.id);
|
|
619
|
+
if (options.emailAndPassword?.onPasswordReset) {
|
|
620
|
+
const user = await context.internalAdapter.findUserById(userId);
|
|
621
|
+
if (user) await options.emailAndPassword.onPasswordReset({ user }, ctx.req);
|
|
622
|
+
}
|
|
623
|
+
if (options.emailAndPassword?.revokeSessionsOnPasswordReset) await context.internalAdapter.deleteSessions(userId);
|
|
624
|
+
return ctx.render({ success: True }, 200);
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
//#endregion
|
|
628
|
+
//#region src/api/schema/update-user.ts
|
|
629
|
+
const updateUserSchema = looseUserSchema.omit({
|
|
630
|
+
id: true,
|
|
631
|
+
createdAt: true,
|
|
632
|
+
updatedAt: true,
|
|
633
|
+
emailVerified: true,
|
|
634
|
+
email: true
|
|
635
|
+
});
|
|
636
|
+
const changePasswordSchema = z$1.object({
|
|
637
|
+
newPassword: passwordSchema.openapi({ description: "The new password to set" }),
|
|
638
|
+
currentPassword: passwordSchema.openapi({ description: "The current password" }),
|
|
639
|
+
revokeOtherSessions: z$1.boolean().optional().openapi({ description: "Revoke all other sessions" })
|
|
640
|
+
});
|
|
641
|
+
const setPasswordSchema = z$1.object({ newPassword: passwordSchema });
|
|
642
|
+
const deleteUserSchema = z$1.object({
|
|
643
|
+
callbackURL: callbackURLSchema(true).openapi({ description: "The callback URL to redirect to after the user is deleted" }),
|
|
644
|
+
password: passwordSchema.optional().openapi({ description: "The password of the user. If the password isn't provided, session freshness will be checked." }),
|
|
645
|
+
token: z$1.string().optional().openapi({ description: "The token to delete the user. If the token is provided, the user will be deleted" })
|
|
646
|
+
});
|
|
647
|
+
const deleteUserCallbackQuerySchema = z$1.object({
|
|
648
|
+
token: z$1.string().openapi({ param: {
|
|
649
|
+
in: "query",
|
|
650
|
+
name: "token"
|
|
651
|
+
} }),
|
|
652
|
+
callbackURL: callbackURLSchema(true).openapi({ param: {
|
|
653
|
+
in: "query",
|
|
654
|
+
name: "callbackURL"
|
|
655
|
+
} })
|
|
656
|
+
});
|
|
657
|
+
const changeEmailSchema = z$1.object({
|
|
658
|
+
newEmail: emailSchema.openapi({ description: "The new email to set" }),
|
|
659
|
+
callbackURL: callbackURLSchema(true).openapi({ description: "The URL to redirect to after email verification" })
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
//#endregion
|
|
663
|
+
//#region src/api/routes/update-user.ts
|
|
664
|
+
const updateUserRoute = createRoute({
|
|
665
|
+
operationId: Routes.UPDATE_USER,
|
|
666
|
+
method: "post",
|
|
667
|
+
path: "/update-user",
|
|
668
|
+
description: "Update the current user",
|
|
669
|
+
middleware: [sessionMiddleware()],
|
|
670
|
+
request: req().bdy(updateUserSchema).bld(),
|
|
671
|
+
responses: res(SCHEMAS[Definitions.SUCCESS].default).zod().err(500, "Failed to update user").bld()
|
|
672
|
+
});
|
|
673
|
+
const updateUser = createEndpoint(updateUserRoute, (options) => async (ctx) => {
|
|
674
|
+
const { name, image,...rest } = ctx.req.valid("json");
|
|
675
|
+
if (image === void 0 && name === void 0 && Object.keys(rest).length === 0) return ctx.render({ success: True }, 200);
|
|
676
|
+
const context = ctx.get("context");
|
|
677
|
+
const session = ctx.get("session");
|
|
678
|
+
const additionalFields = await parseUserInput(options, rest, "update");
|
|
679
|
+
if (additionalFields instanceof Response) return additionalFields;
|
|
680
|
+
const user = await context.internalAdapter.updateUser(session.user.id, {
|
|
681
|
+
...name != null && { name },
|
|
682
|
+
...image !== void 0 && { image },
|
|
683
|
+
...additionalFields
|
|
684
|
+
});
|
|
685
|
+
if (user == null) return ctx.render({
|
|
686
|
+
success: False,
|
|
687
|
+
message: BASE_ERROR_CODES.FAILED_TO_UPDATE_USER
|
|
688
|
+
}, 500);
|
|
689
|
+
/**
|
|
690
|
+
* Update the session cookie with the new user data
|
|
691
|
+
*/
|
|
692
|
+
await setSessionCookie(ctx, options, {
|
|
693
|
+
session: session.session,
|
|
694
|
+
user
|
|
695
|
+
});
|
|
696
|
+
return ctx.render({ success: True }, 200);
|
|
697
|
+
});
|
|
698
|
+
const changePasswordRoute = createRoute({
|
|
699
|
+
operationId: Routes.CHANGE_PASSWORD,
|
|
700
|
+
method: "post",
|
|
701
|
+
path: "/change-password",
|
|
702
|
+
description: "Change the password of the user",
|
|
703
|
+
middleware: [sensitiveSessionMiddleware],
|
|
704
|
+
request: req().bdy(changePasswordSchema).bld(),
|
|
705
|
+
responses: res(SCHEMAS[Definitions.SIGN_IN_UP].default).err(400, "Invalid password").err(404, "Account not found").zod().err(500, "Failed to get session").bld()
|
|
706
|
+
});
|
|
707
|
+
const changePassword = createEndpoint(changePasswordRoute, (options) => async (ctx) => {
|
|
708
|
+
const context = ctx.get("context");
|
|
709
|
+
const session = ctx.get("session");
|
|
710
|
+
const { newPassword, currentPassword, revokeOtherSessions: revokeOtherSessions$1 } = ctx.req.valid("json");
|
|
711
|
+
const { minPasswordLength } = context.password.config;
|
|
712
|
+
if (newPassword.length < minPasswordLength) {
|
|
713
|
+
context.logger.error("Password is too short");
|
|
714
|
+
return ctx.render({
|
|
715
|
+
success: False,
|
|
716
|
+
message: BASE_ERROR_CODES.PASSWORD_TOO_SHORT
|
|
717
|
+
}, 400);
|
|
718
|
+
}
|
|
719
|
+
const { maxPasswordLength } = context.password.config;
|
|
720
|
+
if (newPassword.length > maxPasswordLength) {
|
|
721
|
+
context.logger.error("Password is too long");
|
|
722
|
+
return ctx.render({
|
|
723
|
+
success: False,
|
|
724
|
+
message: BASE_ERROR_CODES.PASSWORD_TOO_LONG
|
|
725
|
+
}, 400);
|
|
726
|
+
}
|
|
727
|
+
const account = (await context.internalAdapter.findAccounts(session.user.id)).find((account$1) => account$1.providerId === "credential" && account$1.password != null);
|
|
728
|
+
if (account?.password == null) return ctx.render({
|
|
729
|
+
success: False,
|
|
730
|
+
message: BASE_ERROR_CODES.CREDENTIAL_ACCOUNT_NOT_FOUND
|
|
731
|
+
}, 404);
|
|
732
|
+
const passwordHash = await context.password.hash(newPassword);
|
|
733
|
+
if (!await context.password.verify({
|
|
734
|
+
hash: account.password,
|
|
735
|
+
password: currentPassword
|
|
736
|
+
})) return ctx.render({
|
|
737
|
+
success: False,
|
|
738
|
+
message: BASE_ERROR_CODES.INVALID_PASSWORD
|
|
739
|
+
}, 400);
|
|
740
|
+
await context.internalAdapter.updateAccount(account.id, { password: passwordHash });
|
|
741
|
+
let token = session.session.token;
|
|
742
|
+
if (revokeOtherSessions$1 === true) {
|
|
743
|
+
await context.internalAdapter.deleteSessions(session.user.id);
|
|
744
|
+
const newSession = await context.internalAdapter.createSession(session.user.id);
|
|
745
|
+
if (newSession == null) return ctx.render({
|
|
746
|
+
success: False,
|
|
747
|
+
message: BASE_ERROR_CODES.FAILED_TO_GET_SESSION
|
|
748
|
+
}, 500);
|
|
749
|
+
await setSessionCookie(ctx, options, {
|
|
750
|
+
session: newSession,
|
|
751
|
+
user: session.user
|
|
752
|
+
});
|
|
753
|
+
token = newSession.token;
|
|
754
|
+
}
|
|
755
|
+
return ctx.render({
|
|
756
|
+
redirect: False,
|
|
757
|
+
success: True,
|
|
758
|
+
token,
|
|
759
|
+
user: session.user
|
|
760
|
+
}, 200);
|
|
761
|
+
});
|
|
762
|
+
const setPasswordRoute = createRoute({
|
|
763
|
+
operationId: Routes.SET_PASSWORD,
|
|
764
|
+
hide: true,
|
|
765
|
+
SERVER_ONLY: true,
|
|
766
|
+
method: "post",
|
|
767
|
+
path: "/set-password",
|
|
768
|
+
middleware: [sensitiveSessionMiddleware],
|
|
769
|
+
request: req().bdy(setPasswordSchema).bld(),
|
|
770
|
+
responses: res(SCHEMAS[Definitions.SUCCESS].default).err(400, "Invalid password or user already has password").zod().bld()
|
|
771
|
+
});
|
|
772
|
+
const setPassword = createEndpoint(setPasswordRoute, (_options) => async (ctx) => {
|
|
773
|
+
const context = ctx.get("context");
|
|
774
|
+
const session = ctx.get("session");
|
|
775
|
+
const { newPassword } = ctx.req.valid("json");
|
|
776
|
+
const { minPasswordLength } = context.password.config;
|
|
777
|
+
if (newPassword.length < minPasswordLength) {
|
|
778
|
+
context.logger.error("Password is too short");
|
|
779
|
+
return ctx.render({
|
|
780
|
+
success: False,
|
|
781
|
+
message: BASE_ERROR_CODES.PASSWORD_TOO_SHORT
|
|
782
|
+
}, 400);
|
|
783
|
+
}
|
|
784
|
+
const { maxPasswordLength } = context.password.config;
|
|
785
|
+
if (newPassword.length > maxPasswordLength) {
|
|
786
|
+
context.logger.error("Password is too long");
|
|
787
|
+
return ctx.render({
|
|
788
|
+
success: False,
|
|
789
|
+
message: BASE_ERROR_CODES.PASSWORD_TOO_LONG
|
|
790
|
+
}, 400);
|
|
791
|
+
}
|
|
792
|
+
const account = (await context.internalAdapter.findAccounts(session.user.id)).find((account$1) => account$1.providerId === "credential" && account$1.password != null);
|
|
793
|
+
const passwordHash = await context.password.hash(newPassword);
|
|
794
|
+
if (!account) {
|
|
795
|
+
await context.internalAdapter.linkAccount({
|
|
796
|
+
userId: session.user.id,
|
|
797
|
+
providerId: "credential",
|
|
798
|
+
accountId: session.user.id,
|
|
799
|
+
password: passwordHash
|
|
800
|
+
});
|
|
801
|
+
return ctx.render({ success: True }, 200);
|
|
802
|
+
}
|
|
803
|
+
return ctx.render({
|
|
804
|
+
success: False,
|
|
805
|
+
message: "User already has a password"
|
|
806
|
+
}, 400);
|
|
807
|
+
});
|
|
808
|
+
const deleteUserRoute = createRoute({
|
|
809
|
+
operationId: Routes.DELETE_USER,
|
|
810
|
+
method: "post",
|
|
811
|
+
path: "/delete-user",
|
|
812
|
+
description: "Delete the user",
|
|
813
|
+
middleware: [sensitiveSessionMiddleware],
|
|
814
|
+
request: req().bdy(deleteUserSchema).bld(),
|
|
815
|
+
responses: res(SCHEMAS[Definitions.SUCCESS].default, "User deletion processed successfully").rdr("Redirect to callback URL").err(400, "Account not found or invalid password").err(404, "Delete user is disabled").zod().bld()
|
|
816
|
+
});
|
|
817
|
+
const deleteUser = createEndpoint(deleteUserRoute, (options) => async (ctx) => {
|
|
818
|
+
const context = ctx.get("context");
|
|
819
|
+
if (options.user?.deleteUser?.enabled !== true) {
|
|
820
|
+
context.logger.error("Delete user is disabled. Enable it in the options");
|
|
821
|
+
return ctx.render({
|
|
822
|
+
success: False,
|
|
823
|
+
message: "Delete user is disabled"
|
|
824
|
+
}, 404);
|
|
825
|
+
}
|
|
826
|
+
const session = ctx.get("session");
|
|
827
|
+
const api = ctx.get("api");
|
|
828
|
+
const { callbackURL, password, token } = ctx.req.valid("json");
|
|
829
|
+
let canDelete = false;
|
|
830
|
+
const account = (await context.internalAdapter.findAccounts(session.user.id)).find((account$1) => account$1.providerId === "credential" && account$1.password != null);
|
|
831
|
+
if (password != null) {
|
|
832
|
+
if (account?.password == null) return ctx.render({
|
|
833
|
+
success: False,
|
|
834
|
+
message: BASE_ERROR_CODES.CREDENTIAL_ACCOUNT_NOT_FOUND
|
|
835
|
+
}, 400);
|
|
836
|
+
if (!await context.password.verify({
|
|
837
|
+
hash: account.password,
|
|
838
|
+
password
|
|
839
|
+
})) return ctx.render({
|
|
840
|
+
success: False,
|
|
841
|
+
message: BASE_ERROR_CODES.INVALID_PASSWORD
|
|
842
|
+
}, 400);
|
|
843
|
+
canDelete = true;
|
|
844
|
+
}
|
|
845
|
+
if (token != null) return await api.deleteUserCallback({ query: { token } }, ctx).then((res$1) => res$1 instanceof Response ? res$1 : ctx.render(res$1, res$1.success === true ? 200 : "errors" in res$1 ? 422 : 404));
|
|
846
|
+
if (options.user?.deleteUser?.sendDeleteAccountVerification != null) {
|
|
847
|
+
if (account?.password != null && !canDelete) return ctx.render({
|
|
848
|
+
success: False,
|
|
849
|
+
message: BASE_ERROR_CODES.USER_ALREADY_HAS_PASSWORD
|
|
850
|
+
}, 400);
|
|
851
|
+
const token$1 = (0, crypto_exports.generateRandomString)(32, "0-9", "a-z");
|
|
852
|
+
await context.internalAdapter.createVerificationValue({
|
|
853
|
+
value: session.user.id,
|
|
854
|
+
identifier: `delete-account-${token$1}`,
|
|
855
|
+
expiresAt: new Date(Date.now() + (options.user.deleteUser.deleteTokenExpiresIn ?? 3600 * 24) * 1e3)
|
|
856
|
+
});
|
|
857
|
+
const url = `${context.baseURL}/delete-user/callback?token=${token$1}${callbackURL ? `&callbackURL=${callbackURL}` : ""}`;
|
|
858
|
+
await options.user.deleteUser.sendDeleteAccountVerification({
|
|
859
|
+
user: session.user,
|
|
860
|
+
url,
|
|
861
|
+
token: token$1
|
|
862
|
+
}, ctx);
|
|
863
|
+
return ctx.render({
|
|
864
|
+
success: True,
|
|
865
|
+
message: "Verification email sent"
|
|
866
|
+
}, 200);
|
|
867
|
+
}
|
|
868
|
+
if (!password && context.sessionConfig.freshAge !== 0) {
|
|
869
|
+
const currentAge = session.session.createdAt.getTime();
|
|
870
|
+
const freshAge = context.sessionConfig.freshAge * 1e3;
|
|
871
|
+
if (Date.now() - currentAge > freshAge * 1e3) return ctx.render({
|
|
872
|
+
success: False,
|
|
873
|
+
message: BASE_ERROR_CODES.SESSION_EXPIRED
|
|
874
|
+
}, 400);
|
|
875
|
+
canDelete = true;
|
|
876
|
+
}
|
|
877
|
+
if (!canDelete) return ctx.render({
|
|
878
|
+
success: False,
|
|
879
|
+
message: "User cannot be deleted. please provide a password or token"
|
|
880
|
+
}, 400);
|
|
881
|
+
if (options.user?.deleteUser?.beforeDelete) await options.user.deleteUser.beforeDelete(session.user, ctx.req);
|
|
882
|
+
await context.internalAdapter.deleteUser(session.user.id);
|
|
883
|
+
await context.internalAdapter.deleteSessions(session.user.id);
|
|
884
|
+
await context.internalAdapter.deleteAccounts(session.user.id);
|
|
885
|
+
deleteSessionCookie(ctx);
|
|
886
|
+
if (options.user?.deleteUser?.afterDelete) await options.user.deleteUser.afterDelete(session.user, ctx.req);
|
|
887
|
+
return ctx.render({
|
|
888
|
+
success: True,
|
|
889
|
+
message: "User deleted"
|
|
890
|
+
}, 200);
|
|
891
|
+
});
|
|
892
|
+
const deleteUserCallbackRoute = createRoute({
|
|
893
|
+
operationId: Routes.DELETE_USER_CALLBACK,
|
|
894
|
+
method: "get",
|
|
895
|
+
path: "/delete-user/callback",
|
|
896
|
+
description: "Callback to complete user deletion with verification token",
|
|
897
|
+
middleware: [originCheck((ctx) => ctx.req.query("callbackURL")), sessionMiddleware()],
|
|
898
|
+
request: req().qry(deleteUserCallbackQuerySchema).bld(),
|
|
899
|
+
responses: res(SCHEMAS[Definitions.SUCCESS].default, "User successfully deleted").rdr("Redirect to callback URL").err(404, "Invalid token or delete user is disabled").zod().bld()
|
|
900
|
+
});
|
|
901
|
+
const deleteUserCallback = createEndpoint(deleteUserCallbackRoute, (options) => async (ctx) => {
|
|
902
|
+
const context = ctx.get("context");
|
|
903
|
+
if (!(options.user?.deleteUser?.enabled ?? false)) {
|
|
904
|
+
context.logger.error("Delete user is disabled. Enable it in the options");
|
|
905
|
+
return ctx.render({
|
|
906
|
+
success: False,
|
|
907
|
+
message: "Delete user is disabled"
|
|
908
|
+
}, 404);
|
|
909
|
+
}
|
|
910
|
+
const session = ctx.get("session");
|
|
911
|
+
const { token: requestedToken, callbackURL } = ctx.req.valid("query");
|
|
912
|
+
const token = await context.internalAdapter.findVerificationValue(`delete-account-${requestedToken}`);
|
|
913
|
+
if (!token || token.expiresAt < /* @__PURE__ */ new Date()) {
|
|
914
|
+
context.logger.info(requestedToken);
|
|
915
|
+
return ctx.render({
|
|
916
|
+
success: False,
|
|
917
|
+
message: BASE_ERROR_CODES.INVALID_TOKEN
|
|
918
|
+
}, 404);
|
|
919
|
+
}
|
|
920
|
+
if (token.value !== session.user.id) return ctx.render({
|
|
921
|
+
success: False,
|
|
922
|
+
message: BASE_ERROR_CODES.INVALID_TOKEN
|
|
923
|
+
}, 404);
|
|
924
|
+
const { beforeDelete } = options.user?.deleteUser ?? {};
|
|
925
|
+
if (beforeDelete) await beforeDelete(session.user, ctx.req);
|
|
926
|
+
await context.internalAdapter.deleteUser(session.user.id);
|
|
927
|
+
await context.internalAdapter.deleteSessions(session.user.id);
|
|
928
|
+
await context.internalAdapter.deleteAccounts(session.user.id);
|
|
929
|
+
await context.internalAdapter.deleteVerificationValue(token.id);
|
|
930
|
+
deleteSessionCookie(ctx);
|
|
931
|
+
const { afterDelete } = options.user?.deleteUser ?? {};
|
|
932
|
+
if (afterDelete) await afterDelete(session.user, ctx.req);
|
|
933
|
+
if (callbackURL != null) return ctx.redirect(callbackURL);
|
|
934
|
+
return ctx.render({
|
|
935
|
+
success: True,
|
|
936
|
+
message: "User deleted"
|
|
937
|
+
}, 200);
|
|
938
|
+
});
|
|
939
|
+
const changeEmailRoute = createRoute({
|
|
940
|
+
operationId: Routes.CHANGE_EMAIL,
|
|
941
|
+
method: "post",
|
|
942
|
+
path: "/change-email",
|
|
943
|
+
middleware: [sensitiveSessionMiddleware],
|
|
944
|
+
request: req().bdy(changeEmailSchema).bld(),
|
|
945
|
+
responses: res(SCHEMAS[Definitions.SUCCESS].default, "Email change request processed successfully").err(400, "Change email is disabled or user already exists").zod().bld()
|
|
946
|
+
});
|
|
947
|
+
const changeEmail = createEndpoint(changeEmailRoute, (options) => async (ctx) => {
|
|
948
|
+
const context = ctx.get("context");
|
|
949
|
+
if (options.user?.changeEmail?.enabled !== true) {
|
|
950
|
+
context.logger.error("Change email is disabled.");
|
|
951
|
+
return ctx.render({
|
|
952
|
+
success: False,
|
|
953
|
+
message: "Change email is disabled"
|
|
954
|
+
}, 400);
|
|
955
|
+
}
|
|
956
|
+
const session = ctx.get("session");
|
|
957
|
+
let { callbackURL, newEmail } = ctx.req.valid("json");
|
|
958
|
+
newEmail = newEmail.toLowerCase();
|
|
959
|
+
if (newEmail === session.user.email) {
|
|
960
|
+
context.logger.error("Email is the same");
|
|
961
|
+
return ctx.render({
|
|
962
|
+
success: False,
|
|
963
|
+
message: "Email is the same"
|
|
964
|
+
}, 400);
|
|
965
|
+
}
|
|
966
|
+
if (await context.internalAdapter.findUserByEmail(newEmail)) {
|
|
967
|
+
context.logger.error("Email already exists");
|
|
968
|
+
return ctx.render({
|
|
969
|
+
success: False,
|
|
970
|
+
message: "Couldn't update your email"
|
|
971
|
+
}, 400);
|
|
972
|
+
}
|
|
973
|
+
/**
|
|
974
|
+
* If the email is not verified, we can update the email
|
|
975
|
+
*/
|
|
976
|
+
if (!session.user.emailVerified) {
|
|
977
|
+
if (await context.internalAdapter.findUserByEmail(newEmail)) return ctx.render({
|
|
978
|
+
success: False,
|
|
979
|
+
message: BASE_ERROR_CODES.USER_ALREADY_EXISTS
|
|
980
|
+
}, 400);
|
|
981
|
+
await context.internalAdapter.updateUserByEmail(session.user.email, {
|
|
982
|
+
email: newEmail,
|
|
983
|
+
emailVerified: false
|
|
984
|
+
});
|
|
985
|
+
await setSessionCookie(ctx, options, {
|
|
986
|
+
session: session.session,
|
|
987
|
+
user: {
|
|
988
|
+
...session.user,
|
|
989
|
+
email: newEmail
|
|
990
|
+
}
|
|
991
|
+
});
|
|
992
|
+
if (options.emailVerification?.sendVerificationEmail) {
|
|
993
|
+
const token$1 = await createEmailVerificationToken(context.secret, newEmail, void 0, options.emailVerification.expiresIn);
|
|
994
|
+
const url$1 = `${context.baseURL}/verify-email?token=${token$1}&callbackURL=${callbackURL ?? "/"}`;
|
|
995
|
+
await options.emailVerification.sendVerificationEmail({
|
|
996
|
+
user: session.user,
|
|
997
|
+
url: url$1,
|
|
998
|
+
token: token$1
|
|
999
|
+
}, ctx);
|
|
1000
|
+
}
|
|
1001
|
+
return ctx.render({ success: True }, 200);
|
|
1002
|
+
}
|
|
1003
|
+
/**
|
|
1004
|
+
* If the email is verified, we need to send a verification email
|
|
1005
|
+
*/
|
|
1006
|
+
if (options.user?.changeEmail?.sendChangeEmailVerification == null) {
|
|
1007
|
+
context.logger.error("Verification email isn't enabled.");
|
|
1008
|
+
return ctx.render({
|
|
1009
|
+
success: False,
|
|
1010
|
+
message: "Verification email isn't enabled"
|
|
1011
|
+
}, 400);
|
|
1012
|
+
}
|
|
1013
|
+
const token = await createEmailVerificationToken(context.secret, session.user.email, newEmail, options.emailVerification?.expiresIn);
|
|
1014
|
+
const url = `${context.baseURL}/verify-email?token=${token}&callbackURL=${callbackURL ?? "/"}`;
|
|
1015
|
+
await options.user.changeEmail.sendChangeEmailVerification({
|
|
1016
|
+
user: session.user,
|
|
1017
|
+
newEmail,
|
|
1018
|
+
url,
|
|
1019
|
+
token
|
|
1020
|
+
}, ctx);
|
|
1021
|
+
return ctx.render({ success: True }, 200);
|
|
1022
|
+
});
|
|
1023
|
+
|
|
1024
|
+
//#endregion
|
|
1025
|
+
//#region src/api/routes/error.ts
|
|
1026
|
+
const sanitize = (input) => input.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'");
|
|
1027
|
+
const html = (errorCode = "Unknown", errorDescription) => `<!DOCTYPE html>
|
|
1028
|
+
<html lang="en">
|
|
1029
|
+
<head>
|
|
1030
|
+
<meta charset="UTF-8">
|
|
1031
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1032
|
+
<title>Authentication Error</title>
|
|
1033
|
+
<style>
|
|
1034
|
+
:root {
|
|
1035
|
+
--bg-color: #f8f9fa;
|
|
1036
|
+
--text-color: #212529;
|
|
1037
|
+
--accent-color: #000000;
|
|
1038
|
+
--error-color: #dc3545;
|
|
1039
|
+
--border-color: #e9ecef;
|
|
1040
|
+
}
|
|
1041
|
+
body {
|
|
1042
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
1043
|
+
background-color: var(--bg-color);
|
|
1044
|
+
color: var(--text-color);
|
|
1045
|
+
display: flex;
|
|
1046
|
+
justify-content: center;
|
|
1047
|
+
align-items: center;
|
|
1048
|
+
height: 100vh;
|
|
1049
|
+
margin: 0;
|
|
1050
|
+
line-height: 1.5;
|
|
1051
|
+
}
|
|
1052
|
+
.error-container {
|
|
1053
|
+
background-color: #ffffff;
|
|
1054
|
+
border-radius: 12px;
|
|
1055
|
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
|
|
1056
|
+
padding: 2.5rem;
|
|
1057
|
+
text-align: center;
|
|
1058
|
+
max-width: 90%;
|
|
1059
|
+
width: 400px;
|
|
1060
|
+
}
|
|
1061
|
+
h1 {
|
|
1062
|
+
color: var(--error-color);
|
|
1063
|
+
font-size: 1.75rem;
|
|
1064
|
+
margin-bottom: 1rem;
|
|
1065
|
+
font-weight: 600;
|
|
1066
|
+
}
|
|
1067
|
+
p {
|
|
1068
|
+
margin-bottom: 1.5rem;
|
|
1069
|
+
color: #495057;
|
|
1070
|
+
}
|
|
1071
|
+
.btn {
|
|
1072
|
+
background-color: var(--accent-color);
|
|
1073
|
+
color: #ffffff;
|
|
1074
|
+
text-decoration: none;
|
|
1075
|
+
padding: 0.75rem 1.5rem;
|
|
1076
|
+
border-radius: 6px;
|
|
1077
|
+
transition: all 0.3s ease;
|
|
1078
|
+
display: inline-block;
|
|
1079
|
+
font-weight: 500;
|
|
1080
|
+
border: 2px solid var(--accent-color);
|
|
1081
|
+
}
|
|
1082
|
+
.btn:hover {
|
|
1083
|
+
background-color: #131721;
|
|
1084
|
+
}
|
|
1085
|
+
.error-code {
|
|
1086
|
+
font-size: 0.875rem;
|
|
1087
|
+
color: #6c757d;
|
|
1088
|
+
margin-top: 1.5rem;
|
|
1089
|
+
padding-top: 1.5rem;
|
|
1090
|
+
border-top: 1px solid var(--border-color);
|
|
1091
|
+
}
|
|
1092
|
+
.icon {
|
|
1093
|
+
font-size: 3rem;
|
|
1094
|
+
margin-bottom: 1rem;
|
|
1095
|
+
}
|
|
1096
|
+
.error-description {
|
|
1097
|
+
font-size: 0.95rem;
|
|
1098
|
+
color: #6c757d;
|
|
1099
|
+
margin: -1rem 0 1.5rem;
|
|
1100
|
+
}
|
|
1101
|
+
</style>
|
|
1102
|
+
</head>
|
|
1103
|
+
<body>
|
|
1104
|
+
<div class="error-container">
|
|
1105
|
+
<div class="icon">!</div>
|
|
1106
|
+
<h1>Faire Auth Error</h1>
|
|
1107
|
+
<p>We encountered an issue while processing your request. Please try again or contact the application owner if the problem persists.</p>
|
|
1108
|
+
${errorDescription ? `<div class="error-description">${sanitize(errorDescription)}</div>` : ""}
|
|
1109
|
+
<a href="/" id="returnLink" class="btn">Return to Application</a>
|
|
1110
|
+
<div class="error-code">Error Code: <span id="errorCode">${sanitize(errorCode)}</span></div>
|
|
1111
|
+
</div>
|
|
1112
|
+
</body>
|
|
1113
|
+
</html>`;
|
|
1114
|
+
const errorQuerySchema = z$1.object({
|
|
1115
|
+
error: z$1.string().optional(),
|
|
1116
|
+
error_description: z$1.string().optional()
|
|
1117
|
+
});
|
|
1118
|
+
const errorRoute = createRoute({
|
|
1119
|
+
operationId: Routes.ERROR,
|
|
1120
|
+
hide: true,
|
|
1121
|
+
isAction: false,
|
|
1122
|
+
method: "get",
|
|
1123
|
+
path: "/error",
|
|
1124
|
+
description: "Displays an error page",
|
|
1125
|
+
request: req().qry(errorQuerySchema).bld(),
|
|
1126
|
+
responses: res(z$1.string().openapi({ description: "The HTML content of the error page" }), "A HTML error page", "text/html").zod().bld()
|
|
1127
|
+
});
|
|
1128
|
+
const error = createEndpoint(errorRoute, (_options) => async (ctx) => {
|
|
1129
|
+
const { error: error$1, error_description: _error_description } = ctx.req.valid("query");
|
|
1130
|
+
return ctx.html(html(error$1), 200);
|
|
1131
|
+
});
|
|
1132
|
+
|
|
1133
|
+
//#endregion
|
|
1134
|
+
//#region src/api/routes/ok.ts
|
|
1135
|
+
const okRoute = createRoute({
|
|
1136
|
+
operationId: Routes.OK,
|
|
1137
|
+
hide: true,
|
|
1138
|
+
isAction: false,
|
|
1139
|
+
method: "get",
|
|
1140
|
+
path: "/ok",
|
|
1141
|
+
description: "Check if the API is working",
|
|
1142
|
+
responses: res(SCHEMAS[Definitions.SUCCESS].default, "API is working").bld()
|
|
1143
|
+
});
|
|
1144
|
+
const ok = createEndpoint(okRoute, (_options) => async (ctx) => ctx.render({ success: True }, 200));
|
|
1145
|
+
|
|
1146
|
+
//#endregion
|
|
1147
|
+
//#region src/api/schema/sign-up.ts
|
|
1148
|
+
const signUpEmailSchema = z$1.looseObject({
|
|
1149
|
+
name: nameSchema,
|
|
1150
|
+
email: emailSchema.openapi({ description: "The email of the user" }),
|
|
1151
|
+
password: passwordSchema.openapi({ description: "The password of the user" }),
|
|
1152
|
+
image: z$1.url().nullish().openapi({ description: "The profile image URL of the user" }),
|
|
1153
|
+
callbackURL: callbackURLSchema(true).openapi({ description: "The URL to use for email verification callback" }),
|
|
1154
|
+
rememberMe: z$1.boolean().default(true).openapi({ description: "If this is false, the session will not be remembered. Default is `true`." })
|
|
1155
|
+
});
|
|
1156
|
+
|
|
1157
|
+
//#endregion
|
|
1158
|
+
//#region src/api/routes/sign-up.ts
|
|
1159
|
+
const signUpEmailRoute = createRoute({
|
|
1160
|
+
operationId: Routes.SIGN_UP_EMAIL,
|
|
1161
|
+
method: "post",
|
|
1162
|
+
path: "/sign-up/email",
|
|
1163
|
+
description: "Sign up a user using email and password",
|
|
1164
|
+
request: req().bdy(signUpEmailSchema).bld(),
|
|
1165
|
+
responses: res(SCHEMAS[Definitions.SIGN_IN_UP].default, "Successfully created user").err(400, "Email and password sign up is not enabled, invalid password, or user already exists").zod().bld()
|
|
1166
|
+
});
|
|
1167
|
+
const signUpEmail = createEndpoint(signUpEmailRoute, (options) => async (ctx) => {
|
|
1168
|
+
if (!(options.emailAndPassword?.enabled ?? false) || options.emailAndPassword?.disableSignUp === true) return ctx.render({
|
|
1169
|
+
success: False,
|
|
1170
|
+
message: "Email and password sign up is not enabled"
|
|
1171
|
+
}, 400);
|
|
1172
|
+
const { name, email, password, image, callbackURL, rememberMe,...additionalFields } = ctx.req.valid("json");
|
|
1173
|
+
const context = ctx.get("context");
|
|
1174
|
+
const { minPasswordLength, maxPasswordLength } = context.password.config;
|
|
1175
|
+
if (password.length < minPasswordLength) {
|
|
1176
|
+
context.logger.error("Password is too short");
|
|
1177
|
+
return ctx.render({
|
|
1178
|
+
success: False,
|
|
1179
|
+
message: BASE_ERROR_CODES.PASSWORD_TOO_SHORT
|
|
1180
|
+
}, 400);
|
|
1181
|
+
}
|
|
1182
|
+
if (password.length > maxPasswordLength) {
|
|
1183
|
+
context.logger.error("Password is too long");
|
|
1184
|
+
return ctx.render({
|
|
1185
|
+
success: False,
|
|
1186
|
+
message: BASE_ERROR_CODES.PASSWORD_TOO_LONG
|
|
1187
|
+
}, 400);
|
|
1188
|
+
}
|
|
1189
|
+
return runWithTransaction(context.adapter, async () => {
|
|
1190
|
+
if ((await context.internalAdapter.findUserByEmail(email))?.user) {
|
|
1191
|
+
context.logger.info(`Sign-up attempt for existing email: ${email}`);
|
|
1192
|
+
return ctx.render({
|
|
1193
|
+
success: False,
|
|
1194
|
+
message: BASE_ERROR_CODES.USER_ALREADY_EXISTS
|
|
1195
|
+
}, 400);
|
|
1196
|
+
}
|
|
1197
|
+
const additionalData = await parseUserInput(options, additionalFields, "create");
|
|
1198
|
+
if (additionalData instanceof Response) return additionalData;
|
|
1199
|
+
/**
|
|
1200
|
+
* Hash the password
|
|
1201
|
+
*
|
|
1202
|
+
* This is done prior to creating the user
|
|
1203
|
+
* to ensure that any plugin that
|
|
1204
|
+
* may break the hashing should break
|
|
1205
|
+
* before the user is created.
|
|
1206
|
+
*/
|
|
1207
|
+
const hash = await context.password.hash(password);
|
|
1208
|
+
let createdUser;
|
|
1209
|
+
try {
|
|
1210
|
+
createdUser = await context.internalAdapter.createUser({
|
|
1211
|
+
email: email.toLowerCase(),
|
|
1212
|
+
name,
|
|
1213
|
+
image,
|
|
1214
|
+
...additionalData,
|
|
1215
|
+
emailVerified: false
|
|
1216
|
+
});
|
|
1217
|
+
} catch (e) {
|
|
1218
|
+
if (isDevelopment()) context.logger.error("Failed to create user", e);
|
|
1219
|
+
return ctx.render({
|
|
1220
|
+
success: False,
|
|
1221
|
+
message: BASE_ERROR_CODES.FAILED_TO_CREATE_USER
|
|
1222
|
+
}, 400);
|
|
1223
|
+
}
|
|
1224
|
+
if (createdUser == null) return ctx.render({
|
|
1225
|
+
success: False,
|
|
1226
|
+
message: BASE_ERROR_CODES.FAILED_TO_CREATE_USER
|
|
1227
|
+
}, 400);
|
|
1228
|
+
await context.internalAdapter.linkAccount({
|
|
1229
|
+
userId: createdUser.id,
|
|
1230
|
+
providerId: "credential",
|
|
1231
|
+
accountId: createdUser.id,
|
|
1232
|
+
password: hash
|
|
1233
|
+
});
|
|
1234
|
+
if (options.emailVerification?.sendOnSignUp === true || options.emailAndPassword?.requireEmailVerification === true) {
|
|
1235
|
+
const token = await createEmailVerificationToken(context.secret, createdUser.email, void 0, options.emailVerification?.expiresIn);
|
|
1236
|
+
const url = `${context.baseURL}/verify-email?token=${token}&callbackURL=${callbackURL ?? "/"}`;
|
|
1237
|
+
await options.emailVerification?.sendVerificationEmail?.({
|
|
1238
|
+
user: createdUser,
|
|
1239
|
+
url,
|
|
1240
|
+
token
|
|
1241
|
+
}, ctx);
|
|
1242
|
+
}
|
|
1243
|
+
if (options.emailAndPassword?.autoSignIn === false || options.emailAndPassword?.requireEmailVerification === true) return ctx.render({
|
|
1244
|
+
success: True,
|
|
1245
|
+
redirect: False,
|
|
1246
|
+
user: createdUser
|
|
1247
|
+
}, 200);
|
|
1248
|
+
const session = await context.internalAdapter.createSession(createdUser.id, rememberMe === false);
|
|
1249
|
+
if (session == null) return ctx.render({
|
|
1250
|
+
success: False,
|
|
1251
|
+
message: BASE_ERROR_CODES.FAILED_TO_CREATE_SESSION
|
|
1252
|
+
}, 400);
|
|
1253
|
+
await setSessionCookie(ctx, options, {
|
|
1254
|
+
session,
|
|
1255
|
+
user: createdUser
|
|
1256
|
+
}, rememberMe === false);
|
|
1257
|
+
return ctx.render({
|
|
1258
|
+
success: True,
|
|
1259
|
+
redirect: False,
|
|
1260
|
+
token: session.token,
|
|
1261
|
+
user: createdUser
|
|
1262
|
+
}, 200);
|
|
1263
|
+
});
|
|
1264
|
+
});
|
|
1265
|
+
|
|
1266
|
+
//#endregion
|
|
1267
|
+
//#region src/api/routes/account.ts
|
|
1268
|
+
const listAccountsRoute = createRoute({
|
|
1269
|
+
operationId: Routes.LIST_ACCOUNTS,
|
|
1270
|
+
method: "get",
|
|
1271
|
+
path: "/list-accounts",
|
|
1272
|
+
description: "List all accounts linked to the user",
|
|
1273
|
+
middleware: [sessionMiddleware()],
|
|
1274
|
+
responses: res(SCHEMAS[Definitions.ACCOUNTS_LIST].default).bld()
|
|
1275
|
+
});
|
|
1276
|
+
const listAccounts = createEndpoint(listAccountsRoute, (_options) => async (ctx) => {
|
|
1277
|
+
const context = ctx.get("context");
|
|
1278
|
+
const session = ctx.get("session");
|
|
1279
|
+
const accounts = await context.internalAdapter.findAccounts(session.user.id);
|
|
1280
|
+
return ctx.render(accounts.map((a) => ({
|
|
1281
|
+
id: a.id,
|
|
1282
|
+
providerId: a.providerId,
|
|
1283
|
+
createdAt: a.createdAt,
|
|
1284
|
+
updatedAt: a.updatedAt,
|
|
1285
|
+
accountId: a.accountId,
|
|
1286
|
+
scopes: a.scope ? a.scope.split(",") : []
|
|
1287
|
+
})), 200);
|
|
1288
|
+
});
|
|
1289
|
+
const linkAccountRoute = createRoute({
|
|
1290
|
+
operationId: Routes.LINK_ACCOUNT,
|
|
1291
|
+
method: "post",
|
|
1292
|
+
path: "/link-social",
|
|
1293
|
+
description: "Link a social account to the user",
|
|
1294
|
+
middleware: [sessionMiddleware()],
|
|
1295
|
+
request: req().bdy(linkAccountSchema).bld(),
|
|
1296
|
+
responses: res(SCHEMAS[Definitions.REDIRECT_URL].default).err(401, "Invalid token, failed to get user info, email not found, linking not allowed, or different emails not allowed").err(404, "Provider not found").err(417, "Unable to create account").zod().bld()
|
|
1297
|
+
});
|
|
1298
|
+
const linkAccount = createEndpoint(linkAccountRoute, (options) => async (ctx) => {
|
|
1299
|
+
const { scopes, provider: requestedProvider, idToken } = ctx.req.valid("json");
|
|
1300
|
+
const context = ctx.get("context");
|
|
1301
|
+
const session = ctx.get("session");
|
|
1302
|
+
const provider = context.socialProviders.find((p) => p.id === requestedProvider);
|
|
1303
|
+
if (!provider) {
|
|
1304
|
+
context.logger.error("Provider not found. Make sure to add the provider in your auth config", { provider: requestedProvider });
|
|
1305
|
+
return ctx.render({
|
|
1306
|
+
success: False,
|
|
1307
|
+
message: BASE_ERROR_CODES.PROVIDER_NOT_FOUND
|
|
1308
|
+
}, 404);
|
|
1309
|
+
}
|
|
1310
|
+
if (idToken) {
|
|
1311
|
+
if (!provider.verifyIdToken) {
|
|
1312
|
+
context.logger.error("Provider does not support id token verification", { provider: requestedProvider });
|
|
1313
|
+
return ctx.render({
|
|
1314
|
+
success: False,
|
|
1315
|
+
message: BASE_ERROR_CODES.ID_TOKEN_NOT_SUPPORTED
|
|
1316
|
+
}, 404);
|
|
1317
|
+
}
|
|
1318
|
+
const { token, nonce } = idToken;
|
|
1319
|
+
if (!await provider.verifyIdToken(token, nonce)) {
|
|
1320
|
+
context.logger.error("Invalid id token", { provider: requestedProvider });
|
|
1321
|
+
return ctx.render({
|
|
1322
|
+
success: False,
|
|
1323
|
+
message: BASE_ERROR_CODES.INVALID_TOKEN
|
|
1324
|
+
}, 401);
|
|
1325
|
+
}
|
|
1326
|
+
const linkingUserInfo = await provider.getUserInfo({
|
|
1327
|
+
idToken: token,
|
|
1328
|
+
accessToken: idToken.accessToken,
|
|
1329
|
+
refreshToken: idToken.refreshToken
|
|
1330
|
+
});
|
|
1331
|
+
if (!linkingUserInfo || !linkingUserInfo?.user) {
|
|
1332
|
+
context.logger.error("Failed to get user info", { provider: requestedProvider });
|
|
1333
|
+
return ctx.render({
|
|
1334
|
+
success: False,
|
|
1335
|
+
message: BASE_ERROR_CODES.FAILED_TO_GET_USER_INFO
|
|
1336
|
+
}, 401);
|
|
1337
|
+
}
|
|
1338
|
+
const linkingUserId = String(linkingUserInfo.user.id);
|
|
1339
|
+
if (!linkingUserInfo.user.email) {
|
|
1340
|
+
context.logger.error("User email not found", { provider: requestedProvider });
|
|
1341
|
+
return ctx.render({
|
|
1342
|
+
success: False,
|
|
1343
|
+
message: BASE_ERROR_CODES.USER_EMAIL_NOT_FOUND
|
|
1344
|
+
}, 401);
|
|
1345
|
+
}
|
|
1346
|
+
if ((await context.internalAdapter.findAccounts(session.user.id)).find((a) => a.providerId === provider.id && a.accountId === linkingUserId)) return ctx.render({ redirect: False }, 200);
|
|
1347
|
+
if (!(options.account?.accountLinking?.trustedProviders)?.includes(provider.id) && !linkingUserInfo.user.emailVerified || options.account?.accountLinking?.enabled === false) return ctx.render({
|
|
1348
|
+
success: False,
|
|
1349
|
+
message: "Account not linked - linking not allowed"
|
|
1350
|
+
}, 401);
|
|
1351
|
+
if (linkingUserInfo.user.email !== session.user.email && options.account?.accountLinking?.allowDifferentEmails !== true) return ctx.render({
|
|
1352
|
+
success: False,
|
|
1353
|
+
message: "Account not linked - different emails not allowed"
|
|
1354
|
+
}, 401);
|
|
1355
|
+
try {
|
|
1356
|
+
await context.internalAdapter.createAccount({
|
|
1357
|
+
userId: session.user.id,
|
|
1358
|
+
providerId: provider.id,
|
|
1359
|
+
accountId: linkingUserId,
|
|
1360
|
+
accessToken: idToken.accessToken,
|
|
1361
|
+
idToken: token,
|
|
1362
|
+
refreshToken: idToken.refreshToken,
|
|
1363
|
+
...idToken.scopes && idToken.scopes.length > 0 && { scope: idToken.scopes.join(",") }
|
|
1364
|
+
});
|
|
1365
|
+
} catch (e) {
|
|
1366
|
+
return ctx.render({
|
|
1367
|
+
success: False,
|
|
1368
|
+
message: "Account not linked - unable to create account"
|
|
1369
|
+
}, 417);
|
|
1370
|
+
}
|
|
1371
|
+
if (options.account?.accountLinking?.updateUserInfoOnLink === true && (linkingUserInfo.user?.name || linkingUserInfo.user?.image)) try {
|
|
1372
|
+
await context.internalAdapter.updateUser(session.user.id, {
|
|
1373
|
+
...linkingUserInfo.user?.name && { name: linkingUserInfo.user.name },
|
|
1374
|
+
...linkingUserInfo.user?.image && { image: linkingUserInfo.user.image }
|
|
1375
|
+
});
|
|
1376
|
+
} catch (e) {
|
|
1377
|
+
console.warn("Could not update user - " + e.toString());
|
|
1378
|
+
}
|
|
1379
|
+
return ctx.render({ redirect: False }, 200);
|
|
1380
|
+
}
|
|
1381
|
+
const state = await generateState(ctx, {
|
|
1382
|
+
userId: session.user.id,
|
|
1383
|
+
email: session.user.email
|
|
1384
|
+
}, options);
|
|
1385
|
+
const url = await provider.createAuthorizationURL({
|
|
1386
|
+
state: state.state,
|
|
1387
|
+
codeVerifier: state.codeVerifier,
|
|
1388
|
+
redirectURI: `${context.baseURL}/callback/${provider.id}`,
|
|
1389
|
+
...scopes && { scopes }
|
|
1390
|
+
});
|
|
1391
|
+
return ctx.render({
|
|
1392
|
+
redirect: True,
|
|
1393
|
+
url: url.toString()
|
|
1394
|
+
}, 200);
|
|
1395
|
+
});
|
|
1396
|
+
const unlinkAccountRoute = createRoute({
|
|
1397
|
+
operationId: Routes.UNLINK_ACCOUNT,
|
|
1398
|
+
method: "post",
|
|
1399
|
+
path: "/unlink-account",
|
|
1400
|
+
description: "Unlink an account",
|
|
1401
|
+
middleware: [freshSessionMiddleware],
|
|
1402
|
+
request: req().bdy(unlinkAccountSchema).bld(),
|
|
1403
|
+
responses: res(SCHEMAS[Definitions.SUCCESS].default).err(400, "Account not found or failed to unlink").zod().bld()
|
|
1404
|
+
});
|
|
1405
|
+
const unlinkAccount = createEndpoint(unlinkAccountRoute, (options) => async (ctx) => {
|
|
1406
|
+
const { providerId, accountId } = ctx.req.valid("json");
|
|
1407
|
+
const context = ctx.get("context");
|
|
1408
|
+
const session = ctx.get("session");
|
|
1409
|
+
const accounts = await context.internalAdapter.findAccounts(session.user.id);
|
|
1410
|
+
if (accounts.length === 1 && options.account?.accountLinking?.allowUnlinkingAll !== true) return ctx.render({
|
|
1411
|
+
success: False,
|
|
1412
|
+
message: BASE_ERROR_CODES.FAILED_TO_UNLINK_LAST_ACCOUNT
|
|
1413
|
+
}, 400);
|
|
1414
|
+
const accountExist = accounts.find((account) => accountId != null ? account.accountId === accountId && account.providerId === providerId : account.providerId === providerId);
|
|
1415
|
+
if (!accountExist) return ctx.render({
|
|
1416
|
+
success: False,
|
|
1417
|
+
message: BASE_ERROR_CODES.ACCOUNT_NOT_FOUND
|
|
1418
|
+
}, 400);
|
|
1419
|
+
await context.internalAdapter.deleteAccount(accountExist.id);
|
|
1420
|
+
return ctx.render({ success: True }, 200);
|
|
1421
|
+
});
|
|
1422
|
+
const getAccessTokenRoute = createRoute({
|
|
1423
|
+
operationId: Routes.GET_ACCESS_TOKEN,
|
|
1424
|
+
method: "post",
|
|
1425
|
+
path: "/get-access-token",
|
|
1426
|
+
description: "Get a valid access token, doing a refresh if needed",
|
|
1427
|
+
middleware: [requestOnlySessionMiddleware],
|
|
1428
|
+
request: req().bdy(getAccessTokenSchema).bld(),
|
|
1429
|
+
responses: res(SCHEMAS[Definitions.ACCESS_TOKEN].default, "A valid access token").err(400, "Invalid refresh token or provider configuration").zod().bld()
|
|
1430
|
+
});
|
|
1431
|
+
const getAccessToken = createEndpoint(getAccessTokenRoute, (options) => async (ctx) => {
|
|
1432
|
+
const { providerId, accountId, userId } = ctx.req.valid("json");
|
|
1433
|
+
if (userId && ctx.get("isServer") !== true) return ctx.render({ success: False }, 401);
|
|
1434
|
+
const resolvedUserId = ctx.get("session")?.user.id ?? userId;
|
|
1435
|
+
if (resolvedUserId == null) return ctx.render({
|
|
1436
|
+
success: False,
|
|
1437
|
+
message: `Either userId or session is required`
|
|
1438
|
+
}, 400);
|
|
1439
|
+
const context = ctx.get("context");
|
|
1440
|
+
if (!context.socialProviders.find((p) => p.id === providerId)) return ctx.render({
|
|
1441
|
+
success: False,
|
|
1442
|
+
message: `Provider ${providerId} is not supported.`
|
|
1443
|
+
}, 400);
|
|
1444
|
+
const account = (await context.internalAdapter.findAccounts(resolvedUserId)).find((acc) => accountId != null ? acc.id === accountId && acc.providerId === providerId : acc.providerId === providerId);
|
|
1445
|
+
if (!account) return ctx.render({
|
|
1446
|
+
success: False,
|
|
1447
|
+
message: "Account not found"
|
|
1448
|
+
}, 400);
|
|
1449
|
+
const provider = context.socialProviders.find((p) => p.id === providerId);
|
|
1450
|
+
if (!provider) return ctx.render({
|
|
1451
|
+
success: False,
|
|
1452
|
+
message: `Provider ${providerId} not found.`
|
|
1453
|
+
}, 400);
|
|
1454
|
+
try {
|
|
1455
|
+
let newTokens = null;
|
|
1456
|
+
const accessTokenExpired = account.accessTokenExpiresAt && new Date(account.accessTokenExpiresAt).getTime() - Date.now() < 5e3;
|
|
1457
|
+
if (account.refreshToken && accessTokenExpired && provider.refreshAccessToken) {
|
|
1458
|
+
newTokens = await provider.refreshAccessToken(account.refreshToken);
|
|
1459
|
+
await context.internalAdapter.updateAccount(account.id, {
|
|
1460
|
+
...newTokens.accessToken && {
|
|
1461
|
+
accessToken: await setTokenUtil(newTokens.accessToken, ctx, options),
|
|
1462
|
+
...newTokens.accessTokenExpiresAt && { accessTokenExpiresAt: newTokens.accessTokenExpiresAt }
|
|
1463
|
+
},
|
|
1464
|
+
...newTokens.refreshToken && {
|
|
1465
|
+
refreshToken: await setTokenUtil(newTokens.refreshToken, ctx, options),
|
|
1466
|
+
...newTokens.refreshTokenExpiresAt && { refreshTokenExpiresAt: newTokens.refreshTokenExpiresAt }
|
|
1467
|
+
}
|
|
1468
|
+
});
|
|
1469
|
+
}
|
|
1470
|
+
const tokens = {
|
|
1471
|
+
accessToken: await decryptOAuthToken(newTokens?.accessToken ?? account.accessToken ?? "", ctx, options),
|
|
1472
|
+
...newTokens?.accessTokenExpiresAt != null || account.accessTokenExpiresAt != null ? { accessTokenExpiresAt: newTokens?.accessTokenExpiresAt ?? account.accessTokenExpiresAt } : {},
|
|
1473
|
+
scopes: account.scope ? account.scope.split(",") : [],
|
|
1474
|
+
...(newTokens?.idToken != null && newTokens.idToken !== "" || account.idToken != null && account.idToken !== "") && { idToken: newTokens?.idToken || account.idToken }
|
|
1475
|
+
};
|
|
1476
|
+
return ctx.render(tokens, 200);
|
|
1477
|
+
} catch {
|
|
1478
|
+
return ctx.render({
|
|
1479
|
+
success: False,
|
|
1480
|
+
message: "Failed to get a valid access token"
|
|
1481
|
+
}, 400);
|
|
1482
|
+
}
|
|
1483
|
+
});
|
|
1484
|
+
const refreshAccessTokenRoute = createRoute({
|
|
1485
|
+
operationId: Routes.REFRESH_ACCESS_TOKEN,
|
|
1486
|
+
method: "post",
|
|
1487
|
+
path: "/refresh-token",
|
|
1488
|
+
description: "Refresh the access token using a refresh token",
|
|
1489
|
+
middleware: [sessionMiddleware()],
|
|
1490
|
+
request: req().bdy(refreshAccessTokenSchema).bld(),
|
|
1491
|
+
responses: res(SCHEMAS[Definitions.ACCESS_TOKEN].default, "Access token refreshed successfully").err(400, "Invalid refresh token or provider configuration").zod().bld()
|
|
1492
|
+
});
|
|
1493
|
+
const refreshAccessToken = createEndpoint(refreshAccessTokenRoute, (options) => async (ctx) => {
|
|
1494
|
+
const context = ctx.get("context");
|
|
1495
|
+
const session = ctx.get("session");
|
|
1496
|
+
const { providerId, accountId, userId } = ctx.req.valid("json");
|
|
1497
|
+
const resolvedUserId = userId ?? session.user.id;
|
|
1498
|
+
if (!context.socialProviders.find((p) => p.id === providerId)) return ctx.render({
|
|
1499
|
+
success: False,
|
|
1500
|
+
message: `Provider ${providerId} is not supported.`
|
|
1501
|
+
}, 400);
|
|
1502
|
+
const account = (await context.internalAdapter.findAccounts(resolvedUserId)).find((acc) => accountId != null ? acc.id === accountId && acc.providerId === providerId : acc.providerId === providerId);
|
|
1503
|
+
if (account?.refreshToken == null) return ctx.render({
|
|
1504
|
+
success: False,
|
|
1505
|
+
message: "Account not found"
|
|
1506
|
+
}, 400);
|
|
1507
|
+
const provider = context.socialProviders.find((p) => p.id === providerId);
|
|
1508
|
+
if (!provider) return ctx.render({
|
|
1509
|
+
success: False,
|
|
1510
|
+
message: `Provider ${providerId} not found.`
|
|
1511
|
+
}, 400);
|
|
1512
|
+
if (!provider.refreshAccessToken) return ctx.render({
|
|
1513
|
+
success: False,
|
|
1514
|
+
message: `Provider ${providerId} does not support token refreshing.`
|
|
1515
|
+
}, 400);
|
|
1516
|
+
try {
|
|
1517
|
+
const tokens = await provider.refreshAccessToken(account.refreshToken);
|
|
1518
|
+
await context.internalAdapter.updateAccount(account.id, {
|
|
1519
|
+
...tokens.accessToken && {
|
|
1520
|
+
accessToken: await setTokenUtil(tokens.accessToken, ctx, options),
|
|
1521
|
+
...tokens.accessTokenExpiresAt && { accessTokenExpiresAt: tokens.accessTokenExpiresAt }
|
|
1522
|
+
},
|
|
1523
|
+
...tokens.refreshToken && {
|
|
1524
|
+
refreshToken: await setTokenUtil(tokens.refreshToken, ctx, options),
|
|
1525
|
+
...tokens.refreshTokenExpiresAt && { refreshTokenExpiresAt: tokens.refreshTokenExpiresAt }
|
|
1526
|
+
}
|
|
1527
|
+
});
|
|
1528
|
+
return ctx.render(tokens, 200);
|
|
1529
|
+
} catch {
|
|
1530
|
+
return ctx.render({
|
|
1531
|
+
success: False,
|
|
1532
|
+
message: "Failed to refresh access token"
|
|
1533
|
+
}, 400);
|
|
1534
|
+
}
|
|
1535
|
+
});
|
|
1536
|
+
const getAccountInfoRoute = createRoute({
|
|
1537
|
+
operationId: Routes.GET_ACCOUNT_INFO,
|
|
1538
|
+
method: "post",
|
|
1539
|
+
path: "/account-info",
|
|
1540
|
+
description: "Get the account info provided by the provider",
|
|
1541
|
+
middleware: [sessionMiddleware()],
|
|
1542
|
+
request: req().bdy(getAccountInfoSchema).bld(),
|
|
1543
|
+
responses: res(SCHEMAS[Definitions.ACCOUNT_INFO].default).err(400, "Account not found").zod().err(500, "Provider not configured or failed to get user info").bld()
|
|
1544
|
+
});
|
|
1545
|
+
const getAccountInfo = createEndpoint(getAccountInfoRoute, (_options) => async (ctx) => {
|
|
1546
|
+
const { accountId } = ctx.req.valid("json");
|
|
1547
|
+
const context = ctx.get("context");
|
|
1548
|
+
const session = ctx.get("session");
|
|
1549
|
+
const api = ctx.get("api");
|
|
1550
|
+
const account = await context.internalAdapter.findAccount(accountId);
|
|
1551
|
+
if (!account || account.userId !== session.user.id) return ctx.render({
|
|
1552
|
+
success: False,
|
|
1553
|
+
message: "Account not found"
|
|
1554
|
+
}, 400);
|
|
1555
|
+
const provider = context.socialProviders.find((p) => p.id === account.providerId);
|
|
1556
|
+
if (!provider) return ctx.render({
|
|
1557
|
+
success: False,
|
|
1558
|
+
message: `Provider account provider is ${account.providerId} but it is not configured`
|
|
1559
|
+
}, 500);
|
|
1560
|
+
const tokensResponse = await api.getAccessToken({ json: {
|
|
1561
|
+
accountId: account.id,
|
|
1562
|
+
providerId: account.providerId,
|
|
1563
|
+
userId: account.userId
|
|
1564
|
+
} }, ctx);
|
|
1565
|
+
if (tokensResponse.success === false) return ctx.render(tokensResponse, 500);
|
|
1566
|
+
const info = await provider.getUserInfo({
|
|
1567
|
+
...tokensResponse.data,
|
|
1568
|
+
...tokensResponse.data.accessTokenExpiresAt != null && { accessTokenExpiresAt: new Date(tokensResponse.data.accessTokenExpiresAt) },
|
|
1569
|
+
...tokensResponse.data.refreshTokenExpiresAt != null && { refreshTokenExpiresAt: new Date(tokensResponse.data.refreshTokenExpiresAt) }
|
|
1570
|
+
});
|
|
1571
|
+
if (info == null) return ctx.render({
|
|
1572
|
+
success: False,
|
|
1573
|
+
message: "Failed to get user info"
|
|
1574
|
+
}, 500);
|
|
1575
|
+
return ctx.render(info, 200);
|
|
1576
|
+
});
|
|
1577
|
+
|
|
1578
|
+
//#endregion
|
|
1579
|
+
//#region src/api/endpoints.ts
|
|
1580
|
+
const staticRouteMap = {
|
|
1581
|
+
[Routes.SET_PASSWORD]: setPassword,
|
|
1582
|
+
[Routes.CHANGE_EMAIL]: changeEmail,
|
|
1583
|
+
[Routes.CHANGE_PASSWORD]: changePassword,
|
|
1584
|
+
[Routes.DELETE_USER]: deleteUser,
|
|
1585
|
+
[Routes.DELETE_USER_CALLBACK]: deleteUserCallback,
|
|
1586
|
+
[Routes.ERROR]: error,
|
|
1587
|
+
[Routes.GET_ACCESS_TOKEN]: getAccessToken,
|
|
1588
|
+
[Routes.GET_ACCOUNT_INFO]: getAccountInfo,
|
|
1589
|
+
[Routes.GET_SESSION]: getSession,
|
|
1590
|
+
[Routes.LINK_ACCOUNT]: linkAccount,
|
|
1591
|
+
[Routes.LIST_ACCOUNTS]: listAccounts,
|
|
1592
|
+
[Routes.LIST_SESSIONS]: listSessions,
|
|
1593
|
+
[Routes.OAUTH_CALLBACK_GET]: getOAuthCallback,
|
|
1594
|
+
[Routes.OAUTH_CALLBACK_POST]: postOAuthCallback,
|
|
1595
|
+
[Routes.OK]: ok,
|
|
1596
|
+
[Routes.REFRESH_ACCESS_TOKEN]: refreshAccessToken,
|
|
1597
|
+
[Routes.REQUEST_PASSWORD_RESET]: requestPasswordReset,
|
|
1598
|
+
[Routes.REQUEST_PASSWORD_RESET_CALLBACK]: requestPasswordResetCallback,
|
|
1599
|
+
[Routes.RESET_PASSWORD]: resetPassword,
|
|
1600
|
+
[Routes.REVOKE_OTHER_SESSIONS]: revokeOtherSessions,
|
|
1601
|
+
[Routes.REVOKE_SESSION]: revokeSession,
|
|
1602
|
+
[Routes.REVOKE_SESSIONS]: revokeSessions,
|
|
1603
|
+
[Routes.SEND_VERIFICATION_EMAIL]: sendVerificationEmail,
|
|
1604
|
+
[Routes.SIGN_IN_EMAIL]: signInEmail,
|
|
1605
|
+
[Routes.SIGN_IN_SOCIAL]: signInSocial,
|
|
1606
|
+
[Routes.SIGN_OUT]: signOut,
|
|
1607
|
+
[Routes.SIGN_UP_EMAIL]: signUpEmail,
|
|
1608
|
+
[Routes.UNLINK_ACCOUNT]: unlinkAccount,
|
|
1609
|
+
[Routes.UPDATE_USER]: updateUser,
|
|
1610
|
+
[Routes.VERIFY_EMAIL]: verifyEmail
|
|
1611
|
+
};
|
|
1612
|
+
const resolveHooks = (options) => {
|
|
1613
|
+
const routeHooks = { ...options.routeHooks };
|
|
1614
|
+
options.plugins?.forEach((p) => {
|
|
1615
|
+
if (p.routeHooks) Object.assign(routeHooks, p.routeHooks);
|
|
1616
|
+
});
|
|
1617
|
+
return routeHooks;
|
|
1618
|
+
};
|
|
1619
|
+
const resolveRoutes = (options) => {
|
|
1620
|
+
const routeMap = { ...staticRouteMap };
|
|
1621
|
+
options.plugins?.forEach((p) => {
|
|
1622
|
+
if (p.routes) Object.assign(routeMap, p.routes);
|
|
1623
|
+
});
|
|
1624
|
+
return routeMap;
|
|
1625
|
+
};
|
|
1626
|
+
const createEndpoints = (options) => {
|
|
1627
|
+
const routeMap = resolveRoutes(options);
|
|
1628
|
+
const routeHooks = resolveHooks(options);
|
|
1629
|
+
return {
|
|
1630
|
+
endpoints: Object.fromEntries(Object.entries(routeMap).map(([k, v]) => [k, v(options)])),
|
|
1631
|
+
hooks: routeHooks
|
|
1632
|
+
};
|
|
1633
|
+
};
|
|
1634
|
+
const createArgs = (endpoints, builtSchemas, context, routeHooks) => {
|
|
1635
|
+
const groups = {
|
|
1636
|
+
priv: [],
|
|
1637
|
+
pub: []
|
|
1638
|
+
};
|
|
1639
|
+
for (const [k, v] of Object.entries(endpoints)) {
|
|
1640
|
+
const args = v.toArgs(builtSchemas, context, endpoints, routeHooks[k]);
|
|
1641
|
+
groups[args[0].SERVER_ONLY === true ? "priv" : "pub"].push(args);
|
|
1642
|
+
}
|
|
1643
|
+
return groups;
|
|
1644
|
+
};
|
|
1645
|
+
|
|
1646
|
+
//#endregion
|
|
1647
|
+
//#region src/api/middleware/intercept.ts
|
|
1648
|
+
const runIntercept = (ctx, hooks) => {
|
|
1649
|
+
let response = void 0;
|
|
1650
|
+
for (const hook of hooks) try {
|
|
1651
|
+
const result = hook(ctx);
|
|
1652
|
+
if (result instanceof Response) response = result;
|
|
1653
|
+
} catch (error$1) {
|
|
1654
|
+
ctx.error = error$1;
|
|
1655
|
+
}
|
|
1656
|
+
return response;
|
|
1657
|
+
};
|
|
1658
|
+
const getHooks = (options) => {
|
|
1659
|
+
const beforeHooks = [];
|
|
1660
|
+
const afterHooks = [];
|
|
1661
|
+
options.plugins?.forEach((p) => {
|
|
1662
|
+
if (p.onRequest) beforeHooks.push(p.onRequest);
|
|
1663
|
+
if (p.onResponse) afterHooks.push(p.onResponse);
|
|
1664
|
+
});
|
|
1665
|
+
return {
|
|
1666
|
+
beforeHooks,
|
|
1667
|
+
afterHooks
|
|
1668
|
+
};
|
|
1669
|
+
};
|
|
1670
|
+
/**
|
|
1671
|
+
* Initializes the intercept middleware.
|
|
1672
|
+
* @template O - The options type.
|
|
1673
|
+
* @param options - The FaireAuth options.
|
|
1674
|
+
* @returns The middleware function.
|
|
1675
|
+
*/
|
|
1676
|
+
const initInterceptMiddleware = (options) => {
|
|
1677
|
+
const { beforeHooks, afterHooks } = getHooks(options);
|
|
1678
|
+
return createMiddleware()(async (ctx, next) => {
|
|
1679
|
+
const before = runIntercept(ctx, beforeHooks);
|
|
1680
|
+
if (before instanceof Response) return before;
|
|
1681
|
+
await next();
|
|
1682
|
+
const after = runIntercept(ctx, afterHooks);
|
|
1683
|
+
if (after instanceof Response) ctx.res = after;
|
|
1684
|
+
});
|
|
1685
|
+
};
|
|
1686
|
+
|
|
1687
|
+
//#endregion
|
|
1688
|
+
//#region src/api/middleware/on-disabled.ts
|
|
1689
|
+
/**
|
|
1690
|
+
* Initializes the middleware to handle disabled paths.
|
|
1691
|
+
* @param options - The FaireAuth options.
|
|
1692
|
+
* @returns The middleware function.
|
|
1693
|
+
*/
|
|
1694
|
+
const initHandleDisabledMiddleware = (options) => {
|
|
1695
|
+
let disabledPaths = null;
|
|
1696
|
+
return createMiddleware()(async (ctx, next) => {
|
|
1697
|
+
disabledPaths ??= options.disabledPaths ?? [];
|
|
1698
|
+
if (disabledPaths.includes(ctx.get("path"))) return ctx.notFound();
|
|
1699
|
+
return await next();
|
|
1700
|
+
});
|
|
1701
|
+
};
|
|
1702
|
+
|
|
1703
|
+
//#endregion
|
|
1704
|
+
//#region src/api/middleware/rate-limit.ts
|
|
1705
|
+
const shouldRateLimit = (max, window, rateLimitData) => {
|
|
1706
|
+
const now = Date.now();
|
|
1707
|
+
const windowInMs = window * 1e3;
|
|
1708
|
+
return now - rateLimitData.lastRequest < windowInMs && rateLimitData.count >= max;
|
|
1709
|
+
};
|
|
1710
|
+
const rateLimitResponse = (ctx, retryAfter) => {
|
|
1711
|
+
ctx.header("X-Retry-After", retryAfter.toString());
|
|
1712
|
+
return ctx.json({
|
|
1713
|
+
status: False,
|
|
1714
|
+
message: "Too many requests. Please try again later."
|
|
1715
|
+
}, 429);
|
|
1716
|
+
};
|
|
1717
|
+
const getRetryAfter = (lastRequest, window) => {
|
|
1718
|
+
const now = Date.now();
|
|
1719
|
+
const windowInMs = window * 1e3;
|
|
1720
|
+
return Math.ceil((lastRequest + windowInMs - now) / 1e3);
|
|
1721
|
+
};
|
|
1722
|
+
const createDBStorage = (ctx) => {
|
|
1723
|
+
const model = "rateLimit";
|
|
1724
|
+
const db = ctx.adapter;
|
|
1725
|
+
return {
|
|
1726
|
+
get: async (key) => {
|
|
1727
|
+
const res$1 = db.findMany({
|
|
1728
|
+
model,
|
|
1729
|
+
where: [{
|
|
1730
|
+
field: "key",
|
|
1731
|
+
value: key
|
|
1732
|
+
}]
|
|
1733
|
+
});
|
|
1734
|
+
const data = isPromise(res$1) ? (await res$1)[0] : res$1[0];
|
|
1735
|
+
if (typeof data?.lastRequest === "bigint") data.lastRequest = Number(data.lastRequest);
|
|
1736
|
+
return data;
|
|
1737
|
+
},
|
|
1738
|
+
set: async (key, value, _update) => {
|
|
1739
|
+
try {
|
|
1740
|
+
if (_update === true) {
|
|
1741
|
+
const res$1 = db.updateMany({
|
|
1742
|
+
model: "rateLimit",
|
|
1743
|
+
where: [{
|
|
1744
|
+
field: "key",
|
|
1745
|
+
value: key
|
|
1746
|
+
}],
|
|
1747
|
+
update: {
|
|
1748
|
+
count: value.count,
|
|
1749
|
+
lastRequest: value.lastRequest
|
|
1750
|
+
}
|
|
1751
|
+
});
|
|
1752
|
+
if (isPromise(res$1)) await res$1;
|
|
1753
|
+
} else {
|
|
1754
|
+
const res$1 = await db.create({
|
|
1755
|
+
model: "rateLimit",
|
|
1756
|
+
data: {
|
|
1757
|
+
key,
|
|
1758
|
+
count: value.count,
|
|
1759
|
+
lastRequest: value.lastRequest
|
|
1760
|
+
}
|
|
1761
|
+
});
|
|
1762
|
+
if (isPromise(res$1)) await res$1;
|
|
1763
|
+
}
|
|
1764
|
+
} catch (e) {
|
|
1765
|
+
ctx.logger.error("Error setting rate limit", e);
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
};
|
|
1769
|
+
};
|
|
1770
|
+
const memory = /* @__PURE__ */ new Map();
|
|
1771
|
+
/**
|
|
1772
|
+
* Gets the rate limit storage based on the configuration.
|
|
1773
|
+
* @param context - The auth context with rateLimit, adapter, and logger.
|
|
1774
|
+
* @param options - The FaireAuth options with rateLimit and secondaryStorage.
|
|
1775
|
+
* @returns The storage object with get and set methods.
|
|
1776
|
+
*/
|
|
1777
|
+
const getRateLimitStorage = (config, context, options) => {
|
|
1778
|
+
if (options.rateLimit?.customStorage) return options.rateLimit.customStorage;
|
|
1779
|
+
if (config.storage === "secondary-storage") return {
|
|
1780
|
+
get: (key) => {
|
|
1781
|
+
const res$1 = options.secondaryStorage?.get(key);
|
|
1782
|
+
return isPromise(res$1) ? res$1.then((r) => r ? safeJSONParse(r) : void 0) : res$1 ? safeJSONParse(res$1) : void 0;
|
|
1783
|
+
},
|
|
1784
|
+
set: (key, value) => options.secondaryStorage?.set(key, JSON.stringify(value))
|
|
1785
|
+
};
|
|
1786
|
+
if (config.storage === "memory") return {
|
|
1787
|
+
get: (key) => memory.get(key),
|
|
1788
|
+
set: (key, value, _update) => memory.set(key, value)
|
|
1789
|
+
};
|
|
1790
|
+
return createDBStorage(context);
|
|
1791
|
+
};
|
|
1792
|
+
const getDefaultSpecialRules = () => [{
|
|
1793
|
+
pathMatcher: (path) => path.startsWith("/sign-in") || path.startsWith("/sign-up") || path.startsWith("/change-password") || path.startsWith("/change-email"),
|
|
1794
|
+
window: 10,
|
|
1795
|
+
max: 3
|
|
1796
|
+
}];
|
|
1797
|
+
/**
|
|
1798
|
+
* Initializes the rate limit middleware.
|
|
1799
|
+
* @param options - The FaireAuth options.
|
|
1800
|
+
* @returns The middleware function.
|
|
1801
|
+
*/
|
|
1802
|
+
const initRateLimitMiddleware = (options, rateLimit) => {
|
|
1803
|
+
const specialRules = getDefaultSpecialRules();
|
|
1804
|
+
return createMiddleware()(async (ctx, next) => {
|
|
1805
|
+
if (!rateLimit.enabled) return await next();
|
|
1806
|
+
const ip = getIp(ctx.req, options);
|
|
1807
|
+
if (!ip) return await next();
|
|
1808
|
+
const context = ctx.get("context");
|
|
1809
|
+
const path = ctx.get("path");
|
|
1810
|
+
const key = ip + path;
|
|
1811
|
+
let window = rateLimit.window;
|
|
1812
|
+
let max = rateLimit.max;
|
|
1813
|
+
const specialRule = specialRules.find((rule) => rule.pathMatcher(path));
|
|
1814
|
+
if (specialRule) {
|
|
1815
|
+
window = specialRule.window;
|
|
1816
|
+
max = specialRule.max;
|
|
1817
|
+
}
|
|
1818
|
+
for (const plugin of options.plugins ?? []) if (plugin.rateLimit) {
|
|
1819
|
+
const matchedRule = plugin.rateLimit.find((rule) => rule.pathMatcher(path));
|
|
1820
|
+
if (matchedRule) {
|
|
1821
|
+
window = matchedRule.window;
|
|
1822
|
+
max = matchedRule.max;
|
|
1823
|
+
break;
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
if (rateLimit.customRules) {
|
|
1827
|
+
const _path = Object.keys(rateLimit.customRules).find((p) => p.includes("*") ? wildcardMatch(p)(path) : p === path);
|
|
1828
|
+
if (_path) {
|
|
1829
|
+
const customRule = rateLimit.customRules[_path];
|
|
1830
|
+
const res$2 = typeof customRule === "function" ? customRule(ctx.req) : customRule;
|
|
1831
|
+
const resolved = isPromise(res$2) ? await res$2 : res$2;
|
|
1832
|
+
if (resolved) {
|
|
1833
|
+
window = resolved.window;
|
|
1834
|
+
max = resolved.max;
|
|
1835
|
+
}
|
|
1836
|
+
if (resolved === false) return await next();
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
const storage = getRateLimitStorage(rateLimit, context, options);
|
|
1840
|
+
const res$1 = storage.get(key);
|
|
1841
|
+
const now = Date.now();
|
|
1842
|
+
const data = isPromise(res$1) ? await res$1 : res$1;
|
|
1843
|
+
if (!data) {
|
|
1844
|
+
const res2 = storage.set(key, {
|
|
1845
|
+
key,
|
|
1846
|
+
count: 1,
|
|
1847
|
+
lastRequest: now
|
|
1848
|
+
});
|
|
1849
|
+
if (isPromise(res2)) await res2;
|
|
1850
|
+
} else {
|
|
1851
|
+
const timeSinceLastRequest = now - data.lastRequest;
|
|
1852
|
+
if (shouldRateLimit(max, window, data)) return rateLimitResponse(ctx, getRetryAfter(data.lastRequest, window));
|
|
1853
|
+
if (timeSinceLastRequest > window * 1e3) {
|
|
1854
|
+
const res3 = storage.set(key, {
|
|
1855
|
+
...data,
|
|
1856
|
+
count: 1,
|
|
1857
|
+
lastRequest: now
|
|
1858
|
+
}, true);
|
|
1859
|
+
if (isPromise(res3)) await res3;
|
|
1860
|
+
} else {
|
|
1861
|
+
const res3 = storage.set(key, {
|
|
1862
|
+
...data,
|
|
1863
|
+
count: data.count + 1,
|
|
1864
|
+
lastRequest: now
|
|
1865
|
+
}, true);
|
|
1866
|
+
if (isPromise(res3)) await res3;
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
return await next();
|
|
1870
|
+
});
|
|
1871
|
+
};
|
|
1872
|
+
|
|
1873
|
+
//#endregion
|
|
1874
|
+
//#region src/api/index.ts
|
|
1875
|
+
const router = (context, options) => {
|
|
1876
|
+
const schemas = buildSchemas(SCHEMAS, options);
|
|
1877
|
+
const { endpoints, hooks } = createEndpoints(options);
|
|
1878
|
+
const { pub } = createArgs(endpoints, schemas, context, hooks);
|
|
1879
|
+
const api = createAPI(endpoints);
|
|
1880
|
+
let app = new OpenAPIHono(options).openapi(...pub[0]).openapi(...pub[1]).openapi(...pub[2]).openapi(...pub[3]).openapi(...pub[4]).openapi(...pub[5]).openapi(...pub[6]).openapi(...pub[7]).openapi(...pub[8]).openapi(...pub[9]).openapi(...pub[10]).openapi(...pub[11]).openapi(...pub[12]).openapi(...pub[13]).openapi(...pub[14]).openapi(...pub[15]).openapi(...pub[16]).openapi(...pub[17]).openapi(...pub[18]).openapi(...pub[19]).openapi(...pub[20]).openapi(...pub[21]).openapi(...pub[22]).openapi(...pub[23]).openapi(...pub[24]).openapi(...pub[25]).openapi(...pub[26]).openapi(...pub[27]).openapi(...pub[28]);
|
|
1881
|
+
pub.slice(29).forEach((route) => app = app.openapi(...route));
|
|
1882
|
+
return {
|
|
1883
|
+
api,
|
|
1884
|
+
app: new Hono({
|
|
1885
|
+
router: new RegExpRouter(),
|
|
1886
|
+
...options.hono?.init
|
|
1887
|
+
}).basePath(options.basePath).use(setDefaultExecutionCtx, setRenderer, initContextMiddleware(options, context, endpoints), contextStorage(), initHandleDisabledMiddleware(options), initInterceptMiddleware(options), initRateLimitMiddleware(options, context.rateLimit), initOriginCheckMiddleware(options, context), initHooksMiddleware(options)).onError(initErrorHandler(options)).route("/", app)
|
|
1888
|
+
};
|
|
1889
|
+
};
|
|
1890
|
+
|
|
1891
|
+
//#endregion
|
|
1892
|
+
export { router as t };
|