@internetderdinge/api 1.224.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/copilot-instructions.md +77 -0
- package/CHANGELOG.md +11 -0
- package/README.md +52 -0
- package/package.json +112 -0
- package/src/accounts/accounts.controller.ts +166 -0
- package/src/accounts/accounts.route.ts +107 -0
- package/src/accounts/accounts.schemas.ts +16 -0
- package/src/accounts/accounts.service.ts +85 -0
- package/src/accounts/accounts.validation.ts +118 -0
- package/src/accounts/auth0.service.ts +226 -0
- package/src/config/config.ts +49 -0
- package/src/config/logger.ts +33 -0
- package/src/config/morgan.ts +22 -0
- package/src/config/passport.cjs +30 -0
- package/src/config/roles.ts +13 -0
- package/src/config/tokens.cjs +10 -0
- package/src/devices/devices.controller.ts +276 -0
- package/src/devices/devices.model.ts +126 -0
- package/src/devices/devices.route.ts +198 -0
- package/src/devices/devices.schemas.ts +94 -0
- package/src/devices/devices.service.ts +320 -0
- package/src/devices/devices.validation.ts +221 -0
- package/src/devicesNotifications/devicesNotifications.controller.ts +72 -0
- package/src/devicesNotifications/devicesNotifications.model.ts +67 -0
- package/src/devicesNotifications/devicesNotifications.route.ts +150 -0
- package/src/devicesNotifications/devicesNotifications.schemas.ts +11 -0
- package/src/devicesNotifications/devicesNotifications.service.ts +222 -0
- package/src/devicesNotifications/devicesNotifications.validation.ts +56 -0
- package/src/email/email.service.ts +609 -0
- package/src/files/upload.service.ts +145 -0
- package/src/i18n/i18n.ts +51 -0
- package/src/i18n/saveMissingLocalJsonBackend.ts +92 -0
- package/src/index.ts +7 -0
- package/src/iotdevice/iotdevice.controller.ts +136 -0
- package/src/iotdevice/iotdevice.model.ts +32 -0
- package/src/iotdevice/iotdevice.route.ts +181 -0
- package/src/iotdevice/iotdevice.schemas.ts +79 -0
- package/src/iotdevice/iotdevice.service.ts +732 -0
- package/src/iotdevice/iotdevice.validation.ts +61 -0
- package/src/middlewares/auth.ts +110 -0
- package/src/middlewares/checkJwt.cjs +19 -0
- package/src/middlewares/error.js.legacy +44 -0
- package/src/middlewares/error.ts +41 -0
- package/src/middlewares/mongooseValidations/ensureSameOrganization.ts +15 -0
- package/src/middlewares/rateLimiter.ts +10 -0
- package/src/middlewares/validate.ts +25 -0
- package/src/middlewares/validateAction.ts +41 -0
- package/src/middlewares/validateAdmin.ts +21 -0
- package/src/middlewares/validateAi.ts +24 -0
- package/src/middlewares/validateCurrentAuthUser.ts +23 -0
- package/src/middlewares/validateCurrentUser.ts +35 -0
- package/src/middlewares/validateDevice.ts +191 -0
- package/src/middlewares/validateDeviceUserOrganization.ts +54 -0
- package/src/middlewares/validateOrganization.ts +109 -0
- package/src/middlewares/validateQuerySearchUserAndOrganization.ts +75 -0
- package/src/middlewares/validateTokens.ts +36 -0
- package/src/middlewares/validateUser.ts +75 -0
- package/src/middlewares/validateZod.ts +54 -0
- package/src/models/plugins/index.ts +7 -0
- package/src/models/plugins/paginate.plugin.ts +145 -0
- package/src/models/plugins/paginateNew.plugin.ts +206 -0
- package/src/models/plugins/simplePopulate.ts +12 -0
- package/src/models/plugins/toJSON.plugin.ts +51 -0
- package/src/organizations/organizations.controller.ts +101 -0
- package/src/organizations/organizations.model.ts +62 -0
- package/src/organizations/organizations.route.ts +119 -0
- package/src/organizations/organizations.schemas.ts +8 -0
- package/src/organizations/organizations.service.ts +85 -0
- package/src/organizations/organizations.validation.ts +76 -0
- package/src/pdf/pdf.controller.ts +18 -0
- package/src/pdf/pdf.route.ts +28 -0
- package/src/pdf/pdf.schemas.ts +7 -0
- package/src/pdf/pdf.service.ts +89 -0
- package/src/pdf/pdf.validation.ts +30 -0
- package/src/tokens/tokens.controller.ts +81 -0
- package/src/tokens/tokens.model.ts +24 -0
- package/src/tokens/tokens.route.ts +66 -0
- package/src/tokens/tokens.schemas.ts +15 -0
- package/src/tokens/tokens.service.ts +46 -0
- package/src/tokens/tokens.validation.ts +13 -0
- package/src/types/routeSpec.ts +1 -0
- package/src/users/users.controller.ts +234 -0
- package/src/users/users.model.ts +89 -0
- package/src/users/users.route.ts +171 -0
- package/src/users/users.schemas.ts +79 -0
- package/src/users/users.service.ts +393 -0
- package/src/users/users.validation.ts +166 -0
- package/src/utils/ApiError.ts +18 -0
- package/src/utils/buildRouterAndDocs.ts +85 -0
- package/src/utils/catchAsync.ts +9 -0
- package/src/utils/comparePapers.service.ts +48 -0
- package/src/utils/filterOptions.ts +37 -0
- package/src/utils/medicationName.ts +12 -0
- package/src/utils/pick.ts +16 -0
- package/src/utils/registerOpenApi.ts +32 -0
- package/src/utils/urlUtils.ts +14 -0
- package/src/utils/userName.ts +27 -0
- package/src/utils/zValidations.ts +89 -0
- package/src/validations/auth.validation.cjs +60 -0
- package/src/validations/custom.validation.ts +26 -0
- package/src/validations/index.cjs +2 -0
- package/tsconfig.json +22 -0
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import httpStatus from "http-status";
|
|
2
|
+
import mongoose from "mongoose";
|
|
3
|
+
import multer from "multer";
|
|
4
|
+
import type { Request, Response } from "express";
|
|
5
|
+
import pick from "../utils/pick.js";
|
|
6
|
+
import ApiError from "../utils/ApiError.js";
|
|
7
|
+
import catchAsync from "../utils/catchAsync.js";
|
|
8
|
+
import * as devicesService from "./devices.service.js";
|
|
9
|
+
import * as iotDevicesService from "../iotdevice/iotdevice.service.js";
|
|
10
|
+
import { filterOptions } from "../utils/filterOptions.js";
|
|
11
|
+
import { isAdmin } from "../middlewares/validateAdmin.js";
|
|
12
|
+
|
|
13
|
+
const createEntry = catchAsync(
|
|
14
|
+
async (req: Request, res: Response): Promise<void> => {
|
|
15
|
+
const user = await devicesService.createDevice(req.body);
|
|
16
|
+
res.status(httpStatus.CREATED).send(user);
|
|
17
|
+
},
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
const registerDevice = catchAsync(
|
|
21
|
+
async (req: Request, res: Response): Promise<void> => {
|
|
22
|
+
const checkDevice = await devicesService.getDeviceByDeviceId(
|
|
23
|
+
req.params.deviceId,
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
if (checkDevice) {
|
|
27
|
+
const iotDevices = await iotDevicesService.activateDevice(
|
|
28
|
+
req.params.deviceId,
|
|
29
|
+
req.body.organization,
|
|
30
|
+
true,
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
// Get Events if update is happening
|
|
34
|
+
if (iotDevices.activation_status === "success") {
|
|
35
|
+
if (checkDevice.organization.equals(req.body.organization)) {
|
|
36
|
+
res.status(httpStatus.CONFLICT).send({
|
|
37
|
+
message:
|
|
38
|
+
"Device is already existing and registered in this organization, no hardware reset detected",
|
|
39
|
+
device: checkDevice,
|
|
40
|
+
});
|
|
41
|
+
} else {
|
|
42
|
+
throw new ApiError(
|
|
43
|
+
httpStatus.CONFLICT,
|
|
44
|
+
"Device is already existing and registered in another organization, no hardware reset detected",
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
} else {
|
|
48
|
+
// Delete device if already existing but not activation_status
|
|
49
|
+
await devicesService.deleteById(checkDevice.id, false);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
res.status(httpStatus.CREATED).send(iotDevices);
|
|
53
|
+
} else {
|
|
54
|
+
const devices = await devicesService.registerDevice({
|
|
55
|
+
id: req.params.deviceId,
|
|
56
|
+
body: req.body,
|
|
57
|
+
});
|
|
58
|
+
res.status(httpStatus.CREATED).send(devices);
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
const getEntries = catchAsync(
|
|
64
|
+
async (req: Request, res: Response): Promise<void> => {
|
|
65
|
+
const filter = pick(req.query, ["name", "role"]);
|
|
66
|
+
const options = pick(req.query, ["sortBy", "limit", "page"]);
|
|
67
|
+
const result = await devicesService.queryDevices(filter, options);
|
|
68
|
+
res.send(result);
|
|
69
|
+
},
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const queryDevicesByUser = catchAsync(
|
|
73
|
+
async (req: Request, res: Response): Promise<void> => {
|
|
74
|
+
const filter = pick(req.query, ["name", "role", "organization", "patient"]);
|
|
75
|
+
const options = pick(req.query, ["sortBy", "limit", "page"]);
|
|
76
|
+
|
|
77
|
+
options.limit = 1000;
|
|
78
|
+
|
|
79
|
+
const filteredOptions = filterOptions(req.query, filter, {
|
|
80
|
+
objectIds: ["_id", "patient"],
|
|
81
|
+
search: [
|
|
82
|
+
"meta.name",
|
|
83
|
+
"deviceId",
|
|
84
|
+
"payment.id",
|
|
85
|
+
"payment.customer",
|
|
86
|
+
"kind",
|
|
87
|
+
"patientData.meta.firstName",
|
|
88
|
+
"patientData.meta.lastName",
|
|
89
|
+
],
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const optionsPopulate = {
|
|
93
|
+
...options,
|
|
94
|
+
populate: "patientData",
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const result = await devicesService.queryDevicesByUser(
|
|
98
|
+
filteredOptions,
|
|
99
|
+
optionsPopulate,
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
if (
|
|
103
|
+
req.currentUser?.role &&
|
|
104
|
+
req.currentUser.role === "onlyself" &&
|
|
105
|
+
result?.results
|
|
106
|
+
) {
|
|
107
|
+
const currentUserId = new mongoose.Types.ObjectId(req.currentUser.id);
|
|
108
|
+
result.results = result.results.filter((user) => {
|
|
109
|
+
// Prefer value comparison over object reference comparison
|
|
110
|
+
return currentUserId.equals(user.patient);
|
|
111
|
+
// Alternatively:
|
|
112
|
+
// return String(user.patient) === String(req.currentUser.id);
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
res.send(result);
|
|
117
|
+
},
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
const getEntry = catchAsync(
|
|
121
|
+
async (req: Request, res: Response): Promise<void> => {
|
|
122
|
+
const device = await devicesService.getByIdWithIoT(req.params.deviceId);
|
|
123
|
+
|
|
124
|
+
if (!device) {
|
|
125
|
+
throw new ApiError(httpStatus.NOT_FOUND, "Device not found");
|
|
126
|
+
}
|
|
127
|
+
res.send(device);
|
|
128
|
+
},
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
const getImageById = catchAsync(
|
|
132
|
+
async (req: Request, res: Response): Promise<void> => {
|
|
133
|
+
const device = await devicesService.getImageById(
|
|
134
|
+
req.params.deviceId,
|
|
135
|
+
req.params.uuid,
|
|
136
|
+
);
|
|
137
|
+
if (!device) {
|
|
138
|
+
throw new ApiError(httpStatus.NOT_FOUND, "Device not found");
|
|
139
|
+
}
|
|
140
|
+
res.send(device);
|
|
141
|
+
},
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
const updateEntry = catchAsync(
|
|
145
|
+
async (req: Request, res: Response): Promise<void> => {
|
|
146
|
+
// TODO: Remove in newer versions
|
|
147
|
+
const { updatedAt, createdAt, payment, iotDevice, ...newBody } = req.body;
|
|
148
|
+
|
|
149
|
+
const body = {
|
|
150
|
+
...newBody,
|
|
151
|
+
shadow:
|
|
152
|
+
typeof req.body?.shadow === "object" ? req.body.shadow : undefined,
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
if (isAdmin(res.req.auth) && payment) {
|
|
156
|
+
body.payment = payment;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const device = await devicesService.updateById(req.params.deviceId, body); //TODO: remove shadow
|
|
160
|
+
|
|
161
|
+
if (isAdmin(res.req.auth) && iotDevice && device.deviceId) {
|
|
162
|
+
const iotDeviceUpdate = await iotDevicesService.updateDevice(
|
|
163
|
+
device.deviceId,
|
|
164
|
+
iotDevice,
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
res.send(device);
|
|
169
|
+
},
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
const getEvents = catchAsync(
|
|
173
|
+
async (req: Request, res: Response): Promise<void> => {
|
|
174
|
+
const device = await devicesService.getById(req.params.deviceId);
|
|
175
|
+
const events = await iotDevicesService.getEvents({
|
|
176
|
+
...req.query,
|
|
177
|
+
createdAt: device.createdAt,
|
|
178
|
+
DeviceId: device.deviceId,
|
|
179
|
+
});
|
|
180
|
+
res.send({ ...events, device });
|
|
181
|
+
},
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
const deleteEntry = catchAsync(
|
|
185
|
+
async (req: Request, res: Response): Promise<void> => {
|
|
186
|
+
const entry = await devicesService.deleteById(req.params.deviceId);
|
|
187
|
+
//res.status(httpStatus.NO_CONTENT).send();
|
|
188
|
+
res.send(entry);
|
|
189
|
+
},
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
const pingDevice = catchAsync(
|
|
193
|
+
async (req: Request, res: Response): Promise<void> => {
|
|
194
|
+
const device = await devicesService.getByIdWithIoT(req.params.deviceId);
|
|
195
|
+
const ping = await iotDevicesService.pingDevice(device.deviceId, req.query);
|
|
196
|
+
res.send({ device, ping });
|
|
197
|
+
},
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
export const resetDevice = catchAsync(
|
|
201
|
+
async (req: Request, res: Response): Promise<void> => {
|
|
202
|
+
const device = await devicesService.getByIdWithIoT(req.params.deviceId);
|
|
203
|
+
const reset = await iotDevicesService.resetDevice(
|
|
204
|
+
device.deviceId,
|
|
205
|
+
req.body,
|
|
206
|
+
);
|
|
207
|
+
res.send({ device, reset });
|
|
208
|
+
},
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
const ledLight = catchAsync(
|
|
212
|
+
async (req: Request, res: Response): Promise<void> => {
|
|
213
|
+
const device = await devicesService.getByIdWithIoT(req.params.deviceId);
|
|
214
|
+
const ping = await iotDevicesService.ledLightHint(
|
|
215
|
+
device.deviceId,
|
|
216
|
+
req.body,
|
|
217
|
+
);
|
|
218
|
+
res.send({ device, ping });
|
|
219
|
+
},
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
const rebootDevice = catchAsync(
|
|
223
|
+
async (req: Request, res: Response): Promise<void> => {
|
|
224
|
+
const device = await devicesService.getByIdWithIoT(req.params.deviceId);
|
|
225
|
+
const reboot = await iotDevicesService.rebootDevice(device.deviceId);
|
|
226
|
+
res.send({ device, reboot });
|
|
227
|
+
},
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
const updateSingleImageMeta = catchAsync(
|
|
231
|
+
async (req: Request, res: Response): Promise<void> => {
|
|
232
|
+
const device = await devicesService.getById(req.params.deviceId);
|
|
233
|
+
|
|
234
|
+
const { uuid, ...body } = req.body;
|
|
235
|
+
const deviceUpdate = await devicesService.updateById(req.params.deviceId, {
|
|
236
|
+
meta: { ...device.meta, file: uuid },
|
|
237
|
+
});
|
|
238
|
+
const deviceMeta = await devicesService.updateSingleImageMeta(
|
|
239
|
+
device.deviceId,
|
|
240
|
+
body,
|
|
241
|
+
);
|
|
242
|
+
res.send({ deviceMeta, deviceUpdate });
|
|
243
|
+
},
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
const uploadSingleImage = catchAsync(
|
|
247
|
+
async (req: Request, res: Response): Promise<void> => {
|
|
248
|
+
const device = await devicesService.getById(req.params.deviceId);
|
|
249
|
+
|
|
250
|
+
const iotUpload = await iotDevicesService.uploadSingleImage({
|
|
251
|
+
deviceName: device.deviceId,
|
|
252
|
+
buffer: req.files[0].buffer,
|
|
253
|
+
deviceId: req.params.deviceId,
|
|
254
|
+
uuid: req.body.uuid,
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
res.send(iotUpload);
|
|
258
|
+
},
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
export {
|
|
262
|
+
createEntry,
|
|
263
|
+
getEntries,
|
|
264
|
+
queryDevicesByUser,
|
|
265
|
+
getEvents,
|
|
266
|
+
getImageById,
|
|
267
|
+
registerDevice,
|
|
268
|
+
pingDevice,
|
|
269
|
+
ledLight,
|
|
270
|
+
rebootDevice,
|
|
271
|
+
uploadSingleImage,
|
|
272
|
+
updateSingleImageMeta,
|
|
273
|
+
getEntry,
|
|
274
|
+
updateEntry,
|
|
275
|
+
deleteEntry,
|
|
276
|
+
};
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import mongoose, { Schema, Document, Model, Types } from "mongoose";
|
|
2
|
+
import { toJSON, paginate } from "../models/plugins/index.js";
|
|
3
|
+
import { ensureSameOrganization } from "../middlewares/mongooseValidations/ensureSameOrganization.js";
|
|
4
|
+
|
|
5
|
+
interface IDevice extends Document {
|
|
6
|
+
name?: string;
|
|
7
|
+
meta?: Record<string, any>;
|
|
8
|
+
organization?: mongoose.Types.ObjectId;
|
|
9
|
+
patient?: mongoose.Types.ObjectId;
|
|
10
|
+
paper?: mongoose.Types.ObjectId;
|
|
11
|
+
timezone?: string;
|
|
12
|
+
deviceId?: string;
|
|
13
|
+
kind?: string;
|
|
14
|
+
eventDate?: Date;
|
|
15
|
+
payment?: Record<string, any>;
|
|
16
|
+
loadedAt?: Date;
|
|
17
|
+
patientData?: any; // Replace `any` with the actual type if available
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const deviceSchema = new Schema(
|
|
21
|
+
{
|
|
22
|
+
name: {
|
|
23
|
+
type: String,
|
|
24
|
+
},
|
|
25
|
+
meta: { type: Object },
|
|
26
|
+
organization: {
|
|
27
|
+
type: Schema.Types.ObjectId,
|
|
28
|
+
ref: "Organization",
|
|
29
|
+
immutable: true,
|
|
30
|
+
},
|
|
31
|
+
patient: { type: Schema.Types.ObjectId, ref: "User" },
|
|
32
|
+
paper: { type: mongoose.Schema.Types.ObjectId, ref: "Paper" },
|
|
33
|
+
timezone: { type: String },
|
|
34
|
+
deviceId: { type: String, immutable: true },
|
|
35
|
+
kind: { type: String, immutable: true },
|
|
36
|
+
eventDate: { type: Date },
|
|
37
|
+
payment: { type: Object },
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
timestamps: true,
|
|
41
|
+
toJSON: { virtuals: true },
|
|
42
|
+
toObject: { virtuals: true },
|
|
43
|
+
},
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
deviceSchema.virtual("patientData", {
|
|
47
|
+
ref: "User",
|
|
48
|
+
localField: "patient",
|
|
49
|
+
foreignField: "_id",
|
|
50
|
+
justOne: true,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
function addIotDevice(schema: Schema<IDevice>): void {
|
|
54
|
+
schema
|
|
55
|
+
.virtual("loadedAt")
|
|
56
|
+
.get(function (this: IDevice) {
|
|
57
|
+
return this._loadedAt;
|
|
58
|
+
})
|
|
59
|
+
.set(function (this: IDevice, v: Date) {
|
|
60
|
+
this._loadedAt = v;
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
schema.post(["find", "findOne"], function (docs: IDevice | IDevice[] | null) {
|
|
64
|
+
// nothing was found, bail out
|
|
65
|
+
if (!docs) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// normalize to array
|
|
70
|
+
const docsArray = Array.isArray(docs) ? docs : [docs];
|
|
71
|
+
const now = new Date();
|
|
72
|
+
|
|
73
|
+
for (const doc of docsArray) {
|
|
74
|
+
if (doc) {
|
|
75
|
+
doc.loadedAt = now;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Ensure patient is member of the same organization
|
|
83
|
+
*/
|
|
84
|
+
deviceSchema.pre<IDevice>("save", async function (next) {
|
|
85
|
+
if (!this.patient) return next();
|
|
86
|
+
try {
|
|
87
|
+
await ensureSameOrganization(
|
|
88
|
+
this.patient,
|
|
89
|
+
this.organization!,
|
|
90
|
+
mongoose.model("User"),
|
|
91
|
+
);
|
|
92
|
+
next();
|
|
93
|
+
} catch (err) {
|
|
94
|
+
next(err as Error);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
deviceSchema.pre("findOneAndUpdate", async function (next) {
|
|
99
|
+
const update = this.getUpdate() as any;
|
|
100
|
+
if (!update?.patient) return next();
|
|
101
|
+
try {
|
|
102
|
+
// need org from the existing doc
|
|
103
|
+
const device = await this.model
|
|
104
|
+
.findOne(this.getQuery())
|
|
105
|
+
.select("organization");
|
|
106
|
+
await ensureSameOrganization(
|
|
107
|
+
update.patient,
|
|
108
|
+
device!.organization,
|
|
109
|
+
mongoose.model("User"),
|
|
110
|
+
);
|
|
111
|
+
next();
|
|
112
|
+
} catch (err) {
|
|
113
|
+
next(err as Error);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// Add the IoT device functionality
|
|
118
|
+
addIotDevice(deviceSchema);
|
|
119
|
+
|
|
120
|
+
// Add plugins that convert mongoose to JSON and enable pagination
|
|
121
|
+
deviceSchema.plugin((schema: Schema) => toJSON(schema, true));
|
|
122
|
+
deviceSchema.plugin(paginate);
|
|
123
|
+
|
|
124
|
+
const Devices: Model<IDevice> = mongoose.model<IDevice>("Device", deviceSchema);
|
|
125
|
+
|
|
126
|
+
export default Devices;
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { Router } from "express";
|
|
2
|
+
import multer from "multer";
|
|
3
|
+
import buildRouterAndDocs from "../utils/buildRouterAndDocs.js";
|
|
4
|
+
import type { RouteSpec } from "../types/routeSpec";
|
|
5
|
+
import auth from "../middlewares/auth.js";
|
|
6
|
+
import {
|
|
7
|
+
validateBodyOrganization,
|
|
8
|
+
validateQueryOrganization,
|
|
9
|
+
} from "../middlewares/validateOrganization.js";
|
|
10
|
+
import { validateQuerySearchUserAndOrganization } from "../middlewares/validateQuerySearchUserAndOrganization.js";
|
|
11
|
+
import { validateDevice } from "../middlewares/validateDevice.js";
|
|
12
|
+
import { validateDeviceUserOrganization } from "../middlewares/validateDeviceUserOrganization.js";
|
|
13
|
+
import * as devicesController from "./devices.controller.js";
|
|
14
|
+
import { resetDevice } from "./devices.controller.js";
|
|
15
|
+
import {
|
|
16
|
+
createDeviceSchema,
|
|
17
|
+
queryDevicesSchema,
|
|
18
|
+
getDeviceSchema,
|
|
19
|
+
updateDeviceSchema,
|
|
20
|
+
deleteDeviceSchema,
|
|
21
|
+
getEventsSchema,
|
|
22
|
+
pingDeviceSchema,
|
|
23
|
+
registerDeviceSchema,
|
|
24
|
+
ledLightSchema,
|
|
25
|
+
rebootDeviceSchema,
|
|
26
|
+
getImageSchema,
|
|
27
|
+
updateSingleImageMetaSchema,
|
|
28
|
+
uploadSingleImageSchema,
|
|
29
|
+
resetDeviceSchema,
|
|
30
|
+
} from "./devices.validation.js";
|
|
31
|
+
import {
|
|
32
|
+
deviceResponseSchema,
|
|
33
|
+
devicesResponseSchema,
|
|
34
|
+
eventResponseSchema,
|
|
35
|
+
genericResponseSchema,
|
|
36
|
+
imageResponseSchema,
|
|
37
|
+
uploadResponseSchema,
|
|
38
|
+
resetResponseSchema,
|
|
39
|
+
} from "./devices.schemas.js";
|
|
40
|
+
import {
|
|
41
|
+
validateOrganizationDelete,
|
|
42
|
+
validateOrganizationUpdate,
|
|
43
|
+
} from "../middlewares/validateAction.js";
|
|
44
|
+
|
|
45
|
+
export const devicesRouteSpecs: RouteSpec[] = [
|
|
46
|
+
{
|
|
47
|
+
method: "post",
|
|
48
|
+
path: "/",
|
|
49
|
+
validate: [
|
|
50
|
+
auth("manageUsers"),
|
|
51
|
+
validateBodyOrganization,
|
|
52
|
+
validateOrganizationDelete,
|
|
53
|
+
],
|
|
54
|
+
requestSchema: createDeviceSchema,
|
|
55
|
+
responseSchema: deviceResponseSchema,
|
|
56
|
+
handler: devicesController.createEntry,
|
|
57
|
+
summary: "Create a new device",
|
|
58
|
+
description:
|
|
59
|
+
"Create a new device and associate it with the current organization.",
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
method: "get",
|
|
63
|
+
path: "/",
|
|
64
|
+
validate: [auth("getUsers"), validateQuerySearchUserAndOrganization],
|
|
65
|
+
requestSchema: queryDevicesSchema,
|
|
66
|
+
responseSchema: devicesResponseSchema,
|
|
67
|
+
handler: devicesController.queryDevicesByUser,
|
|
68
|
+
summary: "Query devices by user",
|
|
69
|
+
description:
|
|
70
|
+
"Retrieve a paginated list of devices visible to the authenticated user.",
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
method: "get",
|
|
74
|
+
path: "/events/:deviceId",
|
|
75
|
+
validate: [auth("getUsers"), validateDevice],
|
|
76
|
+
requestSchema: getEventsSchema,
|
|
77
|
+
responseSchema: eventResponseSchema.array(),
|
|
78
|
+
handler: devicesController.getEvents,
|
|
79
|
+
summary: "Get events for a device",
|
|
80
|
+
description:
|
|
81
|
+
"Fetch a chronological list of events generated by the specified device.",
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
method: "get",
|
|
85
|
+
path: "/:deviceId",
|
|
86
|
+
validate: [auth("getUsers"), validateDevice],
|
|
87
|
+
requestSchema: getDeviceSchema,
|
|
88
|
+
responseSchema: deviceResponseSchema,
|
|
89
|
+
handler: devicesController.getEntry,
|
|
90
|
+
summary: "Get a device by ID",
|
|
91
|
+
description:
|
|
92
|
+
"Retrieve detailed information for the device identified by its ID.",
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
method: "post",
|
|
96
|
+
path: "/:deviceId",
|
|
97
|
+
handler: devicesController.deleteEntry,
|
|
98
|
+
summary: "Delete a device",
|
|
99
|
+
description: "Remove the specified device from the system.",
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
method: "post",
|
|
103
|
+
path: "/registerdevice/:deviceId",
|
|
104
|
+
validate: [
|
|
105
|
+
auth("getUsers"),
|
|
106
|
+
validateBodyOrganization,
|
|
107
|
+
validateOrganizationDelete,
|
|
108
|
+
],
|
|
109
|
+
requestSchema: registerDeviceSchema,
|
|
110
|
+
responseSchema: genericResponseSchema,
|
|
111
|
+
handler: devicesController.registerDevice,
|
|
112
|
+
summary: "Register a device",
|
|
113
|
+
description:
|
|
114
|
+
"Associate an existing device with the authenticated organization.",
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
method: "post",
|
|
118
|
+
path: "/ledlight/:deviceId",
|
|
119
|
+
validate: [auth("getUsers"), validateDevice],
|
|
120
|
+
requestSchema: ledLightSchema,
|
|
121
|
+
responseSchema: genericResponseSchema,
|
|
122
|
+
handler: devicesController.ledLight,
|
|
123
|
+
summary: "Set LED light on device",
|
|
124
|
+
description:
|
|
125
|
+
"Turn the device’s LED on or off, or set its color/brightness.",
|
|
126
|
+
memoOnly: true,
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
method: "get",
|
|
130
|
+
path: "/ping/:deviceId",
|
|
131
|
+
validate: [auth("getUsers"), validateDevice],
|
|
132
|
+
requestSchema: pingDeviceSchema,
|
|
133
|
+
responseSchema: genericResponseSchema,
|
|
134
|
+
handler: devicesController.pingDevice,
|
|
135
|
+
summary: "Ping a device",
|
|
136
|
+
description:
|
|
137
|
+
"Check connectivity and round-trip time to the specified device.",
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
method: "post",
|
|
141
|
+
path: "/reset/:deviceId",
|
|
142
|
+
validate: [auth("getUsers"), validateDevice, validateOrganizationDelete],
|
|
143
|
+
requestSchema: resetDeviceSchema,
|
|
144
|
+
responseSchema: resetResponseSchema,
|
|
145
|
+
handler: resetDevice,
|
|
146
|
+
summary: "Reset the sensors of a device",
|
|
147
|
+
description:
|
|
148
|
+
"Remotely reset all sensors on the target device to factory defaults.",
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
method: "post",
|
|
152
|
+
path: "/reboot/:deviceId",
|
|
153
|
+
validate: [auth("getUsers"), validateDevice, validateOrganizationDelete],
|
|
154
|
+
requestSchema: rebootDeviceSchema,
|
|
155
|
+
responseSchema: genericResponseSchema,
|
|
156
|
+
handler: devicesController.rebootDevice,
|
|
157
|
+
summary: "Reboot a device",
|
|
158
|
+
description: "Initiate a remote reboot of the specified device.",
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
method: "get",
|
|
162
|
+
path: "/image/:deviceId/:uuid",
|
|
163
|
+
validate: [auth("getUsers"), validateDevice],
|
|
164
|
+
requestSchema: getImageSchema,
|
|
165
|
+
responseSchema: imageResponseSchema,
|
|
166
|
+
handler: devicesController.getImageById,
|
|
167
|
+
summary: "Fetch an image by UUID",
|
|
168
|
+
description:
|
|
169
|
+
"Download a previously uploaded image for the device by its UUID.",
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
method: "post",
|
|
173
|
+
path: "/updateSingleImageMeta/:deviceId",
|
|
174
|
+
validate: [auth("getUsers"), validateDevice],
|
|
175
|
+
requestSchema: updateSingleImageMetaSchema,
|
|
176
|
+
responseSchema: uploadResponseSchema,
|
|
177
|
+
handler: devicesController.updateSingleImageMeta,
|
|
178
|
+
summary: "Update image metadata",
|
|
179
|
+
description:
|
|
180
|
+
"Modify metadata (e.g., title, tags) for an existing device image.",
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
method: "post",
|
|
184
|
+
path: "/uploadSingleImage/:deviceId",
|
|
185
|
+
validate: [auth("getUsers"), multer().array("picture", 2), validateDevice],
|
|
186
|
+
requestSchema: uploadSingleImageSchema,
|
|
187
|
+
responseSchema: uploadResponseSchema,
|
|
188
|
+
handler: devicesController.uploadSingleImage,
|
|
189
|
+
summary: "Upload one or two images",
|
|
190
|
+
description:
|
|
191
|
+
"Upload up to two image files to the device for processing or storage.",
|
|
192
|
+
},
|
|
193
|
+
];
|
|
194
|
+
|
|
195
|
+
const router: Router = Router();
|
|
196
|
+
buildRouterAndDocs(router, devicesRouteSpecs, "/devices", ["Devices"]);
|
|
197
|
+
|
|
198
|
+
export default router;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { zObjectId } from "../utils/zValidations.js";
|
|
3
|
+
|
|
4
|
+
export const deviceResponseSchema = z.object({
|
|
5
|
+
id: z.string(),
|
|
6
|
+
name: z.string(),
|
|
7
|
+
serialNumber: z.string().optional(),
|
|
8
|
+
status: z.enum(["online", "offline", "error"]),
|
|
9
|
+
createdAt: z.string(), // ISO timestamp
|
|
10
|
+
updatedAt: z.string(), // ISO timestamp
|
|
11
|
+
// ...other device fields...
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
export const devicesResponseSchema = deviceResponseSchema.array();
|
|
15
|
+
|
|
16
|
+
export const eventResponseSchema = z.object({
|
|
17
|
+
id: z.string(),
|
|
18
|
+
deviceId: z.string(),
|
|
19
|
+
type: z.string(),
|
|
20
|
+
payload: z.record(z.any()),
|
|
21
|
+
timestamp: z.string(), // ISO timestamp
|
|
22
|
+
// ...other event fields...
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
export const genericResponseSchema = z.object({
|
|
26
|
+
message: z.string(),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
export const imageResponseSchema = z.object({
|
|
30
|
+
uuid: z.string(),
|
|
31
|
+
deviceId: z.string(),
|
|
32
|
+
url: z.string().url(),
|
|
33
|
+
metadata: z
|
|
34
|
+
.object({
|
|
35
|
+
width: z.number().optional(),
|
|
36
|
+
height: z.number().optional(),
|
|
37
|
+
size: z.number().optional(), // bytes
|
|
38
|
+
// ...other metadata fields...
|
|
39
|
+
})
|
|
40
|
+
.optional(),
|
|
41
|
+
uploadedAt: z.string(), // ISO timestamp
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
export const checkoutSessionResponseSchema = z.object({
|
|
45
|
+
sessionId: z.string(),
|
|
46
|
+
url: z.string().url(),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
export const customerPortalSessionResponseSchema = z.object({
|
|
50
|
+
url: z.string().url(),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
export const subscriptionResponseSchema = z.object({
|
|
54
|
+
status: z.enum(["active", "past_due", "canceled", "unpaid"]),
|
|
55
|
+
currentPeriodStart: z.string(), // ISO timestamp
|
|
56
|
+
currentPeriodEnd: z.string(), // ISO timestamp
|
|
57
|
+
plan: z.object({
|
|
58
|
+
id: z.string(),
|
|
59
|
+
amount: z.number(),
|
|
60
|
+
currency: z.string(),
|
|
61
|
+
interval: z.enum(["day", "week", "month", "year"]),
|
|
62
|
+
// ...other plan fields...
|
|
63
|
+
}),
|
|
64
|
+
// ...other subscription fields...
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
export const uploadResponseSchema = z.object({
|
|
68
|
+
filenames: z.array(z.string()),
|
|
69
|
+
urls: z.array(z.string().url()),
|
|
70
|
+
// ...other upload result fields...
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
export const pingDeviceSchema = {
|
|
74
|
+
params: z.object({
|
|
75
|
+
deviceId: zObjectId.openapi({ description: "Device ObjectId" }),
|
|
76
|
+
}),
|
|
77
|
+
query: z.object({
|
|
78
|
+
dataResponse: z
|
|
79
|
+
.string()
|
|
80
|
+
.openapi({ description: "Data response", example: "false" }),
|
|
81
|
+
}),
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export const resetResponseSchema = z.object({
|
|
85
|
+
message: z.string().openapi({
|
|
86
|
+
example:
|
|
87
|
+
"Reset all Variables and Memory and reboot Device: nrf-352656106701140",
|
|
88
|
+
description: "Response message from reset operation",
|
|
89
|
+
}),
|
|
90
|
+
success: z.boolean().openapi({
|
|
91
|
+
example: true,
|
|
92
|
+
description: "Indicates if the reset operation was successful",
|
|
93
|
+
}),
|
|
94
|
+
});
|