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