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