Graphical PDS migrator for AT Protocol
1import { useState } from "preact/hooks";
2import { Link } from "../components/Link.tsx";
3
4interface PlcUpdateStep {
5 name: string;
6 status: "pending" | "in-progress" | "verifying" | "completed" | "error";
7 error?: string;
8}
9
10// Content chunks for the description
11const contentChunks = [
12 {
13 title: "Welcome to Key Management",
14 subtitle: "BOARDING PASS - SECTION A",
15 content: (
16 <>
17 <div class="passenger-info text-slate-600 dark:text-slate-300 font-mono text-sm mb-4">
18 GATE: KEY-01 • SEAT: DID-1A
19 </div>
20 <p class="text-slate-700 dark:text-slate-300 mb-4">
21 This tool helps you add a new rotation key to your{" "}
22 <Link
23 href="https://web.plc.directory/"
24 isExternal
25 class="text-blue-600 dark:text-blue-400"
26 >
27 PLC (Public Ledger of Credentials)
28 </Link>
29 . Having control of a rotation key gives you sovereignty over your DID
30 (Decentralized Identifier).
31 </p>
32 </>
33 ),
34 },
35 {
36 title: "Key Benefits",
37 subtitle: "BOARDING PASS - SECTION B",
38 content: (
39 <>
40 <div class="passenger-info text-slate-600 dark:text-slate-300 font-mono text-sm mb-4">
41 GATE: KEY-02 • SEAT: DID-1B
42 </div>
43 <div class="space-y-4">
44 <div class="p-3 bg-white dark:bg-slate-800 rounded-lg border border-slate-200 dark:border-slate-700">
45 <h4 class="font-mono font-bold text-amber-500 dark:text-amber-400 mb-2">
46 PROVIDER MOBILITY ✈️
47 </h4>
48 <p class="text-slate-700 dark:text-slate-300">
49 Change your PDS without losing your identity, protecting you if
50 your provider becomes hostile.
51 </p>
52 </div>
53 <div class="p-3 bg-white dark:bg-slate-800 rounded-lg border border-slate-200 dark:border-slate-700">
54 <h4 class="font-mono font-bold text-amber-500 dark:text-amber-400 mb-2">
55 IDENTITY CONTROL ✨
56 </h4>
57 <p class="text-slate-700 dark:text-slate-300">
58 Modify your DID document independently of your provider.
59 </p>
60 </div>
61 <div class="p-3 bg-white dark:bg-slate-800 rounded-lg border border-slate-200 dark:border-slate-700">
62 <p class="text-slate-700 dark:text-slate-300">
63 💡 It's good practice to have a rotation key so you can move to a
64 different provider if you need to.
65 </p>
66 </div>
67 </div>
68 </>
69 ),
70 },
71 {
72 title: "⚠️ CRITICAL SECURITY WARNING",
73 subtitle: "BOARDING PASS - SECTION C",
74 content: (
75 <>
76 <div class="passenger-info text-slate-600 dark:text-slate-300 font-mono text-sm mb-4">
77 GATE: KEY-03 • SEAT: DID-1C
78 </div>
79 <div class="p-4 bg-red-50 dark:bg-red-900 rounded-lg border-2 border-red-500 dark:border-red-700 mb-4">
80 <div class="flex items-center mb-3">
81 <span class="text-2xl mr-2">⚠️</span>
82 <h4 class="font-mono font-bold text-red-700 dark:text-red-400 text-lg">
83 NON-REVOCABLE KEY WARNING
84 </h4>
85 </div>
86 <div class="space-y-3 text-red-700 dark:text-red-300">
87 <p class="font-bold">
88 This rotation key CANNOT BE DISABLED OR DELETED once added:
89 </p>
90 <ul class="list-disc pl-5 space-y-2">
91 <li>
92 If compromised, the attacker can take complete control of your
93 account and identity
94 </li>
95 <li>
96 Malicious actors with this key have COMPLETE CONTROL of your
97 account and identity
98 </li>
99 <li>
100 Store securely, like a password (e.g. <strong>DO NOT</strong>
101 {" "}
102 keep it in Notes or any easily accessible app on an unlocked
103 device).
104 </li>
105 </ul>
106 </div>
107 </div>
108 <div class="p-3 bg-white dark:bg-slate-800 rounded-lg border border-slate-200 dark:border-slate-700">
109 <p class="text-slate-700 dark:text-slate-300">
110 💡 We recommend adding a custom rotation key but recommend{" "}
111 <strong class="italic">against</strong>{" "}
112 having more than one custom rotation key, as more than one increases
113 risk.
114 </p>
115 </div>
116 </>
117 ),
118 },
119 {
120 title: "Technical Overview",
121 subtitle: "BOARDING PASS - SECTION C",
122 content: (
123 <>
124 <div class="passenger-info text-slate-600 dark:text-slate-300 font-mono text-sm mb-4">
125 GATE: KEY-03 • SEAT: DID-1C
126 </div>
127 <div class="p-4 bg-white dark:bg-slate-800 rounded-lg border border-slate-200 dark:border-slate-700">
128 <div class="flex items-center mb-3">
129 <span class="text-lg mr-2">📝</span>
130 <h4 class="font-mono font-bold text-amber-500 dark:text-amber-400">
131 TECHNICAL DETAILS
132 </h4>
133 </div>
134 <p class="text-slate-700 dark:text-slate-300">
135 The rotation key is a did:key that will be added to your PLC
136 document's rotationKeys array. This process uses the AT Protocol's
137 PLC operations to update your DID document.
138 <Link
139 href="https://web.plc.directory/"
140 class="block ml-1 text-blue-600 dark:text-blue-400"
141 isExternal
142 >
143 Learn more about did:plc
144 </Link>
145 </p>
146 </div>
147 </>
148 ),
149 },
150];
151
152export default function PlcUpdateProgress() {
153 const [hasStarted, setHasStarted] = useState(false);
154 const [currentChunkIndex, setCurrentChunkIndex] = useState(0);
155 const [steps, setSteps] = useState<PlcUpdateStep[]>([
156 { name: "Generate Rotation Key", status: "pending" },
157 { name: "Start PLC update", status: "pending" },
158 { name: "Complete PLC update", status: "pending" },
159 ]);
160 const [generatedKey, setGeneratedKey] = useState<string>("");
161 const [keyJson, setKeyJson] = useState<any>(null);
162 const [emailToken, setEmailToken] = useState<string>("");
163 const [hasDownloadedKey, setHasDownloadedKey] = useState(false);
164 const [downloadedKeyId, setDownloadedKeyId] = useState<string | null>(null);
165
166 const updateStepStatus = (
167 index: number,
168 status: PlcUpdateStep["status"],
169 error?: string,
170 ) => {
171 console.log(
172 `Updating step ${index} to ${status}${
173 error ? ` with error: ${error}` : ""
174 }`,
175 );
176 setSteps((prevSteps) =>
177 prevSteps.map((step, i) =>
178 i === index
179 ? { ...step, status, error }
180 : i > index
181 ? { ...step, status: "pending", error: undefined }
182 : step
183 )
184 );
185 };
186
187 const handleStart = () => {
188 setHasStarted(true);
189 // Automatically start the first step
190 setTimeout(() => {
191 handleGenerateKey();
192 }, 100);
193 };
194
195 const getStepDisplayName = (step: PlcUpdateStep, index: number) => {
196 if (step.status === "completed") {
197 switch (index) {
198 case 0:
199 return "Rotation Key Generated";
200 case 1:
201 return "PLC Operation Requested";
202 case 2:
203 return "PLC Update Completed";
204 }
205 }
206
207 if (step.status === "in-progress") {
208 switch (index) {
209 case 0:
210 return "Generating Rotation Key...";
211 case 1:
212 return "Requesting PLC Operation Token...";
213 case 2:
214 return step.name ===
215 "Enter the code sent to your email to complete PLC update"
216 ? step.name
217 : "Completing PLC Update...";
218 }
219 }
220
221 if (step.status === "verifying") {
222 switch (index) {
223 case 0:
224 return "Verifying Rotation Key Generation...";
225 case 1:
226 return "Verifying PLC Operation Token Request...";
227 case 2:
228 return "Verifying PLC Update Completion...";
229 }
230 }
231
232 return step.name;
233 };
234
235 const handleStartPlcUpdate = async (keyToUse?: string) => {
236 const key = keyToUse || generatedKey;
237
238 // Debug logging
239 console.log("=== PLC Update Debug ===");
240 console.log("Current state:", {
241 keyToUse,
242 generatedKey,
243 key,
244 hasKeyJson: !!keyJson,
245 keyJsonId: keyJson?.publicKeyDid,
246 hasDownloadedKey,
247 downloadedKeyId,
248 steps: steps.map((s) => ({ name: s.name, status: s.status })),
249 });
250
251 if (!key) {
252 console.log("No key generated yet");
253 updateStepStatus(1, "error", "No key generated yet");
254 return;
255 }
256
257 if (!keyJson || keyJson.publicKeyDid !== key) {
258 console.log("Key mismatch or missing:", {
259 hasKeyJson: !!keyJson,
260 keyJsonId: keyJson?.publicKeyDid,
261 expectedKey: key,
262 });
263 updateStepStatus(
264 1,
265 "error",
266 "Please ensure you have the correct key loaded",
267 );
268 return;
269 }
270
271 updateStepStatus(1, "in-progress");
272 try {
273 // First request the token
274 console.log("Requesting PLC token...");
275 const tokenRes = await fetch("/api/plc/token", {
276 method: "GET",
277 });
278 const tokenText = await tokenRes.text();
279 console.log("Token response:", tokenText);
280
281 if (!tokenRes.ok) {
282 try {
283 const json = JSON.parse(tokenText);
284 throw new Error(json.message || "Failed to request PLC token");
285 } catch {
286 throw new Error(tokenText || "Failed to request PLC token");
287 }
288 }
289
290 let data;
291 try {
292 data = JSON.parse(tokenText);
293 if (!data.success) {
294 throw new Error(data.message || "Failed to request token");
295 }
296 } catch {
297 throw new Error("Invalid response from server");
298 }
299
300 console.log("Token request successful, updating UI...");
301 // Update step name to prompt for token
302 setSteps((prevSteps) =>
303 prevSteps.map((step, i) =>
304 i === 1
305 ? {
306 ...step,
307 name: "Enter the code sent to your email to complete PLC update",
308 status: "in-progress",
309 }
310 : step
311 )
312 );
313 } catch (error) {
314 console.error("Token request failed:", error);
315 updateStepStatus(
316 1,
317 "error",
318 error instanceof Error ? error.message : String(error),
319 );
320 }
321 };
322
323 const handleTokenSubmit = async () => {
324 console.log("=== Token Submit Debug ===");
325 console.log("Current state:", {
326 emailToken,
327 generatedKey,
328 keyJsonId: keyJson?.publicKeyDid,
329 steps: steps.map((s) => ({ name: s.name, status: s.status })),
330 });
331
332 if (!emailToken) {
333 console.log("No token provided");
334 updateStepStatus(1, "error", "Please enter the email token");
335 return;
336 }
337
338 if (!keyJson || !keyJson.publicKeyDid) {
339 console.log("Missing key data");
340 updateStepStatus(1, "error", "Key data is missing, please try again");
341 return;
342 }
343
344 // Prevent duplicate submissions
345 if (steps[1].status === "completed" || steps[2].status === "completed") {
346 console.log("Update already completed, preventing duplicate submission");
347 return;
348 }
349
350 updateStepStatus(1, "completed");
351 try {
352 updateStepStatus(2, "in-progress");
353 console.log("Submitting update request with token...");
354 // Send the update request with both key and token
355 const res = await fetch("/api/plc/update", {
356 method: "POST",
357 headers: { "Content-Type": "application/json" },
358 body: JSON.stringify({
359 key: keyJson.publicKeyDid,
360 token: emailToken,
361 }),
362 });
363 const text = await res.text();
364 console.log("Update response:", text);
365
366 let data;
367 try {
368 data = JSON.parse(text);
369 } catch {
370 throw new Error("Invalid response from server");
371 }
372
373 // Check for error responses
374 if (!res.ok || !data.success) {
375 const errorMessage = data.message || "Failed to complete PLC update";
376 console.error("Update failed:", errorMessage);
377 throw new Error(errorMessage);
378 }
379
380 // Only proceed if we have a successful response
381 console.log("Update completed successfully!");
382
383 // Add a delay before marking steps as completed for better UX
384 updateStepStatus(2, "verifying");
385
386 const verifyRes = await fetch("/api/plc/verify", {
387 method: "POST",
388 headers: { "Content-Type": "application/json" },
389 body: JSON.stringify({
390 key: keyJson.publicKeyDid,
391 }),
392 });
393
394 const verifyText = await verifyRes.text();
395 console.log("Verification response:", verifyText);
396
397 let verifyData;
398 try {
399 verifyData = JSON.parse(verifyText);
400 } catch {
401 throw new Error("Invalid verification response from server");
402 }
403
404 if (!verifyRes.ok || !verifyData.success) {
405 const errorMessage = verifyData.message ||
406 "Failed to verify PLC update";
407 console.error("Verification failed:", errorMessage);
408 throw new Error(errorMessage);
409 }
410
411 console.log("Verification successful, marking steps as completed");
412 updateStepStatus(2, "completed");
413 } catch (error) {
414 console.error("Update failed:", error);
415 // Reset the steps to error state
416 updateStepStatus(
417 1,
418 "error",
419 error instanceof Error ? error.message : String(error),
420 );
421 updateStepStatus(2, "pending"); // Reset the final step
422
423 // If token is invalid, we should clear it so user can try again
424 if (
425 error instanceof Error &&
426 error.message.toLowerCase().includes("token is invalid")
427 ) {
428 setEmailToken("");
429 }
430 }
431 };
432
433 const handleDownload = () => {
434 console.log("=== Download Debug ===");
435 console.log("Download started with:", {
436 hasKeyJson: !!keyJson,
437 keyJsonId: keyJson?.publicKeyDid,
438 });
439
440 if (!keyJson) {
441 console.error("No key JSON to download");
442 return;
443 }
444
445 try {
446 const jsonString = JSON.stringify(keyJson, null, 2);
447 const blob = new Blob([jsonString], {
448 type: "application/json",
449 });
450 const url = URL.createObjectURL(blob);
451 const a = document.createElement("a");
452 a.href = url;
453 a.download = `plc-key-${keyJson.publicKeyDid || "unknown"}.json`;
454 a.style.display = "none";
455 document.body.appendChild(a);
456 a.click();
457 document.body.removeChild(a);
458 URL.revokeObjectURL(url);
459
460 console.log("Download completed, proceeding to next step...");
461 setHasDownloadedKey(true);
462 setDownloadedKeyId(keyJson.publicKeyDid);
463
464 // Automatically proceed to the next step after successful download
465 setTimeout(() => {
466 console.log("Auto-proceeding with key:", keyJson.publicKeyDid);
467 handleStartPlcUpdate(keyJson.publicKeyDid);
468 }, 1000);
469 } catch (error) {
470 console.error("Download failed:", error);
471 }
472 };
473
474 const handleGenerateKey = async () => {
475 console.log("=== Generate Key Debug ===");
476 updateStepStatus(0, "in-progress");
477 setKeyJson(null);
478 setGeneratedKey("");
479 setHasDownloadedKey(false);
480 setDownloadedKeyId(null);
481
482 try {
483 console.log("Requesting new key...");
484 const res = await fetch("/api/plc/keys");
485 const text = await res.text();
486 console.log("Key generation response:", text);
487
488 if (!res.ok) {
489 try {
490 const json = JSON.parse(text);
491 throw new Error(json.message || "Failed to generate key");
492 } catch {
493 throw new Error(text || "Failed to generate key");
494 }
495 }
496
497 let data;
498 try {
499 data = JSON.parse(text);
500 } catch {
501 throw new Error("Invalid response from /api/plc/keys");
502 }
503
504 if (!data.publicKeyDid || !data.privateKeyHex) {
505 throw new Error("Key generation failed: missing key data");
506 }
507
508 console.log("Key generated successfully:", {
509 keyId: data.publicKeyDid,
510 });
511
512 setGeneratedKey(data.publicKeyDid);
513 setKeyJson(data);
514 updateStepStatus(0, "completed");
515 } catch (error) {
516 console.error("Key generation failed:", error);
517 updateStepStatus(
518 0,
519 "error",
520 error instanceof Error ? error.message : String(error),
521 );
522 }
523 };
524
525 const getStepIcon = (status: PlcUpdateStep["status"]) => {
526 switch (status) {
527 case "pending":
528 return (
529 <div class="w-8 h-8 rounded-full border-2 border-gray-300 dark:border-gray-600 flex items-center justify-center">
530 <div class="w-3 h-3 rounded-full bg-gray-300 dark:bg-gray-600" />
531 </div>
532 );
533 case "in-progress":
534 return (
535 <div class="w-8 h-8 rounded-full border-2 border-blue-500 border-t-transparent animate-spin flex items-center justify-center">
536 <div class="w-3 h-3 rounded-full bg-blue-500" />
537 </div>
538 );
539 case "verifying":
540 return (
541 <div class="w-8 h-8 rounded-full border-2 border-yellow-500 border-t-transparent animate-spin flex items-center justify-center">
542 <div class="w-3 h-3 rounded-full bg-yellow-500" />
543 </div>
544 );
545 case "completed":
546 return (
547 <div class="w-8 h-8 rounded-full bg-green-500 flex items-center justify-center">
548 <svg
549 class="w-5 h-5 text-white"
550 fill="none"
551 stroke="currentColor"
552 viewBox="0 0 24 24"
553 >
554 <path
555 stroke-linecap="round"
556 stroke-linejoin="round"
557 stroke-width="2"
558 d="M5 13l4 4L19 7"
559 />
560 </svg>
561 </div>
562 );
563 case "error":
564 return (
565 <div class="w-8 h-8 rounded-full bg-red-500 flex items-center justify-center">
566 <svg
567 class="w-5 h-5 text-white"
568 fill="none"
569 stroke="currentColor"
570 viewBox="0 0 24 24"
571 >
572 <path
573 stroke-linecap="round"
574 stroke-linejoin="round"
575 stroke-width="2"
576 d="M6 18L18 6M6 6l12 12"
577 />
578 </svg>
579 </div>
580 );
581 }
582 };
583
584 const getStepClasses = (status: PlcUpdateStep["status"]) => {
585 const baseClasses =
586 "flex items-center space-x-3 p-4 rounded-lg transition-colors duration-200";
587 switch (status) {
588 case "pending":
589 return `${baseClasses} bg-gray-50 dark:bg-gray-800`;
590 case "in-progress":
591 return `${baseClasses} bg-blue-50 dark:bg-blue-900`;
592 case "verifying":
593 return `${baseClasses} bg-yellow-50 dark:bg-yellow-900`;
594 case "completed":
595 return `${baseClasses} bg-green-50 dark:bg-green-900`;
596 case "error":
597 return `${baseClasses} bg-red-50 dark:bg-red-900`;
598 }
599 };
600
601 const requestNewToken = async () => {
602 try {
603 console.log("Requesting new token...");
604 const res = await fetch("/api/plc/token", {
605 method: "GET",
606 });
607 const text = await res.text();
608 console.log("Token request response:", text);
609
610 if (!res.ok) {
611 throw new Error(text || "Failed to request new token");
612 }
613
614 let data;
615 try {
616 data = JSON.parse(text);
617 if (!data.success) {
618 throw new Error(data.message || "Failed to request token");
619 }
620 } catch {
621 throw new Error("Invalid response from server");
622 }
623
624 // Clear any existing error and token
625 setEmailToken("");
626 updateStepStatus(1, "in-progress");
627 updateStepStatus(2, "pending");
628 } catch (error) {
629 console.error("Failed to request new token:", error);
630 updateStepStatus(
631 1,
632 "error",
633 error instanceof Error ? error.message : String(error),
634 );
635 }
636 };
637
638 if (!hasStarted) {
639 return (
640 <div class="space-y-6">
641 <div class="ticket bg-white dark:bg-slate-800 p-6 relative">
642 <div class="boarding-label text-amber-500 dark:text-amber-400 font-mono font-bold tracking-wider text-sm mb-2">
643 {contentChunks[currentChunkIndex].subtitle}
644 </div>
645
646 <div class="flex justify-between items-start mb-4">
647 <h3 class="text-2xl font-mono text-slate-800 dark:text-slate-200">
648 {contentChunks[currentChunkIndex].title}
649 </h3>
650 </div>
651
652 {/* Main Description */}
653 <div class="mb-6">{contentChunks[currentChunkIndex].content}</div>
654
655 {/* Navigation */}
656 <div class="mt-8 border-t border-dashed border-slate-200 dark:border-slate-700 pt-4">
657 <div class="flex justify-between items-center">
658 <button
659 type="button"
660 onClick={() =>
661 setCurrentChunkIndex((prev) => Math.max(0, prev - 1))}
662 class={`px-4 py-2 font-mono text-slate-600 dark:text-slate-400 hover:text-slate-800 dark:hover:text-slate-200 transition-colors duration-200 flex items-center space-x-2 ${
663 currentChunkIndex === 0 ? "invisible" : ""
664 }`}
665 >
666 <svg
667 class="w-5 h-5 rotate-180"
668 fill="none"
669 stroke="currentColor"
670 viewBox="0 0 24 24"
671 >
672 <path
673 stroke-linecap="round"
674 stroke-linejoin="round"
675 stroke-width="2"
676 d="M9 5l7 7-7 7"
677 />
678 </svg>
679 <span>Previous Gate</span>
680 </button>
681
682 {currentChunkIndex === contentChunks.length - 1
683 ? (
684 <button
685 type="button"
686 onClick={handleStart}
687 class="px-6 py-2 bg-amber-500 hover:bg-amber-600 text-white font-mono rounded-md transition-colors duration-200 flex items-center space-x-2"
688 >
689 <span>Begin Key Generation</span>
690 <svg
691 class="w-5 h-5"
692 fill="none"
693 stroke="currentColor"
694 viewBox="0 0 24 24"
695 >
696 <path
697 stroke-linecap="round"
698 stroke-linejoin="round"
699 stroke-width="2"
700 d="M9 5l7 7-7 7"
701 />
702 </svg>
703 </button>
704 )
705 : (
706 <button
707 type="button"
708 onClick={() =>
709 setCurrentChunkIndex((prev) =>
710 Math.min(contentChunks.length - 1, prev + 1)
711 )}
712 class="px-4 py-2 font-mono text-slate-600 dark:text-slate-400 hover:text-slate-800 dark:hover:text-slate-200 transition-colors duration-200 flex items-center space-x-2"
713 >
714 <span>Next Gate</span>
715 <svg
716 class="w-5 h-5"
717 fill="none"
718 stroke="currentColor"
719 viewBox="0 0 24 24"
720 >
721 <path
722 stroke-linecap="round"
723 stroke-linejoin="round"
724 stroke-width="2"
725 d="M9 5l7 7-7 7"
726 />
727 </svg>
728 </button>
729 )}
730 </div>
731
732 {/* Progress Dots */}
733 <div class="flex justify-center space-x-3 mt-4">
734 {contentChunks.map((_, index) => (
735 <div
736 key={index}
737 class={`h-1.5 w-8 rounded-full transition-colors duration-200 ${
738 index === currentChunkIndex
739 ? "bg-amber-500"
740 : "bg-slate-200 dark:bg-slate-700"
741 }`}
742 />
743 ))}
744 </div>
745 </div>
746 </div>
747 </div>
748 );
749 }
750
751 return (
752 <div class="space-y-8">
753 {/* Progress Steps */}
754 <div class="space-y-4">
755 <div class="flex items-center justify-between">
756 <h3 class="text-lg font-medium text-gray-900 dark:text-gray-100">
757 Key Generation Progress
758 </h3>
759 {/* Add a help tooltip */}
760 <div class="relative group">
761 <button class="text-gray-400 hover:text-gray-500" type="button">
762 <svg
763 class="w-5 h-5"
764 fill="none"
765 stroke="currentColor"
766 viewBox="0 0 24 24"
767 >
768 <path
769 stroke-linecap="round"
770 stroke-linejoin="round"
771 stroke-width="2"
772 d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
773 />
774 </svg>
775 </button>
776 <div class="absolute right-0 w-64 p-2 mt-2 space-y-1 text-sm bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 hidden group-hover:block z-10">
777 <p class="text-gray-600 dark:text-gray-400">
778 Follow these steps to securely add a new rotation key to your
779 PLC record. Each step requires completion before proceeding.
780 </p>
781 </div>
782 </div>
783 </div>
784
785 {/* Steps with enhanced visual hierarchy */}
786 {steps.map((step, index) => (
787 <div
788 key={step.name}
789 class={`${getStepClasses(step.status)} ${
790 step.status === "in-progress"
791 ? "ring-2 ring-blue-500 ring-opacity-50"
792 : ""
793 }`}
794 >
795 <div class="flex-shrink-0">{getStepIcon(step.status)}</div>
796 <div class="flex-1 min-w-0">
797 <div class="flex items-center justify-between">
798 <p
799 class={`font-medium ${
800 step.status === "error"
801 ? "text-red-900 dark:text-red-200"
802 : step.status === "completed"
803 ? "text-green-900 dark:text-green-200"
804 : step.status === "in-progress"
805 ? "text-blue-900 dark:text-blue-200"
806 : "text-gray-900 dark:text-gray-200"
807 }`}
808 >
809 {getStepDisplayName(step, index)}
810 </p>
811 {/* Add step number */}
812 <span class="text-sm text-gray-500 dark:text-gray-400">
813 Step {index + 1} of {steps.length}
814 </span>
815 </div>
816
817 {step.error && (
818 <div class="mt-2 p-2 bg-red-50 dark:bg-red-900/50 rounded-md">
819 <p class="text-sm text-red-600 dark:text-red-400 flex items-center">
820 <svg
821 class="w-4 h-4 mr-1"
822 fill="none"
823 stroke="currentColor"
824 viewBox="0 0 24 24"
825 >
826 <path
827 stroke-linecap="round"
828 stroke-linejoin="round"
829 stroke-width="2"
830 d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
831 />
832 </svg>
833 {(() => {
834 try {
835 const err = JSON.parse(step.error);
836 return err.message || step.error;
837 } catch {
838 return step.error;
839 }
840 })()}
841 </p>
842 </div>
843 )}
844
845 {/* Key Download Warning */}
846 {index === 0 &&
847 step.status === "completed" &&
848 !hasDownloadedKey && (
849 <div class="mt-4 space-y-4">
850 <div class="bg-yellow-50 dark:bg-yellow-900/50 p-4 rounded-lg border border-yellow-200 dark:border-yellow-800">
851 <div class="flex items-start">
852 <div class="flex-shrink-0">
853 <svg
854 class="h-5 w-5 text-yellow-400"
855 viewBox="0 0 20 20"
856 fill="currentColor"
857 >
858 <path
859 fill-rule="evenodd"
860 d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495zM10 5a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 0110 5zm0 9a1 1 0 100-2 1 1 0 000 2z"
861 clip-rule="evenodd"
862 />
863 </svg>
864 </div>
865 <div class="ml-3">
866 <h3 class="text-sm font-medium text-yellow-800 dark:text-yellow-200">
867 Critical Security Step
868 </h3>
869 <div class="mt-2 text-sm text-yellow-700 dark:text-yellow-300">
870 <p class="mb-2">
871 Your rotation key grants control over your identity:
872 </p>
873 <ul class="list-disc pl-5 space-y-2">
874 <li>
875 <strong>Store Securely:</strong>{" "}
876 Use a password manager
877 </li>
878 <li>
879 <strong>Keep Private:</strong>{" "}
880 Never share with anyone
881 </li>
882 <li>
883 <strong>Backup:</strong> Keep a secure backup copy
884 </li>
885 <li>
886 <strong>Required:</strong>{" "}
887 Needed for future DID modifications
888 </li>
889 </ul>
890 </div>
891 </div>
892 </div>
893 </div>
894
895 <div class="flex items-center justify-between">
896 <button
897 type="button"
898 onClick={handleDownload}
899 class="px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-md transition-colors duration-200 flex items-center space-x-2"
900 >
901 <svg
902 class="w-5 h-5"
903 fill="none"
904 stroke="currentColor"
905 viewBox="0 0 24 24"
906 >
907 <path
908 stroke-linecap="round"
909 stroke-linejoin="round"
910 stroke-width="2"
911 d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
912 />
913 </svg>
914 <span>Download Key</span>
915 </button>
916
917 <div class="flex items-center text-sm text-red-600 dark:text-red-400">
918 <svg
919 class="w-4 h-4 mr-1"
920 fill="none"
921 stroke="currentColor"
922 viewBox="0 0 24 24"
923 >
924 <path
925 stroke-linecap="round"
926 stroke-linejoin="round"
927 stroke-width="2"
928 d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
929 />
930 </svg>
931 Download required to proceed
932 </div>
933 </div>
934 </div>
935 )}
936
937 {/* Email Code Input */}
938 {index === 1 &&
939 (step.status === "in-progress" ||
940 step.status === "verifying") &&
941 step.name ===
942 "Enter the code sent to your email to complete PLC update" &&
943 (
944 <div class="mt-4 space-y-4">
945 <div class="bg-blue-50 dark:bg-blue-900/50 p-4 rounded-lg">
946 <p class="text-sm text-blue-800 dark:text-blue-200 mb-3">
947 Check your email for the verification code to complete
948 the PLC update:
949 </p>
950 <div class="flex space-x-2">
951 <div class="flex-1 relative">
952 <input
953 type="text"
954 value={emailToken}
955 onChange={(e) =>
956 setEmailToken(e.currentTarget.value)}
957 placeholder="Enter verification code"
958 class="w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:focus:border-blue-400 dark:focus:ring-blue-400"
959 />
960 </div>
961 <button
962 type="button"
963 onClick={handleTokenSubmit}
964 disabled={!emailToken || step.status === "verifying"}
965 class="px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed flex items-center space-x-2"
966 >
967 <span>
968 {step.status === "verifying"
969 ? "Verifying..."
970 : "Verify"}
971 </span>
972 <svg
973 class="w-4 h-4"
974 fill="none"
975 stroke="currentColor"
976 viewBox="0 0 24 24"
977 >
978 <path
979 stroke-linecap="round"
980 stroke-linejoin="round"
981 stroke-width="2"
982 d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
983 />
984 </svg>
985 </button>
986 </div>
987 {step.error && (
988 <div class="mt-2 p-2 bg-red-50 dark:bg-red-900/50 rounded-md">
989 <p class="text-sm text-red-600 dark:text-red-400 flex items-center">
990 <svg
991 class="w-4 h-4 mr-1"
992 fill="none"
993 stroke="currentColor"
994 viewBox="0 0 24 24"
995 >
996 <path
997 stroke-linecap="round"
998 stroke-linejoin="round"
999 stroke-width="2"
1000 d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
1001 />
1002 </svg>
1003 {step.error}
1004 </p>
1005 {step.error
1006 .toLowerCase()
1007 .includes("token is invalid") && (
1008 <div class="mt-2">
1009 <p class="text-sm text-red-500 dark:text-red-300 mb-2">
1010 The verification code may have expired. Request
1011 a new code to try again.
1012 </p>
1013 <button
1014 type="button"
1015 onClick={requestNewToken}
1016 class="text-sm px-3 py-1 bg-red-100 hover:bg-red-200 dark:bg-red-800 dark:hover:bg-red-700 text-red-700 dark:text-red-200 rounded-md transition-colors duration-200 flex items-center space-x-1"
1017 >
1018 <svg
1019 class="w-4 h-4"
1020 fill="none"
1021 stroke="currentColor"
1022 viewBox="0 0 24 24"
1023 >
1024 <path
1025 stroke-linecap="round"
1026 stroke-linejoin="round"
1027 stroke-width="2"
1028 d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
1029 />
1030 </svg>
1031 <span>Request New Code</span>
1032 </button>
1033 </div>
1034 )}
1035 </div>
1036 )}
1037 </div>
1038 </div>
1039 )}
1040 </div>
1041 </div>
1042 ))}
1043 </div>
1044
1045 {/* Success Message */}
1046 {steps[2].status === "completed" && (
1047 <div class="p-4 bg-green-50 dark:bg-green-900/50 rounded-lg border-2 border-green-200 dark:border-green-800">
1048 <div class="flex items-center space-x-3 mb-4">
1049 <svg
1050 class="w-6 h-6 text-green-500"
1051 fill="none"
1052 stroke="currentColor"
1053 viewBox="0 0 24 24"
1054 >
1055 <path
1056 stroke-linecap="round"
1057 stroke-linejoin="round"
1058 stroke-width="2"
1059 d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
1060 />
1061 </svg>
1062 <h4 class="text-lg font-medium text-green-800 dark:text-green-200">
1063 PLC Update Successful!
1064 </h4>
1065 </div>
1066 <p class="text-sm text-green-700 dark:text-green-300 mb-4">
1067 Your rotation key has been successfully added to your PLC record.
1068 You can now use this key for future DID modifications.
1069 </p>
1070 <div class="flex space-x-4">
1071 <button
1072 type="button"
1073 onClick={async () => {
1074 try {
1075 const response = await fetch("/api/logout", {
1076 method: "POST",
1077 credentials: "include",
1078 });
1079 if (!response.ok) {
1080 throw new Error("Logout failed");
1081 }
1082 globalThis.location.href = "/";
1083 } catch (error) {
1084 console.error("Failed to logout:", error);
1085 }
1086 }}
1087 class="px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-md transition-colors duration-200 flex items-center space-x-2"
1088 >
1089 <svg
1090 class="w-5 h-5"
1091 fill="none"
1092 stroke="currentColor"
1093 viewBox="0 0 24 24"
1094 >
1095 <path
1096 stroke-linecap="round"
1097 stroke-linejoin="round"
1098 stroke-width="2"
1099 d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"
1100 />
1101 </svg>
1102 <span>Sign Out</span>
1103 </button>
1104 <a
1105 href="https://ko-fi.com/knotbin"
1106 target="_blank"
1107 class="px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-md transition-colors duration-200 flex items-center space-x-2"
1108 >
1109 <svg
1110 class="w-5 h-5"
1111 fill="none"
1112 stroke="currentColor"
1113 viewBox="0 0 24 24"
1114 >
1115 <path
1116 stroke-linecap="round"
1117 stroke-linejoin="round"
1118 stroke-width="2"
1119 d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"
1120 />
1121 </svg>
1122 <span>Support Us</span>
1123 </a>
1124 </div>
1125 </div>
1126 )}
1127 </div>
1128 );
1129}