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 passwordCopied = $state(false)
15 let passwordAcknowledged = $state(false)
16 let revoking = $state<string | null>(null)
17 $effect(() => {
18 if (!auth.loading && !auth.session) {
19 navigate('/login')
20 }
21 })
22 $effect(() => {
23 if (auth.session) {
24 loadPasswords()
25 }
26 })
27 async function loadPasswords() {
28 if (!auth.session) return
29 loading = true
30 error = null
31 try {
32 const result = await api.listAppPasswords(auth.session.accessJwt)
33 passwords = result.passwords
34 } catch (e) {
35 error = e instanceof ApiError ? e.message : 'Failed to load app passwords'
36 } finally {
37 loading = false
38 }
39 }
40 async function handleCreate(e: Event) {
41 e.preventDefault()
42 if (!auth.session || !newPasswordName.trim()) return
43 creating = true
44 error = null
45 try {
46 const result = await api.createAppPassword(auth.session.accessJwt, newPasswordName.trim())
47 createdPassword = { name: result.name, password: result.password }
48 newPasswordName = ''
49 await loadPasswords()
50 } catch (e) {
51 error = e instanceof ApiError ? e.message : 'Failed to create app password'
52 } finally {
53 creating = false
54 }
55 }
56 async function handleRevoke(name: string) {
57 if (!auth.session) return
58 if (!confirm($_('appPasswords.revokeConfirm', { values: { name } }))) {
59 return
60 }
61 revoking = name
62 error = null
63 try {
64 await api.revokeAppPassword(auth.session.accessJwt, name)
65 await loadPasswords()
66 } catch (e) {
67 error = e instanceof ApiError ? e.message : 'Failed to revoke app password'
68 } finally {
69 revoking = null
70 }
71 }
72 function copyPassword() {
73 if (createdPassword) {
74 navigator.clipboard.writeText(createdPassword.password)
75 passwordCopied = true
76 }
77 }
78 function dismissCreated() {
79 createdPassword = null
80 passwordCopied = false
81 passwordAcknowledged = false
82 }
83</script>
84<div class="page">
85 <header>
86 <a href="#/dashboard" class="back">{$_('common.backToDashboard')}</a>
87 <h1>{$_('appPasswords.title')}</h1>
88 </header>
89 <p class="description">
90 {$_('appPasswords.description')}
91 </p>
92 {#if error}
93 <div class="error">{error}</div>
94 {/if}
95 {#if createdPassword}
96 <div class="created-password">
97 <div class="warning-box">
98 <strong>{$_('appPasswords.saveWarningTitle')}</strong>
99 <p>{$_('appPasswords.saveWarningMessage')}</p>
100 </div>
101 <div class="password-display">
102 <div class="password-label">{$_('common.name')}: <strong>{createdPassword.name}</strong></div>
103 <code class="password-code">{createdPassword.password}</code>
104 <button type="button" class="copy-btn" onclick={copyPassword}>
105 {passwordCopied ? $_('common.copied') : $_('common.copyToClipboard')}
106 </button>
107 </div>
108 <label class="checkbox-label">
109 <input type="checkbox" bind:checked={passwordAcknowledged} />
110 <span>{$_('appPasswords.acknowledgeLabel')}</span>
111 </label>
112 <button onclick={dismissCreated} disabled={!passwordAcknowledged}>{$_('common.done')}</button>
113 </div>
114 {/if}
115 <section class="create-section">
116 <h2>{$_('appPasswords.createNew')}</h2>
117 <form onsubmit={handleCreate}>
118 <input
119 type="text"
120 bind:value={newPasswordName}
121 placeholder={$_('appPasswords.appNamePlaceholder')}
122 disabled={creating}
123 required
124 />
125 <button type="submit" disabled={creating || !newPasswordName.trim()}>
126 {creating ? $_('appPasswords.creating') : $_('common.create')}
127 </button>
128 </form>
129 </section>
130 <section class="list-section">
131 <h2>{$_('appPasswords.yourPasswords')}</h2>
132 {#if loading}
133 <p class="empty">{$_('common.loading')}</p>
134 {:else if passwords.length === 0}
135 <p class="empty">{$_('appPasswords.noPasswords')}</p>
136 {:else}
137 <ul class="password-list">
138 {#each passwords as pw}
139 <li>
140 <div class="password-info">
141 <span class="name">{pw.name}</span>
142 <span class="date">{$_('common.created')} {formatDate(pw.createdAt)}</span>
143 </div>
144 <button
145 class="revoke"
146 onclick={() => handleRevoke(pw.name)}
147 disabled={revoking === pw.name}
148 >
149 {revoking === pw.name ? $_('appPasswords.revoking') : $_('appPasswords.revoke')}
150 </button>
151 </li>
152 {/each}
153 </ul>
154 {/if}
155 </section>
156</div>
157<style>
158 .page {
159 max-width: var(--width-lg);
160 margin: 0 auto;
161 padding: var(--space-7);
162 }
163
164 header {
165 margin-bottom: var(--space-4);
166 }
167
168 .back {
169 color: var(--text-secondary);
170 text-decoration: none;
171 font-size: var(--text-sm);
172 }
173
174 .back:hover {
175 color: var(--accent);
176 }
177
178 h1 {
179 margin: var(--space-2) 0 0 0;
180 }
181
182 .description {
183 color: var(--text-secondary);
184 margin-bottom: var(--space-7);
185 }
186
187 .error {
188 padding: var(--space-3);
189 background: var(--error-bg);
190 border: 1px solid var(--error-border);
191 border-radius: var(--radius-md);
192 color: var(--error-text);
193 margin-bottom: var(--space-4);
194 }
195
196 .created-password {
197 display: flex;
198 flex-direction: column;
199 gap: var(--space-4);
200 padding: var(--space-6);
201 background: var(--bg-secondary);
202 border: 1px solid var(--border-color);
203 border-radius: var(--radius-xl);
204 margin-bottom: var(--space-7);
205 }
206
207 .warning-box {
208 padding: var(--space-5);
209 background: var(--warning-bg);
210 border: 1px solid var(--warning-border);
211 border-radius: var(--radius-lg);
212 font-size: var(--text-sm);
213 }
214
215 .warning-box strong {
216 display: block;
217 margin-bottom: var(--space-2);
218 color: var(--warning-text);
219 }
220
221 .warning-box p {
222 margin: 0;
223 color: var(--warning-text);
224 }
225
226 .password-display {
227 background: var(--bg-card);
228 border: 2px solid var(--accent);
229 border-radius: var(--radius-xl);
230 padding: var(--space-6);
231 text-align: center;
232 }
233
234 .password-label {
235 font-size: var(--text-sm);
236 color: var(--text-secondary);
237 margin-bottom: var(--space-4);
238 }
239
240 .password-code {
241 display: block;
242 font-size: var(--text-xl);
243 font-family: ui-monospace, monospace;
244 letter-spacing: 0.1em;
245 padding: var(--space-5);
246 background: var(--bg-input);
247 border-radius: var(--radius-md);
248 margin-bottom: var(--space-4);
249 user-select: all;
250 word-break: break-all;
251 }
252
253 .copy-btn {
254 padding: var(--space-3) var(--space-5);
255 font-size: var(--text-sm);
256 }
257
258 .checkbox-label {
259 display: flex;
260 align-items: center;
261 gap: var(--space-3);
262 cursor: pointer;
263 font-weight: var(--font-normal);
264 }
265
266 .checkbox-label input[type="checkbox"] {
267 width: auto;
268 padding: 0;
269 }
270
271 section {
272 margin-bottom: var(--space-7);
273 }
274
275 section h2 {
276 font-size: var(--text-lg);
277 margin: 0 0 var(--space-4) 0;
278 }
279
280 .create-section form {
281 display: flex;
282 gap: var(--space-2);
283 }
284
285 .create-section input {
286 flex: 1;
287 }
288
289 .password-list {
290 list-style: none;
291 padding: 0;
292 margin: 0;
293 }
294
295 .password-list li {
296 display: flex;
297 justify-content: space-between;
298 align-items: center;
299 padding: var(--space-4);
300 border: 1px solid var(--border-color);
301 border-radius: var(--radius-md);
302 margin-bottom: var(--space-2);
303 background: var(--bg-card);
304 }
305
306 .password-info {
307 display: flex;
308 flex-direction: column;
309 gap: var(--space-1);
310 }
311
312 .name {
313 font-weight: var(--font-medium);
314 }
315
316 .date {
317 font-size: var(--text-sm);
318 color: var(--text-secondary);
319 }
320
321 .revoke {
322 padding: var(--space-2) var(--space-4);
323 background: transparent;
324 border: 1px solid var(--error-text);
325 border-radius: var(--radius-md);
326 color: var(--error-text);
327 cursor: pointer;
328 }
329
330 .revoke:hover:not(:disabled) {
331 background: var(--error-bg);
332 }
333
334 .revoke:disabled {
335 opacity: 0.6;
336 cursor: not-allowed;
337 }
338
339 .empty {
340 color: var(--text-secondary);
341 text-align: center;
342 padding: var(--space-7);
343 }
344</style>