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