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