Our Personal Data Server from scratch! tranquil.farm
oauth atproto pds rust postgresql objectstorage fun
at main 403 lines 12 kB view raw
1<script lang="ts"> 2 import type { AuthMethod, HandlePreservation, ServerDescription } from '../../lib/migration/types' 3 import { _ } from '../../lib/i18n' 4 import HandleInput from '../HandleInput.svelte' 5 6 interface Props { 7 handleInput: string 8 selectedDomain: string 9 handleAvailable: boolean | null 10 checkingHandle: boolean 11 email: string 12 password: string 13 authMethod: AuthMethod 14 inviteCode: string 15 serverInfo: ServerDescription | null 16 migratingFromLabel: string 17 migratingFromValue: string 18 loading?: boolean 19 sourceHandle: string 20 sourceDid: string 21 handlePreservation: HandlePreservation 22 existingHandleVerified: boolean 23 verifyingExistingHandle?: boolean 24 existingHandleError?: string | null 25 onHandleChange: (handle: string) => void 26 onDomainChange: (domain: string) => void 27 onCheckHandle: () => void 28 onEmailChange: (email: string) => void 29 onPasswordChange: (password: string) => void 30 onAuthMethodChange: (method: AuthMethod) => void 31 onInviteCodeChange: (code: string) => void 32 onHandlePreservationChange?: (preservation: HandlePreservation) => void 33 onVerifyExistingHandle?: () => void 34 onBack: () => void 35 onContinue: () => void 36 } 37 38 let { 39 handleInput, 40 selectedDomain, 41 handleAvailable, 42 checkingHandle, 43 email, 44 password, 45 authMethod, 46 inviteCode, 47 serverInfo, 48 migratingFromLabel, 49 migratingFromValue, 50 loading = false, 51 sourceHandle, 52 sourceDid, 53 handlePreservation, 54 existingHandleVerified, 55 verifyingExistingHandle = false, 56 existingHandleError = null, 57 onHandleChange, 58 onDomainChange, 59 onCheckHandle, 60 onEmailChange, 61 onPasswordChange, 62 onAuthMethodChange, 63 onInviteCodeChange, 64 onHandlePreservationChange, 65 onVerifyExistingHandle, 66 onBack, 67 onContinue, 68 }: Props = $props() 69 70 const handleTooShort = $derived(handleInput.trim().length > 0 && handleInput.trim().length < 3) 71 72 const isExternalHandle = $derived( 73 serverInfo != null && 74 sourceHandle.includes('.') && 75 !serverInfo.availableUserDomains.some(d => sourceHandle.endsWith(`.${d}`)) 76 ) 77 78 const canContinue = $derived( 79 email && 80 (authMethod === 'passkey' || password) && 81 ( 82 (handlePreservation === 'existing' && existingHandleVerified) || 83 (handlePreservation === 'new' && handleInput.trim().length >= 3 && handleAvailable !== false) 84 ) 85 ) 86</script> 87 88<div class="step-content"> 89 <h2>{$_('migration.inbound.chooseHandle.title')}</h2> 90 <p>{$_('migration.inbound.chooseHandle.desc')}</p> 91 92 <div class="current-info"> 93 <span class="label">{migratingFromLabel}:</span> 94 <span class="value">{migratingFromValue}</span> 95 </div> 96 97 {#if isExternalHandle} 98 <div class="field"> 99 <span class="field-label">{$_('migration.inbound.chooseHandle.handleChoice')}</span> 100 <div class="handle-choice-options"> 101 <label class="handle-choice-option" class:selected={handlePreservation === 'existing'}> 102 <input 103 type="radio" 104 name="handle-preservation" 105 value="existing" 106 checked={handlePreservation === 'existing'} 107 onchange={() => onHandlePreservationChange?.('existing')} 108 /> 109 <div class="handle-choice-content"> 110 <strong>{$_('migration.inbound.chooseHandle.keepExisting')}</strong> 111 <span class="handle-preview">@{sourceHandle}</span> 112 </div> 113 </label> 114 <label class="handle-choice-option" class:selected={handlePreservation === 'new'}> 115 <input 116 type="radio" 117 name="handle-preservation" 118 value="new" 119 checked={handlePreservation === 'new'} 120 onchange={() => onHandlePreservationChange?.('new')} 121 /> 122 <div class="handle-choice-content"> 123 <strong>{$_('migration.inbound.chooseHandle.createNew')}</strong> 124 </div> 125 </label> 126 </div> 127 </div> 128 {/if} 129 130 {#if handlePreservation === 'existing' && isExternalHandle} 131 <div class="field"> 132 <span class="field-label">{$_('migration.inbound.chooseHandle.existingHandle')}</span> 133 <div class="existing-handle-display"> 134 <span class="handle-value">@{sourceHandle}</span> 135 {#if existingHandleVerified} 136 <span class="verified-badge">{$_('migration.inbound.chooseHandle.verified')}</span> 137 {/if} 138 </div> 139 140 {#if !existingHandleVerified} 141 <div class="verification-instructions"> 142 <p class="instruction-header">{$_('migration.inbound.chooseHandle.verifyInstructions')}</p> 143 <div class="verification-record"> 144 <code>_atproto.{sourceHandle} TXT "did={sourceDid}"</code> 145 </div> 146 <p class="instruction-or">{$_('migration.inbound.chooseHandle.or')}</p> 147 <div class="verification-record"> 148 <code>https://{sourceHandle}/.well-known/atproto-did</code> 149 <span class="record-content">{$_('migration.inbound.chooseHandle.returning')} <code>{sourceDid}</code></span> 150 </div> 151 </div> 152 153 <button 154 class="verify-btn" 155 onclick={() => onVerifyExistingHandle?.()} 156 disabled={verifyingExistingHandle} 157 > 158 {#if verifyingExistingHandle} 159 {$_('migration.inbound.chooseHandle.verifying')} 160 {:else if existingHandleError} 161 {$_('migration.inbound.chooseHandle.checkAgain')} 162 {:else} 163 {$_('migration.inbound.chooseHandle.verifyOwnership')} 164 {/if} 165 </button> 166 167 {#if existingHandleError} 168 <p class="hint error">{existingHandleError}</p> 169 {/if} 170 {/if} 171 </div> 172 {:else} 173 <div class="field"> 174 <label for="new-handle">{$_('migration.inbound.chooseHandle.newHandle')}</label> 175 <HandleInput 176 id="new-handle" 177 value={handleInput} 178 domains={serverInfo?.availableUserDomains ?? []} 179 {selectedDomain} 180 placeholder="username" 181 onInput={onHandleChange} 182 onDomainChange={onDomainChange} 183 /> 184 185 {#if handleTooShort} 186 <p class="hint error">{$_('migration.inbound.chooseHandle.handleTooShort')}</p> 187 {:else if checkingHandle} 188 <p class="hint">{$_('migration.inbound.chooseHandle.checkingAvailability')}</p> 189 {:else if handleAvailable === true} 190 <p class="hint" style="color: var(--success-text)">{$_('migration.inbound.chooseHandle.handleAvailable')}</p> 191 {:else if handleAvailable === false} 192 <p class="hint error">{$_('migration.inbound.chooseHandle.handleTaken')}</p> 193 {:else} 194 <p class="hint">{$_('migration.inbound.chooseHandle.handleHint')}</p> 195 {/if} 196 </div> 197 {/if} 198 199 <div class="field"> 200 <label for="email">{$_('migration.inbound.chooseHandle.email')}</label> 201 <input 202 id="email" 203 type="email" 204 placeholder="you@example.com" 205 value={email} 206 oninput={(e) => onEmailChange((e.target as HTMLInputElement).value)} 207 required 208 /> 209 </div> 210 211 <div class="field"> 212 <span class="field-label">{$_('migration.inbound.chooseHandle.authMethod')}</span> 213 <div class="auth-method-options"> 214 <label class="auth-option" class:selected={authMethod === 'password'}> 215 <input 216 type="radio" 217 name="auth-method" 218 value="password" 219 checked={authMethod === 'password'} 220 onchange={() => onAuthMethodChange('password')} 221 /> 222 <div class="auth-option-content"> 223 <strong>{$_('migration.inbound.chooseHandle.authPassword')}</strong> 224 <span>{$_('migration.inbound.chooseHandle.authPasswordDesc')}</span> 225 </div> 226 </label> 227 <label class="auth-option" class:selected={authMethod === 'passkey'}> 228 <input 229 type="radio" 230 name="auth-method" 231 value="passkey" 232 checked={authMethod === 'passkey'} 233 onchange={() => onAuthMethodChange('passkey')} 234 /> 235 <div class="auth-option-content"> 236 <strong>{$_('migration.inbound.chooseHandle.authPasskey')}</strong> 237 <span>{$_('migration.inbound.chooseHandle.authPasskeyDesc')}</span> 238 </div> 239 </label> 240 </div> 241 </div> 242 243 {#if authMethod === 'password'} 244 <div class="field"> 245 <label for="new-password">{$_('migration.inbound.chooseHandle.password')}</label> 246 <input 247 id="new-password" 248 type="password" 249 placeholder="Password for your new account" 250 value={password} 251 oninput={(e) => onPasswordChange((e.target as HTMLInputElement).value)} 252 required 253 minlength={8} 254 /> 255 <p class="hint">{$_('migration.inbound.chooseHandle.passwordHint')}</p> 256 </div> 257 {:else} 258 <div class="info-box"> 259 <p>{$_('migration.inbound.chooseHandle.passkeyInfo')}</p> 260 </div> 261 {/if} 262 263 {#if serverInfo?.inviteCodeRequired} 264 <div class="field"> 265 <label for="invite">{$_('migration.inbound.chooseHandle.inviteCode')}</label> 266 <input 267 id="invite" 268 type="text" 269 placeholder="Enter invite code" 270 value={inviteCode} 271 oninput={(e) => onInviteCodeChange((e.target as HTMLInputElement).value)} 272 required 273 /> 274 </div> 275 {/if} 276 277 <div class="button-row"> 278 <button class="ghost" onclick={onBack} disabled={loading}>{$_('migration.inbound.common.back')}</button> 279 <button disabled={!canContinue || loading} onclick={onContinue}> 280 {$_('migration.inbound.common.continue')} 281 </button> 282 </div> 283</div> 284 285<style> 286 .handle-choice-options { 287 display: flex; 288 flex-direction: column; 289 gap: var(--space-3); 290 } 291 292 .handle-choice-option { 293 display: flex; 294 align-items: center; 295 gap: var(--space-3); 296 padding: var(--space-4); 297 border: 1px solid var(--border-color); 298 border-radius: var(--radius-lg); 299 cursor: pointer; 300 transition: border-color var(--transition-normal), background var(--transition-normal); 301 } 302 303 .handle-choice-option:hover { 304 border-color: var(--accent); 305 } 306 307 .handle-choice-option.selected { 308 border-color: var(--accent); 309 background: var(--accent-muted); 310 } 311 312 .handle-choice-option input[type="radio"] { 313 flex-shrink: 0; 314 width: 18px; 315 height: 18px; 316 margin: 0; 317 } 318 319 .handle-choice-content { 320 display: flex; 321 flex-direction: column; 322 gap: var(--space-1); 323 } 324 325 .handle-preview { 326 font-family: var(--font-mono); 327 font-size: var(--text-sm); 328 color: var(--text-secondary); 329 } 330 331 .existing-handle-display { 332 display: flex; 333 align-items: center; 334 gap: var(--space-4); 335 padding: var(--space-4); 336 background: var(--bg-secondary); 337 border-radius: var(--radius-lg); 338 margin-bottom: var(--space-4); 339 } 340 341 .handle-value { 342 font-family: var(--font-mono); 343 font-size: var(--text-base); 344 } 345 346 .verified-badge { 347 font-size: var(--text-xs); 348 padding: var(--space-1) var(--space-3); 349 background: var(--success-bg); 350 color: var(--success-text); 351 border-radius: var(--radius-md); 352 } 353 354 .verification-instructions { 355 background: var(--bg-secondary); 356 padding: var(--space-5); 357 border-radius: var(--radius-lg); 358 margin-bottom: var(--space-4); 359 } 360 361 .instruction-header { 362 margin: 0 0 var(--space-4) 0; 363 font-size: var(--text-sm); 364 color: var(--text-secondary); 365 } 366 367 .instruction-or { 368 margin: var(--space-3) 0; 369 font-size: var(--text-xs); 370 color: var(--text-muted); 371 text-align: center; 372 } 373 374 .verification-record { 375 display: flex; 376 flex-direction: column; 377 gap: var(--space-2); 378 } 379 380 .verification-record code { 381 font-size: var(--text-sm); 382 padding: var(--space-3); 383 background: var(--bg-tertiary); 384 border-radius: var(--radius-md); 385 overflow-x: auto; 386 word-break: break-all; 387 } 388 389 .record-content { 390 font-size: var(--text-xs); 391 color: var(--text-secondary); 392 padding-left: var(--space-3); 393 } 394 395 .record-content code { 396 padding: var(--space-1) var(--space-2); 397 font-size: var(--text-xs); 398 } 399 400 .verify-btn { 401 width: 100%; 402 } 403</style>