this repo has no description
1<script lang="ts"> 2 import { onMount } from 'svelte' 3 import { getAuthState } from '../lib/auth.svelte' 4 import { navigate } from '../lib/router.svelte' 5 import { api, ApiError, type VerificationMethod, type DidDocument } from '../lib/api' 6 import { _ } from '../lib/i18n' 7 8 const auth = getAuthState() 9 10 let loading = $state(true) 11 let saving = $state(false) 12 let message = $state<{ type: 'success' | 'error'; text: string } | null>(null) 13 let didDocument = $state<DidDocument | null>(null) 14 let verificationMethods = $state<VerificationMethod[]>([]) 15 let alsoKnownAs = $state<string[]>([]) 16 let serviceEndpoint = $state('') 17 let newKeyId = $state('#atproto') 18 let newKeyPublic = $state('') 19 let newHandle = $state('') 20 21 $effect(() => { 22 if (!auth.loading && !auth.session) { 23 navigate('/login') 24 } 25 }) 26 27 onMount(async () => { 28 if (!auth.session) return 29 try { 30 didDocument = await api.getDidDocument(auth.session.accessJwt) 31 verificationMethods = didDocument.verificationMethod.map(vm => ({ 32 id: vm.id.replace(didDocument!.id, ''), 33 type: vm.type, 34 publicKeyMultibase: vm.publicKeyMultibase 35 })) 36 alsoKnownAs = [...didDocument.alsoKnownAs] 37 const pdsService = didDocument.service.find(s => s.id === '#atproto_pds') 38 serviceEndpoint = pdsService?.serviceEndpoint || '' 39 } catch (e) { 40 showMessage('error', e instanceof ApiError ? e.message : $_('didEditor.loadFailed')) 41 } finally { 42 loading = false 43 } 44 }) 45 46 function showMessage(type: 'success' | 'error', text: string) { 47 message = { type, text } 48 setTimeout(() => { 49 if (message?.text === text) message = null 50 }, 5000) 51 } 52 53 function addVerificationMethod() { 54 if (!newKeyId || !newKeyPublic) return 55 if (!newKeyPublic.startsWith('z')) { 56 showMessage('error', $_('didEditor.invalidMultibase')) 57 return 58 } 59 verificationMethods = [...verificationMethods, { 60 id: newKeyId.startsWith('#') ? newKeyId : `#${newKeyId}`, 61 type: 'Multikey', 62 publicKeyMultibase: newKeyPublic 63 }] 64 newKeyId = '#atproto' 65 newKeyPublic = '' 66 } 67 68 function removeVerificationMethod(index: number) { 69 verificationMethods = verificationMethods.filter((_, i) => i !== index) 70 } 71 72 function addHandle() { 73 if (!newHandle) return 74 if (!newHandle.startsWith('at://')) { 75 showMessage('error', $_('didEditor.invalidHandle')) 76 return 77 } 78 alsoKnownAs = [...alsoKnownAs, newHandle] 79 newHandle = '' 80 } 81 82 function removeHandle(index: number) { 83 alsoKnownAs = alsoKnownAs.filter((_, i) => i !== index) 84 } 85 86 async function handleSave() { 87 if (!auth.session) return 88 saving = true 89 message = null 90 try { 91 await api.updateDidDocument(auth.session.accessJwt, { 92 verificationMethods: verificationMethods.length > 0 ? verificationMethods : undefined, 93 alsoKnownAs: alsoKnownAs.length > 0 ? alsoKnownAs : undefined, 94 serviceEndpoint: serviceEndpoint || undefined 95 }) 96 showMessage('success', $_('didEditor.success')) 97 didDocument = await api.getDidDocument(auth.session.accessJwt) 98 } catch (e) { 99 showMessage('error', e instanceof ApiError ? e.message : $_('didEditor.saveFailed')) 100 } finally { 101 saving = false 102 } 103 } 104</script> 105 106<div class="page"> 107 <header> 108 <a href="#/dashboard" class="back">{$_('common.backToDashboard')}</a> 109 <h1>{$_('didEditor.title')}</h1> 110 </header> 111 112 {#if message} 113 <div class="message {message.type}">{message.text}</div> 114 {/if} 115 116 {#if loading} 117 <div class="loading">{$_('common.loading')}</div> 118 {:else} 119 <div class="help-section"> 120 <h3>{$_('didEditor.helpTitle')}</h3> 121 <p>{$_('didEditor.helpText')}</p> 122 </div> 123 124 <section> 125 <h2>{$_('didEditor.preview')}</h2> 126 <pre class="did-preview">{JSON.stringify(didDocument, null, 2)}</pre> 127 </section> 128 129 <section> 130 <h2>{$_('didEditor.verificationMethods')}</h2> 131 <p class="description">{$_('didEditor.verificationMethodsDesc')}</p> 132 133 {#if verificationMethods.length > 0} 134 <ul class="key-list"> 135 {#each verificationMethods as method, index} 136 <li class="key-item"> 137 <div class="key-info"> 138 <span class="key-id">{method.id}</span> 139 <span class="key-type">{method.type}</span> 140 <code class="key-value">{method.publicKeyMultibase}</code> 141 </div> 142 <button type="button" class="danger-link" onclick={() => removeVerificationMethod(index)}> 143 {$_('didEditor.removeKey')} 144 </button> 145 </li> 146 {/each} 147 </ul> 148 {:else} 149 <p class="empty-state">{$_('didEditor.noKeys')}</p> 150 {/if} 151 152 <div class="add-form"> 153 <h4>{$_('didEditor.addKey')}</h4> 154 <div class="field-row"> 155 <div class="field small"> 156 <label for="key-id">{$_('didEditor.keyId')}</label> 157 <input 158 id="key-id" 159 type="text" 160 bind:value={newKeyId} 161 placeholder={$_('didEditor.keyIdPlaceholder')} 162 /> 163 </div> 164 <div class="field large"> 165 <label for="key-public">{$_('didEditor.publicKey')}</label> 166 <input 167 id="key-public" 168 type="text" 169 bind:value={newKeyPublic} 170 placeholder={$_('didEditor.publicKeyPlaceholder')} 171 /> 172 </div> 173 <button type="button" class="add-btn" onclick={addVerificationMethod} disabled={!newKeyId || !newKeyPublic}> 174 {$_('didEditor.addKey')} 175 </button> 176 </div> 177 </div> 178 </section> 179 180 <section> 181 <h2>{$_('didEditor.alsoKnownAs')}</h2> 182 <p class="description">{$_('didEditor.alsoKnownAsDesc')}</p> 183 184 {#if alsoKnownAs.length > 0} 185 <ul class="handle-list"> 186 {#each alsoKnownAs as handle, index} 187 <li class="handle-item"> 188 <span>{handle}</span> 189 <button type="button" class="danger-link" onclick={() => removeHandle(index)}> 190 {$_('didEditor.removeHandle')} 191 </button> 192 </li> 193 {/each} 194 </ul> 195 {:else} 196 <p class="empty-state">{$_('didEditor.noHandles')}</p> 197 {/if} 198 199 <div class="add-form"> 200 <div class="field-row"> 201 <div class="field large"> 202 <label for="new-handle">{$_('didEditor.handle')}</label> 203 <input 204 id="new-handle" 205 type="text" 206 bind:value={newHandle} 207 placeholder={$_('didEditor.handlePlaceholder')} 208 /> 209 </div> 210 <button type="button" class="add-btn" onclick={addHandle} disabled={!newHandle}> 211 {$_('didEditor.addHandle')} 212 </button> 213 </div> 214 </div> 215 </section> 216 217 <section> 218 <h2>{$_('didEditor.serviceEndpoint')}</h2> 219 <p class="description">{$_('didEditor.serviceEndpointDesc')}</p> 220 <div class="field"> 221 <label for="service-endpoint">{$_('didEditor.currentPds')}</label> 222 <input 223 id="service-endpoint" 224 type="url" 225 bind:value={serviceEndpoint} 226 placeholder="https://pds.example.com" 227 /> 228 </div> 229 </section> 230 231 <div class="actions"> 232 <button onclick={handleSave} disabled={saving}> 233 {saving ? $_('common.saving') : $_('common.save')} 234 </button> 235 </div> 236 {/if} 237</div> 238 239<style> 240 .page { 241 max-width: var(--width-lg); 242 margin: 0 auto; 243 padding: var(--space-7); 244 } 245 246 header { 247 margin-bottom: var(--space-7); 248 } 249 250 .back { 251 color: var(--text-secondary); 252 text-decoration: none; 253 font-size: var(--text-sm); 254 } 255 256 .back:hover { 257 color: var(--accent); 258 } 259 260 h1 { 261 margin: var(--space-2) 0 0 0; 262 } 263 264 .help-section { 265 background: var(--info-bg, #e0f2fe); 266 border: 1px solid var(--info-border, #7dd3fc); 267 border-radius: var(--radius-xl); 268 padding: var(--space-5) var(--space-6); 269 margin-bottom: var(--space-6); 270 } 271 272 .help-section h3 { 273 margin: 0 0 var(--space-2) 0; 274 color: var(--info-text, #0369a1); 275 font-size: var(--text-base); 276 } 277 278 .help-section p { 279 margin: 0; 280 color: var(--info-text, #0369a1); 281 font-size: var(--text-sm); 282 } 283 284 section { 285 padding: var(--space-6); 286 background: var(--bg-secondary); 287 border-radius: var(--radius-xl); 288 margin-bottom: var(--space-6); 289 } 290 291 section h2 { 292 margin: 0 0 var(--space-2) 0; 293 font-size: var(--text-lg); 294 } 295 296 .description { 297 color: var(--text-secondary); 298 font-size: var(--text-sm); 299 margin-bottom: var(--space-4); 300 } 301 302 .did-preview { 303 background: var(--bg-input); 304 padding: var(--space-4); 305 border-radius: var(--radius-md); 306 font-size: var(--text-xs); 307 overflow-x: auto; 308 white-space: pre-wrap; 309 word-break: break-all; 310 max-height: 300px; 311 overflow-y: auto; 312 } 313 314 .key-list, .handle-list { 315 list-style: none; 316 padding: 0; 317 margin: 0 0 var(--space-4) 0; 318 } 319 320 .key-item, .handle-item { 321 display: flex; 322 justify-content: space-between; 323 align-items: flex-start; 324 padding: var(--space-3) var(--space-4); 325 background: var(--bg-card); 326 border: 1px solid var(--border-color); 327 border-radius: var(--radius-md); 328 margin-bottom: var(--space-2); 329 gap: var(--space-4); 330 } 331 332 .key-info { 333 display: flex; 334 flex-direction: column; 335 gap: var(--space-1); 336 flex: 1; 337 min-width: 0; 338 } 339 340 .key-id { 341 font-weight: var(--font-medium); 342 font-size: var(--text-sm); 343 } 344 345 .key-type { 346 color: var(--text-secondary); 347 font-size: var(--text-xs); 348 } 349 350 .key-value { 351 font-size: var(--text-xs); 352 background: var(--bg-input); 353 padding: var(--space-1) var(--space-2); 354 border-radius: var(--radius-sm); 355 word-break: break-all; 356 } 357 358 .handle-item span { 359 font-family: ui-monospace, monospace; 360 font-size: var(--text-sm); 361 } 362 363 .danger-link { 364 background: none; 365 border: none; 366 color: var(--error-text); 367 cursor: pointer; 368 font-size: var(--text-xs); 369 padding: var(--space-1) var(--space-2); 370 white-space: nowrap; 371 } 372 373 .danger-link:hover { 374 text-decoration: underline; 375 } 376 377 .empty-state { 378 color: var(--text-muted); 379 font-size: var(--text-sm); 380 font-style: italic; 381 padding: var(--space-4); 382 text-align: center; 383 background: var(--bg-card); 384 border-radius: var(--radius-md); 385 margin-bottom: var(--space-4); 386 } 387 388 .add-form { 389 background: var(--bg-card); 390 border: 1px solid var(--border-color); 391 border-radius: var(--radius-lg); 392 padding: var(--space-4); 393 } 394 395 .add-form h4 { 396 margin: 0 0 var(--space-3) 0; 397 font-size: var(--text-sm); 398 color: var(--text-secondary); 399 } 400 401 .field-row { 402 display: flex; 403 gap: var(--space-3); 404 align-items: flex-end; 405 } 406 407 .field { 408 display: flex; 409 flex-direction: column; 410 gap: var(--space-1); 411 } 412 413 .field.small { 414 flex: 0 0 120px; 415 } 416 417 .field.large { 418 flex: 1; 419 } 420 421 .field label { 422 font-size: var(--text-xs); 423 color: var(--text-secondary); 424 } 425 426 .add-btn { 427 white-space: nowrap; 428 } 429 430 .actions { 431 display: flex; 432 gap: var(--space-3); 433 justify-content: flex-end; 434 margin-top: var(--space-6); 435 } 436 437 .loading { 438 text-align: center; 439 padding: var(--space-9); 440 color: var(--text-secondary); 441 } 442 443 @media (max-width: 600px) { 444 .field-row { 445 flex-direction: column; 446 align-items: stretch; 447 } 448 449 .field.small, .field.large { 450 flex: none; 451 } 452 453 .add-btn { 454 width: 100%; 455 } 456 } 457</style>