this repo has no description
1<script lang="ts"> 2 import { getAuthState } from '../lib/auth.svelte' 3 import { navigate } from '../lib/router.svelte' 4 import { api, type AppPassword, ApiError } from '../lib/api' 5 import { _ } from '../lib/i18n' 6 import { formatDate } from '../lib/date' 7 const auth = getAuthState() 8 let passwords = $state<AppPassword[]>([]) 9 let loading = $state(true) 10 let error = $state<string | null>(null) 11 let newPasswordName = $state('') 12 let creating = $state(false) 13 let createdPassword = $state<{ name: string; password: string } | null>(null) 14 let passwordCopied = $state(false) 15 let passwordAcknowledged = $state(false) 16 let revoking = $state<string | null>(null) 17 $effect(() => { 18 if (!auth.loading && !auth.session) { 19 navigate('/login') 20 } 21 }) 22 $effect(() => { 23 if (auth.session) { 24 loadPasswords() 25 } 26 }) 27 async function loadPasswords() { 28 if (!auth.session) return 29 loading = true 30 error = null 31 try { 32 const result = await api.listAppPasswords(auth.session.accessJwt) 33 passwords = result.passwords 34 } catch (e) { 35 error = e instanceof ApiError ? e.message : 'Failed to load app passwords' 36 } finally { 37 loading = false 38 } 39 } 40 async function handleCreate(e: Event) { 41 e.preventDefault() 42 if (!auth.session || !newPasswordName.trim()) return 43 creating = true 44 error = null 45 try { 46 const result = await api.createAppPassword(auth.session.accessJwt, newPasswordName.trim()) 47 createdPassword = { name: result.name, password: result.password } 48 newPasswordName = '' 49 await loadPasswords() 50 } catch (e) { 51 error = e instanceof ApiError ? e.message : 'Failed to create app password' 52 } finally { 53 creating = false 54 } 55 } 56 async function handleRevoke(name: string) { 57 if (!auth.session) return 58 if (!confirm($_('appPasswords.revokeConfirm', { values: { name } }))) { 59 return 60 } 61 revoking = name 62 error = null 63 try { 64 await api.revokeAppPassword(auth.session.accessJwt, name) 65 await loadPasswords() 66 } catch (e) { 67 error = e instanceof ApiError ? e.message : 'Failed to revoke app password' 68 } finally { 69 revoking = null 70 } 71 } 72 function copyPassword() { 73 if (createdPassword) { 74 navigator.clipboard.writeText(createdPassword.password) 75 passwordCopied = true 76 } 77 } 78 function dismissCreated() { 79 createdPassword = null 80 passwordCopied = false 81 passwordAcknowledged = false 82 } 83</script> 84<div class="page"> 85 <header> 86 <a href="#/dashboard" class="back">{$_('common.backToDashboard')}</a> 87 <h1>{$_('appPasswords.title')}</h1> 88 </header> 89 <p class="description"> 90 {$_('appPasswords.description')} 91 </p> 92 {#if error} 93 <div class="error">{error}</div> 94 {/if} 95 {#if createdPassword} 96 <div class="created-password"> 97 <div class="warning-box"> 98 <strong>{$_('appPasswords.saveWarningTitle')}</strong> 99 <p>{$_('appPasswords.saveWarningMessage')}</p> 100 </div> 101 <div class="password-display"> 102 <div class="password-label">{$_('common.name')}: <strong>{createdPassword.name}</strong></div> 103 <code class="password-code">{createdPassword.password}</code> 104 <button type="button" class="copy-btn" onclick={copyPassword}> 105 {passwordCopied ? $_('common.copied') : $_('common.copyToClipboard')} 106 </button> 107 </div> 108 <label class="checkbox-label"> 109 <input type="checkbox" bind:checked={passwordAcknowledged} /> 110 <span>{$_('appPasswords.acknowledgeLabel')}</span> 111 </label> 112 <button onclick={dismissCreated} disabled={!passwordAcknowledged}>{$_('common.done')}</button> 113 </div> 114 {/if} 115 <section class="create-section"> 116 <h2>{$_('appPasswords.createNew')}</h2> 117 <form onsubmit={handleCreate}> 118 <input 119 type="text" 120 bind:value={newPasswordName} 121 placeholder={$_('appPasswords.appNamePlaceholder')} 122 disabled={creating} 123 required 124 /> 125 <button type="submit" disabled={creating || !newPasswordName.trim()}> 126 {creating ? $_('appPasswords.creating') : $_('common.create')} 127 </button> 128 </form> 129 </section> 130 <section class="list-section"> 131 <h2>{$_('appPasswords.yourPasswords')}</h2> 132 {#if loading} 133 <p class="empty">{$_('common.loading')}</p> 134 {:else if passwords.length === 0} 135 <p class="empty">{$_('appPasswords.noPasswords')}</p> 136 {:else} 137 <ul class="password-list"> 138 {#each passwords as pw} 139 <li> 140 <div class="password-info"> 141 <span class="name">{pw.name}</span> 142 <span class="date">{$_('common.created')} {formatDate(pw.createdAt)}</span> 143 </div> 144 <button 145 class="revoke" 146 onclick={() => handleRevoke(pw.name)} 147 disabled={revoking === pw.name} 148 > 149 {revoking === pw.name ? $_('appPasswords.revoking') : $_('appPasswords.revoke')} 150 </button> 151 </li> 152 {/each} 153 </ul> 154 {/if} 155 </section> 156</div> 157<style> 158 .page { 159 max-width: var(--width-lg); 160 margin: 0 auto; 161 padding: var(--space-7); 162 } 163 164 header { 165 margin-bottom: var(--space-4); 166 } 167 168 .back { 169 color: var(--text-secondary); 170 text-decoration: none; 171 font-size: var(--text-sm); 172 } 173 174 .back:hover { 175 color: var(--accent); 176 } 177 178 h1 { 179 margin: var(--space-2) 0 0 0; 180 } 181 182 .description { 183 color: var(--text-secondary); 184 margin-bottom: var(--space-7); 185 } 186 187 .error { 188 padding: var(--space-3); 189 background: var(--error-bg); 190 border: 1px solid var(--error-border); 191 border-radius: var(--radius-md); 192 color: var(--error-text); 193 margin-bottom: var(--space-4); 194 } 195 196 .created-password { 197 display: flex; 198 flex-direction: column; 199 gap: var(--space-4); 200 padding: var(--space-6); 201 background: var(--bg-secondary); 202 border: 1px solid var(--border-color); 203 border-radius: var(--radius-xl); 204 margin-bottom: var(--space-7); 205 } 206 207 .warning-box { 208 padding: var(--space-5); 209 background: var(--warning-bg); 210 border: 1px solid var(--warning-border); 211 border-radius: var(--radius-lg); 212 font-size: var(--text-sm); 213 } 214 215 .warning-box strong { 216 display: block; 217 margin-bottom: var(--space-2); 218 color: var(--warning-text); 219 } 220 221 .warning-box p { 222 margin: 0; 223 color: var(--warning-text); 224 } 225 226 .password-display { 227 background: var(--bg-card); 228 border: 2px solid var(--accent); 229 border-radius: var(--radius-xl); 230 padding: var(--space-6); 231 text-align: center; 232 } 233 234 .password-label { 235 font-size: var(--text-sm); 236 color: var(--text-secondary); 237 margin-bottom: var(--space-4); 238 } 239 240 .password-code { 241 display: block; 242 font-size: var(--text-xl); 243 font-family: ui-monospace, monospace; 244 letter-spacing: 0.1em; 245 padding: var(--space-5); 246 background: var(--bg-input); 247 border-radius: var(--radius-md); 248 margin-bottom: var(--space-4); 249 user-select: all; 250 word-break: break-all; 251 } 252 253 .copy-btn { 254 padding: var(--space-3) var(--space-5); 255 font-size: var(--text-sm); 256 } 257 258 .checkbox-label { 259 display: flex; 260 align-items: center; 261 gap: var(--space-3); 262 cursor: pointer; 263 font-weight: var(--font-normal); 264 } 265 266 .checkbox-label input[type="checkbox"] { 267 width: auto; 268 padding: 0; 269 } 270 271 section { 272 margin-bottom: var(--space-7); 273 } 274 275 section h2 { 276 font-size: var(--text-lg); 277 margin: 0 0 var(--space-4) 0; 278 } 279 280 .create-section form { 281 display: flex; 282 gap: var(--space-2); 283 } 284 285 .create-section input { 286 flex: 1; 287 } 288 289 .password-list { 290 list-style: none; 291 padding: 0; 292 margin: 0; 293 } 294 295 .password-list li { 296 display: flex; 297 justify-content: space-between; 298 align-items: center; 299 padding: var(--space-4); 300 border: 1px solid var(--border-color); 301 border-radius: var(--radius-md); 302 margin-bottom: var(--space-2); 303 background: var(--bg-card); 304 } 305 306 .password-info { 307 display: flex; 308 flex-direction: column; 309 gap: var(--space-1); 310 } 311 312 .name { 313 font-weight: var(--font-medium); 314 } 315 316 .date { 317 font-size: var(--text-sm); 318 color: var(--text-secondary); 319 } 320 321 .revoke { 322 padding: var(--space-2) var(--space-4); 323 background: transparent; 324 border: 1px solid var(--error-text); 325 border-radius: var(--radius-md); 326 color: var(--error-text); 327 cursor: pointer; 328 } 329 330 .revoke:hover:not(:disabled) { 331 background: var(--error-bg); 332 } 333 334 .revoke:disabled { 335 opacity: 0.6; 336 cursor: not-allowed; 337 } 338 339 .empty { 340 color: var(--text-secondary); 341 text-align: center; 342 padding: var(--space-7); 343 } 344</style>