+77
components/MigrationCompletion.tsx
+77
components/MigrationCompletion.tsx
···
1
+
export interface MigrationCompletionProps {
2
+
isVisible: boolean;
3
+
}
4
+
5
+
export default function MigrationCompletion(
6
+
{ isVisible }: MigrationCompletionProps,
7
+
) {
8
+
if (!isVisible) return null;
9
+
10
+
const handleLogout = async () => {
11
+
try {
12
+
const response = await fetch("/api/logout", {
13
+
method: "POST",
14
+
credentials: "include",
15
+
});
16
+
if (!response.ok) {
17
+
throw new Error("Logout failed");
18
+
}
19
+
globalThis.location.href = "/";
20
+
} catch (error) {
21
+
console.error("Failed to logout:", error);
22
+
}
23
+
};
24
+
25
+
return (
26
+
<div class="p-4 bg-green-50 dark:bg-green-900 rounded-lg border-2 border-green-200 dark:border-green-800">
27
+
<p class="text-sm text-green-800 dark:text-green-200 pb-2">
28
+
Migration completed successfully! Sign out to finish the process and
29
+
return home.<br />
30
+
Please consider donating to Airport to support server and development
31
+
costs.
32
+
</p>
33
+
<div class="flex space-x-4">
34
+
<button
35
+
type="button"
36
+
onClick={handleLogout}
37
+
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"
38
+
>
39
+
<svg
40
+
class="w-5 h-5"
41
+
fill="none"
42
+
stroke="currentColor"
43
+
viewBox="0 0 24 24"
44
+
>
45
+
<path
46
+
stroke-linecap="round"
47
+
stroke-linejoin="round"
48
+
stroke-width="2"
49
+
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"
50
+
/>
51
+
</svg>
52
+
<span>Sign Out</span>
53
+
</button>
54
+
<a
55
+
href="https://ko-fi.com/knotbin"
56
+
target="_blank"
57
+
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"
58
+
>
59
+
<svg
60
+
class="w-5 h-5"
61
+
fill="none"
62
+
stroke="currentColor"
63
+
viewBox="0 0 24 24"
64
+
>
65
+
<path
66
+
stroke-linecap="round"
67
+
stroke-linejoin="round"
68
+
stroke-width="2"
69
+
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"
70
+
/>
71
+
</svg>
72
+
<span>Support Us</span>
73
+
</a>
74
+
</div>
75
+
</div>
76
+
);
77
+
}
+208
components/MigrationStep.tsx
+208
components/MigrationStep.tsx
···
1
+
import { IS_BROWSER } from "fresh/runtime";
2
+
import { ComponentChildren } from "preact";
3
+
4
+
export type StepStatus =
5
+
| "pending"
6
+
| "in-progress"
7
+
| "verifying"
8
+
| "completed"
9
+
| "error";
10
+
11
+
export interface MigrationStepProps {
12
+
name: string;
13
+
status: StepStatus;
14
+
error?: string;
15
+
isVerificationError?: boolean;
16
+
index: number;
17
+
onRetryVerification?: (index: number) => void;
18
+
children?: ComponentChildren;
19
+
}
20
+
21
+
export function MigrationStep({
22
+
name,
23
+
status,
24
+
error,
25
+
isVerificationError,
26
+
index,
27
+
onRetryVerification,
28
+
children,
29
+
}: MigrationStepProps) {
30
+
return (
31
+
<div key={name} class={getStepClasses(status)}>
32
+
{getStepIcon(status)}
33
+
<div class="flex-1">
34
+
<p
35
+
class={`font-medium ${
36
+
status === "error"
37
+
? "text-red-900 dark:text-red-200"
38
+
: status === "completed"
39
+
? "text-green-900 dark:text-green-200"
40
+
: status === "in-progress"
41
+
? "text-blue-900 dark:text-blue-200"
42
+
: "text-gray-900 dark:text-gray-200"
43
+
}`}
44
+
>
45
+
{getStepDisplayName(
46
+
{ name, status, error, isVerificationError },
47
+
index,
48
+
)}
49
+
</p>
50
+
{error && (
51
+
<div class="mt-1">
52
+
<p class="text-sm text-red-600 dark:text-red-400">
53
+
{(() => {
54
+
try {
55
+
const err = JSON.parse(error);
56
+
return err.message || error;
57
+
} catch {
58
+
return error;
59
+
}
60
+
})()}
61
+
</p>
62
+
{isVerificationError && onRetryVerification && (
63
+
<div class="flex space-x-2 mt-2">
64
+
<button
65
+
type="button"
66
+
onClick={() => onRetryVerification(index)}
67
+
class="px-3 py-1 text-xs bg-blue-600 hover:bg-blue-700 text-white rounded transition-colors duration-200 dark:bg-blue-500 dark:hover:bg-blue-400"
68
+
disabled={!IS_BROWSER}
69
+
>
70
+
Retry Verification
71
+
</button>
72
+
</div>
73
+
)}
74
+
</div>
75
+
)}
76
+
{children}
77
+
</div>
78
+
</div>
79
+
);
80
+
}
81
+
82
+
function getStepDisplayName(
83
+
step: Pick<
84
+
MigrationStepProps,
85
+
"name" | "status" | "error" | "isVerificationError"
86
+
>,
87
+
index: number,
88
+
) {
89
+
if (step.status === "completed") {
90
+
switch (index) {
91
+
case 0:
92
+
return "Account Created";
93
+
case 1:
94
+
return "Data Migrated";
95
+
case 2:
96
+
return "Identity Migrated";
97
+
case 3:
98
+
return "Migration Finalized";
99
+
}
100
+
}
101
+
102
+
if (step.status === "in-progress") {
103
+
switch (index) {
104
+
case 0:
105
+
return "Creating your new account...";
106
+
case 1:
107
+
return "Migrating your data...";
108
+
case 2:
109
+
return step.name ===
110
+
"Enter the token sent to your email to complete identity migration"
111
+
? step.name
112
+
: "Migrating your identity...";
113
+
case 3:
114
+
return "Finalizing migration...";
115
+
}
116
+
}
117
+
118
+
if (step.status === "verifying") {
119
+
switch (index) {
120
+
case 0:
121
+
return "Verifying account creation...";
122
+
case 1:
123
+
return "Verifying data migration...";
124
+
case 2:
125
+
return "Verifying identity migration...";
126
+
case 3:
127
+
return "Verifying migration completion...";
128
+
}
129
+
}
130
+
131
+
return step.name;
132
+
}
133
+
134
+
function getStepIcon(status: StepStatus) {
135
+
switch (status) {
136
+
case "pending":
137
+
return (
138
+
<div class="w-8 h-8 rounded-full border-2 border-gray-300 dark:border-gray-600 flex items-center justify-center">
139
+
<div class="w-3 h-3 rounded-full bg-gray-300 dark:bg-gray-600" />
140
+
</div>
141
+
);
142
+
case "in-progress":
143
+
return (
144
+
<div class="w-8 h-8 rounded-full border-2 border-blue-500 border-t-transparent animate-spin flex items-center justify-center">
145
+
<div class="w-3 h-3 rounded-full bg-blue-500" />
146
+
</div>
147
+
);
148
+
case "verifying":
149
+
return (
150
+
<div class="w-8 h-8 rounded-full border-2 border-yellow-500 border-t-transparent animate-spin flex items-center justify-center">
151
+
<div class="w-3 h-3 rounded-full bg-yellow-500" />
152
+
</div>
153
+
);
154
+
case "completed":
155
+
return (
156
+
<div class="w-8 h-8 rounded-full bg-green-500 flex items-center justify-center">
157
+
<svg
158
+
class="w-5 h-5 text-white"
159
+
fill="none"
160
+
stroke="currentColor"
161
+
viewBox="0 0 24 24"
162
+
>
163
+
<path
164
+
stroke-linecap="round"
165
+
stroke-linejoin="round"
166
+
stroke-width="2"
167
+
d="M5 13l4 4L19 7"
168
+
/>
169
+
</svg>
170
+
</div>
171
+
);
172
+
case "error":
173
+
return (
174
+
<div class="w-8 h-8 rounded-full bg-red-500 flex items-center justify-center">
175
+
<svg
176
+
class="w-5 h-5 text-white"
177
+
fill="none"
178
+
stroke="currentColor"
179
+
viewBox="0 0 24 24"
180
+
>
181
+
<path
182
+
stroke-linecap="round"
183
+
stroke-linejoin="round"
184
+
stroke-width="2"
185
+
d="M6 18L18 6M6 6l12 12"
186
+
/>
187
+
</svg>
188
+
</div>
189
+
);
190
+
}
191
+
}
192
+
193
+
function getStepClasses(status: StepStatus) {
194
+
const baseClasses =
195
+
"flex items-center space-x-3 p-4 rounded-lg transition-colors duration-200";
196
+
switch (status) {
197
+
case "pending":
198
+
return `${baseClasses} bg-gray-50 dark:bg-gray-800`;
199
+
case "in-progress":
200
+
return `${baseClasses} bg-blue-50 dark:bg-blue-900`;
201
+
case "verifying":
202
+
return `${baseClasses} bg-yellow-50 dark:bg-yellow-900`;
203
+
case "completed":
204
+
return `${baseClasses} bg-green-50 dark:bg-green-900`;
205
+
case "error":
206
+
return `${baseClasses} bg-red-50 dark:bg-red-900`;
207
+
}
208
+
}
+6
-7
islands/DidPlcProgress.tsx
+6
-7
islands/DidPlcProgress.tsx
···
7
7
error?: string;
8
8
}
9
9
10
+
interface KeyJson {
11
+
publicKeyDid: string;
12
+
[key: string]: unknown;
13
+
}
14
+
10
15
// Content chunks for the description
11
16
const contentChunks = [
12
17
{
···
158
163
{ name: "Complete PLC update", status: "pending" },
159
164
]);
160
165
const [generatedKey, setGeneratedKey] = useState<string>("");
161
-
const [keyJson, setKeyJson] = useState<any>(null);
166
+
const [keyJson, setKeyJson] = useState<KeyJson | null>(null);
162
167
const [emailToken, setEmailToken] = useState<string>("");
163
-
const [updateResult, setUpdateResult] = useState<string>("");
164
-
const [showDownload, setShowDownload] = useState(false);
165
168
const [hasDownloadedKey, setHasDownloadedKey] = useState(false);
166
169
const [downloadedKeyId, setDownloadedKeyId] = useState<string | null>(null);
167
170
···
381
384
382
385
// Only proceed if we have a successful response
383
386
console.log("Update completed successfully!");
384
-
setUpdateResult("PLC update completed successfully!");
385
387
386
388
// Add a delay before marking steps as completed for better UX
387
389
updateStepStatus(2, "verifying");
···
422
424
error instanceof Error ? error.message : String(error),
423
425
);
424
426
updateStepStatus(2, "pending"); // Reset the final step
425
-
setUpdateResult(error instanceof Error ? error.message : String(error));
426
427
427
428
// If token is invalid, we should clear it so user can try again
428
429
if (
···
478
479
const handleGenerateKey = async () => {
479
480
console.log("=== Generate Key Debug ===");
480
481
updateStepStatus(0, "in-progress");
481
-
setShowDownload(false);
482
482
setKeyJson(null);
483
483
setGeneratedKey("");
484
484
setHasDownloadedKey(false);
···
516
516
517
517
setGeneratedKey(data.publicKeyDid);
518
518
setKeyJson(data);
519
-
setShowDownload(true);
520
519
updateStepStatus(0, "completed");
521
520
} catch (error) {
522
521
console.error("Key generation failed:", error);
+74
-787
islands/MigrationProgress.tsx
+74
-787
islands/MigrationProgress.tsx
···
1
1
import { useEffect, useState } from "preact/hooks";
2
-
3
-
/**
4
-
* The migration state info.
5
-
* @type {MigrationStateInfo}
6
-
*/
7
-
interface MigrationStateInfo {
8
-
state: "up" | "issue" | "maintenance";
9
-
message: string;
10
-
allowMigration: boolean;
11
-
}
2
+
import { MigrationStateInfo } from "../lib/migration-types.ts";
3
+
import AccountCreationStep from "./migration-steps/AccountCreationStep.tsx";
4
+
import DataMigrationStep from "./migration-steps/DataMigrationStep.tsx";
5
+
import IdentityMigrationStep from "./migration-steps/IdentityMigrationStep.tsx";
6
+
import FinalizationStep from "./migration-steps/FinalizationStep.tsx";
7
+
import MigrationCompletion from "../components/MigrationCompletion.tsx";
12
8
13
9
/**
14
10
* The migration progress props.
···
22
18
invite?: string;
23
19
}
24
20
25
-
/**
26
-
* The migration step.
27
-
* @type {MigrationStep}
28
-
*/
29
-
interface MigrationStep {
30
-
name: string;
31
-
status: "pending" | "in-progress" | "verifying" | "completed" | "error";
32
-
error?: string;
33
-
isVerificationError?: boolean;
34
-
}
35
-
36
21
/**
37
22
* The migration progress component.
38
23
* @param props - The migration progress props
···
40
25
* @component
41
26
*/
42
27
export default function MigrationProgress(props: MigrationProgressProps) {
43
-
const [token, setToken] = useState("");
44
28
const [migrationState, setMigrationState] = useState<
45
29
MigrationStateInfo | null
46
30
>(null);
47
-
const [retryAttempts, setRetryAttempts] = useState<Record<number, number>>(
48
-
{},
49
-
);
50
-
const [showContinueAnyway, setShowContinueAnyway] = useState<
51
-
Record<number, boolean>
52
-
>({});
53
-
54
-
const [steps, setSteps] = useState<MigrationStep[]>([
55
-
{ name: "Create Account", status: "pending" },
56
-
{ name: "Migrate Data", status: "pending" },
57
-
{ name: "Migrate Identity", status: "pending" },
58
-
{ name: "Finalize Migration", status: "pending" },
59
-
]);
60
-
61
-
const updateStepStatus = (
62
-
index: number,
63
-
status: MigrationStep["status"],
64
-
error?: string,
65
-
isVerificationError?: boolean,
66
-
) => {
67
-
console.log(
68
-
`Updating step ${index} to ${status}${
69
-
error ? ` with error: ${error}` : ""
70
-
}`,
71
-
);
72
-
setSteps((prevSteps) =>
73
-
prevSteps.map((step, i) =>
74
-
i === index
75
-
? { ...step, status, error, isVerificationError }
76
-
: i > index
77
-
? {
78
-
...step,
79
-
status: "pending",
80
-
error: undefined,
81
-
isVerificationError: undefined,
82
-
}
83
-
: step
84
-
)
85
-
);
31
+
const [currentStep, setCurrentStep] = useState(0);
32
+
const [completedSteps, setCompletedSteps] = useState<Set<number>>(new Set());
33
+
const [hasError, setHasError] = useState(false);
34
+
35
+
const credentials = {
36
+
service: props.service,
37
+
handle: props.handle,
38
+
email: props.email,
39
+
password: props.password,
40
+
invite: props.invite,
86
41
};
87
42
88
43
const validateParams = () => {
89
44
if (!props.service?.trim()) {
90
-
updateStepStatus(0, "error", "Missing service URL");
45
+
setHasError(true);
91
46
return false;
92
47
}
93
48
if (!props.handle?.trim()) {
94
-
updateStepStatus(0, "error", "Missing handle");
49
+
setHasError(true);
95
50
return false;
96
51
}
97
52
if (!props.email?.trim()) {
98
-
updateStepStatus(0, "error", "Missing email");
53
+
setHasError(true);
99
54
return false;
100
55
}
101
56
if (!props.password?.trim()) {
102
-
updateStepStatus(0, "error", "Missing password");
57
+
setHasError(true);
103
58
return false;
104
59
}
105
60
return true;
···
123
78
setMigrationState(migrationData);
124
79
125
80
if (!migrationData.allowMigration) {
126
-
updateStepStatus(0, "error", migrationData.message);
81
+
setHasError(true);
127
82
return;
128
83
}
129
84
}
130
85
} catch (error) {
131
86
console.error("Failed to check migration state:", error);
132
-
updateStepStatus(0, "error", "Unable to verify migration availability");
87
+
setHasError(true);
133
88
return;
134
89
}
135
90
···
138
93
return;
139
94
}
140
95
141
-
startMigration().catch((error) => {
142
-
console.error("Unhandled migration error:", error);
143
-
updateStepStatus(
144
-
0,
145
-
"error",
146
-
error.message || "Unknown error occurred",
147
-
);
148
-
});
96
+
// Start with the first step
97
+
setCurrentStep(0);
149
98
};
150
99
151
100
checkMigrationState();
152
101
}, []);
153
102
154
-
const getStepDisplayName = (step: MigrationStep, index: number) => {
155
-
if (step.status === "completed") {
156
-
switch (index) {
157
-
case 0:
158
-
return "Account Created";
159
-
case 1:
160
-
return "Data Migrated";
161
-
case 2:
162
-
return "Identity Migrated";
163
-
case 3:
164
-
return "Migration Finalized";
165
-
}
166
-
}
167
-
168
-
if (step.status === "in-progress") {
169
-
switch (index) {
170
-
case 0:
171
-
return "Creating your new account...";
172
-
case 1:
173
-
return "Migrating your data...";
174
-
case 2:
175
-
return step.name ===
176
-
"Enter the token sent to your email to complete identity migration"
177
-
? step.name
178
-
: "Migrating your identity...";
179
-
case 3:
180
-
return "Finalizing migration...";
181
-
}
182
-
}
183
-
184
-
if (step.status === "verifying") {
185
-
switch (index) {
186
-
case 0:
187
-
return "Verifying account creation...";
188
-
case 1:
189
-
return "Verifying data migration...";
190
-
case 2:
191
-
return "Verifying identity migration...";
192
-
case 3:
193
-
return "Verifying migration completion...";
194
-
}
195
-
}
196
-
197
-
return step.name;
198
-
};
199
-
200
-
const startMigration = async () => {
201
-
try {
202
-
// Step 1: Create Account
203
-
updateStepStatus(0, "in-progress");
204
-
console.log("Starting account creation...");
205
-
206
-
try {
207
-
const createRes = await fetch("/api/migrate/create", {
208
-
method: "POST",
209
-
headers: { "Content-Type": "application/json" },
210
-
body: JSON.stringify({
211
-
service: props.service,
212
-
handle: props.handle,
213
-
password: props.password,
214
-
email: props.email,
215
-
...(props.invite ? { invite: props.invite } : {}),
216
-
}),
217
-
});
218
-
219
-
console.log("Create account response status:", createRes.status);
220
-
const responseText = await createRes.text();
221
-
console.log("Create account response:", responseText);
222
-
223
-
if (!createRes.ok) {
224
-
try {
225
-
const json = JSON.parse(responseText);
226
-
throw new Error(json.message || "Failed to create account");
227
-
} catch {
228
-
throw new Error(responseText || "Failed to create account");
229
-
}
230
-
}
231
-
232
-
try {
233
-
const jsonData = JSON.parse(responseText);
234
-
if (!jsonData.success) {
235
-
throw new Error(jsonData.message || "Account creation failed");
236
-
}
237
-
} catch (e) {
238
-
console.log("Response is not JSON or lacks success field:", e);
239
-
}
240
-
241
-
updateStepStatus(0, "verifying");
242
-
const verified = await verifyStep(0);
243
-
if (!verified) {
244
-
console.log(
245
-
"Account creation: Verification failed, waiting for user action",
246
-
);
247
-
return;
248
-
}
249
-
250
-
// If verification succeeds, continue to data migration
251
-
await startDataMigration();
252
-
} catch (error) {
253
-
updateStepStatus(
254
-
0,
255
-
"error",
256
-
error instanceof Error ? error.message : String(error),
257
-
);
258
-
throw error;
259
-
}
260
-
} catch (error) {
261
-
console.error("Migration error in try/catch:", error);
262
-
}
263
-
};
264
-
265
-
const handleIdentityMigration = async () => {
266
-
if (!token) return;
267
-
268
-
try {
269
-
const identityRes = await fetch(
270
-
`/api/migrate/identity/sign?token=${encodeURIComponent(token)}`,
271
-
{
272
-
method: "POST",
273
-
headers: { "Content-Type": "application/json" },
274
-
},
275
-
);
276
-
277
-
const identityData = await identityRes.text();
278
-
if (!identityRes.ok) {
279
-
try {
280
-
const json = JSON.parse(identityData);
281
-
throw new Error(
282
-
json.message || "Failed to complete identity migration",
283
-
);
284
-
} catch {
285
-
throw new Error(
286
-
identityData || "Failed to complete identity migration",
287
-
);
288
-
}
289
-
}
290
-
291
-
let data;
292
-
try {
293
-
data = JSON.parse(identityData);
294
-
if (!data.success) {
295
-
throw new Error(data.message || "Identity migration failed");
296
-
}
297
-
} catch {
298
-
throw new Error("Invalid response from server");
299
-
}
300
-
301
-
updateStepStatus(2, "verifying");
302
-
const verified = await verifyStep(2);
303
-
if (!verified) {
304
-
console.log(
305
-
"Identity migration: Verification failed, waiting for user action",
306
-
);
307
-
return;
308
-
}
309
-
310
-
// If verification succeeds, continue to finalization
311
-
await startFinalization();
312
-
} catch (error) {
313
-
console.error("Identity migration error:", error);
314
-
updateStepStatus(
315
-
2,
316
-
"error",
317
-
error instanceof Error ? error.message : String(error),
318
-
);
319
-
}
320
-
};
321
-
322
-
const getStepIcon = (status: MigrationStep["status"]) => {
323
-
switch (status) {
324
-
case "pending":
325
-
return (
326
-
<div class="w-8 h-8 rounded-full border-2 border-gray-300 dark:border-gray-600 flex items-center justify-center">
327
-
<div class="w-3 h-3 rounded-full bg-gray-300 dark:bg-gray-600" />
328
-
</div>
329
-
);
330
-
case "in-progress":
331
-
return (
332
-
<div class="w-8 h-8 rounded-full border-2 border-blue-500 border-t-transparent animate-spin flex items-center justify-center">
333
-
<div class="w-3 h-3 rounded-full bg-blue-500" />
334
-
</div>
335
-
);
336
-
case "verifying":
337
-
return (
338
-
<div class="w-8 h-8 rounded-full border-2 border-yellow-500 border-t-transparent animate-spin flex items-center justify-center">
339
-
<div class="w-3 h-3 rounded-full bg-yellow-500" />
340
-
</div>
341
-
);
342
-
case "completed":
343
-
return (
344
-
<div class="w-8 h-8 rounded-full bg-green-500 flex items-center justify-center">
345
-
<svg
346
-
class="w-5 h-5 text-white"
347
-
fill="none"
348
-
stroke="currentColor"
349
-
viewBox="0 0 24 24"
350
-
>
351
-
<path
352
-
stroke-linecap="round"
353
-
stroke-linejoin="round"
354
-
stroke-width="2"
355
-
d="M5 13l4 4L19 7"
356
-
/>
357
-
</svg>
358
-
</div>
359
-
);
360
-
case "error":
361
-
return (
362
-
<div class="w-8 h-8 rounded-full bg-red-500 flex items-center justify-center">
363
-
<svg
364
-
class="w-5 h-5 text-white"
365
-
fill="none"
366
-
stroke="currentColor"
367
-
viewBox="0 0 24 24"
368
-
>
369
-
<path
370
-
stroke-linecap="round"
371
-
stroke-linejoin="round"
372
-
stroke-width="2"
373
-
d="M6 18L18 6M6 6l12 12"
374
-
/>
375
-
</svg>
376
-
</div>
377
-
);
378
-
}
379
-
};
380
-
381
-
const getStepClasses = (status: MigrationStep["status"]) => {
382
-
const baseClasses =
383
-
"flex items-center space-x-3 p-4 rounded-lg transition-colors duration-200";
384
-
switch (status) {
385
-
case "pending":
386
-
return `${baseClasses} bg-gray-50 dark:bg-gray-800`;
387
-
case "in-progress":
388
-
return `${baseClasses} bg-blue-50 dark:bg-blue-900`;
389
-
case "verifying":
390
-
return `${baseClasses} bg-yellow-50 dark:bg-yellow-900`;
391
-
case "completed":
392
-
return `${baseClasses} bg-green-50 dark:bg-green-900`;
393
-
case "error":
394
-
return `${baseClasses} bg-red-50 dark:bg-red-900`;
395
-
}
396
-
};
397
-
398
-
// Helper to verify a step after completion
399
-
const verifyStep = async (stepNum: number) => {
400
-
console.log(`Verification: Starting step ${stepNum + 1}`);
401
-
updateStepStatus(stepNum, "verifying");
402
-
try {
403
-
console.log(`Verification: Fetching status for step ${stepNum + 1}`);
404
-
const res = await fetch(`/api/migrate/status?step=${stepNum + 1}`);
405
-
console.log(`Verification: Status response status:`, res.status);
406
-
const data = await res.json();
407
-
console.log(`Verification: Status data for step ${stepNum + 1}:`, data);
408
-
409
-
if (data.ready) {
410
-
console.log(`Verification: Step ${stepNum + 1} is ready`);
411
-
updateStepStatus(stepNum, "completed");
412
-
// Reset retry state on success
413
-
setRetryAttempts((prev) => ({ ...prev, [stepNum]: 0 }));
414
-
setShowContinueAnyway((prev) => ({ ...prev, [stepNum]: false }));
103
+
const handleStepComplete = (stepIndex: number) => {
104
+
console.log(`Step ${stepIndex} completed`);
105
+
setCompletedSteps((prev) => new Set([...prev, stepIndex]));
415
106
416
-
// Continue to next step if not the last one
417
-
if (stepNum < 3) {
418
-
setTimeout(() => continueToNextStep(stepNum + 1), 500);
419
-
}
420
-
421
-
return true;
422
-
} else {
423
-
console.log(
424
-
`Verification: Step ${stepNum + 1} is not ready:`,
425
-
data.reason,
426
-
);
427
-
const statusDetails = {
428
-
activated: data.activated,
429
-
validDid: data.validDid,
430
-
repoCommit: data.repoCommit,
431
-
repoRev: data.repoRev,
432
-
repoBlocks: data.repoBlocks,
433
-
expectedRecords: data.expectedRecords,
434
-
indexedRecords: data.indexedRecords,
435
-
privateStateValues: data.privateStateValues,
436
-
expectedBlobs: data.expectedBlobs,
437
-
importedBlobs: data.importedBlobs,
438
-
};
439
-
console.log(
440
-
`Verification: Step ${stepNum + 1} status details:`,
441
-
statusDetails,
442
-
);
443
-
const errorMessage = `${
444
-
data.reason || "Verification failed"
445
-
}\nStatus details: ${JSON.stringify(statusDetails, null, 2)}`;
446
-
447
-
// Track retry attempts
448
-
const currentAttempts = retryAttempts[stepNum] || 0;
449
-
setRetryAttempts((prev) => ({
450
-
...prev,
451
-
[stepNum]: currentAttempts + 1,
452
-
}));
453
-
454
-
// Show continue anyway option if this is the second failure
455
-
if (currentAttempts >= 1) {
456
-
setShowContinueAnyway((prev) => ({ ...prev, [stepNum]: true }));
457
-
}
458
-
459
-
updateStepStatus(stepNum, "error", errorMessage, true);
460
-
return false;
461
-
}
462
-
} catch (e) {
463
-
console.error(`Verification: Error in step ${stepNum + 1}:`, e);
464
-
const currentAttempts = retryAttempts[stepNum] || 0;
465
-
setRetryAttempts((prev) => ({ ...prev, [stepNum]: currentAttempts + 1 }));
466
-
467
-
// Show continue anyway option if this is the second failure
468
-
if (currentAttempts >= 1) {
469
-
setShowContinueAnyway((prev) => ({ ...prev, [stepNum]: true }));
470
-
}
471
-
472
-
updateStepStatus(
473
-
stepNum,
474
-
"error",
475
-
e instanceof Error ? e.message : String(e),
476
-
true,
477
-
);
478
-
return false;
107
+
// Move to next step if not the last one
108
+
if (stepIndex < 3) {
109
+
setCurrentStep(stepIndex + 1);
479
110
}
480
111
};
481
112
482
-
const retryVerification = async (stepNum: number) => {
483
-
console.log(`Retrying verification for step ${stepNum + 1}`);
484
-
await verifyStep(stepNum);
113
+
const handleStepError = (
114
+
stepIndex: number,
115
+
error: string,
116
+
isVerificationError?: boolean,
117
+
) => {
118
+
console.error(`Step ${stepIndex} error:`, error, { isVerificationError });
119
+
// Errors are handled within each step component
485
120
};
486
121
487
-
const continueAnyway = (stepNum: number) => {
488
-
console.log(`Continuing anyway for step ${stepNum + 1}`);
489
-
updateStepStatus(stepNum, "completed");
490
-
setShowContinueAnyway((prev) => ({ ...prev, [stepNum]: false }));
491
-
492
-
// Continue with next step if not the last one
493
-
if (stepNum < 3) {
494
-
continueToNextStep(stepNum + 1);
495
-
}
122
+
const isStepActive = (stepIndex: number) => {
123
+
return currentStep === stepIndex && !hasError;
496
124
};
497
125
498
-
const continueToNextStep = async (stepNum: number) => {
499
-
switch (stepNum) {
500
-
case 1:
501
-
// Continue to data migration
502
-
await startDataMigration();
503
-
break;
504
-
case 2:
505
-
// Continue to identity migration
506
-
await startIdentityMigration();
507
-
break;
508
-
case 3:
509
-
// Continue to finalization
510
-
await startFinalization();
511
-
break;
512
-
}
126
+
const _isStepCompleted = (stepIndex: number) => {
127
+
return completedSteps.has(stepIndex);
513
128
};
514
129
515
-
const startDataMigration = async () => {
516
-
// Step 2: Migrate Data
517
-
updateStepStatus(1, "in-progress");
518
-
console.log("Starting data migration...");
519
-
520
-
try {
521
-
// Step 2.1: Migrate Repo
522
-
console.log("Data migration: Starting repo migration");
523
-
const repoRes = await fetch("/api/migrate/data/repo", {
524
-
method: "POST",
525
-
headers: { "Content-Type": "application/json" },
526
-
});
527
-
528
-
console.log("Repo migration: Response status:", repoRes.status);
529
-
const repoText = await repoRes.text();
530
-
console.log("Repo migration: Raw response:", repoText);
531
-
532
-
if (!repoRes.ok) {
533
-
try {
534
-
const json = JSON.parse(repoText);
535
-
console.error("Repo migration: Error response:", json);
536
-
throw new Error(json.message || "Failed to migrate repo");
537
-
} catch {
538
-
console.error("Repo migration: Non-JSON error response:", repoText);
539
-
throw new Error(repoText || "Failed to migrate repo");
540
-
}
541
-
}
542
-
543
-
// Step 2.2: Migrate Blobs
544
-
console.log("Data migration: Starting blob migration");
545
-
const blobsRes = await fetch("/api/migrate/data/blobs", {
546
-
method: "POST",
547
-
headers: { "Content-Type": "application/json" },
548
-
});
549
-
550
-
console.log("Blob migration: Response status:", blobsRes.status);
551
-
const blobsText = await blobsRes.text();
552
-
console.log("Blob migration: Raw response:", blobsText);
553
-
554
-
if (!blobsRes.ok) {
555
-
try {
556
-
const json = JSON.parse(blobsText);
557
-
console.error("Blob migration: Error response:", json);
558
-
throw new Error(json.message || "Failed to migrate blobs");
559
-
} catch {
560
-
console.error(
561
-
"Blob migration: Non-JSON error response:",
562
-
blobsText,
563
-
);
564
-
throw new Error(blobsText || "Failed to migrate blobs");
565
-
}
566
-
}
567
-
568
-
// Step 2.3: Migrate Preferences
569
-
console.log("Data migration: Starting preferences migration");
570
-
const prefsRes = await fetch("/api/migrate/data/prefs", {
571
-
method: "POST",
572
-
headers: { "Content-Type": "application/json" },
573
-
});
574
-
575
-
console.log("Preferences migration: Response status:", prefsRes.status);
576
-
const prefsText = await prefsRes.text();
577
-
console.log("Preferences migration: Raw response:", prefsText);
578
-
579
-
if (!prefsRes.ok) {
580
-
try {
581
-
const json = JSON.parse(prefsText);
582
-
console.error("Preferences migration: Error response:", json);
583
-
throw new Error(json.message || "Failed to migrate preferences");
584
-
} catch {
585
-
console.error(
586
-
"Preferences migration: Non-JSON error response:",
587
-
prefsText,
588
-
);
589
-
throw new Error(prefsText || "Failed to migrate preferences");
590
-
}
591
-
}
592
-
593
-
console.log("Data migration: Starting verification");
594
-
updateStepStatus(1, "verifying");
595
-
const verified = await verifyStep(1);
596
-
console.log("Data migration: Verification result:", verified);
597
-
if (!verified) {
598
-
console.log(
599
-
"Data migration: Verification failed, waiting for user action",
600
-
);
601
-
return;
602
-
}
603
-
604
-
// If verification succeeds, continue to next step
605
-
await startIdentityMigration();
606
-
} catch (error) {
607
-
console.error("Data migration: Error caught:", error);
608
-
updateStepStatus(
609
-
1,
610
-
"error",
611
-
error instanceof Error ? error.message : String(error),
612
-
);
613
-
throw error;
614
-
}
615
-
};
616
-
617
-
const startIdentityMigration = async () => {
618
-
// Step 3: Request Identity Migration
619
-
updateStepStatus(2, "in-progress");
620
-
console.log("Requesting identity migration...");
621
-
622
-
try {
623
-
const requestRes = await fetch("/api/migrate/identity/request", {
624
-
method: "POST",
625
-
headers: { "Content-Type": "application/json" },
626
-
});
627
-
628
-
console.log("Identity request response status:", requestRes.status);
629
-
const requestText = await requestRes.text();
630
-
console.log("Identity request response:", requestText);
631
-
632
-
if (!requestRes.ok) {
633
-
try {
634
-
const json = JSON.parse(requestText);
635
-
throw new Error(
636
-
json.message || "Failed to request identity migration",
637
-
);
638
-
} catch {
639
-
throw new Error(
640
-
requestText || "Failed to request identity migration",
641
-
);
642
-
}
643
-
}
644
-
645
-
try {
646
-
const jsonData = JSON.parse(requestText);
647
-
if (!jsonData.success) {
648
-
throw new Error(
649
-
jsonData.message || "Identity migration request failed",
650
-
);
651
-
}
652
-
console.log("Identity migration requested successfully");
653
-
654
-
// Update step name to prompt for token
655
-
setSteps((prevSteps) =>
656
-
prevSteps.map((step, i) =>
657
-
i === 2
658
-
? {
659
-
...step,
660
-
name:
661
-
"Enter the token sent to your email to complete identity migration",
662
-
}
663
-
: step
664
-
)
665
-
);
666
-
// Don't continue with migration - wait for token input
667
-
return;
668
-
} catch (e) {
669
-
console.error("Failed to parse identity request response:", e);
670
-
throw new Error(
671
-
"Invalid response from server during identity request",
672
-
);
673
-
}
674
-
} catch (error) {
675
-
updateStepStatus(
676
-
2,
677
-
"error",
678
-
error instanceof Error ? error.message : String(error),
679
-
);
680
-
throw error;
681
-
}
682
-
};
683
-
684
-
const startFinalization = async () => {
685
-
// Step 4: Finalize Migration
686
-
updateStepStatus(3, "in-progress");
687
-
try {
688
-
const finalizeRes = await fetch("/api/migrate/finalize", {
689
-
method: "POST",
690
-
headers: { "Content-Type": "application/json" },
691
-
});
692
-
693
-
const finalizeData = await finalizeRes.text();
694
-
if (!finalizeRes.ok) {
695
-
try {
696
-
const json = JSON.parse(finalizeData);
697
-
throw new Error(json.message || "Failed to finalize migration");
698
-
} catch {
699
-
throw new Error(finalizeData || "Failed to finalize migration");
700
-
}
701
-
}
702
-
703
-
try {
704
-
const jsonData = JSON.parse(finalizeData);
705
-
if (!jsonData.success) {
706
-
throw new Error(jsonData.message || "Finalization failed");
707
-
}
708
-
} catch {
709
-
throw new Error("Invalid response from server during finalization");
710
-
}
711
-
712
-
updateStepStatus(3, "verifying");
713
-
const verified = await verifyStep(3);
714
-
if (!verified) {
715
-
console.log(
716
-
"Finalization: Verification failed, waiting for user action",
717
-
);
718
-
return;
719
-
}
720
-
} catch (error) {
721
-
updateStepStatus(
722
-
3,
723
-
"error",
724
-
error instanceof Error ? error.message : String(error),
725
-
);
726
-
throw error;
727
-
}
728
-
};
130
+
const allStepsCompleted = completedSteps.size === 4;
729
131
730
132
return (
731
133
<div class="space-y-8">
···
761
163
)}
762
164
763
165
<div class="space-y-4">
764
-
{steps.map((step, index) => (
765
-
<div key={step.name} class={getStepClasses(step.status)}>
766
-
{getStepIcon(step.status)}
767
-
<div class="flex-1">
768
-
<p
769
-
class={`font-medium ${
770
-
step.status === "error"
771
-
? "text-red-900 dark:text-red-200"
772
-
: step.status === "completed"
773
-
? "text-green-900 dark:text-green-200"
774
-
: step.status === "in-progress"
775
-
? "text-blue-900 dark:text-blue-200"
776
-
: "text-gray-900 dark:text-gray-200"
777
-
}`}
778
-
>
779
-
{getStepDisplayName(step, index)}
780
-
</p>
781
-
{step.error && (
782
-
<div class="mt-1">
783
-
<p class="text-sm text-red-600 dark:text-red-400">
784
-
{(() => {
785
-
try {
786
-
const err = JSON.parse(step.error);
787
-
return err.message || step.error;
788
-
} catch {
789
-
return step.error;
790
-
}
791
-
})()}
792
-
</p>
793
-
{step.isVerificationError && (
794
-
<div class="flex space-x-2 mt-2">
795
-
<button
796
-
type="button"
797
-
onClick={() => retryVerification(index)}
798
-
class="px-3 py-1 text-xs bg-blue-600 hover:bg-blue-700 text-white rounded transition-colors duration-200 dark:bg-blue-500 dark:hover:bg-blue-400"
799
-
>
800
-
Retry Verification
801
-
</button>
802
-
{showContinueAnyway[index] && (
803
-
<button
804
-
type="button"
805
-
onClick={() => continueAnyway(index)}
806
-
class="px-3 py-1 text-xs bg-white border border-gray-300 text-gray-700 hover:bg-gray-100 rounded transition-colors duration-200
807
-
dark:bg-gray-800 dark:border-gray-600 dark:text-gray-200 dark:hover:bg-gray-700"
808
-
>
809
-
Continue Anyway
810
-
</button>
811
-
)}
812
-
</div>
813
-
)}
814
-
</div>
815
-
)}
816
-
{index === 2 && step.status === "in-progress" &&
817
-
step.name ===
818
-
"Enter the token sent to your email to complete identity migration" &&
819
-
(
820
-
<div class="mt-4 space-y-4">
821
-
<p class="text-sm text-blue-800 dark:text-blue-200">
822
-
Please check your email for the migration token and enter
823
-
it below:
824
-
</p>
825
-
<div class="flex space-x-2">
826
-
<input
827
-
type="text"
828
-
value={token}
829
-
onChange={(e) => setToken(e.currentTarget.value)}
830
-
placeholder="Enter token"
831
-
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"
832
-
/>
833
-
<button
834
-
type="button"
835
-
onClick={handleIdentityMigration}
836
-
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"
837
-
>
838
-
Submit Token
839
-
</button>
840
-
</div>
841
-
</div>
842
-
)}
843
-
</div>
844
-
</div>
845
-
))}
166
+
<AccountCreationStep
167
+
credentials={credentials}
168
+
onStepComplete={() => handleStepComplete(0)}
169
+
onStepError={(error, isVerificationError) =>
170
+
handleStepError(0, error, isVerificationError)}
171
+
isActive={isStepActive(0)}
172
+
/>
173
+
174
+
<DataMigrationStep
175
+
credentials={credentials}
176
+
onStepComplete={() => handleStepComplete(1)}
177
+
onStepError={(error, isVerificationError) =>
178
+
handleStepError(1, error, isVerificationError)}
179
+
isActive={isStepActive(1)}
180
+
/>
181
+
182
+
<IdentityMigrationStep
183
+
credentials={credentials}
184
+
onStepComplete={() => handleStepComplete(2)}
185
+
onStepError={(error, isVerificationError) =>
186
+
handleStepError(2, error, isVerificationError)}
187
+
isActive={isStepActive(2)}
188
+
/>
189
+
190
+
<FinalizationStep
191
+
credentials={credentials}
192
+
onStepComplete={() => handleStepComplete(3)}
193
+
onStepError={(error, isVerificationError) =>
194
+
handleStepError(3, error, isVerificationError)}
195
+
isActive={isStepActive(3)}
196
+
/>
846
197
</div>
847
198
848
-
{steps[3].status === "completed" && (
849
-
<div class="p-4 bg-green-50 dark:bg-green-900 rounded-lg border-2 border-green-200 dark:border-green-800">
850
-
<p class="text-sm text-green-800 dark:text-green-200 pb-2">
851
-
Migration completed successfully! Sign out to finish the process and
852
-
return home.<br />
853
-
Please consider donating to Airport to support server and
854
-
development costs.
855
-
</p>
856
-
<div class="flex space-x-4">
857
-
<button
858
-
type="button"
859
-
onClick={async () => {
860
-
try {
861
-
const response = await fetch("/api/logout", {
862
-
method: "POST",
863
-
credentials: "include",
864
-
});
865
-
if (!response.ok) {
866
-
throw new Error("Logout failed");
867
-
}
868
-
globalThis.location.href = "/";
869
-
} catch (error) {
870
-
console.error("Failed to logout:", error);
871
-
}
872
-
}}
873
-
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"
874
-
>
875
-
<svg
876
-
class="w-5 h-5"
877
-
fill="none"
878
-
stroke="currentColor"
879
-
viewBox="0 0 24 24"
880
-
>
881
-
<path
882
-
stroke-linecap="round"
883
-
stroke-linejoin="round"
884
-
stroke-width="2"
885
-
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"
886
-
/>
887
-
</svg>
888
-
<span>Sign Out</span>
889
-
</button>
890
-
<a
891
-
href="https://ko-fi.com/knotbin"
892
-
target="_blank"
893
-
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"
894
-
>
895
-
<svg
896
-
class="w-5 h-5"
897
-
fill="none"
898
-
stroke="currentColor"
899
-
viewBox="0 0 24 24"
900
-
>
901
-
<path
902
-
stroke-linecap="round"
903
-
stroke-linejoin="round"
904
-
stroke-width="2"
905
-
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"
906
-
/>
907
-
</svg>
908
-
<span>Support Us</span>
909
-
</a>
910
-
</div>
911
-
</div>
912
-
)}
199
+
<MigrationCompletion isVisible={allStepsCompleted} />
913
200
</div>
914
201
);
915
202
}
+151
islands/migration-steps/AccountCreationStep.tsx
+151
islands/migration-steps/AccountCreationStep.tsx
···
1
+
import { useEffect, useState } from "preact/hooks";
2
+
import { MigrationStep } from "../../components/MigrationStep.tsx";
3
+
import {
4
+
parseApiResponse,
5
+
StepCommonProps,
6
+
verifyMigrationStep,
7
+
} from "../../lib/migration-types.ts";
8
+
9
+
interface AccountCreationStepProps extends StepCommonProps {
10
+
isActive: boolean;
11
+
}
12
+
13
+
export default function AccountCreationStep({
14
+
credentials,
15
+
onStepComplete,
16
+
onStepError,
17
+
isActive,
18
+
}: AccountCreationStepProps) {
19
+
const [status, setStatus] = useState<
20
+
"pending" | "in-progress" | "verifying" | "completed" | "error"
21
+
>("pending");
22
+
const [error, setError] = useState<string>();
23
+
const [retryCount, setRetryCount] = useState(0);
24
+
const [showContinueAnyway, setShowContinueAnyway] = useState(false);
25
+
26
+
useEffect(() => {
27
+
if (isActive && status === "pending") {
28
+
startAccountCreation();
29
+
}
30
+
}, [isActive]);
31
+
32
+
const startAccountCreation = async () => {
33
+
setStatus("in-progress");
34
+
setError(undefined);
35
+
36
+
try {
37
+
const createRes = await fetch("/api/migrate/create", {
38
+
method: "POST",
39
+
headers: { "Content-Type": "application/json" },
40
+
body: JSON.stringify({
41
+
service: credentials.service,
42
+
handle: credentials.handle,
43
+
password: credentials.password,
44
+
email: credentials.email,
45
+
...(credentials.invite ? { invite: credentials.invite } : {}),
46
+
}),
47
+
});
48
+
49
+
const responseText = await createRes.text();
50
+
51
+
if (!createRes.ok) {
52
+
const parsed = parseApiResponse(responseText);
53
+
throw new Error(parsed.message || "Failed to create account");
54
+
}
55
+
56
+
const parsed = parseApiResponse(responseText);
57
+
if (!parsed.success) {
58
+
throw new Error(parsed.message || "Account creation failed");
59
+
}
60
+
61
+
// Verify the account creation
62
+
await verifyAccountCreation();
63
+
} catch (error) {
64
+
const errorMessage = error instanceof Error
65
+
? error.message
66
+
: String(error);
67
+
setError(errorMessage);
68
+
setStatus("error");
69
+
onStepError(errorMessage);
70
+
}
71
+
};
72
+
73
+
const verifyAccountCreation = async () => {
74
+
setStatus("verifying");
75
+
76
+
try {
77
+
const result = await verifyMigrationStep(1);
78
+
79
+
if (result.ready) {
80
+
setStatus("completed");
81
+
setRetryCount(0);
82
+
setShowContinueAnyway(false);
83
+
onStepComplete();
84
+
} else {
85
+
const statusDetails = {
86
+
activated: result.activated,
87
+
validDid: result.validDid,
88
+
};
89
+
const errorMessage = `${
90
+
result.reason || "Verification failed"
91
+
}\nStatus details: ${JSON.stringify(statusDetails, null, 2)}`;
92
+
93
+
setRetryCount((prev) => prev + 1);
94
+
if (retryCount >= 1) {
95
+
setShowContinueAnyway(true);
96
+
}
97
+
98
+
setError(errorMessage);
99
+
setStatus("error");
100
+
onStepError(errorMessage, true);
101
+
}
102
+
} catch (error) {
103
+
const errorMessage = error instanceof Error
104
+
? error.message
105
+
: String(error);
106
+
setRetryCount((prev) => prev + 1);
107
+
if (retryCount >= 1) {
108
+
setShowContinueAnyway(true);
109
+
}
110
+
111
+
setError(errorMessage);
112
+
setStatus("error");
113
+
onStepError(errorMessage, true);
114
+
}
115
+
};
116
+
117
+
const retryVerification = async () => {
118
+
await verifyAccountCreation();
119
+
};
120
+
121
+
const continueAnyway = () => {
122
+
setStatus("completed");
123
+
setShowContinueAnyway(false);
124
+
onStepComplete();
125
+
};
126
+
127
+
return (
128
+
<MigrationStep
129
+
name="Create Account"
130
+
status={status}
131
+
error={error}
132
+
isVerificationError={status === "error" &&
133
+
error?.includes("Verification failed")}
134
+
index={0}
135
+
onRetryVerification={retryVerification}
136
+
>
137
+
{status === "error" && showContinueAnyway && (
138
+
<div class="flex space-x-2 mt-2">
139
+
<button
140
+
type="button"
141
+
onClick={continueAnyway}
142
+
class="px-3 py-1 text-xs bg-white border border-gray-300 text-gray-700 hover:bg-gray-100 rounded transition-colors duration-200
143
+
dark:bg-gray-800 dark:border-gray-600 dark:text-gray-200 dark:hover:bg-gray-700"
144
+
>
145
+
Continue Anyway
146
+
</button>
147
+
</div>
148
+
)}
149
+
</MigrationStep>
150
+
);
151
+
}
+172
islands/migration-steps/DataMigrationStep.tsx
+172
islands/migration-steps/DataMigrationStep.tsx
···
1
+
import { useEffect, useState } from "preact/hooks";
2
+
import { MigrationStep } from "../../components/MigrationStep.tsx";
3
+
import {
4
+
parseApiResponse,
5
+
StepCommonProps,
6
+
verifyMigrationStep,
7
+
} from "../../lib/migration-types.ts";
8
+
9
+
interface DataMigrationStepProps extends StepCommonProps {
10
+
isActive: boolean;
11
+
}
12
+
13
+
export default function DataMigrationStep({
14
+
credentials: _credentials,
15
+
onStepComplete,
16
+
onStepError,
17
+
isActive,
18
+
}: DataMigrationStepProps) {
19
+
const [status, setStatus] = useState<
20
+
"pending" | "in-progress" | "verifying" | "completed" | "error"
21
+
>("pending");
22
+
const [error, setError] = useState<string>();
23
+
const [retryCount, setRetryCount] = useState(0);
24
+
const [showContinueAnyway, setShowContinueAnyway] = useState(false);
25
+
26
+
useEffect(() => {
27
+
if (isActive && status === "pending") {
28
+
startDataMigration();
29
+
}
30
+
}, [isActive]);
31
+
32
+
const startDataMigration = async () => {
33
+
setStatus("in-progress");
34
+
setError(undefined);
35
+
36
+
try {
37
+
// Step 1: Migrate Repo
38
+
const repoRes = await fetch("/api/migrate/data/repo", {
39
+
method: "POST",
40
+
headers: { "Content-Type": "application/json" },
41
+
});
42
+
43
+
const repoText = await repoRes.text();
44
+
45
+
if (!repoRes.ok) {
46
+
const parsed = parseApiResponse(repoText);
47
+
throw new Error(parsed.message || "Failed to migrate repo");
48
+
}
49
+
50
+
// Step 2: Migrate Blobs
51
+
const blobsRes = await fetch("/api/migrate/data/blobs", {
52
+
method: "POST",
53
+
headers: { "Content-Type": "application/json" },
54
+
});
55
+
56
+
const blobsText = await blobsRes.text();
57
+
58
+
if (!blobsRes.ok) {
59
+
const parsed = parseApiResponse(blobsText);
60
+
throw new Error(parsed.message || "Failed to migrate blobs");
61
+
}
62
+
63
+
// Step 3: Migrate Preferences
64
+
const prefsRes = await fetch("/api/migrate/data/prefs", {
65
+
method: "POST",
66
+
headers: { "Content-Type": "application/json" },
67
+
});
68
+
69
+
const prefsText = await prefsRes.text();
70
+
71
+
if (!prefsRes.ok) {
72
+
const parsed = parseApiResponse(prefsText);
73
+
throw new Error(parsed.message || "Failed to migrate preferences");
74
+
}
75
+
76
+
// Verify the data migration
77
+
await verifyDataMigration();
78
+
} catch (error) {
79
+
const errorMessage = error instanceof Error
80
+
? error.message
81
+
: String(error);
82
+
setError(errorMessage);
83
+
setStatus("error");
84
+
onStepError(errorMessage);
85
+
}
86
+
};
87
+
88
+
const verifyDataMigration = async () => {
89
+
setStatus("verifying");
90
+
91
+
try {
92
+
const result = await verifyMigrationStep(2);
93
+
94
+
if (result.ready) {
95
+
setStatus("completed");
96
+
setRetryCount(0);
97
+
setShowContinueAnyway(false);
98
+
onStepComplete();
99
+
} else {
100
+
const statusDetails = {
101
+
repoCommit: result.repoCommit,
102
+
repoRev: result.repoRev,
103
+
repoBlocks: result.repoBlocks,
104
+
expectedRecords: result.expectedRecords,
105
+
indexedRecords: result.indexedRecords,
106
+
privateStateValues: result.privateStateValues,
107
+
expectedBlobs: result.expectedBlobs,
108
+
importedBlobs: result.importedBlobs,
109
+
};
110
+
const errorMessage = `${
111
+
result.reason || "Verification failed"
112
+
}\nStatus details: ${JSON.stringify(statusDetails, null, 2)}`;
113
+
114
+
setRetryCount((prev) => prev + 1);
115
+
if (retryCount >= 1) {
116
+
setShowContinueAnyway(true);
117
+
}
118
+
119
+
setError(errorMessage);
120
+
setStatus("error");
121
+
onStepError(errorMessage, true);
122
+
}
123
+
} catch (error) {
124
+
const errorMessage = error instanceof Error
125
+
? error.message
126
+
: String(error);
127
+
setRetryCount((prev) => prev + 1);
128
+
if (retryCount >= 1) {
129
+
setShowContinueAnyway(true);
130
+
}
131
+
132
+
setError(errorMessage);
133
+
setStatus("error");
134
+
onStepError(errorMessage, true);
135
+
}
136
+
};
137
+
138
+
const retryVerification = async () => {
139
+
await verifyDataMigration();
140
+
};
141
+
142
+
const continueAnyway = () => {
143
+
setStatus("completed");
144
+
setShowContinueAnyway(false);
145
+
onStepComplete();
146
+
};
147
+
148
+
return (
149
+
<MigrationStep
150
+
name="Migrate Data"
151
+
status={status}
152
+
error={error}
153
+
isVerificationError={status === "error" &&
154
+
error?.includes("Verification failed")}
155
+
index={1}
156
+
onRetryVerification={retryVerification}
157
+
>
158
+
{status === "error" && showContinueAnyway && (
159
+
<div class="flex space-x-2 mt-2">
160
+
<button
161
+
type="button"
162
+
onClick={continueAnyway}
163
+
class="px-3 py-1 text-xs bg-white border border-gray-300 text-gray-700 hover:bg-gray-100 rounded transition-colors duration-200
164
+
dark:bg-gray-800 dark:border-gray-600 dark:text-gray-200 dark:hover:bg-gray-700"
165
+
>
166
+
Continue Anyway
167
+
</button>
168
+
</div>
169
+
)}
170
+
</MigrationStep>
171
+
);
172
+
}
+143
islands/migration-steps/FinalizationStep.tsx
+143
islands/migration-steps/FinalizationStep.tsx
···
1
+
import { useEffect, useState } from "preact/hooks";
2
+
import { MigrationStep } from "../../components/MigrationStep.tsx";
3
+
import {
4
+
parseApiResponse,
5
+
StepCommonProps,
6
+
verifyMigrationStep,
7
+
} from "../../lib/migration-types.ts";
8
+
9
+
interface FinalizationStepProps extends StepCommonProps {
10
+
isActive: boolean;
11
+
}
12
+
13
+
export default function FinalizationStep({
14
+
credentials: _credentials,
15
+
onStepComplete,
16
+
onStepError,
17
+
isActive,
18
+
}: FinalizationStepProps) {
19
+
const [status, setStatus] = useState<
20
+
"pending" | "in-progress" | "verifying" | "completed" | "error"
21
+
>("pending");
22
+
const [error, setError] = useState<string>();
23
+
const [retryCount, setRetryCount] = useState(0);
24
+
const [showContinueAnyway, setShowContinueAnyway] = useState(false);
25
+
26
+
useEffect(() => {
27
+
if (isActive && status === "pending") {
28
+
startFinalization();
29
+
}
30
+
}, [isActive]);
31
+
32
+
const startFinalization = async () => {
33
+
setStatus("in-progress");
34
+
setError(undefined);
35
+
36
+
try {
37
+
const finalizeRes = await fetch("/api/migrate/finalize", {
38
+
method: "POST",
39
+
headers: { "Content-Type": "application/json" },
40
+
});
41
+
42
+
const finalizeData = await finalizeRes.text();
43
+
if (!finalizeRes.ok) {
44
+
const parsed = parseApiResponse(finalizeData);
45
+
throw new Error(parsed.message || "Failed to finalize migration");
46
+
}
47
+
48
+
const parsed = parseApiResponse(finalizeData);
49
+
if (!parsed.success) {
50
+
throw new Error(parsed.message || "Finalization failed");
51
+
}
52
+
53
+
// Verify the finalization
54
+
await verifyFinalization();
55
+
} catch (error) {
56
+
const errorMessage = error instanceof Error
57
+
? error.message
58
+
: String(error);
59
+
setError(errorMessage);
60
+
setStatus("error");
61
+
onStepError(errorMessage);
62
+
}
63
+
};
64
+
65
+
const verifyFinalization = async () => {
66
+
setStatus("verifying");
67
+
68
+
try {
69
+
const result = await verifyMigrationStep(4);
70
+
71
+
if (result.ready) {
72
+
setStatus("completed");
73
+
setRetryCount(0);
74
+
setShowContinueAnyway(false);
75
+
onStepComplete();
76
+
} else {
77
+
const statusDetails = {
78
+
activated: result.activated,
79
+
validDid: result.validDid,
80
+
};
81
+
const errorMessage = `${
82
+
result.reason || "Verification failed"
83
+
}\nStatus details: ${JSON.stringify(statusDetails, null, 2)}`;
84
+
85
+
setRetryCount((prev) => prev + 1);
86
+
if (retryCount >= 1) {
87
+
setShowContinueAnyway(true);
88
+
}
89
+
90
+
setError(errorMessage);
91
+
setStatus("error");
92
+
onStepError(errorMessage, true);
93
+
}
94
+
} catch (error) {
95
+
const errorMessage = error instanceof Error
96
+
? error.message
97
+
: String(error);
98
+
setRetryCount((prev) => prev + 1);
99
+
if (retryCount >= 1) {
100
+
setShowContinueAnyway(true);
101
+
}
102
+
103
+
setError(errorMessage);
104
+
setStatus("error");
105
+
onStepError(errorMessage, true);
106
+
}
107
+
};
108
+
109
+
const retryVerification = async () => {
110
+
await verifyFinalization();
111
+
};
112
+
113
+
const continueAnyway = () => {
114
+
setStatus("completed");
115
+
setShowContinueAnyway(false);
116
+
onStepComplete();
117
+
};
118
+
119
+
return (
120
+
<MigrationStep
121
+
name="Finalize Migration"
122
+
status={status}
123
+
error={error}
124
+
isVerificationError={status === "error" &&
125
+
error?.includes("Verification failed")}
126
+
index={3}
127
+
onRetryVerification={retryVerification}
128
+
>
129
+
{status === "error" && showContinueAnyway && (
130
+
<div class="flex space-x-2 mt-2">
131
+
<button
132
+
type="button"
133
+
onClick={continueAnyway}
134
+
class="px-3 py-1 text-xs bg-white border border-gray-300 text-gray-700 hover:bg-gray-100 rounded transition-colors duration-200
135
+
dark:bg-gray-800 dark:border-gray-600 dark:text-gray-200 dark:hover:bg-gray-700"
136
+
>
137
+
Continue Anyway
138
+
</button>
139
+
</div>
140
+
)}
141
+
</MigrationStep>
142
+
);
143
+
}
+294
islands/migration-steps/IdentityMigrationStep.tsx
+294
islands/migration-steps/IdentityMigrationStep.tsx
···
1
+
import { useEffect, useRef, useState } from "preact/hooks";
2
+
import { MigrationStep } from "../../components/MigrationStep.tsx";
3
+
import {
4
+
parseApiResponse,
5
+
StepCommonProps,
6
+
verifyMigrationStep,
7
+
} from "../../lib/migration-types.ts";
8
+
9
+
interface IdentityMigrationStepProps extends StepCommonProps {
10
+
isActive: boolean;
11
+
}
12
+
13
+
export default function IdentityMigrationStep({
14
+
credentials: _credentials,
15
+
onStepComplete,
16
+
onStepError,
17
+
isActive,
18
+
}: IdentityMigrationStepProps) {
19
+
const [status, setStatus] = useState<
20
+
"pending" | "in-progress" | "verifying" | "completed" | "error"
21
+
>("pending");
22
+
const [error, setError] = useState<string>();
23
+
const [retryCount, setRetryCount] = useState(0);
24
+
const [showContinueAnyway, setShowContinueAnyway] = useState(false);
25
+
const [token, setToken] = useState("");
26
+
const [identityRequestSent, setIdentityRequestSent] = useState(false);
27
+
const [identityRequestCooldown, setIdentityRequestCooldown] = useState(0);
28
+
const [cooldownInterval, setCooldownInterval] = useState<number | null>(null);
29
+
const [stepName, setStepName] = useState("Migrate Identity");
30
+
const identityRequestInProgressRef = useRef(false);
31
+
32
+
// Clean up interval on unmount
33
+
useEffect(() => {
34
+
return () => {
35
+
if (cooldownInterval !== null) {
36
+
clearInterval(cooldownInterval);
37
+
}
38
+
};
39
+
}, [cooldownInterval]);
40
+
41
+
useEffect(() => {
42
+
if (isActive && status === "pending") {
43
+
startIdentityMigration();
44
+
}
45
+
}, [isActive]);
46
+
47
+
const startIdentityMigration = async () => {
48
+
// Prevent multiple concurrent calls
49
+
if (identityRequestInProgressRef.current) {
50
+
return;
51
+
}
52
+
53
+
identityRequestInProgressRef.current = true;
54
+
setStatus("in-progress");
55
+
setError(undefined);
56
+
57
+
// Don't send duplicate requests
58
+
if (identityRequestSent) {
59
+
setStepName(
60
+
"Enter the token sent to your email to complete identity migration",
61
+
);
62
+
setTimeout(() => {
63
+
identityRequestInProgressRef.current = false;
64
+
}, 1000);
65
+
return;
66
+
}
67
+
68
+
try {
69
+
const requestRes = await fetch("/api/migrate/identity/request", {
70
+
method: "POST",
71
+
headers: { "Content-Type": "application/json" },
72
+
});
73
+
74
+
const requestText = await requestRes.text();
75
+
76
+
if (!requestRes.ok) {
77
+
const parsed = parseApiResponse(requestText);
78
+
throw new Error(
79
+
parsed.message || "Failed to request identity migration",
80
+
);
81
+
}
82
+
83
+
const parsed = parseApiResponse(requestText);
84
+
if (!parsed.success) {
85
+
throw new Error(parsed.message || "Identity migration request failed");
86
+
}
87
+
88
+
// Mark request as sent
89
+
setIdentityRequestSent(true);
90
+
91
+
// Handle rate limiting
92
+
const jsonData = JSON.parse(requestText);
93
+
if (jsonData.rateLimited && jsonData.cooldownRemaining) {
94
+
setIdentityRequestCooldown(jsonData.cooldownRemaining);
95
+
96
+
// Clear any existing interval
97
+
if (cooldownInterval !== null) {
98
+
clearInterval(cooldownInterval);
99
+
}
100
+
101
+
// Set up countdown timer
102
+
const intervalId = setInterval(() => {
103
+
setIdentityRequestCooldown((prev) => {
104
+
if (prev <= 1) {
105
+
clearInterval(intervalId);
106
+
setCooldownInterval(null);
107
+
return 0;
108
+
}
109
+
return prev - 1;
110
+
});
111
+
}, 1000);
112
+
113
+
setCooldownInterval(intervalId);
114
+
}
115
+
116
+
// Update step name to prompt for token
117
+
setStepName(
118
+
identityRequestCooldown > 0
119
+
? `Please wait ${identityRequestCooldown}s before requesting another code`
120
+
: "Enter the token sent to your email to complete identity migration",
121
+
);
122
+
} catch (error) {
123
+
const errorMessage = error instanceof Error
124
+
? error.message
125
+
: String(error);
126
+
// Don't mark as error if it was due to rate limiting
127
+
if (identityRequestCooldown > 0) {
128
+
setStatus("in-progress");
129
+
} else {
130
+
setError(errorMessage);
131
+
setStatus("error");
132
+
onStepError(errorMessage);
133
+
}
134
+
} finally {
135
+
setTimeout(() => {
136
+
identityRequestInProgressRef.current = false;
137
+
}, 1000);
138
+
}
139
+
};
140
+
141
+
const handleIdentityMigration = async () => {
142
+
if (!token) return;
143
+
144
+
try {
145
+
const identityRes = await fetch(
146
+
`/api/migrate/identity/sign?token=${encodeURIComponent(token)}`,
147
+
{
148
+
method: "POST",
149
+
headers: { "Content-Type": "application/json" },
150
+
},
151
+
);
152
+
153
+
const identityData = await identityRes.text();
154
+
if (!identityRes.ok) {
155
+
const parsed = parseApiResponse(identityData);
156
+
throw new Error(
157
+
parsed.message || "Failed to complete identity migration",
158
+
);
159
+
}
160
+
161
+
const parsed = parseApiResponse(identityData);
162
+
if (!parsed.success) {
163
+
throw new Error(parsed.message || "Identity migration failed");
164
+
}
165
+
166
+
// Verify the identity migration
167
+
await verifyIdentityMigration();
168
+
} catch (error) {
169
+
const errorMessage = error instanceof Error
170
+
? error.message
171
+
: String(error);
172
+
setError(errorMessage);
173
+
setStatus("error");
174
+
onStepError(errorMessage);
175
+
}
176
+
};
177
+
178
+
const verifyIdentityMigration = async () => {
179
+
setStatus("verifying");
180
+
181
+
try {
182
+
const result = await verifyMigrationStep(3);
183
+
184
+
if (result.ready) {
185
+
setStatus("completed");
186
+
setRetryCount(0);
187
+
setShowContinueAnyway(false);
188
+
onStepComplete();
189
+
} else {
190
+
const statusDetails = {
191
+
activated: result.activated,
192
+
validDid: result.validDid,
193
+
};
194
+
const errorMessage = `${
195
+
result.reason || "Verification failed"
196
+
}\nStatus details: ${JSON.stringify(statusDetails, null, 2)}`;
197
+
198
+
setRetryCount((prev) => prev + 1);
199
+
if (retryCount >= 1) {
200
+
setShowContinueAnyway(true);
201
+
}
202
+
203
+
setError(errorMessage);
204
+
setStatus("error");
205
+
onStepError(errorMessage, true);
206
+
}
207
+
} catch (error) {
208
+
const errorMessage = error instanceof Error
209
+
? error.message
210
+
: String(error);
211
+
setRetryCount((prev) => prev + 1);
212
+
if (retryCount >= 1) {
213
+
setShowContinueAnyway(true);
214
+
}
215
+
216
+
setError(errorMessage);
217
+
setStatus("error");
218
+
onStepError(errorMessage, true);
219
+
}
220
+
};
221
+
222
+
const retryVerification = async () => {
223
+
await verifyIdentityMigration();
224
+
};
225
+
226
+
const continueAnyway = () => {
227
+
setStatus("completed");
228
+
setShowContinueAnyway(false);
229
+
onStepComplete();
230
+
};
231
+
232
+
return (
233
+
<MigrationStep
234
+
name={stepName}
235
+
status={status}
236
+
error={error}
237
+
isVerificationError={status === "error" &&
238
+
error?.includes("Verification failed")}
239
+
index={2}
240
+
onRetryVerification={retryVerification}
241
+
>
242
+
{status === "error" && showContinueAnyway && (
243
+
<div class="flex space-x-2 mt-2">
244
+
<button
245
+
type="button"
246
+
onClick={continueAnyway}
247
+
class="px-3 py-1 text-xs bg-white border border-gray-300 text-gray-700 hover:bg-gray-100 rounded transition-colors duration-200
248
+
dark:bg-gray-800 dark:border-gray-600 dark:text-gray-200 dark:hover:bg-gray-700"
249
+
>
250
+
Continue Anyway
251
+
</button>
252
+
</div>
253
+
)}
254
+
255
+
{(status === "in-progress" || identityRequestSent) &&
256
+
stepName.includes("Enter the token sent to your email") &&
257
+
(identityRequestCooldown > 0
258
+
? (
259
+
<div class="mt-4">
260
+
<p class="text-sm text-amber-600 dark:text-amber-400">
261
+
<span class="font-medium">Rate limit:</span> Please wait{" "}
262
+
{identityRequestCooldown}{" "}
263
+
seconds before requesting another code. Check your email inbox
264
+
and spam folder for a previously sent code.
265
+
</p>
266
+
</div>
267
+
)
268
+
: (
269
+
<div class="mt-4 space-y-4">
270
+
<p class="text-sm text-blue-800 dark:text-blue-200">
271
+
Please check your email for the migration token and enter it
272
+
below:
273
+
</p>
274
+
<div class="flex space-x-2">
275
+
<input
276
+
type="text"
277
+
value={token}
278
+
onChange={(e) => setToken(e.currentTarget.value)}
279
+
placeholder="Enter token"
280
+
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"
281
+
/>
282
+
<button
283
+
type="button"
284
+
onClick={handleIdentityMigration}
285
+
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"
286
+
>
287
+
Submit Token
288
+
</button>
289
+
</div>
290
+
</div>
291
+
))}
292
+
</MigrationStep>
293
+
);
294
+
}
+63
lib/migration-types.ts
+63
lib/migration-types.ts
···
1
+
/**
2
+
* Shared types for migration components
3
+
*/
4
+
5
+
export interface MigrationStateInfo {
6
+
state: "up" | "issue" | "maintenance";
7
+
message: string;
8
+
allowMigration: boolean;
9
+
}
10
+
11
+
export interface MigrationCredentials {
12
+
service: string;
13
+
handle: string;
14
+
email: string;
15
+
password: string;
16
+
invite?: string;
17
+
}
18
+
19
+
export interface StepCommonProps {
20
+
credentials: MigrationCredentials;
21
+
onStepComplete: () => void;
22
+
onStepError: (error: string, isVerificationError?: boolean) => void;
23
+
}
24
+
25
+
export interface VerificationResult {
26
+
ready: boolean;
27
+
reason?: string;
28
+
activated?: boolean;
29
+
validDid?: boolean;
30
+
repoCommit?: boolean;
31
+
repoRev?: boolean;
32
+
repoBlocks?: number;
33
+
expectedRecords?: number;
34
+
indexedRecords?: number;
35
+
privateStateValues?: number;
36
+
expectedBlobs?: number;
37
+
importedBlobs?: number;
38
+
}
39
+
40
+
/**
41
+
* Helper function to verify a migration step
42
+
*/
43
+
export async function verifyMigrationStep(
44
+
stepNum: number,
45
+
): Promise<VerificationResult> {
46
+
const res = await fetch(`/api/migrate/status?step=${stepNum}`);
47
+
const data = await res.json();
48
+
return data;
49
+
}
50
+
51
+
/**
52
+
* Helper function to handle API responses with proper error parsing
53
+
*/
54
+
export function parseApiResponse(
55
+
responseText: string,
56
+
): { success: boolean; message?: string } {
57
+
try {
58
+
const json = JSON.parse(responseText);
59
+
return { success: json.success !== false, message: json.message };
60
+
} catch {
61
+
return { success: responseText.trim() !== "", message: responseText };
62
+
}
63
+
}
+52
routes/api/migrate/identity/request.ts
+52
routes/api/migrate/identity/request.ts
···
3
3
import { define } from "../../../../utils.ts";
4
4
import { assertMigrationAllowed } from "../../../../lib/migration-state.ts";
5
5
6
+
// Simple in-memory cache for rate limiting
7
+
// In a production environment, you might want to use Redis or another shared cache
8
+
const requestCache = new Map<string, number>();
9
+
const COOLDOWN_PERIOD_MS = 60000; // 1 minute cooldown
10
+
6
11
/**
7
12
* Handle identity migration request
8
13
* Sends a PLC operation signature request to the old account's email
···
70
75
);
71
76
}
72
77
78
+
// Check if we've recently sent a request for this DID
79
+
const did = oldAgent.did || "";
80
+
const now = Date.now();
81
+
const lastRequestTime = requestCache.get(did);
82
+
83
+
if (lastRequestTime && now - lastRequestTime < COOLDOWN_PERIOD_MS) {
84
+
console.log(
85
+
`Rate limiting PLC request for ${did}, last request was ${
86
+
(now - lastRequestTime) / 1000
87
+
} seconds ago`,
88
+
);
89
+
return new Response(
90
+
JSON.stringify({
91
+
success: true,
92
+
message:
93
+
"A PLC code was already sent to your email. Please check your inbox and spam folder.",
94
+
rateLimited: true,
95
+
cooldownRemaining: Math.ceil(
96
+
(COOLDOWN_PERIOD_MS - (now - lastRequestTime)) / 1000,
97
+
),
98
+
}),
99
+
{
100
+
status: 200,
101
+
headers: {
102
+
"Content-Type": "application/json",
103
+
...Object.fromEntries(res.headers),
104
+
},
105
+
},
106
+
);
107
+
}
108
+
73
109
// Request the signature
74
110
console.log("Requesting PLC operation signature...");
75
111
try {
76
112
await oldAgent.com.atproto.identity.requestPlcOperationSignature();
77
113
console.log("Successfully requested PLC operation signature");
114
+
115
+
// Store the request time
116
+
if (did) {
117
+
requestCache.set(did, now);
118
+
119
+
// Optionally, set up cache cleanup for DIDs that haven't been used in a while
120
+
setTimeout(() => {
121
+
if (
122
+
did &&
123
+
requestCache.has(did) &&
124
+
Date.now() - requestCache.get(did)! > COOLDOWN_PERIOD_MS * 2
125
+
) {
126
+
requestCache.delete(did);
127
+
}
128
+
}, COOLDOWN_PERIOD_MS * 2);
129
+
}
78
130
} catch (error) {
79
131
console.error("Error requesting PLC operation signature:", {
80
132
name: error instanceof Error ? error.name : "Unknown",
-2
routes/api/plc/token.ts
-2
routes/api/plc/token.ts
-1
routes/api/plc/update/complete.ts
-1
routes/api/plc/update/complete.ts
+1
-6
routes/ticket-booth/index.tsx
+1
-6
routes/ticket-booth/index.tsx
···
1
-
import { PageProps } from "fresh";
2
-
import MigrationSetup from "../../islands/MigrationSetup.tsx";
3
1
import DidPlcProgress from "../../islands/DidPlcProgress.tsx";
4
2
5
-
export default function TicketBooth(props: PageProps) {
6
-
const service = props.url.searchParams.get("service");
7
-
const handle = props.url.searchParams.get("handle");
8
-
3
+
export default function TicketBooth() {
9
4
return (
10
5
<div class=" bg-gray-50 dark:bg-gray-900 p-4">
11
6
<div class="max-w-2xl mx-auto">