@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.
Files changed (83) hide show
  1. package/.env.example +5 -0
  2. package/client-dist/assets/index-CSiMTJfw.css +14 -0
  3. package/client-dist/assets/index-D6lSIioV.js +11167 -0
  4. package/client-dist/assets/rolldown-runtime-CvHMtSRF.js +33 -0
  5. package/client-dist/assets/vendor-CJ0ET9hP.js +75181 -0
  6. package/client-dist/assets/vendor-CLUPjUnv.css +8747 -0
  7. package/client-dist/favicon-16x16.png +0 -0
  8. package/client-dist/favicon-32x32.png +0 -0
  9. package/client-dist/favicon.ico +0 -0
  10. package/client-dist/index.html +17 -2
  11. package/dist/{dbConfig.js → database/dbConfig.js} +1 -1
  12. package/dist/{erpDb.js → database/erpDb.js} +1 -1
  13. package/dist/erpRoutes.js +115 -0
  14. package/dist/erpServer.js +85 -161
  15. package/dist/error-handler.js +3 -0
  16. package/dist/generated/prisma/internal/class.js +4 -4
  17. package/dist/generated/prisma/internal/prismaNamespace.js +3 -1
  18. package/dist/middleware/auth-middleware.js +146 -0
  19. package/dist/route-helpers.js +2 -2
  20. package/dist/routes/admin.js +15 -7
  21. package/dist/routes/audit.js +1 -1
  22. package/dist/routes/{item-fields.js → items/item-fields.js} +24 -22
  23. package/dist/routes/{item-instances.js → items/item-instances.js} +42 -24
  24. package/dist/routes/{items.js → items/items.js} +35 -33
  25. package/dist/routes/{operation-dependencies.js → operations/operation-dependencies.js} +6 -6
  26. package/dist/routes/{operation-field-refs.js → operations/operation-field-refs.js} +6 -6
  27. package/dist/routes/{operation-run-comments.js → operations/operation-run-comments.js} +5 -5
  28. package/dist/routes/{operation-run-transitions.js → operations/operation-run-transitions.js} +29 -13
  29. package/dist/routes/{operation-runs.js → operations/operation-runs.js} +48 -10
  30. package/dist/routes/{operations.js → operations/operations.js} +6 -6
  31. package/dist/routes/{order-revision-transitions.js → orders/order-revision-transitions.js} +4 -4
  32. package/dist/routes/{order-revisions.js → orders/order-revisions.js} +6 -6
  33. package/dist/routes/{order-run-transitions.js → orders/order-run-transitions.js} +11 -5
  34. package/dist/routes/{order-runs.js → orders/order-runs.js} +7 -5
  35. package/dist/routes/{orders.js → orders/orders.js} +15 -11
  36. package/dist/routes/{dispatch.js → production/dispatch.js} +88 -7
  37. package/dist/routes/{inventory.js → production/inventory.js} +33 -10
  38. package/dist/routes/{labor-tickets.js → production/labor-tickets.js} +7 -7
  39. package/dist/routes/{work-centers.js → production/work-centers.js} +29 -29
  40. package/dist/routes/root.js +1 -1
  41. package/dist/routes/{step-field-attachments.js → steps/step-field-attachments.js} +8 -8
  42. package/dist/routes/{step-fields.js → steps/step-fields.js} +6 -6
  43. package/dist/routes/{step-run-fields.js → steps/step-run-fields.js} +9 -9
  44. package/dist/routes/{step-run-transitions.js → steps/step-run-transitions.js} +6 -6
  45. package/dist/routes/{step-runs.js → steps/step-runs.js} +7 -7
  46. package/dist/routes/{steps.js → steps/steps.js} +5 -5
  47. package/dist/routes/{auth.js → users/auth.js} +11 -23
  48. package/dist/routes/{user-permissions.js → users/user-permissions.js} +21 -7
  49. package/dist/routes/{users.js → users/users.js} +42 -20
  50. package/dist/services/attachment-service.js +2 -2
  51. package/dist/services/{item-instance-service.js → inventory/item-instance-service.js} +2 -2
  52. package/dist/services/{item-service.js → inventory/item-service.js} +2 -2
  53. package/dist/services/{operation-dependency-service.js → operations/operation-dependency-service.js} +1 -1
  54. package/dist/services/{operation-run-comment-service.js → operations/operation-run-comment-service.js} +1 -1
  55. package/dist/services/{operation-run-service.js → operations/operation-run-service.js} +15 -4
  56. package/dist/services/{operation-service.js → operations/operation-service.js} +2 -2
  57. package/dist/services/{step-run-service.js → operations/step-run-service.js} +1 -1
  58. package/dist/services/{step-service.js → operations/step-service.js} +2 -2
  59. package/dist/services/{order-revision-service.js → orders/order-revision-service.js} +4 -5
  60. package/dist/services/{order-run-service.js → orders/order-run-service.js} +68 -22
  61. package/dist/services/{order-service.js → orders/order-service.js} +11 -2
  62. package/dist/services/{revision-diff-service.js → orders/revision-diff-service.js} +11 -10
  63. package/dist/services/{field-ref-service.js → production/field-ref-service.js} +1 -1
  64. package/dist/services/{field-service.js → production/field-service.js} +2 -2
  65. package/dist/services/{field-value-service.js → production/field-value-service.js} +27 -3
  66. package/dist/services/production/labor-ticket-backfill.js +67 -0
  67. package/dist/services/{labor-ticket-service.js → production/labor-ticket-service.js} +21 -15
  68. package/dist/services/{work-center-service.js → production/work-center-service.js} +2 -2
  69. package/dist/services/user-service.js +94 -28
  70. package/dist/version.js +12 -0
  71. package/npm-shrinkwrap.json +3000 -0
  72. package/package.json +11 -9
  73. package/prisma/migrations/20260427010000_hash_user_api_keys/migration.sql +10 -0
  74. package/prisma/migrations/20260427020000_nullable_user_password_hash/migration.sql +39 -0
  75. package/prisma/migrations/20260517000000_add_op_run_tokens/migration.sql +2 -0
  76. package/prisma/schema.prisma +4 -2
  77. package/client-dist/assets/index-45dVo30p.css +0 -1
  78. package/client-dist/assets/index-C9uuPHLH.js +0 -168
  79. package/dist/auth-middleware.js +0 -203
  80. package/dist/userService.js +0 -118
  81. /package/bin/{naisys-erp → naisys-erp.js} +0 -0
  82. /package/dist/{supervisorAuth.js → middleware/supervisorAuth.js} +0 -0
  83. /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 { hasPermission, requirePermission } from "../auth-middleware.js";
