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>