@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.
- package/bin/naisys-erp +2 -0
- package/client-dist/android-chrome-192x192.png +0 -0
- package/client-dist/android-chrome-512x512.png +0 -0
- package/client-dist/apple-touch-icon.png +0 -0
- package/client-dist/assets/index-45dVo30p.css +1 -0
- package/client-dist/assets/index-Dffms7F_.js +168 -0
- package/client-dist/assets/naisys-logo-CzoPnn5I.webp +0 -0
- package/client-dist/favicon.ico +0 -0
- package/client-dist/index.html +42 -0
- package/client-dist/site.webmanifest +22 -0
- package/dist/api-reference.d.ts +10 -0
- package/dist/api-reference.js +101 -0
- package/dist/audit.d.ts +5 -0
- package/dist/audit.js +14 -0
- package/dist/auth-middleware.d.ts +18 -0
- package/dist/auth-middleware.js +203 -0
- package/dist/dbConfig.d.ts +5 -0
- package/dist/dbConfig.js +10 -0
- package/dist/erpDb.d.ts +10 -0
- package/dist/erpDb.js +34 -0
- package/dist/erpServer.d.ts +10 -0
- package/dist/erpServer.js +321 -0
- package/dist/error-handler.d.ts +7 -0
- package/dist/error-handler.js +17 -0
- package/dist/generated/prisma/client.d.ts +154 -0
- package/dist/generated/prisma/client.js +35 -0
- package/dist/generated/prisma/commonInputTypes.d.ts +637 -0
- package/dist/generated/prisma/commonInputTypes.js +11 -0
- package/dist/generated/prisma/enums.d.ts +59 -0
- package/dist/generated/prisma/enums.js +60 -0
- package/dist/generated/prisma/internal/class.d.ts +406 -0
- package/dist/generated/prisma/internal/class.js +50 -0
- package/dist/generated/prisma/internal/prismaNamespace.d.ts +2722 -0
- package/dist/generated/prisma/internal/prismaNamespace.js +366 -0
- package/dist/generated/prisma/models/Attachment.d.ts +1455 -0
- package/dist/generated/prisma/models/Attachment.js +2 -0
- package/dist/generated/prisma/models/AuditLog.d.ts +1359 -0
- package/dist/generated/prisma/models/AuditLog.js +2 -0
- package/dist/generated/prisma/models/Field.d.ts +1880 -0
- package/dist/generated/prisma/models/Field.js +2 -0
- package/dist/generated/prisma/models/FieldAttachment.d.ts +1245 -0
- package/dist/generated/prisma/models/FieldAttachment.js +2 -0
- package/dist/generated/prisma/models/FieldRecord.d.ts +1625 -0
- package/dist/generated/prisma/models/FieldRecord.js +2 -0
- package/dist/generated/prisma/models/FieldSet.d.ts +1577 -0
- package/dist/generated/prisma/models/FieldSet.js +2 -0
- package/dist/generated/prisma/models/FieldValue.d.ts +1908 -0
- package/dist/generated/prisma/models/FieldValue.js +2 -0
- package/dist/generated/prisma/models/Item.d.ts +1858 -0
- package/dist/generated/prisma/models/Item.js +2 -0
- package/dist/generated/prisma/models/ItemInstance.d.ts +1987 -0
- package/dist/generated/prisma/models/ItemInstance.js +2 -0
- package/dist/generated/prisma/models/LaborTicket.d.ts +1867 -0
- package/dist/generated/prisma/models/LaborTicket.js +2 -0
- package/dist/generated/prisma/models/Operation.d.ts +2578 -0
- package/dist/generated/prisma/models/Operation.js +2 -0
- package/dist/generated/prisma/models/OperationDependency.d.ts +1434 -0
- package/dist/generated/prisma/models/OperationDependency.js +2 -0
- package/dist/generated/prisma/models/OperationFieldRef.d.ts +1539 -0
- package/dist/generated/prisma/models/OperationFieldRef.js +2 -0
- package/dist/generated/prisma/models/OperationRun.d.ts +2563 -0
- package/dist/generated/prisma/models/OperationRun.js +2 -0
- package/dist/generated/prisma/models/OperationRunComment.d.ts +1366 -0
- package/dist/generated/prisma/models/OperationRunComment.js +2 -0
- package/dist/generated/prisma/models/Order.d.ts +1931 -0
- package/dist/generated/prisma/models/Order.js +2 -0
- package/dist/generated/prisma/models/OrderRevision.d.ts +1962 -0
- package/dist/generated/prisma/models/OrderRevision.js +2 -0
- package/dist/generated/prisma/models/OrderRun.d.ts +2310 -0
- package/dist/generated/prisma/models/OrderRun.js +2 -0
- package/dist/generated/prisma/models/SchemaVersion.d.ts +985 -0
- package/dist/generated/prisma/models/SchemaVersion.js +2 -0
- package/dist/generated/prisma/models/Session.d.ts +1213 -0
- package/dist/generated/prisma/models/Session.js +2 -0
- package/dist/generated/prisma/models/Step.d.ts +2180 -0
- package/dist/generated/prisma/models/Step.js +2 -0
- package/dist/generated/prisma/models/StepRun.d.ts +1963 -0
- package/dist/generated/prisma/models/StepRun.js +2 -0
- package/dist/generated/prisma/models/User.d.ts +11819 -0
- package/dist/generated/prisma/models/User.js +2 -0
- package/dist/generated/prisma/models/UserPermission.d.ts +1348 -0
- package/dist/generated/prisma/models/UserPermission.js +2 -0
- package/dist/generated/prisma/models/WorkCenter.d.ts +1657 -0
- package/dist/generated/prisma/models/WorkCenter.js +2 -0
- package/dist/generated/prisma/models/WorkCenterUser.d.ts +1390 -0
- package/dist/generated/prisma/models/WorkCenterUser.js +2 -0
- package/dist/generated/prisma/models.d.ts +28 -0
- package/dist/generated/prisma/models.js +2 -0
- package/dist/hateoas.d.ts +7 -0
- package/dist/hateoas.js +61 -0
- package/dist/route-helpers.d.ts +318 -0
- package/dist/route-helpers.js +220 -0
- package/dist/routes/admin.d.ts +3 -0
- package/dist/routes/admin.js +147 -0
- package/dist/routes/audit.d.ts +3 -0
- package/dist/routes/audit.js +36 -0
- package/dist/routes/auth.d.ts +3 -0
- package/dist/routes/auth.js +112 -0
- package/dist/routes/dispatch.d.ts +3 -0
- package/dist/routes/dispatch.js +174 -0
- package/dist/routes/inventory.d.ts +3 -0
- package/dist/routes/inventory.js +70 -0
- package/dist/routes/item-fields.d.ts +3 -0
- package/dist/routes/item-fields.js +220 -0
- package/dist/routes/item-instances.d.ts +3 -0
- package/dist/routes/item-instances.js +426 -0
- package/dist/routes/items.d.ts +3 -0
- package/dist/routes/items.js +252 -0
- package/dist/routes/labor-tickets.d.ts +3 -0
- package/dist/routes/labor-tickets.js +268 -0
- package/dist/routes/operation-dependencies.d.ts +3 -0
- package/dist/routes/operation-dependencies.js +170 -0
- package/dist/routes/operation-field-refs.d.ts +3 -0
- package/dist/routes/operation-field-refs.js +263 -0
- package/dist/routes/operation-run-comments.d.ts +3 -0
- package/dist/routes/operation-run-comments.js +108 -0
- package/dist/routes/operation-run-transitions.d.ts +3 -0
- package/dist/routes/operation-run-transitions.js +249 -0
- package/dist/routes/operation-runs.d.ts +112 -0
- package/dist/routes/operation-runs.js +299 -0
- package/dist/routes/operations.d.ts +3 -0
- package/dist/routes/operations.js +283 -0
- package/dist/routes/order-revision-transitions.d.ts +3 -0
- package/dist/routes/order-revision-transitions.js +86 -0
- package/dist/routes/order-revisions.d.ts +51 -0
- package/dist/routes/order-revisions.js +327 -0
- package/dist/routes/order-run-transitions.d.ts +3 -0
- package/dist/routes/order-run-transitions.js +215 -0
- package/dist/routes/order-runs.d.ts +58 -0
- package/dist/routes/order-runs.js +335 -0
- package/dist/routes/orders.d.ts +3 -0
- package/dist/routes/orders.js +262 -0
- package/dist/routes/root.d.ts +3 -0
- package/dist/routes/root.js +123 -0
- package/dist/routes/schemas.d.ts +3 -0
- package/dist/routes/schemas.js +31 -0
- package/dist/routes/step-field-attachments.d.ts +3 -0
- package/dist/routes/step-field-attachments.js +231 -0
- package/dist/routes/step-fields.d.ts +100 -0
- package/dist/routes/step-fields.js +315 -0
- package/dist/routes/step-run-fields.d.ts +3 -0
- package/dist/routes/step-run-fields.js +438 -0
- package/dist/routes/step-run-transitions.d.ts +3 -0
- package/dist/routes/step-run-transitions.js +113 -0
- package/dist/routes/step-runs.d.ts +332 -0
- package/dist/routes/step-runs.js +324 -0
- package/dist/routes/steps.d.ts +3 -0
- package/dist/routes/steps.js +283 -0
- package/dist/routes/user-permissions.d.ts +3 -0
- package/dist/routes/user-permissions.js +100 -0
- package/dist/routes/users.d.ts +57 -0
- package/dist/routes/users.js +381 -0
- package/dist/routes/work-centers.d.ts +3 -0
- package/dist/routes/work-centers.js +280 -0
- package/dist/schema-registry.d.ts +3 -0
- package/dist/schema-registry.js +45 -0
- package/dist/services/attachment-service.d.ts +33 -0
- package/dist/services/attachment-service.js +118 -0
- package/dist/services/field-ref-service.d.ts +96 -0
- package/dist/services/field-ref-service.js +74 -0
- package/dist/services/field-service.d.ts +49 -0
- package/dist/services/field-service.js +114 -0
- package/dist/services/field-value-service.d.ts +61 -0
- package/dist/services/field-value-service.js +256 -0
- package/dist/services/item-instance-service.d.ts +152 -0
- package/dist/services/item-instance-service.js +155 -0
- package/dist/services/item-service.d.ts +47 -0
- package/dist/services/item-service.js +56 -0
- package/dist/services/labor-ticket-service.d.ts +40 -0
- package/dist/services/labor-ticket-service.js +148 -0
- package/dist/services/log-file-service.d.ts +4 -0
- package/dist/services/log-file-service.js +11 -0
- package/dist/services/operation-dependency-service.d.ts +33 -0
- package/dist/services/operation-dependency-service.js +30 -0
- package/dist/services/operation-run-comment-service.d.ts +17 -0
- package/dist/services/operation-run-comment-service.js +26 -0
- package/dist/services/operation-run-service.d.ts +126 -0
- package/dist/services/operation-run-service.js +347 -0
- package/dist/services/operation-service.d.ts +47 -0
- package/dist/services/operation-service.js +132 -0
- package/dist/services/order-revision-service.d.ts +53 -0
- package/dist/services/order-revision-service.js +264 -0
- package/dist/services/order-run-service.d.ts +138 -0
- package/dist/services/order-run-service.js +356 -0
- package/dist/services/order-service.d.ts +15 -0
- package/dist/services/order-service.js +68 -0
- package/dist/services/revision-diff-service.d.ts +3 -0
- package/dist/services/revision-diff-service.js +194 -0
- package/dist/services/step-run-service.d.ts +172 -0
- package/dist/services/step-run-service.js +106 -0
- package/dist/services/step-service.d.ts +104 -0
- package/dist/services/step-service.js +89 -0
- package/dist/services/user-service.d.ts +185 -0
- package/dist/services/user-service.js +132 -0
- package/dist/services/work-center-service.d.ts +29 -0
- package/dist/services/work-center-service.js +106 -0
- package/dist/supervisorAuth.d.ts +3 -0
- package/dist/supervisorAuth.js +16 -0
- package/dist/userService.d.ts +20 -0
- package/dist/userService.js +118 -0
- 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,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
|