4
- import { notFound } from "../error-handler.js";
5
- import { API_PREFIX, collectionLink, paginationLinks, schemaLink, selfLink, } from "../hateoas.js";
6
- import { formatAuditFields, mutationResult } from "../route-helpers.js";
7
- import { assignUser, createWorkCenter, deleteWorkCenter, findExisting, listWorkCenters, removeUser, updateWorkCenter, } from "../services/work-center-service.js";
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 isAdmin = hasPermission(user, "erp_admin");
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: isAdmin
62
- ? [
63
- {
64
- rel: "remove",
65
- href: `${API_PREFIX}/${RESOURCE}/${wc.key}/users/${a.user.username}`,
66
- method: "DELETE",
67
- title: "Remove",
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: hasPermission(request.erpUser, "erp_admin")
121
- ? [
122
- {
123
- rel: "create",
124
- href: `${API_PREFIX}/${RESOURCE}`,
125
- method: "POST",
126
- title: "Create Work Center",
127
- schema: `${API_PREFIX}/schemas/CreateWorkCenter`,
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
  });
@@ -1,4 +1,4 @@
1
- import { hasPermission } from "../auth-middleware.js";
1
+ import { hasPermission } from "../middleware/auth-middleware.js";
2
2
  export default function rootRoute(fastify) {
3
3
  fastify.get("/", {
4
4
  schema: {
@@ -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 { requirePermission } from "../auth-middleware.js";
6
- import erpDb from "../erpDb.js";
7
- import { conflict, notFound } from "../error-handler.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/field-service.js";
11
- import { findStepRunWithField, rebuildAttachmentFieldValue, upsertFieldValue, } from "../services/field-value-service.js";
12
- import { isUserClockedIn } from "../services/labor-ticket-service.js";
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 { requirePermission } from "../auth-middleware.js";
4
- import erpDb from "../erpDb.js";
5
- import { conflict, notFound } from "../error-handler.js";
6
- import { API_PREFIX, selfLink } from "../hateoas.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/field-service.js";
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 { requirePermission } from "../auth-middleware.js";
4
- import erpDb from "../erpDb.js";
5
- import { conflict, notFound, unprocessable } from "../error-handler.js";
6
- import { API_PREFIX } from "../hateoas.js";
7
- import { checkOpRunInProgress, checkOrderRunStarted, checkWorkCenterAccess, mutationResult, resolveStepRun, } from "../route-helpers.js";
8
- import { ensureStepRunFieldRecord } from "../services/field-service.js";
9
- import { checkFieldValueShape, clearAttachmentFieldValue, deleteFieldValueSet, deserializeFieldValue, findStepRunWithField, serializeFieldValue, upsertFieldValue, validateFieldValue, } from "../services/field-value-service.js";
10
- import { isUserClockedIn } from "../services/labor-ticket-service.js";
11
- import { getStepRunWithFields } from "../services/step-run-service.js";
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 { requirePermission } from "../auth-middleware.js";
4
- import { conflict, notFound, unprocessable } from "../error-handler.js";
5
- import { checkOpRunInProgress, checkOrderRunStarted, checkWorkCenterAccess, mutationResult, resolveStepRun, } from "../route-helpers.js";
6
- import { validateCompletionFields } from "../services/field-value-service.js";
7
- import { isUserClockedIn } from "../services/labor-ticket-service.js";
8
- import { getStepRunWithFields, updateStepRun, } from "../services/step-run-service.js";
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 { hasPermission } from "../auth-middleware.js";
4
- import { notFound } from "../error-handler.js";
5
- import { API_PREFIX, selfLink } from "../hateoas.js";
6
- import { checkWorkCenterAccess, childItemLinks, formatAuditFields, resolveActions, resolveOpRun, resolveStepRun, } from "../route-helpers.js";
7
- import { deserializeFieldValue, validateCompletionFields, validateFieldValue, } from "../services/field-value-service.js";
8
- import { isUserClockedIn } from "../services/labor-ticket-service.js";
9
- import { getStepRunWithFields, listStepRuns, listStepRunsWithFields, } from "../services/step-run-service.js";
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 { requirePermission } from "../auth-middleware.js";
4
- import { conflict, notFound } from "../error-handler.js";
5
- import { API_PREFIX, selfLink } from "../hateoas.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/step-service.js";
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 { authenticateAndCreateSession, deleteSession, } from "@naisys/supervisor-database";
3
+ import { deleteSession } from "@naisys/supervisor-database";
4
4
  import bcrypt from "bcryptjs";
5
5
  import { randomUUID } from "crypto";
6
- import { authCache } from "../auth-middleware.js";
7
- import erpDb from "../erpDb.js";
8
- import { unauthorized } from "../error-handler.js";
9
- import { isSupervisorAuth } from "../supervisorAuth.js";
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: authenticate against supervisor DB
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
- const authResult = await authenticateAndCreateSession(username, password);
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 "../auth-middleware.js";
4
- import { mutationResult } from "../route-helpers.js";
5
- import { getUserById, getUserByUsername, grantPermission, revokePermission, rotateUserApiKey, } from "../services/user-service.js";
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 { success: true, message: "API key rotated" };
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 full = formatUser(user, request.erpUser.id, request.erpUser.permissions);
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 full = formatUser(user, request.erpUser.id, request.erpUser.permissions);
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 { authCache, hasPermission, requirePermission, } from "../auth-middleware.js";
5
- import { API_PREFIX, collectionLink, paginationLinks, schemaLink, selfLink, } from "../hateoas.js";
6
- import { mutationResult } from "../route-helpers.js";
7
- import { createUserForAgent, createUserWithPassword, deleteUser, getUserApiKey, getUserByUsername, getUserByUuid, listUsers, updateUser, } from "../services/user-service.js";
8
- import { isSupervisorAuth } from "../supervisorAuth.js";
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
- if (isSelf) {
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
- actions.push({
49
- rel: "rotate-key",
50
- href: `${href}/rotate-key`,
51
- method: "POST",
52
- title: "Rotate API Key",
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
- apiKey: isAdmin ? (options?.apiKey ?? null) : undefined,
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 apiKey = await getUserApiKey(user.id);
322
- return formatUser(user, request.erpUser.id, request.erpUser.permissions, { apiKey });
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
- // Non-admins can only change their own password
341
- const body = isAdmin ? request.body : { password: request.body.password };
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 full = formatUser(user, request.erpUser.id, request.erpUser.permissions);
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(attachmentsDir(), "tmp");
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 "../erpDb.js";
2
- import { includeUsers } from "../route-helpers.js";
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 "../erpDb.js";
2
- import { includeUsers } from "../route-helpers.js";
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,4 +1,4 @@
1
- import erpDb from "../erpDb.js";
1
+ import erpDb from "../../database/erpDb.js";
2
2
  const depInclude = {
3
3
  predecessor: { select: { seqNo: true, title: true } },
4
4
  createdBy: { select: { username: true } },
@@ -1,4 +1,4 @@
1
- import erpDb from "../erpDb.js";
1
+ import erpDb from "../../database/erpDb.js";
2
2
  // --- Prisma include & result type ---
3
3
  export const includeComment = {
4
4
  createdBy: { select: { username: true } },
@@ -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 erpDb from "../erpDb.js";
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 = new Map(stepRuns.map((sr) => [sr.stepId, sr]));
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 "../erpDb.js";
2
- import { calcNextSeqNo, includeUsers, } from "../route-helpers.js";
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,4 +1,4 @@
1
- import erpDb from "../erpDb.js";
1
+ import erpDb from "../../database/erpDb.js";
2
2
  // --- Prisma include & result type ---
3
3
  export const includeStepRunWithFields = {
4
4
  step: {
@@ -1,5 +1,5 @@
1
- import erpDb from "../erpDb.js";
2
- import { calcNextSeqNo, includeUsers, } from "../route-helpers.js";
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,