1import type { DesktopSshPasswordPromptRequest } from "@t3tools/contracts";
2import { useEffect, useId, useRef, useState } from "react";
3
4import { Button } from "../ui/button";
5import {
6 Dialog,
7 DialogDescription,
8 DialogFooter,
9 DialogHeader,
10 DialogPanel,
11 DialogPopup,
12 DialogTitle,
13} from "../ui/dialog";
14import { Input } from "../ui/input";
15
16function describeSshTarget(request: DesktopSshPasswordPromptRequest): string {
17 return request.username ? `${request.username}@${request.destination}` : request.destination;
18}
19
20function formatRemainingSeconds(seconds: number): string {
21 return `${Math.floor(seconds / 60)}:${String(seconds % 60).padStart(2, "0")}`;
22}
23
24function getPromptErrorMessage(error: unknown): string {
25 const message = error instanceof Error ? error.message : "SSH password prompt failed.";
26 return message.includes("expired") || message.includes("no longer pending")
27 ? "This SSH password prompt expired. Try connecting again."
28 : message;
29}
30
31export function SshPasswordPromptDialog() {
32 const [queue, setQueue] = useState<readonly DesktopSshPasswordPromptRequest[]>([]);
33 const currentRequest = queue[0] ?? null;
34
35 useEffect(() => {
36 const bridge = window.desktopBridge;
37 if (!bridge?.onSshPasswordPrompt) {
38 return;
39 }
40
41 return bridge.onSshPasswordPrompt((request) => {
42 setQueue((currentQueue) => [...currentQueue, request]);
43 });
44 }, []);
45
46 if (!currentRequest) {
47 return null;
48 }
49
50 return (
51 <ActiveSshPasswordPrompt
52 key={currentRequest.requestId}
53 request={currentRequest}
54 onRemove={(requestId) => {
55 setQueue((currentQueue) =>
56 currentQueue[0]?.requestId === requestId ? currentQueue.slice(1) : currentQueue,
57 );
58 }}
59 />
60 );
61}
62
63function ActiveSshPasswordPrompt({
64 request,
65 onRemove,
66}: {
67 readonly request: DesktopSshPasswordPromptRequest;
68 readonly onRemove: (requestId: string) => void;
69}) {
70 const [password, setPassword] = useState("");
71 const [isResponding, setIsResponding] = useState(false);
72 const [now, setNow] = useState(() => Date.now());
73 const [responseError, setResponseError] = useState<string | null>(null);
74 const inputRef = useRef<HTMLInputElement | null>(null);
75 const isRespondingRef = useRef(false);
76 const formId = useId();
77
78 useEffect(() => {
79 const frame = window.requestAnimationFrame(() => {
80 inputRef.current?.focus();
81 inputRef.current?.select();
82 });
83 return () => {
84 window.cancelAnimationFrame(frame);
85 };
86 }, []);
87
88 useEffect(() => {
89 const interval = window.setInterval(() => {
90 setNow(Date.now());
91 }, 1_000);
92 return () => {
93 window.clearInterval(interval);
94 };
95 }, []);
96
97 const expiresAtMs = Date.parse(request.expiresAt);
98 const remainingMs = Number.isFinite(expiresAtMs) ? Math.max(0, expiresAtMs - now) : null;
99 const isExpired = remainingMs !== null && remainingMs <= 0;
100 const remainingSeconds = remainingMs === null ? null : Math.ceil(remainingMs / 1_000);
101 const remainingLabel =
102 remainingSeconds === null ? null : formatRemainingSeconds(remainingSeconds);
103 const visibleResponseError = isExpired
104 ? "This SSH password prompt expired. Try connecting again."
105 : responseError;
106
107 const respond = async (nextPassword: string | null) => {
108 if (isRespondingRef.current) {
109 return;
110 }
111
112 const requestId = request.requestId;
113 if (nextPassword !== null && isExpired) {
114 setResponseError("This SSH password prompt expired. Try connecting again.");
115 return;
116 }
117
118 isRespondingRef.current = true;
119 setIsResponding(true);
120 setResponseError(null);
121 try {
122 await window.desktopBridge?.resolveSshPasswordPrompt(requestId, nextPassword);
123 onRemove(requestId);
124 } catch (error) {
125 if (nextPassword === null) {
126 onRemove(requestId);
127 } else {
128 setResponseError(getPromptErrorMessage(error));
129 }
130 } finally {
131 isRespondingRef.current = false;
132 setIsResponding(false);
133 }
134 };
135
136 const dismissExpiredPrompt = () => {
137 onRemove(request.requestId);
138 };
139
140 const cancelPrompt = () => {
141 if (isExpired) {
142 dismissExpiredPrompt();
143 return;
144 }
145 void respond(null);
146 };
147
148 const target = describeSshTarget(request);
149
150 return (
151 <Dialog
152 open
153 onOpenChange={(open) => {
154 if (!open) {
155 cancelPrompt();
156 }
157 }}
158 >
159 <DialogPopup className="max-w-md" showCloseButton={false}>
160 <DialogHeader>
161 <DialogTitle>SSH Password Required</DialogTitle>
162 <DialogDescription>
163 T3 needs your SSH password to connect to <code>{target}</code>. The password is passed
164 to the local SSH process for this connection attempt and is not saved by The Knife.
165 </DialogDescription>
166 </DialogHeader>
167 <DialogPanel scrollFade={false}>
168 <form
169 className="space-y-3"
170 id={formId}
171 onSubmit={(event) => {
172 event.preventDefault();
173 void respond(password);
174 }}
175 >
176 <div className="space-y-2">
177 <div className="flex items-center justify-between gap-3">
178 <p className="text-sm font-medium text-foreground">{request.prompt}</p>
179 {remainingLabel ? (
180 <span
181 className={
182 isExpired
183 ? "shrink-0 text-xs font-medium text-destructive"
184 : "shrink-0 text-xs text-muted-foreground"
185 }
186 >
187 {isExpired ? "Expired" : remainingLabel}
188 </span>
189 ) : null}
190 </div>
191 <Input
192 ref={inputRef}
193 autoComplete="current-password"
194 disabled={isResponding || isExpired}
195 name="ssh-password"
196 type="password"
197 value={password}
198 onChange={(event) => setPassword(event.target.value)}
199 />
200 </div>
201 {visibleResponseError ? (
202 <p className="text-sm text-destructive">{visibleResponseError}</p>
203 ) : (
204 <p className="text-sm text-muted-foreground">
205 Use SSH keys to avoid repeated password prompts on new SSH sessions.
206 </p>
207 )}
208 </form>
209 </DialogPanel>
210 <DialogFooter>
211 <Button disabled={isResponding} type="button" variant="outline" onClick={cancelPrompt}>
212 {isExpired ? "Dismiss" : "Cancel"}
213 </Button>
214 <Button disabled={isResponding || isExpired} form={formId} type="submit">
215 Continue
216 </Button>
217 </DialogFooter>
218 </DialogPopup>
219 </Dialog>
220 );
221}