@naisys/erp 3.0.0-beta.3

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.

Potentially problematic release.


This version of @naisys/erp might be problematic. Click here for more details.

Files changed (201) hide show
  1. package/bin/naisys-erp +2 -0
  2. package/client-dist/android-chrome-192x192.png +0 -0
  3. package/client-dist/android-chrome-512x512.png +0 -0
  4. package/client-dist/apple-touch-icon.png +0 -0
  5. package/client-dist/assets/index-45dVo30p.css +1 -0
  6. package/client-dist/assets/index-Dffms7F_.js +168 -0
  7. package/client-dist/assets/naisys-logo-CzoPnn5I.webp +0 -0
  8. package/client-dist/favicon.ico +0 -0
  9. package/client-dist/index.html +42 -0
  10. package/client-dist/site.webmanifest +22 -0
  11. package/dist/api-reference.d.ts +10 -0
  12. package/dist/api-reference.js +101 -0
  13. package/dist/audit.d.ts +5 -0
  14. package/dist/audit.js +14 -0
  15. package/dist/auth-middleware.d.ts +18 -0
  16. package/dist/auth-middleware.js +203 -0
  17. package/dist/dbConfig.d.ts +5 -0
  18. package/dist/dbConfig.js +10 -0
  19. package/dist/erpDb.d.ts +10 -0
  20. package/dist/erpDb.js +34 -0
  21. package/dist/erpServer.d.ts +10 -0
  22. package/dist/erpServer.js +321 -0
  23. package/dist/error-handler.d.ts +7 -0
  24. package/dist/error-handler.js +17 -0
  25. package/dist/generated/prisma/client.d.ts +154 -0
  26. package/dist/generated/prisma/client.js +35 -0
  27. package/dist/generated/prisma/commonInputTypes.d.ts +637 -0
  28. package/dist/generated/prisma/commonInputTypes.js +11 -0
  29. package/dist/generated/prisma/enums.d.ts +59 -0
  30. package/dist/generated/prisma/enums.js +60 -0
  31. package/dist/generated/prisma/internal/class.d.ts +406 -0
  32. package/dist/generated/prisma/internal/class.js +50 -0
  33. package/dist/generated/prisma/internal/prismaNamespace.d.ts +2722 -0
  34. package/dist/generated/prisma/internal/prismaNamespace.js +366 -0
  35. package/dist/generated/prisma/models/Attachment.d.ts +1455 -0
  36. package/dist/generated/prisma/models/Attachment.js +2 -0
  37. package/dist/generated/prisma/models/AuditLog.d.ts +1359 -0
  38. package/dist/generated/prisma/models/AuditLog.js +2 -0
  39. package/dist/generated/prisma/models/Field.d.ts +1880 -0
  40. package/dist/generated/prisma/models/Field.js +2 -0
  41. package/dist/generated/prisma/models/FieldAttachment.d.ts +1245 -0
  42. package/dist/generated/prisma/models/FieldAttachment.js +2 -0
  43. package/dist/generated/prisma/models/FieldRecord.d.ts +1625 -0
  44. package/dist/generated/prisma/models/FieldRecord.js +2 -0
  45. package/dist/generated/prisma/models/FieldSet.d.ts +1577 -0
  46. package/dist/generated/prisma/models/FieldSet.js +2 -0
  47. package/dist/generated/prisma/models/FieldValue.d.ts +1908 -0
  48. package/dist/generated/prisma/models/FieldValue.js +2 -0
  49. package/dist/generated/prisma/models/Item.d.ts +1858 -0
  50. package/dist/generated/prisma/models/Item.js +2 -0
  51. package/dist/generated/prisma/models/ItemInstance.d.ts +1987 -0
  52. package/dist/generated/prisma/models/ItemInstance.js +2 -0
  53. package/dist/generated/prisma/models/LaborTicket.d.ts +1867 -0
  54. package/dist/generated/prisma/models/LaborTicket.js +2 -0
  55. package/dist/generated/prisma/models/Operation.d.ts +2578 -0
  56. package/dist/generated/prisma/models/Operation.js +2 -0
  57. package/dist/generated/prisma/models/OperationDependency.d.ts +1434 -0
  58. package/dist/generated/prisma/models/OperationDependency.js +2 -0
  59. package/dist/generated/prisma/models/OperationFieldRef.d.ts +1539 -0
  60. package/dist/generated/prisma/models/OperationFieldRef.js +2 -0
  61. package/dist/generated/prisma/models/OperationRun.d.ts +2563 -0
  62. package/dist/generated/prisma/models/OperationRun.js +2 -0
  63. package/dist/generated/prisma/models/OperationRunComment.d.ts +1366 -0
  64. package/dist/generated/prisma/models/OperationRunComment.js +2 -0
  65. package/dist/generated/prisma/models/Order.d.ts +1931 -0
  66. package/dist/generated/prisma/models/Order.js +2 -0
  67. package/dist/generated/prisma/models/OrderRevision.d.ts +1962 -0
  68. package/dist/generated/prisma/models/OrderRevision.js +2 -0
  69. package/dist/generated/prisma/models/OrderRun.d.ts +2310 -0
  70. package/dist/generated/prisma/models/OrderRun.js +2 -0
  71. package/dist/generated/prisma/models/SchemaVersion.d.ts +985 -0
  72. package/dist/generated/prisma/models/SchemaVersion.js +2 -0
  73. package/dist/generated/prisma/models/Session.d.ts +1213 -0
  74. package/dist/generated/prisma/models/Session.js +2 -0
  75. package/dist/generated/prisma/models/Step.d.ts +2180 -0
  76. package/dist/generated/prisma/models/Step.js +2 -0
  77. package/dist/generated/prisma/models/StepRun.d.ts +1963 -0
  78. package/dist/generated/prisma/models/StepRun.js +2 -0
  79. package/dist/generated/prisma/models/User.d.ts +11819 -0
  80. package/dist/generated/prisma/models/User.js +2 -0
  81. package/dist/generated/prisma/models/UserPermission.d.ts +1348 -0
  82. package/dist/generated/prisma/models/UserPermission.js +2 -0
  83. package/dist/generated/prisma/models/WorkCenter.d.ts +1657 -0
  84. package/dist/generated/prisma/models/WorkCenter.js +2 -0
  85. package/dist/generated/prisma/models/WorkCenterUser.d.ts +1390 -0
  86. package/dist/generated/prisma/models/WorkCenterUser.js +2 -0
  87. package/dist/generated/prisma/models.d.ts +28 -0
  88. package/dist/generated/prisma/models.js +2 -0
  89. package/dist/hateoas.d.ts +7 -0
  90. package/dist/hateoas.js +61 -0
  91. package/dist/route-helpers.d.ts +318 -0
  92. package/dist/route-helpers.js +220 -0
  93. package/dist/routes/admin.d.ts +3 -0
  94. package/dist/routes/admin.js +147 -0
  95. package/dist/routes/audit.d.ts +3 -0
  96. package/dist/routes/audit.js +36 -0
  97. package/dist/routes/auth.d.ts +3 -0
  98. package/dist/routes/auth.js +112 -0
  99. package/dist/routes/dispatch.d.ts +3 -0
  100. package/dist/routes/dispatch.js +174 -0
  101. package/dist/routes/inventory.d.ts +3 -0
  102. package/dist/routes/inventory.js +70 -0
  103. package/dist/routes/item-fields.d.ts +3 -0
  104. package/dist/routes/item-fields.js +220 -0
  105. package/dist/routes/item-instances.d.ts +3 -0
  106. package/dist/routes/item-instances.js +426 -0
  107. package/dist/routes/items.d.ts +3 -0
  108. package/dist/routes/items.js +252 -0
  109. package/dist/routes/labor-tickets.d.ts +3 -0
  110. package/dist/routes/labor-tickets.js +268 -0
  111. package/dist/routes/operation-dependencies.d.ts +3 -0
  112. package/dist/routes/operation-dependencies.js +170 -0
  113. package/dist/routes/operation-field-refs.d.ts +3 -0
  114. package/dist/routes/operation-field-refs.js +263 -0
  115. package/dist/routes/operation-run-comments.d.ts +3 -0
  116. package/dist/routes/operation-run-comments.js +108 -0
  117. package/dist/routes/operation-run-transitions.d.ts +3 -0
  118. package/dist/routes/operation-run-transitions.js +249 -0
  119. package/dist/routes/operation-runs.d.ts +112 -0
  120. package/dist/routes/operation-runs.js +299 -0
  121. package/dist/routes/operations.d.ts +3 -0
  122. package/dist/routes/operations.js +283 -0
  123. package/dist/routes/order-revision-transitions.d.ts +3 -0
  124. package/dist/routes/order-revision-transitions.js +86 -0
  125. package/dist/routes/order-revisions.d.ts +51 -0
  126. package/dist/routes/order-revisions.js +327 -0
  127. package/dist/routes/order-run-transitions.d.ts +3 -0
  128. package/dist/routes/order-run-transitions.js +215 -0
  129. package/dist/routes/order-runs.d.ts +58 -0
  130. package/dist/routes/order-runs.js +335 -0
  131. package/dist/routes/orders.d.ts +3 -0
  132. package/dist/routes/orders.js +262 -0
  133. package/dist/routes/root.d.ts +3 -0
  134. package/dist/routes/root.js +123 -0
  135. package/dist/routes/schemas.d.ts +3 -0
  136. package/dist/routes/schemas.js +31 -0
  137. package/dist/routes/step-field-attachments.d.ts +3 -0
  138. package/dist/routes/step-field-attachments.js +231 -0
  139. package/dist/routes/step-fields.d.ts +100 -0
  140. package/dist/routes/step-fields.js +315 -0
  141. package/dist/routes/step-run-fields.d.ts +3 -0
  142. package/dist/routes/step-run-fields.js +438 -0
  143. package/dist/routes/step-run-transitions.d.ts +3 -0
  144. package/dist/routes/step-run-transitions.js +113 -0
  145. package/dist/routes/step-runs.d.ts +332 -0
  146. package/dist/routes/step-runs.js +324 -0
  147. package/dist/routes/steps.d.ts +3 -0
  148. package/dist/routes/steps.js +283 -0
  149. package/dist/routes/user-permissions.d.ts +3 -0
  150. package/dist/routes/user-permissions.js +100 -0
  151. package/dist/routes/users.d.ts +57 -0
  152. package/dist/routes/users.js +381 -0
  153. package/dist/routes/work-centers.d.ts +3 -0
  154. package/dist/routes/work-centers.js +280 -0
  155. package/dist/schema-registry.d.ts +3 -0
  156. package/dist/schema-registry.js +45 -0
  157. package/dist/services/attachment-service.d.ts +33 -0
  158. package/dist/services/attachment-service.js +118 -0
  159. package/dist/services/field-ref-service.d.ts +96 -0
  160. package/dist/services/field-ref-service.js +74 -0
  161. package/dist/services/field-service.d.ts +49 -0
  162. package/dist/services/field-service.js +114 -0
  163. package/dist/services/field-value-service.d.ts +61 -0
  164. package/dist/services/field-value-service.js +256 -0
  165. package/dist/services/item-instance-service.d.ts +152 -0
  166. package/dist/services/item-instance-service.js +155 -0
  167. package/dist/services/item-service.d.ts +47 -0
  168. package/dist/services/item-service.js +56 -0
  169. package/dist/services/labor-ticket-service.d.ts +40 -0
  170. package/dist/services/labor-ticket-service.js +148 -0
  171. package/dist/services/log-file-service.d.ts +4 -0
  172. package/dist/services/log-file-service.js +11 -0
  173. package/dist/services/operation-dependency-service.d.ts +33 -0
  174. package/dist/services/operation-dependency-service.js +30 -0
  175. package/dist/services/operation-run-comment-service.d.ts +17 -0
  176. package/dist/services/operation-run-comment-service.js +26 -0
  177. package/dist/services/operation-run-service.d.ts +126 -0
  178. package/dist/services/operation-run-service.js +347 -0
  179. package/dist/services/operation-service.d.ts +47 -0
  180. package/dist/services/operation-service.js +132 -0
  181. package/dist/services/order-revision-service.d.ts +53 -0
  182. package/dist/services/order-revision-service.js +264 -0
  183. package/dist/services/order-run-service.d.ts +138 -0
  184. package/dist/services/order-run-service.js +356 -0
  185. package/dist/services/order-service.d.ts +15 -0
  186. package/dist/services/order-service.js +68 -0
  187. package/dist/services/revision-diff-service.d.ts +3 -0
  188. package/dist/services/revision-diff-service.js +194 -0
  189. package/dist/services/step-run-service.d.ts +172 -0
  190. package/dist/services/step-run-service.js +106 -0
  191. package/dist/services/step-service.d.ts +104 -0
  192. package/dist/services/step-service.js +89 -0
  193. package/dist/services/user-service.d.ts +185 -0
  194. package/dist/services/user-service.js +132 -0
  195. package/dist/services/work-center-service.d.ts +29 -0
  196. package/dist/services/work-center-service.js +106 -0
  197. package/dist/supervisorAuth.d.ts +3 -0
  198. package/dist/supervisorAuth.js +16 -0
  199. package/dist/userService.d.ts +20 -0
  200. package/dist/userService.js +118 -0
  201. package/package.json +69 -0
