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">← 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>