@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.
Files changed (71) hide show
  1. package/README.md +23 -1
  2. package/backend/index.ts +20 -0
  3. package/backend/lib/auth/auth-service.ts +484 -0
  4. package/backend/lib/auth/index.ts +4 -0
  5. package/backend/lib/auth/permissions.ts +63 -0
  6. package/backend/lib/auth/rate-limiter.ts +145 -0
  7. package/backend/lib/auth/tokens.ts +53 -0
  8. package/backend/lib/database/migrations/024_create_users_table.ts +29 -0
  9. package/backend/lib/database/migrations/025_create_auth_sessions_table.ts +38 -0
  10. package/backend/lib/database/migrations/026_create_invite_tokens_table.ts +31 -0
  11. package/backend/lib/database/migrations/index.ts +21 -0
  12. package/backend/lib/database/queries/auth-queries.ts +201 -0
  13. package/backend/lib/database/queries/index.ts +2 -1
  14. package/backend/lib/engine/adapters/opencode/server.ts +1 -1
  15. package/backend/lib/mcp/config.ts +13 -18
  16. package/backend/lib/mcp/index.ts +9 -0
  17. package/backend/lib/mcp/remote-server.ts +132 -0
  18. package/backend/lib/mcp/servers/helper.ts +49 -3
  19. package/backend/lib/mcp/servers/index.ts +3 -2
  20. package/backend/lib/preview/browser/browser-audio-capture.ts +20 -3
  21. package/backend/lib/preview/browser/browser-navigation-tracker.ts +3 -0
  22. package/backend/lib/preview/browser/browser-pool.ts +73 -176
  23. package/backend/lib/preview/browser/browser-preview-service.ts +3 -2
  24. package/backend/lib/preview/browser/browser-tab-manager.ts +261 -23
  25. package/backend/lib/preview/browser/browser-video-capture.ts +36 -1
  26. package/backend/lib/utils/ws.ts +65 -1
  27. package/backend/ws/auth/index.ts +17 -0
  28. package/backend/ws/auth/invites.ts +84 -0
  29. package/backend/ws/auth/login.ts +269 -0
  30. package/backend/ws/auth/status.ts +41 -0
  31. package/backend/ws/auth/users.ts +32 -0
  32. package/backend/ws/engine/claude/accounts.ts +3 -1
  33. package/backend/ws/engine/utils.ts +38 -6
  34. package/backend/ws/index.ts +4 -4
  35. package/backend/ws/preview/browser/interact.ts +27 -5
  36. package/bin/clopen.ts +39 -0
  37. package/bun.lock +113 -51
  38. package/frontend/App.svelte +47 -29
  39. package/frontend/lib/components/auth/InvitePage.svelte +215 -0
  40. package/frontend/lib/components/auth/LoginPage.svelte +129 -0
  41. package/frontend/lib/components/auth/SetupPage.svelte +1022 -0
  42. package/frontend/lib/components/common/FolderBrowser.svelte +9 -9
  43. package/frontend/lib/components/common/UpdateBanner.svelte +2 -2
  44. package/frontend/lib/components/preview/browser/BrowserPreview.svelte +1 -1
  45. package/frontend/lib/components/preview/browser/core/mcp-handlers.svelte.ts +12 -4
  46. package/frontend/lib/components/settings/SettingsModal.svelte +50 -15
  47. package/frontend/lib/components/settings/SettingsView.svelte +21 -7
  48. package/frontend/lib/components/settings/account/AccountSettings.svelte +5 -0
  49. package/frontend/lib/components/settings/admin/InviteManagement.svelte +239 -0
  50. package/frontend/lib/components/settings/admin/UserManagement.svelte +127 -0
  51. package/frontend/lib/components/settings/general/AdvancedSettings.svelte +10 -4
  52. package/frontend/lib/components/settings/general/AuthModeSettings.svelte +229 -0
  53. package/frontend/lib/components/settings/general/GeneralSettings.svelte +6 -1
  54. package/frontend/lib/components/settings/general/UpdateSettings.svelte +5 -5
  55. package/frontend/lib/components/settings/security/SecuritySettings.svelte +10 -0
  56. package/frontend/lib/components/settings/system/SystemSettings.svelte +10 -0
  57. package/frontend/lib/components/settings/user/UserSettings.svelte +147 -74
  58. package/frontend/lib/components/workspace/WorkspaceLayout.svelte +5 -10
  59. package/frontend/lib/services/preview/browser/browser-webcodecs.service.ts +31 -8
  60. package/frontend/lib/stores/features/auth.svelte.ts +296 -0
  61. package/frontend/lib/stores/features/settings.svelte.ts +53 -9
  62. package/frontend/lib/stores/features/user.svelte.ts +26 -68
  63. package/frontend/lib/stores/ui/settings-modal.svelte.ts +42 -21
  64. package/frontend/lib/stores/ui/update.svelte.ts +2 -2
  65. package/package.json +8 -6
  66. package/shared/types/stores/settings.ts +16 -2
  67. package/shared/utils/logger.ts +1 -0
  68. package/shared/utils/ws-client.ts +30 -13
  69. package/shared/utils/ws-server.ts +42 -4
  70. package/backend/lib/mcp/stdio-server.ts +0 -103
  71. package/backend/ws/mcp/index.ts +0 -61
