@naisys/erp 3.0.0-beta.36 → 3.0.0-beta.38

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.
@@ -724,7 +724,7 @@ var CompactMarkdown = ({ children }) => (0, import_jsx_runtime.jsx)(Markdown, {
724
724
  });
725
725
  //#endregion
726
726
  //#region ../../../packages/common-browser/dist/SecretField.js
727
- var SecretField = ({ value, onRotate, rotating }) => {
727
+ var SecretField = ({ value, onRotate, rotating, emptyLabel = "Not set" }) => {
728
728
  const [visible, setVisible] = (0, import_react.useState)(false);
729
729
  return (0, import_jsx_runtime.jsxs)(Group, {
730
730
  gap: "xs",
@@ -759,7 +759,7 @@ var SecretField = ({ value, onRotate, rotating }) => {
759
759
  ] }) : (0, import_jsx_runtime.jsx)(Text, {
760
760
  c: "dimmed",
761
761
  size: "sm",
762
- children: "Not set"
762
+ children: emptyLabel
763
763
  }), onRotate && (0, import_jsx_runtime.jsx)(Tooltip, {
764
764
  label: value ? "Rotate key" : "Generate API key",
765
765
  children: (0, import_jsx_runtime.jsx)(ActionIcon, {
@@ -1691,7 +1691,6 @@ CreateResponseSchema.extend({ runNo: number$2() });
1691
1691
  object$1({
1692
1692
  id: number$2(),
1693
1693
  username: string$1(),
1694
- apiKey: string$1().nullable().optional(),
1695
1694
  _links: array$1(HateoasLinkSchema).optional(),
1696
1695
  _actions: array$1(HateoasActionSchema).optional()
1697
1696
  });
@@ -1953,7 +1952,7 @@ object$1({
1953
1952
  isAgent: boolean$1(),
1954
1953
  createdAt: string$1(),
1955
1954
  updatedAt: string$1(),
1956
- apiKey: string$1().nullable().optional(),
1955
+ hasApiKey: boolean$1(),
1957
1956
  permissions: array$1(UserPermissionSchema),
1958
1957
  _links: array$1(any()).optional(),
1959
1958
  _actions: array$1(any()).optional()
@@ -9731,6 +9730,7 @@ var UserDetail = () => {
9731
9730
  const [editError, setEditError] = (0, import_react.useState)("");
9732
9731
  const [grantPerm, setGrantPerm] = (0, import_react.useState)(null);
9733
9732
  const [rotating, setRotating] = (0, import_react.useState)(false);
9733
+ const [apiKey, setApiKey] = (0, import_react.useState)(null);
9734
9734
  const [pwOpened, { open: openPw, close: closePw }] = useDisclosure();
9735
9735
  const [newPassword, setNewPassword] = (0, import_react.useState)("");
9736
9736
  const [pwSaving, setPwSaving] = (0, import_react.useState)(false);
@@ -9740,6 +9740,7 @@ var UserDetail = () => {
9740
9740
  setLoading(true);
9741
9741
  try {
9742
9742
  setUser(await api.get(apiEndpoints.user(routeUsername)));
9743
+ setApiKey(null);
9743
9744
  } catch {} finally {
9744
9745
  setLoading(false);
9745
9746
  }
@@ -9806,11 +9807,15 @@ var UserDetail = () => {
9806
9807
  };
9807
9808
  const handleRotateKey = async () => {
9808
9809
  if (!routeUsername) return;
9809
- if (!confirm("Rotate this user's API key? The old key will stop working immediately.")) return;
9810
+ if (!confirm("Generate a new API key? The old key will stop working immediately.")) return;
9810
9811
  setRotating(true);
9811
9812
  try {
9812
- await api.post(apiEndpoints.userRotateKey(routeUsername), {});
9813
- fetchUser();
9813
+ const result = await api.post(apiEndpoints.userRotateKey(routeUsername), {});
9814
+ setApiKey(result.apiKey ?? null);
9815
+ setUser((current) => current ? {
9816
+ ...current,
9817
+ hasApiKey: !!result.apiKey
9818
+ } : current);
9814
9819
  } catch (err) {
9815
9820
  showErrorNotification(err);
9816
9821
  } finally {
@@ -9912,12 +9917,28 @@ var UserDetail = () => {
9912
9917
  w: 120,
9913
9918
  children: "Type:"
9914
9919
  }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: user.isAgent ? "Agent" : "User" })] }),
9915
- (user.apiKey || hasAction(user._actions, "rotate-key")) && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Group, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
9920
+ supervisorAuth ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Group, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
9921
+ fw: 600,
9922
+ w: 120,
9923
+ children: "Credentials:"
9924
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
9925
+ size: "sm",
9926
+ children: [
9927
+ "Password and API key are managed in the",
9928
+ " ",
9929
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Anchor, {
9930
+ href: `/supervisor/users/${user.username}`,
9931
+ children: "supervisor"
9932
+ }),
9933
+ "."
9934
+ ]
9935
+ })] }) : (user.hasApiKey || hasAction(user._actions, "rotate-key")) && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Group, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
9916
9936
  fw: 600,
9917
9937
  w: 120,
9918
9938
  children: "API Key:"
9919
9939
  }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SecretField, {
9920
- value: user.apiKey ?? null,
9940
+ value: apiKey,
9941
+ emptyLabel: user.hasApiKey ? "Generated (hidden)" : "Not set",
9921
9942
  onRotate: hasAction(user._actions, "rotate-key") ? handleRotateKey : void 0,
9922
9943
  rotating
9923
9944
  })] })
