From 4fbce4ad68d4c2a20eae13a8ee8d6277404990fa Mon Sep 17 00:00:00 2001 From: Bailey Townsend Date: Fri, 10 Jul 2026 21:20:56 +0000 Subject: [PATCH] bugfix: resolves an issue when reseting a user's password --- server/src/pdsClient.ts | 9 ++++++--- web/src/AccountsPanel.tsx | 41 ++++++++++++++++++++++++++++++++++++++++- web/src/api.ts | 2 +- web/src/style.css | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ server/src/routes/accounts.ts | 4 ++-- 5 file(s) changed, 99 insertion(s)(+), 7 deletion(s)(-) diff --git a/server/src/pdsClient.ts b/server/src/pdsClient.ts --- a/server/src/pdsClient.ts +++ b/server/src/pdsClient.ts @@ -1,3 +1,4 @@ +import { randomBytes } from "node:crypto"; import WebSocket from "ws"; import { decodeMultiple } from "cbor-x"; @@ -134,11 +135,13 @@ }); } - async resetAccountPassword(did: string) { - return this.xrpc("com.atproto.admin.sendAccountPasswordResetEmail", { + async resetAccountPassword(did: string): Promise<{ password: string }> { + const password = randomBytes(16).toString("base64url"); + await this.xrpc("com.atproto.admin.updateAccountPassword", { method: "POST", - body: JSON.stringify({ did }), + body: JSON.stringify({ did, password }), }); + return { password }; } /** diff --git a/web/src/AccountsPanel.tsx b/web/src/AccountsPanel.tsx --- a/web/src/AccountsPanel.tsx +++ b/web/src/AccountsPanel.tsx @@ -25,6 +25,9 @@ const [busyDid, setBusyDid] = useState(null); const [confirmDid, setConfirmDid] = useState(null); const [menuDid, setMenuDid] = useState(null); + const [passwordResult, setPasswordResult] = useState<{ handle: string; password: string } | null>( + null, + ); const [toast, showToast] = useToast(); const copy = (text: string, what: string, e: React.MouseEvent) => { @@ -50,6 +53,15 @@ document.removeEventListener("keydown", close); }; }, [menuDid]); + + useEffect(() => { + if (!passwordResult) return; + const close = (e: KeyboardEvent) => { + if (e.key === "Escape") setPasswordResult(null); + }; + document.addEventListener("keydown", close); + return () => document.removeEventListener("keydown", close); + }, [passwordResult]); const refresh = useCallback(() => { setLoading(true); @@ -91,7 +103,13 @@ setBusyDid(did); setConfirmDid(null); try { - await api[action](did); + if (action === "resetPassword") { + const { password } = await api.resetPassword(did); + const handle = accounts.find((a) => a.did === did)?.handle ?? did; + setPasswordResult({ handle, password }); + } else { + await api[action](did); + } refresh(); } catch (e) { setError((e as Error).message); @@ -305,6 +323,27 @@ {toast} + {passwordResult && ( +
setPasswordResult(null)}> +
e.stopPropagation()} + > +

password reset for @{passwordResult.handle}

+
{passwordResult.password}
+

+ this password won’t be shown again — copy it now. +

+
+ + +
+
+
+ )} ); diff --git a/web/src/api.ts b/web/src/api.ts --- a/web/src/api.ts +++ b/web/src/api.ts @@ -56,7 +56,7 @@ }, takedown: (did: string) => req<{ ok: true }>(`/api/accounts/${encodeURIComponent(did)}/takedown`, { method: "POST" }), enable: (did: string) => req<{ ok: true }>(`/api/accounts/${encodeURIComponent(did)}/enable`, { method: "POST" }), - resetPassword: (did: string) => req<{ ok: true }>(`/api/accounts/${encodeURIComponent(did)}/reset-password`, { method: "POST" }), + resetPassword: (did: string) => req<{ ok: true; password: string }>(`/api/accounts/${encodeURIComponent(did)}/reset-password`, { method: "POST" }), cursorStatus: () => req("/api/status/cursor"), requestCrawl: () => req<{ ok: true }>("/api/status/request-crawl", { method: "POST" }), passkeyRegisterOptions: (enrollToken?: string) => diff --git a/web/src/style.css b/web/src/style.css --- a/web/src/style.css +++ b/web/src/style.css @@ -443,6 +443,56 @@ } } +/* sits above menus (20) but below the toast (30) so "copied" feedback shows */ +.modal-backdrop { + position: fixed; + inset: 0; + z-index: 25; + display: flex; + align-items: center; + justify-content: center; + padding: 16px; + background: color-mix(in srgb, var(--shadow) 55%, transparent); +} + +.modal { + width: 100%; + max-width: 28rem; + padding: 16px 18px; + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius); + box-shadow: 0 4px 14px var(--shadow); +} + +.modal h3 { + margin: 0 0 12px; + overflow-wrap: anywhere; +} + +.password-box { + font-family: monospace; + font-size: 15px; + padding: 10px 12px; + border: 1px solid var(--border); + border-radius: var(--radius); + background: color-mix(in srgb, var(--text) 4%, transparent); + overflow-wrap: anywhere; + user-select: all; +} + +.modal-warn { + color: var(--text-dim); + font-size: 13px; + margin: 10px 0 14px; +} + +.modal-actions { + display: flex; + gap: 8px; + justify-content: flex-end; +} + .invite-actions { display: flex; gap: 6px; diff --git a/server/src/routes/accounts.ts b/server/src/routes/accounts.ts --- a/server/src/routes/accounts.ts +++ b/server/src/routes/accounts.ts @@ -149,8 +149,8 @@ app.post("/api/accounts/:did/reset-password", { preHandler: requireAuth }, async (req) => { const { did } = req.params as { did: string }; - await pds.resetAccountPassword(did); + const { password } = await pds.resetAccountPassword(did); await recordAction({ operator: req.session.operator!, action: "reset-password", target: did }); - return { ok: true }; + return { ok: true, password }; }); } -- tangled.sh