Skip to content

Commit 614ff02

Browse files
committed
Fix hosted pairing token retry, SSH prompt double-click, and redundant settings call
- Track token submission in HostedPairingRouteSurface so retry after a consumed one-time token shows a clear message instead of a confusing auth error. - Add isResponding guard to SshPasswordPromptDialog to prevent rapid double-click from corrupting the prompt queue and silently dropping queued prompts. - Refactor applyDesktopTailscaleServeEnabled to accept already-resolved DesktopSettings instead of re-deriving them from the same inputs.
1 parent fc49669 commit 614ff02

3 files changed

Lines changed: 25 additions & 13 deletions

File tree

apps/desktop/src/main.ts

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -402,13 +402,14 @@ async function applyDesktopServerExposureMode(
402402
return getDesktopServerExposureState();
403403
}
404404

405-
async function applyDesktopTailscaleServeEnabled(input: {
406-
readonly enabled: boolean;
407-
readonly port?: number;
408-
}): Promise<DesktopServerExposureState> {
409-
desktopSettings = setDesktopTailscaleServePreference(desktopSettings, input);
405+
async function applyDesktopTailscaleServeEnabled(
406+
resolvedSettings: typeof desktopSettings,
407+
): Promise<DesktopServerExposureState> {
408+
desktopSettings = resolvedSettings;
410409
writeDesktopSettings(DESKTOP_SETTINGS_PATH, desktopSettings);
411-
relaunchDesktopApp(input.enabled ? "tailscale-serve-enabled" : "tailscale-serve-disabled");
410+
relaunchDesktopApp(
411+
desktopSettings.tailscaleServeEnabled ? "tailscale-serve-enabled" : "tailscale-serve-disabled",
412+
);
412413
return getDesktopServerExposureState();
413414
}
414415

@@ -1751,10 +1752,7 @@ function registerIpcHandlers(): void {
17511752
if (nextSettings === desktopSettings) {
17521753
return getDesktopServerExposureState();
17531754
}
1754-
return applyDesktopTailscaleServeEnabled({
1755-
enabled: input.enabled,
1756-
port: nextSettings.tailscaleServePort,
1757-
});
1755+
return applyDesktopTailscaleServeEnabled(nextSettings);
17581756
});
17591757

17601758
ipcMain.removeHandler(GET_ADVERTISED_ENDPOINTS_CHANNEL);

apps/web/src/components/auth/PairingRouteSurface.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,7 @@ export function HostedPairingRouteSurface() {
166166
const [status, setStatus] = useState<"pairing" | "paired" | "error">("pairing");
167167
const [message, setMessage] = useState("Connecting to this backend.");
168168
const submitAttemptedRef = useRef(false);
169+
const tokenSubmittedRef = useRef(false);
169170

170171
const submitHostedPairingRequest = useCallback(async () => {
171172
const request = hostedPairingRequestRef.current;
@@ -176,9 +177,18 @@ export function HostedPairingRouteSurface() {
176177
return;
177178
}
178179

180+
if (tokenSubmittedRef.current) {
181+
setStatus("error");
182+
setMessage(
183+
"The pairing token may have already been used. Please request a new pairing link.",
184+
);
185+
return;
186+
}
187+
179188
setStatus("pairing");
180189
setMessage("Connecting to this backend.");
181190

191+
tokenSubmittedRef.current = true;
182192
try {
183193
const record = await addSavedEnvironment({
184194
label: request.label,

apps/web/src/components/desktop/SshPasswordPromptDialog.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ function describeSshTarget(request: DesktopSshPasswordPromptRequest): string {
2020
export function SshPasswordPromptDialog() {
2121
const [queue, setQueue] = useState<readonly DesktopSshPasswordPromptRequest[]>([]);
2222
const [password, setPassword] = useState("");
23+
const [isResponding, setIsResponding] = useState(false);
2324
const currentRequest = queue[0] ?? null;
2425
const inputRef = useRef<HTMLInputElement | null>(null);
2526

@@ -50,17 +51,20 @@ export function SshPasswordPromptDialog() {
5051
}, [currentRequest]);
5152

5253
const respond = async (nextPassword: string | null) => {
53-
if (!currentRequest) {
54+
if (!currentRequest || isResponding) {
5455
return;
5556
}
5657

5758
const requestId = currentRequest.requestId;
59+
setIsResponding(true);
5860
setQueue((currentQueue) => currentQueue.slice(1));
5961
setPassword("");
6062
try {
6163
await window.desktopBridge?.resolveSshPasswordPrompt(requestId, nextPassword);
6264
} catch (error) {
6365
console.error("Failed to resolve SSH password prompt.", error);
66+
} finally {
67+
setIsResponding(false);
6468
}
6569
};
6670

@@ -101,10 +105,10 @@ export function SshPasswordPromptDialog() {
101105
</p>
102106
</DialogPanel>
103107
<DialogFooter>
104-
<Button variant="outline" onClick={() => void respond(null)}>
108+
<Button disabled={isResponding} variant="outline" onClick={() => void respond(null)}>
105109
Cancel
106110
</Button>
107-
<Button onClick={() => void respond(password)} type="button">
111+
<Button disabled={isResponding} onClick={() => void respond(password)} type="button">
108112
Continue
109113
</Button>
110114
</DialogFooter>

0 commit comments

Comments
 (0)