@rebasepro/server 0.21.1-canary.g8c5a265 → 0.21.1
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/errors.d.ts +41 -0
- package/dist/auth/auth-hooks.d.ts +4 -0
- package/dist/auth/reset-password-admin.d.ts +4 -0
- package/dist/auth/token-revocation.d.ts +39 -5
- package/dist/{auth-BvBllZjy.js → auth-BS4WBA10.js} +98 -57
- package/dist/auth-BS4WBA10.js.map +1 -0
- package/dist/{backup-De-Xm4tg.js → backup-DzI9jLwc.js} +2 -2
- package/dist/{backup-De-Xm4tg.js.map → backup-DzI9jLwc.js.map} +1 -1
- package/dist/{cron-routes-3FVef5QO.js → cron-routes-B_wlLybo.js} +3 -3
- package/dist/{cron-routes-3FVef5QO.js.map → cron-routes-B_wlLybo.js.map} +1 -1
- package/dist/ddl-bootstrap-CfNvxMuK.js.map +1 -1
- package/dist/{errors-DMImyqyR.js → errors-DWsX4yTd.js} +55 -18
- package/dist/errors-DWsX4yTd.js.map +1 -0
- package/dist/{function-routes-C4nB2h0z.js → function-routes-Chet4-lB.js} +2 -2
- package/dist/{function-routes-C4nB2h0z.js.map → function-routes-Chet4-lB.js.map} +1 -1
- package/dist/functions/index.js.map +1 -1
- package/dist/index.es.js +39 -32
- package/dist/index.es.js.map +1 -1
- package/dist/{logs-routes-DnJINsMu.js → logs-routes-3EEzPjhl.js} +2 -2
- package/dist/{logs-routes-DnJINsMu.js.map → logs-routes-3EEzPjhl.js.map} +1 -1
- package/dist/{query-parser-DUl8d557.js → query-parser-C-rl30ce.js} +2 -2
- package/dist/{query-parser-DUl8d557.js.map → query-parser-C-rl30ce.js.map} +1 -1
- package/dist/{request-timeout-BR-OBwES.js → request-timeout-C_4C2BeR.js} +2 -2
- package/dist/{request-timeout-BR-OBwES.js.map → request-timeout-C_4C2BeR.js.map} +1 -1
- package/dist/{schema-editor-routes-BKOmdf4M.js → schema-editor-routes-DdLihzp0.js} +2 -2
- package/dist/{schema-editor-routes-BKOmdf4M.js.map → schema-editor-routes-DdLihzp0.js.map} +1 -1
- package/dist/src-Br6ARbs6.js.map +1 -1
- package/package.json +5 -5
- package/dist/auth-BvBllZjy.js.map +0 -1
- package/dist/errors-DMImyqyR.js.map +0 -1
package/dist/api/errors.d.ts
CHANGED
|
@@ -109,6 +109,47 @@ export interface RebaseApiError extends Error {
|
|
|
109
109
|
code?: string;
|
|
110
110
|
details?: unknown;
|
|
111
111
|
}
|
|
112
|
+
/**
|
|
113
|
+
* The answer an error chose for itself, read the same way at every door.
|
|
114
|
+
*
|
|
115
|
+
* @see declaredErrorAnswer
|
|
116
|
+
*/
|
|
117
|
+
export interface DeclaredErrorAnswer {
|
|
118
|
+
/** The HTTP status the error carries. A socket frame has no slot for it. */
|
|
119
|
+
status: number;
|
|
120
|
+
code: string;
|
|
121
|
+
message: string;
|
|
122
|
+
details?: unknown;
|
|
123
|
+
/** See {@link ApiError.expected}: log it at debug, not warn. */
|
|
124
|
+
expected: boolean;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* The status, code and message an error carries as its own answer — or
|
|
128
|
+
* `undefined` for an error that carries none, which is a server fault and gets
|
|
129
|
+
* masked.
|
|
130
|
+
*
|
|
131
|
+
* Two classes carry one. The server's `ApiError`, and `RebaseApiError` (or its
|
|
132
|
+
* `RebaseClientError` subclass) from `@rebasepro/types` once it has a status.
|
|
133
|
+
* The second is the browser-safe class: a `config/collections/*.ts` file is
|
|
134
|
+
* bundled into the admin SPA and cannot import this package, so it is what a
|
|
135
|
+
* collection callback throws, and what a callback refusal becomes —
|
|
136
|
+
* `callbackRefusal` returns one, and `toCallbackError` wraps anything thrown
|
|
137
|
+
* that does not already carry a status.
|
|
138
|
+
*
|
|
139
|
+
* One function because several doors turn an error into an answer: the REST
|
|
140
|
+
* error handler, the two WebSocket servers, and the Postgres realtime
|
|
141
|
+
* subscriptions. Each used to list the classes it recognised by hand. The
|
|
142
|
+
* sockets listed only `ApiError`, so a `beforeDelete` veto that REST
|
|
143
|
+
* answered as 400 `CALLBACK_REJECTED` with the author's message reached the
|
|
144
|
+
* admin panel — which writes through the socket — as `INTERNAL_ERROR`, and in
|
|
145
|
+
* production as "An unexpected error occurred".
|
|
146
|
+
*
|
|
147
|
+
* Matched by name as well as `instanceof`: a monorepo can resolve two copies of
|
|
148
|
+
* a package, and `instanceof` is false across them. Name matching is also why
|
|
149
|
+
* this file needs no runtime import of `@rebasepro/types`, which it may not
|
|
150
|
+
* have — it is in the graph of `@rebasepro/server/functions`.
|
|
151
|
+
*/
|
|
152
|
+
export declare function declaredErrorAnswer(error: unknown): DeclaredErrorAnswer | undefined;
|
|
112
153
|
/**
|
|
113
154
|
* Hono error-handling middleware (`app.onError`).
|
|
114
155
|
* Converts any error into the canonical `{ error: { message, code } }` shape.
|
|
@@ -222,6 +222,10 @@ export interface AuthHooks {
|
|
|
222
222
|
/**
|
|
223
223
|
* Optional hook to customize or override the default password reset flow via the admin panel.
|
|
224
224
|
* When provided, this replaces the built-in password reset token generation, hashing, and email logic.
|
|
225
|
+
*
|
|
226
|
+
* A `temporaryPassword` it returns becomes the account's password: the route hashes and writes
|
|
227
|
+
* it (the same contract as a collection's `auth.onResetPassword`), so the hook need not write it
|
|
228
|
+
* through `authRepo`. The account's existing sessions end whatever the hook returns.
|
|
225
229
|
*/
|
|
226
230
|
onAdminResetPassword?(uid: string, ctx: {
|
|
227
231
|
authRepo: AuthRepository;
|
|
@@ -5,6 +5,10 @@
|
|
|
5
5
|
* 1. Collection-level hook (`auth.onResetPassword` on the collection)
|
|
6
6
|
* 2. Backend-level hook (`AuthHooks.onAdminResetPassword`)
|
|
7
7
|
* 3. Built-in default (send reset email, or generate temp password)
|
|
8
|
+
*
|
|
9
|
+
* Whichever of them runs, the account's existing sessions end, and a
|
|
10
|
+
* `temporaryPassword` in its result becomes the account's password before the
|
|
11
|
+
* response shows it to the admin.
|
|
8
12
|
*/
|
|
9
13
|
import { Hono } from "hono";
|
|
10
14
|
import type { AuthRepository } from "./interfaces.js";
|
|
@@ -3,11 +3,12 @@ import type { AccessTokenPayload } from "./jwt.js";
|
|
|
3
3
|
/**
|
|
4
4
|
* Has this access token been revoked?
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
6
|
+
* Everything that ends every session a user holds — a password change or reset
|
|
7
|
+
* of any kind, and `DELETE /auth/sessions` — goes through
|
|
8
|
+
* {@link revokeAllSessions}, which stamps a `tokensValidAfter` watermark on the
|
|
9
|
+
* user. It also deletes refresh-token rows, which is what made the gap easy to
|
|
10
|
+
* miss: the session really is gone, and the *refresh* path really does check
|
|
11
|
+
* the watermark — so signing out looked like it worked.
|
|
11
12
|
*
|
|
12
13
|
* The access token was untouched. It is a bearer credential that nothing
|
|
13
14
|
* consulted a database about, so it stayed valid for its full lifetime after
|
|
@@ -36,3 +37,36 @@ import type { AccessTokenPayload } from "./jwt.js";
|
|
|
36
37
|
* failure is logged at warn so it is visible rather than silent.
|
|
37
38
|
*/
|
|
38
39
|
export declare function isAccessTokenRevoked(authRepo: Pick<AuthRepository, "getTokensValidAfter">, payload: Pick<AccessTokenPayload, "uid" | "iat">): Promise<boolean>;
|
|
40
|
+
/**
|
|
41
|
+
* End every session this user holds, on every device.
|
|
42
|
+
*
|
|
43
|
+
* Two writes, because each covers what the other cannot. Deleting the refresh
|
|
44
|
+
* rows ends the sessions that exist at this instant, and on a repository
|
|
45
|
+
* without the watermark it is the only revocation there is. The watermark
|
|
46
|
+
* voids what the delete cannot see: a refresh already in flight that inserts
|
|
47
|
+
* its rotated token a moment after the delete ran, and every access token
|
|
48
|
+
* already handed out, which {@link isAccessTokenRevoked} refuses from here on.
|
|
49
|
+
*
|
|
50
|
+
* The watermark write does not fail the caller. By the time it runs the
|
|
51
|
+
* credential has usually already changed and the rows are gone, so a 500 would
|
|
52
|
+
* tell someone their password did not change when it did. It is logged, because
|
|
53
|
+
* a failure here leaves access tokens working until they expire.
|
|
54
|
+
*/
|
|
55
|
+
export declare function revokeAllSessions(authRepo: Pick<AuthRepository, "deleteAllRefreshTokensForUser" | "setTokensValidAfter">, uid: string): Promise<void>;
|
|
56
|
+
/**
|
|
57
|
+
* Replace a user's password and end every session they hold.
|
|
58
|
+
*
|
|
59
|
+
* The one way a password hash is written over an existing account. A new
|
|
60
|
+
* password is what someone sets when they believe the old one — or a session
|
|
61
|
+
* signed in with it — is in someone else's hands, so a password change that
|
|
62
|
+
* leaves those sessions alive does not do the thing it was done for.
|
|
63
|
+
*
|
|
64
|
+
* It used to be a pair of lines repeated after each `updatePassword`, and it
|
|
65
|
+
* reached the two self-service routes and none of the admin ones: an
|
|
66
|
+
* administrator resetting a phished account left the attacker's refresh token
|
|
67
|
+
* minting access tokens for the rest of its lifetime.
|
|
68
|
+
* `test/password-change-revokes-sessions.test.ts` holds every route that sets a
|
|
69
|
+
* password to this, and fails if anything but this function calls
|
|
70
|
+
* `updatePassword`.
|
|
71
|
+
*/
|
|
72
|
+
export declare function replaceUserPassword(authRepo: Pick<AuthRepository, "updatePassword" | "deleteAllRefreshTokensForUser" | "setTokensValidAfter">, uid: string, passwordHash: string): Promise<void>;
|
|
@@ -8,8 +8,8 @@ import "./src-Br6ARbs6.js";
|
|
|
8
8
|
import { n as createDdlBootstrapper, o as isSQLAdmin } from "./ddl-bootstrap-CfNvxMuK.js";
|
|
9
9
|
import { t as firstSqlRow } from "./sql-rows-C6GEc2oE.js";
|
|
10
10
|
import { r as logger } from "./logger-DO2PZc4i.js";
|
|
11
|
-
import {
|
|
12
|
-
import { a as resolveListLimitParam } from "./query-parser-
|
|
11
|
+
import { r as errorHandler, t as ApiError } from "./errors-DWsX4yTd.js";
|
|
12
|
+
import { a as resolveListLimitParam } from "./query-parser-C-rl30ce.js";
|
|
13
13
|
import { C as require_jsonwebtoken, E as randomInt$1, O as sha256Hex, _ as verifyAccessToken, b as verifyMfaPendingToken, c as getAccessTokenExpiry, d as getRefreshTokenTtlMs, f as hasAsymmetricSigningKey, i as generateDownloadToken, l as getJwks, m as isJwtConfigured, n as configureJwt, o as generateMfaPendingToken, p as hashRefreshToken, r as generateAccessToken, s as generateRefreshToken, t as MAX_COOKIE_AGE_MS, u as getRefreshTokenExpiry, v as verifyDownloadToken, w as constantTimeEqual } from "./jwt-C4OW-DNq.js";
|
|
14
14
|
import { n as hasAdministrativeRole, r as isAdministrativeRole, t as ADMINISTRATIVE_ROLES } from "./admin-roles-vYdp_Pil.js";
|
|
15
15
|
import { c as tryCanonicalStorageKey, i as canonicalStorageId } from "./keys-Qfc4XieN.js";
|
|
@@ -1034,11 +1034,12 @@ async function completeUserCreation(prepared, finalize) {
|
|
|
1034
1034
|
/**
|
|
1035
1035
|
* Has this access token been revoked?
|
|
1036
1036
|
*
|
|
1037
|
-
*
|
|
1038
|
-
*
|
|
1039
|
-
*
|
|
1040
|
-
*
|
|
1041
|
-
*
|
|
1037
|
+
* Everything that ends every session a user holds — a password change or reset
|
|
1038
|
+
* of any kind, and `DELETE /auth/sessions` — goes through
|
|
1039
|
+
* {@link revokeAllSessions}, which stamps a `tokensValidAfter` watermark on the
|
|
1040
|
+
* user. It also deletes refresh-token rows, which is what made the gap easy to
|
|
1041
|
+
* miss: the session really is gone, and the *refresh* path really does check
|
|
1042
|
+
* the watermark — so signing out looked like it worked.
|
|
1042
1043
|
*
|
|
1043
1044
|
* The access token was untouched. It is a bearer credential that nothing
|
|
1044
1045
|
* consulted a database about, so it stayed valid for its full lifetime after
|
|
@@ -1082,6 +1083,52 @@ async function isAccessTokenRevoked(authRepo, payload) {
|
|
|
1082
1083
|
if (!validAfter) return false;
|
|
1083
1084
|
return payload.iat < Math.floor(validAfter.getTime() / 1e3);
|
|
1084
1085
|
}
|
|
1086
|
+
/**
|
|
1087
|
+
* End every session this user holds, on every device.
|
|
1088
|
+
*
|
|
1089
|
+
* Two writes, because each covers what the other cannot. Deleting the refresh
|
|
1090
|
+
* rows ends the sessions that exist at this instant, and on a repository
|
|
1091
|
+
* without the watermark it is the only revocation there is. The watermark
|
|
1092
|
+
* voids what the delete cannot see: a refresh already in flight that inserts
|
|
1093
|
+
* its rotated token a moment after the delete ran, and every access token
|
|
1094
|
+
* already handed out, which {@link isAccessTokenRevoked} refuses from here on.
|
|
1095
|
+
*
|
|
1096
|
+
* The watermark write does not fail the caller. By the time it runs the
|
|
1097
|
+
* credential has usually already changed and the rows are gone, so a 500 would
|
|
1098
|
+
* tell someone their password did not change when it did. It is logged, because
|
|
1099
|
+
* a failure here leaves access tokens working until they expire.
|
|
1100
|
+
*/
|
|
1101
|
+
async function revokeAllSessions(authRepo, uid) {
|
|
1102
|
+
await authRepo.deleteAllRefreshTokensForUser(uid);
|
|
1103
|
+
try {
|
|
1104
|
+
await authRepo.setTokensValidAfter?.(uid, /* @__PURE__ */ new Date());
|
|
1105
|
+
} catch (error) {
|
|
1106
|
+
logger.warn("[Auth] Could not write the token revocation watermark; access tokens issued before now stay valid until they expire", {
|
|
1107
|
+
uid,
|
|
1108
|
+
error
|
|
1109
|
+
});
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
/**
|
|
1113
|
+
* Replace a user's password and end every session they hold.
|
|
1114
|
+
*
|
|
1115
|
+
* The one way a password hash is written over an existing account. A new
|
|
1116
|
+
* password is what someone sets when they believe the old one — or a session
|
|
1117
|
+
* signed in with it — is in someone else's hands, so a password change that
|
|
1118
|
+
* leaves those sessions alive does not do the thing it was done for.
|
|
1119
|
+
*
|
|
1120
|
+
* It used to be a pair of lines repeated after each `updatePassword`, and it
|
|
1121
|
+
* reached the two self-service routes and none of the admin ones: an
|
|
1122
|
+
* administrator resetting a phished account left the attacker's refresh token
|
|
1123
|
+
* minting access tokens for the rest of its lifetime.
|
|
1124
|
+
* `test/password-change-revokes-sessions.test.ts` holds every route that sets a
|
|
1125
|
+
* password to this, and fails if anything but this function calls
|
|
1126
|
+
* `updatePassword`.
|
|
1127
|
+
*/
|
|
1128
|
+
async function replaceUserPassword(authRepo, uid, passwordHash) {
|
|
1129
|
+
await authRepo.updatePassword(uid, passwordHash);
|
|
1130
|
+
await revokeAllSessions(authRepo, uid);
|
|
1131
|
+
}
|
|
1085
1132
|
//#endregion
|
|
1086
1133
|
//#region src/auth/rls-scope.ts
|
|
1087
1134
|
var import_jsonwebtoken = /* @__PURE__ */ __toESM(require_jsonwebtoken(), 1);
|
|
@@ -3633,8 +3680,7 @@ function mountSessionRoutes(opts) {
|
|
|
3633
3680
|
router.delete("/sessions", requireLiveSession, async (c) => {
|
|
3634
3681
|
const userCtx = c.get("user");
|
|
3635
3682
|
if (!userCtx) throw ApiError.unauthorized("Not authenticated");
|
|
3636
|
-
await authRepo
|
|
3637
|
-
await authRepo.setTokensValidAfter?.(userCtx.uid, /* @__PURE__ */ new Date()).catch(() => void 0);
|
|
3683
|
+
await revokeAllSessions(authRepo, userCtx.uid);
|
|
3638
3684
|
return c.json({
|
|
3639
3685
|
success: true,
|
|
3640
3686
|
message: "All sessions revoked successfully"
|
|
@@ -4548,10 +4594,8 @@ function createAuthRoutes(config) {
|
|
|
4548
4594
|
const storedToken = await authRepo.findValidPasswordResetToken(tokenHash);
|
|
4549
4595
|
if (!storedToken) throw ApiError.badRequest("Invalid or expired reset token", "INVALID_TOKEN");
|
|
4550
4596
|
const passwordHash = await ops.hashPassword(password);
|
|
4551
|
-
await authRepo
|
|
4597
|
+
await replaceUserPassword(authRepo, storedToken.uid, passwordHash);
|
|
4552
4598
|
await authRepo.markPasswordResetTokenUsed(tokenHash);
|
|
4553
|
-
await authRepo.deleteAllRefreshTokensForUser(storedToken.uid);
|
|
4554
|
-
await authRepo.setTokensValidAfter?.(storedToken.uid, /* @__PURE__ */ new Date()).catch(() => void 0);
|
|
4555
4599
|
if (ops.onPasswordReset) ops.onPasswordReset(storedToken.uid).catch((err) => {
|
|
4556
4600
|
logger.error("[AuthHooks] onPasswordReset error", { error: err instanceof Error ? err.message : err });
|
|
4557
4601
|
});
|
|
@@ -4574,9 +4618,7 @@ function createAuthRoutes(config) {
|
|
|
4574
4618
|
const passwordValidation = ops.validatePasswordStrength(newPassword);
|
|
4575
4619
|
if (!passwordValidation.valid) throw ApiError.badRequest(passwordValidation.errors.join(". "), "WEAK_PASSWORD");
|
|
4576
4620
|
const passwordHash = await ops.hashPassword(newPassword);
|
|
4577
|
-
await authRepo
|
|
4578
|
-
await authRepo.deleteAllRefreshTokensForUser(user.id);
|
|
4579
|
-
await authRepo.setTokensValidAfter?.(user.id, /* @__PURE__ */ new Date()).catch(() => void 0);
|
|
4621
|
+
await replaceUserPassword(authRepo, user.id, passwordHash);
|
|
4580
4622
|
return c.json({
|
|
4581
4623
|
success: true,
|
|
4582
4624
|
message: "Password has been changed successfully"
|
|
@@ -4809,6 +4851,10 @@ function buildBuiltinAuthCapabilities(inputs) {
|
|
|
4809
4851
|
* 1. Collection-level hook (`auth.onResetPassword` on the collection)
|
|
4810
4852
|
* 2. Backend-level hook (`AuthHooks.onAdminResetPassword`)
|
|
4811
4853
|
* 3. Built-in default (send reset email, or generate temp password)
|
|
4854
|
+
*
|
|
4855
|
+
* Whichever of them runs, the account's existing sessions end, and a
|
|
4856
|
+
* `temporaryPassword` in its result becomes the account's password before the
|
|
4857
|
+
* response shows it to the admin.
|
|
4812
4858
|
*/
|
|
4813
4859
|
/**
|
|
4814
4860
|
* Create a standalone admin route for resetting user passwords.
|
|
@@ -4834,14 +4880,10 @@ function createResetPasswordRoute(config) {
|
|
|
4834
4880
|
let temporaryPassword;
|
|
4835
4881
|
let emailDeliveryFailed = false;
|
|
4836
4882
|
const body = await c.req.json().catch(() => ({}));
|
|
4837
|
-
|
|
4838
|
-
|
|
4839
|
-
const validation = ops.validatePasswordStrength(
|
|
4883
|
+
const chosenPassword = body.password ? body.password : void 0;
|
|
4884
|
+
if (chosenPassword) {
|
|
4885
|
+
const validation = ops.validatePasswordStrength(chosenPassword);
|
|
4840
4886
|
if (!validation.valid) throw ApiError.badRequest(`Password too weak: ${validation.errors.join(", ")}`);
|
|
4841
|
-
const passwordHash = await ops.hashPassword(password);
|
|
4842
|
-
await authRepo.updatePassword(existing.id, passwordHash);
|
|
4843
|
-
temporaryPassword = void 0;
|
|
4844
|
-
invitationSent = false;
|
|
4845
4887
|
} else if (collectionAuthConfig?.onResetPassword) {
|
|
4846
4888
|
const isEmailConfigured = !!(emailService && emailService.isConfigured());
|
|
4847
4889
|
const hookResult = await collectionAuthConfig.onResetPassword(existing.id, {
|
|
@@ -4861,42 +4903,38 @@ function createResetPasswordRoute(config) {
|
|
|
4861
4903
|
});
|
|
4862
4904
|
temporaryPassword = hookResult.temporaryPassword;
|
|
4863
4905
|
invitationSent = hookResult.invitationSent ?? false;
|
|
4864
|
-
} else if (!!(emailService && emailService.isConfigured()))
|
|
4906
|
+
} else if (!!(emailService && emailService.isConfigured())) {
|
|
4865
4907
|
const token = generateSecureToken();
|
|
4866
4908
|
const tokenHash = hashToken$1(token);
|
|
4867
4909
|
const expiresAt = new Date(Date.now() + 3600 * 1e3);
|
|
4868
4910
|
await authRepo.createPasswordResetToken(existing.id, tokenHash, expiresAt);
|
|
4869
|
-
|
|
4870
|
-
|
|
4871
|
-
|
|
4872
|
-
|
|
4873
|
-
|
|
4874
|
-
|
|
4875
|
-
|
|
4876
|
-
|
|
4877
|
-
|
|
4878
|
-
|
|
4879
|
-
|
|
4880
|
-
|
|
4881
|
-
|
|
4882
|
-
|
|
4883
|
-
|
|
4884
|
-
|
|
4885
|
-
|
|
4886
|
-
|
|
4887
|
-
|
|
4888
|
-
|
|
4889
|
-
|
|
4890
|
-
|
|
4891
|
-
|
|
4892
|
-
|
|
4893
|
-
|
|
4894
|
-
|
|
4895
|
-
|
|
4896
|
-
const passwordHash = await ops.hashPassword(clearPassword);
|
|
4897
|
-
await authRepo.updatePassword(existing.id, passwordHash);
|
|
4898
|
-
temporaryPassword = clearPassword;
|
|
4899
|
-
}
|
|
4911
|
+
try {
|
|
4912
|
+
const setPasswordUrl = `${emailConfig?.resetPasswordUrl || ""}/reset-password?token=${token}`;
|
|
4913
|
+
const { appName, logoUrl } = resolveEmailBranding(emailConfig);
|
|
4914
|
+
const templateFn = emailConfig?.templates?.passwordReset;
|
|
4915
|
+
const emailContent = templateFn ? templateFn(setPasswordUrl, {
|
|
4916
|
+
email: existing.email,
|
|
4917
|
+
displayName: existing.displayName
|
|
4918
|
+
}) : getPasswordResetTemplate(setPasswordUrl, {
|
|
4919
|
+
email: existing.email,
|
|
4920
|
+
displayName: existing.displayName
|
|
4921
|
+
}, appName, logoUrl);
|
|
4922
|
+
await emailService.send({
|
|
4923
|
+
to: existing.email,
|
|
4924
|
+
subject: emailContent.subject,
|
|
4925
|
+
html: emailContent.html,
|
|
4926
|
+
text: emailContent.text
|
|
4927
|
+
});
|
|
4928
|
+
invitationSent = true;
|
|
4929
|
+
} catch (emailError) {
|
|
4930
|
+
logger.error("Failed to send reset email", { error: emailError instanceof Error ? emailError.message : emailError });
|
|
4931
|
+
temporaryPassword = generateSecurePassword();
|
|
4932
|
+
emailDeliveryFailed = true;
|
|
4933
|
+
}
|
|
4934
|
+
} else temporaryPassword = generateSecurePassword();
|
|
4935
|
+
const newPassword = chosenPassword ?? temporaryPassword;
|
|
4936
|
+
if (newPassword) await replaceUserPassword(authRepo, existing.id, await ops.hashPassword(newPassword));
|
|
4937
|
+
else await revokeAllSessions(authRepo, existing.id);
|
|
4900
4938
|
const userRoles = await authRepo.getUserRoleIds(existing.id);
|
|
4901
4939
|
return c.json({
|
|
4902
4940
|
user: {
|
|
@@ -5124,12 +5162,14 @@ function createAdminUsersRoute(config) {
|
|
|
5124
5162
|
const updates = {};
|
|
5125
5163
|
if (email !== void 0) updates.email = normalizeEmail(email);
|
|
5126
5164
|
if (displayName !== void 0) updates.displayName = displayName;
|
|
5165
|
+
let passwordHash;
|
|
5127
5166
|
if (password) {
|
|
5128
5167
|
const validation = ops.validatePasswordStrength(password);
|
|
5129
5168
|
if (!validation.valid) throw ApiError.badRequest(`Password too weak: ${validation.errors.join(". ")}`);
|
|
5130
|
-
|
|
5169
|
+
passwordHash = await ops.hashPassword(password);
|
|
5131
5170
|
}
|
|
5132
5171
|
if (Object.keys(updates).length > 0) await authRepo.updateUser(uid, updates);
|
|
5172
|
+
if (passwordHash) await replaceUserPassword(authRepo, uid, passwordHash);
|
|
5133
5173
|
if (roles !== void 0 && Array.isArray(roles)) {
|
|
5134
5174
|
const wasAdmin = (await authRepo.getUserRoleIds(uid)).includes("admin");
|
|
5135
5175
|
const willBeAdmin = roles.includes("admin");
|
|
@@ -5396,8 +5436,9 @@ function createUserManagementFromRepo(repo, resolvedOps) {
|
|
|
5396
5436
|
if (data.displayName !== void 0) updateData.displayName = data.displayName;
|
|
5397
5437
|
if (data.photoUrl !== void 0) updateData.photoUrl = data.photoUrl;
|
|
5398
5438
|
if (data.metadata !== void 0) updateData.metadata = data.metadata;
|
|
5399
|
-
|
|
5439
|
+
const passwordHash = data.password ? await resolvedOps.hashPassword(data.password) : void 0;
|
|
5400
5440
|
const user = await repo.updateUser(id, updateData);
|
|
5441
|
+
if (user && passwordHash) await replaceUserPassword(repo, id, passwordHash);
|
|
5401
5442
|
return user ? toAuthUserData(user) : null;
|
|
5402
5443
|
},
|
|
5403
5444
|
async deleteUser(id) {
|
|
@@ -7137,4 +7178,4 @@ var auth_exports = /* @__PURE__ */ __exportAll({
|
|
|
7137
7178
|
//#endregion
|
|
7138
7179
|
export { requireAdmin as $, defaultAuthLimiter as A, resolveEmailLinkBase as B, providerVerifiedEmail as C, raw as Ct, DEFAULT_FUNCTIONS_ANONYMOUS_LIMIT as D, isPublicStoragePath as Dt, isBootstrapWindowOpen as E, PUBLIC_STORAGE_PREFIX as Et, extractLinks as F, createAdapterAuthMiddleware as G, hashPassword as H, registerDevEmailSink as I, extractUserFromToken as J, createAuthMiddleware as K, SMTPEmailService as L, activeDevEmailSink as M, clearActiveDevEmailSink as N, createDataRateLimiter as O, createDevEmailSink as P, queryTokenAuth as Q, createEmailService as R, pkceTokenParams as S, html as St, createBuiltinAuthAdapter as T, isOperationAllowed as Tt, validatePasswordStrength as U, resolveAuthHooks as V, verifyPassword as W, optionalAuth as X, fileTokenAuth as Y, publicObjectAuth as Z, verifyOidcIdToken as _, getUserInvitationTemplate as _t, resolveRateLimitStoreKind as a, validateApiKey as at, createGoogleProvider as b, RawHtml as bt, createSlackProvider as c, SERVICE_IDENTITY as ct, createDiscordProvider as d, completeUserCreation as dt, requireAuth as et, createTwitterProvider as f, generateSecurePassword as ft, tryVerifyOidcIdToken as g, getPasswordResetTemplate as gt, createMicrosoftProvider as h, getMagicLinkTemplate as ht, createApiKeyStore as i, isApiKeyToken as it, MemoryRateLimitStore as j, createRateLimiter as k, createBitbucketProvider as l, scopeDataDriver as lt, createAppleProvider as m, getEmailVerificationTemplate as mt, createCustomAuthAdapter as n, createFunctionApiKeyGuard as nt, createSqlRateLimitStore as o, extractBearerToken as ot, createFacebookProvider as p, getEmailOtpTemplate as pt, createRequireAuth as q, createApiKeyRoutes as r, createStorageApiKeyGuard as rt, createSpotifyProvider as s, safeCompare as st, auth_exports as t, createApiKeyPreAuth as tt, createGitLabProvider as u, supportsRlsScoping as ut, createGitHubProvider as v, getWelcomeEmailTemplate as vt, createJwksRoutes as w, httpMethodToOperation as wt, oauthCodeFlowSchema as x, escapeHtml as xt, createLinkedinProvider as y, resolveEmailBranding as yt, assertEmailLinkBases as z };
|
|
7139
7180
|
|
|
7140
|
-
//# sourceMappingURL=auth-
|
|
7181
|
+
//# sourceMappingURL=auth-BS4WBA10.js.map
|