this repo has no description
1<script lang="ts">
2 import { getAuthState, logout, setSession } from '../lib/auth.svelte'
3 import { navigate } from '../lib/router.svelte'
4 import {
5 createInboundMigrationFlow,
6 createOutboundMigrationFlow,
7 hasPendingMigration,
8 getResumeInfo,
9 clearMigrationState,
10 loadMigrationState,
11 } from '../lib/migration'
12 import InboundWizard from '../components/migration/InboundWizard.svelte'
13 import OutboundWizard from '../components/migration/OutboundWizard.svelte'
14
15 const auth = getAuthState()
16
17 type Direction = 'select' | 'inbound' | 'outbound'
18 let direction = $state<Direction>('select')
19 let showResumeModal = $state(false)
20 let resumeInfo = $state<ReturnType<typeof getResumeInfo>>(null)
21
22 let inboundFlow = $state<ReturnType<typeof createInboundMigrationFlow> | null>(null)
23 let outboundFlow = $state<ReturnType<typeof createOutboundMigrationFlow> | null>(null)
24
25 if (hasPendingMigration()) {
26 resumeInfo = getResumeInfo()
27 if (resumeInfo) {
28 showResumeModal = true
29 }
30 }
31
32 function selectInbound() {
33 direction = 'inbound'
34 inboundFlow = createInboundMigrationFlow()
35 }
36
37 function selectOutbound() {
38 if (!auth.session) {
39 navigate('/login')
40 return
41 }
42 direction = 'outbound'
43 outboundFlow = createOutboundMigrationFlow()
44 outboundFlow.initLocalClient(auth.session.accessJwt, auth.session.did, auth.session.handle)
45 }
46
47 function handleResume() {
48 const stored = loadMigrationState()
49 if (!stored) return
50
51 showResumeModal = false
52
53 if (stored.direction === 'inbound') {
54 direction = 'inbound'
55 inboundFlow = createInboundMigrationFlow()
56 inboundFlow.resumeFromState(stored)
57 } else {
58 if (!auth.session) {
59 navigate('/login')
60 return
61 }
62 direction = 'outbound'
63 outboundFlow = createOutboundMigrationFlow()
64 outboundFlow.initLocalClient(auth.session.accessJwt, auth.session.did, auth.session.handle)
65 }
66 }
67
68 function handleStartOver() {
69 showResumeModal = false
70 clearMigrationState()
71 resumeInfo = null
72 }
73
74 function handleBack() {
75 if (inboundFlow) {
76 inboundFlow.reset()
77 inboundFlow = null
78 }
79 if (outboundFlow) {
80 outboundFlow.reset()
81 outboundFlow = null
82 }
83 direction = 'select'
84 }
85
86 function handleInboundComplete() {
87 const session = inboundFlow?.getLocalSession()
88 if (session) {
89 setSession({
90 did: session.did,
91 handle: session.handle,
92 accessJwt: session.accessJwt,
93 refreshJwt: '',
94 })
95 }
96 navigate('/dashboard')
97 }
98
99 async function handleOutboundComplete() {
100 await logout()
101 navigate('/login')
102 }
103</script>
104
105<div class="migration-page">
106 {#if showResumeModal && resumeInfo}
107 <div class="modal-overlay">
108 <div class="modal">
109 <h2>Resume Migration?</h2>
110 <p>You have an incomplete migration in progress:</p>
111 <div class="resume-details">
112 <div class="detail-row">
113 <span class="label">Direction:</span>
114 <span class="value">{resumeInfo.direction === 'inbound' ? 'Migrating here' : 'Migrating away'}</span>
115 </div>
116 {#if resumeInfo.sourceHandle}
117 <div class="detail-row">
118 <span class="label">From:</span>
119 <span class="value">{resumeInfo.sourceHandle}</span>
120 </div>
121 {/if}
122 {#if resumeInfo.targetHandle}
123 <div class="detail-row">
124 <span class="label">To:</span>
125 <span class="value">{resumeInfo.targetHandle}</span>
126 </div>
127 {/if}
128 <div class="detail-row">
129 <span class="label">Progress:</span>
130 <span class="value">{resumeInfo.progressSummary}</span>
131 </div>
132 </div>
133 <p class="note">You will need to re-enter your credentials to continue.</p>
134 <div class="modal-actions">
135 <button class="ghost" onclick={handleStartOver}>Start Over</button>
136 <button onclick={handleResume}>Resume</button>
137 </div>
138 </div>
139 </div>
140 {/if}
141
142 {#if direction === 'select'}
143 <header class="page-header">
144 <h1>Account Migration</h1>
145 <p class="subtitle">Move your AT Protocol identity between servers</p>
146 </header>
147
148 <div class="direction-cards">
149 <button class="direction-card ghost" onclick={selectInbound}>
150 <div class="card-icon">↓</div>
151 <h2>Migrate Here</h2>
152 <p>Move your existing AT Protocol account to this PDS from another server.</p>
153 <ul class="features">
154 <li>Bring your DID and identity</li>
155 <li>Transfer all your data</li>
156 <li>Keep your followers</li>
157 </ul>
158 </button>
159
160 <button class="direction-card ghost" onclick={selectOutbound} disabled={!auth.session}>
161 <div class="card-icon">↑</div>
162 <h2>Migrate Away</h2>
163 <p>Move your account from this PDS to another server.</p>
164 <ul class="features">
165 <li>Export your repository</li>
166 <li>Transfer to new PDS</li>
167 <li>Update your identity</li>
168 </ul>
169 {#if !auth.session}
170 <p class="login-required">Login required</p>
171 {/if}
172 </button>
173 </div>
174
175 <div class="info-section">
176 <h3>What is account migration?</h3>
177 <p>
178 Account migration allows you to move your AT Protocol identity between Personal Data Servers (PDSes).
179 Your DID (decentralized identifier) stays the same, so your followers and social connections are preserved.
180 </p>
181
182 <h3>Before you migrate</h3>
183 <ul>
184 <li>You will need your current account credentials</li>
185 <li>Migration requires email verification for security</li>
186 <li>Large accounts with many images may take several minutes</li>
187 <li>Your old PDS will be notified to deactivate your account</li>
188 </ul>
189
190 <div class="warning-box">
191 <strong>Important:</strong> Account migration is a significant action. Make sure you trust the destination PDS
192 and understand that your data will be moved. If something goes wrong, recovery may require manual intervention.
193 <a href="https://github.com/bluesky-social/pds/blob/main/ACCOUNT_MIGRATION.md" target="_blank" rel="noopener">
194 Learn more about migration risks
195 </a>
196 </div>
197 </div>
198
199 {:else if direction === 'inbound' && inboundFlow}
200 <InboundWizard
201 flow={inboundFlow}
202 onBack={handleBack}
203 onComplete={handleInboundComplete}
204 />
205
206 {:else if direction === 'outbound' && outboundFlow}
207 <OutboundWizard
208 flow={outboundFlow}
209 onBack={handleBack}
210 onComplete={handleOutboundComplete}
211 />
212 {/if}
213</div>
214
215<style>
216 .migration-page {
217 max-width: var(--width-lg);
218 margin: var(--space-9) auto;
219 padding: var(--space-7);
220 }
221
222 .page-header {
223 text-align: center;
224 margin-bottom: var(--space-8);
225 }
226
227 .page-header h1 {
228 margin: 0 0 var(--space-3) 0;
229 }
230
231 .subtitle {
232 color: var(--text-secondary);
233 margin: 0;
234 font-size: var(--text-lg);
235 }
236
237 .direction-cards {
238 display: grid;
239 grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
240 gap: var(--space-6);
241 margin-bottom: var(--space-8);
242 }
243
244 .direction-card {
245 background: var(--bg-secondary);
246 border: 1px solid var(--border);
247 border-radius: var(--radius-xl);
248 padding: var(--space-6);
249 text-align: left;
250 cursor: pointer;
251 transition: all 0.2s ease;
252 }
253
254 .direction-card:hover:not(:disabled) {
255 border-color: var(--accent);
256 transform: translateY(-2px);
257 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
258 }
259
260 .direction-card:disabled {
261 opacity: 0.6;
262 cursor: not-allowed;
263 }
264
265 .card-icon {
266 font-size: var(--text-3xl);
267 margin-bottom: var(--space-4);
268 color: var(--accent);
269 }
270
271 .direction-card h2 {
272 margin: 0 0 var(--space-3) 0;
273 font-size: var(--text-xl);
274 color: var(--text-primary);
275 }
276
277 .direction-card p {
278 color: var(--text-secondary);
279 margin: 0 0 var(--space-4) 0;
280 font-size: var(--text-sm);
281 }
282
283 .features {
284 margin: 0;
285 padding-left: var(--space-5);
286 color: var(--text-secondary);
287 font-size: var(--text-sm);
288 }
289
290 .features li {
291 margin-bottom: var(--space-2);
292 }
293
294 .login-required {
295 color: var(--warning-text);
296 font-weight: var(--font-medium);
297 margin-top: var(--space-4);
298 }
299
300 .info-section {
301 background: var(--bg-secondary);
302 border-radius: var(--radius-xl);
303 padding: var(--space-6);
304 }
305
306 .info-section h3 {
307 margin: 0 0 var(--space-3) 0;
308 font-size: var(--text-lg);
309 }
310
311 .info-section h3:not(:first-child) {
312 margin-top: var(--space-6);
313 }
314
315 .info-section p {
316 color: var(--text-secondary);
317 line-height: var(--leading-relaxed);
318 margin: 0;
319 }
320
321 .info-section ul {
322 color: var(--text-secondary);
323 padding-left: var(--space-5);
324 margin: var(--space-3) 0 0 0;
325 }
326
327 .info-section li {
328 margin-bottom: var(--space-2);
329 }
330
331 .warning-box {
332 margin-top: var(--space-6);
333 padding: var(--space-5);
334 background: var(--warning-bg);
335 border: 1px solid var(--warning-border);
336 border-radius: var(--radius-lg);
337 font-size: var(--text-sm);
338 }
339
340 .warning-box strong {
341 color: var(--warning-text);
342 }
343
344 .warning-box a {
345 display: block;
346 margin-top: var(--space-3);
347 color: var(--accent);
348 }
349
350 .modal-overlay {
351 position: fixed;
352 inset: 0;
353 background: rgba(0, 0, 0, 0.5);
354 display: flex;
355 align-items: center;
356 justify-content: center;
357 z-index: 1000;
358 }
359
360 .modal {
361 background: var(--bg-primary);
362 border-radius: var(--radius-xl);
363 padding: var(--space-6);
364 max-width: 400px;
365 width: 90%;
366 }
367
368 .modal h2 {
369 margin: 0 0 var(--space-4) 0;
370 }
371
372 .modal p {
373 color: var(--text-secondary);
374 margin: 0 0 var(--space-4) 0;
375 }
376
377 .resume-details {
378 background: var(--bg-secondary);
379 border-radius: var(--radius-lg);
380 padding: var(--space-4);
381 margin-bottom: var(--space-4);
382 }
383
384 .detail-row {
385 display: flex;
386 justify-content: space-between;
387 padding: var(--space-2) 0;
388 font-size: var(--text-sm);
389 }
390
391 .detail-row:not(:last-child) {
392 border-bottom: 1px solid var(--border);
393 }
394
395 .detail-row .label {
396 color: var(--text-secondary);
397 }
398
399 .detail-row .value {
400 font-weight: var(--font-medium);
401 }
402
403 .note {
404 font-size: var(--text-sm);
405 font-style: italic;
406 }
407
408 .modal-actions {
409 display: flex;
410 gap: var(--space-3);
411 justify-content: flex-end;
412 }
413</style>