@@ -1,9 +1,13 @@
1
1
  <script lang="ts">
2
- import { settings, updateSettings } from '$frontend/lib/stores/features/settings.svelte';
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
- updateSettings({ allowedBasePaths: [...settings.allowedBasePaths, path] });
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
- updateSettings({ allowedBasePaths: newPaths });
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
- updateSettings({ allowedBasePaths: newPaths });
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
- <UpdateSettings />
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 { settings, updateSettings } from '$frontend/lib/stores/features/settings.svelte';
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
- updateSettings({ autoUpdate: !settings.autoUpdate });
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={settings.autoUpdate}
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
- {settings.autoUpdate ? 'bg-violet-600' : 'bg-slate-300 dark:bg-slate-600'}"
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
- {settings.autoUpdate ? 'translate-x-5' : 'translate-x-0'}"
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 AuthModeSettings from '../general/AuthModeSettings.svelte';
3
+ import AdvancedSettings from '../general/AdvancedSettings.svelte';
4
+ </script>
5
+
6
+ <AuthModeSettings />
7
+
8
+ <div class="mt-6">
9
+ <AdvancedSettings />
10
+ </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 { userStore } from '$frontend/lib/stores/features/user.svelte';
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 (userStore.currentUser?.name) {
15
- userNameInput = userStore.currentUser.name;
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
- const success = await userStore.updateName(userNameInput.trim());
34
-
35
- if (success) {
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 = userStore.currentUser?.name || '';
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 display preferences
88
+ Manage your identity and access
77
89
  </p>
78
90
 
79
- {#if !userStore.currentUser}
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: {userStore.currentUser?.color || '#7c3aed'}"
102
+ style="background-color: {user.color || '#7c3aed'}"
95
103
  >
96
- {userStore.currentUser?.avatar || 'U'}
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
- {userStore.currentUser?.name || 'Anonymous User'}
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
- <span class="w-1.5 h-1.5 bg-emerald-500 rounded-full animate-pulse"></span>
108
- <span>Active</span>
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="p-4 bg-slate-100/80 dark:bg-slate-800/80 border border-slate-200 dark:border-slate-800 rounded-xl"
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
- {userStore.currentUser?.name || 'Not set'}
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="p-4 bg-slate-100/80 dark:bg-slate-800/80 border border-slate-200 dark:border-slate-800 rounded-xl"
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
- 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"
188
- >{userStore.currentUser?.id || 'Not available'}</code
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
+ />