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