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 revoking = $state<string | null>(null) 15 $effect(() => { 16 if (!auth.loading && !auth.session) { 17 navigate('/login') 18 } 19 }) 20 $effect(() => { 21 if (auth.session) { 22 loadPasswords() 23 } 24 }) 25 async function loadPasswords() { 26 if (!auth.session) return 27 loading = true 28 error = null 29 try { 30 const result = await api.listAppPasswords(auth.session.accessJwt) 31 passwords = result.passwords 32 } catch (e) { 33 error = e instanceof ApiError ? e.message : 'Failed to load app passwords' 34 } finally { 35 loading = false 36 } 37 } 38 async function handleCreate(e: Event) { 39 e.preventDefault() 40 if (!auth.session || !newPasswordName.trim()) return 41 creating = true 42 error = null 43 try { 44 const result = await api.createAppPassword(auth.session.accessJwt, newPasswordName.trim()) 45 createdPassword = { name: result.name, password: result.password } 46 newPasswordName = '' 47 await loadPasswords() 48 } catch (e) { 49 error = e instanceof ApiError ? e.message : 'Failed to create app password' 50 } finally { 51 creating = false 52 } 53 } 54 async function handleRevoke(name: string) { 55 if (!auth.session) return 56 if (!confirm($_('appPasswords.revokeConfirm', { values: { name } }))) { 57 return 58 } 59 revoking = name 60 error = null 61 try { 62 await api.revokeAppPassword(auth.session.accessJwt, name) 63 await loadPasswords() 64 } catch (e) { 65 error = e instanceof ApiError ? e.message : 'Failed to revoke app password' 66 } finally { 67 revoking = null 68 } 69 } 70 function dismissCreated() { 71 createdPassword = null 72 } 73</script> 74<div class="page"> 75 <header> 76 <a href="#/dashboard" class="back">{$_('common.backToDashboard')}</a> 77 <h1>{$_('appPasswords.title')}</h1> 78 </header> 79 <p class="description"> 80 {$_('appPasswords.description')} 81 </p> 82 {#if error} 83 <div class="error">{error}</div> 84 {/if} 85 {#if createdPassword} 86 <div class="created-password"> 87 <h3>{$_('appPasswords.created')}</h3> 88 <p>{$_('appPasswords.createdMessage')}</p> 89 <div class="password-display"> 90 <code>{createdPassword.password}</code> 91 </div> 92 <p class="password-name">{$_('common.name')}: {createdPassword.name}</p> 93 <button onclick={dismissCreated}>{$_('common.done')}</button> 94 </div> 95 {/if} 96 <section class="create-section"> 97 <h2>{$_('appPasswords.createNew')}</h2> 98 <form onsubmit={handleCreate}> 99 <input 100 type="text" 101 bind:value={newPasswordName} 102 placeholder={$_('appPasswords.appNamePlaceholder')} 103 disabled={creating} 104 required 105 /> 106 <button type="submit" disabled={creating || !newPasswordName.trim()}> 107 {creating ? $_('appPasswords.creating') : $_('common.create')} 108 </button> 109 </form> 110 </section> 111 <section class="list-section"> 112 <h2>{$_('appPasswords.yourPasswords')}</h2> 113 {#if loading} 114 <p class="empty">{$_('common.loading')}</p> 115 {:else if passwords.length === 0} 116 <p class="empty">{$_('appPasswords.noPasswords')}</p> 117 {:else} 118 <ul class="password-list"> 119 {#each passwords as pw} 120 <li> 121 <div class="password-info"> 122 <span class="name">{pw.name}</span> 123 <span class="date">{$_('common.created')} {formatDate(pw.createdAt)}</span> 124 </div> 125 <button 126 class="revoke" 127 onclick={() => handleRevoke(pw.name)} 128 disabled={revoking === pw.name} 129 > 130 {revoking === pw.name ? $_('appPasswords.revoking') : $_('appPasswords.revoke')} 131 </button> 132 </li> 133 {/each} 134 </ul> 135 {/if} 136 </section> 137</div> 138<style> 139 .page { 140 max-width: var(--width-md); 141 margin: 0 auto; 142 padding: var(--space-7); 143 } 144 145 header { 146 margin-bottom: var(--space-4); 147 } 148 149 .back { 150 color: var(--text-secondary); 151 text-decoration: none; 152 font-size: var(--text-sm); 153 } 154 155 .back:hover { 156 color: var(--accent); 157 } 158 159 h1 { 160 margin: var(--space-2) 0 0 0; 161 } 162 163 .description { 164 color: var(--text-secondary); 165 margin-bottom: var(--space-7); 166 } 167 168 .error { 169 padding: var(--space-3); 170 background: var(--error-bg); 171 border: 1px solid var(--error-border); 172 border-radius: var(--radius-md); 173 color: var(--error-text); 174 margin-bottom: var(--space-4); 175 } 176 177 .created-password { 178 padding: var(--space-6); 179 background: var(--success-bg); 180 border: 1px solid var(--success-border); 181 border-radius: var(--radius-xl); 182 margin-bottom: var(--space-7); 183 } 184 185 .created-password h3 { 186 margin: 0 0 var(--space-2) 0; 187 color: var(--success-text); 188 } 189 190 .password-display { 191 background: var(--bg-card); 192 padding: var(--space-4); 193 border-radius: var(--radius-md); 194 margin: var(--space-4) 0; 195 } 196 197 .password-display code { 198 font-size: var(--text-xl); 199 font-family: ui-monospace, monospace; 200 word-break: break-all; 201 } 202 203 .password-name { 204 color: var(--text-secondary); 205 font-size: var(--text-sm); 206 margin-bottom: var(--space-4); 207 } 208 209 section { 210 margin-bottom: var(--space-7); 211 } 212 213 section h2 { 214 font-size: var(--text-lg); 215 margin: 0 0 var(--space-4) 0; 216 } 217 218 .create-section form { 219 display: flex; 220 gap: var(--space-2); 221 } 222 223 .create-section input { 224 flex: 1; 225 } 226 227 .password-list { 228 list-style: none; 229 padding: 0; 230 margin: 0; 231 } 232 233 .password-list li { 234 display: flex; 235 justify-content: space-between; 236 align-items: center; 237 padding: var(--space-4); 238 border: 1px solid var(--border-color); 239 border-radius: var(--radius-md); 240 margin-bottom: var(--space-2); 241 background: var(--bg-card); 242 } 243 244 .password-info { 245 display: flex; 246 flex-direction: column; 247 gap: var(--space-1); 248 } 249 250 .name { 251 font-weight: var(--font-medium); 252 } 253 254 .date { 255 font-size: var(--text-sm); 256 color: var(--text-secondary); 257 } 258 259 .revoke { 260 padding: var(--space-2) var(--space-4); 261 background: transparent; 262 border: 1px solid var(--error-text); 263 border-radius: var(--radius-md); 264 color: var(--error-text); 265 cursor: pointer; 266 } 267 268 .revoke:hover:not(:disabled) { 269 background: var(--error-bg); 270 } 271 272 .revoke:disabled { 273 opacity: 0.6; 274 cursor: not-allowed; 275 } 276 277 .empty { 278 color: var(--text-secondary); 279 text-align: center; 280 padding: var(--space-7); 281 } 282</style>