better-auth 1.6.15 → 1.6.17
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/dist/api/index.d.mts +2 -2
- package/dist/api/index.mjs +3 -4
- package/dist/api/middlewares/origin-check.mjs +6 -1
- package/dist/api/rate-limiter/index.mjs +259 -73
- package/dist/api/routes/account.mjs +31 -11
- package/dist/api/routes/callback.mjs +3 -3
- package/dist/api/routes/index.d.mts +1 -1
- package/dist/api/routes/password.mjs +3 -4
- package/dist/api/routes/session.d.mts +12 -1
- package/dist/api/routes/session.mjs +16 -2
- package/dist/api/routes/sign-in.mjs +5 -5
- package/dist/api/routes/sign-up.mjs +2 -2
- package/dist/api/routes/update-session.mjs +9 -4
- package/dist/api/routes/update-user.mjs +10 -12
- package/dist/auth/base.mjs +11 -7
- package/dist/client/equality.d.mts +19 -0
- package/dist/client/equality.mjs +42 -0
- package/dist/client/index.d.mts +5 -4
- package/dist/client/index.mjs +2 -1
- package/dist/client/lynx/index.d.mts +6 -5
- package/dist/client/path-to-object.d.mts +5 -2
- package/dist/client/plugins/index.d.mts +4 -1
- package/dist/client/plugins/index.mjs +4 -1
- package/dist/client/query.d.mts +4 -3
- package/dist/client/query.mjs +27 -17
- package/dist/client/react/index.d.mts +6 -5
- package/dist/client/session-atom.mjs +129 -4
- package/dist/client/session-refresh.d.mts +3 -18
- package/dist/client/session-refresh.mjs +38 -49
- package/dist/client/solid/index.d.mts +6 -5
- package/dist/client/svelte/index.d.mts +6 -5
- package/dist/client/types.d.mts +2 -2
- package/dist/client/vanilla.d.mts +6 -5
- package/dist/client/vue/index.d.mts +6 -5
- package/dist/context/create-context.mjs +3 -2
- package/dist/context/store-capabilities.mjs +12 -0
- package/dist/cookies/index.mjs +30 -2
- package/dist/db/internal-adapter.mjs +56 -0
- package/dist/oauth2/link-account.d.mts +13 -0
- package/dist/oauth2/link-account.mjs +1 -1
- package/dist/package.mjs +1 -1
- package/dist/plugins/access/access.mjs +49 -19
- package/dist/plugins/admin/access/statement.d.mts +10 -10
- package/dist/plugins/admin/access/statement.mjs +2 -0
- package/dist/plugins/admin/admin.d.mts +6 -3
- package/dist/plugins/admin/client.d.mts +6 -4
- package/dist/plugins/admin/error-codes.d.mts +2 -0
- package/dist/plugins/admin/error-codes.mjs +3 -1
- package/dist/plugins/admin/routes.mjs +73 -5
- package/dist/plugins/admin/schema.d.mts +1 -0
- package/dist/plugins/admin/schema.mjs +2 -1
- package/dist/plugins/captcha/constants.mjs +8 -1
- package/dist/plugins/captcha/index.mjs +8 -2
- package/dist/plugins/captcha/types.d.mts +21 -0
- package/dist/plugins/captcha/verify-handlers/captchafox.mjs +2 -0
- package/dist/plugins/captcha/verify-handlers/cloudflare-turnstile.mjs +7 -2
- package/dist/plugins/captcha/verify-handlers/google-recaptcha.mjs +7 -2
- package/dist/plugins/captcha/verify-handlers/h-captcha.mjs +2 -0
- package/dist/plugins/device-authorization/routes.mjs +16 -9
- package/dist/plugins/email-otp/routes.mjs +23 -53
- package/dist/plugins/generic-oauth/index.mjs +7 -2
- package/dist/plugins/generic-oauth/routes.mjs +20 -9
- package/dist/plugins/haveibeenpwned/index.d.mts +1 -1
- package/dist/plugins/haveibeenpwned/index.mjs +5 -1
- package/dist/plugins/index.d.mts +5 -1
- package/dist/plugins/index.mjs +4 -1
- package/dist/plugins/jwt/index.mjs +2 -2
- package/dist/plugins/mcp/client/index.mjs +1 -0
- package/dist/plugins/mcp/index.mjs +8 -0
- package/dist/plugins/multi-session/index.mjs +7 -5
- package/dist/plugins/oauth-popup/client.d.mts +82 -0
- package/dist/plugins/oauth-popup/client.mjs +203 -0
- package/dist/plugins/oauth-popup/constants.d.mts +11 -0
- package/dist/plugins/oauth-popup/constants.mjs +11 -0
- package/dist/plugins/oauth-popup/error-codes.d.mts +11 -0
- package/dist/plugins/oauth-popup/error-codes.mjs +10 -0
- package/dist/plugins/oauth-popup/index.d.mts +67 -0
- package/dist/plugins/oauth-popup/index.mjs +227 -0
- package/dist/plugins/oauth-popup/types.d.mts +30 -0
- package/dist/plugins/oauth-proxy/index.mjs +2 -2
- package/dist/plugins/oauth-proxy/utils.mjs +16 -2
- package/dist/plugins/oidc-provider/index.mjs +10 -0
- package/dist/plugins/one-tap/client.mjs +12 -6
- package/dist/plugins/one-tap/index.d.mts +1 -0
- package/dist/plugins/one-tap/index.mjs +9 -5
- package/dist/plugins/one-time-token/index.mjs +1 -3
- package/dist/plugins/open-api/generator.mjs +7 -4
- package/dist/plugins/organization/adapter.d.mts +29 -1
- package/dist/plugins/organization/adapter.mjs +66 -6
- package/dist/plugins/organization/organization.mjs +2 -0
- package/dist/plugins/organization/routes/crud-invites.mjs +55 -31
- package/dist/plugins/organization/routes/crud-members.mjs +42 -6
- package/dist/plugins/organization/routes/crud-team.mjs +51 -5
- package/dist/plugins/organization/schema.d.mts +2 -0
- package/dist/plugins/phone-number/routes.mjs +41 -36
- package/dist/plugins/siwe/index.mjs +30 -3
- package/dist/plugins/siwe/parse-message.mjs +60 -0
- package/dist/plugins/two-factor/backup-codes/index.mjs +1 -1
- package/dist/plugins/two-factor/index.mjs +9 -1
- package/dist/plugins/two-factor/otp/index.mjs +11 -13
- package/dist/plugins/two-factor/totp/index.mjs +1 -1
- package/dist/plugins/two-factor/verify-two-factor.mjs +6 -2
- package/dist/plugins/username/index.mjs +6 -6
- package/dist/test-utils/test-instance.d.mts +6 -5
- package/package.json +10 -10
package/dist/api/index.d.mts
CHANGED
|
@@ -11,7 +11,7 @@ import { createEmailVerificationToken, sendVerificationEmail, sendVerificationEm
|
|
|
11
11
|
import { error } from "./routes/error.mjs";
|
|
12
12
|
import { ok } from "./routes/ok.mjs";
|
|
13
13
|
import { requestPasswordReset, requestPasswordResetCallback, resetPassword, verifyPassword } from "./routes/password.mjs";
|
|
14
|
-
import { freshSessionMiddleware, getSession, getSessionFromCtx, listSessions, requestOnlySessionMiddleware, revokeOtherSessions, revokeSession, revokeSessions, sensitiveSessionMiddleware, sessionMiddleware } from "./routes/session.mjs";
|
|
14
|
+
import { freshSessionMiddleware, getSession, getSessionFromCtx, isStateful, listSessions, requestOnlySessionMiddleware, revokeOtherSessions, revokeSession, revokeSessions, sensitiveSessionMiddleware, sessionMiddleware } from "./routes/session.mjs";
|
|
15
15
|
import { signInEmail, signInSocial } from "./routes/sign-in.mjs";
|
|
16
16
|
import { signOut } from "./routes/sign-out.mjs";
|
|
17
17
|
import { signUpEmail } from "./routes/sign-up.mjs";
|
|
@@ -3961,4 +3961,4 @@ declare const router: <Option extends BetterAuthOptions>(ctx: AuthContext, optio
|
|
|
3961
3961
|
} extends infer T_2 ? { [K in keyof T_2 as K extends keyof T_1 ? never : K]: T_2[K] } : never) & T_1> : never : never : never;
|
|
3962
3962
|
};
|
|
3963
3963
|
//#endregion
|
|
3964
|
-
export { APIError, type AuthEndpoint, type AuthMiddleware, type DispatchContext, accountInfo, callbackOAuth, changeEmail, changePassword, checkEndpointConflicts, createAuthEndpoint, createAuthMiddleware, createEmailVerificationToken, deleteUser, deleteUserCallback, dispatchAuthEndpoint, error, formCsrfMiddleware, freshSessionMiddleware, getAccessToken, getEndpoints, getIp, getOAuthState, getSession, getSessionFromCtx, getShouldSkipSessionRefresh, isAPIError, linkSocialAccount, listSessions, listUserAccounts, ok, optionsMiddleware, originCheck, originCheckMiddleware, refreshToken, requestOnlySessionMiddleware, requestPasswordReset, requestPasswordResetCallback, requireOrgRole, requireResourceOwnership, resetPassword, revokeOtherSessions, revokeSession, revokeSessions, router, sendVerificationEmail, sendVerificationEmailFn, sensitiveSessionMiddleware, sessionMiddleware, setPassword, setShouldSkipSessionRefresh, signInEmail, signInSocial, signOut, signUpEmail, unlinkAccount, updateSession, updateUser, verifyEmail, verifyPassword };
|
|
3964
|
+
export { APIError, type AuthEndpoint, type AuthMiddleware, type DispatchContext, accountInfo, callbackOAuth, changeEmail, changePassword, checkEndpointConflicts, createAuthEndpoint, createAuthMiddleware, createEmailVerificationToken, deleteUser, deleteUserCallback, dispatchAuthEndpoint, error, formCsrfMiddleware, freshSessionMiddleware, getAccessToken, getEndpoints, getIp, getOAuthState, getSession, getSessionFromCtx, getShouldSkipSessionRefresh, isAPIError, isStateful, linkSocialAccount, listSessions, listUserAccounts, ok, optionsMiddleware, originCheck, originCheckMiddleware, refreshToken, requestOnlySessionMiddleware, requestPasswordReset, requestPasswordResetCallback, requireOrgRole, requireResourceOwnership, resetPassword, revokeOtherSessions, revokeSession, revokeSessions, router, sendVerificationEmail, sendVerificationEmailFn, sensitiveSessionMiddleware, sessionMiddleware, setPassword, setShouldSkipSessionRefresh, signInEmail, signInSocial, signOut, signUpEmail, unlinkAccount, updateSession, updateUser, verifyEmail, verifyPassword };
|
package/dist/api/index.mjs
CHANGED
|
@@ -2,10 +2,10 @@ import { isAPIError } from "../utils/is-api-error.mjs";
|
|
|
2
2
|
import { requireOrgRole, requireResourceOwnership } from "./middlewares/authorization.mjs";
|
|
3
3
|
import { formCsrfMiddleware, originCheck, originCheckMiddleware } from "./middlewares/origin-check.mjs";
|
|
4
4
|
import { getIp } from "../utils/get-request-ip.mjs";
|
|
5
|
-
import { onRequestRateLimit
|
|
5
|
+
import { onRequestRateLimit } from "./rate-limiter/index.mjs";
|
|
6
6
|
import { getOAuthState } from "./state/oauth.mjs";
|
|
7
7
|
import { getShouldSkipSessionRefresh, setShouldSkipSessionRefresh } from "./state/should-session-refresh.mjs";
|
|
8
|
-
import { freshSessionMiddleware, getSession, getSessionFromCtx, listSessions, requestOnlySessionMiddleware, revokeOtherSessions, revokeSession, revokeSessions, sensitiveSessionMiddleware, sessionMiddleware } from "./routes/session.mjs";
|
|
8
|
+
import { freshSessionMiddleware, getSession, getSessionFromCtx, isStateful, listSessions, requestOnlySessionMiddleware, revokeOtherSessions, revokeSession, revokeSessions, sensitiveSessionMiddleware, sessionMiddleware } from "./routes/session.mjs";
|
|
9
9
|
import { accountInfo, getAccessToken, linkSocialAccount, listUserAccounts, refreshToken, unlinkAccount } from "./routes/account.mjs";
|
|
10
10
|
import { callbackOAuth } from "./routes/callback.mjs";
|
|
11
11
|
import { createEmailVerificationToken, sendVerificationEmail, sendVerificationEmailFn, verifyEmail } from "./routes/email-verification.mjs";
|
|
@@ -178,7 +178,6 @@ const router = (ctx, options) => {
|
|
|
178
178
|
return currentRequest;
|
|
179
179
|
},
|
|
180
180
|
async onResponse(res, req) {
|
|
181
|
-
await onResponseRateLimit(req, ctx);
|
|
182
181
|
for (const plugin of ctx.options.plugins || []) if (plugin.onResponse) {
|
|
183
182
|
const response = await withSpan(`onResponse ${plugin.id}`, {
|
|
184
183
|
[ATTR_HOOK_TYPE]: "onResponse",
|
|
@@ -214,4 +213,4 @@ const router = (ctx, options) => {
|
|
|
214
213
|
});
|
|
215
214
|
};
|
|
216
215
|
//#endregion
|
|
217
|
-
export { APIError, accountInfo, callbackOAuth, changeEmail, changePassword, checkEndpointConflicts, createAuthEndpoint, createAuthMiddleware, createEmailVerificationToken, deleteUser, deleteUserCallback, dispatchAuthEndpoint, error, formCsrfMiddleware, freshSessionMiddleware, getAccessToken, getEndpoints, getIp, getOAuthState, getSession, getSessionFromCtx, getShouldSkipSessionRefresh, isAPIError, linkSocialAccount, listSessions, listUserAccounts, ok, optionsMiddleware, originCheck, originCheckMiddleware, refreshToken, requestOnlySessionMiddleware, requestPasswordReset, requestPasswordResetCallback, requireOrgRole, requireResourceOwnership, resetPassword, revokeOtherSessions, revokeSession, revokeSessions, router, sendVerificationEmail, sendVerificationEmailFn, sensitiveSessionMiddleware, sessionMiddleware, setPassword, setShouldSkipSessionRefresh, signInEmail, signInSocial, signOut, signUpEmail, unlinkAccount, updateSession, updateUser, verifyEmail, verifyPassword };
|
|
216
|
+
export { APIError, accountInfo, callbackOAuth, changeEmail, changePassword, checkEndpointConflicts, createAuthEndpoint, createAuthMiddleware, createEmailVerificationToken, deleteUser, deleteUserCallback, dispatchAuthEndpoint, error, formCsrfMiddleware, freshSessionMiddleware, getAccessToken, getEndpoints, getIp, getOAuthState, getSession, getSessionFromCtx, getShouldSkipSessionRefresh, isAPIError, isStateful, linkSocialAccount, listSessions, listUserAccounts, ok, optionsMiddleware, originCheck, originCheckMiddleware, refreshToken, requestOnlySessionMiddleware, requestPasswordReset, requestPasswordResetCallback, requireOrgRole, requireResourceOwnership, resetPassword, revokeOtherSessions, revokeSession, revokeSessions, router, sendVerificationEmail, sendVerificationEmailFn, sensitiveSessionMiddleware, sessionMiddleware, setPassword, setShouldSkipSessionRefresh, signInEmail, signInSocial, signOut, signUpEmail, unlinkAccount, updateSession, updateUser, verifyEmail, verifyPassword };
|
|
@@ -23,7 +23,10 @@ function shouldSkipOriginCheck(ctx) {
|
|
|
23
23
|
if (Array.isArray(skipOriginCheck) && ctx.request) try {
|
|
24
24
|
const basePath = new URL(ctx.context.baseURL).pathname;
|
|
25
25
|
const currentPath = normalizePathname(ctx.request.url, basePath);
|
|
26
|
-
return skipOriginCheck.some((skipPath) =>
|
|
26
|
+
return skipOriginCheck.some((skipPath) => {
|
|
27
|
+
const normalizedSkipPath = skipPath.replace(/\/+$/, "");
|
|
28
|
+
return currentPath === normalizedSkipPath || currentPath.startsWith(`${normalizedSkipPath}/`);
|
|
29
|
+
});
|
|
27
30
|
} catch {}
|
|
28
31
|
return false;
|
|
29
32
|
}
|
|
@@ -47,6 +50,7 @@ const originCheckMiddleware = createAuthMiddleware(async (ctx) => {
|
|
|
47
50
|
const newUserCallbackURL = body?.newUserCallbackURL;
|
|
48
51
|
const validateURL = (url, label) => {
|
|
49
52
|
if (!url) return;
|
|
53
|
+
if (typeof url !== "string") throw APIError.fromStatus("BAD_REQUEST", { message: `Invalid ${label}: expected a string` });
|
|
50
54
|
if (!ctx.context.isTrustedOrigin(url, { allowRelativePaths: label !== "origin" })) {
|
|
51
55
|
ctx.context.logger.error(`Invalid ${label}: ${url}`);
|
|
52
56
|
ctx.context.logger.info(`If it's a valid URL, please add ${url} to trustedOrigins in your auth config\n`, `Current list of trustedOrigins: ${ctx.context.trustedOrigins}`);
|
|
@@ -141,6 +145,7 @@ async function validateFormCsrf(ctx) {
|
|
|
141
145
|
}
|
|
142
146
|
return await validateOrigin(ctx, true);
|
|
143
147
|
}
|
|
148
|
+
if (headers.get("origin") || headers.get("referer")) return await validateOrigin(ctx, true);
|
|
144
149
|
}
|
|
145
150
|
//#endregion
|
|
146
151
|
export { formCsrfMiddleware, originCheck, originCheckMiddleware };
|
|
@@ -5,10 +5,62 @@ import { normalizePathname } from "@better-auth/core/utils/url";
|
|
|
5
5
|
import { createRateLimitKey } from "@better-auth/core/utils/ip";
|
|
6
6
|
//#region src/api/rate-limiter/index.ts
|
|
7
7
|
const memory = /* @__PURE__ */ new Map();
|
|
8
|
-
|
|
8
|
+
const MEMORY_STORE_MAX_ENTRIES = 1e5;
|
|
9
|
+
function pruneMemoryStore() {
|
|
9
10
|
const now = Date.now();
|
|
10
|
-
const
|
|
11
|
-
|
|
11
|
+
for (const [key, entry] of memory) if (now >= entry.expiresAt) memory.delete(key);
|
|
12
|
+
if (memory.size <= MEMORY_STORE_MAX_ENTRIES) return;
|
|
13
|
+
const overflow = memory.size - MEMORY_STORE_MAX_ENTRIES;
|
|
14
|
+
let removed = 0;
|
|
15
|
+
for (const key of memory.keys()) {
|
|
16
|
+
memory.delete(key);
|
|
17
|
+
if (++removed >= overflow) break;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Decide an atomic rate-limit step against an in-memory `RateLimit` snapshot
|
|
22
|
+
* for the rolling `window` (seconds) and `max`. Shared by the memory backend
|
|
23
|
+
* (read-decide-write is atomic under single-threaded JS) and as the fallback
|
|
24
|
+
* for storages lacking an atomic primitive.
|
|
25
|
+
*/
|
|
26
|
+
function decideConsume(data, rule, now) {
|
|
27
|
+
const windowInMs = rule.window * 1e3;
|
|
28
|
+
if (!data) return {
|
|
29
|
+
next: {
|
|
30
|
+
key: "",
|
|
31
|
+
count: 1,
|
|
32
|
+
lastRequest: now
|
|
33
|
+
},
|
|
34
|
+
update: false,
|
|
35
|
+
allowed: true,
|
|
36
|
+
retryAfter: null
|
|
37
|
+
};
|
|
38
|
+
if (now - data.lastRequest > windowInMs) return {
|
|
39
|
+
next: {
|
|
40
|
+
...data,
|
|
41
|
+
count: 1,
|
|
42
|
+
lastRequest: now
|
|
43
|
+
},
|
|
44
|
+
update: true,
|
|
45
|
+
allowed: true,
|
|
46
|
+
retryAfter: null
|
|
47
|
+
};
|
|
48
|
+
if (data.count >= rule.max) return {
|
|
49
|
+
next: data,
|
|
50
|
+
update: true,
|
|
51
|
+
allowed: false,
|
|
52
|
+
retryAfter: getRetryAfter(data.lastRequest, rule.window)
|
|
53
|
+
};
|
|
54
|
+
return {
|
|
55
|
+
next: {
|
|
56
|
+
...data,
|
|
57
|
+
count: data.count + 1,
|
|
58
|
+
lastRequest: now
|
|
59
|
+
},
|
|
60
|
+
update: true,
|
|
61
|
+
allowed: true,
|
|
62
|
+
retryAfter: null
|
|
63
|
+
};
|
|
12
64
|
}
|
|
13
65
|
function rateLimitResponse(retryAfter) {
|
|
14
66
|
return new Response(JSON.stringify({ message: "Too many requests. Please try again later." }), {
|
|
@@ -25,18 +77,109 @@ function getRetryAfter(lastRequest, window) {
|
|
|
25
77
|
function createDatabaseStorageWrapper(ctx) {
|
|
26
78
|
const model = "rateLimit";
|
|
27
79
|
const db = ctx.adapter;
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
80
|
+
const readRow = async (key) => {
|
|
81
|
+
const data = (await db.findMany({
|
|
82
|
+
model,
|
|
83
|
+
where: [{
|
|
84
|
+
field: "key",
|
|
85
|
+
value: key
|
|
86
|
+
}]
|
|
87
|
+
}))[0];
|
|
88
|
+
if (typeof data?.lastRequest === "bigint") data.lastRequest = Number(data.lastRequest);
|
|
89
|
+
return data;
|
|
90
|
+
};
|
|
91
|
+
const consume = async (key, rule) => {
|
|
92
|
+
const windowInMs = rule.window * 1e3;
|
|
93
|
+
const data = await readRow(key);
|
|
94
|
+
const now = Date.now();
|
|
95
|
+
if (!data) try {
|
|
96
|
+
await db.create({
|
|
97
|
+
model,
|
|
98
|
+
data: {
|
|
99
|
+
key,
|
|
100
|
+
count: 1,
|
|
101
|
+
lastRequest: now
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
return {
|
|
105
|
+
allowed: true,
|
|
106
|
+
retryAfter: null
|
|
107
|
+
};
|
|
108
|
+
} catch (error) {
|
|
109
|
+
if (!await readRow(key)) throw error;
|
|
110
|
+
return consume(key, rule);
|
|
111
|
+
}
|
|
112
|
+
if (now - data.lastRequest > windowInMs) {
|
|
113
|
+
if (await db.incrementOne({
|
|
31
114
|
model,
|
|
32
115
|
where: [{
|
|
33
116
|
field: "key",
|
|
34
117
|
value: key
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
118
|
+
}, {
|
|
119
|
+
field: "lastRequest",
|
|
120
|
+
operator: "lte",
|
|
121
|
+
value: data.lastRequest
|
|
122
|
+
}],
|
|
123
|
+
increment: {},
|
|
124
|
+
set: {
|
|
125
|
+
count: 1,
|
|
126
|
+
lastRequest: now
|
|
127
|
+
}
|
|
128
|
+
})) {
|
|
129
|
+
deleteExpiredRows(now);
|
|
130
|
+
return {
|
|
131
|
+
allowed: true,
|
|
132
|
+
retryAfter: null
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
return consume(key, rule);
|
|
136
|
+
}
|
|
137
|
+
const windowStart = now - windowInMs;
|
|
138
|
+
if (await db.incrementOne({
|
|
139
|
+
model,
|
|
140
|
+
where: [
|
|
141
|
+
{
|
|
142
|
+
field: "key",
|
|
143
|
+
value: key
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
field: "lastRequest",
|
|
147
|
+
operator: "gt",
|
|
148
|
+
value: windowStart
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
field: "count",
|
|
152
|
+
operator: "lt",
|
|
153
|
+
value: rule.max
|
|
154
|
+
}
|
|
155
|
+
],
|
|
156
|
+
increment: { count: 1 },
|
|
157
|
+
set: { lastRequest: now }
|
|
158
|
+
})) return {
|
|
159
|
+
allowed: true,
|
|
160
|
+
retryAfter: null
|
|
161
|
+
};
|
|
162
|
+
const fresh = await readRow(key);
|
|
163
|
+
if (!fresh) return consume(key, rule);
|
|
164
|
+
if (now - fresh.lastRequest > windowInMs) return consume(key, rule);
|
|
165
|
+
return {
|
|
166
|
+
allowed: false,
|
|
167
|
+
retryAfter: getRetryAfter(fresh.lastRequest, rule.window)
|
|
168
|
+
};
|
|
169
|
+
};
|
|
170
|
+
const deleteExpiredRows = (now) => {
|
|
171
|
+
const cutoff = now - Math.max(ctx.rateLimit.window, ...getDefaultSpecialRules().map((r) => r.window)) * 1e3;
|
|
172
|
+
ctx.runInBackground(db.deleteMany({
|
|
173
|
+
model,
|
|
174
|
+
where: [{
|
|
175
|
+
field: "lastRequest",
|
|
176
|
+
operator: "lt",
|
|
177
|
+
value: cutoff
|
|
178
|
+
}]
|
|
179
|
+
}).then(() => void 0).catch((e) => ctx.logger.error("Error pruning rate limit rows", e)));
|
|
180
|
+
};
|
|
181
|
+
return {
|
|
182
|
+
get: readRow,
|
|
40
183
|
set: async (key, value, _update) => {
|
|
41
184
|
try {
|
|
42
185
|
if (_update) await db.updateMany({
|
|
@@ -61,58 +204,88 @@ function createDatabaseStorageWrapper(ctx) {
|
|
|
61
204
|
} catch (e) {
|
|
62
205
|
ctx.logger.error("Error setting rate limit", e);
|
|
63
206
|
}
|
|
64
|
-
}
|
|
207
|
+
},
|
|
208
|
+
consume
|
|
65
209
|
};
|
|
66
210
|
}
|
|
67
211
|
function getRateLimitStorage(ctx, rateLimitSettings) {
|
|
68
212
|
if (ctx.options.rateLimit?.customStorage) return ctx.options.rateLimit.customStorage;
|
|
69
213
|
const storage = ctx.rateLimit.storage;
|
|
70
|
-
if (storage === "secondary-storage")
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
214
|
+
if (storage === "secondary-storage") {
|
|
215
|
+
const ttlFor = (window) => window ?? ctx.options.rateLimit?.window ?? 10;
|
|
216
|
+
return {
|
|
217
|
+
get: async (key) => {
|
|
218
|
+
const data = await ctx.options.secondaryStorage?.get(key);
|
|
219
|
+
return data ? safeJSONParse(data) : null;
|
|
220
|
+
},
|
|
221
|
+
set: async (key, value, _update) => {
|
|
222
|
+
await ctx.options.secondaryStorage?.set?.(key, JSON.stringify(value), ttlFor(rateLimitSettings.window));
|
|
223
|
+
},
|
|
224
|
+
consume: ctx.options.secondaryStorage?.increment ? async (key, rule) => {
|
|
225
|
+
if (await ctx.options.secondaryStorage.increment(key, ttlFor(rule.window)) <= rule.max) return {
|
|
226
|
+
allowed: true,
|
|
227
|
+
retryAfter: null
|
|
228
|
+
};
|
|
229
|
+
return {
|
|
230
|
+
allowed: false,
|
|
231
|
+
retryAfter: rule.window
|
|
232
|
+
};
|
|
233
|
+
} : void 0
|
|
234
|
+
};
|
|
235
|
+
} else if (storage === "memory") {
|
|
236
|
+
const ttlFor = (window) => window ?? ctx.options.rateLimit?.window ?? 10;
|
|
237
|
+
return {
|
|
238
|
+
async get(key) {
|
|
239
|
+
const entry = memory.get(key);
|
|
240
|
+
if (!entry) return null;
|
|
241
|
+
if (Date.now() >= entry.expiresAt) {
|
|
242
|
+
memory.delete(key);
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
return entry.data;
|
|
246
|
+
},
|
|
247
|
+
async set(key, value, _update) {
|
|
248
|
+
const expiresAt = Date.now() + ttlFor(rateLimitSettings.window) * 1e3;
|
|
249
|
+
memory.set(key, {
|
|
250
|
+
data: value,
|
|
251
|
+
expiresAt
|
|
252
|
+
});
|
|
253
|
+
},
|
|
254
|
+
async consume(key, rule) {
|
|
255
|
+
pruneMemoryStore();
|
|
256
|
+
const now = Date.now();
|
|
257
|
+
const entry = memory.get(key);
|
|
258
|
+
const decision = decideConsume(entry && now < entry.expiresAt ? entry.data : void 0, rule, now);
|
|
259
|
+
if (decision.allowed) memory.set(key, {
|
|
260
|
+
data: {
|
|
261
|
+
...decision.next,
|
|
262
|
+
key
|
|
263
|
+
},
|
|
264
|
+
expiresAt: now + ttlFor(rule.window) * 1e3
|
|
265
|
+
});
|
|
266
|
+
return {
|
|
267
|
+
allowed: decision.allowed,
|
|
268
|
+
retryAfter: decision.retryAfter
|
|
269
|
+
};
|
|
87
270
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
async set(key, value, _update) {
|
|
91
|
-
const ttl = rateLimitSettings?.window ?? ctx.options.rateLimit?.window ?? 10;
|
|
92
|
-
const expiresAt = Date.now() + ttl * 1e3;
|
|
93
|
-
memory.set(key, {
|
|
94
|
-
data: value,
|
|
95
|
-
expiresAt
|
|
96
|
-
});
|
|
97
|
-
}
|
|
98
|
-
};
|
|
271
|
+
};
|
|
272
|
+
}
|
|
99
273
|
return createDatabaseStorageWrapper(ctx);
|
|
100
274
|
}
|
|
101
275
|
let ipWarningLogged = false;
|
|
276
|
+
const NO_TRUSTED_IP_KEY = "no-trusted-ip";
|
|
102
277
|
async function resolveRateLimitConfig(req, ctx) {
|
|
103
278
|
const basePath = new URL(ctx.baseURL).pathname;
|
|
104
279
|
const path = normalizePathname(req.url, basePath);
|
|
105
280
|
let currentWindow = ctx.rateLimit.window;
|
|
106
281
|
let currentMax = ctx.rateLimit.max;
|
|
107
282
|
const ip = getIp(req, ctx.options);
|
|
108
|
-
if (!ip)
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
}
|
|
113
|
-
return null;
|
|
283
|
+
if (!ip && ctx.options.advanced?.ipAddress?.disableIpTracking) return null;
|
|
284
|
+
if (!ip && !ipWarningLogged) {
|
|
285
|
+
ctx.logger.warn("Rate limiting could not determine a client IP and is falling back to a single shared per-path bucket. Ensure your runtime forwards a trusted client IP header and configure `advanced.ipAddress.ipAddressHeaders` if needed.");
|
|
286
|
+
ipWarningLogged = true;
|
|
114
287
|
}
|
|
115
|
-
const key = createRateLimitKey(ip, path);
|
|
288
|
+
const key = createRateLimitKey(ip ?? NO_TRUSTED_IP_KEY, path);
|
|
116
289
|
const specialRule = getDefaultSpecialRules().find((rule) => rule.pathMatcher(path));
|
|
117
290
|
if (specialRule) {
|
|
118
291
|
currentWindow = specialRule.window;
|
|
@@ -150,37 +323,50 @@ async function resolveRateLimitConfig(req, ctx) {
|
|
|
150
323
|
currentMax
|
|
151
324
|
};
|
|
152
325
|
}
|
|
326
|
+
let legacyFallbackWarningLogged = false;
|
|
327
|
+
/**
|
|
328
|
+
* Decides the rate limit for the request in a single atomic step. The whole
|
|
329
|
+
* check-and-increment happens here in the request phase; there is no separate
|
|
330
|
+
* response-phase write-back, so concurrent requests cannot all pass a stale
|
|
331
|
+
* read before any increment lands.
|
|
332
|
+
*/
|
|
153
333
|
async function onRequestRateLimit(req, ctx) {
|
|
154
334
|
if (!ctx.rateLimit.enabled) return;
|
|
155
335
|
const config = await resolveRateLimitConfig(req, ctx);
|
|
156
336
|
if (!config) return;
|
|
157
337
|
const { key, currentWindow, currentMax } = config;
|
|
158
|
-
const data = await getRateLimitStorage(ctx, { window: currentWindow }).get(key);
|
|
159
|
-
if (data && shouldRateLimit(currentMax, currentWindow, data)) return rateLimitResponse(getRetryAfter(data.lastRequest, currentWindow));
|
|
160
|
-
}
|
|
161
|
-
async function onResponseRateLimit(req, ctx) {
|
|
162
|
-
if (!ctx.rateLimit.enabled) return;
|
|
163
|
-
const config = await resolveRateLimitConfig(req, ctx);
|
|
164
|
-
if (!config) return;
|
|
165
|
-
const { key, currentWindow } = config;
|
|
166
338
|
const storage = getRateLimitStorage(ctx, { window: currentWindow });
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
339
|
+
const rule = {
|
|
340
|
+
window: currentWindow,
|
|
341
|
+
max: currentMax
|
|
342
|
+
};
|
|
343
|
+
if (storage.consume) {
|
|
344
|
+
const { allowed, retryAfter } = await storage.consume(key, rule);
|
|
345
|
+
if (!allowed) return rateLimitResponse(retryAfter ?? currentWindow);
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
return legacyConsume(ctx, storage, key, rule);
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Non-atomic check-then-increment for storages that do not implement `consume`
|
|
352
|
+
* (custom storages, or secondary storages without `increment`). Under
|
|
353
|
+
* concurrency this is best-effort: simultaneous requests can each pass the
|
|
354
|
+
* check before either write lands.
|
|
355
|
+
*
|
|
356
|
+
* FIXME(rate-limit-consume-required): remove on `next` once `consume` is the
|
|
357
|
+
* sole required member of the storage contract.
|
|
358
|
+
*/
|
|
359
|
+
async function legacyConsume(ctx, storage, key, rule) {
|
|
360
|
+
if (!legacyFallbackWarningLogged) {
|
|
361
|
+
ctx.logger.warn("Rate limiting is best-effort: the configured storage has no atomic `consume`, so concurrent requests may bypass the limit. Provide a storage that implements `consume` for strict enforcement.");
|
|
362
|
+
legacyFallbackWarningLogged = true;
|
|
363
|
+
}
|
|
364
|
+
const decision = decideConsume(await storage.get(key), rule, Date.now());
|
|
365
|
+
if (!decision.allowed) return rateLimitResponse(decision.retryAfter ?? rule.window);
|
|
366
|
+
await storage.set(key, {
|
|
367
|
+
...decision.next,
|
|
368
|
+
key
|
|
369
|
+
}, decision.update);
|
|
184
370
|
}
|
|
185
371
|
function getDefaultSpecialRules() {
|
|
186
372
|
return [{
|
|
@@ -198,4 +384,4 @@ function getDefaultSpecialRules() {
|
|
|
198
384
|
}];
|
|
199
385
|
}
|
|
200
386
|
//#endregion
|
|
201
|
-
export { onRequestRateLimit
|
|
387
|
+
export { onRequestRateLimit };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { shouldBindAccountCookieToSessionUser } from "../../context/store-capabilities.mjs";
|
|
1
2
|
import { parseAccountOutput } from "../../db/schema.mjs";
|
|
2
3
|
import { getAccountCookie, setAccountCookie } from "../../cookies/session-store.mjs";
|
|
3
4
|
import { getAwaitableValue } from "../../context/helpers.mjs";
|
|
@@ -5,7 +6,7 @@ import { missingEmailLogMessage } from "../../oauth2/errors.mjs";
|
|
|
5
6
|
import { decryptOAuthToken, setTokenUtil } from "../../oauth2/utils.mjs";
|
|
6
7
|
import { applyUpdateUserInfoOnLink } from "../../oauth2/link-account.mjs";
|
|
7
8
|
import { generateState } from "../../oauth2/state.mjs";
|
|
8
|
-
import { freshSessionMiddleware, getSessionFromCtx, sessionMiddleware } from "./session.mjs";
|
|
9
|
+
import { freshSessionMiddleware, getSessionFromCtx, isStateful, sessionMiddleware } from "./session.mjs";
|
|
9
10
|
import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
|
|
10
11
|
import { SocialProviderListEnum } from "@better-auth/core/social-providers";
|
|
11
12
|
import { createAuthEndpoint } from "@better-auth/core/api";
|
|
@@ -121,7 +122,7 @@ const linkSocialAccount = createAuthEndpoint("/link-social", {
|
|
|
121
122
|
}
|
|
122
123
|
const { token, nonce } = c.body.idToken;
|
|
123
124
|
if (!await provider.verifyIdToken(token, nonce)) {
|
|
124
|
-
c.context.logger.
|
|
125
|
+
c.context.logger.warn("Invalid id token", { provider: c.body.provider });
|
|
125
126
|
throw APIError.from("UNAUTHORIZED", BASE_ERROR_CODES.INVALID_TOKEN);
|
|
126
127
|
}
|
|
127
128
|
const linkingUserInfo = await provider.getUserInfo({
|
|
@@ -225,9 +226,15 @@ const unlinkAccount = createAuthEndpoint("/unlink-account", {
|
|
|
225
226
|
* `userId` directly. Throws `UNAUTHORIZED` when an HTTP caller is
|
|
226
227
|
* unauthenticated, and `USER_ID_OR_SESSION_REQUIRED` when neither a session
|
|
227
228
|
* nor a `userId` is available.
|
|
229
|
+
*
|
|
230
|
+
* When a durable store is authoritative, bypasses the cookie cache: these
|
|
231
|
+
* routes mint or refresh provider access tokens, so a server-side session
|
|
232
|
+
* revocation must take effect immediately rather than waiting for the cached
|
|
233
|
+
* cookie to expire. DB-less deployments keep the session in the cookie itself,
|
|
234
|
+
* so the cache is left in place for them.
|
|
228
235
|
*/
|
|
229
236
|
async function resolveUserId(ctx, userId) {
|
|
230
|
-
const session = await getSessionFromCtx(ctx);
|
|
237
|
+
const session = await getSessionFromCtx(ctx, { disableCookieCache: isStateful(ctx) });
|
|
231
238
|
if (!session && (ctx.request || ctx.headers)) throw ctx.error("UNAUTHORIZED");
|
|
232
239
|
const resolvedUserId = session?.user?.id || userId;
|
|
233
240
|
if (!resolvedUserId) throw APIError.from("BAD_REQUEST", {
|
|
@@ -236,6 +243,9 @@ async function resolveUserId(ctx, userId) {
|
|
|
236
243
|
});
|
|
237
244
|
return resolvedUserId;
|
|
238
245
|
}
|
|
246
|
+
function matchesAccountSelection(ctx, account, { resolvedUserId, providerId, accountId }) {
|
|
247
|
+
return (!shouldBindAccountCookieToSessionUser(ctx.context.options) || account.userId === resolvedUserId) && (!providerId || providerId === account.providerId) && (!accountId || account.accountId === accountId);
|
|
248
|
+
}
|
|
239
249
|
/**
|
|
240
250
|
* Fetches a currently-valid access token for a user's provider account,
|
|
241
251
|
* refreshing and persisting it when it is within five seconds of expiry.
|
|
@@ -251,7 +261,11 @@ async function getValidAccessToken(ctx, { resolvedUserId, providerId, accountId,
|
|
|
251
261
|
let account = resolvedAccount;
|
|
252
262
|
if (!account) {
|
|
253
263
|
const accountData = await getAccountCookie(ctx);
|
|
254
|
-
if (accountData &&
|
|
264
|
+
if (accountData && matchesAccountSelection(ctx, accountData, {
|
|
265
|
+
resolvedUserId,
|
|
266
|
+
providerId,
|
|
267
|
+
accountId
|
|
268
|
+
})) account = accountData;
|
|
255
269
|
else account = (await ctx.context.internalAdapter.findAccounts(resolvedUserId)).find((acc) => accountId ? acc.accountId === accountId && acc.providerId === providerId : acc.providerId === providerId);
|
|
256
270
|
}
|
|
257
271
|
if (!account) throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.ACCOUNT_NOT_FOUND);
|
|
@@ -382,12 +396,15 @@ const refreshToken = createAuthEndpoint("/refresh-token", {
|
|
|
382
396
|
});
|
|
383
397
|
let account = void 0;
|
|
384
398
|
const accountData = await getAccountCookie(ctx);
|
|
385
|
-
|
|
399
|
+
const usedAccountCookie = !!accountData && matchesAccountSelection(ctx, accountData, {
|
|
400
|
+
resolvedUserId,
|
|
401
|
+
providerId,
|
|
402
|
+
accountId
|
|
403
|
+
});
|
|
404
|
+
if (usedAccountCookie) account = accountData;
|
|
386
405
|
else account = (await ctx.context.internalAdapter.findAccounts(resolvedUserId)).find((acc) => accountId ? acc.accountId === accountId && acc.providerId === providerId : acc.providerId === providerId);
|
|
387
406
|
if (!account) throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.ACCOUNT_NOT_FOUND);
|
|
388
|
-
|
|
389
|
-
if (accountData && providerId === accountData.providerId) refreshToken = accountData.refreshToken ?? void 0;
|
|
390
|
-
else refreshToken = account.refreshToken ?? void 0;
|
|
407
|
+
const refreshToken = account.refreshToken ?? void 0;
|
|
391
408
|
if (!refreshToken) throw APIError.from("BAD_REQUEST", {
|
|
392
409
|
message: "Refresh token not found",
|
|
393
410
|
code: "REFRESH_TOKEN_NOT_FOUND"
|
|
@@ -409,7 +426,7 @@ const refreshToken = createAuthEndpoint("/refresh-token", {
|
|
|
409
426
|
};
|
|
410
427
|
await ctx.context.internalAdapter.updateAccount(account.id, updateData);
|
|
411
428
|
}
|
|
412
|
-
if (
|
|
429
|
+
if (usedAccountCookie && ctx.context.options.account?.storeAccountCookie) await setAccountCookie(ctx, {
|
|
413
430
|
...accountData,
|
|
414
431
|
accessToken: await setTokenUtil(tokens.accessToken, ctx.context),
|
|
415
432
|
refreshToken: resolvedRefreshToken,
|
|
@@ -479,7 +496,10 @@ const accountInfo = createAuthEndpoint("/account-info", {
|
|
|
479
496
|
if (!providedAccountId) {
|
|
480
497
|
if (ctx.context.options.account?.storeAccountCookie) {
|
|
481
498
|
const accountData = await getAccountCookie(ctx);
|
|
482
|
-
if (accountData
|
|
499
|
+
if (accountData && matchesAccountSelection(ctx, accountData, {
|
|
500
|
+
resolvedUserId,
|
|
501
|
+
providerId: providedProviderId
|
|
502
|
+
})) account = accountData;
|
|
483
503
|
}
|
|
484
504
|
} else {
|
|
485
505
|
const matchingAccounts = (await ctx.context.internalAdapter.findAccounts(resolvedUserId)).filter((acc) => acc.accountId === providedAccountId && (!providedProviderId || acc.providerId === providedProviderId));
|
|
@@ -489,7 +509,7 @@ const accountInfo = createAuthEndpoint("/account-info", {
|
|
|
489
509
|
});
|
|
490
510
|
account = matchingAccounts[0];
|
|
491
511
|
}
|
|
492
|
-
if (!account || account
|
|
512
|
+
if (!account || !matchesAccountSelection(ctx, account, { resolvedUserId })) throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.ACCOUNT_NOT_FOUND);
|
|
493
513
|
const provider = await getAwaitableValue(ctx.context.socialProviders, { value: account.providerId });
|
|
494
514
|
if (!provider) throw APIError.from("BAD_REQUEST", {
|
|
495
515
|
message: "Account is not associated with a configured social provider.",
|
|
@@ -55,12 +55,12 @@ const callbackOAuth = createAuthEndpoint("/callback/:id", {
|
|
|
55
55
|
const resolvedErrorURL = errorURL ?? defaultErrorURL;
|
|
56
56
|
if (error) redirectOnError(c, resolvedErrorURL, error, error_description);
|
|
57
57
|
if (!code) {
|
|
58
|
-
c.context.logger.
|
|
58
|
+
c.context.logger.warn("Code not found");
|
|
59
59
|
redirectOnError(c, resolvedErrorURL, "no_code");
|
|
60
60
|
}
|
|
61
61
|
const provider = await getAwaitableValue(c.context.socialProviders, { value: c.params.id });
|
|
62
62
|
if (!provider) {
|
|
63
|
-
c.context.logger.
|
|
63
|
+
c.context.logger.warn("OAuth provider not found", { providerId: c.params.id });
|
|
64
64
|
redirectOnError(c, resolvedErrorURL, "oauth_provider_not_found");
|
|
65
65
|
}
|
|
66
66
|
let tokens;
|
|
@@ -81,7 +81,7 @@ const callbackOAuth = createAuthEndpoint("/callback/:id", {
|
|
|
81
81
|
...tokens,
|
|
82
82
|
user: parsedUserData ?? void 0
|
|
83
83
|
}).then((res) => res?.user);
|
|
84
|
-
if (!userInfo || userInfo.id === void 0 || userInfo.id === null) {
|
|
84
|
+
if (!userInfo || userInfo.id === void 0 || userInfo.id === null || userInfo.id === "") {
|
|
85
85
|
c.context.logger.error("Unable to get user info");
|
|
86
86
|
redirectOnError(c, resolvedErrorURL, "unable_to_get_user_info");
|
|
87
87
|
}
|
|
@@ -4,7 +4,7 @@ import { createEmailVerificationToken, sendVerificationEmail, sendVerificationEm
|
|
|
4
4
|
import { error } from "./error.mjs";
|
|
5
5
|
import { ok } from "./ok.mjs";
|
|
6
6
|
import { requestPasswordReset, requestPasswordResetCallback, resetPassword, verifyPassword } from "./password.mjs";
|
|
7
|
-
import { freshSessionMiddleware, getSession, getSessionFromCtx, listSessions, requestOnlySessionMiddleware, revokeOtherSessions, revokeSession, revokeSessions, sensitiveSessionMiddleware, sessionMiddleware } from "./session.mjs";
|
|
7
|
+
import { freshSessionMiddleware, getSession, getSessionFromCtx, isStateful, listSessions, requestOnlySessionMiddleware, revokeOtherSessions, revokeSession, revokeSessions, sensitiveSessionMiddleware, sessionMiddleware } from "./session.mjs";
|
|
8
8
|
import { signInEmail, signInSocial } from "./sign-in.mjs";
|
|
9
9
|
import { signOut } from "./sign-out.mjs";
|
|
10
10
|
import { signUpEmail } from "./sign-up.mjs";
|
|
@@ -55,7 +55,7 @@ const requestPasswordReset = createAuthEndpoint("/request-password-reset", {
|
|
|
55
55
|
*/
|
|
56
56
|
generateId(24);
|
|
57
57
|
await ctx.context.internalAdapter.findVerificationValue("dummy-verification-token");
|
|
58
|
-
ctx.context.logger.
|
|
58
|
+
ctx.context.logger.warn("Reset Password: User not found");
|
|
59
59
|
return ctx.json({
|
|
60
60
|
status: true,
|
|
61
61
|
message: "If this email exists in our system, check your email for the reset link"
|
|
@@ -145,8 +145,8 @@ const resetPassword = createAuthEndpoint("/reset-password", {
|
|
|
145
145
|
if (newPassword.length < minLength) throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.PASSWORD_TOO_SHORT);
|
|
146
146
|
if (newPassword.length > maxLength) throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.PASSWORD_TOO_LONG);
|
|
147
147
|
const id = `reset-password:${token}`;
|
|
148
|
-
const verification = await ctx.context.internalAdapter.
|
|
149
|
-
if (!verification
|
|
148
|
+
const verification = await ctx.context.internalAdapter.consumeVerificationValue(id);
|
|
149
|
+
if (!verification) throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.INVALID_TOKEN);
|
|
150
150
|
const userId = verification.value;
|
|
151
151
|
const hashedPassword = await ctx.context.password.hash(newPassword);
|
|
152
152
|
if (!(await ctx.context.internalAdapter.findAccounts(userId)).find((ac) => ac.providerId === "credential")) await ctx.context.internalAdapter.createAccount({
|
|
@@ -156,7 +156,6 @@ const resetPassword = createAuthEndpoint("/reset-password", {
|
|
|
156
156
|
accountId: userId
|
|
157
157
|
});
|
|
158
158
|
else await ctx.context.internalAdapter.updatePassword(userId, hashedPassword);
|
|
159
|
-
await ctx.context.internalAdapter.deleteVerificationByIdentifier(id);
|
|
160
159
|
if (ctx.context.options.emailAndPassword?.onPasswordReset) {
|
|
161
160
|
const user = await ctx.context.internalAdapter.findUserById(userId);
|
|
162
161
|
if (user) await ctx.context.options.emailAndPassword.onPasswordReset({ user }, ctx.request);
|