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>