@nebulr-group/bridge-svelte 0.1.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +63 -0
- package/dist/auth/route-guard.d.ts +2 -0
- package/dist/auth/route-guard.js +28 -0
- package/dist/client/BridgeBootstrap.d.ts +8 -0
- package/dist/client/BridgeBootstrap.js +105 -0
- package/dist/client/BridgeBootstrap.svelte +43 -0
- package/dist/client/BridgeBootstrap.svelte.d.ts +6 -0
- package/dist/client/components/FeatureFlag.svelte +37 -0
- package/dist/client/components/FeatureFlag.svelte.d.ts +15 -0
- package/dist/client/components/ProfileName.svelte +18 -0
- package/dist/client/components/ProfileName.svelte.d.ts +6 -0
- package/dist/client/components/developer/ApiTokenManagement.svelte +365 -0
- package/dist/client/components/developer/ApiTokenManagement.svelte.d.ts +6 -0
- package/dist/client/components/sdk-auth/ForgotPassword.svelte +170 -0
- package/dist/client/components/sdk-auth/ForgotPassword.svelte.d.ts +10 -0
- package/dist/client/components/sdk-auth/LoginForm.svelte +414 -0
- package/dist/client/components/sdk-auth/LoginForm.svelte.d.ts +25 -0
- package/dist/client/components/sdk-auth/MagicLink.svelte +92 -0
- package/dist/client/components/sdk-auth/MagicLink.svelte.d.ts +9 -0
- package/dist/client/components/sdk-auth/MfaChallenge.svelte +150 -0
- package/dist/client/components/sdk-auth/MfaChallenge.svelte.d.ts +9 -0
- package/dist/client/components/sdk-auth/MfaSetup.svelte +182 -0
- package/dist/client/components/sdk-auth/MfaSetup.svelte.d.ts +8 -0
- package/dist/client/components/sdk-auth/PasskeyLogin.svelte +99 -0
- package/dist/client/components/sdk-auth/PasskeyLogin.svelte.d.ts +10 -0
- package/dist/client/components/sdk-auth/PasskeyRequestSetupLink.svelte +83 -0
- package/dist/client/components/sdk-auth/PasskeyRequestSetupLink.svelte.d.ts +7 -0
- package/dist/client/components/sdk-auth/PasskeySetup.svelte +151 -0
- package/dist/client/components/sdk-auth/PasskeySetup.svelte.d.ts +11 -0
- package/dist/client/components/sdk-auth/SignupForm.svelte +119 -0
- package/dist/client/components/sdk-auth/SignupForm.svelte.d.ts +14 -0
- package/dist/client/components/sdk-auth/SsoButton.svelte +70 -0
- package/dist/client/components/sdk-auth/SsoButton.svelte.d.ts +13 -0
- package/dist/client/components/sdk-auth/SsoProviderIcon.svelte +56 -0
- package/dist/client/components/sdk-auth/SsoProviderIcon.svelte.d.ts +7 -0
- package/dist/client/components/sdk-auth/TenantSelector.svelte +83 -0
- package/dist/client/components/sdk-auth/TenantSelector.svelte.d.ts +11 -0
- package/dist/client/components/sdk-auth/WorkspaceSelector.svelte +108 -0
- package/dist/client/components/sdk-auth/WorkspaceSelector.svelte.d.ts +16 -0
- package/dist/client/components/sdk-auth/shared/Alert.svelte +17 -0
- package/dist/client/components/sdk-auth/shared/Alert.svelte.d.ts +9 -0
- package/dist/client/components/sdk-auth/shared/AuthFormWrapper.svelte +23 -0
- package/dist/client/components/sdk-auth/shared/AuthFormWrapper.svelte.d.ts +10 -0
- package/dist/client/components/sdk-auth/shared/Spinner.svelte +16 -0
- package/dist/client/components/sdk-auth/shared/Spinner.svelte.d.ts +7 -0
- package/dist/client/components/subscription/PlanSelector.svelte +204 -0
- package/dist/client/components/subscription/PlanSelector.svelte.d.ts +22 -0
- package/dist/client/components/team/TeamAddUserDialog.svelte +125 -0
- package/dist/client/components/team/TeamAddUserDialog.svelte.d.ts +17 -0
- package/dist/client/components/team/TeamConfirmDialog.svelte +80 -0
- package/dist/client/components/team/TeamConfirmDialog.svelte.d.ts +21 -0
- package/dist/client/components/team/TeamEditUserDialog.svelte +131 -0
- package/dist/client/components/team/TeamEditUserDialog.svelte.d.ts +19 -0
- package/dist/client/components/team/TeamManagementPanel.svelte +72 -0
- package/dist/client/components/team/TeamManagementPanel.svelte.d.ts +20 -0
- package/dist/client/components/team/TeamProfileForm.svelte +129 -0
- package/dist/client/components/team/TeamProfileForm.svelte.d.ts +7 -0
- package/dist/client/components/team/TeamUserActionsMenu.svelte +63 -0
- package/dist/client/components/team/TeamUserActionsMenu.svelte.d.ts +9 -0
- package/dist/client/components/team/TeamUserList.svelte +226 -0
- package/dist/client/components/team/TeamUserList.svelte.d.ts +7 -0
- package/dist/client/components/team/TeamWorkspaceForm.svelte +134 -0
- package/dist/client/components/team/TeamWorkspaceForm.svelte.d.ts +7 -0
- package/dist/client/stores/config.store.d.ts +6 -0
- package/dist/client/stores/config.store.js +76 -0
- package/dist/client/utils/cn.d.ts +1 -0
- package/dist/client/utils/cn.js +3 -0
- package/dist/core/bridge-instance.d.ts +52 -0
- package/dist/core/bridge-instance.js +160 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.js +39 -0
- package/dist/shared/feature-flag.d.ts +6 -0
- package/dist/shared/feature-flag.js +13 -0
- package/dist/shared/logger.d.ts +18 -0
- package/dist/shared/logger.js +32 -0
- package/dist/shared/profile.d.ts +18 -0
- package/dist/shared/profile.js +25 -0
- package/dist/shared/types/config.d.ts +42 -0
- package/dist/shared/types/config.js +1 -0
- package/dist/styles.css +1089 -0
- package/package.json +86 -0
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
|
+
import { getBridgeAuth } from '../../../core/bridge-instance.js';
|
|
4
|
+
import type { ApiToken, AvailablePrivilege, CreateApiTokenInput } from '@thebridge/auth-core';
|
|
5
|
+
|
|
6
|
+
interface Props extends HTMLAttributes<HTMLDivElement> {}
|
|
7
|
+
|
|
8
|
+
let { class: className, style, ...rest }: Props = $props();
|
|
9
|
+
|
|
10
|
+
// ─── State ─────────────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
let tokens = $state<ApiToken[]>([]);
|
|
13
|
+
let loading = $state(true);
|
|
14
|
+
let error = $state<string | null>(null);
|
|
15
|
+
let success = $state<string | null>(null);
|
|
16
|
+
|
|
17
|
+
// Create form
|
|
18
|
+
let showCreateForm = $state(false);
|
|
19
|
+
let creating = $state(false);
|
|
20
|
+
let createName = $state('');
|
|
21
|
+
let createExpiry = $state('');
|
|
22
|
+
|
|
23
|
+
// Privilege picker
|
|
24
|
+
let availablePrivileges = $state<AvailablePrivilege[]>([]);
|
|
25
|
+
let selectedPrivileges = $state<string[]>([]);
|
|
26
|
+
let privSearch = $state('');
|
|
27
|
+
let privDropdownOpen = $state(false);
|
|
28
|
+
|
|
29
|
+
// Show-once token display
|
|
30
|
+
let newToken = $state<string | null>(null);
|
|
31
|
+
let showToken = $state(false);
|
|
32
|
+
|
|
33
|
+
// Revoke confirmation
|
|
34
|
+
let revokeTarget = $state<ApiToken | null>(null);
|
|
35
|
+
let revokeDialogOpen = $state(false);
|
|
36
|
+
let revoking = $state(false);
|
|
37
|
+
|
|
38
|
+
// ─── Load ───────────────────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
async function loadTokens() {
|
|
41
|
+
loading = true;
|
|
42
|
+
error = null;
|
|
43
|
+
try {
|
|
44
|
+
tokens = await getBridgeAuth().apiTokens.listAppTokens();
|
|
45
|
+
} catch (err) {
|
|
46
|
+
error = err instanceof Error ? err.message : 'Failed to load API tokens';
|
|
47
|
+
} finally {
|
|
48
|
+
loading = false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
$effect(() => { loadTokens(); });
|
|
53
|
+
|
|
54
|
+
// ─── Load privileges ────────────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
async function loadPrivileges() {
|
|
57
|
+
try {
|
|
58
|
+
availablePrivileges = await getBridgeAuth().apiTokens.listAvailablePrivileges();
|
|
59
|
+
} catch { /* non-fatal — picker shows empty */ }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
$effect(() => { if (showCreateForm) loadPrivileges(); });
|
|
63
|
+
|
|
64
|
+
// ─── Create ─────────────────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
async function createToken() {
|
|
67
|
+
creating = true;
|
|
68
|
+
error = null;
|
|
69
|
+
try {
|
|
70
|
+
const input: CreateApiTokenInput = {
|
|
71
|
+
name: createName.trim(),
|
|
72
|
+
privileges: selectedPrivileges,
|
|
73
|
+
expireAt: createExpiry || undefined,
|
|
74
|
+
};
|
|
75
|
+
const result = await getBridgeAuth().apiTokens.createAppToken(input);
|
|
76
|
+
newToken = result.token;
|
|
77
|
+
tokens = [result.record, ...tokens];
|
|
78
|
+
showCreateForm = false;
|
|
79
|
+
resetCreateForm();
|
|
80
|
+
success = 'Token created. Copy it now — it will not be shown again.';
|
|
81
|
+
} catch (err) {
|
|
82
|
+
error = err instanceof Error ? err.message : 'Failed to create token';
|
|
83
|
+
} finally {
|
|
84
|
+
creating = false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function resetCreateForm() {
|
|
89
|
+
createName = '';
|
|
90
|
+
selectedPrivileges = [];
|
|
91
|
+
privSearch = '';
|
|
92
|
+
privDropdownOpen = false;
|
|
93
|
+
createExpiry = '';
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ─── Revoke ─────────────────────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
async function confirmRevoke() {
|
|
99
|
+
if (!revokeTarget) return;
|
|
100
|
+
revoking = true;
|
|
101
|
+
error = null;
|
|
102
|
+
try {
|
|
103
|
+
await getBridgeAuth().apiTokens.revokeAppToken(revokeTarget.id);
|
|
104
|
+
tokens = tokens.filter((t) => t.id !== revokeTarget!.id);
|
|
105
|
+
success = `Token "${revokeTarget.name}" revoked.`;
|
|
106
|
+
revokeDialogOpen = false;
|
|
107
|
+
revokeTarget = null;
|
|
108
|
+
} catch (err) {
|
|
109
|
+
error = err instanceof Error ? err.message : 'Failed to revoke token';
|
|
110
|
+
} finally {
|
|
111
|
+
revoking = false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ─── Clipboard ──────────────────────────────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
async function copyToClipboard(text: string) {
|
|
118
|
+
try {
|
|
119
|
+
await navigator.clipboard.writeText(text);
|
|
120
|
+
success = 'Copied to clipboard!';
|
|
121
|
+
setTimeout(() => (success = null), 3000);
|
|
122
|
+
} catch {
|
|
123
|
+
error = 'Failed to copy to clipboard';
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ─── Click-outside: close privilege dropdown ─────────────────────────────────
|
|
128
|
+
|
|
129
|
+
$effect(() => {
|
|
130
|
+
if (!privDropdownOpen) return;
|
|
131
|
+
function handleOutsideClick(e: MouseEvent) {
|
|
132
|
+
const picker = document.querySelector('.bridge-privilege-picker');
|
|
133
|
+
if (picker && !picker.contains(e.target as Node)) {
|
|
134
|
+
privDropdownOpen = false;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
document.addEventListener('click', handleOutsideClick);
|
|
138
|
+
return () => document.removeEventListener('click', handleOutsideClick);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
142
|
+
|
|
143
|
+
function formatDate(iso: string | null | undefined): string {
|
|
144
|
+
if (!iso) return '—';
|
|
145
|
+
return new Date(iso).toLocaleDateString();
|
|
146
|
+
}
|
|
147
|
+
</script>
|
|
148
|
+
|
|
149
|
+
<div class={className} {style} data-bridge-api-tokens data-loading={loading} data-creating={creating} {...rest}>
|
|
150
|
+
<!-- Header -->
|
|
151
|
+
<div class="bridge-api-header">
|
|
152
|
+
<div>
|
|
153
|
+
<h2 class="bridge-api-title">API Tokens</h2>
|
|
154
|
+
<p class="bridge-api-subtitle">
|
|
155
|
+
Long-lived JWT tokens for programmatic API access.
|
|
156
|
+
</p>
|
|
157
|
+
</div>
|
|
158
|
+
<button
|
|
159
|
+
class="bridge-btn bridge-btn-primary"
|
|
160
|
+
onclick={() => { showCreateForm = !showCreateForm; if (!showCreateForm) resetCreateForm(); }}
|
|
161
|
+
>
|
|
162
|
+
{showCreateForm ? '✕ Cancel' : '+ Create Token'}
|
|
163
|
+
</button>
|
|
164
|
+
</div>
|
|
165
|
+
|
|
166
|
+
<!-- Error / Success banners -->
|
|
167
|
+
{#if error}
|
|
168
|
+
<div class="bridge-api-error-banner">{error}</div>
|
|
169
|
+
{/if}
|
|
170
|
+
{#if success}
|
|
171
|
+
<div class="bridge-api-success-banner">{success}</div>
|
|
172
|
+
{/if}
|
|
173
|
+
|
|
174
|
+
<!-- Show-once token display -->
|
|
175
|
+
{#if newToken}
|
|
176
|
+
<div class="bridge-api-new-token-banner">
|
|
177
|
+
<p class="bridge-api-new-token-warning">
|
|
178
|
+
⚠ Store this token securely — you won't be able to see it again.
|
|
179
|
+
</p>
|
|
180
|
+
<div class="bridge-api-new-token-row">
|
|
181
|
+
<input
|
|
182
|
+
class="bridge-input bridge-api-token-input"
|
|
183
|
+
type={showToken ? 'text' : 'password'}
|
|
184
|
+
readonly
|
|
185
|
+
value={newToken}
|
|
186
|
+
/>
|
|
187
|
+
<button
|
|
188
|
+
class="bridge-btn-outline"
|
|
189
|
+
onclick={() => (showToken = !showToken)}
|
|
190
|
+
title={showToken ? 'Hide token' : 'Show token'}
|
|
191
|
+
>
|
|
192
|
+
{showToken ? '🙈' : '👁'}
|
|
193
|
+
</button>
|
|
194
|
+
<button
|
|
195
|
+
class="bridge-btn-outline"
|
|
196
|
+
onclick={() => copyToClipboard(newToken!)}
|
|
197
|
+
>
|
|
198
|
+
Copy
|
|
199
|
+
</button>
|
|
200
|
+
</div>
|
|
201
|
+
<button
|
|
202
|
+
class="bridge-btn-outline"
|
|
203
|
+
onclick={() => { newToken = null; showToken = false; }}
|
|
204
|
+
>
|
|
205
|
+
Dismiss
|
|
206
|
+
</button>
|
|
207
|
+
</div>
|
|
208
|
+
{/if}
|
|
209
|
+
|
|
210
|
+
<!-- Inline create form -->
|
|
211
|
+
{#if showCreateForm}
|
|
212
|
+
<div class="bridge-api-inline-form">
|
|
213
|
+
<h3 class="bridge-api-inline-form-title">New API Token</h3>
|
|
214
|
+
<form
|
|
215
|
+
class="bridge-api-inline-form-fields"
|
|
216
|
+
onsubmit={(e) => { e.preventDefault(); createToken(); }}
|
|
217
|
+
>
|
|
218
|
+
<div class="bridge-api-inline-form-row">
|
|
219
|
+
<div class="bridge-api-inline-field">
|
|
220
|
+
<label class="bridge-label" for="token-name">Name <span class="bridge-api-required">*</span></label>
|
|
221
|
+
<input
|
|
222
|
+
id="token-name"
|
|
223
|
+
class="bridge-input"
|
|
224
|
+
type="text"
|
|
225
|
+
placeholder="e.g. CI pipeline token"
|
|
226
|
+
required
|
|
227
|
+
bind:value={createName}
|
|
228
|
+
/>
|
|
229
|
+
</div>
|
|
230
|
+
<div class="bridge-api-inline-field">
|
|
231
|
+
<label class="bridge-label">Privileges</label>
|
|
232
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
233
|
+
<div class="bridge-privilege-picker" data-open={privDropdownOpen}>
|
|
234
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
235
|
+
<div class="bridge-privilege-chips" onclick={() => (privDropdownOpen = !privDropdownOpen)}>
|
|
236
|
+
{#each selectedPrivileges as priv (priv)}
|
|
237
|
+
<span class="bridge-privilege-chip">
|
|
238
|
+
{priv}
|
|
239
|
+
<button type="button" onclick={(e) => { e.stopPropagation(); selectedPrivileges = selectedPrivileges.filter((p) => p !== priv); }}>×</button>
|
|
240
|
+
</span>
|
|
241
|
+
{/each}
|
|
242
|
+
{#if !selectedPrivileges.length}
|
|
243
|
+
<span class="bridge-privilege-placeholder">Select privileges…</span>
|
|
244
|
+
{/if}
|
|
245
|
+
</div>
|
|
246
|
+
{#if privDropdownOpen}
|
|
247
|
+
<div class="bridge-privilege-dropdown">
|
|
248
|
+
<input
|
|
249
|
+
class="bridge-privilege-search"
|
|
250
|
+
type="text"
|
|
251
|
+
placeholder="Search…"
|
|
252
|
+
bind:value={privSearch}
|
|
253
|
+
onclick={(e) => e.stopPropagation()}
|
|
254
|
+
/>
|
|
255
|
+
{#each availablePrivileges.filter((p) => !selectedPrivileges.includes(p.key) && p.key.toLowerCase().includes(privSearch.toLowerCase())) as priv (priv.key)}
|
|
256
|
+
<button type="button" class="bridge-privilege-option"
|
|
257
|
+
onclick={(e) => { e.stopPropagation(); selectedPrivileges = [...selectedPrivileges, priv.key]; privSearch = ''; }}>
|
|
258
|
+
<span class="bridge-privilege-option-key">{priv.key}</span>
|
|
259
|
+
{#if priv.description}<span class="bridge-privilege-option-desc">{priv.description}</span>{/if}
|
|
260
|
+
</button>
|
|
261
|
+
{:else}
|
|
262
|
+
<div class="bridge-privilege-empty">No privileges found</div>
|
|
263
|
+
{/each}
|
|
264
|
+
</div>
|
|
265
|
+
{/if}
|
|
266
|
+
</div>
|
|
267
|
+
</div>
|
|
268
|
+
<div class="bridge-api-inline-field bridge-api-inline-field--narrow">
|
|
269
|
+
<label class="bridge-label" for="token-expiry">Expiry (optional)</label>
|
|
270
|
+
<input
|
|
271
|
+
id="token-expiry"
|
|
272
|
+
class="bridge-input"
|
|
273
|
+
type="date"
|
|
274
|
+
bind:value={createExpiry}
|
|
275
|
+
/>
|
|
276
|
+
</div>
|
|
277
|
+
</div>
|
|
278
|
+
<div class="bridge-api-inline-form-actions">
|
|
279
|
+
<button
|
|
280
|
+
type="submit"
|
|
281
|
+
class="bridge-btn bridge-btn-primary"
|
|
282
|
+
disabled={creating || !createName.trim()}
|
|
283
|
+
>
|
|
284
|
+
{creating ? 'Creating…' : 'Create Token'}
|
|
285
|
+
</button>
|
|
286
|
+
</div>
|
|
287
|
+
</form>
|
|
288
|
+
</div>
|
|
289
|
+
{/if}
|
|
290
|
+
|
|
291
|
+
<!-- Token table -->
|
|
292
|
+
<div class="bridge-api-table-wrapper">
|
|
293
|
+
{#if loading}
|
|
294
|
+
<div class="bridge-api-empty">Loading…</div>
|
|
295
|
+
{:else if tokens.length === 0}
|
|
296
|
+
<div class="bridge-api-empty">
|
|
297
|
+
No API tokens yet. Create one to get started.
|
|
298
|
+
</div>
|
|
299
|
+
{:else}
|
|
300
|
+
<table class="bridge-api-table">
|
|
301
|
+
<thead class="bridge-api-table-head">
|
|
302
|
+
<tr>
|
|
303
|
+
<th class="bridge-api-th">Name</th>
|
|
304
|
+
<th class="bridge-api-th">Privileges</th>
|
|
305
|
+
<th class="bridge-api-th">Created</th>
|
|
306
|
+
<th class="bridge-api-th">Expires</th>
|
|
307
|
+
<th class="bridge-api-th"></th>
|
|
308
|
+
</tr>
|
|
309
|
+
</thead>
|
|
310
|
+
<tbody>
|
|
311
|
+
{#each tokens as token (token.id)}
|
|
312
|
+
<tr class="bridge-api-row">
|
|
313
|
+
<td class="bridge-api-td-name">{token.name}</td>
|
|
314
|
+
<td class="bridge-api-td">
|
|
315
|
+
<div class="bridge-api-privileges">
|
|
316
|
+
{#each token.privileges as priv (priv)}
|
|
317
|
+
<span class="bridge-badge">{priv}</span>
|
|
318
|
+
{/each}
|
|
319
|
+
</div>
|
|
320
|
+
</td>
|
|
321
|
+
<td class="bridge-api-td-muted">{formatDate(token.createdAt)}</td>
|
|
322
|
+
<td class="bridge-api-td-muted">{formatDate(token.expireAt)}</td>
|
|
323
|
+
<td class="bridge-api-td-actions">
|
|
324
|
+
<button
|
|
325
|
+
class="bridge-btn-danger-sm"
|
|
326
|
+
onclick={() => { revokeTarget = token; revokeDialogOpen = true; }}
|
|
327
|
+
>
|
|
328
|
+
Revoke
|
|
329
|
+
</button>
|
|
330
|
+
</td>
|
|
331
|
+
</tr>
|
|
332
|
+
{/each}
|
|
333
|
+
</tbody>
|
|
334
|
+
</table>
|
|
335
|
+
{/if}
|
|
336
|
+
</div>
|
|
337
|
+
</div>
|
|
338
|
+
|
|
339
|
+
<!-- Revoke Confirmation Dialog -->
|
|
340
|
+
{#if revokeDialogOpen}
|
|
341
|
+
<div
|
|
342
|
+
class="bridge-dialog-overlay"
|
|
343
|
+
role="presentation"
|
|
344
|
+
onclick={() => { revokeDialogOpen = false; revokeTarget = null; }}
|
|
345
|
+
></div>
|
|
346
|
+
<div class="bridge-dialog" role="dialog" aria-modal="true">
|
|
347
|
+
<h3 class="bridge-dialog-title">Revoke token?</h3>
|
|
348
|
+
<p class="bridge-dialog-body">
|
|
349
|
+
Token <strong>{revokeTarget?.name}</strong> will be permanently revoked and can no longer be used.
|
|
350
|
+
</p>
|
|
351
|
+
<div class="bridge-dialog-footer">
|
|
352
|
+
<button
|
|
353
|
+
class="bridge-btn-outline"
|
|
354
|
+
onclick={() => { revokeDialogOpen = false; revokeTarget = null; }}
|
|
355
|
+
>Cancel</button>
|
|
356
|
+
<button
|
|
357
|
+
class="bridge-btn bridge-btn-danger"
|
|
358
|
+
disabled={revoking}
|
|
359
|
+
onclick={confirmRevoke}
|
|
360
|
+
>
|
|
361
|
+
{revoking ? 'Revoking…' : 'Revoke'}
|
|
362
|
+
</button>
|
|
363
|
+
</div>
|
|
364
|
+
</div>
|
|
365
|
+
{/if}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
2
|
+
interface Props extends HTMLAttributes<HTMLDivElement> {
|
|
3
|
+
}
|
|
4
|
+
declare const ApiTokenManagement: import("svelte").Component<Props, {}, "">;
|
|
5
|
+
type ApiTokenManagement = ReturnType<typeof ApiTokenManagement>;
|
|
6
|
+
export default ApiTokenManagement;
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
|
+
import { getBridgeAuth } from '../../../core/bridge-instance.js';
|
|
4
|
+
import AuthFormWrapper from './shared/AuthFormWrapper.svelte';
|
|
5
|
+
import Spinner from './shared/Spinner.svelte';
|
|
6
|
+
import Alert from './shared/Alert.svelte';
|
|
7
|
+
|
|
8
|
+
interface Props extends HTMLAttributes<HTMLDivElement> {
|
|
9
|
+
token?: string;
|
|
10
|
+
onComplete?: () => void;
|
|
11
|
+
onError?: (error: Error) => void;
|
|
12
|
+
loginHref?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let {
|
|
16
|
+
token,
|
|
17
|
+
onComplete,
|
|
18
|
+
onError,
|
|
19
|
+
loginHref = '/login',
|
|
20
|
+
class: className,
|
|
21
|
+
style,
|
|
22
|
+
...rest
|
|
23
|
+
}: Props = $props();
|
|
24
|
+
|
|
25
|
+
let email = $state('');
|
|
26
|
+
let password = $state('');
|
|
27
|
+
let confirmPassword = $state('');
|
|
28
|
+
let loading = $state(false);
|
|
29
|
+
let error = $state<string | null>(null);
|
|
30
|
+
let emailSent = $state(false);
|
|
31
|
+
let passwordReset = $state(false);
|
|
32
|
+
let showPasswords = $state(false);
|
|
33
|
+
|
|
34
|
+
const isSetMode = $derived(!!token);
|
|
35
|
+
|
|
36
|
+
async function handleSendLink() {
|
|
37
|
+
if (loading) return;
|
|
38
|
+
error = null;
|
|
39
|
+
loading = true;
|
|
40
|
+
try {
|
|
41
|
+
await getBridgeAuth().sendResetPasswordLink(email);
|
|
42
|
+
emailSent = true;
|
|
43
|
+
} catch (err: any) {
|
|
44
|
+
error = err.message || 'Failed to send reset link.';
|
|
45
|
+
onError?.(err);
|
|
46
|
+
} finally {
|
|
47
|
+
loading = false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function handleSetPassword() {
|
|
52
|
+
if (loading) return;
|
|
53
|
+
error = null;
|
|
54
|
+
|
|
55
|
+
if (password !== confirmPassword) {
|
|
56
|
+
error = 'Passwords do not match.';
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (password.length < 8) {
|
|
61
|
+
error = 'Password must be at least 8 characters.';
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
loading = true;
|
|
66
|
+
try {
|
|
67
|
+
await getBridgeAuth().updatePassword(token!, password);
|
|
68
|
+
passwordReset = true;
|
|
69
|
+
onComplete?.();
|
|
70
|
+
} catch (err: any) {
|
|
71
|
+
error = err.message || 'Failed to update password.';
|
|
72
|
+
onError?.(err);
|
|
73
|
+
} finally {
|
|
74
|
+
loading = false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
</script>
|
|
78
|
+
|
|
79
|
+
<AuthFormWrapper
|
|
80
|
+
heading={isSetMode ? 'Set new password' : 'Reset your password'}
|
|
81
|
+
class={className}
|
|
82
|
+
{style}
|
|
83
|
+
{...rest}
|
|
84
|
+
>
|
|
85
|
+
{#if error}
|
|
86
|
+
<Alert variant="error">{error}</Alert>
|
|
87
|
+
{/if}
|
|
88
|
+
|
|
89
|
+
{#if isSetMode}
|
|
90
|
+
{#if passwordReset}
|
|
91
|
+
<h2 class="bridge-success-heading">Password set</h2>
|
|
92
|
+
<div class="bridge-form-footer">
|
|
93
|
+
<a href={loginHref}>Back to login</a>
|
|
94
|
+
</div>
|
|
95
|
+
{:else}
|
|
96
|
+
<form onsubmit={(e) => { e.preventDefault(); handleSetPassword(); }}>
|
|
97
|
+
<div class="bridge-form-group">
|
|
98
|
+
<label for="newPassword">New password</label>
|
|
99
|
+
<div class="bridge-password-wrapper">
|
|
100
|
+
<input
|
|
101
|
+
id="newPassword"
|
|
102
|
+
type={showPasswords ? 'text' : 'password'}
|
|
103
|
+
placeholder="At least 8 characters"
|
|
104
|
+
required
|
|
105
|
+
bind:value={password}
|
|
106
|
+
disabled={loading}
|
|
107
|
+
/>
|
|
108
|
+
<button
|
|
109
|
+
type="button"
|
|
110
|
+
class="bridge-password-toggle"
|
|
111
|
+
onclick={() => showPasswords = !showPasswords}
|
|
112
|
+
tabindex={-1}
|
|
113
|
+
aria-label={showPasswords ? 'Hide passwords' : 'Show passwords'}
|
|
114
|
+
>
|
|
115
|
+
{#if showPasswords}
|
|
116
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94"/><path d="M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19"/><line x1="1" y1="1" x2="23" y2="23"/></svg>
|
|
117
|
+
{:else}
|
|
118
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
|
|
119
|
+
{/if}
|
|
120
|
+
</button>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
<div class="bridge-form-group">
|
|
124
|
+
<label for="confirmPassword">Confirm password</label>
|
|
125
|
+
<div class="bridge-password-wrapper">
|
|
126
|
+
<input
|
|
127
|
+
id="confirmPassword"
|
|
128
|
+
type={showPasswords ? 'text' : 'password'}
|
|
129
|
+
placeholder="Repeat password"
|
|
130
|
+
required
|
|
131
|
+
bind:value={confirmPassword}
|
|
132
|
+
disabled={loading}
|
|
133
|
+
/>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
<button type="submit" class="bridge-btn bridge-btn-primary" disabled={loading || !password}>
|
|
137
|
+
{#if loading}<Spinner size={16} />{:else}Set a password{/if}
|
|
138
|
+
</button>
|
|
139
|
+
</form>
|
|
140
|
+
{/if}
|
|
141
|
+
{:else}
|
|
142
|
+
{#if emailSent}
|
|
143
|
+
<Alert variant="success">Check your email for a password reset link.</Alert>
|
|
144
|
+
<div class="bridge-form-footer">
|
|
145
|
+
<a href={loginHref}>Back to login</a>
|
|
146
|
+
</div>
|
|
147
|
+
{:else}
|
|
148
|
+
<p class="bridge-step-desc">Enter your email and we'll send you a link to reset your password.</p>
|
|
149
|
+
<form onsubmit={(e) => { e.preventDefault(); handleSendLink(); }}>
|
|
150
|
+
<div class="bridge-form-group">
|
|
151
|
+
<label for="reset-email">Email</label>
|
|
152
|
+
<input
|
|
153
|
+
id="reset-email"
|
|
154
|
+
type="email"
|
|
155
|
+
placeholder="you@example.com"
|
|
156
|
+
required
|
|
157
|
+
bind:value={email}
|
|
158
|
+
disabled={loading}
|
|
159
|
+
/>
|
|
160
|
+
</div>
|
|
161
|
+
<button type="submit" class="bridge-btn bridge-btn-primary" disabled={loading || !email.trim()}>
|
|
162
|
+
{#if loading}<Spinner size={16} />{:else}Send reset link{/if}
|
|
163
|
+
</button>
|
|
164
|
+
</form>
|
|
165
|
+
<div class="bridge-form-footer">
|
|
166
|
+
<a href={loginHref}>Back to login</a>
|
|
167
|
+
</div>
|
|
168
|
+
{/if}
|
|
169
|
+
{/if}
|
|
170
|
+
</AuthFormWrapper>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
2
|
+
interface Props extends HTMLAttributes<HTMLDivElement> {
|
|
3
|
+
token?: string;
|
|
4
|
+
onComplete?: () => void;
|
|
5
|
+
onError?: (error: Error) => void;
|
|
6
|
+
loginHref?: string;
|
|
7
|
+
}
|
|
8
|
+
declare const ForgotPassword: import("svelte").Component<Props, {}, "">;
|
|
9
|
+
type ForgotPassword = ReturnType<typeof ForgotPassword>;
|
|
10
|
+
export default ForgotPassword;
|