@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,118 @@
|
|
|
1
|
+
import { extendZodWithOpenApi } from "@asteasolutions/zod-to-openapi";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { objectId } from "../validations/custom.validation.js";
|
|
4
|
+
import {
|
|
5
|
+
zPagination,
|
|
6
|
+
zGet,
|
|
7
|
+
zObjectId,
|
|
8
|
+
zPatchBody,
|
|
9
|
+
zUpdate,
|
|
10
|
+
zDelete,
|
|
11
|
+
} from "../utils/zValidations.js";
|
|
12
|
+
|
|
13
|
+
extendZodWithOpenApi(z);
|
|
14
|
+
|
|
15
|
+
export const createAccountSchema = {
|
|
16
|
+
body: z.object({
|
|
17
|
+
name: z
|
|
18
|
+
.string()
|
|
19
|
+
.openapi({ example: "Sample Entry", description: "Name of the entry" })
|
|
20
|
+
.optional(),
|
|
21
|
+
medication: zObjectId.openapi({ description: "Medication ObjectId" }),
|
|
22
|
+
organization: zObjectId.openapi({ description: "Organization ObjectId" }),
|
|
23
|
+
patient: zObjectId.openapi({ description: "Patient ObjectId" }),
|
|
24
|
+
meta: z
|
|
25
|
+
.record(z.any())
|
|
26
|
+
.openapi({
|
|
27
|
+
example: { key: "value" },
|
|
28
|
+
description: "Additional metadata for the entry",
|
|
29
|
+
})
|
|
30
|
+
.optional(),
|
|
31
|
+
}),
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const getUsersSchema = zPagination;
|
|
35
|
+
|
|
36
|
+
export const getAccountSchema = {
|
|
37
|
+
params: z.object({
|
|
38
|
+
accountId: z
|
|
39
|
+
.string()
|
|
40
|
+
.openapi({
|
|
41
|
+
example: "auth0%7C60452f4c0dc85b0062326",
|
|
42
|
+
description: "Auth Account ID",
|
|
43
|
+
}),
|
|
44
|
+
}),
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const updateAccountSchema = {
|
|
48
|
+
params: z.object({
|
|
49
|
+
accountId: z
|
|
50
|
+
.string()
|
|
51
|
+
.openapi({
|
|
52
|
+
example: "auth0%7C60452f4c0dc85b0062326",
|
|
53
|
+
description: "Auth Account ID",
|
|
54
|
+
}),
|
|
55
|
+
}),
|
|
56
|
+
body: zPatchBody({
|
|
57
|
+
language: z
|
|
58
|
+
.string()
|
|
59
|
+
.nullable()
|
|
60
|
+
.optional()
|
|
61
|
+
.openapi({ example: "en", description: "Language code" }),
|
|
62
|
+
gender: z
|
|
63
|
+
.string()
|
|
64
|
+
.nullable()
|
|
65
|
+
.optional()
|
|
66
|
+
.openapi({ example: "female", description: "Gender" }),
|
|
67
|
+
email: z
|
|
68
|
+
.string()
|
|
69
|
+
.email()
|
|
70
|
+
.optional()
|
|
71
|
+
.openapi({ description: "User email address" }),
|
|
72
|
+
given_name: z
|
|
73
|
+
.string()
|
|
74
|
+
.optional()
|
|
75
|
+
.openapi({ example: "John", description: "Given name" }),
|
|
76
|
+
family_name: z
|
|
77
|
+
.string()
|
|
78
|
+
.optional()
|
|
79
|
+
.openapi({ example: "Doe", description: "Family name" }),
|
|
80
|
+
debug: z.boolean().optional(),
|
|
81
|
+
demo: z.boolean().optional(),
|
|
82
|
+
notification: z
|
|
83
|
+
.record(z.any())
|
|
84
|
+
.optional()
|
|
85
|
+
.openapi({ description: "Notification settings object" }),
|
|
86
|
+
}),
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export const deleteEntrySchema = {
|
|
90
|
+
params: z.object({
|
|
91
|
+
accountId: z
|
|
92
|
+
.string()
|
|
93
|
+
.openapi({
|
|
94
|
+
example: "auth0%7C60452f4c0dc85b0062326",
|
|
95
|
+
description: "Auth Account ID",
|
|
96
|
+
}),
|
|
97
|
+
}),
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export const currentAccountSchema = {};
|
|
101
|
+
|
|
102
|
+
export const currentAccountMfaEnrollSchema = {
|
|
103
|
+
/* body: z.object({
|
|
104
|
+
mfaEnroll: z.string().openapi({ example: 'totp', description: 'MFA token to enroll' }),
|
|
105
|
+
}), */
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
export const setDeviceTokenSchema = {
|
|
109
|
+
body: z.object({
|
|
110
|
+
token: z
|
|
111
|
+
.string()
|
|
112
|
+
.openapi({ example: "device-token", description: "Device token to set" }),
|
|
113
|
+
}),
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
export const getAvatarSchema = {};
|
|
117
|
+
|
|
118
|
+
export const deleteCurrentSchema = {};
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { AuthenticationClient, ManagementClient } from 'auth0';
|
|
2
|
+
import type { ManagementClientOptions, User } from 'auth0';
|
|
3
|
+
import { promises as fs, readFileSync } from 'fs';
|
|
4
|
+
import { config } from 'process';
|
|
5
|
+
|
|
6
|
+
interface TokenManagementClient {
|
|
7
|
+
clientId?: string;
|
|
8
|
+
clientSecret?: string;
|
|
9
|
+
token?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
type CachedToken = {
|
|
13
|
+
token: string;
|
|
14
|
+
expiresAt: number;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
let tokenManagementClient: TokenManagementClient = {
|
|
18
|
+
clientId: process.env.AUTH0_CLIENT_ID,
|
|
19
|
+
clientSecret: process.env.AUTH0_CLIENT_SECRET,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
//if (config.env !== 'production') {
|
|
23
|
+
try {
|
|
24
|
+
console.warn('Auth0 client: use local token from cache try');
|
|
25
|
+
const token = readFileSync('./token.txt', 'utf8');
|
|
26
|
+
console.warn('Auth0 client: use local token from cache');
|
|
27
|
+
tokenManagementClient = { token };
|
|
28
|
+
} catch (error) {
|
|
29
|
+
console.log('Auth0 Client: use new token');
|
|
30
|
+
}
|
|
31
|
+
//}
|
|
32
|
+
|
|
33
|
+
// IoT Api
|
|
34
|
+
/*
|
|
35
|
+
export const auth0Management = new ManagementClient({
|
|
36
|
+
...tokenManagementClient,
|
|
37
|
+
domain: process.env.AUTH0_MANAGEMENT_DOMAIN!,
|
|
38
|
+
grant_type: 'client_credentials',
|
|
39
|
+
audience: process.env.AUTH0_AUDIENCE!,
|
|
40
|
+
} as ManagementClientOptions);
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
// add this AuthenticationClient just for token fetching
|
|
44
|
+
export const auth0AuthClient = new AuthenticationClient({
|
|
45
|
+
domain: process.env.AUTH0_MANAGEMENT_DOMAIN!,
|
|
46
|
+
clientId: process.env.AUTH0_MANAGEMENT_CLIENT_ID!,
|
|
47
|
+
clientSecret: process.env.AUTH0_MANAGEMENT_CLIENT_SECRET!,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// In-memory cache for client-credentials tokens
|
|
51
|
+
let cachedToken: string | null = null;
|
|
52
|
+
let tokenExpiresAt: number | null = null;
|
|
53
|
+
let pendingTokenPromise: Promise<string> | null = null;
|
|
54
|
+
|
|
55
|
+
let cachedManagementToken: string | null = null;
|
|
56
|
+
let managementTokenExpiresAt: number | null = null;
|
|
57
|
+
let pendingManagementTokenPromise: Promise<string> | null = null;
|
|
58
|
+
|
|
59
|
+
const TOKEN_FILE_PATH = './token.txt';
|
|
60
|
+
const MANAGEMENT_TOKEN_FILE_PATH = './token.management.txt';
|
|
61
|
+
const TOKEN_BUFFER_SECONDS = 60; // refresh a minute before expiry
|
|
62
|
+
const TOKEN_FALLBACK_TTL_SECONDS = 3300; // ~55 minutes when no expiry metadata exists
|
|
63
|
+
|
|
64
|
+
const loadTokenFromFile = async (filePath: string): Promise<CachedToken | null> => {
|
|
65
|
+
try {
|
|
66
|
+
const raw = await fs.readFile(filePath, 'utf8');
|
|
67
|
+
try {
|
|
68
|
+
const parsed = JSON.parse(raw);
|
|
69
|
+
if (parsed?.token && parsed?.expiresAt) {
|
|
70
|
+
return { token: parsed.token, expiresAt: parsed.expiresAt };
|
|
71
|
+
}
|
|
72
|
+
} catch (parseError) {
|
|
73
|
+
// Backward compatibility: token.txt previously contained only the token string
|
|
74
|
+
const now = Math.floor(Date.now() / 1000);
|
|
75
|
+
return { token: raw.trim(), expiresAt: now + TOKEN_FALLBACK_TTL_SECONDS };
|
|
76
|
+
}
|
|
77
|
+
} catch (error) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const writeTokenFile = async (filePath: string, token: string, expiresAt: number): Promise<void> => {
|
|
84
|
+
const payload = JSON.stringify({ token, expiresAt });
|
|
85
|
+
await fs.writeFile(filePath, payload, 'utf8');
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const tokenIsValid = (token: string | null, expiresAt: number | null, now: number): boolean => {
|
|
89
|
+
if (!token || !expiresAt) return false;
|
|
90
|
+
return now < expiresAt - TOKEN_BUFFER_SECONDS;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export const getAuth0Token = async (): Promise<string> => {
|
|
94
|
+
const audience = process.env.AUTH0_AUDIENCE || process.env.AUTH0_MANAGEMENT_AUDIENCE || 'localhost:3000/';
|
|
95
|
+
const grantOpts = { audience };
|
|
96
|
+
|
|
97
|
+
const now = Math.floor(Date.now() / 1000);
|
|
98
|
+
if (tokenIsValid(cachedToken, tokenExpiresAt, now)) return cachedToken as string;
|
|
99
|
+
if (pendingTokenPromise) return pendingTokenPromise;
|
|
100
|
+
|
|
101
|
+
pendingTokenPromise = (async () => {
|
|
102
|
+
// Non-production: try to reuse a file-cached token (for local dev) before minting a new one
|
|
103
|
+
// if (process.env.NODE_ENV !== 'production') {
|
|
104
|
+
const fileToken = await loadTokenFromFile(TOKEN_FILE_PATH);
|
|
105
|
+
if (fileToken && fileToken.expiresAt > now + TOKEN_BUFFER_SECONDS) {
|
|
106
|
+
cachedToken = fileToken.token;
|
|
107
|
+
tokenExpiresAt = fileToken.expiresAt;
|
|
108
|
+
pendingTokenPromise = null;
|
|
109
|
+
return cachedToken;
|
|
110
|
+
}
|
|
111
|
+
//}
|
|
112
|
+
|
|
113
|
+
const tokenResponse = await auth0AuthClient.oauth.clientCredentialsGrant(grantOpts);
|
|
114
|
+
const expiresIn = tokenResponse.data.expires_in || 3600;
|
|
115
|
+
cachedToken = tokenResponse.data.access_token;
|
|
116
|
+
tokenExpiresAt = now + expiresIn;
|
|
117
|
+
|
|
118
|
+
// if (process.env.NODE_ENV !== 'production') {
|
|
119
|
+
await writeTokenFile(TOKEN_FILE_PATH, cachedToken, tokenExpiresAt);
|
|
120
|
+
// }
|
|
121
|
+
|
|
122
|
+
pendingTokenPromise = null;
|
|
123
|
+
return cachedToken;
|
|
124
|
+
})();
|
|
125
|
+
|
|
126
|
+
return pendingTokenPromise;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
export const getAuth0ManagementToken = async (): Promise<string> => {
|
|
130
|
+
const audience = process.env.AUTH0_MANAGEMENT_AUDIENCE;
|
|
131
|
+
if (!audience) {
|
|
132
|
+
throw new Error('Missing AUTH0_MANAGEMENT_AUDIENCE; cannot mint Management API token');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const grantOpts = { audience };
|
|
136
|
+
const now = Math.floor(Date.now() / 1000);
|
|
137
|
+
|
|
138
|
+
if (tokenIsValid(cachedManagementToken, managementTokenExpiresAt, now)) return cachedManagementToken as string;
|
|
139
|
+
if (pendingManagementTokenPromise) return pendingManagementTokenPromise;
|
|
140
|
+
|
|
141
|
+
pendingManagementTokenPromise = (async () => {
|
|
142
|
+
const fileToken = await loadTokenFromFile(MANAGEMENT_TOKEN_FILE_PATH);
|
|
143
|
+
if (fileToken && fileToken.expiresAt > now + TOKEN_BUFFER_SECONDS) {
|
|
144
|
+
cachedManagementToken = fileToken.token;
|
|
145
|
+
managementTokenExpiresAt = fileToken.expiresAt;
|
|
146
|
+
pendingManagementTokenPromise = null;
|
|
147
|
+
return cachedManagementToken;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const tokenResponse = await auth0AuthClient.oauth.clientCredentialsGrant(grantOpts);
|
|
151
|
+
const expiresIn = tokenResponse.data.expires_in || 3600;
|
|
152
|
+
cachedManagementToken = tokenResponse.data.access_token;
|
|
153
|
+
managementTokenExpiresAt = now + expiresIn;
|
|
154
|
+
|
|
155
|
+
await writeTokenFile(MANAGEMENT_TOKEN_FILE_PATH, cachedManagementToken, managementTokenExpiresAt);
|
|
156
|
+
|
|
157
|
+
pendingManagementTokenPromise = null;
|
|
158
|
+
return cachedManagementToken;
|
|
159
|
+
})();
|
|
160
|
+
|
|
161
|
+
return pendingManagementTokenPromise;
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
export const auth0 = new ManagementClient({
|
|
165
|
+
domain: process.env.AUTH0_MANAGEMENT_DOMAIN!,
|
|
166
|
+
audience: process.env.AUTH0_MANAGEMENT_AUDIENCE!,
|
|
167
|
+
token: getAuth0ManagementToken,
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
export const getUserIdByEmail = async (email: string): Promise<User[]> => {
|
|
171
|
+
// use the users resource to look up by email
|
|
172
|
+
return auth0.usersByEmail.getByEmail({ email });
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
export const sendVerificationEmail = async (userID: string): Promise<void> => {
|
|
176
|
+
await auth0.jobs.verifyEmail({ user_id: userID });
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
export const getUserById = async (userId: string): Promise<User> => {
|
|
180
|
+
return auth0.users.get({ id: userId });
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
export const avatar = async (userId: string): Promise<User> => {
|
|
184
|
+
return auth0.users.get({ id: userId });
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
export const mfaEnrollAccount = async (userId: string, mfaToken: string): Promise<any> => {
|
|
188
|
+
const ticketResponse = await auth0.guardian.createEnrollmentTicket({
|
|
189
|
+
user_id: userId,
|
|
190
|
+
send_mail: false,
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
return ticketResponse;
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
export const mfaDisableAccount = async (userId: string): Promise<any> => {
|
|
197
|
+
await auth0.users.deleteAuthenticationMethods({ id: userId });
|
|
198
|
+
return { success: true };
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
export const getUsersByIds = async (postIDs: string[]): Promise<User[]> => {
|
|
202
|
+
let q = '';
|
|
203
|
+
postIDs.forEach((e, i) => {
|
|
204
|
+
if (e) q = `${q} ${i >= 2 ? ' OR ' : ''} user_id:"${e}"`;
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
const params = {
|
|
208
|
+
search_engine: 'v3',
|
|
209
|
+
q,
|
|
210
|
+
per_page: 100,
|
|
211
|
+
page: 0,
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
return auth0.users.getAll(params);
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
export default {
|
|
218
|
+
auth0,
|
|
219
|
+
avatar,
|
|
220
|
+
getAuth0Token,
|
|
221
|
+
getUsersByIds,
|
|
222
|
+
getUserIdByEmail,
|
|
223
|
+
getUserById,
|
|
224
|
+
mfaEnrollAccount,
|
|
225
|
+
sendVerificationEmail,
|
|
226
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import dotenv from "dotenv";
|
|
2
|
+
|
|
3
|
+
// Load env from the current working directory
|
|
4
|
+
dotenv.config();
|
|
5
|
+
|
|
6
|
+
import Joi from "joi";
|
|
7
|
+
|
|
8
|
+
const envVarsSchema = Joi.object()
|
|
9
|
+
.keys({
|
|
10
|
+
NODE_ENV: Joi.string()
|
|
11
|
+
.valid("production", "development", "test")
|
|
12
|
+
.required(),
|
|
13
|
+
PORT: Joi.number().default(3000),
|
|
14
|
+
MONGODB_URL: Joi.string().required().description("Mongo DB url"),
|
|
15
|
+
})
|
|
16
|
+
.unknown();
|
|
17
|
+
|
|
18
|
+
const { value: envVars, error } = envVarsSchema
|
|
19
|
+
.prefs({ errors: { label: "key" } })
|
|
20
|
+
.validate(process.env);
|
|
21
|
+
|
|
22
|
+
if (error) {
|
|
23
|
+
throw new Error(`Config validation error: ${error.message}`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const config = {
|
|
27
|
+
env: envVars.NODE_ENV,
|
|
28
|
+
port: envVars.PORT,
|
|
29
|
+
mongoose: {
|
|
30
|
+
url: envVars.MONGODB_URL + (envVars.NODE_ENV === "test" ? "-test" : ""),
|
|
31
|
+
useCreateIndex: true,
|
|
32
|
+
useNewUrlParser: true,
|
|
33
|
+
useUnifiedTopology: true,
|
|
34
|
+
useFindAndModify: false,
|
|
35
|
+
},
|
|
36
|
+
/* email: {
|
|
37
|
+
smtp: {
|
|
38
|
+
host: envVars.SMTP_HOST,
|
|
39
|
+
port: envVars.SMTP_PORT,
|
|
40
|
+
auth: {
|
|
41
|
+
user: envVars.SMTP_USERNAME,
|
|
42
|
+
pass: envVars.SMTP_PASSWORD,
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
from: envVars.EMAIL_FROM,
|
|
46
|
+
}, */
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export default config;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import winston from 'winston';
|
|
2
|
+
import type { Format } from 'logform';
|
|
3
|
+
import config from './config';
|
|
4
|
+
|
|
5
|
+
const enumerateErrorFormat: Format = winston.format((info) => {
|
|
6
|
+
if (info instanceof Error) {
|
|
7
|
+
Object.assign(info, { message: info.stack });
|
|
8
|
+
}
|
|
9
|
+
return info;
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const logger = winston.createLogger({
|
|
13
|
+
level: config.env === 'development' ? 'debug' : 'info',
|
|
14
|
+
format: winston.format.combine(
|
|
15
|
+
enumerateErrorFormat(),
|
|
16
|
+
config.env === 'development' ? winston.format.colorize() : winston.format.uncolorize(),
|
|
17
|
+
winston.format.splat(),
|
|
18
|
+
winston.format.printf((info) => {
|
|
19
|
+
const { level, message, stack, ...rest } = info as Record<string, unknown>;
|
|
20
|
+
const restKeys = Object.keys(rest);
|
|
21
|
+
const restText = restKeys.length ? ` ${JSON.stringify(rest)}` : '';
|
|
22
|
+
const stackText = stack ? `\n${stack}` : '';
|
|
23
|
+
return `${level}: ${message}${stackText}${restText}`;
|
|
24
|
+
}),
|
|
25
|
+
),
|
|
26
|
+
transports: [
|
|
27
|
+
new winston.transports.Console({
|
|
28
|
+
stderrLevels: ['error'],
|
|
29
|
+
}),
|
|
30
|
+
],
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
export default logger;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import morgan from 'morgan';
|
|
2
|
+
import type { Request, Response } from 'express';
|
|
3
|
+
import config from './config';
|
|
4
|
+
import logger from './logger';
|
|
5
|
+
|
|
6
|
+
morgan.token('message', (req: Request, res: Response) => res.locals.errorMessage || '');
|
|
7
|
+
|
|
8
|
+
const getIpFormat = (): string => (config.env === 'production' ? ':remote-addr - ' : '');
|
|
9
|
+
const successResponseFormat = `${getIpFormat()}:method :url :status - :response-time ms`;
|
|
10
|
+
const errorResponseFormat = `${getIpFormat()}:method :url :status - :response-time ms - message: :message`;
|
|
11
|
+
|
|
12
|
+
const successHandler = morgan(successResponseFormat, {
|
|
13
|
+
skip: (req: Request, res: Response) => res.statusCode >= 400,
|
|
14
|
+
stream: { write: (message: string) => logger.info(message.trim()) },
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const errorHandler = morgan(errorResponseFormat, {
|
|
18
|
+
skip: (req: Request, res: Response) => res.statusCode < 400,
|
|
19
|
+
stream: { write: (message: string) => logger.error(message.trim()) },
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
export default { successHandler, errorHandler };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const { Strategy: JwtStrategy, ExtractJwt } = require('passport-jwt');
|
|
2
|
+
const config = require('./config.cjs');
|
|
3
|
+
const { tokenTypes } = require('./tokens.cjs');
|
|
4
|
+
const { User } = require('../models.cjs');
|
|
5
|
+
|
|
6
|
+
const jwtOptions = {
|
|
7
|
+
secretOrKey: config.jwt.secret,
|
|
8
|
+
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const jwtVerify = async (payload, done) => {
|
|
12
|
+
try {
|
|
13
|
+
if (payload.type !== tokenTypes.ACCESS) {
|
|
14
|
+
throw new Error('Invalid token type');
|
|
15
|
+
}
|
|
16
|
+
const user = await User.findById(payload.sub);
|
|
17
|
+
if (!user) {
|
|
18
|
+
return done(null, false);
|
|
19
|
+
}
|
|
20
|
+
done(null, user);
|
|
21
|
+
} catch (error) {
|
|
22
|
+
done(error, false);
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const jwtStrategy = new JwtStrategy(jwtOptions, jwtVerify);
|
|
27
|
+
|
|
28
|
+
module.exports = {
|
|
29
|
+
jwtStrategy,
|
|
30
|
+
};
|