@@ -45,7 +45,7 @@
45
45
  <meta name="format-detection" content="telephone=no" />
46
46
 
47
47
  <title>NAISYS ERP</title>
48
- <script type="module" crossorigin src="/erp/assets/index-jH5OYerq.js"></script>
48
+ <script type="module" crossorigin src="/erp/assets/index-By5W5npg.js"></script>
49
49
  <link rel="modulepreload" crossorigin href="/erp/assets/rolldown-runtime-CvHMtSRF.js">
50
50
  <link rel="modulepreload" crossorigin href="/erp/assets/vendor-DFaFIeiT.js">
51
51
  <link rel="stylesheet" crossorigin href="/erp/assets/vendor-CLUPjUnv.css">
@@ -1,4 +1,4 @@
1
- import { AuthCache } from "@naisys/common";
1
+ import { AuthCache, urlMatchesPrefix } from "@naisys/common";
2
2
  import { extractBearerToken, hashToken, SESSION_COOKIE_NAME, } from "@naisys/common-node";
3
3
  import { findAgentByApiKey } from "@naisys/hub-database";
4
4
  import { findSession, findUserByApiKey } from "@naisys/supervisor-database";
@@ -13,6 +13,63 @@ async function loadPermissions(userId) {
13
13
  });
14
14
  return perms.map((p) => p.permission);
15
15
  }
