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