@naisys/erp 3.0.0-beta.37 → 3.0.0-beta.39
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/client-dist/assets/{index-jH5OYerq.js → index-CBngjRDx.js} +31 -10
- package/client-dist/index.html +1 -1
- package/dist/auth-middleware.js +66 -124
- package/dist/dbConfig.js +1 -1
- package/dist/erpServer.js +14 -2
- package/dist/generated/prisma/internal/class.js +4 -4
- package/dist/generated/prisma/internal/prismaNamespace.js +1 -1
- package/dist/routes/auth.js +1 -1
- package/dist/routes/user-permissions.js +19 -5
- package/dist/routes/users.js +37 -16
- package/dist/services/user-service.js +14 -31
- package/dist/userService.js +3 -7
- package/npm-shrinkwrap.json +28 -28
- package/package.json +6 -6
- package/prisma/migrations/20260427010000_hash_user_api_keys/migration.sql +10 -0
- package/prisma/migrations/20260427020000_nullable_user_password_hash/migration.sql +39 -0
- package/prisma/schema.prisma +2 -2
|
@@ -47,7 +47,7 @@ object({
|
|
|
47
47
|
chatEnabled: boolean().optional().describe("Show chat commands to the agent. Chat encourages more concise communication"),
|
|
48
48
|
webEnabled: boolean().optional().describe("Allow agent to browse the web with a context-optimized text browser built on Lynx. Javascript not supported. Requires `lynx` on the host (e.g. `apt install lynx`)"),
|
|
49
49
|
browserEnabled: boolean().optional().describe("Allow agent to browse the web with a real headless Chromium browser via Playwright. Vision-capable models see screenshots; others fall back to a text/selector mode. Requires `npx playwright install chromium` on the host"),
|
|
50
|
-
completeSessionEnabled: boolean().optional().describe("Allow the agent to end its session. Once ended, it can only be restarted explicitly or via mail if wakeOnMessage is enabled. Disable on root agents to prevent the system from going unresponsive"),
|
|
50
|
+
completeSessionEnabled: boolean().optional().describe("Allow the agent to end its session. Once ended, it can only be restarted explicitly or via chat/mail if wakeOnMessage is enabled. Disable on root agents to prevent the system from going unresponsive"),
|
|
51
51
|
debugPauseSeconds: number().int("Must be a whole number").min(0, "Must be non-negative").optional().describe("Seconds to wait at the debug prompt before auto-continuing, only applies when the agent's console is in focus. Set to 0 to continue immediately. Unset waits indefinitely for manual input"),
|
|
52
52
|
wakeOnMessage: boolean().optional().describe("When mail or chat is received, start the agent automatically, or wake it from its wait state"),
|
|
53
53
|
commandProtection: _enum([
|
|
@@ -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:
|
|
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
|
-
|
|
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("
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
})] })
|
package/client-dist/index.html
CHANGED
|
@@ -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-
|
|
48
|
+
<script type="module" crossorigin src="/erp/assets/index-CBngjRDx.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">
|
package/dist/auth-middleware.js
CHANGED
|
@@ -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
|
|
105
|
+
if (urlMatchesPrefix(url, prefix))
|
|
50
106
|
return true;
|
|
51
107
|
}
|
|
52
|
-
|
|
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
|
|
67
|
-
|
|
68
|
-
|
|
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: "!sso-passkey-only",
|
|
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
|
|
132
|
-
|
|
133
|
-
|
|
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
package/dist/erpServer.js
CHANGED
|
@@ -17,6 +17,7 @@ import Fastify from "fastify";
|
|
|
17
17
|
import { jsonSchemaTransform, jsonSchemaTransformObject, serializerCompiler, validatorCompiler, } from "fastify-type-provider-zod";
|
|
18
18
|
import path from "path";
|
|
19
19
|
import { fileURLToPath } from "url";
|
|
20
|
+
import { takeCoverage } from "v8";
|
|
20
21
|
import { registerApiReference } from "./api-reference.js";
|
|
21
22
|
import { registerAuthMiddleware } from "./auth-middleware.js";
|
|
22
23
|
import { ERP_DB_VERSION, erpDbPath } from "./dbConfig.js";
|
|
@@ -129,6 +130,9 @@ async function startServer(wizardRan) {
|
|
|
129
130
|
const isProd = process.env.NODE_ENV === "production";
|
|
130
131
|
const fastify = Fastify({
|
|
131
132
|
pluginTimeout: 60_000,
|
|
133
|
+
// trustProxy: TLS terminates at the reverse proxy, so honor X-Forwarded-*
|
|
134
|
+
// headers — otherwise request.protocol reads the internal http hop.
|
|
135
|
+
trustProxy: true,
|
|
132
136
|
logger: {
|
|
133
137
|
transport: {
|
|
134
138
|
target: "pino-pretty",
|
|
@@ -159,6 +163,12 @@ async function startServer(wizardRan) {
|
|
|
159
163
|
fastify.get("/", { schema: { hide: true } }, async (_request, reply) => {
|
|
160
164
|
return reply.redirect("/erp/");
|
|
161
165
|
});
|
|
166
|
+
if (process.env.NODE_V8_COVERAGE) {
|
|
167
|
+
fastify.post("/erp/api/__coverage/flush", { schema: { hide: true } }, () => {
|
|
168
|
+
takeCoverage();
|
|
169
|
+
return { ok: true };
|
|
170
|
+
});
|
|
171
|
+
}
|
|
162
172
|
const superAdminPassword = wizardRan && !isSupervisorAuth()
|
|
163
173
|
? await promptSuperAdminPassword("ERP Setup")
|
|
164
174
|
: undefined;
|
|
@@ -196,7 +206,6 @@ if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
|
196
206
|
},
|
|
197
207
|
{ key: "SERVER_PORT", label: "Server Port" },
|
|
198
208
|
{ key: "SUPERVISOR_AUTH", label: "Use Supervisor for Auth" },
|
|
199
|
-
{ key: "PUBLIC_READ", label: "Public Read Access" },
|
|
200
209
|
],
|
|
201
210
|
},
|
|
202
211
|
],
|
|
@@ -207,7 +216,10 @@ if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
|
207
216
|
wizardRan = await runSetupWizard(path.resolve(".env"), erpExampleUrl, erpWizardConfig);
|
|
208
217
|
expandNaisysFolder();
|
|
209
218
|
}
|
|
210
|
-
|
|
219
|
+
if (process.env.NAISYS_SKIP_DOTENV_CHECK !== "1") {
|
|
220
|
+
wizardRan =
|
|
221
|
+
(await ensureDotEnv(erpExampleUrl, erpWizardConfig)) || wizardRan;
|
|
222
|
+
}
|
|
211
223
|
const fastify = await startServer(wizardRan);
|
|
212
224
|
let shuttingDown = false;
|
|
213
225
|
const handleShutdown = async (signal) => {
|