@myrialabs/clopen 0.1.10 → 0.2.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/README.md +23 -1
- package/backend/index.ts +20 -0
- package/backend/lib/auth/auth-service.ts +484 -0
- package/backend/lib/auth/index.ts +4 -0
- package/backend/lib/auth/permissions.ts +63 -0
- package/backend/lib/auth/rate-limiter.ts +145 -0
- package/backend/lib/auth/tokens.ts +53 -0
- package/backend/lib/database/migrations/024_create_users_table.ts +29 -0
- package/backend/lib/database/migrations/025_create_auth_sessions_table.ts +38 -0
- package/backend/lib/database/migrations/026_create_invite_tokens_table.ts +31 -0
- package/backend/lib/database/migrations/index.ts +21 -0
- package/backend/lib/database/queries/auth-queries.ts +201 -0
- package/backend/lib/database/queries/index.ts +2 -1
- package/backend/lib/engine/adapters/opencode/server.ts +1 -1
- package/backend/lib/mcp/config.ts +13 -18
- package/backend/lib/mcp/index.ts +9 -0
- package/backend/lib/mcp/remote-server.ts +132 -0
- package/backend/lib/mcp/servers/helper.ts +49 -3
- package/backend/lib/mcp/servers/index.ts +3 -2
- package/backend/lib/preview/browser/browser-audio-capture.ts +20 -3
- package/backend/lib/preview/browser/browser-navigation-tracker.ts +3 -0
- package/backend/lib/preview/browser/browser-pool.ts +73 -176
- package/backend/lib/preview/browser/browser-preview-service.ts +3 -2
- package/backend/lib/preview/browser/browser-tab-manager.ts +261 -23
- package/backend/lib/preview/browser/browser-video-capture.ts +36 -1
- package/backend/lib/utils/ws.ts +65 -1
- package/backend/ws/auth/index.ts +17 -0
- package/backend/ws/auth/invites.ts +84 -0
- package/backend/ws/auth/login.ts +269 -0
- package/backend/ws/auth/status.ts +41 -0
- package/backend/ws/auth/users.ts +32 -0
- package/backend/ws/engine/claude/accounts.ts +3 -1
- package/backend/ws/engine/utils.ts +38 -6
- package/backend/ws/index.ts +4 -4
- package/backend/ws/preview/browser/interact.ts +27 -5
- package/bin/clopen.ts +39 -0
- package/bun.lock +113 -51
- package/frontend/App.svelte +47 -29
- package/frontend/lib/components/auth/InvitePage.svelte +215 -0
- package/frontend/lib/components/auth/LoginPage.svelte +129 -0
- package/frontend/lib/components/auth/SetupPage.svelte +1022 -0
- package/frontend/lib/components/common/FolderBrowser.svelte +9 -9
- package/frontend/lib/components/common/UpdateBanner.svelte +2 -2
- package/frontend/lib/components/preview/browser/BrowserPreview.svelte +1 -1
- package/frontend/lib/components/preview/browser/core/mcp-handlers.svelte.ts +12 -4
- package/frontend/lib/components/settings/SettingsModal.svelte +50 -15
- package/frontend/lib/components/settings/SettingsView.svelte +21 -7
- package/frontend/lib/components/settings/account/AccountSettings.svelte +5 -0
- package/frontend/lib/components/settings/admin/InviteManagement.svelte +239 -0
- package/frontend/lib/components/settings/admin/UserManagement.svelte +127 -0
- package/frontend/lib/components/settings/general/AdvancedSettings.svelte +10 -4
- package/frontend/lib/components/settings/general/AuthModeSettings.svelte +229 -0
- package/frontend/lib/components/settings/general/GeneralSettings.svelte +6 -1
- package/frontend/lib/components/settings/general/UpdateSettings.svelte +5 -5
- package/frontend/lib/components/settings/security/SecuritySettings.svelte +10 -0
- package/frontend/lib/components/settings/system/SystemSettings.svelte +10 -0
- package/frontend/lib/components/settings/user/UserSettings.svelte +147 -74
- package/frontend/lib/components/workspace/WorkspaceLayout.svelte +5 -10
- package/frontend/lib/services/preview/browser/browser-webcodecs.service.ts +31 -8
- package/frontend/lib/stores/features/auth.svelte.ts +296 -0
- package/frontend/lib/stores/features/settings.svelte.ts +53 -9
- package/frontend/lib/stores/features/user.svelte.ts +26 -68
- package/frontend/lib/stores/ui/settings-modal.svelte.ts +42 -21
- package/frontend/lib/stores/ui/update.svelte.ts +2 -2
- package/package.json +8 -6
- package/shared/types/stores/settings.ts +16 -2
- package/shared/utils/logger.ts +1 -0
- package/shared/utils/ws-client.ts +30 -13
- package/shared/utils/ws-server.ts +42 -4
- package/backend/lib/mcp/stdio-server.ts +0 -103
- package/backend/ws/mcp/index.ts +0 -61
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import {
|
|
2
|
+
import { systemSettings, updateSystemSettings } from '$frontend/lib/stores/features/settings.svelte';
|
|
3
|
+
import { authStore } from '$frontend/lib/stores/features/auth.svelte';
|
|
3
4
|
import Icon from '../../common/Icon.svelte';
|
|
4
5
|
import Dialog from '../../common/Dialog.svelte';
|
|
5
6
|
import { detectPlatform } from '$frontend/lib/utils/platform';
|
|
6
7
|
|
|
8
|
+
const isAdmin = $derived(authStore.isAdmin);
|
|
9
|
+
const settings = $derived(systemSettings);
|
|
10
|
+
|
|
7
11
|
let showAddPathDialog = $state(false);
|
|
8
12
|
let newPathValue = $state('');
|
|
9
13
|
|
|
@@ -29,7 +33,7 @@
|
|
|
29
33
|
function addPath() {
|
|
30
34
|
const path = newPathValue.trim();
|
|
31
35
|
if (path && !settings.allowedBasePaths.includes(path)) {
|
|
32
|
-
|
|
36
|
+
updateSystemSettings({ allowedBasePaths: [...settings.allowedBasePaths, path] });
|
|
33
37
|
}
|
|
34
38
|
newPathValue = '';
|
|
35
39
|
showAddPathDialog = false;
|
|
@@ -38,7 +42,7 @@
|
|
|
38
42
|
function removePath(index: number) {
|
|
39
43
|
const newPaths = [...settings.allowedBasePaths];
|
|
40
44
|
newPaths.splice(index, 1);
|
|
41
|
-
|
|
45
|
+
updateSystemSettings({ allowedBasePaths: newPaths });
|
|
42
46
|
if (editingIndex === index) {
|
|
43
47
|
editingIndex = null;
|
|
44
48
|
editingValue = '';
|
|
@@ -56,7 +60,7 @@
|
|
|
56
60
|
if (path) {
|
|
57
61
|
const newPaths = [...settings.allowedBasePaths];
|
|
58
62
|
newPaths[editingIndex] = path;
|
|
59
|
-
|
|
63
|
+
updateSystemSettings({ allowedBasePaths: newPaths });
|
|
60
64
|
}
|
|
61
65
|
editingIndex = null;
|
|
62
66
|
editingValue = '';
|
|
@@ -73,6 +77,7 @@
|
|
|
73
77
|
}
|
|
74
78
|
</script>
|
|
75
79
|
|
|
80
|
+
{#if isAdmin}
|
|
76
81
|
<div class="py-1">
|
|
77
82
|
<h3 class="text-base font-bold text-slate-900 dark:text-slate-100 mb-1.5">Advanced</h3>
|
|
78
83
|
<p class="text-sm text-slate-600 dark:text-slate-500 mb-5">Security and access control settings</p>
|
|
@@ -170,6 +175,7 @@
|
|
|
170
175
|
</div>
|
|
171
176
|
</div>
|
|
172
177
|
</div>
|
|
178
|
+
{/if}
|
|
173
179
|
|
|
174
180
|
<Dialog
|
|
175
181
|
bind:isOpen={showAddPathDialog}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { systemSettings, updateSystemSettings } from '$frontend/lib/stores/features/settings.svelte';
|
|
3
|
+
import { authStore } from '$frontend/lib/stores/features/auth.svelte';
|
|
4
|
+
import { addNotification } from '$frontend/lib/stores/ui/notification.svelte';
|
|
5
|
+
import Icon from '../../common/Icon.svelte';
|
|
6
|
+
import Dialog from '../../common/Dialog.svelte';
|
|
7
|
+
import type { AuthMode } from '$shared/types/stores/settings';
|
|
8
|
+
import { debug } from '$shared/utils/logger';
|
|
9
|
+
|
|
10
|
+
const isAdmin = $derived(authStore.isAdmin);
|
|
11
|
+
const currentMode = $derived(systemSettings.authMode);
|
|
12
|
+
|
|
13
|
+
// Simple confirmation dialog (required → none)
|
|
14
|
+
let showSimpleConfirm = $state(false);
|
|
15
|
+
let pendingMode = $state<AuthMode>('required');
|
|
16
|
+
|
|
17
|
+
// PAT dialog (none → required)
|
|
18
|
+
let showPatDialog = $state(false);
|
|
19
|
+
let generatedPat = $state('');
|
|
20
|
+
let patCopied = $state(false);
|
|
21
|
+
let isPreparingSwitch = $state(false);
|
|
22
|
+
|
|
23
|
+
async function requestModeChange(mode: AuthMode) {
|
|
24
|
+
if (mode === currentMode) return;
|
|
25
|
+
pendingMode = mode;
|
|
26
|
+
|
|
27
|
+
if (currentMode === 'none' && mode === 'required') {
|
|
28
|
+
// Switching to with-auth: regenerate PAT first, then show dialog
|
|
29
|
+
isPreparingSwitch = true;
|
|
30
|
+
try {
|
|
31
|
+
const pat = await authStore.regeneratePAT();
|
|
32
|
+
generatedPat = pat;
|
|
33
|
+
showPatDialog = true;
|
|
34
|
+
} catch (error) {
|
|
35
|
+
debug.error('settings', 'Failed to regenerate PAT for auth mode switch:', error);
|
|
36
|
+
addNotification({ type: 'error', title: 'Error', message: 'Failed to prepare authentication switch' });
|
|
37
|
+
} finally {
|
|
38
|
+
isPreparingSwitch = false;
|
|
39
|
+
}
|
|
40
|
+
} else {
|
|
41
|
+
showSimpleConfirm = true;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function confirmSimpleChange() {
|
|
46
|
+
showSimpleConfirm = false;
|
|
47
|
+
updateSystemSettings({ authMode: pendingMode });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function confirmWithAuthSwitch() {
|
|
51
|
+
showPatDialog = false;
|
|
52
|
+
|
|
53
|
+
// Save auth mode
|
|
54
|
+
await updateSystemSettings({ authMode: 'required' });
|
|
55
|
+
|
|
56
|
+
// Logout all sessions (including current) — forces everyone to re-login
|
|
57
|
+
await authStore.logoutAll();
|
|
58
|
+
|
|
59
|
+
generatedPat = '';
|
|
60
|
+
patCopied = false;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function cancelPatDialog() {
|
|
64
|
+
showPatDialog = false;
|
|
65
|
+
generatedPat = '';
|
|
66
|
+
patCopied = false;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function copyPat() {
|
|
70
|
+
if (generatedPat) {
|
|
71
|
+
await navigator.clipboard.writeText(generatedPat);
|
|
72
|
+
patCopied = true;
|
|
73
|
+
setTimeout(() => { patCopied = false; }, 2000);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
</script>
|
|
77
|
+
|
|
78
|
+
{#if isAdmin}
|
|
79
|
+
<div class="py-1">
|
|
80
|
+
<h3 class="text-base font-bold text-slate-900 dark:text-slate-100 mb-1.5">Authentication</h3>
|
|
81
|
+
<p class="text-sm text-slate-600 dark:text-slate-500 mb-5">Configure how users access Clopen</p>
|
|
82
|
+
|
|
83
|
+
<div class="flex flex-col gap-3.5">
|
|
84
|
+
<!-- No Login -->
|
|
85
|
+
<button
|
|
86
|
+
type="button"
|
|
87
|
+
disabled={isPreparingSwitch}
|
|
88
|
+
class="w-full text-left p-4 bg-slate-100/80 dark:bg-slate-800/80 border rounded-xl transition-all
|
|
89
|
+
{currentMode === 'none'
|
|
90
|
+
? 'border-violet-500/50 ring-1 ring-violet-500/20'
|
|
91
|
+
: 'border-slate-200 dark:border-slate-800 hover:border-slate-300 dark:hover:border-slate-700'}
|
|
92
|
+
disabled:opacity-50 disabled:cursor-not-allowed"
|
|
93
|
+
onclick={() => requestModeChange('none')}
|
|
94
|
+
>
|
|
95
|
+
<div class="flex items-center gap-3.5">
|
|
96
|
+
<div class="flex items-center justify-center w-10 h-10 rounded-lg shrink-0
|
|
97
|
+
{currentMode === 'none'
|
|
98
|
+
? 'bg-violet-500/15 text-violet-600 dark:text-violet-400'
|
|
99
|
+
: 'bg-slate-200/80 dark:bg-slate-700 text-slate-400'}">
|
|
100
|
+
<Icon name="lucide:lock-open" class="w-5 h-5" />
|
|
101
|
+
</div>
|
|
102
|
+
<div class="flex-1 min-w-0">
|
|
103
|
+
<div class="text-sm font-semibold text-slate-900 dark:text-slate-100">No Login</div>
|
|
104
|
+
<div class="text-xs text-slate-600 dark:text-slate-500">
|
|
105
|
+
No authentication required. Anyone with access to this URL can use Clopen.
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
{#if currentMode === 'none'}
|
|
109
|
+
<div class="flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium bg-violet-100 dark:bg-violet-900/30 text-violet-700 dark:text-violet-300 shrink-0">
|
|
110
|
+
<span class="w-1.5 h-1.5 rounded-full bg-violet-500"></span>
|
|
111
|
+
Active
|
|
112
|
+
</div>
|
|
113
|
+
{/if}
|
|
114
|
+
</div>
|
|
115
|
+
</button>
|
|
116
|
+
|
|
117
|
+
<!-- With Login -->
|
|
118
|
+
<button
|
|
119
|
+
type="button"
|
|
120
|
+
disabled={isPreparingSwitch}
|
|
121
|
+
class="w-full text-left p-4 bg-slate-100/80 dark:bg-slate-800/80 border rounded-xl transition-all
|
|
122
|
+
{currentMode === 'required'
|
|
123
|
+
? 'border-violet-500/50 ring-1 ring-violet-500/20'
|
|
124
|
+
: 'border-slate-200 dark:border-slate-800 hover:border-slate-300 dark:hover:border-slate-700'}
|
|
125
|
+
disabled:opacity-50 disabled:cursor-not-allowed"
|
|
126
|
+
onclick={() => requestModeChange('required')}
|
|
127
|
+
>
|
|
128
|
+
<div class="flex items-center gap-3.5">
|
|
129
|
+
<div class="flex items-center justify-center w-10 h-10 rounded-lg shrink-0
|
|
130
|
+
{currentMode === 'required'
|
|
131
|
+
? 'bg-violet-500/15 text-violet-600 dark:text-violet-400'
|
|
132
|
+
: 'bg-slate-200/80 dark:bg-slate-700 text-slate-400'}">
|
|
133
|
+
<Icon name="lucide:lock" class="w-5 h-5" />
|
|
134
|
+
</div>
|
|
135
|
+
<div class="flex-1 min-w-0">
|
|
136
|
+
<div class="text-sm font-semibold text-slate-900 dark:text-slate-100">With Login</div>
|
|
137
|
+
<div class="text-xs text-slate-600 dark:text-slate-500">
|
|
138
|
+
Authenticate with a Personal Access Token. Supports multiple users and invite links.
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
{#if currentMode === 'required'}
|
|
142
|
+
<div class="flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium bg-violet-100 dark:bg-violet-900/30 text-violet-700 dark:text-violet-300 shrink-0">
|
|
143
|
+
<span class="w-1.5 h-1.5 rounded-full bg-violet-500"></span>
|
|
144
|
+
Active
|
|
145
|
+
</div>
|
|
146
|
+
{/if}
|
|
147
|
+
</div>
|
|
148
|
+
</button>
|
|
149
|
+
</div>
|
|
150
|
+
|
|
151
|
+
{#if isPreparingSwitch}
|
|
152
|
+
<div class="flex items-center gap-2 mt-4 text-sm text-slate-500">
|
|
153
|
+
<div class="w-4 h-4 border-2 border-violet-500/20 border-t-violet-600 rounded-full animate-spin"></div>
|
|
154
|
+
<span>Preparing authentication switch...</span>
|
|
155
|
+
</div>
|
|
156
|
+
{/if}
|
|
157
|
+
</div>
|
|
158
|
+
{/if}
|
|
159
|
+
|
|
160
|
+
<!-- Simple confirm dialog (required → none) -->
|
|
161
|
+
<Dialog
|
|
162
|
+
bind:isOpen={showSimpleConfirm}
|
|
163
|
+
onClose={() => { showSimpleConfirm = false; }}
|
|
164
|
+
type="info"
|
|
165
|
+
title="Change Authentication Mode"
|
|
166
|
+
message="Switching to No Login mode will disable login requirements. Existing users and sessions will be preserved but bypassed."
|
|
167
|
+
confirmText="Confirm"
|
|
168
|
+
cancelText="Cancel"
|
|
169
|
+
showCancel={true}
|
|
170
|
+
onConfirm={confirmSimpleChange}
|
|
171
|
+
/>
|
|
172
|
+
|
|
173
|
+
<!-- PAT dialog (none → required) -->
|
|
174
|
+
<Dialog
|
|
175
|
+
bind:isOpen={showPatDialog}
|
|
176
|
+
onClose={cancelPatDialog}
|
|
177
|
+
type="warning"
|
|
178
|
+
title="Change Authentication Mode"
|
|
179
|
+
closable={false}
|
|
180
|
+
confirmText="Confirm"
|
|
181
|
+
cancelText="Cancel"
|
|
182
|
+
showCancel={true}
|
|
183
|
+
onConfirm={confirmWithAuthSwitch}
|
|
184
|
+
>
|
|
185
|
+
{#snippet children()}
|
|
186
|
+
<div class="flex items-start space-x-4">
|
|
187
|
+
<div class="bg-amber-50 dark:bg-amber-900/20 border-amber-200 dark:border-amber-700/50 rounded-xl p-3 border">
|
|
188
|
+
<Icon name="lucide:triangle-alert" class="w-6 h-6 text-amber-600 dark:text-amber-400" />
|
|
189
|
+
</div>
|
|
190
|
+
|
|
191
|
+
<div class="flex-1 space-y-3">
|
|
192
|
+
<h3 class="text-lg font-semibold text-slate-900 dark:text-slate-100">
|
|
193
|
+
Change Authentication Mode
|
|
194
|
+
</h3>
|
|
195
|
+
<p class="text-sm text-slate-600 dark:text-slate-400 leading-relaxed">
|
|
196
|
+
Switching to With Login mode will require authentication. All sessions will be logged out and you will need this token to log in again.
|
|
197
|
+
</p>
|
|
198
|
+
|
|
199
|
+
<!-- PAT Display -->
|
|
200
|
+
<div class="p-3.5 rounded-lg bg-amber-50 dark:bg-amber-950/30 border border-amber-200 dark:border-amber-800">
|
|
201
|
+
<div class="flex items-center gap-2 text-sm font-semibold text-amber-800 dark:text-amber-200 mb-2">
|
|
202
|
+
<Icon name="lucide:key-round" class="w-4 h-4" />
|
|
203
|
+
<span>Your Personal Access Token</span>
|
|
204
|
+
</div>
|
|
205
|
+
<div class="flex items-center gap-2">
|
|
206
|
+
<code class="flex-1 px-3 py-2 rounded-md bg-white dark:bg-slate-900 border border-amber-300 dark:border-amber-700 text-xs font-mono text-slate-900 dark:text-slate-100 select-all break-all">
|
|
207
|
+
{generatedPat}
|
|
208
|
+
</code>
|
|
209
|
+
<button
|
|
210
|
+
type="button"
|
|
211
|
+
onclick={copyPat}
|
|
212
|
+
class="shrink-0 flex items-center justify-center w-9 h-9 rounded-lg transition-all
|
|
213
|
+
{patCopied
|
|
214
|
+
? 'bg-green-100 dark:bg-green-900/30 text-green-600 dark:text-green-400'
|
|
215
|
+
: 'bg-amber-100 dark:bg-amber-900 hover:bg-amber-200 dark:hover:bg-amber-800 text-amber-800 dark:text-amber-200'}"
|
|
216
|
+
title="Copy token"
|
|
217
|
+
>
|
|
218
|
+
<Icon name={patCopied ? 'lucide:check' : 'lucide:copy'} class="w-4 h-4" />
|
|
219
|
+
</button>
|
|
220
|
+
</div>
|
|
221
|
+
<div class="flex items-center gap-1.5 mt-2 text-xs text-amber-600 dark:text-amber-400">
|
|
222
|
+
<Icon name="lucide:triangle-alert" class="w-3.5 h-3.5 shrink-0" />
|
|
223
|
+
<span>Copy this token now. It won't be shown again.</span>
|
|
224
|
+
</div>
|
|
225
|
+
</div>
|
|
226
|
+
</div>
|
|
227
|
+
</div>
|
|
228
|
+
{/snippet}
|
|
229
|
+
</Dialog>
|
|
@@ -2,9 +2,14 @@
|
|
|
2
2
|
import DataManagementSettings from './DataManagementSettings.svelte';
|
|
3
3
|
import AdvancedSettings from './AdvancedSettings.svelte';
|
|
4
4
|
import UpdateSettings from './UpdateSettings.svelte';
|
|
5
|
+
import AuthModeSettings from './AuthModeSettings.svelte';
|
|
5
6
|
</script>
|
|
6
7
|
|
|
7
|
-
<
|
|
8
|
+
<AuthModeSettings />
|
|
9
|
+
|
|
10
|
+
<div class="mt-6">
|
|
11
|
+
<UpdateSettings />
|
|
12
|
+
</div>
|
|
8
13
|
|
|
9
14
|
<div class="mt-6">
|
|
10
15
|
<DataManagementSettings />
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import {
|
|
2
|
+
import { systemSettings, updateSystemSettings } from '$frontend/lib/stores/features/settings.svelte';
|
|
3
3
|
import { updateState, checkForUpdate, runUpdate } from '$frontend/lib/stores/ui/update.svelte';
|
|
4
4
|
import Icon from '../../common/Icon.svelte';
|
|
5
5
|
|
|
6
6
|
function toggleAutoUpdate() {
|
|
7
|
-
|
|
7
|
+
updateSystemSettings({ autoUpdate: !systemSettings.autoUpdate });
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
function handleCheckNow() {
|
|
@@ -108,14 +108,14 @@
|
|
|
108
108
|
<button
|
|
109
109
|
type="button"
|
|
110
110
|
role="switch"
|
|
111
|
-
aria-checked={
|
|
111
|
+
aria-checked={systemSettings.autoUpdate}
|
|
112
112
|
onclick={toggleAutoUpdate}
|
|
113
113
|
class="relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-violet-500/30
|
|
114
|
-
{
|
|
114
|
+
{systemSettings.autoUpdate ? 'bg-violet-600' : 'bg-slate-300 dark:bg-slate-600'}"
|
|
115
115
|
>
|
|
116
116
|
<span
|
|
117
117
|
class="pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow-lg ring-0 transition duration-200 ease-in-out
|
|
118
|
-
{
|
|
118
|
+
{systemSettings.autoUpdate ? 'translate-x-5' : 'translate-x-0'}"
|
|
119
119
|
></span>
|
|
120
120
|
</button>
|
|
121
121
|
</div>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import UpdateSettings from '../general/UpdateSettings.svelte';
|
|
3
|
+
import DataManagementSettings from '../general/DataManagementSettings.svelte';
|
|
4
|
+
</script>
|
|
5
|
+
|
|
6
|
+
<UpdateSettings />
|
|
7
|
+
|
|
8
|
+
<div class="mt-6">
|
|
9
|
+
<DataManagementSettings />
|
|
10
|
+
</div>
|
|
@@ -1,121 +1,127 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import {
|
|
2
|
+
import { authStore } from '$frontend/lib/stores/features/auth.svelte';
|
|
3
|
+
import { systemSettings } from '$frontend/lib/stores/features/settings.svelte';
|
|
3
4
|
import { addNotification } from '$frontend/lib/stores/ui/notification.svelte';
|
|
4
5
|
import Icon from '../../common/Icon.svelte';
|
|
6
|
+
import Dialog from '../../common/Dialog.svelte';
|
|
5
7
|
import { debug } from '$shared/utils/logger';
|
|
6
8
|
|
|
9
|
+
const isNoAuth = $derived(systemSettings.authMode === 'none');
|
|
10
|
+
|
|
7
11
|
// State
|
|
8
12
|
let userNameInput = $state('');
|
|
9
13
|
let isEditing = $state(false);
|
|
10
14
|
let isSaving = $state(false);
|
|
11
15
|
|
|
16
|
+
// PAT state
|
|
17
|
+
let showPAT = $state(false);
|
|
18
|
+
let currentPAT = $state('');
|
|
19
|
+
let isRegeneratingPAT = $state(false);
|
|
20
|
+
let showRegenerateConfirm = $state(false);
|
|
21
|
+
|
|
22
|
+
const user = $derived(authStore.currentUser);
|
|
23
|
+
|
|
12
24
|
// Update input when user changes
|
|
13
25
|
$effect(() => {
|
|
14
|
-
if (
|
|
15
|
-
userNameInput =
|
|
26
|
+
if (user?.name) {
|
|
27
|
+
userNameInput = user.name;
|
|
16
28
|
}
|
|
17
29
|
});
|
|
18
30
|
|
|
19
|
-
// Handle save user name
|
|
20
31
|
async function saveUserName() {
|
|
21
32
|
if (!userNameInput.trim()) {
|
|
22
|
-
addNotification({
|
|
23
|
-
type: 'error',
|
|
24
|
-
title: 'Validation Error',
|
|
25
|
-
message: 'Name cannot be empty'
|
|
26
|
-
});
|
|
33
|
+
addNotification({ type: 'error', title: 'Validation Error', message: 'Name cannot be empty' });
|
|
27
34
|
return;
|
|
28
35
|
}
|
|
29
36
|
|
|
30
37
|
isSaving = true;
|
|
31
|
-
|
|
32
38
|
try {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
isEditing = false;
|
|
37
|
-
addNotification({
|
|
38
|
-
type: 'success',
|
|
39
|
-
title: 'Updated',
|
|
40
|
-
message: 'Display name updated successfully'
|
|
41
|
-
});
|
|
42
|
-
} else {
|
|
43
|
-
addNotification({
|
|
44
|
-
type: 'error',
|
|
45
|
-
title: 'Error',
|
|
46
|
-
message: 'Failed to update user name'
|
|
47
|
-
});
|
|
48
|
-
}
|
|
39
|
+
await authStore.updateName(userNameInput.trim());
|
|
40
|
+
isEditing = false;
|
|
41
|
+
addNotification({ type: 'success', title: 'Updated', message: 'Display name updated' });
|
|
49
42
|
} catch (error) {
|
|
50
43
|
debug.error('settings', 'Error updating user name:', error);
|
|
51
|
-
addNotification({
|
|
52
|
-
type: 'error',
|
|
53
|
-
title: 'Error',
|
|
54
|
-
message: 'An error occurred while updating user name'
|
|
55
|
-
});
|
|
44
|
+
addNotification({ type: 'error', title: 'Error', message: 'Failed to update display name' });
|
|
56
45
|
} finally {
|
|
57
46
|
isSaving = false;
|
|
58
47
|
}
|
|
59
48
|
}
|
|
60
49
|
|
|
61
|
-
// Handle cancel edit
|
|
62
50
|
function cancelEdit() {
|
|
63
|
-
userNameInput =
|
|
51
|
+
userNameInput = user?.name || '';
|
|
64
52
|
isEditing = false;
|
|
65
53
|
}
|
|
66
54
|
|
|
67
|
-
// Handle start edit
|
|
68
55
|
function startEdit() {
|
|
69
56
|
isEditing = true;
|
|
70
57
|
}
|
|
58
|
+
|
|
59
|
+
async function regeneratePAT() {
|
|
60
|
+
isRegeneratingPAT = true;
|
|
61
|
+
try {
|
|
62
|
+
const pat = await authStore.regeneratePAT();
|
|
63
|
+
currentPAT = pat;
|
|
64
|
+
showPAT = true;
|
|
65
|
+
showRegenerateConfirm = false;
|
|
66
|
+
addNotification({ type: 'success', title: 'Regenerated', message: 'Personal Access Token has been regenerated' });
|
|
67
|
+
} catch (error) {
|
|
68
|
+
debug.error('settings', 'Error regenerating PAT:', error);
|
|
69
|
+
addNotification({ type: 'error', title: 'Error', message: 'Failed to regenerate token' });
|
|
70
|
+
} finally {
|
|
71
|
+
isRegeneratingPAT = false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function copyPAT() {
|
|
76
|
+
navigator.clipboard.writeText(currentPAT);
|
|
77
|
+
addNotification({ type: 'success', title: 'Copied', message: 'Token copied to clipboard' });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function handleLogout() {
|
|
81
|
+
await authStore.logout();
|
|
82
|
+
}
|
|
71
83
|
</script>
|
|
72
84
|
|
|
73
85
|
<div class="py-1">
|
|
74
86
|
<h3 class="text-base font-bold text-slate-900 dark:text-slate-100 mb-1.5">User Profile</h3>
|
|
75
87
|
<p class="text-sm text-slate-600 dark:text-slate-500 mb-5">
|
|
76
|
-
Manage your identity and
|
|
88
|
+
Manage your identity and access
|
|
77
89
|
</p>
|
|
78
90
|
|
|
79
|
-
{#if !
|
|
91
|
+
{#if !user}
|
|
80
92
|
<div class="flex items-center justify-center gap-3 py-10 text-slate-600 dark:text-slate-500 text-sm">
|
|
81
|
-
<div
|
|
82
|
-
class="w-5 h-5 border-2 border-violet-500/20 border-t-violet-600 rounded-full animate-spin"
|
|
83
|
-
></div>
|
|
93
|
+
<div class="w-5 h-5 border-2 border-violet-500/20 border-t-violet-600 rounded-full animate-spin"></div>
|
|
84
94
|
<span>Loading user settings...</span>
|
|
85
95
|
</div>
|
|
86
96
|
{:else}
|
|
87
97
|
<div class="flex flex-col gap-4">
|
|
88
98
|
<!-- Current User Card -->
|
|
89
|
-
<div
|
|
90
|
-
class="flex items-center gap-3.5 p-4.5 bg-gradient-to-br from-violet-500/10 to-purple-500/5 dark:from-violet-500/10 dark:to-purple-500/8 border border-violet-500/20 rounded-xl"
|
|
91
|
-
>
|
|
99
|
+
<div class="flex items-center gap-3.5 p-4.5 bg-gradient-to-br from-violet-500/10 to-purple-500/5 dark:from-violet-500/10 dark:to-purple-500/8 border border-violet-500/20 rounded-xl">
|
|
92
100
|
<div
|
|
93
101
|
class="flex items-center justify-center w-12 h-12 rounded-xl text-lg font-bold text-white shrink-0"
|
|
94
|
-
style="background-color: {
|
|
102
|
+
style="background-color: {user.color || '#7c3aed'}"
|
|
95
103
|
>
|
|
96
|
-
{
|
|
104
|
+
{user.avatar || 'U'}
|
|
97
105
|
</div>
|
|
98
106
|
<div class="flex-1 min-w-0">
|
|
99
107
|
<div class="text-base font-semibold text-slate-900 dark:text-slate-100 mb-0.5">
|
|
100
|
-
{
|
|
108
|
+
{user.name}
|
|
109
|
+
</div>
|
|
110
|
+
<div class="text-xs text-slate-600 dark:text-slate-500">
|
|
111
|
+
{user.role === 'admin' ? 'Administrator' : 'Member'}
|
|
101
112
|
</div>
|
|
102
|
-
<div class="text-xs text-slate-600 dark:text-slate-500">Anonymous user identity</div>
|
|
103
113
|
</div>
|
|
104
|
-
<div
|
|
105
|
-
class="flex items-center gap-1.5 py-1.5 px-3 bg-emerald-500/15 rounded-full text-xs font-medium text-emerald-500"
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
114
|
+
<div class="flex items-center gap-2">
|
|
115
|
+
<span class="inline-flex items-center gap-1.5 py-1.5 px-3 bg-{user.role === 'admin' ? 'violet' : 'emerald'}-500/15 rounded-full text-xs font-medium text-{user.role === 'admin' ? 'violet' : 'emerald'}-500">
|
|
116
|
+
<Icon name="lucide:{user.role === 'admin' ? 'shield' : 'user'}" class="w-3 h-3" />
|
|
117
|
+
{user.role === 'admin' ? 'Admin' : 'Member'}
|
|
118
|
+
</span>
|
|
109
119
|
</div>
|
|
110
120
|
</div>
|
|
111
121
|
|
|
112
122
|
<!-- Edit Display Name -->
|
|
113
|
-
<div
|
|
114
|
-
class="
|
|
115
|
-
>
|
|
116
|
-
<div
|
|
117
|
-
class="flex items-center gap-2 text-sm font-semibold text-slate-500 mb-3"
|
|
118
|
-
>
|
|
123
|
+
<div class="p-4 bg-slate-100/80 dark:bg-slate-800/80 border border-slate-200 dark:border-slate-800 rounded-xl">
|
|
124
|
+
<div class="flex items-center gap-2 text-sm font-semibold text-slate-500 mb-3">
|
|
119
125
|
<Icon name="lucide:pencil" class="w-4 h-4 opacity-70" />
|
|
120
126
|
<span>Display Name</span>
|
|
121
127
|
</div>
|
|
@@ -136,9 +142,7 @@
|
|
|
136
142
|
disabled={!userNameInput.trim() || isSaving}
|
|
137
143
|
>
|
|
138
144
|
{#if isSaving}
|
|
139
|
-
<div
|
|
140
|
-
class="w-3.5 h-3.5 border-2 border-white/30 border-t-white rounded-full animate-spin"
|
|
141
|
-
></div>
|
|
145
|
+
<div class="w-3.5 h-3.5 border-2 border-white/30 border-t-white rounded-full animate-spin"></div>
|
|
142
146
|
Saving...
|
|
143
147
|
{:else}
|
|
144
148
|
<Icon name="lucide:check" class="w-4 h-4" />
|
|
@@ -158,7 +162,7 @@
|
|
|
158
162
|
{:else}
|
|
159
163
|
<div class="flex items-center justify-between gap-3">
|
|
160
164
|
<div class="text-sm text-slate-900 dark:text-slate-100">
|
|
161
|
-
{
|
|
165
|
+
{user.name}
|
|
162
166
|
</div>
|
|
163
167
|
<button
|
|
164
168
|
type="button"
|
|
@@ -172,26 +176,95 @@
|
|
|
172
176
|
{/if}
|
|
173
177
|
</div>
|
|
174
178
|
|
|
179
|
+
{#if !isNoAuth}
|
|
180
|
+
<!-- Personal Access Token -->
|
|
181
|
+
<div class="p-4 bg-slate-100/80 dark:bg-slate-800/80 border border-slate-200 dark:border-slate-800 rounded-xl">
|
|
182
|
+
<div class="flex items-center gap-2 text-sm font-semibold text-slate-500 mb-3">
|
|
183
|
+
<Icon name="lucide:key-round" class="w-4 h-4 opacity-70" />
|
|
184
|
+
<span>Personal Access Token</span>
|
|
185
|
+
</div>
|
|
186
|
+
<p class="text-xs text-slate-600 dark:text-slate-500 mb-3">
|
|
187
|
+
Use this token to log in from other devices. Keep it secret.
|
|
188
|
+
</p>
|
|
189
|
+
|
|
190
|
+
{#if showPAT && currentPAT}
|
|
191
|
+
<div class="flex flex-col gap-2 mb-3">
|
|
192
|
+
<div class="flex items-center gap-2">
|
|
193
|
+
<code class="flex-1 py-2.5 px-3.5 bg-slate-50 dark:bg-slate-900/80 border border-emerald-500/30 rounded-lg font-mono text-xs text-slate-700 dark:text-slate-300 break-all">
|
|
194
|
+
{currentPAT}
|
|
195
|
+
</code>
|
|
196
|
+
<button
|
|
197
|
+
type="button"
|
|
198
|
+
onclick={copyPAT}
|
|
199
|
+
class="flex items-center justify-center w-9 h-9 rounded-lg bg-emerald-500/10 hover:bg-emerald-500/20 text-emerald-600 dark:text-emerald-400 transition-all"
|
|
200
|
+
title="Copy token"
|
|
201
|
+
>
|
|
202
|
+
<Icon name="lucide:copy" class="w-4 h-4" />
|
|
203
|
+
</button>
|
|
204
|
+
</div>
|
|
205
|
+
<div class="flex items-center gap-1.5 text-xs text-amber-600 dark:text-amber-400">
|
|
206
|
+
<Icon name="lucide:triangle-alert" class="w-3.5 h-3.5" />
|
|
207
|
+
<span>This token is shown only once. Copy and store it securely.</span>
|
|
208
|
+
</div>
|
|
209
|
+
</div>
|
|
210
|
+
{/if}
|
|
211
|
+
|
|
212
|
+
<button
|
|
213
|
+
type="button"
|
|
214
|
+
onclick={() => { showRegenerateConfirm = true; }}
|
|
215
|
+
disabled={isRegeneratingPAT}
|
|
216
|
+
class="inline-flex items-center gap-1.5 py-2 px-3.5 bg-amber-500/10 border border-amber-500/20 rounded-lg text-amber-600 dark:text-amber-400 text-xs font-semibold cursor-pointer transition-all duration-150 hover:bg-amber-500/20 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
217
|
+
>
|
|
218
|
+
{#if isRegeneratingPAT}
|
|
219
|
+
<div class="w-3.5 h-3.5 border-2 border-amber-600/30 border-t-amber-600 rounded-full animate-spin"></div>
|
|
220
|
+
Regenerating...
|
|
221
|
+
{:else}
|
|
222
|
+
<Icon name="lucide:refresh-cw" class="w-3.5 h-3.5" />
|
|
223
|
+
Regenerate Token
|
|
224
|
+
{/if}
|
|
225
|
+
</button>
|
|
226
|
+
</div>
|
|
227
|
+
{/if}
|
|
228
|
+
|
|
175
229
|
<!-- User ID -->
|
|
176
|
-
<div
|
|
177
|
-
class="
|
|
178
|
-
>
|
|
179
|
-
<div
|
|
180
|
-
class="flex items-center gap-2 text-sm font-semibold text-slate-500 mb-3"
|
|
181
|
-
>
|
|
230
|
+
<div class="p-4 bg-slate-100/80 dark:bg-slate-800/80 border border-slate-200 dark:border-slate-800 rounded-xl">
|
|
231
|
+
<div class="flex items-center gap-2 text-sm font-semibold text-slate-500 mb-3">
|
|
182
232
|
<Icon name="lucide:fingerprint" class="w-4 h-4 opacity-70" />
|
|
183
233
|
<span>User ID</span>
|
|
184
234
|
</div>
|
|
185
235
|
<div class="flex flex-col gap-1.5">
|
|
186
|
-
<code
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
>
|
|
190
|
-
<span class="text-xs text-slate-600 dark:text-slate-500"
|
|
191
|
-
>Unique identifier for this session</span
|
|
192
|
-
>
|
|
236
|
+
<code class="py-2.5 px-3.5 bg-slate-50 dark:bg-slate-900/80 border border-slate-200 dark:border-slate-800 rounded-lg font-mono text-xs text-slate-500 break-all">
|
|
237
|
+
{user.id}
|
|
238
|
+
</code>
|
|
193
239
|
</div>
|
|
194
240
|
</div>
|
|
241
|
+
|
|
242
|
+
{#if !isNoAuth}
|
|
243
|
+
<!-- Logout -->
|
|
244
|
+
<div class="pt-2">
|
|
245
|
+
<button
|
|
246
|
+
type="button"
|
|
247
|
+
onclick={handleLogout}
|
|
248
|
+
class="inline-flex items-center gap-1.5 py-2.5 px-4 bg-red-500/10 border border-red-500/20 rounded-lg text-red-600 dark:text-red-400 text-sm font-semibold cursor-pointer transition-all duration-150 hover:bg-red-500/20 hover:border-red-600/40"
|
|
249
|
+
>
|
|
250
|
+
<Icon name="lucide:log-out" class="w-4 h-4" />
|
|
251
|
+
Sign Out
|
|
252
|
+
</button>
|
|
253
|
+
</div>
|
|
254
|
+
{/if}
|
|
195
255
|
</div>
|
|
196
256
|
{/if}
|
|
197
257
|
</div>
|
|
258
|
+
|
|
259
|
+
<!-- Regenerate PAT Confirmation Dialog -->
|
|
260
|
+
<Dialog
|
|
261
|
+
bind:isOpen={showRegenerateConfirm}
|
|
262
|
+
onClose={() => { showRegenerateConfirm = false; }}
|
|
263
|
+
title="Regenerate Access Token"
|
|
264
|
+
type="warning"
|
|
265
|
+
message="This will invalidate your current Personal Access Token. Any devices using the old token will need to log in again with the new one. Continue?"
|
|
266
|
+
confirmText="Regenerate"
|
|
267
|
+
cancelText="Cancel"
|
|
268
|
+
showCancel={true}
|
|
269
|
+
onConfirm={regeneratePAT}
|
|
270
|
+
/>
|