clay-server 5.1.0-beta.1 → 5.1.0-beta.3
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/LICENSES/README.md +4 -0
- package/LICENSES/dicebear-avatars.md +16 -0
- package/bin/cli.js +9 -91
- package/lib/builtin-mates.js +7 -7
- package/lib/daemon-project-access.js +7 -1
- package/lib/daemon-settings.js +11 -0
- package/lib/daemon.js +10 -37
- package/lib/dicebear-avatar.js +62 -0
- package/lib/knowledge-db-service.js +175 -0
- package/lib/knowledge-db-validation.js +0 -0
- package/lib/knowledge-document-index.js +49 -0
- package/lib/knowledge-draft-store.js +56 -0
- package/lib/knowledge-file-store.js +199 -0
- package/lib/knowledge-markdown.js +128 -0
- package/lib/linear-store.js +35 -8
- package/lib/mate-identity-file.js +39 -0
- package/lib/mate-knowledge-mcp-server.js +29 -6
- package/lib/mate-knowledge-service.js +23 -2
- package/lib/mate-knowledge-sync.js +37 -22
- package/lib/mate-ready-creation.js +2 -1
- package/lib/mate-workspace-color.js +4 -0
- package/lib/mates-identity.js +4 -4
- package/lib/mates-prompts.js +4 -3
- package/lib/mates.js +30 -11
- package/lib/os-users.js +5 -16
- package/lib/project-connection.js +36 -9
- package/lib/project-debate-proposal.js +6 -0
- package/lib/project-debate-workspace.js +43 -0
- package/lib/project-debate.js +4 -2
- package/lib/project-filesystem.js +21 -39
- package/lib/project-knowledge.js +315 -136
- package/lib/project-mate-creation-proposal.js +1 -0
- package/lib/project-mate-interaction.js +7 -6
- package/lib/project-server-settings.js +31 -0
- package/lib/project-sessions.js +92 -182
- package/lib/project-user-knowledge.js +60 -0
- package/lib/project.js +62 -21
- package/lib/public/app.js +79 -71
- package/lib/public/clay-studio-wordmark-bold.svg +20 -0
- package/lib/public/css/clay-workspace.css +105 -0
- package/lib/public/css/debates-workbench.css +47 -0
- package/lib/public/css/home-debate-live.css +26 -0
- package/lib/public/css/home-hub.css +3 -1
- package/lib/public/css/home-mate-model-picker.css +15 -131
- package/lib/public/css/home-mate-settings.css +168 -16
- package/lib/public/css/icon-strip.css +249 -32
- package/lib/public/css/input.css +3 -1
- package/lib/public/css/mate-conversation.css +176 -0
- package/lib/public/css/mate-knowledge-workbench.css +118 -0
- package/lib/public/css/mate-workspace-color.css +16 -0
- package/lib/public/css/mates.css +28 -28
- package/lib/public/css/menus.css +3 -0
- package/lib/public/css/messages.css +1 -114
- package/lib/public/css/mobile-nav.css +16 -16
- package/lib/public/css/overlays.css +15 -0
- package/lib/public/css/project-logs.css +7 -0
- package/lib/public/css/project-mate-creation.css +105 -0
- package/lib/public/css/project-settings-modal.css +56 -0
- package/lib/public/css/right-workbench.css +3 -3
- package/lib/public/css/server-settings-system.css +125 -0
- package/lib/public/css/session-folders.css +80 -21
- package/lib/public/css/session-github.css +25 -16
- package/lib/public/css/settings-ui.css +247 -22
- package/lib/public/css/shared-browser.css +7 -3
- package/lib/public/css/sidebar.css +183 -107
- package/lib/public/css/title-bar.css +156 -1
- package/lib/public/css/worker-delegation.css +47 -0
- package/lib/public/css/worker-proposal.css +72 -150
- package/lib/public/css/you.css +74 -0
- package/lib/public/index.html +206 -548
- package/lib/public/modules/account-island.js +232 -0
- package/lib/public/modules/admin-settings-email.js +197 -0
- package/lib/public/modules/admin-settings-invitations.js +183 -0
- package/lib/public/modules/admin-settings-projects.js +198 -0
- package/lib/public/modules/admin-settings-ui.js +100 -0
- package/lib/public/modules/admin-settings-users.js +331 -0
- package/lib/public/modules/admin.js +19 -949
- package/lib/public/modules/app-cursors.js +19 -43
- package/lib/public/modules/app-debate-ui.js +4 -3
- package/lib/public/modules/app-dm.js +0 -2
- package/lib/public/modules/app-header.js +106 -0
- package/lib/public/modules/app-message-router.js +12 -3
- package/lib/public/modules/app-messages.js +47 -61
- package/lib/public/modules/app-notifications.js +4 -7
- package/lib/public/modules/app-projects.js +5 -38
- package/lib/public/modules/app-rate-limit.js +70 -12
- package/lib/public/modules/app-rendering.js +12 -13
- package/lib/public/modules/avatar.js +30 -9
- package/lib/public/modules/chat-render-runtime.js +16 -4
- package/lib/public/modules/command-palette.js +2 -15
- package/lib/public/modules/context-sources.js +27 -18
- package/lib/public/modules/cursor-sharing-preference.js +3 -3
- package/lib/public/modules/debate-controls.js +299 -0
- package/lib/public/modules/debate.js +2 -0
- package/lib/public/modules/debates-workbench.js +339 -0
- package/lib/public/modules/default-ai.js +88 -65
- package/lib/public/modules/header-tui-font.js +10 -20
- package/lib/public/modules/home-debate-controls.js +18 -289
- package/lib/public/modules/home-mate-avatar-editor.js +228 -0
- package/lib/public/modules/home-mate-color-picker.js +66 -0
- package/lib/public/modules/home-mate-creation.js +1 -1
- package/lib/public/modules/home-mate-instructions.js +90 -0
- package/lib/public/modules/home-mate-model-picker.js +80 -271
- package/lib/public/modules/home-mate-profile-editor.js +72 -0
- package/lib/public/modules/home-mate-settings.js +118 -125
- package/lib/public/modules/home-shell.js +3 -0
- package/lib/public/modules/home-surface-boot.js +1 -9
- package/lib/public/modules/icon-strip-tooltip.js +95 -0
- package/lib/public/modules/knowledge-db-edit-state.js +35 -0
- package/lib/public/modules/knowledge-db-workbench.js +234 -0
- package/lib/public/modules/knowledge-history-state.js +55 -0
- package/lib/public/modules/knowledge-markdown-editor.js +57 -0
- package/lib/public/modules/knowledge-preview-links.js +70 -0
- package/lib/public/modules/knowledge-workbench-shell.js +21 -0
- package/lib/public/modules/linear-settings.js +3 -2
- package/lib/public/modules/markdown.js +5 -6
- package/lib/public/modules/mate-knowledge-workbench-state.js +174 -0
- package/lib/public/modules/mate-knowledge-workbench.js +500 -0
- package/lib/public/modules/mate-knowledge.js +11 -3
- package/lib/public/modules/mate-management.js +54 -10
- package/lib/public/modules/mate-sidebar.js +11 -1
- package/lib/public/modules/mate-workspace-color.js +33 -0
- package/lib/public/modules/mention.js +5 -13
- package/lib/public/modules/notification-settings.js +81 -0
- package/lib/public/modules/notifications.js +44 -28
- package/lib/public/modules/personal-settings-dialog.js +51 -0
- package/lib/public/modules/profile.js +66 -24
- package/lib/public/modules/project-chat-identity.js +79 -0
- package/lib/public/modules/project-debate-planning-workspace.js +250 -0
- package/lib/public/modules/project-debate-workspace.js +185 -0
- package/lib/public/modules/project-header-identity.js +127 -0
- package/lib/public/modules/project-logs-render.js +9 -4
- package/lib/public/modules/project-mate-creation-workspace.js +414 -0
- package/lib/public/modules/project-mate-navigation.js +238 -0
- package/lib/public/modules/project-session-navigation.js +29 -0
- package/lib/public/modules/project-settings-environment.js +106 -0
- package/lib/public/modules/project-settings.js +72 -628
- package/lib/public/modules/project-switcher.js +6 -8
- package/lib/public/modules/projects-hub-view.js +5 -6
- package/lib/public/modules/projects-hub.js +2 -6
- package/lib/public/modules/qrcode.js +27 -8
- package/lib/public/modules/search-clay-chat.js +8 -11
- package/lib/public/modules/server-settings-controls.js +316 -0
- package/lib/public/modules/server-settings-environment.js +194 -0
- package/lib/public/modules/server-settings-navigation.js +39 -0
- package/lib/public/modules/server-settings-state.js +54 -0
- package/lib/public/modules/server-settings.js +116 -788
- package/lib/public/modules/server-storage-settings.js +84 -0
- package/lib/public/modules/session-activity-tooltip.js +12 -0
- package/lib/public/modules/session-create-form.js +86 -14
- package/lib/public/modules/session-create.js +50 -7
- package/lib/public/modules/session-github.js +2 -1
- package/lib/public/modules/session-list-visibility.js +41 -0
- package/lib/public/modules/session-presence-ui.js +3 -0
- package/lib/public/modules/session-title-marquee.js +51 -0
- package/lib/public/modules/settings-env-values.js +34 -0
- package/lib/public/modules/settings-select.js +121 -0
- package/lib/public/modules/shared-browser.js +22 -12
- package/lib/public/modules/sidebar-mates.js +3 -50
- package/lib/public/modules/sidebar-mobile.js +63 -60
- package/lib/public/modules/sidebar-projects.js +84 -97
- package/lib/public/modules/sidebar-session-folders-ui.js +2 -2
- package/lib/public/modules/sidebar-session-hierarchy.js +149 -70
- package/lib/public/modules/sidebar-sessions.js +49 -50
- package/lib/public/modules/sidebar.js +11 -13
- package/lib/public/modules/stt-picker.js +14 -5
- package/lib/public/modules/terminal-prefs.js +22 -0
- package/lib/public/modules/theme.js +88 -52
- package/lib/public/modules/thinking-lifecycle.js +7 -6
- package/lib/public/modules/tool-palette-order.js +4 -3
- package/lib/public/modules/tool-palette.js +13 -3
- package/lib/public/modules/tools.js +22 -22
- package/lib/public/modules/user-settings-account.js +318 -0
- package/lib/public/modules/user-settings-appearance.js +55 -0
- package/lib/public/modules/user-settings-editors.js +24 -0
- package/lib/public/modules/user-settings-integrations.js +104 -0
- package/lib/public/modules/user-settings-layout.js +80 -0
- package/lib/public/modules/user-settings-preferences.js +48 -0
- package/lib/public/modules/user-settings-profile.js +53 -0
- package/lib/public/modules/user-settings-themes.js +38 -0
- package/lib/public/modules/user-settings-voice.js +75 -0
- package/lib/public/modules/user-settings.js +53 -480
- package/lib/public/modules/worker-proposal.js +42 -8
- package/lib/public/modules/worker-runtime-picker.js +115 -0
- package/lib/public/modules/you-activity.js +121 -0
- package/lib/public/modules/you.js +251 -0
- package/lib/public/style.css +20 -8
- package/lib/public/vendor/codemirror/LICENSE +21 -0
- package/lib/public/vendor/codemirror/codemirror.css +344 -0
- package/lib/public/vendor/codemirror/codemirror.js +9884 -0
- package/lib/public/vendor/codemirror/continuelist.js +101 -0
- package/lib/public/vendor/codemirror/markdown.js +886 -0
- package/lib/public/vendor/codemirror/show-hint.css +37 -0
- package/lib/public/vendor/codemirror/show-hint.js +523 -0
- package/lib/sdk-bridge.js +15 -3
- package/lib/sdk-message-processor.js +16 -4
- package/lib/sdk-quota-snapshot.js +37 -0
- package/lib/server-admin.js +11 -0
- package/lib/server-appearance-settings.js +71 -0
- package/lib/server-home-chat-events.js +1 -1
- package/lib/server-home-chat.js +25 -3
- package/lib/server-home-debate-planning.js +4 -1
- package/lib/server-home-debates.js +2 -0
- package/lib/server-home-mate-creation.js +4 -1
- package/lib/server-linear.js +14 -8
- package/lib/server-mate-instructions.js +44 -0
- package/lib/server-mates.js +14 -4
- package/lib/server-notification-settings.js +67 -0
- package/lib/server-palette.js +1 -1
- package/lib/server-settings.js +69 -1
- package/lib/server-storage.js +57 -0
- package/lib/server-user-knowledge.js +119 -0
- package/lib/server.js +31 -3
- package/lib/session-visibility.js +7 -2
- package/lib/sessions.js +35 -10
- package/lib/startup-projects.js +40 -0
- package/lib/storage-native-walk.js +85 -0
- package/lib/storage-usage.js +144 -0
- package/lib/user-knowledge-service.js +167 -0
- package/lib/users-permissions.js +7 -0
- package/lib/users-preferences.js +0 -74
- package/lib/users-project-mate-filter-preferences.js +39 -0
- package/lib/users.js +6 -4
- package/lib/workspace-query-access.js +4 -1
- package/lib/ws-schema.js +52 -4
- package/lib/yoke/acp-process-manager.js +2 -2
- package/lib/yoke/adapters/codex.js +12 -52
- package/lib/yoke/codex-app-server.js +2 -2
- package/lib/yoke/codex-model-catalog.js +52 -0
- package/lib/yoke/codex-rate-limits.js +26 -0
- package/package.json +12 -3
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { store } from './store.js';
|
|
2
|
+
import { iconHtml, refreshIcons } from './icons.js';
|
|
3
|
+
import { showToast, copyToClipboard, escapeHtml } from './utils.js';
|
|
4
|
+
import { apiGet, apiPost, apiPut, apiDelete, showConfirmDialog, showConfirmResetPin, prepareAdminModal } from './admin-settings-ui.js';
|
|
5
|
+
// --- Projects ---
|
|
6
|
+
export function loadProjectsTab(body) {
|
|
7
|
+
var projectList = store.get('projectsHubList') || [];
|
|
8
|
+
// Exclude worktree and mate projects (mates are always private to their owner)
|
|
9
|
+
projectList = projectList.filter(function (p) { return !p.isWorktree && !p.isMate; });
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
if (projectList.length === 0) {
|
|
13
|
+
body.innerHTML = '<div class="admin-empty">No projects registered.</div>';
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
var accessPromises = projectList.map(function (p) {
|
|
18
|
+
return apiGet("/api/admin/projects/" + p.slug + "/access").then(function (access) {
|
|
19
|
+
return { slug: p.slug, title: p.title || p.project || p.slug, projectOwnerId: p.projectOwnerId || null, visibility: access.visibility || "public", allowedUsers: access.allowedUsers || [] };
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
Promise.all([
|
|
24
|
+
Promise.all(accessPromises),
|
|
25
|
+
apiGet("/api/admin/users"),
|
|
26
|
+
]).then(function (results) {
|
|
27
|
+
renderProjectsTab(body, results[0], results[1].users || []);
|
|
28
|
+
}).catch(function (error) { body.textContent = error.message || "Project access unavailable. Reopen Settings to retry."; });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function renderProjectsTab(body, projectAccessList, allUsers) {
|
|
32
|
+
var html = '<div class="admin-project-list">';
|
|
33
|
+
for (var i = 0; i < projectAccessList.length; i++) {
|
|
34
|
+
var p = projectAccessList[i];
|
|
35
|
+
var visClass = p.visibility === "private" ? "admin-vis-private" : "admin-vis-public";
|
|
36
|
+
html += '<div class="admin-project-item" data-slug="' + escapeHtml(p.slug) + '">';
|
|
37
|
+
html += '<div class="admin-project-info">';
|
|
38
|
+
html += '<div class="admin-project-name">' + escapeHtml(p.title) + '</div>';
|
|
39
|
+
html += '<div class="admin-project-slug">' + escapeHtml(p.slug) + '</div>';
|
|
40
|
+
html += '</div>';
|
|
41
|
+
html += '<div class="admin-project-controls">';
|
|
42
|
+
// Owner select
|
|
43
|
+
html += '<select aria-label="Project owner" class="admin-owner-select" data-slug="' + escapeHtml(p.slug) + '">';
|
|
44
|
+
html += '<option value=""' + (!p.projectOwnerId ? ' selected' : '') + '>No owner</option>';
|
|
45
|
+
for (var u = 0; u < allUsers.length; u++) {
|
|
46
|
+
var user = allUsers[u];
|
|
47
|
+
var sel = p.projectOwnerId === user.id ? ' selected' : '';
|
|
48
|
+
var label = escapeHtml(user.displayName || user.username);
|
|
49
|
+
if (user.linuxUser) label += ' (' + escapeHtml(user.linuxUser) + ')';
|
|
50
|
+
html += '<option value="' + escapeHtml(user.id) + '"' + sel + '>' + label + '</option>';
|
|
51
|
+
}
|
|
52
|
+
html += '</select>';
|
|
53
|
+
// Visibility select
|
|
54
|
+
html += '<select aria-label="Project visibility" class="admin-vis-select ' + visClass + '" data-slug="' + escapeHtml(p.slug) + '">';
|
|
55
|
+
html += '<option value="public"' + (p.visibility === "public" ? " selected" : "") + '>Public</option>';
|
|
56
|
+
html += '<option value="private"' + (p.visibility === "private" ? " selected" : "") + '>Private</option>';
|
|
57
|
+
html += '</select>';
|
|
58
|
+
if (p.visibility === "private") {
|
|
59
|
+
html += '<button aria-label="Manage allowed users" class="admin-manage-users-btn" data-slug="' + escapeHtml(p.slug) + '">' + iconHtml("users") + '</button>';
|
|
60
|
+
}
|
|
61
|
+
html += '</div>';
|
|
62
|
+
html += '</div>';
|
|
63
|
+
}
|
|
64
|
+
html += '</div>';
|
|
65
|
+
|
|
66
|
+
body.innerHTML = html;
|
|
67
|
+
refreshIcons(body);
|
|
68
|
+
|
|
69
|
+
// Bind owner selects
|
|
70
|
+
var ownerSelects = body.querySelectorAll(".admin-owner-select");
|
|
71
|
+
for (var oi = 0; oi < ownerSelects.length; oi++) {
|
|
72
|
+
ownerSelects[oi].dataset.savedValue = ownerSelects[oi].value;
|
|
73
|
+
ownerSelects[oi].addEventListener("change", function () {
|
|
74
|
+
var slug = this.dataset.slug;
|
|
75
|
+
var userId = this.value;
|
|
76
|
+
this.value = this.dataset.savedValue; this.dispatchEvent(new Event("settings-sync"));
|
|
77
|
+
showConfirmDialog("Transfer project ownership? New processes will use the selected owner’s identity where OS-user mode is enabled.", "Transfer ownership", function () { setProjectOwner(slug, userId, body); });
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Bind visibility selects
|
|
82
|
+
var visSelects = body.querySelectorAll(".admin-vis-select");
|
|
83
|
+
for (var j = 0; j < visSelects.length; j++) {
|
|
84
|
+
visSelects[j].dataset.savedValue = visSelects[j].value;
|
|
85
|
+
visSelects[j].addEventListener("change", function () {
|
|
86
|
+
var slug = this.dataset.slug;
|
|
87
|
+
var visibility = this.value;
|
|
88
|
+
this.value = this.dataset.savedValue; this.dispatchEvent(new Event("settings-sync"));
|
|
89
|
+
showConfirmDialog("Change project visibility to " + visibility + "? This changes who can access this project.", "Change visibility", function () { setProjectVisibility(slug, visibility, body); });
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Bind manage users buttons
|
|
94
|
+
var manageUserBtns = body.querySelectorAll(".admin-manage-users-btn");
|
|
95
|
+
for (var k = 0; k < manageUserBtns.length; k++) {
|
|
96
|
+
manageUserBtns[k].addEventListener("click", function () {
|
|
97
|
+
var slug = this.dataset.slug;
|
|
98
|
+
showProjectUsersModal(slug, body);
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function setProjectOwner(slug, userId, body) {
|
|
104
|
+
apiPut("/api/admin/projects/" + slug + "/owner", { userId: userId || null }).then(function (data) {
|
|
105
|
+
if (data.ok) {
|
|
106
|
+
showToast("Project owner updated");
|
|
107
|
+
loadProjectsTab(body);
|
|
108
|
+
} else {
|
|
109
|
+
showToast(data.error || "Failed to update owner", "error");
|
|
110
|
+
}
|
|
111
|
+
}).catch(function () {
|
|
112
|
+
showToast("Failed to update owner", "error");
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function setProjectVisibility(slug, visibility, body) {
|
|
117
|
+
apiPut("/api/admin/projects/" + slug + "/visibility", { visibility: visibility }).then(function (data) {
|
|
118
|
+
if (data.ok) {
|
|
119
|
+
showToast("Visibility updated");
|
|
120
|
+
loadProjectsTab(body);
|
|
121
|
+
} else {
|
|
122
|
+
showToast(data.error || "Failed to update visibility");
|
|
123
|
+
}
|
|
124
|
+
}).catch(function () {
|
|
125
|
+
showToast("Failed to update visibility");
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function showProjectUsersModal(slug, parentBody) {
|
|
130
|
+
Promise.all([
|
|
131
|
+
apiGet("/api/admin/users"),
|
|
132
|
+
apiGet("/api/admin/projects/" + slug + "/access"),
|
|
133
|
+
]).then(function (results) {
|
|
134
|
+
var allUsers = results[0].users || [];
|
|
135
|
+
var access = results[1];
|
|
136
|
+
var allowed = access.allowedUsers || [];
|
|
137
|
+
|
|
138
|
+
var modal = document.createElement("div");
|
|
139
|
+
modal.className = "admin-modal-overlay";
|
|
140
|
+
|
|
141
|
+
var html = '<div class="admin-modal">';
|
|
142
|
+
html += '<div class="admin-modal-header">';
|
|
143
|
+
html += '<h3>Manage Access: ' + escapeHtml(slug) + '</h3>';
|
|
144
|
+
html += '<button class="admin-modal-close">' + iconHtml("x") + '</button>';
|
|
145
|
+
html += '</div>';
|
|
146
|
+
html += '<div class="admin-modal-body">';
|
|
147
|
+
html += '<p class="admin-modal-desc">Select users who can access this private project:</p>';
|
|
148
|
+
|
|
149
|
+
for (var i = 0; i < allUsers.length; i++) {
|
|
150
|
+
var u = allUsers[i];
|
|
151
|
+
if (u.role === "admin") continue;
|
|
152
|
+
var checked = allowed.indexOf(u.id) >= 0 ? " checked" : "";
|
|
153
|
+
html += '<label class="admin-user-check">';
|
|
154
|
+
html += '<input type="checkbox" value="' + u.id + '"' + checked + '>';
|
|
155
|
+
html += '<span>' + escapeHtml(u.displayName || u.username) + ' <small>' + escapeHtml(u.username) + '</small></span>';
|
|
156
|
+
html += '</label>';
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
html += '</div>';
|
|
160
|
+
html += '<div class="admin-modal-footer">';
|
|
161
|
+
html += '<button class="admin-modal-save">Save</button>';
|
|
162
|
+
html += '<button class="admin-modal-cancel">Cancel</button>';
|
|
163
|
+
html += '</div>';
|
|
164
|
+
html += '</div>';
|
|
165
|
+
|
|
166
|
+
modal.innerHTML = html;
|
|
167
|
+
document.body.appendChild(modal);
|
|
168
|
+
prepareAdminModal(modal);
|
|
169
|
+
refreshIcons(modal);
|
|
170
|
+
|
|
171
|
+
modal.querySelector(".admin-modal-close").addEventListener("click", function () { modal.remove(); });
|
|
172
|
+
modal.querySelector(".admin-modal-cancel").addEventListener("click", function () { modal.remove(); });
|
|
173
|
+
modal.addEventListener("click", function (e) {
|
|
174
|
+
if (e.target === modal) modal.remove();
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
modal.querySelector(".admin-modal-save").addEventListener("click", function () {
|
|
178
|
+
var checkboxes = modal.querySelectorAll('input[type="checkbox"]');
|
|
179
|
+
var selectedUsers = [];
|
|
180
|
+
for (var ci = 0; ci < checkboxes.length; ci++) {
|
|
181
|
+
if (checkboxes[ci].checked) selectedUsers.push(checkboxes[ci].value);
|
|
182
|
+
}
|
|
183
|
+
apiPut("/api/admin/projects/" + slug + "/users", { allowedUsers: selectedUsers }).then(function (data) {
|
|
184
|
+
if (data.ok) {
|
|
185
|
+
showToast("Project access updated");
|
|
186
|
+
modal.remove();
|
|
187
|
+
loadProjectsTab(parentBody);
|
|
188
|
+
} else {
|
|
189
|
+
showToast(data.error || "Failed to update access");
|
|
190
|
+
}
|
|
191
|
+
}).catch(function () {
|
|
192
|
+
showToast("Failed to update access");
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
}).catch(function () {
|
|
196
|
+
showToast("Failed to load project access info");
|
|
197
|
+
});
|
|
198
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { iconHtml, refreshIcons } from './icons.js';
|
|
2
|
+
import { escapeHtml } from './utils.js';
|
|
3
|
+
import { confirmSettingsAction, trapSettingsFocus, isolateSettingsDialog } from './server-settings-state.js';
|
|
4
|
+
import { enhanceSettingsSelects } from './settings-select.js';
|
|
5
|
+
|
|
6
|
+
async function request(url, method, body) {
|
|
7
|
+
var response = await fetch(url, { method: method, credentials: 'same-origin', headers: { 'Content-Type': 'application/json' }, body: body === undefined ? undefined : JSON.stringify(body) });
|
|
8
|
+
var data = await response.json();
|
|
9
|
+
if (!response.ok) throw new Error(data.error || 'Request failed (HTTP ' + response.status + ').');
|
|
10
|
+
return data;
|
|
11
|
+
}
|
|
12
|
+
export function apiGet(url) { return request(url, 'GET'); }
|
|
13
|
+
export function apiPost(url, body) { return request(url, 'POST', body || {}); }
|
|
14
|
+
export function apiPut(url, body) { return request(url, 'PUT', body); }
|
|
15
|
+
export function apiDelete(url) { return request(url, 'DELETE'); }
|
|
16
|
+
|
|
17
|
+
export function prepareAdminModal(modal) {
|
|
18
|
+
if (modal.dataset.prepared) return;
|
|
19
|
+
modal.dataset.prepared = 'true';
|
|
20
|
+
var previous = document.activeElement;
|
|
21
|
+
var restore = isolateSettingsDialog(modal);
|
|
22
|
+
var dialog = modal.querySelector('.admin-modal');
|
|
23
|
+
dialog.setAttribute('role', 'dialog'); dialog.setAttribute('aria-modal', 'true');
|
|
24
|
+
var title = dialog.querySelector('h3'); dialog.setAttribute('aria-label', title ? title.textContent : 'Confirm action');
|
|
25
|
+
var dirty = false;
|
|
26
|
+
modal.addEventListener('input', function () { dirty = true; });
|
|
27
|
+
modal.addEventListener('change', function () { dirty = true; });
|
|
28
|
+
function cancel(event) {
|
|
29
|
+
if (dirty) {
|
|
30
|
+
event.preventDefault(); event.stopImmediatePropagation();
|
|
31
|
+
confirmSettingsAction('Discard the unsaved changes in this dialog?', 'Discard changes', function () { modal.remove(); });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
modal.addEventListener('click', function (event) {
|
|
35
|
+
if (event.target === modal || event.target.closest('.admin-modal-close, .admin-modal-cancel')) cancel(event);
|
|
36
|
+
}, true);
|
|
37
|
+
modal.addEventListener('keydown', function (event) {
|
|
38
|
+
event.stopPropagation();
|
|
39
|
+
if (event.key === 'Escape') { event.preventDefault(); if (dirty) cancel(event); else modal.remove(); }
|
|
40
|
+
trapSettingsFocus(event, modal);
|
|
41
|
+
});
|
|
42
|
+
modal.querySelectorAll('.admin-smtp-row').forEach(function (row) {
|
|
43
|
+
var label = row.querySelector('label'); var input = row.querySelector('input, select');
|
|
44
|
+
if (label && input && input.id) label.htmlFor = input.id;
|
|
45
|
+
if (label && input && input.tagName === 'SELECT') input.setAttribute('aria-label', label.textContent);
|
|
46
|
+
});
|
|
47
|
+
var closeButton = modal.querySelector('.admin-modal-close'); if (closeButton) closeButton.setAttribute('aria-label', 'Close dialog');
|
|
48
|
+
enhanceSettingsSelects(modal);
|
|
49
|
+
var closeObserver = new MutationObserver(function () {
|
|
50
|
+
if (!modal.isConnected) { closeObserver.disconnect(); restore(); if (previous && previous.isConnected) previous.focus(); }
|
|
51
|
+
});
|
|
52
|
+
closeObserver.observe(document.body, { childList: true });
|
|
53
|
+
var focus = modal.querySelector('input:not([type=hidden]), .admin-modal-cancel, button');
|
|
54
|
+
if (focus) focus.focus();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function showConfirmDialog(message, actionLabel, onConfirm) {
|
|
58
|
+
var modal = document.createElement("div");
|
|
59
|
+
modal.className = "admin-modal-overlay";
|
|
60
|
+
var html = '<div class="admin-modal" role="dialog" aria-modal="true" aria-label="Confirm action">' +
|
|
61
|
+
'<div class="admin-modal-body" style="padding:20px 16px 16px">' +
|
|
62
|
+
'<p class="admin-modal-desc" style="margin:0;font-size:14px;color:var(--text)">' + escapeHtml(message) + '</p>' +
|
|
63
|
+
'</div>' +
|
|
64
|
+
'<div class="admin-modal-footer">' +
|
|
65
|
+
'<button class="admin-modal-save admin-modal-confirm-danger">' + escapeHtml(actionLabel) + '</button>' +
|
|
66
|
+
'<button class="admin-modal-cancel">Cancel</button>' +
|
|
67
|
+
'</div></div>';
|
|
68
|
+
modal.innerHTML = html;
|
|
69
|
+
document.body.appendChild(modal);
|
|
70
|
+
prepareAdminModal(modal);
|
|
71
|
+
refreshIcons(modal);
|
|
72
|
+
modal.querySelector(".admin-modal-cancel").addEventListener("click", function () { modal.remove(); });
|
|
73
|
+
modal.addEventListener("click", function (e) { if (e.target === modal) modal.remove(); });
|
|
74
|
+
modal.querySelector(".admin-modal-confirm-danger").addEventListener("click", function () {
|
|
75
|
+
modal.remove();
|
|
76
|
+
onConfirm();
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function showConfirmResetPin(name, onConfirm) {
|
|
81
|
+
var modal = document.createElement("div");
|
|
82
|
+
modal.className = "admin-modal-overlay";
|
|
83
|
+
var html = '<div class="admin-modal">' +
|
|
84
|
+
'<div class="admin-modal-body" style="padding:20px 16px 16px">' +
|
|
85
|
+
'<p class="admin-modal-desc" style="margin:0;font-size:14px;color:var(--text)">Reset PIN for <strong>' + escapeHtml(name) + '</strong>? A new temporary PIN will be generated and they will need to change it on next login.</p>' +
|
|
86
|
+
'</div>' +
|
|
87
|
+
'<div class="admin-modal-footer">' +
|
|
88
|
+
'<button class="admin-modal-save">Reset PIN</button>' +
|
|
89
|
+
'<button class="admin-modal-cancel">Cancel</button>' +
|
|
90
|
+
'</div></div>';
|
|
91
|
+
modal.innerHTML = html;
|
|
92
|
+
document.body.appendChild(modal);
|
|
93
|
+
prepareAdminModal(modal);
|
|
94
|
+
modal.querySelector(".admin-modal-cancel").addEventListener("click", function () { modal.remove(); });
|
|
95
|
+
modal.addEventListener("click", function (e) { if (e.target === modal) modal.remove(); });
|
|
96
|
+
modal.querySelector(".admin-modal-save").addEventListener("click", function () {
|
|
97
|
+
modal.remove();
|
|
98
|
+
onConfirm();
|
|
99
|
+
});
|
|
100
|
+
}
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import { store } from './store.js';
|
|
2
|
+
import { iconHtml, refreshIcons } from './icons.js';
|
|
3
|
+
import { showToast, copyToClipboard, escapeHtml } from './utils.js';
|
|
4
|
+
import { apiGet, apiPost, apiPut, apiDelete, showConfirmDialog, showConfirmResetPin, prepareAdminModal } from './admin-settings-ui.js';
|
|
5
|
+
// --- Users ---
|
|
6
|
+
export function loadUsersTab(body) {
|
|
7
|
+
apiGet("/api/admin/users").then(function (data) {
|
|
8
|
+
store.set({ adminUsers: data.users || [] });
|
|
9
|
+
renderUsersTab(body);
|
|
10
|
+
}).catch(function () {
|
|
11
|
+
body.innerHTML = '<div class="admin-error">Failed to load users</div>';
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function renderUsersTab(body) {
|
|
16
|
+
var html = '<div class="admin-section-header">' +
|
|
17
|
+
'<div class="admin-header-btns">' +
|
|
18
|
+
'<button class="admin-action-btn" id="admin-add-user">' + iconHtml("user-plus") + ' Add user</button>' +
|
|
19
|
+
'</div></div>';
|
|
20
|
+
html += '<div class="admin-user-list">';
|
|
21
|
+
for (var i = 0; i < store.get('adminUsers').length; i++) {
|
|
22
|
+
var u = store.get('adminUsers')[i];
|
|
23
|
+
var isMe = store.get('adminMe') && store.get('adminMe').user && store.get('adminMe').user.id === u.id;
|
|
24
|
+
var created = new Date(u.createdAt).toLocaleDateString();
|
|
25
|
+
html += '<div class="admin-user-item">';
|
|
26
|
+
html += '<div class="admin-user-info">';
|
|
27
|
+
html += '<div class="admin-user-name">';
|
|
28
|
+
html += '<strong>' + escapeHtml(u.displayName || u.username) + '</strong>';
|
|
29
|
+
if (u.role === "admin") html += ' <span class="admin-badge">admin</span>';
|
|
30
|
+
if (isMe) html += ' <span class="admin-you-badge">you</span>';
|
|
31
|
+
html += '</div>';
|
|
32
|
+
html += '<div class="admin-user-meta">' + escapeHtml(u.username) + ' · joined ' + created + '</div>';
|
|
33
|
+
html += '</div>';
|
|
34
|
+
if (!isMe) {
|
|
35
|
+
html += '<div style="display:flex;align-items:center;gap:2px">';
|
|
36
|
+
if (u.role !== "admin") {
|
|
37
|
+
html += '<button class="admin-remove-btn admin-perms-btn" data-user-id="' + u.id + '" title="Permissions">' + iconHtml("shield") + '</button>';
|
|
38
|
+
}
|
|
39
|
+
html += '<button class="admin-remove-btn admin-reset-pin-btn" data-user-id="' + u.id + '" title="Reset PIN">' + iconHtml("key-round") + '</button>';
|
|
40
|
+
if (u.role !== "admin") {
|
|
41
|
+
html += '<button class="admin-remove-btn" data-user-id="' + u.id + '" title="Remove user">' + iconHtml("trash-2") + '</button>';
|
|
42
|
+
}
|
|
43
|
+
html += '</div>';
|
|
44
|
+
}
|
|
45
|
+
html += '</div>';
|
|
46
|
+
}
|
|
47
|
+
html += '</div>';
|
|
48
|
+
|
|
49
|
+
body.innerHTML = html;
|
|
50
|
+
refreshIcons(body);
|
|
51
|
+
|
|
52
|
+
// Bind add user button
|
|
53
|
+
var addUserBtn = body.querySelector("#admin-add-user");
|
|
54
|
+
if (addUserBtn) {
|
|
55
|
+
addUserBtn.addEventListener("click", function () {
|
|
56
|
+
showAddUserModal(body);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Bind reset PIN buttons
|
|
61
|
+
var resetBtns = body.querySelectorAll(".admin-reset-pin-btn");
|
|
62
|
+
for (var j = 0; j < resetBtns.length; j++) {
|
|
63
|
+
resetBtns[j].addEventListener("click", function (e) {
|
|
64
|
+
e.stopPropagation();
|
|
65
|
+
var userId = this.dataset.userId;
|
|
66
|
+
var user = store.get('adminUsers').find(function (u) { return u.id === userId; });
|
|
67
|
+
var name = user ? (user.displayName || user.username) : "this user";
|
|
68
|
+
showConfirmResetPin(name, function () {
|
|
69
|
+
apiPost("/api/admin/users/" + userId + "/reset-pin").then(function (data) {
|
|
70
|
+
if (data.ok) {
|
|
71
|
+
showTempPinModal({ username: user.username, displayName: user.displayName || user.username }, data.tempPin);
|
|
72
|
+
} else {
|
|
73
|
+
showToast(data.error || "Failed to reset PIN");
|
|
74
|
+
}
|
|
75
|
+
}).catch(function () {
|
|
76
|
+
showToast("Failed to reset PIN");
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Bind permissions buttons
|
|
83
|
+
var permsBtns = body.querySelectorAll(".admin-perms-btn");
|
|
84
|
+
for (var p = 0; p < permsBtns.length; p++) {
|
|
85
|
+
permsBtns[p].addEventListener("click", function (e) {
|
|
86
|
+
e.stopPropagation();
|
|
87
|
+
var userId = this.dataset.userId;
|
|
88
|
+
var user = store.get('adminUsers').find(function (u) { return u.id === userId; });
|
|
89
|
+
if (user) showPermissionsModal(user, body);
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Bind remove buttons
|
|
94
|
+
var removeBtns = body.querySelectorAll(".admin-remove-btn:not(.admin-reset-pin-btn):not(.admin-perms-btn)");
|
|
95
|
+
for (var k = 0; k < removeBtns.length; k++) {
|
|
96
|
+
removeBtns[k].addEventListener("click", function () {
|
|
97
|
+
var userId = this.dataset.userId;
|
|
98
|
+
var user = store.get('adminUsers').find(function (u) { return u.id === userId; });
|
|
99
|
+
var name = user ? (user.displayName || user.username) : "this user";
|
|
100
|
+
showConfirmDialog("Remove " + name + "? This cannot be undone.", "Remove user", function () {
|
|
101
|
+
removeUser(userId, body);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
var PERM_LABELS = [
|
|
108
|
+
{ key: "terminal", label: "Terminal", desc: "Access the web terminal" },
|
|
109
|
+
{ key: "fileBrowser", label: "File Browser", desc: "Browse and manage files" },
|
|
110
|
+
{ key: "createProject", label: "Create Projects", desc: "Create or clone new projects" },
|
|
111
|
+
{ key: "deleteProject", label: "Delete Projects", desc: "Remove projects" },
|
|
112
|
+
{ key: "skills", label: "Skills", desc: "View and install skills" },
|
|
113
|
+
{ key: "sessionDelete", label: "Delete Sessions", desc: "Delete chat sessions" },
|
|
114
|
+
{ key: "scheduledTasks", label: "Scheduled Tasks", desc: "Create and manage scheduled tasks" },
|
|
115
|
+
{ key: "projectSettings", label: "Project Settings", desc: "Access project settings" },
|
|
116
|
+
];
|
|
117
|
+
|
|
118
|
+
var DEFAULT_PERMISSIONS = {
|
|
119
|
+
terminal: false,
|
|
120
|
+
fileBrowser: false,
|
|
121
|
+
createProject: true,
|
|
122
|
+
deleteProject: false,
|
|
123
|
+
skills: true,
|
|
124
|
+
sessionDelete: false,
|
|
125
|
+
scheduledTasks: false,
|
|
126
|
+
projectSettings: false,
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
function showPermissionsModal(user, body) {
|
|
130
|
+
var perms = {};
|
|
131
|
+
var keys = PERM_LABELS.map(function (p) { return p.key; });
|
|
132
|
+
for (var i = 0; i < keys.length; i++) {
|
|
133
|
+
var k = keys[i];
|
|
134
|
+
perms[k] = (user.permissions && user.permissions[k] !== undefined) ? user.permissions[k] : DEFAULT_PERMISSIONS[k];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
var modal = document.createElement("div");
|
|
138
|
+
modal.className = "admin-modal-overlay";
|
|
139
|
+
var html = '<div class="admin-modal">' +
|
|
140
|
+
'<div class="admin-modal-header">' +
|
|
141
|
+
'<h3>Permissions: ' + escapeHtml(user.displayName || user.username) + '</h3>' +
|
|
142
|
+
'<button class="admin-modal-close">×</button>' +
|
|
143
|
+
'</div>' +
|
|
144
|
+
'<div class="admin-modal-body">' +
|
|
145
|
+
'<div class="admin-perms-list">';
|
|
146
|
+
|
|
147
|
+
for (var j = 0; j < PERM_LABELS.length; j++) {
|
|
148
|
+
var p = PERM_LABELS[j];
|
|
149
|
+
var checked = perms[p.key] ? " checked" : "";
|
|
150
|
+
html += '<label class="admin-perm-row">' +
|
|
151
|
+
'<div class="admin-perm-info">' +
|
|
152
|
+
'<span class="admin-perm-label">' + p.label + '</span>' +
|
|
153
|
+
'<span class="admin-perm-desc">' + p.desc + '</span>' +
|
|
154
|
+
'</div>' +
|
|
155
|
+
'<input type="checkbox" class="admin-perm-toggle" data-perm="' + p.key + '"' + checked + '>' +
|
|
156
|
+
'</label>';
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
html += '</div></div>' +
|
|
160
|
+
'<div class="admin-modal-footer">' +
|
|
161
|
+
'<button class="admin-modal-save" id="admin-perms-save">Save</button>' +
|
|
162
|
+
'<button class="admin-modal-cancel">Cancel</button>' +
|
|
163
|
+
'</div></div>';
|
|
164
|
+
|
|
165
|
+
modal.innerHTML = html;
|
|
166
|
+
document.body.appendChild(modal);
|
|
167
|
+
prepareAdminModal(modal);
|
|
168
|
+
refreshIcons(modal);
|
|
169
|
+
|
|
170
|
+
modal.querySelector(".admin-modal-close").addEventListener("click", function () { modal.remove(); });
|
|
171
|
+
modal.querySelector(".admin-modal-cancel").addEventListener("click", function () { modal.remove(); });
|
|
172
|
+
modal.addEventListener("click", function (e) { if (e.target === modal) modal.remove(); });
|
|
173
|
+
|
|
174
|
+
modal.querySelector("#admin-perms-save").addEventListener("click", function () {
|
|
175
|
+
var toggles = modal.querySelectorAll(".admin-perm-toggle");
|
|
176
|
+
var newPerms = {};
|
|
177
|
+
for (var t = 0; t < toggles.length; t++) {
|
|
178
|
+
newPerms[toggles[t].dataset.perm] = toggles[t].checked;
|
|
179
|
+
}
|
|
180
|
+
apiPut("/api/admin/users/" + user.id + "/permissions", { permissions: newPerms }).then(function (data) {
|
|
181
|
+
if (data.ok) {
|
|
182
|
+
showToast("Permissions updated");
|
|
183
|
+
modal.remove();
|
|
184
|
+
// Update cached user
|
|
185
|
+
for (var c = 0; c < store.get('adminUsers').length; c++) {
|
|
186
|
+
if (store.get('adminUsers')[c].id === user.id) {
|
|
187
|
+
store.get('adminUsers')[c].permissions = data.permissions;
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
} else {
|
|
192
|
+
showToast(data.error || "Failed to update permissions");
|
|
193
|
+
}
|
|
194
|
+
}).catch(function () {
|
|
195
|
+
showToast("Failed to update permissions");
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function showAddUserModal(body) {
|
|
201
|
+
var modal = document.createElement("div");
|
|
202
|
+
modal.className = "admin-modal-overlay";
|
|
203
|
+
var html = '<div class="admin-modal">' +
|
|
204
|
+
'<div class="admin-modal-header">' +
|
|
205
|
+
'<h3>Add user</h3>' +
|
|
206
|
+
'<button class="admin-modal-close">' + iconHtml("x") + '</button>' +
|
|
207
|
+
'</div>' +
|
|
208
|
+
'<div class="admin-modal-body">' +
|
|
209
|
+
'<p class="admin-modal-desc">Create a new user account. A temporary 6-digit PIN will be generated automatically. The user must change it on first login.</p>' +
|
|
210
|
+
'<div class="admin-smtp-row"><label>Username</label>' +
|
|
211
|
+
'<input type="text" class="admin-smtp-input" id="admin-new-username" placeholder="username" autocomplete="off" maxlength="100"></div>' +
|
|
212
|
+
'<div class="admin-smtp-row"><label>Display name</label>' +
|
|
213
|
+
'<input type="text" class="admin-smtp-input" id="admin-new-displayname" placeholder="Display name (optional)" autocomplete="off" maxlength="30"></div>' +
|
|
214
|
+
'<div class="admin-smtp-row"><label>Email</label>' +
|
|
215
|
+
'<input type="email" class="admin-smtp-input" id="admin-new-email" placeholder="user@example.com (optional)" autocomplete="off"></div>' +
|
|
216
|
+
'<div class="admin-smtp-row"><label>Role</label>' +
|
|
217
|
+
'<select class="admin-smtp-input" id="admin-new-role"><option value="user">User</option><option value="admin">Admin</option></select></div>' +
|
|
218
|
+
'<div class="admin-smtp-error" id="admin-new-user-error"></div>' +
|
|
219
|
+
'</div>' +
|
|
220
|
+
'<div class="admin-modal-footer">' +
|
|
221
|
+
'<button class="admin-modal-save" id="admin-new-user-create">Create user</button>' +
|
|
222
|
+
'<button class="admin-modal-cancel">Cancel</button>' +
|
|
223
|
+
'</div></div>';
|
|
224
|
+
modal.innerHTML = html;
|
|
225
|
+
document.body.appendChild(modal);
|
|
226
|
+
prepareAdminModal(modal);
|
|
227
|
+
refreshIcons(modal);
|
|
228
|
+
|
|
229
|
+
var usernameInput = modal.querySelector("#admin-new-username");
|
|
230
|
+
var displayNameInput = modal.querySelector("#admin-new-displayname");
|
|
231
|
+
var emailInput = modal.querySelector("#admin-new-email");
|
|
232
|
+
var roleSelect = modal.querySelector("#admin-new-role");
|
|
233
|
+
var createBtn = modal.querySelector("#admin-new-user-create");
|
|
234
|
+
var errorEl = modal.querySelector("#admin-new-user-error");
|
|
235
|
+
|
|
236
|
+
modal.querySelector(".admin-modal-close").addEventListener("click", function () { modal.remove(); });
|
|
237
|
+
modal.querySelector(".admin-modal-cancel").addEventListener("click", function () { modal.remove(); });
|
|
238
|
+
modal.addEventListener("click", function (e) { if (e.target === modal) modal.remove(); });
|
|
239
|
+
|
|
240
|
+
usernameInput.focus();
|
|
241
|
+
usernameInput.addEventListener("keydown", function (e) {
|
|
242
|
+
if (e.key === "Enter") createBtn.click();
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
createBtn.addEventListener("click", function () {
|
|
246
|
+
var username = usernameInput.value.trim();
|
|
247
|
+
if (!username) {
|
|
248
|
+
errorEl.textContent = "Username is required";
|
|
249
|
+
errorEl.className = "admin-smtp-error admin-smtp-error-visible";
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
createBtn.disabled = true;
|
|
253
|
+
createBtn.textContent = "Creating...";
|
|
254
|
+
errorEl.textContent = "";
|
|
255
|
+
errorEl.className = "admin-smtp-error";
|
|
256
|
+
apiPost("/api/admin/users", {
|
|
257
|
+
username: username,
|
|
258
|
+
displayName: displayNameInput.value.trim() || username,
|
|
259
|
+
email: emailInput.value.trim() || null,
|
|
260
|
+
role: roleSelect.value,
|
|
261
|
+
}).then(function (data) {
|
|
262
|
+
if (data.ok) {
|
|
263
|
+
modal.remove();
|
|
264
|
+
showTempPinModal(data.user, data.tempPin);
|
|
265
|
+
loadUsersTab(body);
|
|
266
|
+
} else {
|
|
267
|
+
errorEl.textContent = data.error || "Failed to create user";
|
|
268
|
+
errorEl.className = "admin-smtp-error admin-smtp-error-visible";
|
|
269
|
+
createBtn.disabled = false;
|
|
270
|
+
createBtn.textContent = "Create user";
|
|
271
|
+
}
|
|
272
|
+
}).catch(function () {
|
|
273
|
+
errorEl.textContent = "Failed to create user";
|
|
274
|
+
errorEl.className = "admin-smtp-error admin-smtp-error-visible";
|
|
275
|
+
createBtn.disabled = false;
|
|
276
|
+
createBtn.textContent = "Create user";
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function showTempPinModal(user, tempPin) {
|
|
282
|
+
var modal = document.createElement("div");
|
|
283
|
+
modal.className = "admin-modal-overlay";
|
|
284
|
+
var html = '<div class="admin-modal">' +
|
|
285
|
+
'<div class="admin-modal-header">' +
|
|
286
|
+
'<h3>User Created</h3>' +
|
|
287
|
+
'<button class="admin-modal-close">' + iconHtml("x") + '</button>' +
|
|
288
|
+
'</div>' +
|
|
289
|
+
'<div class="admin-modal-body">' +
|
|
290
|
+
'<p class="admin-modal-desc">Account for <strong>' + escapeHtml(user.displayName || user.username) + '</strong> has been created. Share these credentials with the user:</p>' +
|
|
291
|
+
'<div class="admin-temp-pin-box">' +
|
|
292
|
+
'<div class="admin-temp-pin-row"><span class="admin-temp-pin-label">Username</span><code class="admin-temp-pin-value">' + escapeHtml(user.username) + '</code></div>' +
|
|
293
|
+
'<div class="admin-temp-pin-row"><span class="admin-temp-pin-label">Temporary PIN</span><code class="admin-temp-pin-value admin-temp-pin-highlight">' + escapeHtml(tempPin) + '</code></div>' +
|
|
294
|
+
'</div>' +
|
|
295
|
+
'<p class="admin-modal-desc" style="margin-top:12px;color:var(--text-secondary);font-size:12px;">This PIN is one-time use. The user will be prompted to set a new PIN on first login.</p>' +
|
|
296
|
+
'</div>' +
|
|
297
|
+
'<div class="admin-modal-footer">' +
|
|
298
|
+
'<button class="admin-modal-save" id="admin-copy-credentials">Copy Credentials</button>' +
|
|
299
|
+
'<button class="admin-modal-cancel">Close</button>' +
|
|
300
|
+
'</div></div>';
|
|
301
|
+
modal.innerHTML = html;
|
|
302
|
+
document.body.appendChild(modal);
|
|
303
|
+
prepareAdminModal(modal);
|
|
304
|
+
refreshIcons(modal);
|
|
305
|
+
|
|
306
|
+
modal.querySelector(".admin-modal-close").addEventListener("click", function () { modal.remove(); });
|
|
307
|
+
modal.querySelector(".admin-modal-cancel").addEventListener("click", function () { modal.remove(); });
|
|
308
|
+
modal.addEventListener("click", function (e) { if (e.target === modal) modal.remove(); });
|
|
309
|
+
|
|
310
|
+
modal.querySelector("#admin-copy-credentials").addEventListener("click", function () {
|
|
311
|
+
var text = "Username: " + user.username + "\nTemporary PIN: " + tempPin;
|
|
312
|
+
copyToClipboard(text).then(function () {
|
|
313
|
+
showToast("Credentials copied to clipboard");
|
|
314
|
+
}).catch(function () {
|
|
315
|
+
showToast("Username: " + user.username + " / PIN: " + tempPin);
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function removeUser(userId, body) {
|
|
321
|
+
apiDelete("/api/admin/users/" + userId).then(function (data) {
|
|
322
|
+
if (data.ok) {
|
|
323
|
+
showToast("User removed");
|
|
324
|
+
loadUsersTab(body);
|
|
325
|
+
} else {
|
|
326
|
+
showToast(data.error || "Failed to remove user");
|
|
327
|
+
}
|
|
328
|
+
}).catch(function () {
|
|
329
|
+
showToast("Failed to remove user");
|
|
330
|
+
});
|
|
331
|
+
}
|