@internetderdinge/api 1.229.0 → 1.229.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/dist/src/accounts/accounts.controller.js +89 -0
- package/dist/src/accounts/accounts.route.js +101 -0
- package/dist/src/accounts/accounts.schemas.js +12 -0
- package/dist/src/accounts/accounts.service.js +65 -0
- package/dist/src/accounts/accounts.validation.js +99 -0
- package/dist/src/accounts/auth0.service.js +188 -0
- package/dist/src/config/config.js +48 -0
- package/dist/src/config/logger.js +27 -0
- package/dist/src/config/morgan.js +16 -0
- package/dist/src/config/passport.cjs +28 -0
- package/dist/src/config/roles.js +11 -0
- package/dist/src/config/tokens.cjs +10 -0
- package/dist/src/devices/devices.controller.js +172 -0
- package/dist/src/devices/devices.model.js +94 -0
- package/dist/src/devices/devices.route.js +153 -0
- package/dist/src/devices/devices.schemas.js +84 -0
- package/dist/src/devices/devices.service.js +198 -0
- package/dist/src/devices/devices.types.js +1 -0
- package/dist/src/devices/devices.validation.js +257 -0
- package/dist/src/devicesNotifications/devicesNotifications.controller.js +69 -0
- package/dist/src/devicesNotifications/devicesNotifications.model.js +39 -0
- package/dist/src/devicesNotifications/devicesNotifications.route.js +124 -0
- package/dist/src/devicesNotifications/devicesNotifications.schemas.js +10 -0
- package/dist/src/devicesNotifications/devicesNotifications.service.js +181 -0
- package/dist/src/devicesNotifications/devicesNotifications.validation.js +46 -0
- package/dist/src/email/email.service.js +580 -0
- package/dist/src/files/upload.service.js +124 -0
- package/dist/src/i18n/i18n.js +38 -0
- package/dist/src/i18n/saveMissingLocalJsonBackend.js +53 -0
- package/dist/src/i18n/types.js +1 -0
- package/dist/src/index.js +48 -0
- package/dist/src/iotdevice/iotdevice.controller.js +96 -0
- package/dist/src/iotdevice/iotdevice.model.js +17 -0
- package/dist/src/iotdevice/iotdevice.route.js +143 -0
- package/dist/src/iotdevice/iotdevice.schemas.js +60 -0
- package/dist/src/iotdevice/iotdevice.service.js +579 -0
- package/dist/src/iotdevice/iotdevice.types.js +1 -0
- package/dist/src/iotdevice/iotdevice.validation.js +54 -0
- package/dist/src/middlewares/auth.js +75 -0
- package/dist/src/middlewares/checkJwt.cjs +17 -0
- package/dist/src/middlewares/error.js +36 -0
- package/dist/src/middlewares/mongooseValidations/ensureSameOrganization.js +13 -0
- package/dist/src/middlewares/rateLimiter.js +7 -0
- package/dist/src/middlewares/validate.js +18 -0
- package/dist/src/middlewares/validateAction.js +35 -0
- package/dist/src/middlewares/validateAdmin.js +18 -0
- package/dist/src/middlewares/validateAi.js +16 -0
- package/dist/src/middlewares/validateCurrentAuthUser.js +17 -0
- package/dist/src/middlewares/validateCurrentUser.js +20 -0
- package/dist/src/middlewares/validateDevice.js +98 -0
- package/dist/src/middlewares/validateDeviceUserOrganization.js +26 -0
- package/dist/src/middlewares/validateOrganization.js +63 -0
- package/dist/src/middlewares/validateQuerySearchUserAndOrganization.js +44 -0
- package/dist/src/middlewares/validateTokens.js +23 -0
- package/dist/src/middlewares/validateUser.js +38 -0
- package/dist/src/middlewares/validateZod.js +33 -0
- package/dist/src/models/plugins/index.js +4 -0
- package/dist/src/models/plugins/paginate.plugin.js +117 -0
- package/dist/src/models/plugins/paginateNew.plugin.js +185 -0
- package/dist/src/models/plugins/simplePopulate.js +16 -0
- package/dist/src/models/plugins/toJSON.plugin.js +35 -0
- package/dist/src/organizations/organizations.controller.js +64 -0
- package/dist/src/organizations/organizations.model.js +41 -0
- package/dist/src/organizations/organizations.route.js +98 -0
- package/dist/src/organizations/organizations.schemas.js +7 -0
- package/dist/src/organizations/organizations.service.js +59 -0
- package/dist/src/organizations/organizations.validation.js +62 -0
- package/dist/src/pdf/pdf.controller.js +24 -0
- package/dist/src/pdf/pdf.route.js +22 -0
- package/dist/src/pdf/pdf.schemas.js +6 -0
- package/dist/src/pdf/pdf.service.js +65 -0
- package/dist/src/pdf/pdf.validation.js +27 -0
- package/dist/src/tokens/tokens.controller.js +60 -0
- package/dist/src/tokens/tokens.model.js +17 -0
- package/dist/src/tokens/tokens.route.js +52 -0
- package/dist/src/tokens/tokens.schemas.js +14 -0
- package/dist/src/tokens/tokens.service.js +30 -0
- package/dist/src/tokens/tokens.validation.js +9 -0
- package/dist/src/types/routeSpec.js +1 -0
- package/dist/src/users/users.controller.js +147 -0
- package/dist/src/users/users.model.js +50 -0
- package/dist/src/users/users.route.js +137 -0
- package/dist/src/users/users.schemas.js +69 -0
- package/dist/src/users/users.service.js +295 -0
- package/dist/src/users/users.types.js +1 -0
- package/dist/src/users/users.validation.js +144 -0
- package/dist/src/utils/ApiError.js +16 -0
- package/dist/src/utils/buildRouterAndDocs.js +72 -0
- package/dist/src/utils/catchAsync.js +4 -0
- package/dist/src/utils/comparePapers.service.js +32 -0
- package/dist/src/utils/deviceUtils.js +63 -0
- package/dist/src/utils/filterOptions.js +24 -0
- package/dist/src/utils/medicationName.js +10 -0
- package/dist/src/utils/pick.js +16 -0
- package/dist/src/utils/registerOpenApi.js +67 -0
- package/dist/src/utils/urlUtils.js +15 -0
- package/dist/src/utils/userName.js +22 -0
- package/dist/src/utils/zValidations.js +143 -0
- package/dist/src/validations/auth.validation.cjs +53 -0
- package/dist/src/validations/custom.validation.js +19 -0
- package/dist/src/validations/index.cjs +3 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +97 -80
- package/scripts/release-and-sync-paperless.mjs +137 -0
- package/src/accounts/accounts.controller.ts +1 -0
- package/src/accounts/accounts.service.ts +1 -0
- package/src/accounts/accounts.validation.ts +8 -5
- package/src/accounts/auth0.service.ts +55 -28
- package/src/config/config.ts +6 -0
- package/src/config/logger.ts +15 -9
- package/src/devices/devices.controller.ts +7 -1
- package/src/devices/devices.model.ts +4 -1
- package/src/devices/devices.schemas.ts +11 -9
- package/src/devices/devices.service.ts +1 -0
- package/src/devices/devices.types.ts +1 -0
- package/src/devices/devices.validation.ts +93 -32
- package/src/devicesNotifications/devicesNotifications.controller.ts +57 -28
- package/src/devicesNotifications/devicesNotifications.model.ts +20 -12
- package/src/devicesNotifications/devicesNotifications.service.ts +35 -17
- package/src/files/upload.service.ts +52 -28
- package/src/i18n/i18n.ts +1 -1
- package/src/i18n/types.ts +1 -0
- package/src/index.ts +47 -0
- package/src/iotdevice/iotdevice.controller.ts +1 -0
- package/src/iotdevice/iotdevice.model.ts +6 -3
- package/src/iotdevice/iotdevice.route.ts +85 -76
- package/src/iotdevice/iotdevice.service.ts +4 -3
- package/src/iotdevice/iotdevice.types.ts +6 -0
- package/src/middlewares/auth.ts +2 -8
- package/src/middlewares/error.ts +26 -12
- package/src/middlewares/mongooseValidations/ensureSameOrganization.ts +4 -3
- package/src/middlewares/validateAi.ts +17 -9
- package/src/middlewares/validateDevice.ts +1 -0
- package/src/middlewares/validateDeviceUserOrganization.ts +1 -0
- package/src/middlewares/validateOrganization.ts +1 -1
- package/src/middlewares/validateQuerySearchUserAndOrganization.ts +1 -0
- package/src/middlewares/validateTokens.ts +2 -1
- package/src/middlewares/validateUser.ts +1 -0
- package/src/middlewares/validateZod.ts +5 -5
- package/src/models/plugins/index.ts +5 -4
- package/src/models/plugins/paginate.plugin.ts +26 -16
- package/src/models/plugins/paginateNew.plugin.ts +33 -21
- package/src/models/plugins/simplePopulate.ts +8 -3
- package/src/models/plugins/toJSON.plugin.ts +12 -5
- package/src/organizations/organizations.controller.ts +1 -2
- package/src/organizations/organizations.model.ts +4 -4
- package/src/organizations/organizations.route.ts +1 -1
- package/src/organizations/organizations.service.ts +15 -6
- package/src/organizations/organizations.validation.ts +1 -1
- package/src/pdf/pdf.controller.ts +18 -1
- package/src/pdf/pdf.service.ts +25 -16
- package/src/tokens/tokens.controller.ts +6 -8
- package/src/tokens/tokens.model.ts +3 -1
- package/src/tokens/tokens.service.ts +3 -2
- package/src/types/express.d.ts +17 -0
- package/src/types/mongoose.d.ts +22 -0
- package/src/users/users.controller.ts +8 -9
- package/src/users/users.model.ts +6 -5
- package/src/users/users.route.ts +0 -1
- package/src/users/users.service.ts +16 -0
- package/src/users/users.types.ts +1 -0
- package/src/users/users.validation.ts +6 -2
- package/src/utils/ApiError.ts +8 -1
- package/src/utils/buildRouterAndDocs.ts +57 -22
- package/src/utils/catchAsync.ts +27 -3
- package/src/utils/medicationName.ts +5 -4
- package/src/utils/pick.ts +5 -1
- package/src/utils/registerOpenApi.ts +75 -24
- package/src/utils/userName.ts +1 -0
- package/src/utils/zValidations.ts +98 -27
- package/tsconfig.json +13 -4
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { extendZodWithOpenApi } from '@asteasolutions/zod-to-openapi';
|
|
3
|
+
extendZodWithOpenApi(z);
|
|
4
|
+
export const createUserResponseSchema = z.object({
|
|
5
|
+
id: z.string(),
|
|
6
|
+
name: z.string(),
|
|
7
|
+
email: z.string(),
|
|
8
|
+
});
|
|
9
|
+
export const getUsersResponseSchema = z.array(z.object({
|
|
10
|
+
id: z.string(),
|
|
11
|
+
name: z.string(),
|
|
12
|
+
email: z.string(),
|
|
13
|
+
}));
|
|
14
|
+
export const updateUserSchema = z.object({
|
|
15
|
+
name: z.string().optional(),
|
|
16
|
+
email: z.string().email().optional(),
|
|
17
|
+
});
|
|
18
|
+
export const updateUserResponseSchema = z.object({
|
|
19
|
+
id: z.string(),
|
|
20
|
+
name: z.string(),
|
|
21
|
+
email: z.string(),
|
|
22
|
+
});
|
|
23
|
+
export const deleteUserResponseSchema = z.object({
|
|
24
|
+
success: z.boolean(),
|
|
25
|
+
});
|
|
26
|
+
export const updateTimesByIdResponseSchema = z
|
|
27
|
+
.array(z.object({
|
|
28
|
+
rrule: z
|
|
29
|
+
.object({
|
|
30
|
+
freq: z.string().optional().openapi({ example: 'DAILY', description: 'Recurrence frequency' }),
|
|
31
|
+
byweekday: z
|
|
32
|
+
.array(z.number())
|
|
33
|
+
.openapi({ example: [0, 1, 2, 6, 3], description: 'Days of week to repeat (0=Sunday)' }),
|
|
34
|
+
exclude: z.array(z.string()).openapi({ example: ['2024-03-28T10:45:00.000Z'], description: 'Dates to skip' }),
|
|
35
|
+
})
|
|
36
|
+
.openapi({ description: 'Recurrence rule object' }),
|
|
37
|
+
medication: z.string().optional().openapi({ example: '6152c5f3902e7f91374d9f75', description: 'Medication ObjectId' }),
|
|
38
|
+
patient: z.string().openapi({ example: '614fb1d709dd9f6de85d6374', description: 'Patient ObjectId' }),
|
|
39
|
+
date: z.string().openapi({ example: '2024-03-25T00:30:00.000Z', description: 'Scheduled date/time (ISO)' }),
|
|
40
|
+
timeCategory: z.string().openapi({ example: 'noon', description: 'Time category (e.g. morning, noon)' }),
|
|
41
|
+
amount: z.number().openapi({ example: 1, description: 'Dosage amount' }),
|
|
42
|
+
emptyStomach: z.boolean().openapi({ example: false, description: 'Whether to take on empty stomach' }),
|
|
43
|
+
instruction: z.string().optional().openapi({ example: '', description: 'Additional instructions' }),
|
|
44
|
+
unit: z.string().optional().openapi({ example: 'St', description: 'Dosage unit' }),
|
|
45
|
+
bake: z.boolean().openapi({ example: false, description: 'Baking flag (if applicable)' }),
|
|
46
|
+
id: z.string().openapi({ example: '660079fd11fdc2dd935e43af', description: 'Entry identifier' }),
|
|
47
|
+
}))
|
|
48
|
+
.openapi({
|
|
49
|
+
example: [
|
|
50
|
+
{
|
|
51
|
+
rrule: {
|
|
52
|
+
freq: 'DAILY',
|
|
53
|
+
byweekday: [0, 1, 2, 6, 3],
|
|
54
|
+
exclude: ['2024-03-28T10:45:00.000Z'],
|
|
55
|
+
},
|
|
56
|
+
patient: '614fb1d709dd9f6de85d6374',
|
|
57
|
+
date: '2024-03-25T00:30:00.000Z',
|
|
58
|
+
timeCategory: 'noon',
|
|
59
|
+
amount: 1,
|
|
60
|
+
emptyStomach: false,
|
|
61
|
+
instruction: '',
|
|
62
|
+
unit: 'St',
|
|
63
|
+
bake: false,
|
|
64
|
+
id: '660079fd11fdc2dd935e43af',
|
|
65
|
+
},
|
|
66
|
+
// ...other items
|
|
67
|
+
],
|
|
68
|
+
description: 'Array of updated time entries by ID',
|
|
69
|
+
});
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
import httpStatus from "http-status";
|
|
3
|
+
import { User } from "./users.model.js";
|
|
4
|
+
import ApiError from "../utils/ApiError.js";
|
|
5
|
+
import auth0Service from "../accounts/auth0.service";
|
|
6
|
+
import organizationsService from "../organizations/organizations.service";
|
|
7
|
+
import { sendEmail } from "../email/email.service";
|
|
8
|
+
import i18n from "../i18n/i18n";
|
|
9
|
+
let updateTimesByIdHook = null;
|
|
10
|
+
export const setUpdateTimesByIdHook = (hook) => {
|
|
11
|
+
updateTimesByIdHook = hook ?? null;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Create a new user
|
|
15
|
+
*/
|
|
16
|
+
export const createUser = async (userBody) => {
|
|
17
|
+
return User.create(userBody);
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Create the “current” user (alias of createUser)
|
|
21
|
+
*/
|
|
22
|
+
export const createCurrentUser = async (userBody) => {
|
|
23
|
+
return createUser(userBody);
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Populate a single Auth0 user
|
|
27
|
+
*/
|
|
28
|
+
const populateAuth0User = async (user) => {
|
|
29
|
+
if (!user)
|
|
30
|
+
return undefined;
|
|
31
|
+
const auth0users = await auth0Service.getUsersByIds([user.owner]);
|
|
32
|
+
return auth0users?.data?.find((u) => u.user_id === user.owner);
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Populate many Auth0 users
|
|
36
|
+
*/
|
|
37
|
+
const populateAuth0Users = async (data) => {
|
|
38
|
+
const owners = data.map((u) => u.owner);
|
|
39
|
+
const auth0users = await auth0Service.getUsersByIds(owners);
|
|
40
|
+
if (!auth0users)
|
|
41
|
+
return data;
|
|
42
|
+
return data.map((doc) => ({
|
|
43
|
+
...doc.toJSON(),
|
|
44
|
+
auth0User: auth0users?.data?.find((u) => u.user_id === doc.owner),
|
|
45
|
+
}));
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Query for users with pagination + Auth0 enrichment
|
|
49
|
+
*/
|
|
50
|
+
export const queryUsers = async (filter, options) => {
|
|
51
|
+
const result = await User.paginate(filter, options);
|
|
52
|
+
result.results = await populateAuth0Users(result.results);
|
|
53
|
+
return result;
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* Get user by Mongo _id
|
|
57
|
+
*/
|
|
58
|
+
export const getById = async (id) => {
|
|
59
|
+
return User.findById(id);
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* Get user by _id with Auth0 info
|
|
63
|
+
*/
|
|
64
|
+
export const getByIdWithAuth0 = async (id) => {
|
|
65
|
+
const user = await getById(id);
|
|
66
|
+
if (!user)
|
|
67
|
+
return null;
|
|
68
|
+
const auth0User = await populateAuth0User(user);
|
|
69
|
+
const json = user.toJSON();
|
|
70
|
+
json.auth0User = auth0User;
|
|
71
|
+
return json;
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* Get all users in a given category (and optional organization)
|
|
75
|
+
*/
|
|
76
|
+
export const getUsersByCategory = async (category, organization) => {
|
|
77
|
+
const filter = { category };
|
|
78
|
+
if (organization)
|
|
79
|
+
filter.organization = organization;
|
|
80
|
+
return User.find(filter);
|
|
81
|
+
};
|
|
82
|
+
/**
|
|
83
|
+
* Get all users for an organization
|
|
84
|
+
*/
|
|
85
|
+
export const getUsersByOrganization = async (organization) => {
|
|
86
|
+
return User.find({ organization }).lean();
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* Get one user by organization + userId
|
|
90
|
+
*/
|
|
91
|
+
export const getUsersByOrganizationAndId = async (organization, userId) => {
|
|
92
|
+
return User.findOne({ organization, _id: userId }).lean();
|
|
93
|
+
};
|
|
94
|
+
/**
|
|
95
|
+
* Get all users for a given owner
|
|
96
|
+
*/
|
|
97
|
+
export const getUsersByOwner = async (owner) => {
|
|
98
|
+
return User.find({ owner });
|
|
99
|
+
};
|
|
100
|
+
/**
|
|
101
|
+
* Get single user by owner + organization, with Auth0 info
|
|
102
|
+
*/
|
|
103
|
+
export const getUserByOwner = async (owner, organization) => {
|
|
104
|
+
const user = await User.findOne({ owner, organization });
|
|
105
|
+
if (!user)
|
|
106
|
+
return null;
|
|
107
|
+
const auth0User = await populateAuth0User(user);
|
|
108
|
+
const json = user.toJSON();
|
|
109
|
+
json.auth0User = auth0User;
|
|
110
|
+
return json;
|
|
111
|
+
};
|
|
112
|
+
/**
|
|
113
|
+
* Get user by email
|
|
114
|
+
*/
|
|
115
|
+
export const getUserByEmail = async (email) => {
|
|
116
|
+
return User.findOne({ email });
|
|
117
|
+
};
|
|
118
|
+
/**
|
|
119
|
+
* Send an invite email
|
|
120
|
+
*/
|
|
121
|
+
export const sendInviteEmail = async (params) => {
|
|
122
|
+
const { auth, user, inviteCode, email } = params;
|
|
123
|
+
const organization = await organizationsService.getOrganizationById(user.organization);
|
|
124
|
+
const auth0User = await auth0Service.getUserById(auth.sub);
|
|
125
|
+
const lng = auth0User.data?.app_metadata?.language;
|
|
126
|
+
const title = `${i18n.t("Invite to ", { lng })}${organization.kind === "private-wirewire" ? "paperlesspaper" : "ANABOX smart"}`;
|
|
127
|
+
const body = i18n.t("You have been invited to join the group. Click on the link to accept the invitation.", { lng });
|
|
128
|
+
await sendEmail({
|
|
129
|
+
title,
|
|
130
|
+
body,
|
|
131
|
+
url: `/${user.organization}/invite/${inviteCode}`,
|
|
132
|
+
actionButtonText: "Accept invite",
|
|
133
|
+
domain: organization.kind === "private-wirewire" ? "web" : "memo",
|
|
134
|
+
email,
|
|
135
|
+
});
|
|
136
|
+
};
|
|
137
|
+
/**
|
|
138
|
+
* Update a user by ID
|
|
139
|
+
*/
|
|
140
|
+
export const updateUserById = async (userId, updateBody, auth) => {
|
|
141
|
+
const user = await getById(userId);
|
|
142
|
+
if (!user) {
|
|
143
|
+
throw new ApiError(httpStatus.NOT_FOUND, `User not found: ${userId}`);
|
|
144
|
+
}
|
|
145
|
+
if (user.status === "invited") {
|
|
146
|
+
await sendInviteEmail({
|
|
147
|
+
auth,
|
|
148
|
+
user,
|
|
149
|
+
inviteCode: user.inviteCode,
|
|
150
|
+
email: updateBody.email,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
//TODO: restrict fields that can be updated, temporarily excluding role
|
|
154
|
+
const { role, ...updateBodyRest } = updateBody;
|
|
155
|
+
const meta = { ...user.meta, ...updateBodyRest.meta };
|
|
156
|
+
Object.assign(user, { ...updateBodyRest, meta });
|
|
157
|
+
await user.save();
|
|
158
|
+
return user;
|
|
159
|
+
};
|
|
160
|
+
/**
|
|
161
|
+
* Delete a user by ID
|
|
162
|
+
*/
|
|
163
|
+
export const deleteUserById = async (userId) => {
|
|
164
|
+
const user = await getById(userId);
|
|
165
|
+
if (!user) {
|
|
166
|
+
throw new ApiError(httpStatus.NOT_FOUND, `User not found: ${userId}`);
|
|
167
|
+
}
|
|
168
|
+
if (user.role === "admin") {
|
|
169
|
+
const admins = await User.find({
|
|
170
|
+
organization: user.organization,
|
|
171
|
+
role: "admin",
|
|
172
|
+
});
|
|
173
|
+
if (admins.length < 2) {
|
|
174
|
+
throw new ApiError(httpStatus.BAD_REQUEST, "At least one admin is required");
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
await user.deleteOne();
|
|
178
|
+
return user;
|
|
179
|
+
};
|
|
180
|
+
/**
|
|
181
|
+
* (Misnamed) delete a user’s image by ID
|
|
182
|
+
*/
|
|
183
|
+
export const userImageById = async (userId) => {
|
|
184
|
+
const user = await getById(userId);
|
|
185
|
+
if (!user) {
|
|
186
|
+
throw new ApiError(httpStatus.NOT_FOUND, `User not found: ${userId}`);
|
|
187
|
+
}
|
|
188
|
+
await user.deleteOne();
|
|
189
|
+
return user;
|
|
190
|
+
};
|
|
191
|
+
/**
|
|
192
|
+
* Invite a user to an organization
|
|
193
|
+
*/
|
|
194
|
+
export const organizationInvite = async (body, oldUser, status = "invited") => {
|
|
195
|
+
const user = await getById(oldUser.id);
|
|
196
|
+
if (!user)
|
|
197
|
+
throw new ApiError(httpStatus.NOT_FOUND, "User not found");
|
|
198
|
+
if (user.organizations.some((o) => o.id.equals(body.organizationId))) {
|
|
199
|
+
throw new ApiError(httpStatus.BAD_REQUEST, "Invite already exists");
|
|
200
|
+
}
|
|
201
|
+
user.organizations.push({ id: body.organizationId, role: body.role, status });
|
|
202
|
+
await user.save();
|
|
203
|
+
return user;
|
|
204
|
+
};
|
|
205
|
+
/**
|
|
206
|
+
* Get an invite by code
|
|
207
|
+
*/
|
|
208
|
+
export const getInvite = async (params) => {
|
|
209
|
+
return User.findOne({
|
|
210
|
+
inviteCode: params.inviteCode,
|
|
211
|
+
owner: { $exists: false },
|
|
212
|
+
}).populate("organizationData");
|
|
213
|
+
};
|
|
214
|
+
/**
|
|
215
|
+
* Accept or decline an invite
|
|
216
|
+
*/
|
|
217
|
+
export const updateInvite = async (params) => {
|
|
218
|
+
const user = await User.findOne({
|
|
219
|
+
inviteCode: params.inviteCode,
|
|
220
|
+
owner: { $exists: false },
|
|
221
|
+
});
|
|
222
|
+
if (!user)
|
|
223
|
+
throw new ApiError(httpStatus.NOT_FOUND, "Invite not found");
|
|
224
|
+
user.status = params.status;
|
|
225
|
+
user.owner = params.owner;
|
|
226
|
+
user.inviteCode = null;
|
|
227
|
+
await user.save();
|
|
228
|
+
return user;
|
|
229
|
+
};
|
|
230
|
+
/**
|
|
231
|
+
* Update a user's organization membership (placeholder for legacy callers)
|
|
232
|
+
*/
|
|
233
|
+
export const organizationUpdate = async (_body) => {
|
|
234
|
+
throw new ApiError(httpStatus.NOT_IMPLEMENTED, "organizationUpdate not implemented");
|
|
235
|
+
};
|
|
236
|
+
/**
|
|
237
|
+
* Remove a user from an organization
|
|
238
|
+
*/
|
|
239
|
+
export const organizationRemove = async (body) => {
|
|
240
|
+
const user = await getById(body.userId);
|
|
241
|
+
if (!user)
|
|
242
|
+
throw new ApiError(httpStatus.NOT_FOUND, "User not found");
|
|
243
|
+
user.organizations = user.organizations.filter((o) => !o.id.equals(body.organizationId));
|
|
244
|
+
await user.save();
|
|
245
|
+
return user;
|
|
246
|
+
};
|
|
247
|
+
/**
|
|
248
|
+
* Fetch up to 100k users with organization populated
|
|
249
|
+
*/
|
|
250
|
+
export const queryAllCalendars = async () => {
|
|
251
|
+
return User.find({}, null, { limit: 100000 }).populate("organizationData");
|
|
252
|
+
};
|
|
253
|
+
/**
|
|
254
|
+
* Delete many users by ID list
|
|
255
|
+
*/
|
|
256
|
+
export const deleteMany = async (idList) => {
|
|
257
|
+
return User.deleteMany({ _id: { $in: idList } });
|
|
258
|
+
};
|
|
259
|
+
/**
|
|
260
|
+
* Update times for a user by ID (hooked from memo-api)
|
|
261
|
+
*/
|
|
262
|
+
export const updateTimesById = async (userId, updateBody, dryRun = true) => {
|
|
263
|
+
if (!updateTimesByIdHook) {
|
|
264
|
+
throw new ApiError(httpStatus.NOT_IMPLEMENTED, "updateTimesById not configured");
|
|
265
|
+
}
|
|
266
|
+
return updateTimesByIdHook(userId, updateBody, dryRun);
|
|
267
|
+
};
|
|
268
|
+
export default {
|
|
269
|
+
createUser,
|
|
270
|
+
createCurrentUser,
|
|
271
|
+
getById,
|
|
272
|
+
getByIdWithAuth0,
|
|
273
|
+
getUsersByCategory,
|
|
274
|
+
getUsersByOrganization,
|
|
275
|
+
getUsersByOrganizationAndId,
|
|
276
|
+
getUsersByOwner,
|
|
277
|
+
getUserByEmail,
|
|
278
|
+
getUserByOwner,
|
|
279
|
+
sendInviteEmail,
|
|
280
|
+
updateUserById,
|
|
281
|
+
updateTimesById,
|
|
282
|
+
deleteUserById,
|
|
283
|
+
userImageById,
|
|
284
|
+
organizationInvite,
|
|
285
|
+
getInvite,
|
|
286
|
+
updateInvite,
|
|
287
|
+
organizationUpdate,
|
|
288
|
+
organizationRemove,
|
|
289
|
+
queryUsers,
|
|
290
|
+
queryAllCalendars,
|
|
291
|
+
deleteMany,
|
|
292
|
+
sendEmail,
|
|
293
|
+
populateAuth0User,
|
|
294
|
+
populateAuth0Users,
|
|
295
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { extendZodWithOpenApi } from "@asteasolutions/zod-to-openapi";
|
|
4
|
+
import { password } from "../validations/custom.validation.js";
|
|
5
|
+
import { zPagination, zGet, zObjectId, zObjectIdFor, zPatchBody, zUpdate, zDelete, } from "../utils/zValidations.js";
|
|
6
|
+
extendZodWithOpenApi(z);
|
|
7
|
+
export const createUserSchema = {
|
|
8
|
+
body: z.object({
|
|
9
|
+
meta: z
|
|
10
|
+
.record(z.any())
|
|
11
|
+
.optional()
|
|
12
|
+
.openapi({
|
|
13
|
+
example: { key: "value" },
|
|
14
|
+
description: "Additional metadata for the user",
|
|
15
|
+
}),
|
|
16
|
+
organization: zObjectId.openapi({
|
|
17
|
+
description: "Organization ObjectId",
|
|
18
|
+
}),
|
|
19
|
+
email: z.string().email().optional().nullable().openapi({
|
|
20
|
+
example: "user@example.com",
|
|
21
|
+
description: "User email address",
|
|
22
|
+
}),
|
|
23
|
+
timezone: z.string().optional().openapi({
|
|
24
|
+
example: "Europe/Berlin",
|
|
25
|
+
description: "IANA timezone string",
|
|
26
|
+
}),
|
|
27
|
+
role: z.enum(["user", "admin", "patient", "onlyself"]).optional().openapi({
|
|
28
|
+
description: "Role assigned to the user",
|
|
29
|
+
}),
|
|
30
|
+
category: z
|
|
31
|
+
.enum(["doctor", "nurse", "patient", "pharmacist", "relative"])
|
|
32
|
+
.optional()
|
|
33
|
+
.openapi({
|
|
34
|
+
description: "Category of the user",
|
|
35
|
+
}),
|
|
36
|
+
}),
|
|
37
|
+
};
|
|
38
|
+
export const createCurrentUserSchema = createUserSchema;
|
|
39
|
+
export const queryUsersSchema = {
|
|
40
|
+
...zPagination,
|
|
41
|
+
query: zPagination.query.extend({
|
|
42
|
+
organization: zObjectIdFor("organization").openapi({
|
|
43
|
+
description: "Filter users by organization ObjectId",
|
|
44
|
+
example: process.env.SCHEMA_EXAMPLE_ORGANIZATION_ID ||
|
|
45
|
+
"60c72b2f9b1e8d001c8e4f3a",
|
|
46
|
+
}),
|
|
47
|
+
}),
|
|
48
|
+
};
|
|
49
|
+
export const getUserSchema = zGet("userId");
|
|
50
|
+
export const getCurrentUserSchema = {
|
|
51
|
+
query: z.object({
|
|
52
|
+
organization: zObjectId,
|
|
53
|
+
}),
|
|
54
|
+
};
|
|
55
|
+
export const updateUserSchema = {
|
|
56
|
+
...zUpdate("userId"),
|
|
57
|
+
body: zPatchBody({
|
|
58
|
+
password: z
|
|
59
|
+
.string()
|
|
60
|
+
.refine((val) => {
|
|
61
|
+
try {
|
|
62
|
+
password(val);
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}, { message: "Invalid password format" })
|
|
69
|
+
.optional()
|
|
70
|
+
.openapi({ description: "New user password" }),
|
|
71
|
+
name: z.string().optional().openapi({ description: "User full name" }),
|
|
72
|
+
timezone: z.string().optional().openapi({ description: "IANA timezone" }),
|
|
73
|
+
avatar: z.string().optional().openapi({ description: "Avatar URL" }),
|
|
74
|
+
meta: z
|
|
75
|
+
.record(z.any())
|
|
76
|
+
.optional()
|
|
77
|
+
.openapi({ description: "Additional metadata" }),
|
|
78
|
+
category: z
|
|
79
|
+
.enum(["doctor", "nurse", "patient", "pharmacist", "relative"])
|
|
80
|
+
.optional()
|
|
81
|
+
.openapi({ description: "User category" }),
|
|
82
|
+
email: z
|
|
83
|
+
.string()
|
|
84
|
+
.email()
|
|
85
|
+
.nullable()
|
|
86
|
+
.optional()
|
|
87
|
+
.openapi({ description: "User email address" }),
|
|
88
|
+
role: z
|
|
89
|
+
.enum(["user", "admin", "patient", "onlyself"])
|
|
90
|
+
.optional()
|
|
91
|
+
.openapi({ description: "User role" }),
|
|
92
|
+
inviteCode: z.string().optional().openapi({ description: "Invite code" }),
|
|
93
|
+
organization: zObjectId
|
|
94
|
+
.optional()
|
|
95
|
+
.openapi({ description: "Organization ObjectId" }),
|
|
96
|
+
}),
|
|
97
|
+
};
|
|
98
|
+
export const deleteUserSchema = zDelete("userId");
|
|
99
|
+
export const organizationInviteSchema = {
|
|
100
|
+
body: z.object({
|
|
101
|
+
organizationId: zObjectId.openapi({ description: "Organization ObjectId" }),
|
|
102
|
+
action: z.string().optional().openapi({ description: "Invite action" }),
|
|
103
|
+
role: z.string().optional().openapi({ description: "Role on invite" }),
|
|
104
|
+
}),
|
|
105
|
+
};
|
|
106
|
+
export const updateInviteSchema = {
|
|
107
|
+
body: z.object({
|
|
108
|
+
organization: zObjectId.openapi({ description: "Organization ObjectId" }),
|
|
109
|
+
status: z.enum(["accepted"]).openapi({ description: "Invite status" }),
|
|
110
|
+
inviteCode: z.string().optional().openapi({ description: "Invite code" }),
|
|
111
|
+
}),
|
|
112
|
+
};
|
|
113
|
+
export const organizationRemoveSchema = {
|
|
114
|
+
body: z.object({
|
|
115
|
+
userId: zObjectId.openapi({ description: "User ObjectId" }),
|
|
116
|
+
organizationId: zObjectId.openapi({ description: "Organization ObjectId" }),
|
|
117
|
+
}),
|
|
118
|
+
};
|
|
119
|
+
export const updateTimesByIdSchema = {
|
|
120
|
+
...zUpdate("userId"),
|
|
121
|
+
body: z
|
|
122
|
+
.object({})
|
|
123
|
+
.catchall(z.string())
|
|
124
|
+
.openapi({
|
|
125
|
+
description: "Arbitrary key/value map of intake times",
|
|
126
|
+
example: {
|
|
127
|
+
"intake-morning": "10:00",
|
|
128
|
+
"intake-noon": "",
|
|
129
|
+
"intake-afternoon": "",
|
|
130
|
+
"intake-night": "15:00",
|
|
131
|
+
},
|
|
132
|
+
}),
|
|
133
|
+
};
|
|
134
|
+
export const updateTimesByIdSchemas = {
|
|
135
|
+
// if you need a path param, uncomment and adjust:
|
|
136
|
+
// ...zUpdate('timeId'),
|
|
137
|
+
...updateTimesByIdSchema,
|
|
138
|
+
};
|
|
139
|
+
export const validateGetInviteSchema = {
|
|
140
|
+
params: z.object({
|
|
141
|
+
inviteCode: z.string().openapi({ description: "Invite code to validate" }),
|
|
142
|
+
}),
|
|
143
|
+
};
|
|
144
|
+
export const sendVerificationEmailValidationSchema = {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
export class ApiError extends Error {
|
|
3
|
+
constructor(statusCode, message, isOperational = true, stack = "", raw = null) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.statusCode = statusCode;
|
|
6
|
+
this.isOperational = isOperational;
|
|
7
|
+
this.raw = raw;
|
|
8
|
+
if (stack) {
|
|
9
|
+
this.stack = stack;
|
|
10
|
+
}
|
|
11
|
+
else {
|
|
12
|
+
Error.captureStackTrace(this, this.constructor);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export default ApiError;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { registry } from "../utils/registerOpenApi";
|
|
2
|
+
import { validateZod } from "../middlewares/validateZod";
|
|
3
|
+
import { bearerAuth, xApiKey } from "../utils/registerOpenApi";
|
|
4
|
+
import { extendZodWithOpenApi } from "@asteasolutions/zod-to-openapi";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
extendZodWithOpenApi(z);
|
|
7
|
+
const roleValidatorNames = ["validateAiRole", "validateAdmin"];
|
|
8
|
+
function hasRoleValidation(validators = []) {
|
|
9
|
+
return validators.some((fn) => roleValidatorNames.includes(fn.name));
|
|
10
|
+
}
|
|
11
|
+
export default function buildAiRouterAndDocs(router, routeSpecs, basePath = "/", tags = []) {
|
|
12
|
+
routeSpecs.forEach((spec) => {
|
|
13
|
+
// mount Express
|
|
14
|
+
if (!spec.validate) {
|
|
15
|
+
spec.validate = [];
|
|
16
|
+
}
|
|
17
|
+
if (spec.requestSchema) {
|
|
18
|
+
spec.validateWithRequestSchema = [
|
|
19
|
+
validateZod(spec.requestSchema),
|
|
20
|
+
...spec.validate,
|
|
21
|
+
];
|
|
22
|
+
}
|
|
23
|
+
if (spec.validateWithRequestSchema) {
|
|
24
|
+
router[spec.method](spec.path, ...spec.validateWithRequestSchema, spec.handler);
|
|
25
|
+
}
|
|
26
|
+
var { body, ...rest } = spec.requestSchema || {};
|
|
27
|
+
if (body) {
|
|
28
|
+
rest.body = {
|
|
29
|
+
content: {
|
|
30
|
+
"application/json": {
|
|
31
|
+
schema: body,
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
console.log("spec.requestScbn");
|
|
37
|
+
if (spec.responseSchema &&
|
|
38
|
+
!hasRoleValidation(spec.validate) &&
|
|
39
|
+
spec.privateDocs !== true &&
|
|
40
|
+
spec.memoOnly !== true) {
|
|
41
|
+
// collect all middleware fn names (falls back to '<anonymous>' if unnamed)
|
|
42
|
+
const middlewareNames = (spec.validate || []).map((fn) => `\`${fn.name}\`` || "<anonymous>");
|
|
43
|
+
const openApiPath = (basePath + spec.path).replace(/:([A-Za-z0-9_]+)/g, "{$1}");
|
|
44
|
+
registry.registerPath({
|
|
45
|
+
method: spec.method,
|
|
46
|
+
path: openApiPath,
|
|
47
|
+
summary: spec.summary,
|
|
48
|
+
request: rest,
|
|
49
|
+
// append middleware names to the description
|
|
50
|
+
description: [
|
|
51
|
+
spec.description,
|
|
52
|
+
`\n\nMiddlewares: ${middlewareNames.join(", ")}`,
|
|
53
|
+
]
|
|
54
|
+
.filter(Boolean)
|
|
55
|
+
.join("\n"),
|
|
56
|
+
// (optionally) expose them as a custom extension instead:
|
|
57
|
+
"x-middlewares": middlewareNames,
|
|
58
|
+
security: [{ [bearerAuth.name]: [] }, { [xApiKey.name]: [] }],
|
|
59
|
+
responses: {
|
|
60
|
+
200: {
|
|
61
|
+
description: "Object with user data.",
|
|
62
|
+
content: {
|
|
63
|
+
"application/json": { schema: spec.responseSchema },
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
tags,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
// else: streaming endpoint, we don’t register it in OpenAPI
|
|
71
|
+
});
|
|
72
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import sharp from 'sharp';
|
|
2
|
+
import pixelmatch from 'pixelmatch';
|
|
3
|
+
/**
|
|
4
|
+
* Compares two image buffers and returns the similarity percentage
|
|
5
|
+
* @param {Buffer} imgBuffer1 - First image buffer
|
|
6
|
+
* @param {Buffer} imgBuffer2 - Second image buffer
|
|
7
|
+
* @returns {Promise<number>} - Percentage of similarity (0-100)
|
|
8
|
+
*/
|
|
9
|
+
export async function compareImages(imgBuffer1, imgBuffer2) {
|
|
10
|
+
// Load the images using sharp and resize them to the same size
|
|
11
|
+
const image1 = await sharp(imgBuffer1).raw().ensureAlpha().toBuffer({ resolveWithObject: true });
|
|
12
|
+
const image2 = await sharp(imgBuffer2).raw().ensureAlpha().toBuffer({ resolveWithObject: true });
|
|
13
|
+
// Ensure both images are of the same dimensions
|
|
14
|
+
const width = Math.min(image1.info.width, image2.info.width);
|
|
15
|
+
const height = Math.min(image1.info.height, image2.info.height);
|
|
16
|
+
if (image1.info.width !== image2.info.width || image1.info.height !== image2.info.height) {
|
|
17
|
+
return 0;
|
|
18
|
+
}
|
|
19
|
+
// Resize the images to match in size
|
|
20
|
+
const resizedImage1 = await sharp(imgBuffer1).resize(width, height).raw().ensureAlpha().toBuffer();
|
|
21
|
+
const resizedImage2 = await sharp(imgBuffer2).resize(width, height).raw().ensureAlpha().toBuffer();
|
|
22
|
+
// Create an empty array to store pixel differences
|
|
23
|
+
const diff = new Uint8Array(width * height * 4);
|
|
24
|
+
// Compare the two images pixel by pixel
|
|
25
|
+
const numDiffPixels = pixelmatch(resizedImage1, resizedImage2, diff, width, height, { threshold: 0.3 } // Threshold for pixel comparison
|
|
26
|
+
);
|
|
27
|
+
// Calculate similarity percentage
|
|
28
|
+
const totalPixels = width * height;
|
|
29
|
+
const similarityPercentage = ((totalPixels - numDiffPixels) / totalPixels) * 100;
|
|
30
|
+
return similarityPercentage;
|
|
31
|
+
}
|
|
32
|
+
export default { compareImages };
|