clay-server 5.1.0-beta.2 → 5.1.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/bin/cli.js +9 -91
- package/lib/builtin-mates.js +7 -7
- package/lib/daemon-settings.js +11 -0
- package/lib/daemon.js +8 -37
- package/lib/linear-store.js +35 -8
- package/lib/mate-identity-file.js +39 -0
- package/lib/mate-knowledge-sync.js +1 -1
- package/lib/mate-ready-creation.js +2 -1
- package/lib/mates-identity.js +4 -4
- package/lib/mates-prompts.js +4 -3
- package/lib/mates.js +12 -11
- package/lib/os-users.js +5 -16
- package/lib/project-connection.js +0 -7
- package/lib/project-debate.js +3 -2
- package/lib/project-filesystem.js +21 -39
- package/lib/project-mate-interaction.js +7 -6
- package/lib/project-server-settings.js +31 -0
- package/lib/project-sessions.js +57 -184
- package/lib/project.js +3 -3
- package/lib/public/app.js +6 -37
- package/lib/public/css/input.css +3 -1
- package/lib/public/css/menus.css +3 -0
- package/lib/public/css/overlays.css +7 -0
- package/lib/public/css/project-settings-modal.css +56 -0
- package/lib/public/css/server-settings-system.css +125 -0
- 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/title-bar.css +5 -1
- package/lib/public/index.html +173 -447
- package/lib/public/modules/account-island.js +9 -7
- 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-dm.js +0 -2
- package/lib/public/modules/app-messages.js +13 -52
- package/lib/public/modules/app-rate-limit.js +70 -12
- package/lib/public/modules/avatar.js +6 -1
- package/lib/public/modules/context-sources.js +27 -18
- package/lib/public/modules/default-ai.js +14 -7
- package/lib/public/modules/header-tui-font.js +10 -20
- package/lib/public/modules/home-mate-avatar-editor.js +2 -2
- package/lib/public/modules/linear-settings.js +3 -2
- package/lib/public/modules/markdown.js +5 -6
- 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 +15 -6
- package/lib/public/modules/project-settings-environment.js +106 -0
- package/lib/public/modules/project-settings.js +72 -628
- 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-github.js +2 -1
- package/lib/public/modules/session-presence-ui.js +1 -1
- 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/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/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/style.css +4 -0
- package/lib/sdk-message-processor.js +16 -4
- package/lib/sdk-quota-snapshot.js +37 -0
- package/lib/server-appearance-settings.js +71 -0
- package/lib/server-linear.js +14 -8
- package/lib/server-mate-instructions.js +2 -2
- package/lib/server-mates.js +2 -1
- package/lib/server-notification-settings.js +67 -0
- package/lib/server-settings.js +4 -0
- package/lib/server-storage.js +57 -0
- package/lib/server.js +10 -2
- package/lib/sessions.js +3 -4
- package/lib/startup-projects.js +40 -0
- package/lib/storage-native-walk.js +85 -0
- package/lib/storage-usage.js +144 -0
- package/lib/users-preferences.js +0 -74
- package/lib/users.js +0 -4
- package/lib/ws-schema.js +2 -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 +3 -3
|
@@ -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
|
+
}
|