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}