@naisys/erp 3.0.0-beta.9 → 3.0.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/.env.example +5 -0
- package/client-dist/assets/index-CSiMTJfw.css +14 -0
- package/client-dist/assets/index-D6lSIioV.js +11167 -0
- package/client-dist/assets/rolldown-runtime-CvHMtSRF.js +33 -0
- package/client-dist/assets/vendor-CJ0ET9hP.js +75181 -0
- package/client-dist/assets/vendor-CLUPjUnv.css +8747 -0
- package/client-dist/favicon-16x16.png +0 -0
- package/client-dist/favicon-32x32.png +0 -0
- package/client-dist/favicon.ico +0 -0
- package/client-dist/index.html +17 -2
- package/dist/{dbConfig.js → database/dbConfig.js} +1 -1
- package/dist/{erpDb.js → database/erpDb.js} +1 -1
- package/dist/erpRoutes.js +115 -0
- package/dist/erpServer.js +85 -161
- package/dist/error-handler.js +3 -0
- package/dist/generated/prisma/internal/class.js +4 -4
- package/dist/generated/prisma/internal/prismaNamespace.js +3 -1
- package/dist/middleware/auth-middleware.js +146 -0
- package/dist/route-helpers.js +2 -2
- package/dist/routes/admin.js +15 -7
- package/dist/routes/audit.js +1 -1
- package/dist/routes/{item-fields.js → items/item-fields.js} +24 -22
- package/dist/routes/{item-instances.js → items/item-instances.js} +42 -24
- package/dist/routes/{items.js → items/items.js} +35 -33
- package/dist/routes/{operation-dependencies.js → operations/operation-dependencies.js} +6 -6
- package/dist/routes/{operation-field-refs.js → operations/operation-field-refs.js} +6 -6
- package/dist/routes/{operation-run-comments.js → operations/operation-run-comments.js} +5 -5
- package/dist/routes/{operation-run-transitions.js → operations/operation-run-transitions.js} +29 -13
- package/dist/routes/{operation-runs.js → operations/operation-runs.js} +48 -10
- package/dist/routes/{operations.js → operations/operations.js} +6 -6
- package/dist/routes/{order-revision-transitions.js → orders/order-revision-transitions.js} +4 -4
- package/dist/routes/{order-revisions.js → orders/order-revisions.js} +6 -6
- package/dist/routes/{order-run-transitions.js → orders/order-run-transitions.js} +11 -5
- package/dist/routes/{order-runs.js → orders/order-runs.js} +7 -5
- package/dist/routes/{orders.js → orders/orders.js} +15 -11
- package/dist/routes/{dispatch.js → production/dispatch.js} +88 -7
- package/dist/routes/{inventory.js → production/inventory.js} +33 -10
- package/dist/routes/{labor-tickets.js → production/labor-tickets.js} +7 -7
- package/dist/routes/{work-centers.js → production/work-centers.js} +29 -29
- package/dist/routes/root.js +1 -1
- package/dist/routes/{step-field-attachments.js → steps/step-field-attachments.js} +8 -8
- package/dist/routes/{step-fields.js → steps/step-fields.js} +6 -6
- package/dist/routes/{step-run-fields.js → steps/step-run-fields.js} +9 -9
- package/dist/routes/{step-run-transitions.js → steps/step-run-transitions.js} +6 -6
- package/dist/routes/{step-runs.js → steps/step-runs.js} +7 -7
- package/dist/routes/{steps.js → steps/steps.js} +5 -5
- package/dist/routes/{auth.js → users/auth.js} +11 -23
- package/dist/routes/{user-permissions.js → users/user-permissions.js} +21 -7
- package/dist/routes/{users.js → users/users.js} +42 -20
- package/dist/services/attachment-service.js +2 -2
- package/dist/services/{item-instance-service.js → inventory/item-instance-service.js} +2 -2
- package/dist/services/{item-service.js → inventory/item-service.js} +2 -2
- package/dist/services/{operation-dependency-service.js → operations/operation-dependency-service.js} +1 -1
- package/dist/services/{operation-run-comment-service.js → operations/operation-run-comment-service.js} +1 -1
- package/dist/services/{operation-run-service.js → operations/operation-run-service.js} +15 -4
- package/dist/services/{operation-service.js → operations/operation-service.js} +2 -2
- package/dist/services/{step-run-service.js → operations/step-run-service.js} +1 -1
- package/dist/services/{step-service.js → operations/step-service.js} +2 -2
- package/dist/services/{order-revision-service.js → orders/order-revision-service.js} +4 -5
- package/dist/services/{order-run-service.js → orders/order-run-service.js} +68 -22
- package/dist/services/{order-service.js → orders/order-service.js} +11 -2
- package/dist/services/{revision-diff-service.js → orders/revision-diff-service.js} +11 -10
- package/dist/services/{field-ref-service.js → production/field-ref-service.js} +1 -1
- package/dist/services/{field-service.js → production/field-service.js} +2 -2
- package/dist/services/{field-value-service.js → production/field-value-service.js} +27 -3
- package/dist/services/production/labor-ticket-backfill.js +67 -0
- package/dist/services/{labor-ticket-service.js → production/labor-ticket-service.js} +21 -15
- package/dist/services/{work-center-service.js → production/work-center-service.js} +2 -2
- package/dist/services/user-service.js +94 -28
- package/dist/version.js +12 -0
- package/npm-shrinkwrap.json +3000 -0
- package/package.json +11 -9
- 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/migrations/20260517000000_add_op_run_tokens/migration.sql +2 -0
- package/prisma/schema.prisma +4 -2
- package/client-dist/assets/index-45dVo30p.css +0 -1
- package/client-dist/assets/index-C9uuPHLH.js +0 -168
- package/dist/auth-middleware.js +0 -203
- package/dist/userService.js +0 -118
- /package/bin/{naisys-erp → naisys-erp.js} +0 -0
- /package/dist/{supervisorAuth.js → middleware/supervisorAuth.js} +0 -0
- /package/dist/{audit.js → services/audit.js} +0 -0
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { AssignWorkCenterUserSchema, CreateWorkCenterSchema, ErrorResponseSchema, KeyCreateResponseSchema, MutateResponseSchema, UpdateWorkCenterSchema, WorkCenterListQuerySchema, WorkCenterListResponseSchema, WorkCenterSchema, } from "@naisys/erp-shared";
|
|
2
2
|
import { z } from "zod/v4";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { formatAuditFields, mutationResult } from "
|
|
7
|
-
import { assignUser, createWorkCenter, deleteWorkCenter, findExisting, listWorkCenters, removeUser, updateWorkCenter, } from "
|
|
3
|
+
import { notFound } from "../../error-handler.js";
|
|
4
|
+
import { API_PREFIX, collectionLink, paginationLinks, schemaLink, selfLink, } from "../../hateoas.js";
|
|
5
|
+
import { hasPermission, requirePermission, } from "../../middleware/auth-middleware.js";
|
|
6
|
+
import { formatAuditFields, mutationResult, permGate, } from "../../route-helpers.js";
|
|
7
|
+
import { assignUser, createWorkCenter, deleteWorkCenter, findExisting, listWorkCenters, removeUser, updateWorkCenter, } from "../../services/production/work-center-service.js";
|
|
8
8
|
const RESOURCE = "work-centers";
|
|
9
9
|
const KeyParamsSchema = z.object({
|
|
10
10
|
key: z.string(),
|
|
@@ -21,9 +21,8 @@ function wcLinks(key) {
|
|
|
21
21
|
];
|
|
22
22
|
}
|
|
23
23
|
function wcActions(key, user) {
|
|
24
|
-
if (!hasPermission(user, "erp_admin"))
|
|
25
|
-
return [];
|
|
26
24
|
const href = `${API_PREFIX}/${RESOURCE}/${key}`;
|
|
25
|
+
const gate = permGate(hasPermission(user, "erp_admin"), "erp_admin");
|
|
27
26
|
return [
|
|
28
27
|
{
|
|
29
28
|
rel: "update",
|
|
@@ -31,12 +30,14 @@ function wcActions(key, user) {
|
|
|
31
30
|
method: "PUT",
|
|
32
31
|
title: "Update",
|
|
33
32
|
schema: `${API_PREFIX}/schemas/UpdateWorkCenter`,
|
|
33
|
+
...gate,
|
|
34
34
|
},
|
|
35
35
|
{
|
|
36
36
|
rel: "delete",
|
|
37
37
|
href,
|
|
38
38
|
method: "DELETE",
|
|
39
39
|
title: "Delete",
|
|
40
|
+
...gate,
|
|
40
41
|
},
|
|
41
42
|
{
|
|
42
43
|
rel: "assignUser",
|
|
@@ -44,11 +45,12 @@ function wcActions(key, user) {
|
|
|
44
45
|
method: "POST",
|
|
45
46
|
title: "Assign User",
|
|
46
47
|
schema: `${API_PREFIX}/schemas/AssignWorkCenterUser`,
|
|
48
|
+
...gate,
|
|
47
49
|
},
|
|
48
50
|
];
|
|
49
51
|
}
|
|
50
52
|
function formatWorkCenter(wc, user) {
|
|
51
|
-
const
|
|
53
|
+
const adminGate = permGate(hasPermission(user, "erp_admin"), "erp_admin");
|
|
52
54
|
return {
|
|
53
55
|
id: wc.id,
|
|
54
56
|
key: wc.key,
|
|
@@ -58,16 +60,15 @@ function formatWorkCenter(wc, user) {
|
|
|
58
60
|
username: a.user.username,
|
|
59
61
|
createdAt: a.createdAt.toISOString(),
|
|
60
62
|
createdBy: a.createdBy?.username ?? null,
|
|
61
|
-
_actions:
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
: [],
|
|
63
|
+
_actions: [
|
|
64
|
+
{
|
|
65
|
+
rel: "remove",
|
|
66
|
+
href: `${API_PREFIX}/${RESOURCE}/${wc.key}/users/${a.user.username}`,
|
|
67
|
+
method: "DELETE",
|
|
68
|
+
title: "Remove",
|
|
69
|
+
...adminGate,
|
|
70
|
+
},
|
|
71
|
+
],
|
|
71
72
|
})),
|
|
72
73
|
...formatAuditFields(wc),
|
|
73
74
|
_links: wcLinks(wc.key),
|
|
@@ -117,17 +118,16 @@ export default function workCenterRoutes(fastify) {
|
|
|
117
118
|
hrefTemplate: `${API_PREFIX}/work-centers/{key}`,
|
|
118
119
|
},
|
|
119
120
|
],
|
|
120
|
-
_actions:
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
: [],
|
|
121
|
+
_actions: [
|
|
122
|
+
{
|
|
123
|
+
rel: "create",
|
|
124
|
+
href: `${API_PREFIX}/${RESOURCE}`,
|
|
125
|
+
method: "POST",
|
|
126
|
+
title: "Create Work Center",
|
|
127
|
+
schema: `${API_PREFIX}/schemas/CreateWorkCenter`,
|
|
128
|
+
...permGate(hasPermission(request.erpUser, "erp_admin"), "erp_admin"),
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
131
|
};
|
|
132
132
|
},
|
|
133
133
|
});
|
package/dist/routes/root.js
CHANGED
|
@@ -2,14 +2,14 @@ import { mimeFromFilename } from "@naisys/common";
|
|
|
2
2
|
import { ErrorResponseSchema, UploadAttachmentResponseSchema, } from "@naisys/erp-shared";
|
|
3
3
|
import { createReadStream, existsSync, statSync } from "fs";
|
|
4
4
|
import { z } from "zod/v4";
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
import {
|
|
8
|
-
import { checkOpRunInProgress, checkOrderRunStarted, checkWorkCenterAccess, resolveStepRun, } from "
|
|
9
|
-
import { deleteFieldAttachment, getAttachmentFilePath, uploadAttachment, } from "
|
|
10
|
-
import { ensureStepRunFieldRecord } from "
|
|
11
|
-
import { findStepRunWithField, rebuildAttachmentFieldValue, upsertFieldValue, } from "
|
|
12
|
-
import { isUserClockedIn } from "
|
|
5
|
+
import erpDb from "../../database/erpDb.js";
|
|
6
|
+
import { conflict, notFound } from "../../error-handler.js";
|
|
7
|
+
import { requirePermission } from "../../middleware/auth-middleware.js";
|
|
8
|
+
import { checkOpRunInProgress, checkOrderRunStarted, checkWorkCenterAccess, resolveStepRun, } from "../../route-helpers.js";
|
|
9
|
+
import { deleteFieldAttachment, getAttachmentFilePath, uploadAttachment, } from "../../services/attachment-service.js";
|
|
10
|
+
import { ensureStepRunFieldRecord } from "../../services/production/field-service.js";
|
|
11
|
+
import { findStepRunWithField, rebuildAttachmentFieldValue, upsertFieldValue, } from "../../services/production/field-value-service.js";
|
|
12
|
+
import { isUserClockedIn } from "../../services/production/labor-ticket-service.js";
|
|
13
13
|
const FieldSeqNoParamsSchema = z.object({
|
|
14
14
|
orderKey: z.string(),
|
|
15
15
|
runNo: z.coerce.number().int(),
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { BatchCreateFieldSchema, BatchSeqNoCreateResponseSchema, CreateFieldSchema, ErrorResponseSchema, FieldListResponseSchema, FieldSchema, MutateResponseSchema, RevisionStatus, SeqNoCreateResponseSchema, UpdateFieldSchema, } from "@naisys/erp-shared";
|
|
2
2
|
import { z } from "zod/v4";
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { calcNextSeqNo, childItemLinks, draftCrudActions, formatAuditFields, mutationResult, resolveActions, resolveStep, } from "
|
|
8
|
-
import { createField, createFields, deleteField, ensureFieldSet, findExistingField, getField, listFields, updateField, } from "
|
|
3
|
+
import erpDb from "../../database/erpDb.js";
|
|
4
|
+
import { conflict, notFound } from "../../error-handler.js";
|
|
5
|
+
import { API_PREFIX, selfLink } from "../../hateoas.js";
|
|
6
|
+
import { requirePermission } from "../../middleware/auth-middleware.js";
|
|
7
|
+
import { calcNextSeqNo, childItemLinks, draftCrudActions, formatAuditFields, mutationResult, resolveActions, resolveStep, } from "../../route-helpers.js";
|
|
8
|
+
import { createField, createFields, deleteField, ensureFieldSet, findExistingField, getField, listFields, updateField, } from "../../services/production/field-service.js";
|
|
9
9
|
const ParamsSchema = z.object({
|
|
10
10
|
orderKey: z.string(),
|
|
11
11
|
revNo: z.coerce.number().int(),
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { BatchFieldValueMutateResponseSchema, BatchFieldValueUpdateResponseSchema, BatchUpdateFieldValuesSchema, DeleteSetMutateResponseSchema, ErrorResponseSchema, fieldTypeString, FieldValueMutateResponseSchema, getValueFormatHint, UpdateFieldValueSchema, } from "@naisys/erp-shared";
|
|
2
2
|
import { z } from "zod/v4";
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { checkOpRunInProgress, checkOrderRunStarted, checkWorkCenterAccess, mutationResult, resolveStepRun, } from "
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
3
|
+
import erpDb from "../../database/erpDb.js";
|
|
4
|
+
import { conflict, notFound, unprocessable } from "../../error-handler.js";
|
|
5
|
+
import { API_PREFIX } from "../../hateoas.js";
|
|
6
|
+
import { requirePermission } from "../../middleware/auth-middleware.js";
|
|
7
|
+
import { checkOpRunInProgress, checkOrderRunStarted, checkWorkCenterAccess, mutationResult, resolveStepRun, } from "../../route-helpers.js";
|
|
8
|
+
import { getStepRunWithFields } from "../../services/operations/step-run-service.js";
|
|
9
|
+
import { ensureStepRunFieldRecord } from "../../services/production/field-service.js";
|
|
10
|
+
import { checkFieldValueShape, clearAttachmentFieldValue, deleteFieldValueSet, deserializeFieldValue, findStepRunWithField, serializeFieldValue, upsertFieldValue, validateFieldValue, } from "../../services/production/field-value-service.js";
|
|
11
|
+
import { isUserClockedIn } from "../../services/production/labor-ticket-service.js";
|
|
12
12
|
import { computeStepRunHateoas, stepRunResource } from "./step-runs.js";
|
|
13
13
|
const FieldSeqNoParamsSchema = z.object({
|
|
14
14
|
orderKey: z.string(),
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { ErrorResponseSchema, StepRunTransitionSlimSchema, TransitionNoteSchema, } from "@naisys/erp-shared";
|
|
2
2
|
import { z } from "zod/v4";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { checkOpRunInProgress, checkOrderRunStarted, checkWorkCenterAccess, mutationResult, resolveStepRun, } from "
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
3
|
+
import { conflict, notFound, unprocessable } from "../../error-handler.js";
|
|
4
|
+
import { requirePermission } from "../../middleware/auth-middleware.js";
|
|
5
|
+
import { checkOpRunInProgress, checkOrderRunStarted, checkWorkCenterAccess, mutationResult, resolveStepRun, } from "../../route-helpers.js";
|
|
6
|
+
import { getStepRunWithFields, updateStepRun, } from "../../services/operations/step-run-service.js";
|
|
7
|
+
import { validateCompletionFields } from "../../services/production/field-value-service.js";
|
|
8
|
+
import { isUserClockedIn } from "../../services/production/labor-ticket-service.js";
|
|
9
9
|
import { formatStepRunTransition } from "./step-runs.js";
|
|
10
10
|
const StepSeqNoParamsSchema = z.object({
|
|
11
11
|
orderKey: z.string(),
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { ErrorResponseSchema, fieldTypeString, getValueFormatHint, OperationRunStatus, StepRunListQuerySchema, StepRunListResponseSchema, StepRunSchema, } from "@naisys/erp-shared";
|
|
2
2
|
import { z } from "zod/v4";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { checkWorkCenterAccess, childItemLinks, formatAuditFields, resolveActions, resolveOpRun, resolveStepRun, } from "
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
3
|
+
import { notFound } from "../../error-handler.js";
|
|
4
|
+
import { API_PREFIX, selfLink } from "../../hateoas.js";
|
|
5
|
+
import { hasPermission } from "../../middleware/auth-middleware.js";
|
|
6
|
+
import { checkWorkCenterAccess, childItemLinks, formatAuditFields, resolveActions, resolveOpRun, resolveStepRun, } from "../../route-helpers.js";
|
|
7
|
+
import { getStepRunWithFields, listStepRuns, listStepRunsWithFields, } from "../../services/operations/step-run-service.js";
|
|
8
|
+
import { deserializeFieldValue, validateCompletionFields, validateFieldValue, } from "../../services/production/field-value-service.js";
|
|
9
|
+
import { isUserClockedIn } from "../../services/production/labor-ticket-service.js";
|
|
10
10
|
export function stepRunResource(orderKey, runNo, seqNo) {
|
|
11
11
|
return `orders/${orderKey}/runs/${runNo}/ops/${seqNo}/steps`;
|
|
12
12
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { BatchCreateStepSchema, BatchSeqNoCreateResponseSchema, CreateStepSchema, ErrorResponseSchema, MutateResponseSchema, RevisionStatus, SeqNoCreateResponseSchema, StepListResponseSchema, StepSchema, UpdateStepSchema, } from "@naisys/erp-shared";
|
|
2
2
|
import { z } from "zod/v4";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { calcNextSeqNo, childItemLinks, draftCrudActions, formatAuditFields, mutationResult, resolveActions, resolveOperation, } from "
|
|
7
|
-
import { createStep, createSteps, deleteStep, findExisting, getStep, listSteps, updateStep, } from "
|
|
3
|
+
import { conflict, notFound } from "../../error-handler.js";
|
|
4
|
+
import { API_PREFIX, selfLink } from "../../hateoas.js";
|
|
5
|
+
import { requirePermission } from "../../middleware/auth-middleware.js";
|
|
6
|
+
import { calcNextSeqNo, childItemLinks, draftCrudActions, formatAuditFields, mutationResult, resolveActions, resolveOperation, } from "../../route-helpers.js";
|
|
7
|
+
import { createStep, createSteps, deleteStep, findExisting, getStep, listSteps, updateStep, } from "../../services/operations/step-service.js";
|
|
8
8
|
import { formatFieldListResponse } from "./step-fields.js";
|
|
9
9
|
const ParamsSchema = z.object({
|
|
10
10
|
orderKey: z.string(),
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { hashToken, SESSION_COOKIE_NAME, sessionCookieOptions, } from "@naisys/common-node";
|
|
2
2
|
import { AuthUserSchema, ErrorResponseSchema, LoginRequestSchema, LoginResponseSchema, } from "@naisys/erp-shared";
|
|
3
|
-
import {
|
|
3
|
+
import { deleteSession } from "@naisys/supervisor-database";
|
|
4
4
|
import bcrypt from "bcryptjs";
|
|
5
5
|
import { randomUUID } from "crypto";
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
import {
|
|
9
|
-
import { isSupervisorAuth } from "
|
|
6
|
+
import erpDb from "../../database/erpDb.js";
|
|
7
|
+
import { unauthorized } from "../../error-handler.js";
|
|
8
|
+
import { authCache } from "../../middleware/auth-middleware.js";
|
|
9
|
+
import { isSupervisorAuth } from "../../middleware/supervisorAuth.js";
|
|
10
10
|
const SESSION_DURATION_MS = 30 * 24 * 60 * 60 * 1000; // 30 days
|
|
11
11
|
export default function authRoutes(fastify) {
|
|
12
12
|
const app = fastify.withTypeProvider();
|
|
@@ -14,7 +14,7 @@ export default function authRoutes(fastify) {
|
|
|
14
14
|
app.post("/login", {
|
|
15
15
|
config: {
|
|
16
16
|
rateLimit: {
|
|
17
|
-
max: 5,
|
|
17
|
+
max: Number(process.env.AUTH_LOGIN_RATE_LIMIT) || 5,
|
|
18
18
|
timeWindow: "1 minute",
|
|
19
19
|
},
|
|
20
20
|
},
|
|
@@ -30,27 +30,15 @@ export default function authRoutes(fastify) {
|
|
|
30
30
|
},
|
|
31
31
|
handler: async (request, reply) => {
|
|
32
32
|
const { username, password } = request.body;
|
|
33
|
-
// SSO mode:
|
|
33
|
+
// SSO mode: supervisor handles login via passkey. ERP doesn't accept
|
|
34
|
+
// password credentials at all — clients should authenticate against
|
|
35
|
+
// /supervisor/login and reuse the resulting session cookie here.
|
|
34
36
|
if (isSupervisorAuth()) {
|
|
35
|
-
|
|
36
|
-
if (!authResult) {
|
|
37
|
-
return unauthorized(reply, "Invalid username or password");
|
|
38
|
-
}
|
|
39
|
-
const ssoData = {
|
|
40
|
-
username,
|
|
41
|
-
passwordHash: authResult.user.passwordHash,
|
|
42
|
-
};
|
|
43
|
-
const user = await erpDb.user.upsert({
|
|
44
|
-
where: { uuid: authResult.user.uuid },
|
|
45
|
-
create: { uuid: authResult.user.uuid, ...ssoData },
|
|
46
|
-
update: ssoData,
|
|
47
|
-
});
|
|
48
|
-
reply.setCookie(SESSION_COOKIE_NAME, authResult.token, sessionCookieOptions(authResult.expiresAt));
|
|
49
|
-
return { user: { id: user.id, username: user.username } };
|
|
37
|
+
return unauthorized(reply, "Sign in via the supervisor login page (passkey required)");
|
|
50
38
|
}
|
|
51
39
|
// Standalone mode: authenticate against local DB
|
|
52
40
|
const user = await erpDb.user.findUnique({ where: { username } });
|
|
53
|
-
if (!user) {
|
|
41
|
+
if (!user || user.passwordHash === null) {
|
|
54
42
|
return unauthorized(reply, "Invalid username or password");
|
|
55
43
|
}
|
|
56
44
|
const valid = await bcrypt.compare(password, user.passwordHash);
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { ErpPermissionEnum, GrantPermissionSchema } from "@naisys/erp-shared";
|
|
2
2
|
import { z } from "zod/v4";
|
|
3
|
-
import { authCache, requirePermission } from "
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
3
|
+
import { authCache, requirePermission, } from "../../middleware/auth-middleware.js";
|
|
4
|
+
import { isSupervisorAuth } from "../../middleware/supervisorAuth.js";
|
|
5
|
+
import { mutationResult } from "../../route-helpers.js";
|
|
6
|
+
import { getUserById, getUserByUsername, grantPermission, hasUserApiKey, revokePermission, rotateUserApiKey, } from "../../services/user-service.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
|
});
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { ChangePasswordSchema, CreateAgentUserSchema, CreateUserSchema, UpdateUserSchema, } from "@naisys/erp-shared";
|
|
2
2
|
import { getHubAgentById } from "@naisys/hub-database";
|
|
3
3
|
import { z } from "zod/v4";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
4
|
+
import { API_PREFIX, collectionLink, paginationLinks, schemaLink, selfLink, } from "../../hateoas.js";
|
|
5
|
+
import { authCache, hasPermission, requirePermission, } from "../../middleware/auth-middleware.js";
|
|
6
|
+
import { isSupervisorAuth } from "../../middleware/supervisorAuth.js";
|
|
7
|
+
import { mutationResult } from "../../route-helpers.js";
|
|
8
|
+
import { createUserForAgent, createUserWithPassword, deleteUser, getUserByUsername, getUserByUuid, hasUserApiKey, listUsers, updateUser, } from "../../services/user-service.js";
|
|
9
9
|
function userItemLinks(username) {
|
|
10
10
|
return [
|
|
11
11
|
selfLink(`/users/${username}`),
|
|
@@ -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(),
|
|
@@ -127,6 +132,7 @@ export default function userRoutes(fastify) {
|
|
|
127
132
|
statusCode: 403,
|
|
128
133
|
error: "Forbidden",
|
|
129
134
|
message: "Permission 'erp_admin' required",
|
|
135
|
+
missingPermission: "erp_admin",
|
|
130
136
|
});
|
|
131
137
|
return;
|
|
132
138
|
}
|
|
@@ -200,6 +206,13 @@ export default function userRoutes(fastify) {
|
|
|
200
206
|
});
|
|
201
207
|
return;
|
|
202
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
|
+
}
|
|
203
216
|
await updateUser(request.erpUser.id, {
|
|
204
217
|
password: request.body.password,
|
|
205
218
|
});
|
|
@@ -222,7 +235,6 @@ export default function userRoutes(fastify) {
|
|
|
222
235
|
return mutationResult(request, reply, full, {
|
|
223
236
|
id: full.id,
|
|
224
237
|
username: full.username,
|
|
225
|
-
apiKey: full.apiKey,
|
|
226
238
|
_links: full._links,
|
|
227
239
|
_actions: full._actions,
|
|
228
240
|
});
|
|
@@ -287,7 +299,6 @@ export default function userRoutes(fastify) {
|
|
|
287
299
|
return mutationResult(request, reply, full, {
|
|
288
300
|
id: full.id,
|
|
289
301
|
username: full.username,
|
|
290
|
-
apiKey: full.apiKey,
|
|
291
302
|
_links: full._links,
|
|
292
303
|
_actions: full._actions,
|
|
293
304
|
});
|
|
@@ -318,8 +329,10 @@ export default function userRoutes(fastify) {
|
|
|
318
329
|
reply.code(404);
|
|
319
330
|
return { success: false, message: "User not found" };
|
|
320
331
|
}
|
|
321
|
-
const
|
|
322
|
-
|
|
332
|
+
const hasApiKey = isSupervisorAuth()
|
|
333
|
+
? false
|
|
334
|
+
: await hasUserApiKey(user.id);
|
|
335
|
+
return formatUser(user, request.erpUser.id, request.erpUser.permissions, { hasApiKey });
|
|
323
336
|
});
|
|
324
337
|
// UPDATE USER (admin can update any field; non-admin can only change own password)
|
|
325
338
|
app.put("/:username", {
|
|
@@ -337,12 +350,21 @@ export default function userRoutes(fastify) {
|
|
|
337
350
|
return { success: false, message: "User not found" };
|
|
338
351
|
}
|
|
339
352
|
const isAdmin = hasPermission(request.erpUser, "erp_admin");
|
|
340
|
-
//
|
|
341
|
-
|
|
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 };
|
|
342
363
|
try {
|
|
343
364
|
const user = await updateUser(targetUser.id, body);
|
|
344
365
|
authCache.clear();
|
|
345
|
-
const
|
|
366
|
+
const hasApiKey = sso ? false : await hasUserApiKey(user.id);
|
|
367
|
+
const full = formatUser(user, request.erpUser.id, request.erpUser.permissions, { hasApiKey });
|
|
346
368
|
return mutationResult(request, reply, full, {
|
|
347
369
|
_actions: full._actions,
|
|
348
370
|
});
|
|
@@ -2,7 +2,7 @@ import { MAX_ATTACHMENT_SIZE } from "@naisys/common";
|
|
|
2
2
|
import { createHash, randomBytes } from "crypto";
|
|
3
3
|
import { createWriteStream, existsSync, mkdirSync, renameSync, unlinkSync, } from "fs";
|
|
4
4
|
import { join } from "path";
|
|
5
|
-
import erpDb from "../erpDb.js";
|
|
5
|
+
import erpDb from "../database/erpDb.js";
|
|
6
6
|
function attachmentsDir() {
|
|
7
7
|
return join(process.env.NAISYS_FOLDER || "", "attachments");
|
|
8
8
|
}
|
|
@@ -20,7 +20,7 @@ export async function uploadAttachment(fileBuffer, filename, uploadedById, field
|
|
|
20
20
|
}
|
|
21
21
|
const fileHash = createHash("sha256").update(fileBuffer).digest("hex");
|
|
22
22
|
// Write to temp, then move to content-addressable path
|
|
23
|
-
const tmpDir = join(
|
|
23
|
+
const tmpDir = join(process.env.NAISYS_FOLDER || "", "tmp", "erp", "attachments");
|
|
24
24
|
mkdirSync(tmpDir, { recursive: true });
|
|
25
25
|
const tmpPath = join(tmpDir, `${Date.now()}_${uploadedById}_${Math.random().toString(36).slice(2)}`);
|
|
26
26
|
const ws = createWriteStream(tmpPath);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import erpDb from "
|
|
2
|
-
import { includeUsers } from "
|
|
1
|
+
import erpDb from "../../database/erpDb.js";
|
|
2
|
+
import { includeUsers } from "../../route-helpers.js";
|
|
3
3
|
// --- Prisma include & result type ---
|
|
4
4
|
export const includeItemInstanceRelations = {
|
|
5
5
|
...includeUsers,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import erpDb from "
|
|
2
|
-
import { includeUsers } from "
|
|
1
|
+
import erpDb from "../../database/erpDb.js";
|
|
2
|
+
import { includeUsers } from "../../route-helpers.js";
|
|
3
3
|
// --- Prisma include & result type ---
|
|
4
4
|
export const includeUsersAndFieldSet = {
|
|
5
5
|
...includeUsers,
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import { keyBy } from "@naisys/common";
|
|
1
2
|
import { OperationRunStatus as OperationRunStatusValues, } from "@naisys/erp-shared";
|
|
2
3
|
import { fieldTypeString, getValueFormatHint, } from "@naisys/erp-shared";
|
|
4
|
+
import erpDb from "../../database/erpDb.js";
|
|
5
|
+
import { API_PREFIX } from "../../hateoas.js";
|
|
3
6
|
import { writeAuditEntry } from "../audit.js";
|
|
4
|
-
import
|
|
5
|
-
import { API_PREFIX } from "../hateoas.js";
|
|
6
|
-
import { deserializeFieldValue, validateFieldValue, } from "./field-value-service.js";
|
|
7
|
+
import { deserializeFieldValue, validateFieldValue, } from "../production/field-value-service.js";
|
|
7
8
|
// --- Prisma include & result type ---
|
|
8
9
|
export const includeOp = {
|
|
9
10
|
operation: {
|
|
@@ -14,6 +15,11 @@ export const includeOp = {
|
|
|
14
15
|
workCenter: { select: { key: true } },
|
|
15
16
|
},
|
|
16
17
|
},
|
|
18
|
+
orderRun: {
|
|
19
|
+
select: {
|
|
20
|
+
orderRev: { select: { revNo: true, description: true } },
|
|
21
|
+
},
|
|
22
|
+
},
|
|
17
23
|
assignedTo: { select: { username: true } },
|
|
18
24
|
createdBy: { select: { username: true } },
|
|
19
25
|
updatedBy: { select: { username: true } },
|
|
@@ -35,6 +41,11 @@ export async function listOpRuns(runId) {
|
|
|
35
41
|
},
|
|
36
42
|
},
|
|
37
43
|
},
|
|
44
|
+
orderRun: {
|
|
45
|
+
select: {
|
|
46
|
+
orderRev: { select: { revNo: true, description: true } },
|
|
47
|
+
},
|
|
48
|
+
},
|
|
38
49
|
_count: { select: { stepRuns: true } },
|
|
39
50
|
assignedTo: { select: { username: true } },
|
|
40
51
|
createdBy: { select: { username: true } },
|
|
@@ -143,7 +154,7 @@ export async function getOpRunFieldRefSummary(operationId, orderRunId, orderKey,
|
|
|
143
154
|
},
|
|
144
155
|
},
|
|
145
156
|
});
|
|
146
|
-
const stepRunMap =
|
|
157
|
+
const stepRunMap = keyBy(stepRuns, (sr) => sr.stepId);
|
|
147
158
|
return fieldRefs.map((ref) => {
|
|
148
159
|
const sr = stepRunMap.get(ref.sourceStep.id);
|
|
149
160
|
const storedFieldValues = sr?.fieldRecord?.fieldValues ?? [];
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import erpDb from "
|
|
2
|
-
import { calcNextSeqNo, includeUsers, } from "
|
|
1
|
+
import erpDb from "../../database/erpDb.js";
|
|
2
|
+
import { calcNextSeqNo, includeUsers, } from "../../route-helpers.js";
|
|
3
3
|
// --- Prisma include & result type ---
|
|
4
4
|
const includeWorkCenter = {
|
|
5
5
|
workCenter: { select: { key: true } },
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import erpDb from "
|
|
2
|
-
import { calcNextSeqNo, includeUsers, } from "
|
|
1
|
+
import erpDb from "../../database/erpDb.js";
|
|
2
|
+
import { calcNextSeqNo, includeUsers, } from "../../route-helpers.js";
|
|
3
3
|
// --- Prisma include & result type ---
|
|
4
4
|
export const includeUsersAndFields = {
|
|
5
5
|
...includeUsers,
|