···
interface PlcUpdateStep {
5
-
status: "pending" | "in-progress" | "completed" | "error";
5
+
status: "pending" | "in-progress" | "verifying" | "completed" | "error";
···
const [hasStarted, setHasStarted] = useState(false);
const [steps, setSteps] = useState<PlcUpdateStep[]>([
{ name: "Generate PLC key", status: "pending" },
13
-
{ name: "Update PLC key", status: "pending" },
13
+
{ name: "Start PLC update", status: "pending" },
14
+
{ name: "Complete PLC update", status: "pending" },
const [generatedKey, setGeneratedKey] = useState<string>("");
16
-
const [updateKey, setUpdateKey] = useState<string>("");
17
+
const [keyJson, setKeyJson] = useState<any>(null);
18
+
const [emailToken, setEmailToken] = useState<string>("");
const [updateResult, setUpdateResult] = useState<string>("");
20
+
const [showDownload, setShowDownload] = useState(false);
21
+
const [showKeyInfo, setShowKeyInfo] = useState(false);
const updateStepStatus = (
status: PlcUpdateStep["status"],
29
+
`Updating step ${index} to ${status}${
30
+
error ? ` with error: ${error}` : ""
prevSteps.map((step, i) =>
26
-
i === index ? { ...step, status, error } : step
36
+
? { ...step, status, error }
38
+
? { ...step, status: "pending", error: undefined }
const handleStart = () => {
46
+
// Automatically start the first step
48
+
handleGenerateKey();
52
+
const getStepDisplayName = (step: PlcUpdateStep, index: number) => {
53
+
if (step.status === "completed") {
56
+
return "PLC Key Generated";
58
+
return "PLC Update Started";
60
+
return "PLC Update Completed";
64
+
if (step.status === "in-progress") {
67
+
return "Generating PLC key...";
69
+
return "Starting PLC update...";
71
+
return step.name ===
72
+
"Enter the token sent to your email to complete PLC update"
74
+
: "Completing PLC update...";
78
+
if (step.status === "verifying") {
81
+
return "Verifying key generation...";
83
+
return "Verifying PLC update start...";
85
+
return "Verifying PLC update completion...";
const handleGenerateKey = async () => {
updateStepStatus(0, "in-progress");
94
+
setShowDownload(false);
96
+
setGeneratedKey("");
const res = await fetch("/api/plc/keys");
const text = await res.text();
···
throw new Error("Invalid response from /api/plc/keys");
54
-
if (!data.did || !data.signature) {
55
-
throw new Error("Key generation failed: missing did or signature");
114
+
if (!data.publicKeyDid || !data.privateKeyHex) {
115
+
throw new Error("Key generation failed: missing key data");
57
-
setGeneratedKey(data.did);
58
-
setUpdateKey(data.did);
117
+
setGeneratedKey(data.publicKeyDid);
119
+
setShowDownload(true);
updateStepStatus(0, "completed");
122
+
// Auto-download the key
124
+
console.log("Attempting auto-download with keyJson:", keyJson);
128
+
// Auto-continue to next step with the generated key
130
+
handleStartPlcUpdate(data.publicKeyDid);
···
69
-
const handleUpdateKey = async () => {
141
+
const handleStartPlcUpdate = async (keyToUse?: string) => {
142
+
const key = keyToUse || generatedKey;
144
+
console.log("No key generated yet", { key, generatedKey });
145
+
updateStepStatus(1, "error", "No key generated yet");
updateStepStatus(1, "in-progress");
71
-
setUpdateResult("");
const res = await fetch("/api/plc/update", {
headers: { "Content-Type": "application/json" },
76
-
body: JSON.stringify({ key: updateKey }),
154
+
body: JSON.stringify({ key: key }),
const text = await res.text();
const json = JSON.parse(text);
82
-
throw new Error(json.message || "Failed to update key");
160
+
throw new Error(json.message || "Failed to start PLC update");
84
-
throw new Error(text || "Failed to update key");
162
+
throw new Error(text || "Failed to start PLC update");
87
-
setUpdateResult("Key updated successfully!");
166
+
// Update step name to prompt for token
167
+
setSteps((prevSteps) =>
168
+
prevSteps.map((step, i) =>
172
+
name: "Enter the token sent to your email to complete PLC update",
updateStepStatus(1, "completed");
···
error instanceof Error ? error.message : String(error)
187
+
const handleCompletePlcUpdate = async () => {
189
+
updateStepStatus(2, "error", "Please enter the email token");
193
+
updateStepStatus(2, "in-progress");
195
+
const res = await fetch(
196
+
`/api/plc/update/complete?token=${encodeURIComponent(emailToken)}`,
199
+
headers: { "Content-Type": "application/json" },
202
+
const text = await res.text();
205
+
const json = JSON.parse(text);
206
+
throw new Error(json.message || "Failed to complete PLC update");
208
+
throw new Error(text || "Failed to complete PLC update");
214
+
data = JSON.parse(text);
215
+
if (!data.success) {
216
+
throw new Error(data.message || "PLC update failed");
219
+
throw new Error("Invalid response from server");
222
+
setUpdateResult("PLC update completed successfully!");
223
+
updateStepStatus(2, "completed");
228
+
error instanceof Error ? error.message : String(error)
setUpdateResult(error instanceof Error ? error.message : String(error));
234
+
const handleDownload = () => {
235
+
console.log("handleDownload called with keyJson:", keyJson);
237
+
console.error("No key JSON to download");
241
+
const jsonString = JSON.stringify(keyJson, null, 2);
242
+
console.log("JSON string to download:", jsonString);
243
+
const blob = new Blob([jsonString], {
244
+
type: "application/json",
246
+
const url = URL.createObjectURL(blob);
247
+
const a = document.createElement("a");
249
+
a.download = `plc-key-${keyJson.publicKeyDid || "unknown"}.json`;
250
+
a.style.display = "none";
251
+
document.body.appendChild(a);
252
+
console.log("Download link created, clicking...");
254
+
document.body.removeChild(a);
255
+
URL.revokeObjectURL(url);
256
+
console.log("Key downloaded successfully:", keyJson.publicKeyDid);
258
+
console.error("Download failed:", error);
262
+
const getStepIcon = (status: PlcUpdateStep["status"]) => {
266
+
<div class="w-8 h-8 rounded-full border-2 border-gray-300 dark:border-gray-600 flex items-center justify-center">
267
+
<div class="w-3 h-3 rounded-full bg-gray-300 dark:bg-gray-600" />
270
+
case "in-progress":
272
+
<div class="w-8 h-8 rounded-full border-2 border-blue-500 border-t-transparent animate-spin flex items-center justify-center">
273
+
<div class="w-3 h-3 rounded-full bg-blue-500" />
278
+
<div class="w-8 h-8 rounded-full border-2 border-yellow-500 border-t-transparent animate-spin flex items-center justify-center">
279
+
<div class="w-3 h-3 rounded-full bg-yellow-500" />
284
+
<div class="w-8 h-8 rounded-full bg-green-500 flex items-center justify-center">
286
+
class="w-5 h-5 text-white"
288
+
stroke="currentColor"
289
+
viewBox="0 0 24 24"
292
+
stroke-linecap="round"
293
+
stroke-linejoin="round"
302
+
<div class="w-8 h-8 rounded-full bg-red-500 flex items-center justify-center">
304
+
class="w-5 h-5 text-white"
306
+
stroke="currentColor"
307
+
viewBox="0 0 24 24"
310
+
stroke-linecap="round"
311
+
stroke-linejoin="round"
313
+
d="M6 18L18 6M6 6l12 12"
321
+
const getStepClasses = (status: PlcUpdateStep["status"]) => {
322
+
const baseClasses =
323
+
"flex items-center space-x-3 p-4 rounded-lg transition-colors duration-200";
326
+
return `${baseClasses} bg-gray-50 dark:bg-gray-800`;
327
+
case "in-progress":
328
+
return `${baseClasses} bg-blue-50 dark:bg-blue-900`;
330
+
return `${baseClasses} bg-yellow-50 dark:bg-yellow-900`;
332
+
return `${baseClasses} bg-green-50 dark:bg-green-900`;
334
+
return `${baseClasses} bg-red-50 dark:bg-red-900`;
···
• Generate a new PLC key with cryptographic signature verification
114
-
<p>• Update your existing DID with the new key</p>
353
+
<p>• Start PLC update process (sends email with token)</p>
354
+
<p>• Complete PLC update using email token</p>
<p>• All operations require authentication</p>
···
370
+
{/* Steps Section */}
131
-
{/* Step 1: Generate PLC key */}
133
-
class={`flex items-center space-x-3 p-4 rounded-lg ${
134
-
steps[0].status === "completed"
135
-
? "bg-green-50 dark:bg-green-900"
136
-
: steps[0].status === "in-progress"
137
-
? "bg-blue-50 dark:bg-blue-900"
138
-
: steps[0].status === "error"
139
-
? "bg-red-50 dark:bg-red-900"
140
-
: "bg-gray-50 dark:bg-gray-800"
372
+
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100">
375
+
{steps.map((step, index) => (
376
+
<div key={step.name} class={getStepClasses(step.status)}>
377
+
{getStepIcon(step.status)}
378
+
<div class="flex-1">
380
+
class={`font-medium ${
381
+
step.status === "error"
382
+
? "text-red-900 dark:text-red-200"
383
+
: step.status === "completed"
384
+
? "text-green-900 dark:text-green-200"
385
+
: step.status === "in-progress"
386
+
? "text-blue-900 dark:text-blue-200"
387
+
: "text-gray-900 dark:text-gray-200"
390
+
{getStepDisplayName(step, index)}
393
+
<p class="text-sm text-red-600 dark:text-red-400 mt-1">
396
+
const err = JSON.parse(step.error);
397
+
return err.message || step.error;
404
+
{index === 1 && step.status === "completed" && (
408
+
onClick={() => handleStartPlcUpdate()}
409
+
class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md transition-colors duration-200"
416
+
step.status === "in-progress" &&
418
+
"Enter the token sent to your email to complete PLC update" && (
419
+
<div class="mt-4 space-y-4">
420
+
<p class="text-sm text-blue-800 dark:text-blue-200">
421
+
Please check your email for the PLC update token and enter
424
+
<div class="flex space-x-2">
428
+
onChange={(e) => setEmailToken(e.currentTarget.value)}
429
+
placeholder="Enter token"
430
+
class="flex-1 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"
434
+
onClick={handleCompletePlcUpdate}
435
+
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"
447
+
{/* Key Information Section - Collapsible at bottom */}
449
+
<div class="border border-gray-200 dark:border-gray-700 rounded-lg">
144
-
class="px-4 py-2 bg-blue-600 text-white rounded-md"
145
-
onClick={handleGenerateKey}
146
-
disabled={steps[0].status === "in-progress"}
451
+
onClick={() => setShowKeyInfo(!showKeyInfo)}
452
+
class="w-full p-4 text-left bg-gray-50 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-t-lg flex items-center justify-between"
454
+
<span class="font-medium text-gray-900 dark:text-gray-100">
455
+
Generated Key Information
458
+
class={`w-5 h-5 text-gray-500 transition-transform ${
459
+
showKeyInfo ? "rotate-180" : ""
462
+
stroke="currentColor"
463
+
viewBox="0 0 24 24"
466
+
stroke-linecap="round"
467
+
stroke-linejoin="round"
150
-
{steps[0].status === "completed" && (
151
-
<span class="text-green-700 ml-4">Key generated!</span>
153
-
{steps[0].status === "error" && (
154
-
<span class="text-red-700 ml-4">{steps[0].error}</span>
474
+
<div class="p-4 bg-white dark:bg-gray-900 rounded-b-lg">
475
+
<div class="space-y-3 text-sm text-gray-700 dark:text-gray-300">
477
+
<b>Key type:</b> {keyJson.keyType}
480
+
<b>Public key (did:key):</b>{" "}
481
+
<span class="break-all font-mono">
482
+
{keyJson.publicKeyDid}
486
+
<b>Private key (hex):</b>{" "}
487
+
<span class="break-all font-mono">
488
+
{keyJson.privateKeyHex}
492
+
<b>Private key (multikey):</b>{" "}
493
+
<span class="break-all font-mono">
494
+
{keyJson.privateKeyMultikey}
500
+
class="px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-md text-sm"
501
+
onClick={handleDownload}
158
-
<div class="p-2 bg-gray-100 dark:bg-gray-700 rounded">
159
-
<div class="text-xs text-gray-700 dark:text-gray-200 break-all">
160
-
<b>Generated DID:</b> {generatedKey}
164
-
{/* Step 2: Update PLC key */}
166
-
class={`flex flex-col space-y-2 p-4 rounded-lg ${
167
-
steps[1].status === "completed"
168
-
? "bg-green-50 dark:bg-green-900"
169
-
: steps[1].status === "in-progress"
170
-
? "bg-blue-50 dark:bg-blue-900"
171
-
: steps[1].status === "error"
172
-
? "bg-red-50 dark:bg-red-900"
173
-
: "bg-gray-50 dark:bg-gray-800"
176
-
<label class="text-sm mb-1">DID to update:</label>
178
-
class="p-2 rounded border border-gray-300 dark:border-gray-600"
181
-
onInput={(e) => setUpdateKey(e.currentTarget.value)}
182
-
placeholder="Paste or use generated DID"
511
+
{steps[2].status === "completed" && (
512
+
<div class="p-4 bg-green-50 dark:bg-green-900 rounded-lg border-2 border-green-200 dark:border-green-800">
513
+
<p class="text-sm text-green-800 dark:text-green-200">
514
+
PLC update completed successfully! You can now close this page.
185
-
class="mt-2 px-4 py-2 bg-blue-600 text-white rounded-md"
186
-
onClick={handleUpdateKey}
187
-
disabled={steps[1].status === "in-progress" || !updateKey}
518
+
onClick={async () => {
520
+
const response = await fetch("/api/logout", {
522
+
credentials: "include",
524
+
if (!response.ok) {
525
+
throw new Error("Logout failed");
527
+
globalThis.location.href = "/";
529
+
console.error("Failed to logout:", error);
532
+
class="mt-4 mr-4 px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-md transition-colors duration-200"
191
-
{steps[1].status === "completed" && (
192
-
<span class="text-green-700 mt-2">{updateResult}</span>
194
-
{steps[1].status === "error" && (
195
-
<span class="text-red-700 mt-2">{steps[1].error}</span>
537
+
href="https://ko-fi.com/knotbin"
539
+
class="mt-4 px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-md transition-colors duration-200"