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