@naisys/erp 3.0.0-beta.6

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 (150) 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.js +101 -0
  12. package/dist/audit.js +14 -0
  13. package/dist/auth-middleware.js +203 -0
  14. package/dist/dbConfig.js +10 -0
  15. package/dist/erpDb.js +34 -0
  16. package/dist/erpServer.js +321 -0
  17. package/dist/error-handler.js +17 -0
  18. package/dist/generated/prisma/client.js +35 -0
  19. package/dist/generated/prisma/commonInputTypes.js +11 -0
  20. package/dist/generated/prisma/enums.js +60 -0
  21. package/dist/generated/prisma/internal/class.js +50 -0
  22. package/dist/generated/prisma/internal/prismaNamespace.js +366 -0
  23. package/dist/generated/prisma/models/Attachment.js +2 -0
  24. package/dist/generated/prisma/models/AuditLog.js +2 -0
  25. package/dist/generated/prisma/models/Field.js +2 -0
  26. package/dist/generated/prisma/models/FieldAttachment.js +2 -0
  27. package/dist/generated/prisma/models/FieldRecord.js +2 -0
  28. package/dist/generated/prisma/models/FieldSet.js +2 -0
  29. package/dist/generated/prisma/models/FieldValue.js +2 -0
  30. package/dist/generated/prisma/models/Item.js +2 -0
  31. package/dist/generated/prisma/models/ItemInstance.js +2 -0
  32. package/dist/generated/prisma/models/LaborTicket.js +2 -0
  33. package/dist/generated/prisma/models/Operation.js +2 -0
  34. package/dist/generated/prisma/models/OperationDependency.js +2 -0
  35. package/dist/generated/prisma/models/OperationFieldRef.js +2 -0
  36. package/dist/generated/prisma/models/OperationRun.js +2 -0
  37. package/dist/generated/prisma/models/OperationRunComment.js +2 -0
  38. package/dist/generated/prisma/models/Order.js +2 -0
  39. package/dist/generated/prisma/models/OrderRevision.js +2 -0
  40. package/dist/generated/prisma/models/OrderRun.js +2 -0
  41. package/dist/generated/prisma/models/SchemaVersion.js +2 -0
  42. package/dist/generated/prisma/models/Session.js +2 -0
  43. package/dist/generated/prisma/models/Step.js +2 -0
  44. package/dist/generated/prisma/models/StepRun.js +2 -0
  45. package/dist/generated/prisma/models/User.js +2 -0
  46. package/dist/generated/prisma/models/UserPermission.js +2 -0
  47. package/dist/generated/prisma/models/WorkCenter.js +2 -0
  48. package/dist/generated/prisma/models/WorkCenterUser.js +2 -0
  49. package/dist/generated/prisma/models.js +2 -0
  50. package/dist/hateoas.js +61 -0
  51. package/dist/route-helpers.js +220 -0
  52. package/dist/routes/admin.js +147 -0
  53. package/dist/routes/audit.js +36 -0
  54. package/dist/routes/auth.js +112 -0
  55. package/dist/routes/dispatch.js +174 -0
  56. package/dist/routes/inventory.js +70 -0
  57. package/dist/routes/item-fields.js +220 -0
  58. package/dist/routes/item-instances.js +426 -0
  59. package/dist/routes/items.js +252 -0
  60. package/dist/routes/labor-tickets.js +268 -0
  61. package/dist/routes/operation-dependencies.js +170 -0
  62. package/dist/routes/operation-field-refs.js +263 -0
  63. package/dist/routes/operation-run-comments.js +108 -0
  64. package/dist/routes/operation-run-transitions.js +249 -0
  65. package/dist/routes/operation-runs.js +299 -0
  66. package/dist/routes/operations.js +283 -0
  67. package/dist/routes/order-revision-transitions.js +86 -0
  68. package/dist/routes/order-revisions.js +327 -0
  69. package/dist/routes/order-run-transitions.js +215 -0
  70. package/dist/routes/order-runs.js +335 -0
  71. package/dist/routes/orders.js +262 -0
  72. package/dist/routes/root.js +123 -0
  73. package/dist/routes/schemas.js +31 -0
  74. package/dist/routes/step-field-attachments.js +231 -0
  75. package/dist/routes/step-fields.js +315 -0
  76. package/dist/routes/step-run-fields.js +438 -0
  77. package/dist/routes/step-run-transitions.js +113 -0
  78. package/dist/routes/step-runs.js +324 -0
  79. package/dist/routes/steps.js +283 -0
  80. package/dist/routes/user-permissions.js +100 -0
  81. package/dist/routes/users.js +381 -0
  82. package/dist/routes/work-centers.js +280 -0
  83. package/dist/schema-registry.js +45 -0
  84. package/dist/services/attachment-service.js +118 -0
  85. package/dist/services/field-ref-service.js +74 -0
  86. package/dist/services/field-service.js +114 -0
  87. package/dist/services/field-value-service.js +256 -0
  88. package/dist/services/item-instance-service.js +155 -0
  89. package/dist/services/item-service.js +56 -0
  90. package/dist/services/labor-ticket-service.js +148 -0
  91. package/dist/services/log-file-service.js +11 -0
  92. package/dist/services/operation-dependency-service.js +30 -0
  93. package/dist/services/operation-run-comment-service.js +26 -0
  94. package/dist/services/operation-run-service.js +347 -0
  95. package/dist/services/operation-service.js +132 -0
  96. package/dist/services/order-revision-service.js +264 -0
  97. package/dist/services/order-run-service.js +356 -0
  98. package/dist/services/order-service.js +68 -0
  99. package/dist/services/revision-diff-service.js +194 -0
  100. package/dist/services/step-run-service.js +106 -0
  101. package/dist/services/step-service.js +89 -0
  102. package/dist/services/user-service.js +132 -0
  103. package/dist/services/work-center-service.js +106 -0
  104. package/dist/supervisorAuth.js +16 -0
  105. package/dist/userService.js +118 -0
  106. package/package.json +75 -0
  107. package/prisma/migrations/20260212170352_init/migration.sql +125 -0
  108. package/prisma/migrations/20260308000000_multi_session/migration.sql +23 -0
  109. package/prisma/migrations/20260309000000_add_user_api_key/migration.sql +5 -0
  110. package/prisma/migrations/20260309010000_add_plan_operations/migration.sql +21 -0
  111. package/prisma/migrations/20260309020000_rename_exec_orders_to_order_runs/migration.sql +13 -0
  112. package/prisma/migrations/20260310000000_rename_plan_order_revs_to_order_revisions/migration.sql +22 -0
  113. package/prisma/migrations/20260310100000_rename_plan_orders_to_orders/migration.sql +23 -0
  114. package/prisma/migrations/20260310200000_rename_order_no_to_run_no/migration.sql +3 -0
  115. package/prisma/migrations/20260312000000_add_user_permissions/migration.sql +16 -0
  116. package/prisma/migrations/20260313000000_rename_plan_operations_to_operations/migration.sql +2 -0
  117. package/prisma/migrations/20260313100000_add_steps/migration.sql +20 -0
  118. package/prisma/migrations/20260314000000_add_step_fields/migration.sql +22 -0
  119. package/prisma/migrations/20260315000000_add_operation_runs/migration.sql +24 -0
  120. package/prisma/migrations/20260315100000_add_step_runs/migration.sql +40 -0
  121. package/prisma/migrations/20260316000000_drop_order_name/migration.sql +12 -0
  122. package/prisma/migrations/20260317000000_add_attachments/migration.sql +28 -0
  123. package/prisma/migrations/20260317000000_add_items/migration.sql +21 -0
  124. package/prisma/migrations/20260317100000_add_order_item_id/migration.sql +8 -0
  125. package/prisma/migrations/20260318000000_add_labor_tickets/migration.sql +27 -0
  126. package/prisma/migrations/20260319000000_add_operation_dependencies/migration.sql +17 -0
  127. package/prisma/migrations/20260320000000_step_field_is_array/migration.sql +5 -0
  128. package/prisma/migrations/20260320100000_rename_is_array_to_multi_value/migration.sql +2 -0
  129. package/prisma/migrations/20260320200000_add_field_types/migration.sql +2 -0
  130. package/prisma/migrations/20260321000000_add_multi_set/migration.sql +13 -0
  131. package/prisma/migrations/20260322000000_add_field_sets/migration.sql +90 -0
  132. package/prisma/migrations/20260323000000_add_item_instances/migration.sql +18 -0
  133. package/prisma/migrations/20260324000000_add_field_records/migration.sql +77 -0
  134. package/prisma/migrations/20260325000000_add_run_costs/migration.sql +3 -0
  135. package/prisma/migrations/20260326000000_add_comments/migration.sql +16 -0
  136. package/prisma/migrations/20260327000000_move_assigned_to_op_run/migration.sql +3 -0
  137. package/prisma/migrations/20260328000000_add_step_completion_note/migration.sql +2 -0
  138. package/prisma/migrations/20260328000000_add_step_title/migration.sql +2 -0
  139. package/prisma/migrations/20260329000000_rename_notes_to_release_note/migration.sql +2 -0
  140. package/prisma/migrations/20260329000000_simplify_order_run_dates/migration.sql +5 -0
  141. package/prisma/migrations/20260330000000_add_operation_run_completion_note/migration.sql +2 -0
  142. package/prisma/migrations/20260331000000_add_work_centers/migration.sql +30 -0
  143. package/prisma/migrations/20260401000000_fix_field_values_column_shift/migration.sql +26 -0
  144. package/prisma/migrations/20260402000000_rename_completion_note_to_status_note/migration.sql +5 -0
  145. package/prisma/migrations/20260403000000_add_operation_field_refs/migration.sql +16 -0
  146. package/prisma/migrations/20260404000000_rename_multi_value_to_is_array/migration.sql +2 -0
  147. package/prisma/migrations/20260404100000_add_attachment_public_id/migration.sql +8 -0
  148. package/prisma/migrations/migration_lock.toml +3 -0
  149. package/prisma/schema.prisma +595 -0
  150. package/prisma.config.ts +18 -0
