@agenticmail/enterprise 0.5.302 → 0.5.304
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/dist/chunk-4VGAZULN.js +1519 -0
- package/dist/chunk-CRXYUYVJ.js +4395 -0
- package/dist/chunk-QCGSFWKU.js +48 -0
- package/dist/cli-agent-YSQVDBOZ.js +1778 -0
- package/dist/cli-recover-2LWWVD4Q.js +487 -0
- package/dist/cli-serve-JZEXPYXY.js +143 -0
- package/dist/cli-verify-TZMX3GWV.js +149 -0
- package/dist/cli.js +5 -5
- package/dist/dashboard/app.js +57 -1
- package/dist/dashboard/components/org-switcher.js +57 -35
- package/dist/dashboard/pages/organizations.js +39 -3
- package/dist/dashboard/pages/users.js +36 -4
- package/dist/factory-QYGGXVYW.js +9 -0
- package/dist/index.js +3 -3
- package/dist/postgres-ALNOGUUM.js +819 -0
- package/dist/server-3R5KZPLA.js +15 -0
- package/dist/setup-ZZAOBEY4.js +20 -0
- package/dist/sqlite-2QPVZQ27.js +566 -0
- package/package.json +1 -1
- package/src/admin/routes.ts +32 -1
- package/src/auth/routes.ts +36 -2
- package/src/dashboard/app.js +57 -1
- package/src/dashboard/components/org-switcher.js +57 -35
- package/src/dashboard/pages/organizations.js +39 -3
- package/src/dashboard/pages/users.js +60 -4
- package/src/db/adapter.ts +1 -0
- package/src/db/postgres.ts +3 -0
- package/src/db/sqlite.ts +7 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DomainLock
|
|
3
|
+
} from "./chunk-UPU23ZRG.js";
|
|
4
|
+
import "./chunk-KFQGP6VL.js";
|
|
5
|
+
|
|
6
|
+
// src/domain-lock/cli-verify.ts
|
|
7
|
+
function getFlag(args, name) {
|
|
8
|
+
const idx = args.indexOf(name);
|
|
9
|
+
if (idx !== -1 && args[idx + 1]) return args[idx + 1];
|
|
10
|
+
return void 0;
|
|
11
|
+
}
|
|
12
|
+
function hasFlag(args, name) {
|
|
13
|
+
return args.includes(name);
|
|
14
|
+
}
|
|
15
|
+
function detectDbType(url) {
|
|
16
|
+
const u = url.toLowerCase().trim();
|
|
17
|
+
if (u.startsWith("postgres") || u.startsWith("pg:")) return "postgres";
|
|
18
|
+
if (u.startsWith("mysql")) return "mysql";
|
|
19
|
+
if (u.startsWith("mongodb")) return "mongodb";
|
|
20
|
+
if (u.startsWith("libsql") || u.includes(".turso.io")) return "turso";
|
|
21
|
+
if (u.endsWith(".db") || u.endsWith(".sqlite") || u.endsWith(".sqlite3") || u.startsWith("file:")) return "sqlite";
|
|
22
|
+
return "postgres";
|
|
23
|
+
}
|
|
24
|
+
async function runVerifyDomain(args) {
|
|
25
|
+
const { default: chalk } = await import("chalk");
|
|
26
|
+
const { default: ora } = await import("ora");
|
|
27
|
+
console.log("");
|
|
28
|
+
console.log(chalk.bold(" AgenticMail Enterprise \u2014 Domain Verification"));
|
|
29
|
+
console.log("");
|
|
30
|
+
let domain = getFlag(args, "--domain");
|
|
31
|
+
let dnsChallenge;
|
|
32
|
+
let db = null;
|
|
33
|
+
let dbConnected = false;
|
|
34
|
+
const envDbUrl = process.env.DATABASE_URL;
|
|
35
|
+
if (envDbUrl) {
|
|
36
|
+
const dbType = detectDbType(envDbUrl);
|
|
37
|
+
const spinner = ora(`Connecting to database (${dbType})...`).start();
|
|
38
|
+
try {
|
|
39
|
+
const { createAdapter } = await import("./factory-QYGGXVYW.js");
|
|
40
|
+
db = await createAdapter({ type: dbType, connectionString: envDbUrl });
|
|
41
|
+
await db.migrate();
|
|
42
|
+
const settings = await db.getSettings();
|
|
43
|
+
if (!domain && settings?.domain) domain = settings.domain;
|
|
44
|
+
if (settings?.domainDnsChallenge) dnsChallenge = settings.domainDnsChallenge;
|
|
45
|
+
dbConnected = true;
|
|
46
|
+
spinner.succeed(`Connected to ${dbType} database` + (domain ? ` (domain: ${domain})` : ""));
|
|
47
|
+
} catch (err) {
|
|
48
|
+
spinner.warn(`Could not connect via DATABASE_URL: ${err.message}`);
|
|
49
|
+
db = null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (!dbConnected) {
|
|
53
|
+
const dbPath = getFlag(args, "--db");
|
|
54
|
+
const dbType = getFlag(args, "--db-type") || "sqlite";
|
|
55
|
+
if (dbPath) {
|
|
56
|
+
const spinner = ora(`Connecting to ${dbType} database...`).start();
|
|
57
|
+
try {
|
|
58
|
+
const { createAdapter } = await import("./factory-QYGGXVYW.js");
|
|
59
|
+
db = await createAdapter({ type: dbType, connectionString: dbPath });
|
|
60
|
+
await db.migrate();
|
|
61
|
+
const settings = await db.getSettings();
|
|
62
|
+
if (!domain && settings?.domain) domain = settings.domain;
|
|
63
|
+
if (settings?.domainDnsChallenge) dnsChallenge = settings.domainDnsChallenge;
|
|
64
|
+
dbConnected = true;
|
|
65
|
+
spinner.succeed(`Connected to ${dbType} database`);
|
|
66
|
+
} catch {
|
|
67
|
+
spinner.warn("Could not read local database");
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (!domain) {
|
|
72
|
+
const { default: inquirer } = await import("inquirer");
|
|
73
|
+
const answer = await inquirer.prompt([{
|
|
74
|
+
type: "input",
|
|
75
|
+
name: "domain",
|
|
76
|
+
message: "Domain to verify:",
|
|
77
|
+
suffix: chalk.dim(" (e.g. agents.yourcompany.com)"),
|
|
78
|
+
validate: (v) => v.includes(".") || "Enter a valid domain",
|
|
79
|
+
filter: (v) => v.trim().toLowerCase()
|
|
80
|
+
}]);
|
|
81
|
+
domain = answer.domain;
|
|
82
|
+
}
|
|
83
|
+
const lock = new DomainLock();
|
|
84
|
+
const maxAttempts = hasFlag(args, "--poll") ? 5 : 1;
|
|
85
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
86
|
+
const spinner = ora(
|
|
87
|
+
maxAttempts > 1 ? `Checking DNS verification (attempt ${attempt}/${maxAttempts})...` : "Checking DNS verification..."
|
|
88
|
+
).start();
|
|
89
|
+
try {
|
|
90
|
+
const result = await lock.checkVerification(domain);
|
|
91
|
+
if (!result.success) {
|
|
92
|
+
spinner.fail("Verification check failed");
|
|
93
|
+
console.log("");
|
|
94
|
+
console.error(chalk.red(` ${result.error}`));
|
|
95
|
+
console.log("");
|
|
96
|
+
if (db) await db.disconnect();
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
if (result.verified) {
|
|
100
|
+
spinner.succeed("Domain verified!");
|
|
101
|
+
if (dbConnected && db) {
|
|
102
|
+
try {
|
|
103
|
+
await db.updateSettings({
|
|
104
|
+
domainStatus: "verified",
|
|
105
|
+
domainVerifiedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
106
|
+
});
|
|
107
|
+
console.log(chalk.dim(" Database updated with verified status."));
|
|
108
|
+
} catch {
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
console.log("");
|
|
112
|
+
console.log(chalk.green.bold(` \u2713 ${domain} is verified and protected.`));
|
|
113
|
+
console.log(chalk.dim(" Your deployment domain is locked. No other instance can claim it."));
|
|
114
|
+
console.log(chalk.dim(" The system now operates 100% offline \u2014 no outbound calls are made."));
|
|
115
|
+
console.log("");
|
|
116
|
+
if (db) await db.disconnect();
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
} catch (err) {
|
|
120
|
+
spinner.warn(`Check failed: ${err.message}`);
|
|
121
|
+
}
|
|
122
|
+
if (attempt < maxAttempts) {
|
|
123
|
+
const waitSpinner = ora(`DNS record not found yet. Retrying in 10 seconds...`).start();
|
|
124
|
+
await new Promise((r) => setTimeout(r, 1e4));
|
|
125
|
+
waitSpinner.stop();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
console.log("");
|
|
129
|
+
console.log(chalk.yellow(" DNS record not detected yet."));
|
|
130
|
+
console.log("");
|
|
131
|
+
console.log(chalk.bold(" Make sure this TXT record exists at your DNS provider:"));
|
|
132
|
+
console.log("");
|
|
133
|
+
console.log(` ${chalk.bold("Host:")} ${chalk.cyan(`_agenticmail-verify.${domain}`)}`);
|
|
134
|
+
console.log(` ${chalk.bold("Type:")} ${chalk.cyan("TXT")}`);
|
|
135
|
+
if (dnsChallenge) {
|
|
136
|
+
console.log(` ${chalk.bold("Value:")} ${chalk.cyan(dnsChallenge)}`);
|
|
137
|
+
} else {
|
|
138
|
+
console.log(` ${chalk.bold("Value:")} ${chalk.dim("(check your dashboard or setup output)")}`);
|
|
139
|
+
}
|
|
140
|
+
console.log("");
|
|
141
|
+
console.log(chalk.dim(" DNS propagation can take up to 48 hours."));
|
|
142
|
+
console.log(chalk.dim(" Run with --poll to retry automatically:"));
|
|
143
|
+
console.log(chalk.dim(` npx @agenticmail/enterprise verify-domain --domain ${domain} --poll`));
|
|
144
|
+
console.log("");
|
|
145
|
+
if (db) await db.disconnect();
|
|
146
|
+
}
|
|
147
|
+
export {
|
|
148
|
+
runVerifyDomain
|
|
149
|
+
};
|
package/dist/cli.js
CHANGED
|
@@ -14,10 +14,10 @@ switch (command) {
|
|
|
14
14
|
import("./cli-submit-skill-LDFJGSKO.js").then((m) => m.runSubmitSkill(args.slice(1))).catch(fatal);
|
|
15
15
|
break;
|
|
16
16
|
case "recover":
|
|
17
|
-
import("./cli-recover-
|
|
17
|
+
import("./cli-recover-2LWWVD4Q.js").then((m) => m.runRecover(args.slice(1))).catch(fatal);
|
|
18
18
|
break;
|
|
19
19
|
case "verify-domain":
|
|
20
|
-
import("./cli-verify-
|
|
20
|
+
import("./cli-verify-TZMX3GWV.js").then((m) => m.runVerifyDomain(args.slice(1))).catch(fatal);
|
|
21
21
|
break;
|
|
22
22
|
case "reset-password":
|
|
23
23
|
import("./cli-reset-password-SO5Y6MW7.js").then((m) => m.runResetPassword(args.slice(1))).catch(fatal);
|
|
@@ -57,14 +57,14 @@ Skill Development:
|
|
|
57
57
|
break;
|
|
58
58
|
case "serve":
|
|
59
59
|
case "start":
|
|
60
|
-
import("./cli-serve-
|
|
60
|
+
import("./cli-serve-JZEXPYXY.js").then((m) => m.runServe(args.slice(1))).catch(fatal);
|
|
61
61
|
break;
|
|
62
62
|
case "agent":
|
|
63
|
-
import("./cli-agent-
|
|
63
|
+
import("./cli-agent-YSQVDBOZ.js").then((m) => m.runAgent(args.slice(1))).catch(fatal);
|
|
64
64
|
break;
|
|
65
65
|
case "setup":
|
|
66
66
|
default:
|
|
67
|
-
import("./setup-
|
|
67
|
+
import("./setup-ZZAOBEY4.js").then((m) => m.runSetupWizard()).catch(fatal);
|
|
68
68
|
break;
|
|
69
69
|
}
|
|
70
70
|
function fatal(err) {
|
package/dist/dashboard/app.js
CHANGED
|
@@ -97,6 +97,7 @@ function App() {
|
|
|
97
97
|
const [permissions, setPermissions] = useState('*'); // '*' = full access, or { pageId: true | ['tab1','tab2'] }
|
|
98
98
|
const [mustResetPassword, setMustResetPassword] = useState(false);
|
|
99
99
|
const [show2faReminder, setShow2faReminder] = useState(false);
|
|
100
|
+
const [impersonating, setImpersonating] = useState(null); // { user, impersonatedBy }
|
|
100
101
|
const [forceResetPw, setForceResetPw] = useState('');
|
|
101
102
|
const [forceResetPw2, setForceResetPw2] = useState('');
|
|
102
103
|
const [forceResetLoading, setForceResetLoading] = useState(false);
|
|
@@ -146,6 +147,12 @@ function App() {
|
|
|
146
147
|
apiCall('/settings').then(d => { const s = d.settings || d || {}; if (s.primaryColor) applyBrandColor(s.primaryColor); if (s.orgId) setOrgId(s.orgId); }).catch(() => {});
|
|
147
148
|
apiCall('/me/permissions').then(d => {
|
|
148
149
|
if (d && d.permissions) setPermissions(d.permissions);
|
|
150
|
+
// If user is assigned to a client org, auto-set org context
|
|
151
|
+
if (d && d.clientOrgId) {
|
|
152
|
+
localStorage.setItem('em_client_org_id', d.clientOrgId);
|
|
153
|
+
} else {
|
|
154
|
+
localStorage.removeItem('em_client_org_id');
|
|
155
|
+
}
|
|
149
156
|
}).catch(() => {});
|
|
150
157
|
}, [authed]);
|
|
151
158
|
|
|
@@ -281,7 +288,44 @@ function App() {
|
|
|
281
288
|
const PageComponent = canAccessPage ? (pages[page] || DashboardPage) : null;
|
|
282
289
|
const sidebarClass = 'sidebar' + (sidebarPinned ? ' expanded' : sidebarHovered ? ' hover-expanded' : '') + (mobileMenuOpen ? ' mobile-open' : '');
|
|
283
290
|
|
|
284
|
-
|
|
291
|
+
// Impersonation functions
|
|
292
|
+
const startImpersonation = useCallback(async (userId) => {
|
|
293
|
+
try {
|
|
294
|
+
const d = await authCall('/impersonate/' + userId, { method: 'POST' });
|
|
295
|
+
if (d.token && d.user) {
|
|
296
|
+
// Store real user info
|
|
297
|
+
setImpersonating({ user: d.user, impersonatedBy: d.impersonatedBy, originalToken: localStorage.getItem('em_token') });
|
|
298
|
+
// Set impersonated user's token
|
|
299
|
+
localStorage.setItem('em_token', d.token);
|
|
300
|
+
setUser(d.user);
|
|
301
|
+
if (d.user.permissions) setPermissions(d.user.permissions);
|
|
302
|
+
if (d.user.clientOrgId) {
|
|
303
|
+
localStorage.setItem('em_client_org_id', d.user.clientOrgId);
|
|
304
|
+
// Fetch org name for display
|
|
305
|
+
apiCall('/organizations/' + d.user.clientOrgId).then(function(o) {
|
|
306
|
+
if (o && o.name) setImpersonating(function(prev) { return prev ? Object.assign({}, prev, { user: Object.assign({}, prev.user, { clientOrgName: o.name }) }) : prev; });
|
|
307
|
+
}).catch(function() {});
|
|
308
|
+
} else localStorage.removeItem('em_client_org_id');
|
|
309
|
+
toast('Now viewing as ' + d.user.name, 'info');
|
|
310
|
+
setPage('dashboard');
|
|
311
|
+
}
|
|
312
|
+
} catch (e) { toast(e.message || 'Impersonation failed', 'error'); }
|
|
313
|
+
}, []);
|
|
314
|
+
|
|
315
|
+
const stopImpersonation = useCallback(() => {
|
|
316
|
+
if (impersonating && impersonating.originalToken) {
|
|
317
|
+
localStorage.setItem('em_token', impersonating.originalToken);
|
|
318
|
+
}
|
|
319
|
+
setImpersonating(null);
|
|
320
|
+
localStorage.removeItem('em_client_org_id');
|
|
321
|
+
// Reload real user
|
|
322
|
+
authCall('/me').then(d => { setUser(d.user || d); }).catch(() => {});
|
|
323
|
+
apiCall('/me/permissions').then(d => { if (d && d.permissions) setPermissions(d.permissions); }).catch(() => {});
|
|
324
|
+
toast('Stopped impersonation', 'success');
|
|
325
|
+
setPage('users');
|
|
326
|
+
}, [impersonating]);
|
|
327
|
+
|
|
328
|
+
return h(AppContext.Provider, { value: { toast, toasts, user, theme, setPage, permissions, impersonating, startImpersonation, stopImpersonation } },
|
|
285
329
|
h('div', { className: 'app-layout' },
|
|
286
330
|
// Mobile hamburger
|
|
287
331
|
h('button', { className: 'mobile-hamburger', onClick: () => setMobileMenuOpen(true) },
|
|
@@ -337,6 +381,18 @@ function App() {
|
|
|
337
381
|
)
|
|
338
382
|
),
|
|
339
383
|
h('div', { className: 'page-content' },
|
|
384
|
+
// Impersonation banner
|
|
385
|
+
impersonating && h('div', { style: { display: 'flex', alignItems: 'center', gap: 12, padding: '10px 16px', margin: '0 0 16px', background: 'rgba(99,102,241,0.12)', border: '2px solid var(--primary, #6366f1)', borderRadius: 8, fontSize: 13 } },
|
|
386
|
+
I.agents(),
|
|
387
|
+
h('div', { style: { flex: 1 } },
|
|
388
|
+
h('strong', null, 'Viewing as: '),
|
|
389
|
+
impersonating.user.name + ' (' + impersonating.user.email + ')',
|
|
390
|
+
impersonating.user.role && h('span', { className: 'badge badge-neutral', style: { marginLeft: 8, fontSize: 10 } }, impersonating.user.role),
|
|
391
|
+
impersonating.user.clientOrgName && h('span', { className: 'badge badge-info', style: { marginLeft: 8, fontSize: 10 } }, 'Org: ' + impersonating.user.clientOrgName),
|
|
392
|
+
impersonating.user.clientOrgId && !impersonating.user.clientOrgName && h('span', { className: 'badge badge-info', style: { marginLeft: 8, fontSize: 10 } }, 'Client Org')
|
|
393
|
+
),
|
|
394
|
+
h('button', { className: 'btn btn-primary btn-sm', onClick: stopImpersonation }, 'Stop Impersonating')
|
|
395
|
+
),
|
|
340
396
|
// 2FA recommendation banner
|
|
341
397
|
show2faReminder && h('div', { style: { display: 'flex', alignItems: 'center', gap: 12, padding: '10px 16px', margin: '0 0 16px', background: 'var(--warning-soft, rgba(245,158,11,0.1))', border: '1px solid var(--warning, #f59e0b)', borderRadius: 8, fontSize: 13 } },
|
|
342
398
|
I.shield(),
|
|
@@ -1,17 +1,11 @@
|
|
|
1
|
-
import { h, useState, useEffect, Fragment, apiCall } from './utils.js';
|
|
1
|
+
import { h, useState, useEffect, Fragment, apiCall, useApp } from './utils.js';
|
|
2
2
|
import { I } from './icons.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* OrgContextSwitcher — Global org context picker for multi-tenant pages.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* selectedOrgId — currently selected org ID ('' = my org)
|
|
10
|
-
* style — optional container style override
|
|
11
|
-
* showLabel — show "Viewing:" label (default true)
|
|
12
|
-
*
|
|
13
|
-
* The component loads client_organizations from the API and renders a
|
|
14
|
-
* compact dropdown that switches between "My Organization" and client orgs.
|
|
7
|
+
* If the current user has a clientOrgId, the switcher is LOCKED to that org
|
|
8
|
+
* (they can only see their org's data). Owners/admins can switch freely.
|
|
15
9
|
*/
|
|
16
10
|
export function OrgContextSwitcher(props) {
|
|
17
11
|
var onOrgChange = props.onOrgChange;
|
|
@@ -19,6 +13,11 @@ export function OrgContextSwitcher(props) {
|
|
|
19
13
|
var showLabel = props.showLabel !== false;
|
|
20
14
|
var style = props.style || {};
|
|
21
15
|
|
|
16
|
+
var app = useApp();
|
|
17
|
+
var user = app.user || {};
|
|
18
|
+
var userOrgId = user.clientOrgId || null;
|
|
19
|
+
var isLocked = !!userOrgId && user.role !== 'owner' && user.role !== 'admin';
|
|
20
|
+
|
|
22
21
|
var _orgs = useState([]);
|
|
23
22
|
var orgs = _orgs[0]; var setOrgs = _orgs[1];
|
|
24
23
|
var _loaded = useState(false);
|
|
@@ -26,16 +25,23 @@ export function OrgContextSwitcher(props) {
|
|
|
26
25
|
|
|
27
26
|
useEffect(function() {
|
|
28
27
|
apiCall('/organizations').then(function(d) {
|
|
29
|
-
|
|
28
|
+
var list = d.organizations || [];
|
|
29
|
+
setOrgs(list);
|
|
30
30
|
setLoaded(true);
|
|
31
|
+
// Auto-select user's org on first load if org-bound
|
|
32
|
+
if (userOrgId && !selectedOrgId) {
|
|
33
|
+
var org = list.find(function(o) { return o.id === userOrgId; });
|
|
34
|
+
if (org) onOrgChange(userOrgId, org);
|
|
35
|
+
}
|
|
31
36
|
}).catch(function() { setLoaded(true); });
|
|
32
|
-
}, []);
|
|
37
|
+
}, [userOrgId]);
|
|
33
38
|
|
|
34
|
-
// Don't render if no client orgs
|
|
35
|
-
if (loaded && orgs.length === 0) return null;
|
|
39
|
+
// Don't render if no client orgs and user isn't org-bound
|
|
40
|
+
if (loaded && orgs.length === 0 && !userOrgId) return null;
|
|
36
41
|
if (!loaded) return null;
|
|
37
42
|
|
|
38
|
-
var
|
|
43
|
+
var effectiveId = isLocked ? userOrgId : selectedOrgId;
|
|
44
|
+
var selectedOrg = orgs.find(function(o) { return o.id === effectiveId; });
|
|
39
45
|
|
|
40
46
|
return h('div', {
|
|
41
47
|
style: Object.assign({
|
|
@@ -45,41 +51,57 @@ export function OrgContextSwitcher(props) {
|
|
|
45
51
|
}, style)
|
|
46
52
|
},
|
|
47
53
|
showLabel && h('span', { style: { color: 'var(--text-muted)', fontWeight: 600, whiteSpace: 'nowrap' } }, I.building(), ' Viewing:'),
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
54
|
+
isLocked
|
|
55
|
+
? h('div', { style: { fontWeight: 600, fontSize: 13, color: 'var(--text)', display: 'flex', alignItems: 'center', gap: 6 } },
|
|
56
|
+
selectedOrg ? selectedOrg.name : 'Your Organization',
|
|
57
|
+
h('span', { className: 'badge badge-neutral', style: { fontSize: 10 } }, 'Locked')
|
|
58
|
+
)
|
|
59
|
+
: h('select', {
|
|
60
|
+
value: selectedOrgId,
|
|
61
|
+
onChange: function(e) {
|
|
62
|
+
var id = e.target.value;
|
|
63
|
+
var org = orgs.find(function(o) { return o.id === id; });
|
|
64
|
+
onOrgChange(id, org || null);
|
|
65
|
+
},
|
|
66
|
+
style: {
|
|
67
|
+
padding: '6px 10px', borderRadius: 6, border: '1px solid var(--border)',
|
|
68
|
+
background: 'var(--bg-card)', color: 'var(--text)', fontSize: 13,
|
|
69
|
+
cursor: 'pointer', fontWeight: 600, flex: 1, maxWidth: 300
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
h('option', { value: '' }, 'My Organization'),
|
|
73
|
+
orgs.filter(function(o) { return o.is_active !== false; }).map(function(o) {
|
|
74
|
+
return h('option', { key: o.id, value: o.id }, o.name + (o.billing_rate_per_agent > 0 ? ' (' + (o.currency || 'USD') + ' ' + parseFloat(o.billing_rate_per_agent).toFixed(0) + '/agent)' : ''));
|
|
75
|
+
})
|
|
76
|
+
),
|
|
66
77
|
selectedOrg && h('span', { style: { fontSize: 11, color: 'var(--text-muted)' } },
|
|
67
78
|
selectedOrg.contact_name ? selectedOrg.contact_name : '',
|
|
68
79
|
selectedOrg.contact_email ? ' \u2022 ' + selectedOrg.contact_email : ''
|
|
69
|
-
)
|
|
80
|
+
),
|
|
81
|
+
// Impersonation banner
|
|
82
|
+
app.impersonating && h('span', { className: 'badge badge-warning', style: { fontSize: 10, marginLeft: 'auto' } }, 'Impersonating: ' + (user.name || user.email))
|
|
70
83
|
);
|
|
71
84
|
}
|
|
72
85
|
|
|
73
86
|
/**
|
|
74
87
|
* useOrgContext — Hook that provides org switching state.
|
|
75
|
-
*
|
|
88
|
+
* Auto-selects the user's client org if they are org-bound.
|
|
76
89
|
*/
|
|
77
90
|
export function useOrgContext() {
|
|
78
|
-
var
|
|
91
|
+
var app = useApp();
|
|
92
|
+
var user = app.user || {};
|
|
93
|
+
var userOrgId = user.clientOrgId || '';
|
|
94
|
+
|
|
95
|
+
var _sel = useState(userOrgId);
|
|
79
96
|
var selectedOrgId = _sel[0]; var setSelectedOrgId = _sel[1];
|
|
80
97
|
var _org = useState(null);
|
|
81
98
|
var selectedOrg = _org[0]; var setSelectedOrg = _org[1];
|
|
82
99
|
|
|
100
|
+
// If user changes (e.g. impersonation), update default
|
|
101
|
+
useEffect(function() {
|
|
102
|
+
if (userOrgId && !selectedOrgId) setSelectedOrgId(userOrgId);
|
|
103
|
+
}, [userOrgId]);
|
|
104
|
+
|
|
83
105
|
var onOrgChange = function(id, org) {
|
|
84
106
|
setSelectedOrgId(id);
|
|
85
107
|
setSelectedOrg(org);
|
|
@@ -342,9 +342,15 @@ export function OrganizationsPage() {
|
|
|
342
342
|
)
|
|
343
343
|
),
|
|
344
344
|
// Billing rate in header
|
|
345
|
-
detailOrg.billing_rate_per_agent > 0 && h('div', { style: { display: 'flex', alignItems: 'center', gap: 16, padding: '10px 14px', background: 'var(--success-soft, rgba(21,128,61,0.06))', borderRadius: 8, marginBottom: 16, fontSize: 13 } },
|
|
346
|
-
h('div', null, h('strong', null, 'Rate: '), (detailOrg.currency || 'USD') + ' ' + parseFloat(detailOrg.billing_rate_per_agent).toFixed(2) + '/agent/month'),
|
|
347
|
-
h('div', null, h('strong', null, 'Monthly Revenue: '), (
|
|
345
|
+
(detailOrg.billing_rate_per_agent > 0 || detailAgents.some(function(a) { return a.billing_rate > 0; })) && h('div', { style: { display: 'flex', alignItems: 'center', gap: 16, padding: '10px 14px', background: 'var(--success-soft, rgba(21,128,61,0.06))', borderRadius: 8, marginBottom: 16, fontSize: 13, flexWrap: 'wrap' } },
|
|
346
|
+
h('div', null, h('strong', null, 'Default Rate: '), (detailOrg.currency || 'USD') + ' ' + parseFloat(detailOrg.billing_rate_per_agent || 0).toFixed(2) + '/agent/month'),
|
|
347
|
+
h('div', null, h('strong', null, 'Monthly Revenue: '), (function() {
|
|
348
|
+
var total = detailAgents.reduce(function(sum, a) {
|
|
349
|
+
var rate = a.billing_rate > 0 ? parseFloat(a.billing_rate) : parseFloat(detailOrg.billing_rate_per_agent || 0);
|
|
350
|
+
return sum + rate;
|
|
351
|
+
}, 0);
|
|
352
|
+
return (detailOrg.currency || 'USD') + ' ' + total.toFixed(2);
|
|
353
|
+
})()),
|
|
348
354
|
h('div', null, h('strong', null, 'Agents: '), detailAgents.length)
|
|
349
355
|
),
|
|
350
356
|
|
|
@@ -438,6 +444,36 @@ export function OrganizationsPage() {
|
|
|
438
444
|
'No billing data yet. Billing records are created as agents process tasks and accumulate token costs.'
|
|
439
445
|
),
|
|
440
446
|
|
|
447
|
+
// Per-agent billing rates
|
|
448
|
+
detailAgents.length > 0 && h('div', { style: { marginBottom: 20 } },
|
|
449
|
+
h('div', { style: { fontSize: 14, fontWeight: 700, marginBottom: 8 } }, 'Per-Agent Billing Rates'),
|
|
450
|
+
h('div', { style: { fontSize: 11, color: 'var(--text-muted)', marginBottom: 10 } }, 'Set custom billing rates per agent. Leave blank to use the default org rate (' + (detailOrg.currency || 'USD') + ' ' + parseFloat(detailOrg.billing_rate_per_agent || 0).toFixed(2) + '/agent/month).'),
|
|
451
|
+
h('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(220px, 1fr))', gap: 10 } },
|
|
452
|
+
detailAgents.map(function(a) {
|
|
453
|
+
var agentRate = a.billing_rate > 0 ? parseFloat(a.billing_rate) : 0;
|
|
454
|
+
var effectiveRate = agentRate > 0 ? agentRate : parseFloat(detailOrg.billing_rate_per_agent || 0);
|
|
455
|
+
return h('div', { key: a.id, style: { padding: 10, background: 'var(--bg-tertiary)', borderRadius: 8 } },
|
|
456
|
+
h('div', { style: { fontWeight: 600, fontSize: 13, marginBottom: 6 } }, a.name || a.id),
|
|
457
|
+
h('div', { style: { display: 'flex', gap: 6, alignItems: 'center' } },
|
|
458
|
+
h('span', { style: { fontSize: 12, color: 'var(--text-muted)' } }, detailOrg.currency || 'USD'),
|
|
459
|
+
h('input', { className: 'input', type: 'number', step: '0.01', min: '0', value: agentRate > 0 ? agentRate : '',
|
|
460
|
+
placeholder: effectiveRate.toFixed(2),
|
|
461
|
+
onChange: function(e) {
|
|
462
|
+
var val = parseFloat(e.target.value) || 0;
|
|
463
|
+
apiCall('/agents/' + a.id, { method: 'PATCH', body: JSON.stringify({ billingRate: val }) })
|
|
464
|
+
.then(function() { toast('Rate updated for ' + (a.name || a.id), 'success'); })
|
|
465
|
+
.catch(function(err) { toast(err.message, 'error'); });
|
|
466
|
+
},
|
|
467
|
+
style: { width: 90, fontSize: 12, padding: '4px 6px' }
|
|
468
|
+
}),
|
|
469
|
+
h('span', { style: { fontSize: 10, color: 'var(--text-muted)' } }, '/mo')
|
|
470
|
+
),
|
|
471
|
+
agentRate > 0 && h('div', { style: { fontSize: 10, color: 'var(--success, #15803d)', marginTop: 4 } }, 'Custom rate')
|
|
472
|
+
);
|
|
473
|
+
})
|
|
474
|
+
)
|
|
475
|
+
),
|
|
476
|
+
|
|
441
477
|
// Stats summary
|
|
442
478
|
(function() {
|
|
443
479
|
var totRev = billingSummary.reduce(function(a, m) { return a + (parseFloat(m.total_revenue) || 0); }, 0);
|
|
@@ -5,7 +5,7 @@ import { HelpButton } from '../components/help-button.js';
|
|
|
5
5
|
|
|
6
6
|
// ─── Permission Editor Component ───────────────────
|
|
7
7
|
|
|
8
|
-
function PermissionEditor({ userId, userName, currentPerms, pageRegistry, onSave, onClose }) {
|
|
8
|
+
function PermissionEditor({ userId, userName, currentPerms, pageRegistry, onSave, onClose, userObj }) {
|
|
9
9
|
// Deep clone perms (skip _allowedAgents from page grants)
|
|
10
10
|
var [grants, setGrants] = useState(function() {
|
|
11
11
|
if (currentPerms === '*') {
|
|
@@ -34,8 +34,13 @@ function PermissionEditor({ userId, userName, currentPerms, pageRegistry, onSave
|
|
|
34
34
|
return (currentPerms || {})._allowedAgents === '*' || !(currentPerms || {})._allowedAgents;
|
|
35
35
|
});
|
|
36
36
|
|
|
37
|
+
// Client org assignment
|
|
38
|
+
var [permOrgs, setPermOrgs] = useState([]);
|
|
39
|
+
var [userOrgId, setUserOrgId] = useState((userObj && userObj.clientOrgId) || '');
|
|
40
|
+
|
|
37
41
|
useEffect(function() {
|
|
38
42
|
apiCall('/agents').then(function(d) { setAgents(d.agents || d || []); }).catch(function() {});
|
|
43
|
+
apiCall('/organizations').then(function(d) { setPermOrgs(d.organizations || []); }).catch(function() {});
|
|
39
44
|
}, []);
|
|
40
45
|
var [saving, setSaving] = useState(false);
|
|
41
46
|
var [expandedPage, setExpandedPage] = useState(null);
|
|
@@ -130,6 +135,8 @@ function PermissionEditor({ userId, userName, currentPerms, pageRegistry, onSave
|
|
|
130
135
|
}
|
|
131
136
|
}
|
|
132
137
|
await onSave(permsToSave);
|
|
138
|
+
// Save org assignment
|
|
139
|
+
await apiCall('/users/' + userId, { method: 'PATCH', body: JSON.stringify({ clientOrgId: userOrgId || null }) }).catch(function() {});
|
|
133
140
|
} catch(e) { /* handled by parent */ }
|
|
134
141
|
setSaving(false);
|
|
135
142
|
};
|
|
@@ -415,8 +422,9 @@ export function UsersPage() {
|
|
|
415
422
|
var toast = app.toast;
|
|
416
423
|
var [users, setUsers] = useState([]);
|
|
417
424
|
var [creating, setCreating] = useState(false);
|
|
418
|
-
var [form, setForm] = useState({ email: '', password: '', name: '', role: 'viewer', permissions: '*' });
|
|
425
|
+
var [form, setForm] = useState({ email: '', password: '', name: '', role: 'viewer', permissions: '*', clientOrgId: '' });
|
|
419
426
|
var [resetTarget, setResetTarget] = useState(null);
|
|
427
|
+
var [clientOrgs, setClientOrgs] = useState([]);
|
|
420
428
|
var [newPassword, setNewPassword] = useState('');
|
|
421
429
|
var [resetting, setResetting] = useState(false);
|
|
422
430
|
var [permTarget, setPermTarget] = useState(null); // user object for permission editing
|
|
@@ -427,6 +435,7 @@ export function UsersPage() {
|
|
|
427
435
|
useEffect(function() {
|
|
428
436
|
load();
|
|
429
437
|
apiCall('/page-registry').then(function(d) { setPageRegistry(d); }).catch(function() {});
|
|
438
|
+
apiCall('/organizations').then(function(d) { setClientOrgs(d.organizations || []); }).catch(function() {});
|
|
430
439
|
}, []);
|
|
431
440
|
|
|
432
441
|
var generateCreatePassword = function() {
|
|
@@ -442,9 +451,10 @@ export function UsersPage() {
|
|
|
442
451
|
try {
|
|
443
452
|
var body = { email: form.email, password: form.password, name: form.name, role: form.role };
|
|
444
453
|
if (form.permissions !== '*') body.permissions = form.permissions;
|
|
454
|
+
if (form.clientOrgId) body.clientOrgId = form.clientOrgId;
|
|
445
455
|
await apiCall('/users', { method: 'POST', body: JSON.stringify(body) });
|
|
446
456
|
toast('User created. They will be prompted to set a new password on first login.', 'success');
|
|
447
|
-
setCreating(false); setForm({ email: '', password: '', name: '', role: 'viewer', permissions: '*' }); setShowCreatePerms(false); load();
|
|
457
|
+
setCreating(false); setForm({ email: '', password: '', name: '', role: 'viewer', permissions: '*', clientOrgId: '' }); setShowCreatePerms(false); load();
|
|
448
458
|
} catch (e) { toast(e.message, 'error'); }
|
|
449
459
|
};
|
|
450
460
|
|
|
@@ -566,6 +576,17 @@ export function UsersPage() {
|
|
|
566
576
|
)
|
|
567
577
|
),
|
|
568
578
|
h('div', { className: 'form-group' }, h('label', { className: 'form-label' }, 'Role'), h('select', { className: 'input', value: form.role, onChange: function(e) { setForm(function(f) { return Object.assign({}, f, { role: e.target.value }); }); } }, h('option', { value: 'viewer' }, 'Viewer'), h('option', { value: 'member' }, 'Member'), h('option', { value: 'admin' }, 'Admin'), h('option', { value: 'owner' }, 'Owner'))),
|
|
579
|
+
// Client organization assignment
|
|
580
|
+
clientOrgs.length > 0 && h('div', { className: 'form-group' },
|
|
581
|
+
h('label', { className: 'form-label' }, 'Client Organization'),
|
|
582
|
+
h('select', { className: 'input', value: form.clientOrgId, onChange: function(e) { setForm(function(f) { return Object.assign({}, f, { clientOrgId: e.target.value }); }); } },
|
|
583
|
+
h('option', { value: '' }, 'None (internal user)'),
|
|
584
|
+
clientOrgs.filter(function(o) { return o.is_active !== false; }).map(function(o) {
|
|
585
|
+
return h('option', { key: o.id, value: o.id }, o.name);
|
|
586
|
+
})
|
|
587
|
+
),
|
|
588
|
+
h('div', { style: { fontSize: 11, color: 'var(--text-muted)', marginTop: 4 } }, 'Assigning to a client org restricts this user to only see that organization\'s agents and data.')
|
|
589
|
+
),
|
|
569
590
|
// Inline permissions for member/viewer
|
|
570
591
|
(form.role === 'member' || form.role === 'viewer') && h('div', { style: { marginTop: 4 } },
|
|
571
592
|
h('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between' } },
|
|
@@ -700,7 +721,7 @@ export function UsersPage() {
|
|
|
700
721
|
h('div', { className: 'card-body-flush' },
|
|
701
722
|
users.length === 0 ? h('div', { style: { padding: 24, textAlign: 'center', color: 'var(--text-muted)' } }, 'No users')
|
|
702
723
|
: h('table', null,
|
|
703
|
-
h('thead', null, h('tr', null, h('th', null, 'Name'), h('th', null, 'Email'), h('th', null, 'Role'), h('th', null, 'Status'), h('th', null, 'Access'), h('th', null, '2FA'), h('th', null, 'Created'), h('th', { style: { width:
|
|
724
|
+
h('thead', null, h('tr', null, h('th', null, 'Name'), h('th', null, 'Email'), h('th', null, 'Role'), h('th', null, 'Organization'), h('th', null, 'Status'), h('th', null, 'Access'), h('th', null, '2FA'), h('th', null, 'Created'), h('th', { style: { width: 240 } }, 'Actions'))),
|
|
704
725
|
h('tbody', null, users.map(function(u) {
|
|
705
726
|
var isRestricted = u.role === 'member' || u.role === 'viewer';
|
|
706
727
|
var isDeactivated = u.isActive === false;
|
|
@@ -709,6 +730,10 @@ export function UsersPage() {
|
|
|
709
730
|
h('td', null, h('strong', null, u.name || '-')),
|
|
710
731
|
h('td', null, h('span', { style: { fontFamily: 'var(--font-mono)', fontSize: 12 } }, u.email)),
|
|
711
732
|
h('td', null, h('span', { className: 'badge badge-' + (u.role === 'owner' ? 'warning' : u.role === 'admin' ? 'primary' : 'neutral') }, u.role)),
|
|
733
|
+
h('td', null, (function() {
|
|
734
|
+
var org = u.clientOrgId && clientOrgs.find(function(o) { return o.id === u.clientOrgId; });
|
|
735
|
+
return org ? h('span', { className: 'badge badge-info', style: { fontSize: 10 } }, org.name) : h('span', { style: { color: 'var(--text-muted)', fontSize: 11 } }, 'Internal');
|
|
736
|
+
})()),
|
|
712
737
|
h('td', null, isDeactivated
|
|
713
738
|
? h('span', { className: 'badge badge-danger', style: { fontSize: 10 } }, 'Deactivated')
|
|
714
739
|
: h('span', { className: 'badge badge-success', style: { fontSize: 10 } }, 'Active')
|
|
@@ -725,6 +750,13 @@ export function UsersPage() {
|
|
|
725
750
|
style: !isRestricted ? { opacity: 0.4 } : {}
|
|
726
751
|
}, I.shield()),
|
|
727
752
|
h('button', { className: 'btn btn-ghost btn-sm', title: 'Reset Password', onClick: function() { setResetTarget(u); setNewPassword(''); } }, I.lock()),
|
|
753
|
+
// Impersonate (owner-only, not self)
|
|
754
|
+
!isSelf && app.user && app.user.role === 'owner' && !isDeactivated && h('button', {
|
|
755
|
+
className: 'btn btn-ghost btn-sm',
|
|
756
|
+
title: 'View as ' + (u.name || u.email),
|
|
757
|
+
onClick: function() { if (app.startImpersonation) app.startImpersonation(u.id); },
|
|
758
|
+
style: { color: 'var(--primary)' }
|
|
759
|
+
}, I.agents()),
|
|
728
760
|
// Deactivate / Reactivate
|
|
729
761
|
!isSelf && h('button', {
|
|
730
762
|
className: 'btn btn-ghost btn-sm',
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
provision,
|
|
3
3
|
runSetupWizard
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-4VGAZULN.js";
|
|
5
5
|
import {
|
|
6
6
|
AgenticMailManager,
|
|
7
7
|
GoogleEmailProvider,
|
|
@@ -42,7 +42,7 @@ import {
|
|
|
42
42
|
requireRole,
|
|
43
43
|
securityHeaders,
|
|
44
44
|
validate
|
|
45
|
-
} from "./chunk-
|
|
45
|
+
} from "./chunk-CRXYUYVJ.js";
|
|
46
46
|
import "./chunk-OF4MUWWS.js";
|
|
47
47
|
import {
|
|
48
48
|
PROVIDER_REGISTRY,
|
|
@@ -113,7 +113,7 @@ import {
|
|
|
113
113
|
import {
|
|
114
114
|
createAdapter,
|
|
115
115
|
getSupportedDatabases
|
|
116
|
-
} from "./chunk-
|
|
116
|
+
} from "./chunk-QCGSFWKU.js";
|
|
117
117
|
import {
|
|
118
118
|
AGENTICMAIL_TOOLS,
|
|
119
119
|
ALL_TOOLS,
|