@naisys/erp 3.0.0-beta.37 → 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.
- package/client-dist/assets/{index-jH5OYerq.js → index-By5W5npg.js} +30 -9
- package/client-dist/index.html +1 -1
- package/dist/auth-middleware.js +66 -124
- package/dist/dbConfig.js +1 -1
- package/dist/erpServer.js +3 -0
- 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
package/dist/routes/auth.js
CHANGED
|
@@ -38,7 +38,7 @@ export default function authRoutes(fastify) {
|
|
|
38
38
|
}
|
|
39
39
|
// Standalone mode: authenticate against local DB
|
|
40
40
|
const user = await erpDb.user.findUnique({ where: { username } });
|
|
41
|
-
if (!user) {
|
|
41
|
+
if (!user || user.passwordHash === null) {
|
|
42
42
|
return unauthorized(reply, "Invalid username or password");
|
|
43
43
|
}
|
|
44
44
|
const valid = await bcrypt.compare(password, user.passwordHash);
|
|
@@ -2,7 +2,8 @@ import { ErpPermissionEnum, GrantPermissionSchema } from "@naisys/erp-shared";
|
|
|
2
2
|
import { z } from "zod/v4";
|
|
3
3
|
import { authCache, requirePermission } from "../auth-middleware.js";
|
|
4
4
|
import { mutationResult } from "../route-helpers.js";
|
|
5
|
-
import { getUserById, getUserByUsername, grantPermission, revokePermission, rotateUserApiKey, } from "../services/user-service.js";
|
|
5
|
+
import { getUserById, getUserByUsername, grantPermission, hasUserApiKey, revokePermission, rotateUserApiKey, } from "../services/user-service.js";
|
|
6
|
+
import { isSupervisorAuth } from "../supervisorAuth.js";
|
|
6
7
|
import { formatUser } from "./users.js";
|
|
7
8
|
export default function userPermissionRoutes(fastify) {
|
|
8
9
|
const app = fastify.withTypeProvider();
|
|
@@ -17,14 +18,25 @@ export default function userPermissionRoutes(fastify) {
|
|
|
17
18
|
params: usernameParams,
|
|
18
19
|
},
|
|
19
20
|
}, async (request, reply) => {
|
|
21
|
+
if (isSupervisorAuth()) {
|
|
22
|
+
reply.code(400);
|
|
23
|
+
return {
|
|
24
|
+
success: false,
|
|
25
|
+
message: "API keys are managed by the supervisor when SSO is enabled.",
|
|
26
|
+
};
|
|
27
|
+
}
|
|
20
28
|
const targetUser = await getUserByUsername(request.params.username);
|
|
21
29
|
if (!targetUser) {
|
|
22
30
|
reply.code(404);
|
|
23
31
|
return { success: false, message: "User not found" };
|
|
24
32
|
}
|
|
25
|
-
await rotateUserApiKey(targetUser.id);
|
|
33
|
+
const apiKey = await rotateUserApiKey(targetUser.id);
|
|
26
34
|
authCache.clear();
|
|
27
|
-
return {
|
|
35
|
+
return {
|
|
36
|
+
success: true,
|
|
37
|
+
message: "API key generated. Copy it now; it cannot be shown again.",
|
|
38
|
+
apiKey,
|
|
39
|
+
};
|
|
28
40
|
});
|
|
29
41
|
// GRANT PERMISSION
|
|
30
42
|
app.post("/:username/permissions", {
|
|
@@ -45,7 +57,8 @@ export default function userPermissionRoutes(fastify) {
|
|
|
45
57
|
await grantPermission(targetUser.id, request.body.permission, request.erpUser.id);
|
|
46
58
|
authCache.clear();
|
|
47
59
|
const user = await getUserById(targetUser.id);
|
|
48
|
-
const
|
|
60
|
+
const hasApiKey = user ? await hasUserApiKey(user.id) : false;
|
|
61
|
+
const full = formatUser(user, request.erpUser.id, request.erpUser.permissions, { hasApiKey });
|
|
49
62
|
return mutationResult(request, reply, full, {
|
|
50
63
|
_actions: full._actions,
|
|
51
64
|
});
|
|
@@ -91,7 +104,8 @@ export default function userPermissionRoutes(fastify) {
|
|
|
91
104
|
await revokePermission(targetUser.id, permission);
|
|
92
105
|
authCache.clear();
|
|
93
106
|
const user = await getUserById(targetUser.id);
|
|
94
|
-
const
|
|
107
|
+
const hasApiKey = user ? await hasUserApiKey(user.id) : false;
|
|
108
|
+
const full = formatUser(user, request.erpUser.id, request.erpUser.permissions, { hasApiKey });
|
|
95
109
|
return mutationResult(request, reply, full, {
|
|
96
110
|
_actions: full._actions,
|
|
97
111
|
});
|
package/dist/routes/users.js
CHANGED
|
@@ -4,7 +4,7 @@ import { z } from "zod/v4";
|
|
|
4
4
|
import { authCache, hasPermission, requirePermission, } from "../auth-middleware.js";
|
|
5
5
|
import { API_PREFIX, collectionLink, paginationLinks, schemaLink, selfLink, } from "../hateoas.js";
|
|
6
6
|
import { mutationResult } from "../route-helpers.js";
|
|
7
|
-
import { createUserForAgent, createUserWithPassword, deleteUser,
|
|
7
|
+
import { createUserForAgent, createUserWithPassword, deleteUser, getUserByUsername, getUserByUuid, hasUserApiKey, listUsers, updateUser, } from "../services/user-service.js";
|
|
8
8
|
import { isSupervisorAuth } from "../supervisorAuth.js";
|
|
9
9
|
function userItemLinks(username) {
|
|
10
10
|
return [
|
|
@@ -26,7 +26,10 @@ function userActions(username, isSelf, isAdmin) {
|
|
|
26
26
|
body: { username: "" },
|
|
27
27
|
});
|
|
28
28
|
}
|
|
29
|
-
|
|
29
|
+
// In SSO mode the supervisor owns passwords (passkey-only) and external API
|
|
30
|
+
// keys, so ERP doesn't expose its own change-password / rotate-key actions.
|
|
31
|
+
const sso = isSupervisorAuth();
|
|
32
|
+
if (isSelf && !sso) {
|
|
30
33
|
actions.push({
|
|
31
34
|
rel: "change-password",
|
|
32
35
|
href: `${API_PREFIX}/users/me/password`,
|
|
@@ -45,12 +48,14 @@ function userActions(username, isSelf, isAdmin) {
|
|
|
45
48
|
schema: `${API_PREFIX}/schemas/GrantPermission`,
|
|
46
49
|
body: { permission: "" },
|
|
47
50
|
});
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
if (!sso) {
|
|
52
|
+
actions.push({
|
|
53
|
+
rel: "rotate-key",
|
|
54
|
+
href: `${href}/rotate-key`,
|
|
55
|
+
method: "POST",
|
|
56
|
+
title: "Generate API Key",
|
|
57
|
+
});
|
|
58
|
+
}
|
|
54
59
|
if (!isSelf) {
|
|
55
60
|
actions.push({
|
|
56
61
|
rel: "delete",
|
|
@@ -88,7 +93,7 @@ export function formatUser(user, currentUserId, currentUserPermissions, options)
|
|
|
88
93
|
isAgent: user.isAgent,
|
|
89
94
|
createdAt: user.createdAt.toISOString(),
|
|
90
95
|
updatedAt: user.updatedAt.toISOString(),
|
|
91
|
-
|
|
96
|
+
hasApiKey: options?.hasApiKey ?? false,
|
|
92
97
|
permissions: user.permissions.map((p) => ({
|
|
93
98
|
permission: p.permission,
|
|
94
99
|
grantedAt: p.grantedAt.toISOString(),
|
|
@@ -201,6 +206,13 @@ export default function userRoutes(fastify) {
|
|
|
201
206
|
});
|
|
202
207
|
return;
|
|
203
208
|
}
|
|
209
|
+
if (isSupervisorAuth()) {
|
|
210
|
+
reply.code(400);
|
|
211
|
+
return {
|
|
212
|
+
success: false,
|
|
213
|
+
message: "Passwords are managed by the supervisor when SSO is enabled.",
|
|
214
|
+
};
|
|
215
|
+
}
|
|
204
216
|
await updateUser(request.erpUser.id, {
|
|
205
217
|
password: request.body.password,
|
|
206
218
|
});
|
|
@@ -223,7 +235,6 @@ export default function userRoutes(fastify) {
|
|
|
223
235
|
return mutationResult(request, reply, full, {
|
|
224
236
|
id: full.id,
|
|
225
237
|
username: full.username,
|
|
226
|
-
apiKey: full.apiKey,
|
|
227
238
|
_links: full._links,
|
|
228
239
|
_actions: full._actions,
|
|
229
240
|
});
|
|
@@ -288,7 +299,6 @@ export default function userRoutes(fastify) {
|
|
|
288
299
|
return mutationResult(request, reply, full, {
|
|
289
300
|
id: full.id,
|
|
290
301
|
username: full.username,
|
|
291
|
-
apiKey: full.apiKey,
|
|
292
302
|
_links: full._links,
|
|
293
303
|
_actions: full._actions,
|
|
294
304
|
});
|
|
@@ -319,8 +329,10 @@ export default function userRoutes(fastify) {
|
|
|
319
329
|
reply.code(404);
|
|
320
330
|
return { success: false, message: "User not found" };
|
|
321
331
|
}
|
|
322
|
-
const
|
|
323
|
-
|
|
332
|
+
const hasApiKey = isSupervisorAuth()
|
|
333
|
+
? false
|
|
334
|
+
: await hasUserApiKey(user.id);
|
|
335
|
+
return formatUser(user, request.erpUser.id, request.erpUser.permissions, { hasApiKey });
|
|
324
336
|
});
|
|
325
337
|
// UPDATE USER (admin can update any field; non-admin can only change own password)
|
|
326
338
|
app.put("/:username", {
|
|
@@ -338,12 +350,21 @@ export default function userRoutes(fastify) {
|
|
|
338
350
|
return { success: false, message: "User not found" };
|
|
339
351
|
}
|
|
340
352
|
const isAdmin = hasPermission(request.erpUser, "erp_admin");
|
|
341
|
-
//
|
|
342
|
-
|
|
353
|
+
// In SSO mode the supervisor owns passwords, so we strip any password
|
|
354
|
+
// field before forwarding the update — even from admins.
|
|
355
|
+
const sso = isSupervisorAuth();
|
|
356
|
+
const body = isAdmin
|
|
357
|
+
? sso
|
|
358
|
+
? { username: request.body.username }
|
|
359
|
+
: request.body
|
|
360
|
+
: sso
|
|
361
|
+
? {}
|
|
362
|
+
: { password: request.body.password };
|
|
343
363
|
try {
|
|
344
364
|
const user = await updateUser(targetUser.id, body);
|
|
345
365
|
authCache.clear();
|
|
346
|
-
const
|
|
366
|
+
const hasApiKey = sso ? false : await hasUserApiKey(user.id);
|
|
367
|
+
const full = formatUser(user, request.erpUser.id, request.erpUser.permissions, { hasApiKey });
|
|
347
368
|
return mutationResult(request, reply, full, {
|
|
348
369
|
_actions: full._actions,
|
|
349
370
|
});
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { generatePersistentUserApiKey } from "@naisys/common-node";
|
|
2
2
|
import bcrypt from "bcryptjs";
|
|
3
|
-
import {
|
|
3
|
+
import { randomUUID } from "crypto";
|
|
4
4
|
import erpDb from "../erpDb.js";
|
|
5
|
-
import { isSupervisorAuth } from "../supervisorAuth.js";
|
|
6
5
|
// --- Prisma include & result type ---
|
|
7
6
|
export const includePermissions = {
|
|
8
7
|
permissions: true,
|
|
@@ -37,19 +36,12 @@ export async function getUserById(id) {
|
|
|
37
36
|
include: includePermissions,
|
|
38
37
|
});
|
|
39
38
|
}
|
|
40
|
-
export async function
|
|
39
|
+
export async function hasUserApiKey(id) {
|
|
41
40
|
const user = await erpDb.user.findUnique({
|
|
42
41
|
where: { id },
|
|
43
|
-
select: {
|
|
42
|
+
select: { apiKeyHash: true },
|
|
44
43
|
});
|
|
45
|
-
|
|
46
|
-
return null;
|
|
47
|
-
if (user.isAgent && isSupervisorAuth()) {
|
|
48
|
-
return getAgentApiKeyByUuid(user.uuid);
|
|
49
|
-
}
|
|
50
|
-
else {
|
|
51
|
-
return user.apiKey ?? null;
|
|
52
|
-
}
|
|
44
|
+
return !!user?.apiKeyHash;
|
|
53
45
|
}
|
|
54
46
|
// --- Mutations ---
|
|
55
47
|
export async function getUserByUuid(uuid) {
|
|
@@ -63,7 +55,6 @@ export async function createUserForAgent(username, uuid) {
|
|
|
63
55
|
data: {
|
|
64
56
|
username,
|
|
65
57
|
uuid,
|
|
66
|
-
passwordHash: "",
|
|
67
58
|
isAgent: true,
|
|
68
59
|
},
|
|
69
60
|
include: includePermissions,
|
|
@@ -78,7 +69,6 @@ export async function createUserWithPassword(data) {
|
|
|
78
69
|
uuid,
|
|
79
70
|
passwordHash,
|
|
80
71
|
isAgent: false,
|
|
81
|
-
apiKey: randomBytes(32).toString("hex"),
|
|
82
72
|
},
|
|
83
73
|
include: includePermissions,
|
|
84
74
|
});
|
|
@@ -111,22 +101,15 @@ export async function revokePermission(userId, permission) {
|
|
|
111
101
|
});
|
|
112
102
|
}
|
|
113
103
|
export async function rotateUserApiKey(id) {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
104
|
+
return generatePersistentUserApiKey(id, {
|
|
105
|
+
userExists: async (userId) => (await erpDb.user.findUnique({
|
|
106
|
+
where: { id: userId },
|
|
107
|
+
select: { id: true },
|
|
108
|
+
})) !== null,
|
|
109
|
+
updateApiKeyHash: (userId, apiKeyHash) => erpDb.user.update({
|
|
110
|
+
where: { id: userId },
|
|
111
|
+
data: { apiKeyHash },
|
|
112
|
+
}),
|
|
118
113
|
});
|
|
119
|
-
if (!user)
|
|
120
|
-
throw new Error("User not found");
|
|
121
|
-
if (user.isAgent && isSupervisorAuth()) {
|
|
122
|
-
await rotateAgentApiKeyByUuid(user.uuid, newKey);
|
|
123
|
-
}
|
|
124
|
-
else {
|
|
125
|
-
await erpDb.user.update({
|
|
126
|
-
where: { id },
|
|
127
|
-
data: { apiKey: newKey },
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
return newKey;
|
|
131
114
|
}
|
|
132
115
|
//# sourceMappingURL=user-service.js.map
|
package/dist/userService.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { SUPER_ADMIN_USERNAME } from "@naisys/common";
|
|
2
2
|
import { ensureSuperAdmin } from "@naisys/supervisor-database";
|
|
3
3
|
import bcrypt from "bcryptjs";
|
|
4
|
-
import {
|
|
4
|
+
import { randomUUID } from "crypto";
|
|
5
5
|
import erpDb from "./erpDb.js";
|
|
6
6
|
const SALT_ROUNDS = 10;
|
|
7
7
|
/**
|
|
@@ -31,7 +31,6 @@ export async function ensureLocalSuperAdmin(password) {
|
|
|
31
31
|
uuid: randomUUID(),
|
|
32
32
|
username: SUPER_ADMIN_USERNAME,
|
|
33
33
|
passwordHash: hash,
|
|
34
|
-
apiKey: randomBytes(32).toString("hex"),
|
|
35
34
|
},
|
|
36
35
|
});
|
|
37
36
|
await ensureErpAdminPermission(user.id);
|
|
@@ -50,8 +49,8 @@ export async function ensureLocalSuperAdmin(password) {
|
|
|
50
49
|
}
|
|
51
50
|
/**
|
|
52
51
|
* Sync superadmin from supervisor into ERP DB and ensure permissions.
|
|
53
|
-
* For supervisor auth mode.
|
|
54
|
-
* mirrored ERP row
|
|
52
|
+
* For supervisor auth mode. Supervisor uses passkey-only auth — the
|
|
53
|
+
* mirrored ERP row has no passwordHash.
|
|
55
54
|
*/
|
|
56
55
|
export async function ensureSupervisorSuperAdmin() {
|
|
57
56
|
const result = await ensureSuperAdmin();
|
|
@@ -60,12 +59,9 @@ export async function ensureSupervisorSuperAdmin() {
|
|
|
60
59
|
create: {
|
|
61
60
|
uuid: result.user.uuid,
|
|
62
61
|
username: result.user.username,
|
|
63
|
-
passwordHash: "!sso-passkey-only",
|
|
64
|
-
apiKey: result.user.apiKey,
|
|
65
62
|
},
|
|
66
63
|
update: {
|
|
67
64
|
username: result.user.username,
|
|
68
|
-
apiKey: result.user.apiKey,
|
|
69
65
|
},
|
|
70
66
|
});
|
|
71
67
|
const localSuperAdmin = await erpDb.user.findUnique({
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@naisys/erp",
|
|
3
|
-
"version": "3.0.0-beta.
|
|
3
|
+
"version": "3.0.0-beta.38",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@naisys/erp",
|
|
9
|
-
"version": "3.0.0-beta.
|
|
9
|
+
"version": "3.0.0-beta.38",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@fastify/cookie": "^11.0.2",
|
|
12
12
|
"@fastify/cors": "^11.2.0",
|
|
@@ -14,11 +14,11 @@
|
|
|
14
14
|
"@fastify/rate-limit": "^10.3.0",
|
|
15
15
|
"@fastify/static": "^9.0.0",
|
|
16
16
|
"@fastify/swagger": "^9.7.0",
|
|
17
|
-
"@naisys/common": "3.0.0-beta.
|
|
18
|
-
"@naisys/common-node": "3.0.0-beta.
|
|
19
|
-
"@naisys/erp-shared": "3.0.0-beta.
|
|
20
|
-
"@naisys/hub-database": "3.0.0-beta.
|
|
21
|
-
"@naisys/supervisor-database": "3.0.0-beta.
|
|
17
|
+
"@naisys/common": "3.0.0-beta.38",
|
|
18
|
+
"@naisys/common-node": "3.0.0-beta.38",
|
|
19
|
+
"@naisys/erp-shared": "3.0.0-beta.38",
|
|
20
|
+
"@naisys/hub-database": "3.0.0-beta.38",
|
|
21
|
+
"@naisys/supervisor-database": "3.0.0-beta.38",
|
|
22
22
|
"@prisma/adapter-better-sqlite3": "^7.5.0",
|
|
23
23
|
"@prisma/client": "^7.5.0",
|
|
24
24
|
"@scalar/fastify-api-reference": "^1.48.7",
|
|
@@ -394,41 +394,41 @@
|
|
|
394
394
|
}
|
|
395
395
|
},
|
|
396
396
|
"node_modules/@naisys/common": {
|
|
397
|
-
"version": "3.0.0-beta.
|
|
398
|
-
"resolved": "https://registry.npmjs.org/@naisys/common/-/common-3.0.0-beta.
|
|
399
|
-
"integrity": "sha512-
|
|
397
|
+
"version": "3.0.0-beta.38",
|
|
398
|
+
"resolved": "https://registry.npmjs.org/@naisys/common/-/common-3.0.0-beta.38.tgz",
|
|
399
|
+
"integrity": "sha512-YJIawcV12XNgzQz/QcM2oAYhu4O4V55tHm0JZrEdmBt2GS33VsGIVJqhDtg3sVWvtsDfh8QkK7EjwPEvgj7fzA==",
|
|
400
400
|
"dependencies": {
|
|
401
401
|
"semver": "^7.7.4",
|
|
402
402
|
"zod": "^4.3.6"
|
|
403
403
|
}
|
|
404
404
|
},
|
|
405
405
|
"node_modules/@naisys/common-node": {
|
|
406
|
-
"version": "3.0.0-beta.
|
|
407
|
-
"resolved": "https://registry.npmjs.org/@naisys/common-node/-/common-node-3.0.0-beta.
|
|
408
|
-
"integrity": "sha512-
|
|
406
|
+
"version": "3.0.0-beta.38",
|
|
407
|
+
"resolved": "https://registry.npmjs.org/@naisys/common-node/-/common-node-3.0.0-beta.38.tgz",
|
|
408
|
+
"integrity": "sha512-dNVvY+gWzBx4I+3chvNl74MgrcxVBFpPB+0acYSl7bcINvhqmbqLMYryR4AwOvU+dc09GDbpgugE6EWIQnn/zA==",
|
|
409
409
|
"dependencies": {
|
|
410
|
-
"@naisys/common": "3.0.0-beta.
|
|
410
|
+
"@naisys/common": "3.0.0-beta.38",
|
|
411
411
|
"better-sqlite3": "^12.6.2",
|
|
412
412
|
"js-yaml": "^4.1.1",
|
|
413
413
|
"pino": "^10.3.1"
|
|
414
414
|
}
|
|
415
415
|
},
|
|
416
416
|
"node_modules/@naisys/erp-shared": {
|
|
417
|
-
"version": "3.0.0-beta.
|
|
418
|
-
"resolved": "https://registry.npmjs.org/@naisys/erp-shared/-/erp-shared-3.0.0-beta.
|
|
419
|
-
"integrity": "sha512-
|
|
417
|
+
"version": "3.0.0-beta.38",
|
|
418
|
+
"resolved": "https://registry.npmjs.org/@naisys/erp-shared/-/erp-shared-3.0.0-beta.38.tgz",
|
|
419
|
+
"integrity": "sha512-3lDS/qJBl1brKhkMilliUJY+k9K9E8ctARFwdPuwrLCUleIVUKx2VGEKsEWF0mYLIrvTMOSwB8gyG+ZQlCQaVw==",
|
|
420
420
|
"dependencies": {
|
|
421
|
-
"@naisys/common": "3.0.0-beta.
|
|
421
|
+
"@naisys/common": "3.0.0-beta.38",
|
|
422
422
|
"zod": "^4.3.6"
|
|
423
423
|
}
|
|
424
424
|
},
|
|
425
425
|
"node_modules/@naisys/hub-database": {
|
|
426
|
-
"version": "3.0.0-beta.
|
|
427
|
-
"resolved": "https://registry.npmjs.org/@naisys/hub-database/-/hub-database-3.0.0-beta.
|
|
428
|
-
"integrity": "sha512-
|
|
426
|
+
"version": "3.0.0-beta.38",
|
|
427
|
+
"resolved": "https://registry.npmjs.org/@naisys/hub-database/-/hub-database-3.0.0-beta.38.tgz",
|
|
428
|
+
"integrity": "sha512-h+e50Qjg389ewSKU2jAisexlmajEAK34lMz6FKWd8Hog/PI6v3BYKB/8wgL2HmI78apmHIjh1FRW3NQNF+D8rw==",
|
|
429
429
|
"dependencies": {
|
|
430
|
-
"@naisys/common": "3.0.0-beta.
|
|
431
|
-
"@naisys/common-node": "3.0.0-beta.
|
|
430
|
+
"@naisys/common": "3.0.0-beta.38",
|
|
431
|
+
"@naisys/common-node": "3.0.0-beta.38",
|
|
432
432
|
"@prisma/adapter-better-sqlite3": "^7.5.0",
|
|
433
433
|
"@prisma/client": "^7.5.0",
|
|
434
434
|
"better-sqlite3": "^12.6.2",
|
|
@@ -436,12 +436,12 @@
|
|
|
436
436
|
}
|
|
437
437
|
},
|
|
438
438
|
"node_modules/@naisys/supervisor-database": {
|
|
439
|
-
"version": "3.0.0-beta.
|
|
440
|
-
"resolved": "https://registry.npmjs.org/@naisys/supervisor-database/-/supervisor-database-3.0.0-beta.
|
|
441
|
-
"integrity": "sha512-
|
|
439
|
+
"version": "3.0.0-beta.38",
|
|
440
|
+
"resolved": "https://registry.npmjs.org/@naisys/supervisor-database/-/supervisor-database-3.0.0-beta.38.tgz",
|
|
441
|
+
"integrity": "sha512-3lV4sFHQZkTtNK80N3DpJ3YrfxE/3Pq6CtoavXiEsM2TaMAThjewbHTlY3RyrdfrHK0txTCiC24LCwiSn4qUpg==",
|
|
442
442
|
"dependencies": {
|
|
443
|
-
"@naisys/common": "3.0.0-beta.
|
|
444
|
-
"@naisys/common-node": "3.0.0-beta.
|
|
443
|
+
"@naisys/common": "3.0.0-beta.38",
|
|
444
|
+
"@naisys/common-node": "3.0.0-beta.38",
|
|
445
445
|
"@prisma/adapter-better-sqlite3": "^7.5.0",
|
|
446
446
|
"@prisma/client": "^7.5.0",
|
|
447
447
|
"bcryptjs": "^3.0.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@naisys/erp",
|
|
3
|
-
"version": "3.0.0-beta.
|
|
3
|
+
"version": "3.0.0-beta.38",
|
|
4
4
|
"description": "NAISYS ERP - Web UI for AI-driven order and work management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/erpServer.js",
|
|
@@ -46,11 +46,11 @@
|
|
|
46
46
|
"@fastify/rate-limit": "^10.3.0",
|
|
47
47
|
"@fastify/static": "^9.0.0",
|
|
48
48
|
"@fastify/swagger": "^9.7.0",
|
|
49
|
-
"@naisys/common": "3.0.0-beta.
|
|
50
|
-
"@naisys/common-node": "3.0.0-beta.
|
|
51
|
-
"@naisys/erp-shared": "3.0.0-beta.
|
|
52
|
-
"@naisys/hub-database": "3.0.0-beta.
|
|
53
|
-
"@naisys/supervisor-database": "3.0.0-beta.
|
|
49
|
+
"@naisys/common": "3.0.0-beta.38",
|
|
50
|
+
"@naisys/common-node": "3.0.0-beta.38",
|
|
51
|
+
"@naisys/erp-shared": "3.0.0-beta.38",
|
|
52
|
+
"@naisys/hub-database": "3.0.0-beta.38",
|
|
53
|
+
"@naisys/supervisor-database": "3.0.0-beta.38",
|
|
54
54
|
"@prisma/adapter-better-sqlite3": "^7.5.0",
|
|
55
55
|
"@prisma/client": "^7.5.0",
|
|
56
56
|
"@scalar/fastify-api-reference": "^1.48.7",
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
-- Replace persisted plaintext user API keys with nullable hashes.
|
|
2
|
+
-- Existing plaintext keys are intentionally discarded.
|
|
3
|
+
|
|
4
|
+
DROP INDEX IF EXISTS "users_api_key_key";
|
|
5
|
+
|
|
6
|
+
ALTER TABLE "users" RENAME COLUMN "api_key" TO "api_key_hash";
|
|
7
|
+
|
|
8
|
+
UPDATE "users" SET "api_key_hash" = NULL;
|
|
9
|
+
|
|
10
|
+
CREATE UNIQUE INDEX "users_api_key_hash_key" ON "users"("api_key_hash");
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
-- Make users.password_hash nullable. NULL means the user is not
|
|
2
|
+
-- password-authable (passkey-only, API-key-only, or agent).
|
|
3
|
+
-- Existing sentinel values are converted to NULL.
|
|
4
|
+
|
|
5
|
+
PRAGMA foreign_keys=OFF;
|
|
6
|
+
|
|
7
|
+
CREATE TABLE "users_new" (
|
|
8
|
+
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
|
9
|
+
"uuid" TEXT NOT NULL,
|
|
10
|
+
"username" TEXT NOT NULL,
|
|
11
|
+
"password_hash" TEXT,
|
|
12
|
+
"is_agent" INTEGER NOT NULL DEFAULT 0,
|
|
13
|
+
"created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
14
|
+
"updated_at" DATETIME NOT NULL,
|
|
15
|
+
"deleted_at" DATETIME,
|
|
16
|
+
"api_key_hash" TEXT
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
INSERT INTO "users_new" ("id", "uuid", "username", "password_hash", "is_agent", "created_at", "updated_at", "deleted_at", "api_key_hash")
|
|
20
|
+
SELECT
|
|
21
|
+
"id",
|
|
22
|
+
"uuid",
|
|
23
|
+
"username",
|
|
24
|
+
CASE WHEN "password_hash" IN ('!sso-passkey-only', '!api-key-only', '') THEN NULL ELSE "password_hash" END,
|
|
25
|
+
"is_agent",
|
|
26
|
+
"created_at",
|
|
27
|
+
"updated_at",
|
|
28
|
+
"deleted_at",
|
|
29
|
+
"api_key_hash"
|
|
30
|
+
FROM "users";
|
|
31
|
+
|
|
32
|
+
DROP TABLE "users";
|
|
33
|
+
ALTER TABLE "users_new" RENAME TO "users";
|
|
34
|
+
|
|
35
|
+
CREATE UNIQUE INDEX "users_uuid_key" ON "users"("uuid");
|
|
36
|
+
CREATE UNIQUE INDEX "users_username_key" ON "users"("username");
|
|
37
|
+
CREATE UNIQUE INDEX "users_api_key_hash_key" ON "users"("api_key_hash");
|
|
38
|
+
|
|
39
|
+
PRAGMA foreign_keys=ON;
|
package/prisma/schema.prisma
CHANGED
|
@@ -418,8 +418,8 @@ model User {
|
|
|
418
418
|
id Int @id @default(autoincrement())
|
|
419
419
|
uuid String @unique
|
|
420
420
|
username String @unique
|
|
421
|
-
passwordHash String
|
|
422
|
-
|
|
421
|
+
passwordHash String? @map("password_hash")
|
|
422
|
+
apiKeyHash String? @unique @map("api_key_hash")
|
|
423
423
|
isAgent Boolean @default(false) @map("is_agent")
|
|
424
424
|
createdAt DateTime @default(now()) @map("created_at")
|
|
425
425
|
updatedAt DateTime @updatedAt @map("updated_at")
|