@@ -0,0 +1,100 @@
1
+ import { ErpPermissionEnum, GrantPermissionSchema } from "@naisys/erp-shared";
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";
6
+ import { formatUser } from "./users.js";
7
+ export default function userPermissionRoutes(fastify) {
8
+ const app = fastify.withTypeProvider();
9
+ const adminPreHandler = [requirePermission("erp_admin")];
10
+ const usernameParams = z.object({ username: z.string() });
11
+ // ROTATE API KEY
12
+ app.post("/:username/rotate-key", {
13
+ preHandler: adminPreHandler,
14
+ schema: {
15
+ description: "Rotate a user's API key",
16
+ tags: ["Users"],
17
+ params: usernameParams,
18
+ },
19
+ }, async (request, reply) => {
20
+ const targetUser = await getUserByUsername(request.params.username);
21
+ if (!targetUser) {
22
+ reply.code(404);
23
+ return { success: false, message: "User not found" };
24
+ }
25
+ await rotateUserApiKey(targetUser.id);
26
+ authCache.clear();
27
+ return { success: true, message: "API key rotated" };
28
+ });
29
+ // GRANT PERMISSION
30
+ app.post("/:username/permissions", {
31
+ preHandler: adminPreHandler,
32
+ schema: {
33
+ description: "Grant a permission to a user",
34
+ tags: ["Users"],
35
+ params: usernameParams,
36
+ body: GrantPermissionSchema,
37
+ },
38
+ }, async (request, reply) => {
39
+ const targetUser = await getUserByUsername(request.params.username);
40
+ if (!targetUser) {
41
+ reply.code(404);
42
+ return { success: false, message: "User not found" };
43
+ }
44
+ try {
45
+ await grantPermission(targetUser.id, request.body.permission, request.erpUser.id);
46
+ authCache.clear();
47
+ const user = await getUserById(targetUser.id);
48
+ const full = formatUser(user, request.erpUser.id, request.erpUser.permissions);
49
+ return mutationResult(request, reply, full, {
50
+ _actions: full._actions,
51
+ });
52
+ }
53
+ catch (err) {
54
+ if (err instanceof Error && err.message.includes("Unique constraint")) {
55
+ reply.code(409);
56
+ return {
57
+ success: false,
58
+ message: "Permission already granted",
59
+ };
60
+ }
61
+ throw err;
62
+ }
63
+ });
64
+ // REVOKE PERMISSION
65
+ app.delete("/:username/permissions/:permission", {
66
+ preHandler: adminPreHandler,
67
+ schema: {
68
+ description: "Revoke a permission from a user",
69
+ tags: ["Users"],
70
+ params: z.object({
71
+ username: z.string(),
72
+ permission: ErpPermissionEnum,
73
+ }),
74
+ },
75
+ }, async (request, reply) => {
76
+ const { username, permission } = request.params;
77
+ // Cannot revoke own erp_admin
78
+ if (username === request.erpUser.username &&
79
+ permission === "erp_admin") {
80
+ reply.code(409);
81
+ return {
82
+ success: false,
83
+ message: "Cannot revoke your own erp_admin permission",
84
+ };
85
+ }
86
+ const targetUser = await getUserByUsername(username);
87
+ if (!targetUser) {
88
+ reply.code(404);
89
+ return { success: false, message: "User not found" };
90
+ }
91
+ await revokePermission(targetUser.id, permission);
92
+ authCache.clear();
93
+ const user = await getUserById(targetUser.id);
94
+ const full = formatUser(user, request.erpUser.id, request.erpUser.permissions);
95
+ return mutationResult(request, reply, full, {
96
+ _actions: full._actions,
97
+ });
98
+ });
99
+ }
100
+ //# sourceMappingURL=user-permissions.js.map
@@ -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