@@ -0,0 +1,381 @@
1
+ import { getHubAgentById } from "@naisys/hub-database";
2
+ import { ChangePasswordSchema, CreateAgentUserSchema, CreateUserSchema, UpdateUserSchema, } from "@naisys/erp-shared";
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";
9
+ function userItemLinks(username) {
10
+ return [
11
+ selfLink(`/users/${username}`),
12
+ collectionLink("users"),
13
+ schemaLink("UpdateUser"),
14
+ ];
15
+ }
16
+ function userActions(username, isSelf, isAdmin) {
17
+ const href = `${API_PREFIX}/users/${username}`;
18
+ const actions = [];
19
+ if (isAdmin) {
20
+ actions.push({
21
+ rel: "update",
22
+ href,
23
+ method: "PUT",
24
+ title: "Update",
25
+ schema: `${API_PREFIX}/schemas/UpdateUser`,
26
+ body: { username: "" },
27
+ });
28
+ }
29
+ if (isSelf) {
30
+ actions.push({
31
+ rel: "change-password",
32
+ href: `${API_PREFIX}/users/me/password`,
33
+ method: "POST",
34
+ title: "Change Password",
35
+ schema: `${API_PREFIX}/schemas/ChangePassword`,
36
+ body: { password: "" },
37
+ });
38
+ }
39
+ if (isAdmin) {
40
+ actions.push({
41
+ rel: "grant-permission",
42
+ href: `${href}/permissions`,
43
+ method: "POST",
44
+ title: "Grant Permission",
45
+ schema: `${API_PREFIX}/schemas/GrantPermission`,
46
+ body: { permission: "" },
47
+ });
48
+ actions.push({
49
+ rel: "rotate-key",
50
+ href: `${href}/rotate-key`,
51
+ method: "POST",
52
+ title: "Rotate API Key",
53
+ });
54
+ if (!isSelf) {
55
+ actions.push({
56
+ rel: "delete",
57
+ href,
58
+ method: "DELETE",
59
+ title: "Delete",
60
+ });
61
+ }
62
+ }
63
+ return actions;
64
+ }
65
+ function permissionActions(username, permission, isSelf, isAdmin) {
66
+ if (!isAdmin)
67
+ return [];
68
+ const actions = [];
69
+ // Cannot revoke own erp_admin
70
+ if (!(isSelf && permission === "erp_admin")) {
71
+ actions.push({
72
+ rel: "revoke",
73
+ href: `${API_PREFIX}/users/${username}/permissions/${permission}`,
74
+ method: "DELETE",
75
+ title: "Revoke",
76
+ });
77
+ }
78
+ return actions;
79
+ }
80
+ export function formatUser(user, currentUserId, currentUserPermissions, options) {
81
+ if (!user)
82
+ return null;
83
+ const isSelf = user.id === currentUserId;
84
+ const isAdmin = currentUserPermissions.includes("erp_admin");
85
+ return {
86
+ id: user.id,
87
+ username: user.username,
88
+ isAgent: user.isAgent,
89
+ createdAt: user.createdAt.toISOString(),
90
+ updatedAt: user.updatedAt.toISOString(),
91
+ apiKey: isAdmin ? (options?.apiKey ?? null) : undefined,
92
+ permissions: user.permissions.map((p) => ({
93
+ permission: p.permission,
94
+ grantedAt: p.grantedAt.toISOString(),
95
+ grantedBy: p.grantedBy,
96
+ _actions: permissionActions(user.username, p.permission, isSelf, isAdmin),
97
+ })),
98
+ _links: userItemLinks(user.username),
99
+ _actions: userActions(user.username, isSelf, isAdmin),
100
+ };
101
+ }
102
+ function formatListUser(user) {
103
+ return {
104
+ id: user.id,
105
+ username: user.username,
106
+ isAgent: user.isAgent,
107
+ createdAt: user.createdAt.toISOString(),
108
+ permissionCount: user.permissions.length,
109
+ };
110
+ }
111
+ export default function userRoutes(fastify) {
112
+ const app = fastify.withTypeProvider();
113
+ const adminPreHandler = [requirePermission("erp_admin")];
114
+ const requireAdminOrSelf = async (request, reply) => {
115
+ if (!request.erpUser) {
116
+ reply.status(401).send({
117
+ statusCode: 401,
118
+ error: "Unauthorized",
119
+ message: "Authentication required",
120
+ });
121
+ return;
122
+ }
123
+ const isAdmin = hasPermission(request.erpUser, "erp_admin");
124
+ const isSelf = request.params.username === request.erpUser.username;
125
+ if (!isAdmin && !isSelf) {
126
+ reply.status(403).send({
127
+ statusCode: 403,
128
+ error: "Forbidden",
129
+ message: "Permission 'erp_admin' required",
130
+ });
131
+ return;
132
+ }
133
+ };
134
+ const usernameParams = z.object({ username: z.string() });
135
+ // LIST USERS
136
+ app.get("/", {
137
+ preHandler: adminPreHandler,
138
+ schema: {
139
+ description: "List all users with pagination",
140
+ tags: ["Users"],
141
+ querystring: z.object({
142
+ page: z.coerce.number().int().min(1).default(1),
143
+ pageSize: z.coerce.number().int().min(1).max(100).default(20),
144
+ search: z.string().optional(),
145
+ }),
146
+ },
147
+ }, async (request) => {
148
+ const { page, pageSize, search } = request.query;
149
+ const result = await listUsers({ page, pageSize, search });
150
+ const actions = [
151
+ {
152
+ rel: "create",
153
+ href: `${API_PREFIX}/users`,
154
+ method: "POST",
155
+ title: "Create User",
156
+ schema: `${API_PREFIX}/schemas/CreateUser`,
157
+ body: { username: "", password: "" },
158
+ },
159
+ ];
160
+ if (isSupervisorAuth()) {
161
+ actions.push({
162
+ rel: "create-from-agent",
163
+ href: `${API_PREFIX}/users/from-agent`,
164
+ method: "POST",
165
+ title: "Create Agent User",
166
+ schema: `${API_PREFIX}/schemas/CreateAgentUser`,
167
+ body: { agentId: 0 },
168
+ });
169
+ }
170
+ return {
171
+ items: result.items.map(formatListUser),
172
+ total: result.total,
173
+ page,
174
+ pageSize: result.pageSize,
175
+ _links: paginationLinks("users", page, pageSize, result.total, {
176
+ search,
177
+ }),
178
+ _linkTemplates: [
179
+ {
180
+ rel: "item",
181
+ hrefTemplate: `${API_PREFIX}/users/{username}`,
182
+ },
183
+ ],
184
+ _actions: actions,
185
+ };
186
+ });
187
+ // CHANGE OWN PASSWORD (must be registered before /:username routes)
188
+ app.post("/me/password", {
189
+ schema: {
190
+ description: "Change the current user's password",
191
+ tags: ["Users"],
192
+ body: ChangePasswordSchema,
193
+ },
194
+ }, async (request, reply) => {
195
+ if (!request.erpUser) {
196
+ reply.status(401).send({
197
+ statusCode: 401,
198
+ error: "Unauthorized",
199
+ message: "Authentication required",
200
+ });
201
+ return;
202
+ }
203
+ await updateUser(request.erpUser.id, {
204
+ password: request.body.password,
205
+ });
206
+ authCache.clear();
207
+ return { success: true, message: "Password changed" };
208
+ });
209
+ // CREATE USER
210
+ app.post("/", {
211
+ preHandler: adminPreHandler,
212
+ schema: {
213
+ description: "Create a new user",
214
+ tags: ["Users"],
215
+ body: CreateUserSchema,
216
+ },
217
+ }, async (request, reply) => {
218
+ try {
219
+ const user = await createUserWithPassword(request.body);
220
+ const full = formatUser(user, request.erpUser.id, request.erpUser.permissions);
221
+ reply.code(201);
222
+ return mutationResult(request, reply, full, {
223
+ id: full.id,
224
+ username: full.username,
225
+ apiKey: full.apiKey,
226
+ _links: full._links,
227
+ _actions: full._actions,
228
+ });
229
+ }
230
+ catch (err) {
231
+ if (err instanceof Error && err.message.includes("Unique constraint")) {
232
+ reply.code(409);
233
+ return { success: false, message: "Username already exists" };
234
+ }
235
+ throw err;
236
+ }
237
+ });
238
+ // CREATE AGENT USER (from hub agent)
239
+ app.post("/from-agent", {
240
+ preHandler: adminPreHandler,
241
+ schema: {
242
+ description: "Create an ERP user from an existing hub agent",
243
+ tags: ["Users"],
244
+ body: CreateAgentUserSchema,
245
+ },
246
+ }, async (request, reply) => {
247
+ if (!isSupervisorAuth()) {
248
+ reply.code(400);
249
+ return {
250
+ statusCode: 400,
251
+ error: "Bad Request",
252
+ message: "Supervisor auth is not enabled",
253
+ };
254
+ }
255
+ const { agentId } = request.body;
256
+ const hubAgent = await getHubAgentById(agentId);
257
+ if (!hubAgent) {
258
+ reply.code(404);
259
+ return {
260
+ statusCode: 404,
261
+ error: "Not Found",
262
+ message: "Agent not found",
263
+ };
264
+ }
265
+ const existingByUuid = await getUserByUuid(hubAgent.uuid);
266
+ if (existingByUuid) {
267
+ reply.code(409);
268
+ return {
269
+ statusCode: 409,
270
+ error: "Conflict",
271
+ message: "A user with this agent's UUID already exists",
272
+ };
273
+ }
274
+ const existingByUsername = await getUserByUsername(hubAgent.username);
275
+ if (existingByUsername) {
276
+ reply.code(409);
277
+ return {
278
+ statusCode: 409,
279
+ error: "Conflict",
280
+ message: "Username already exists",
281
+ };
282
+ }
283
+ try {
284
+ const user = await createUserForAgent(hubAgent.username, hubAgent.uuid);
285
+ const full = formatUser(user, request.erpUser.id, request.erpUser.permissions);
286
+ reply.code(201);
287
+ return mutationResult(request, reply, full, {
288
+ id: full.id,
289
+ username: full.username,
290
+ apiKey: full.apiKey,
291
+ _links: full._links,
292
+ _actions: full._actions,
293
+ });
294
+ }
295
+ catch (err) {
296
+ if (err instanceof Error && err.message.includes("Unique constraint")) {
297
+ reply.code(409);
298
+ return {
299
+ statusCode: 409,
300
+ error: "Conflict",
301
+ message: "Username already exists",
302
+ };
303
+ }
304
+ throw err;
305
+ }
306
+ });
307
+ // GET USER (admin or self)
308
+ app.get("/:username", {
309
+ preHandler: [requireAdminOrSelf],
310
+ schema: {
311
+ description: "Get user details",
312
+ tags: ["Users"],
313
+ params: usernameParams,
314
+ },
315
+ }, async (request, reply) => {
316
+ const user = await getUserByUsername(request.params.username);
317
+ if (!user) {
318
+ reply.code(404);
319
+ return { success: false, message: "User not found" };
320
+ }
321
+ const apiKey = await getUserApiKey(user.id);
322
+ return formatUser(user, request.erpUser.id, request.erpUser.permissions, { apiKey });
323
+ });
324
+ // UPDATE USER (admin can update any field; non-admin can only change own password)
325
+ app.put("/:username", {
326
+ preHandler: [requireAdminOrSelf],
327
+ schema: {
328
+ description: "Update a user",
329
+ tags: ["Users"],
330
+ params: usernameParams,
331
+ body: UpdateUserSchema,
332
+ },
333
+ }, async (request, reply) => {
334
+ const targetUser = await getUserByUsername(request.params.username);
335
+ if (!targetUser) {
336
+ reply.code(404);
337
+ return { success: false, message: "User not found" };
338
+ }
339
+ 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 };
342
+ try {
343
+ const user = await updateUser(targetUser.id, body);
344
+ authCache.clear();
345
+ const full = formatUser(user, request.erpUser.id, request.erpUser.permissions);
346
+ return mutationResult(request, reply, full, {
347
+ _actions: full._actions,
348
+ });
349
+ }
350
+ catch (err) {
351
+ if (err instanceof Error && err.message.includes("Unique constraint")) {
352
+ reply.code(409);
353
+ return { success: false, message: "Username already exists" };
354
+ }
355
+ throw err;
356
+ }
357
+ });
358
+ // DELETE USER
359
+ app.delete("/:username", {
360
+ preHandler: adminPreHandler,
361
+ schema: {
362
+ description: "Delete a user",
363
+ tags: ["Users"],
364
+ params: usernameParams,
365
+ },
366
+ }, async (request, reply) => {
367
+ if (request.params.username === request.erpUser.username) {
368
+ reply.code(409);
369
+ return { success: false, message: "Cannot delete yourself" };
370
+ }
371
+ const targetUser = await getUserByUsername(request.params.username);
372
+ if (!targetUser) {
373
+ reply.code(404);
374
+ return { success: false, message: "User not found" };
375
+ }
376
+ await deleteUser(targetUser.id);
377
+ authCache.clear();
378
+ return { success: true, message: "User deleted" };
379
+ });
380
+ }
381
+ //# sourceMappingURL=users.js.map
@@ -0,0 +1,3 @@
1
+ import type { FastifyInstance } from "fastify";
2
+ export default function workCenterRoutes(fastify: FastifyInstance): void;
3
+ //# sourceMappingURL=work-centers.d.ts.map
@@ -0,0 +1,280 @@
1
+ import { AssignWorkCenterUserSchema, CreateWorkCenterSchema, ErrorResponseSchema, KeyCreateResponseSchema, MutateResponseSchema, UpdateWorkCenterSchema, WorkCenterListQuerySchema, WorkCenterListResponseSchema, WorkCenterSchema, } from "@naisys/erp-shared";
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";
8
+ const RESOURCE = "work-centers";
9
+ const KeyParamsSchema = z.object({
10
+ key: z.string(),
11
+ });
12
+ const UserParamsSchema = z.object({
13
+ key: z.string(),
14
+ username: z.string(),
15
+ });
16
+ function wcLinks(key) {
17
+ return [
18
+ selfLink(`/${RESOURCE}/${key}`),
19
+ collectionLink(RESOURCE),
20
+ schemaLink("WorkCenter"),
21
+ ];
22
+ }
23
+ function wcActions(key, user) {
24
+ if (!hasPermission(user, "erp_admin"))
25
+ return [];
26
+ const href = `${API_PREFIX}/${RESOURCE}/${key}`;
27
+ return [
28
+ {
29
+ rel: "update",
30
+ href,
31
+ method: "PUT",
32
+ title: "Update",
33
+ schema: `${API_PREFIX}/schemas/UpdateWorkCenter`,
34
+ },
35
+ {
36
+ rel: "delete",
37
+ href,
38
+ method: "DELETE",
39
+ title: "Delete",
40
+ },
41
+ {
42
+ rel: "assignUser",
43
+ href: `${href}/users`,
44
+ method: "POST",
45
+ title: "Assign User",
46
+ schema: `${API_PREFIX}/schemas/AssignWorkCenterUser`,
47
+ },
48
+ ];
49
+ }
50
+ function formatWorkCenter(wc, user) {
51
+ const isAdmin = hasPermission(user, "erp_admin");
52
+ return {
53
+ id: wc.id,
54
+ key: wc.key,
55
+ description: wc.description,
56
+ userAssignments: wc.userAssignments.map((a) => ({
57
+ userId: a.user.id,
58
+ username: a.user.username,
59
+ createdAt: a.createdAt.toISOString(),
60
+ 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
+ : [],
71
+ })),
72
+ ...formatAuditFields(wc),
73
+ _links: wcLinks(wc.key),
74
+ _actions: wcActions(wc.key, user),
75
+ };
76
+ }
77
+ function formatListItem(wc) {
78
+ return {
79
+ id: wc.id,
80
+ key: wc.key,
81
+ description: wc.description,
82
+ userCount: wc._count.userAssignments,
83
+ ...formatAuditFields(wc),
84
+ };
85
+ }
86
+ export default function workCenterRoutes(fastify) {
87
+ const app = fastify.withTypeProvider();
88
+ // LIST
89
+ app.get("/", {
90
+ schema: {
91
+ description: "List work centers with pagination and search",
92
+ tags: ["Work Centers"],
93
+ querystring: WorkCenterListQuerySchema,
94
+ response: {
95
+ 200: WorkCenterListResponseSchema,
96
+ },
97
+ },
98
+ handler: async (request) => {
99
+ const { page, pageSize, search } = request.query;
100
+ const where = {};
101
+ if (search) {
102
+ where.OR = [
103
+ { key: { contains: search } },
104
+ { description: { contains: search } },
105
+ ];
106
+ }
107
+ const [items, total] = await listWorkCenters(where, page, pageSize);
108
+ return {
109
+ items: items.map((wc) => formatListItem(wc)),
110
+ total,
111
+ page,
112
+ pageSize,
113
+ _links: paginationLinks(RESOURCE, page, pageSize, total, { search }),
114
+ _linkTemplates: [
115
+ {
116
+ rel: "item",
117
+ hrefTemplate: `${API_PREFIX}/work-centers/{key}`,
118
+ },
119
+ ],
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
+ : [],
131
+ };
132
+ },
133
+ });
134
+ // CREATE
135
+ app.post("/", {
136
+ schema: {
137
+ description: "Create a new work center",
138
+ tags: ["Work Centers"],
139
+ body: CreateWorkCenterSchema,
140
+ response: {
141
+ 201: KeyCreateResponseSchema,
142
+ },
143
+ },
144
+ preHandler: requirePermission("erp_admin"),
145
+ handler: async (request, reply) => {
146
+ const { key, description } = request.body;
147
+ const userId = request.erpUser.id;
148
+ const wc = await createWorkCenter(key, description, userId);
149
+ const full = formatWorkCenter(wc, request.erpUser);
150
+ reply.status(201);
151
+ return mutationResult(request, reply, full, {
152
+ id: full.id,
153
+ key: full.key,
154
+ _links: full._links,
155
+ _actions: full._actions,
156
+ });
157
+ },
158
+ });
159
+ // GET by key
160
+ app.get("/:key", {
161
+ schema: {
162
+ description: "Get a single work center by key",
163
+ tags: ["Work Centers"],
164
+ params: KeyParamsSchema,
165
+ response: {
166
+ 200: WorkCenterSchema,
167
+ 404: ErrorResponseSchema,
168
+ },
169
+ },
170
+ handler: async (request, reply) => {
171
+ const { key } = request.params;
172
+ const wc = await findExisting(key);
173
+ if (!wc) {
174
+ return notFound(reply, `Work center '${key}' not found`);
175
+ }
176
+ return formatWorkCenter(wc, request.erpUser);
177
+ },
178
+ });
179
+ // UPDATE
180
+ app.put("/:key", {
181
+ schema: {
182
+ description: "Update a work center",
183
+ tags: ["Work Centers"],
184
+ params: KeyParamsSchema,
185
+ body: UpdateWorkCenterSchema,
186
+ response: {
187
+ 200: MutateResponseSchema,
188
+ 404: ErrorResponseSchema,
189
+ },
190
+ },
191
+ preHandler: requirePermission("erp_admin"),
192
+ handler: async (request, reply) => {
193
+ const { key } = request.params;
194
+ const data = request.body;
195
+ const userId = request.erpUser.id;
196
+ const existing = await findExisting(key);
197
+ if (!existing) {
198
+ return notFound(reply, `Work center '${key}' not found`);
199
+ }
200
+ const wc = await updateWorkCenter(key, data, userId);
201
+ const full = formatWorkCenter(wc, request.erpUser);
202
+ return mutationResult(request, reply, full, {
203
+ _actions: full._actions,
204
+ });
205
+ },
206
+ });
207
+ // DELETE
208
+ app.delete("/:key", {
209
+ schema: {
210
+ description: "Delete a work center",
211
+ tags: ["Work Centers"],
212
+ params: KeyParamsSchema,
213
+ response: {
214
+ 204: z.void(),
215
+ 404: ErrorResponseSchema,
216
+ },
217
+ },
218
+ preHandler: requirePermission("erp_admin"),
219
+ handler: async (request, reply) => {
220
+ const { key } = request.params;
221
+ const existing = await findExisting(key);
222
+ if (!existing) {
223
+ return notFound(reply, `Work center '${key}' not found`);
224
+ }
225
+ await deleteWorkCenter(key);
226
+ reply.status(204);
227
+ },
228
+ });
229
+ // ASSIGN USER
230
+ app.post("/:key/users", {
231
+ schema: {
232
+ description: "Assign a user to a work center",
233
+ tags: ["Work Centers"],
234
+ params: KeyParamsSchema,
235
+ body: AssignWorkCenterUserSchema,
236
+ response: {
237
+ 200: MutateResponseSchema,
238
+ 404: ErrorResponseSchema,
239
+ },
240
+ },
241
+ preHandler: requirePermission("erp_admin"),
242
+ handler: async (request, reply) => {
243
+ const { key } = request.params;
244
+ const { username } = request.body;
245
+ const userId = request.erpUser.id;
246
+ const existing = await findExisting(key);
247
+ if (!existing) {
248
+ return notFound(reply, `Work center '${key}' not found`);
249
+ }
250
+ const wc = await assignUser(key, username, userId);
251
+ const full = formatWorkCenter(wc, request.erpUser);
252
+ return mutationResult(request, reply, full, {
253
+ _actions: full._actions,
254
+ });
255
+ },
256
+ });
257
+ // REMOVE USER
258
+ app.delete("/:key/users/:username", {
259
+ schema: {
260
+ description: "Remove a user from a work center",
261
+ tags: ["Work Centers"],
262
+ params: UserParamsSchema,
263
+ response: {
264
+ 204: z.void(),
265
+ 404: ErrorResponseSchema,
266
+ },
267
+ },
268
+ preHandler: requirePermission("erp_admin"),
269
+ handler: async (request, reply) => {
270
+ const { key, username } = request.params;
271
+ const existing = await findExisting(key);
272
+ if (!existing) {
273
+ return notFound(reply, `Work center '${key}' not found`);
274
+ }
275
+ await removeUser(key, username);
276
+ reply.status(204);
277
+ },
278
+ });
279
+ }
280
+ //# sourceMappingURL=work-centers.js.map
@@ -0,0 +1,3 @@
1
+ import type { $ZodType } from "zod/v4/core";
2
+ export declare const schemaRegistry: Record<string, $ZodType>;
3
+ //# sourceMappingURL=schema-registry.d.ts.map