16
+ async function materializeErpUser(localUser) {
17
+ return {
18
+ id: localUser.id,
19
+ username: localUser.username,
20
+ permissions: await loadPermissions(localUser.id),
21
+ };
22
+ }
23
+ async function resolveCookie(token) {
24
+ const tokenHash = hashToken(token);
25
+ return authCache.getOrLoad(`cookie:${tokenHash}`, async () => {
26
+ const localUser = isSupervisorAuth()
27
+ ? await loadCookieUserSso(tokenHash)
28
+ : await loadCookieUserStandalone(tokenHash);
29
+ return localUser ? materializeErpUser(localUser) : null;
30
+ });
31
+ }
32
+ async function loadCookieUserSso(tokenHash) {
33
+ const session = await findSession(tokenHash);
34
+ if (!session)
35
+ return null;
36
+ return erpDb.user.upsert({
37
+ where: { uuid: session.uuid },
38
+ create: { uuid: session.uuid, username: session.username },
39
+ update: {},
40
+ });
41
+ }
42
+ async function loadCookieUserStandalone(tokenHash) {
43
+ const session = await erpDb.session.findUnique({
44
+ where: { tokenHash, expiresAt: { gt: new Date() } },
45
+ include: { user: true },
46
+ });
47
+ return session?.user ?? null;
48
+ }
49
+ async function resolveApiKey(apiKey) {
50
+ const apiKeyHash = hashToken(apiKey);
51
+ return authCache.getOrLoad(`apikey:${apiKeyHash}`, async () => {
52
+ const localUser = isSupervisorAuth()
53
+ ? await loadApiKeyUserSso(apiKey)
54
+ : await erpDb.user.findUnique({ where: { apiKeyHash } });
55
+ return localUser ? materializeErpUser(localUser) : null;
56
+ });
57
+ }
58
+ async function loadApiKeyUserSso(apiKey) {
59
+ // Try supervisor DB (humans + agents with external keys),
60
+ // then hub DB (agents matching their hub-issued runtime key).
61
+ const supervisorUser = await findUserByApiKey(apiKey);
62
+ const hubAgent = supervisorUser ? null : await findAgentByApiKey(apiKey);
63
+ const match = supervisorUser ?? hubAgent;
64
+ if (!match)
65
+ return null;
66
+ const isAgent = supervisorUser?.isAgent ?? !!hubAgent;
67
+ return erpDb.user.upsert({
68
+ where: { uuid: match.uuid },
69
+ create: { uuid: match.uuid, username: match.username, isAgent },
70
+ update: {},
71
+ });
72
+ }
16
73
  export function hasPermission(user, permission) {
17
74
  if (!user)
18
75
  return false;
@@ -44,13 +101,11 @@ function isPublicRoute(url) {
44
101
  // Exact match: API root
45
102
  if (url === "/erp/api/" || url === "/erp/api")
46
103
  return true;
47
- // Prefix matches
48
104
  for (const prefix of PUBLIC_PREFIXES) {
49
- if (url.startsWith(prefix))
105
+ if (urlMatchesPrefix(url, prefix))
50
106
  return true;
51
107
  }
52
- // Schema routes
53
- if (url.startsWith("/erp/api/schemas"))
108
+ if (urlMatchesPrefix(url, "/erp/api/schemas"))
54
109
  return true;
55
110
  // Non-ERP-API paths (static files, supervisor routes, etc.)
56
111
  if (!url.startsWith("/erp/api"))
@@ -63,131 +118,18 @@ export function registerAuthMiddleware(fastify) {
63
118
  fastify.addHook("onRequest", async (request, reply) => {
64
119
  const token = request.cookies?.[SESSION_COOKIE_NAME];
65
120
  if (token) {
66
- const tokenHash = hashToken(token);
67
- const cacheKey = `cookie:${tokenHash}`;
68
- const cached = authCache.get(cacheKey);
69
- if (cached !== undefined) {
70
- // Cache hit (valid or negative)
71
- if (cached)
72
- request.erpUser = cached;
73
- }
74
- else if (isSupervisorAuth()) {
75
- // SSO mode: supervisor DB is source of truth for sessions
76
- const session = await findSession(tokenHash);
77
- if (session) {
78
- let localUser = await erpDb.user.findUnique({
79
- where: { uuid: session.uuid },
80
- });
81
- if (!localUser) {
82
- localUser = await erpDb.user.create({
83
- data: {
84
- uuid: session.uuid,
85
- username: session.username,
86
- passwordHash: session.passwordHash,
87
- },
88
- });
89
- }
90
- const permissions = await loadPermissions(localUser.id);
91
- const erpUser = {
92
- id: localUser.id,
93
- username: localUser.username,
94
- permissions,
95
- };
96
- authCache.set(cacheKey, erpUser);
97
- request.erpUser = erpUser;
98
- }
99
- else {
100
- authCache.set(cacheKey, null);
101
- }
102
- }
103
- else {
104
- // Standalone mode: local session only
105
- const session = await erpDb.session.findUnique({
106
- where: {
107
- tokenHash,
108
- expiresAt: { gt: new Date() },
109
- },
110
- include: { user: true },
111
- });
112
- if (session) {
113
- const permissions = await loadPermissions(session.user.id);
114
- const erpUser = {
115
- id: session.user.id,
116
- username: session.user.username,
117
- permissions,
118
- };
119
- authCache.set(cacheKey, erpUser);
120
- request.erpUser = erpUser;
121
- }
122
- else {
123
- authCache.set(cacheKey, null);
124
- }
125
- }
121
+ const user = await resolveCookie(token);
122
+ if (user)
123
+ request.erpUser = user;
126
124
  }
127
- // API key auth (for agents / machine-to-machine)
128
125
  if (!request.erpUser) {
129
126
  const apiKey = extractBearerToken(request.headers.authorization);
130
127
  if (apiKey) {
131
- const apiKeyHash = hashToken(apiKey);
132
- const cacheKey = `apikey:${apiKeyHash}`;
133
- const cached = authCache.get(cacheKey);
134
- if (cached !== undefined) {
135
- if (cached)
136
- request.erpUser = cached;
137
- }
138
- else if (isSupervisorAuth()) {
139
- // SSO mode: try supervisor DB (human users), then hub DB (agents)
140
- const match = (await findUserByApiKey(apiKey)) ??
141
- (await findAgentByApiKey(apiKey));
142
- if (match) {
143
- let localUser = await erpDb.user.findUnique({
144
- where: { uuid: match.uuid },
145
- });
146
- if (!localUser) {
147
- localUser = await erpDb.user.create({
148
- data: {
149
- uuid: match.uuid,
150
- username: match.username,
151
- passwordHash: "!api-key-only",
152
- isAgent: true,
153
- },
154
- });
155
- }
156
- const permissions = await loadPermissions(localUser.id);
157
- const erpUser = {
158
- id: localUser.id,
159
- username: localUser.username,
160
- permissions,
161
- };
162
- authCache.set(cacheKey, erpUser);
163
- request.erpUser = erpUser;
164
- }
165
- else {
166
- authCache.set(cacheKey, null);
167
- }
168
- }
169
- else {
170
- // Standalone mode: check local ERP user table
171
- const localUser = await erpDb.user.findUnique({
172
- where: { apiKey },
173
- });
174
- if (localUser) {
175
- const permissions = await loadPermissions(localUser.id);
176
- const erpUser = {
177
- id: localUser.id,
178
- username: localUser.username,
179
- permissions,
180
- };
181
- authCache.set(cacheKey, erpUser);
182
- request.erpUser = erpUser;
183
- }
184
- else {
185
- authCache.set(cacheKey, null);
186
- }
187
- }
128
+ const user = await resolveApiKey(apiKey);
129
+ if (user)
130
+ request.erpUser = user;
188
131
  }
189
132
  }
190
- // Check if auth is required
191
133
  if (request.erpUser)
192
134
  return; // Authenticated, always allowed
193
135
  if (isPublicRoute(request.url))
package/dist/dbConfig.js CHANGED
@@ -6,5 +6,5 @@ export function erpDbUrl() {
6
6
  return "file:" + erpDbPath();
7
7
  }
8
8
  /** We run migration scripts if this is greater than what's in the schema_version table */
9
- export const ERP_DB_VERSION = 43;
9
+ export const ERP_DB_VERSION = 44;
10
10
  //# sourceMappingURL=dbConfig.js.map
package/dist/erpServer.js CHANGED
@@ -129,6 +129,9 @@ async function startServer(wizardRan) {
129
129
  const isProd = process.env.NODE_ENV === "production";
130
130
  const fastify = Fastify({
131
131
  pluginTimeout: 60_000,
132
+ // trustProxy: TLS terminates at the reverse proxy, so honor X-Forwarded-*
133
+ // headers — otherwise request.protocol reads the internal http hop.
134
+ trustProxy: true,
132
135
  logger: {
133
136
  transport: {
134
137
  target: "pino-pretty",