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}