@naturalcycles/backend-lib 9.71.1 → 9.73.0
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/admin/base.admin.service.d.ts +70 -34
- package/dist/admin/base.admin.service.js +137 -104
- package/dist/express/startServer.js +1 -1
- package/dist/server/serverStatusMiddleware.js +7 -1
- package/package.json +1 -1
- package/src/admin/base.admin.service.ts +227 -154
- package/src/express/startServer.ts +1 -1
- package/src/server/serverStatusMiddleware.ts +8 -0
|
@@ -1,21 +1,6 @@
|
|
|
1
|
+
import type { AnyObject } from '@naturalcycles/js-lib/types';
|
|
1
2
|
import type { Auth } from 'firebase-admin/auth';
|
|
2
3
|
import type { BackendRequest, BackendRequestHandler } from '../server/server.model.js';
|
|
3
|
-
export interface AdminServiceCfg {
|
|
4
|
-
/**
|
|
5
|
-
* @default 'admin_token'
|
|
6
|
-
*/
|
|
7
|
-
adminTokenKey?: string;
|
|
8
|
-
/**
|
|
9
|
-
* If false - disables auth completely (useful for debugging locally, but never in production).
|
|
10
|
-
*
|
|
11
|
-
* @default true
|
|
12
|
-
*/
|
|
13
|
-
authEnabled?: boolean;
|
|
14
|
-
}
|
|
15
|
-
export interface AdminInfo {
|
|
16
|
-
email: string;
|
|
17
|
-
permissions: string[];
|
|
18
|
-
}
|
|
19
4
|
/**
|
|
20
5
|
* Base implementation based on Firebase Auth tokens passed as 'admin_token' cookie.
|
|
21
6
|
*/
|
|
@@ -24,35 +9,47 @@ export declare class BaseAdminService {
|
|
|
24
9
|
constructor(loadFirebaseAuth: () => Promise<Auth>, cfg: AdminServiceCfg);
|
|
25
10
|
cfg: Required<AdminServiceCfg>;
|
|
26
11
|
adminInfoDisabled(): AdminInfo;
|
|
12
|
+
isAdmin(req: BackendRequest | undefined): Promise<boolean>;
|
|
13
|
+
getAdminInfo(req: BackendRequest): Promise<AdminInfo | undefined>;
|
|
14
|
+
hasPermission(req: BackendRequest, reqPermission: string, meta?: AnyObject): Promise<boolean>;
|
|
15
|
+
requirePermission(req: BackendRequest, reqPermission: string, meta?: AnyObject): Promise<AdminInfo>;
|
|
27
16
|
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* Returns undefined if it's not an Admin.
|
|
31
|
-
* Otherwise returns Set of permissions.
|
|
32
|
-
* Empty array means it IS and Admin, but has no permissions (except being an Admin).
|
|
17
|
+
* Returns AdminInfo if it has all required permissions.
|
|
18
|
+
* Otherwise returns undefined
|
|
33
19
|
*/
|
|
34
|
-
|
|
20
|
+
hasPermissions(req: BackendRequest, reqPermissions?: string[], meta?: AnyObject): Promise<AdminInfo | undefined>;
|
|
21
|
+
requirePermissions(req: BackendRequest, reqPermissions?: string[], meta?: AnyObject, andComparison?: boolean): Promise<AdminInfo>;
|
|
35
22
|
/**
|
|
36
|
-
*
|
|
23
|
+
* Doesn't check permissions, nor `authEnabled`. See `checkPermissions`.
|
|
37
24
|
*/
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
25
|
+
resolveAdmin(req: BackendRequest): Promise<ResolvedAdmin>;
|
|
26
|
+
/**
|
|
27
|
+
* Doesn't log or throw. See `requireGranted`.
|
|
28
|
+
*/
|
|
29
|
+
checkPermissions(admin: ResolvedAdmin, reqPermissions?: string[], opt?: CheckPermissionsOptions): CheckPermissionsResult;
|
|
30
|
+
/**
|
|
31
|
+
* Throws 401 if there's no email, 403 if not granted.
|
|
32
|
+
*/
|
|
33
|
+
requireGranted(result: CheckPermissionsResult): AdminInfo;
|
|
41
34
|
/**
|
|
42
35
|
* Current implementation is based on req=Request (from Express).
|
|
43
36
|
* Override if needed.
|
|
44
37
|
*/
|
|
45
38
|
getAdminToken(req: BackendRequest): string | undefined;
|
|
46
|
-
|
|
47
|
-
|
|
39
|
+
getEmailByToken(req: BackendRequest, adminToken?: string): Promise<string | undefined>;
|
|
40
|
+
private getFirebaseAuth;
|
|
48
41
|
/**
|
|
49
|
-
*
|
|
50
|
-
*
|
|
42
|
+
* To be extended.
|
|
43
|
+
*
|
|
44
|
+
* Returns undefined if it's not an Admin.
|
|
45
|
+
* Otherwise returns Set of permissions.
|
|
46
|
+
* Empty array means it IS and Admin, but has no permissions (except being an Admin).
|
|
47
|
+
*/
|
|
48
|
+
getEmailPermissions(email?: string): Promise<Set<string> | undefined>;
|
|
49
|
+
/**
|
|
50
|
+
* To be extended.
|
|
51
51
|
*/
|
|
52
|
-
|
|
53
|
-
requirePermissions(req: BackendRequest, reqPermissions?: string[], meta?: Record<string, any>, andComparison?: boolean): Promise<AdminInfo>;
|
|
54
|
-
hasPermission(req: BackendRequest, reqPermission: string, meta?: Record<string, any>): Promise<boolean>;
|
|
55
|
-
requirePermission(req: BackendRequest, reqPermission: string, meta?: Record<string, any>): Promise<AdminInfo>;
|
|
52
|
+
protected onPermissionCheck(req: BackendRequest, email: string, reqPermissions: string[], required: boolean, granted: boolean, meta?: AnyObject): Promise<void>;
|
|
56
53
|
/**
|
|
57
54
|
* Install it on POST /admin/login url
|
|
58
55
|
*
|
|
@@ -64,3 +61,42 @@ export declare class BaseAdminService {
|
|
|
64
61
|
*/
|
|
65
62
|
getFirebaseAuthLoginHandler(): BackendRequestHandler;
|
|
66
63
|
}
|
|
64
|
+
export interface AdminServiceCfg {
|
|
65
|
+
/**
|
|
66
|
+
* @default 'admin_token'
|
|
67
|
+
*/
|
|
68
|
+
adminTokenKey?: string;
|
|
69
|
+
/**
|
|
70
|
+
* If false - disables auth completely (useful for debugging locally, but never in production).
|
|
71
|
+
*
|
|
72
|
+
* @default true
|
|
73
|
+
*/
|
|
74
|
+
authEnabled?: boolean;
|
|
75
|
+
}
|
|
76
|
+
export interface AdminInfo {
|
|
77
|
+
email: string;
|
|
78
|
+
permissions: string[];
|
|
79
|
+
}
|
|
80
|
+
export interface ResolvedAdmin {
|
|
81
|
+
/** undefined - no valid admin token */
|
|
82
|
+
email?: string;
|
|
83
|
+
/** undefined - not an Admin */
|
|
84
|
+
permissions?: Set<string>;
|
|
85
|
+
}
|
|
86
|
+
export interface CheckPermissionsOptions {
|
|
87
|
+
/**
|
|
88
|
+
* false - one granted permission is enough.
|
|
89
|
+
*
|
|
90
|
+
* @default true
|
|
91
|
+
*/
|
|
92
|
+
andComparison?: boolean;
|
|
93
|
+
}
|
|
94
|
+
export interface CheckPermissionsResult {
|
|
95
|
+
/** undefined - no valid admin token */
|
|
96
|
+
email?: string;
|
|
97
|
+
isAdmin: boolean;
|
|
98
|
+
granted: boolean;
|
|
99
|
+
reqPermissions: string[];
|
|
100
|
+
grantedPermissions: string[];
|
|
101
|
+
authDisabled: boolean;
|
|
102
|
+
}
|
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
import { __decorate } from "tslib";
|
|
2
2
|
import { _Memo } from '@naturalcycles/js-lib/decorators';
|
|
3
|
-
import { _assert
|
|
3
|
+
import { _assert } from '@naturalcycles/js-lib/error';
|
|
4
4
|
import { dimGrey, green, red } from '@naturalcycles/nodejs-lib/colors';
|
|
5
|
-
const adminInfoDisabled = () => ({
|
|
6
|
-
email: 'authDisabled',
|
|
7
|
-
permissions: [],
|
|
8
|
-
});
|
|
9
5
|
/**
|
|
10
6
|
* Base implementation based on Firebase Auth tokens passed as 'admin_token' cookie.
|
|
11
7
|
*/
|
|
@@ -26,64 +22,11 @@ export class BaseAdminService {
|
|
|
26
22
|
permissions: [],
|
|
27
23
|
};
|
|
28
24
|
}
|
|
29
|
-
/**
|
|
30
|
-
* To be extended.
|
|
31
|
-
*
|
|
32
|
-
* Returns undefined if it's not an Admin.
|
|
33
|
-
* Otherwise returns Set of permissions.
|
|
34
|
-
* Empty array means it IS and Admin, but has no permissions (except being an Admin).
|
|
35
|
-
*/
|
|
36
|
-
async getEmailPermissions(email) {
|
|
37
|
-
if (!email)
|
|
38
|
-
return;
|
|
39
|
-
console.log(`getEmailPermissions (${dimGrey(email)}) returning undefined (please override the implementation)`);
|
|
40
|
-
}
|
|
41
|
-
/**
|
|
42
|
-
* To be extended.
|
|
43
|
-
*/
|
|
44
|
-
// oxlint-disable-next-line max-params
|
|
45
|
-
async onPermissionCheck(req, email, reqPermissions, required, granted, meta = {}) {
|
|
46
|
-
req.log(`${dimGrey(email)} ${required ? 'required' : 'optional'} permissions check [${dimGrey(reqPermissions.join(', '))}]: ${granted ? green('GRANTED') : red('DENIED')}`, meta);
|
|
47
|
-
}
|
|
48
|
-
async getEmailByToken(req, adminToken) {
|
|
49
|
-
if (!adminToken)
|
|
50
|
-
return;
|
|
51
|
-
try {
|
|
52
|
-
const auth = await this.getFirebaseAuth();
|
|
53
|
-
const decodedToken = await auth.verifyIdToken(adminToken);
|
|
54
|
-
const email = decodedToken?.email;
|
|
55
|
-
req.log(`admin email: ${dimGrey(email)}`);
|
|
56
|
-
return email;
|
|
57
|
-
}
|
|
58
|
-
catch (err) {
|
|
59
|
-
// example:
|
|
60
|
-
// FirebaseAuthError: Firebase ID token has expired. Get a fresh ID token from your client app and try again (auth/id-token-expired).
|
|
61
|
-
if (
|
|
62
|
-
// err instanceof FirebaseAuthError && err.hasCode('id-token-expired')
|
|
63
|
-
err?.code?.includes('id-token-expired')) {
|
|
64
|
-
return; // skip logging, expected error
|
|
65
|
-
}
|
|
66
|
-
req.error(`getEmailByToken error:`, err);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
async getFirebaseAuth() {
|
|
70
|
-
return await this.loadFirebaseAuth();
|
|
71
|
-
}
|
|
72
|
-
/**
|
|
73
|
-
* Current implementation is based on req=Request (from Express).
|
|
74
|
-
* Override if needed.
|
|
75
|
-
*/
|
|
76
|
-
getAdminToken(req) {
|
|
77
|
-
return (req.cookies?.[this.cfg.adminTokenKey] ||
|
|
78
|
-
req.header(this.cfg.adminTokenKey) ||
|
|
79
|
-
req.header('x-admin-token'));
|
|
80
|
-
}
|
|
81
25
|
async isAdmin(req) {
|
|
82
26
|
if (!req)
|
|
83
27
|
return false;
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
return !!(await this.getEmailPermissions(email));
|
|
28
|
+
const { permissions } = await this.resolveAdmin(req);
|
|
29
|
+
return !!permissions;
|
|
87
30
|
}
|
|
88
31
|
async getAdminInfo(req) {
|
|
89
32
|
return await this.hasPermissions(req);
|
|
@@ -92,6 +35,14 @@ export class BaseAdminService {
|
|
|
92
35
|
// async reqAdmin (req: Request): Promise<void> {
|
|
93
36
|
// await this.reqPermissions(req)
|
|
94
37
|
// }
|
|
38
|
+
// convenience method
|
|
39
|
+
async hasPermission(req, reqPermission, meta) {
|
|
40
|
+
const adminInfo = await this.hasPermissions(req, [reqPermission], meta);
|
|
41
|
+
return !!adminInfo;
|
|
42
|
+
}
|
|
43
|
+
async requirePermission(req, reqPermission, meta) {
|
|
44
|
+
return await this.requirePermissions(req, [reqPermission], meta);
|
|
45
|
+
}
|
|
95
46
|
/**
|
|
96
47
|
* Returns AdminInfo if it has all required permissions.
|
|
97
48
|
* Otherwise returns undefined
|
|
@@ -99,70 +50,148 @@ export class BaseAdminService {
|
|
|
99
50
|
async hasPermissions(req, reqPermissions = [], meta = {}) {
|
|
100
51
|
if (!this.cfg.authEnabled)
|
|
101
52
|
return adminInfoDisabled();
|
|
102
|
-
const
|
|
103
|
-
const email
|
|
104
|
-
|
|
105
|
-
if (!hasPermissions)
|
|
53
|
+
const admin = await this.resolveAdmin(req);
|
|
54
|
+
const { email, permissions } = admin;
|
|
55
|
+
if (!email || !permissions)
|
|
106
56
|
return;
|
|
107
|
-
const
|
|
108
|
-
void this.onPermissionCheck(req, email, reqPermissions, false, granted, meta);
|
|
109
|
-
if (!granted)
|
|
57
|
+
const result = this.checkPermissions(admin, reqPermissions);
|
|
58
|
+
void this.onPermissionCheck(req, email, reqPermissions, false, result.granted, meta);
|
|
59
|
+
if (!result.granted)
|
|
110
60
|
return;
|
|
111
61
|
return {
|
|
112
|
-
email
|
|
113
|
-
permissions: Array.from(
|
|
62
|
+
email,
|
|
63
|
+
permissions: Array.from(permissions),
|
|
114
64
|
};
|
|
115
65
|
}
|
|
116
66
|
async requirePermissions(req, reqPermissions = [], meta = {}, andComparison = true) {
|
|
117
67
|
if (!this.cfg.authEnabled)
|
|
118
68
|
return adminInfoDisabled();
|
|
69
|
+
const admin = await this.resolveAdmin(req);
|
|
70
|
+
const result = this.checkPermissions(admin, reqPermissions, { andComparison });
|
|
71
|
+
if (result.email) {
|
|
72
|
+
// Log only the granted permissions (differs from reqPermissions only on OR-comparison)
|
|
73
|
+
const checkedPermissions = result.granted ? result.grantedPermissions : reqPermissions;
|
|
74
|
+
void this.onPermissionCheck(req, result.email, checkedPermissions, true, result.granted, meta);
|
|
75
|
+
}
|
|
76
|
+
return this.requireGranted(result);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Doesn't check permissions, nor `authEnabled`. See `checkPermissions`.
|
|
80
|
+
*/
|
|
81
|
+
async resolveAdmin(req) {
|
|
119
82
|
const adminToken = this.getAdminToken(req);
|
|
120
83
|
const email = await this.getEmailByToken(req, adminToken);
|
|
121
|
-
if (!email)
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
84
|
+
if (!email)
|
|
85
|
+
return {};
|
|
86
|
+
const permissions = await this.getEmailPermissions(email);
|
|
87
|
+
return { email, permissions };
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Doesn't log or throw. See `requireGranted`.
|
|
91
|
+
*/
|
|
92
|
+
checkPermissions(admin, reqPermissions = [], opt = {}) {
|
|
93
|
+
const { andComparison = true } = opt;
|
|
94
|
+
const { email, permissions: hasPermissions } = admin;
|
|
95
|
+
const isAdmin = !!hasPermissions;
|
|
96
|
+
if (!this.cfg.authEnabled) {
|
|
97
|
+
return {
|
|
98
|
+
email,
|
|
99
|
+
isAdmin,
|
|
100
|
+
granted: true,
|
|
101
|
+
reqPermissions,
|
|
102
|
+
grantedPermissions: [],
|
|
103
|
+
authDisabled: true,
|
|
104
|
+
};
|
|
127
105
|
}
|
|
128
|
-
const hasPermissions = await this.getEmailPermissions(email);
|
|
129
106
|
const grantedPermissions = hasPermissions
|
|
130
107
|
? reqPermissions.filter(p => hasPermissions.has(p))
|
|
131
108
|
: [];
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
granted
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
109
|
+
const granted = andComparison
|
|
110
|
+
? isAdmin && grantedPermissions.length === reqPermissions.length // All permissions granted
|
|
111
|
+
: isAdmin && grantedPermissions.length > 0; // 1+ is required
|
|
112
|
+
return {
|
|
113
|
+
email,
|
|
114
|
+
isAdmin,
|
|
115
|
+
granted,
|
|
116
|
+
reqPermissions,
|
|
117
|
+
grantedPermissions,
|
|
118
|
+
authDisabled: false,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Throws 401 if there's no email, 403 if not granted.
|
|
123
|
+
*/
|
|
124
|
+
requireGranted(result) {
|
|
125
|
+
if (result.authDisabled)
|
|
126
|
+
return adminInfoDisabled();
|
|
127
|
+
const { email, granted, reqPermissions, grantedPermissions } = result;
|
|
128
|
+
_assert(email, 'adminToken required', {
|
|
129
|
+
adminAuthRequired: true,
|
|
130
|
+
backendResponseStatusCode: 401,
|
|
131
|
+
userFriendly: true,
|
|
132
|
+
});
|
|
133
|
+
_assert(granted, `Admin permissions required: [${reqPermissions.join(', ')}]`, {
|
|
134
|
+
adminPermissionsRequired: reqPermissions,
|
|
135
|
+
email,
|
|
136
|
+
backendResponseStatusCode: 403,
|
|
137
|
+
userFriendly: true,
|
|
138
|
+
});
|
|
155
139
|
return {
|
|
156
140
|
email,
|
|
157
141
|
permissions: grantedPermissions.slice(),
|
|
158
142
|
};
|
|
159
143
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
144
|
+
/**
|
|
145
|
+
* Current implementation is based on req=Request (from Express).
|
|
146
|
+
* Override if needed.
|
|
147
|
+
*/
|
|
148
|
+
getAdminToken(req) {
|
|
149
|
+
return (req.cookies?.[this.cfg.adminTokenKey] ||
|
|
150
|
+
req.header(this.cfg.adminTokenKey) ||
|
|
151
|
+
req.header('x-admin-token'));
|
|
163
152
|
}
|
|
164
|
-
async
|
|
165
|
-
|
|
153
|
+
async getEmailByToken(req, adminToken) {
|
|
154
|
+
if (!adminToken)
|
|
155
|
+
return;
|
|
156
|
+
try {
|
|
157
|
+
const auth = await this.getFirebaseAuth();
|
|
158
|
+
const decodedToken = await auth.verifyIdToken(adminToken);
|
|
159
|
+
const email = decodedToken?.email;
|
|
160
|
+
req.log(`admin email: ${dimGrey(email)}`);
|
|
161
|
+
return email;
|
|
162
|
+
}
|
|
163
|
+
catch (err) {
|
|
164
|
+
// example:
|
|
165
|
+
// FirebaseAuthError: Firebase ID token has expired. Get a fresh ID token from your client app and try again (auth/id-token-expired).
|
|
166
|
+
if (
|
|
167
|
+
// err instanceof FirebaseAuthError && err.hasCode('id-token-expired')
|
|
168
|
+
err?.code?.includes('id-token-expired')) {
|
|
169
|
+
return; // skip logging, expected error
|
|
170
|
+
}
|
|
171
|
+
req.error(`getEmailByToken error:`, err);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
async getFirebaseAuth() {
|
|
175
|
+
return await this.loadFirebaseAuth();
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* To be extended.
|
|
179
|
+
*
|
|
180
|
+
* Returns undefined if it's not an Admin.
|
|
181
|
+
* Otherwise returns Set of permissions.
|
|
182
|
+
* Empty array means it IS and Admin, but has no permissions (except being an Admin).
|
|
183
|
+
*/
|
|
184
|
+
async getEmailPermissions(email) {
|
|
185
|
+
if (!email)
|
|
186
|
+
return;
|
|
187
|
+
console.log(`getEmailPermissions (${dimGrey(email)}) returning undefined (please override the implementation)`);
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* To be extended.
|
|
191
|
+
*/
|
|
192
|
+
// oxlint-disable-next-line max-params
|
|
193
|
+
async onPermissionCheck(req, email, reqPermissions, required, granted, meta = {}) {
|
|
194
|
+
req.log(`${dimGrey(email)} ${required ? 'required' : 'optional'} permissions check [${dimGrey(reqPermissions.join(', '))}]: ${granted ? green('GRANTED') : red('DENIED')}`, meta);
|
|
166
195
|
}
|
|
167
196
|
/**
|
|
168
197
|
* Install it on POST /admin/login url
|
|
@@ -202,3 +231,7 @@ export class BaseAdminService {
|
|
|
202
231
|
__decorate([
|
|
203
232
|
_Memo()
|
|
204
233
|
], BaseAdminService.prototype, "getFirebaseAuth", null);
|
|
234
|
+
const adminInfoDisabled = () => ({
|
|
235
|
+
email: 'authDisabled',
|
|
236
|
+
permissions: [],
|
|
237
|
+
});
|
|
@@ -63,7 +63,7 @@ export class BackendServer {
|
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
65
|
const cpus = os.cpus().length;
|
|
66
|
-
const availableParallelism = os.availableParallelism
|
|
66
|
+
const availableParallelism = os.availableParallelism();
|
|
67
67
|
const { version, platform, arch } = process;
|
|
68
68
|
console.log(dimGrey(`node ${version} ${platform} ${arch}, NODE_OPTIONS: ${NODE_OPTIONS || 'undefined'}, APP_ENV: ${APP_ENV || 'undefined'}, cpus: ${cpus}, availableParallelism: ${availableParallelism}`));
|
|
69
69
|
console.log(`serverStarted on ${white(address)} in ${dimGrey(_ms(process.uptime() * 1000))}`);
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import os from 'node:os';
|
|
1
2
|
import { localTime } from '@naturalcycles/js-lib/datetime/localTime.js';
|
|
2
3
|
import { _filterNullishValues } from '@naturalcycles/js-lib/object/object.util.js';
|
|
3
4
|
import { memoryUsageFull, processSharedUtil } from '@naturalcycles/nodejs-lib';
|
|
4
5
|
import { getDeployInfo } from '../deploy/deployInfo.util.js';
|
|
5
6
|
const { versions, arch, platform } = process;
|
|
6
|
-
const
|
|
7
|
+
const availableParallelismAtStart = os.availableParallelism();
|
|
8
|
+
const { GAE_APPLICATION, GAE_SERVICE, GAE_VERSION, GOOGLE_CLOUD_PROJECT, K_SERVICE, K_REVISION, APP_ENV, NODE_ENV, NODE_OPTIONS, UV_THREADPOOL_SIZE, MALLOC_ARENA_MAX, DEPLOY_BUILD_TIME, BUILD_VERSION, } = process.env;
|
|
7
9
|
export function serverStatusMiddleware(projectDir, extra) {
|
|
8
10
|
return async (_req, res) => {
|
|
9
11
|
res.json(getServerStatusData(projectDir, extra));
|
|
@@ -15,6 +17,7 @@ export function getServerStatusData(projectDir = process.cwd(), extra) {
|
|
|
15
17
|
const { ts } = getDeployInfo(projectDir);
|
|
16
18
|
deployBuildTime = localTime(ts).toPretty();
|
|
17
19
|
}
|
|
20
|
+
const availableParallelism = os.availableParallelism();
|
|
18
21
|
return _filterNullishValues({
|
|
19
22
|
nodeProcessStarted: getStartedStr(),
|
|
20
23
|
deployBuildTime,
|
|
@@ -33,10 +36,13 @@ export function getServerStatusData(projectDir = process.cwd(), extra) {
|
|
|
33
36
|
mem: memoryUsageFull(),
|
|
34
37
|
cpuAvg: processSharedUtil.cpuAvg(),
|
|
35
38
|
cpuInfo: processSharedUtil.cpuInfo(),
|
|
39
|
+
availableParallelismAtStart,
|
|
40
|
+
availableParallelism,
|
|
36
41
|
versions,
|
|
37
42
|
NODE_OPTIONS,
|
|
38
43
|
NODE_ENV,
|
|
39
44
|
UV_THREADPOOL_SIZE,
|
|
45
|
+
MALLOC_ARENA_MAX,
|
|
40
46
|
...extra,
|
|
41
47
|
});
|
|
42
48
|
}
|
package/package.json
CHANGED
|
@@ -1,33 +1,10 @@
|
|
|
1
1
|
import { _Memo } from '@naturalcycles/js-lib/decorators'
|
|
2
|
-
import { _assert
|
|
2
|
+
import { _assert } from '@naturalcycles/js-lib/error'
|
|
3
|
+
import type { AnyObject } from '@naturalcycles/js-lib/types'
|
|
3
4
|
import { dimGrey, green, red } from '@naturalcycles/nodejs-lib/colors'
|
|
4
5
|
import type { Auth } from 'firebase-admin/auth'
|
|
5
6
|
import type { BackendRequest, BackendRequestHandler } from '../server/server.model.js'
|
|
6
7
|
|
|
7
|
-
export interface AdminServiceCfg {
|
|
8
|
-
/**
|
|
9
|
-
* @default 'admin_token'
|
|
10
|
-
*/
|
|
11
|
-
adminTokenKey?: string
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* If false - disables auth completely (useful for debugging locally, but never in production).
|
|
15
|
-
*
|
|
16
|
-
* @default true
|
|
17
|
-
*/
|
|
18
|
-
authEnabled?: boolean
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export interface AdminInfo {
|
|
22
|
-
email: string
|
|
23
|
-
permissions: string[]
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const adminInfoDisabled = (): AdminInfo => ({
|
|
27
|
-
email: 'authDisabled',
|
|
28
|
-
permissions: [],
|
|
29
|
-
})
|
|
30
|
-
|
|
31
8
|
/**
|
|
32
9
|
* Base implementation based on Firebase Auth tokens passed as 'admin_token' cookie.
|
|
33
10
|
*/
|
|
@@ -52,87 +29,10 @@ export class BaseAdminService {
|
|
|
52
29
|
}
|
|
53
30
|
}
|
|
54
31
|
|
|
55
|
-
/**
|
|
56
|
-
* To be extended.
|
|
57
|
-
*
|
|
58
|
-
* Returns undefined if it's not an Admin.
|
|
59
|
-
* Otherwise returns Set of permissions.
|
|
60
|
-
* Empty array means it IS and Admin, but has no permissions (except being an Admin).
|
|
61
|
-
*/
|
|
62
|
-
async getEmailPermissions(email?: string): Promise<Set<string> | undefined> {
|
|
63
|
-
if (!email) return
|
|
64
|
-
console.log(
|
|
65
|
-
`getEmailPermissions (${dimGrey(
|
|
66
|
-
email,
|
|
67
|
-
)}) returning undefined (please override the implementation)`,
|
|
68
|
-
)
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* To be extended.
|
|
73
|
-
*/
|
|
74
|
-
// oxlint-disable-next-line max-params
|
|
75
|
-
protected async onPermissionCheck(
|
|
76
|
-
req: BackendRequest,
|
|
77
|
-
email: string,
|
|
78
|
-
reqPermissions: string[],
|
|
79
|
-
required: boolean,
|
|
80
|
-
granted: boolean,
|
|
81
|
-
meta: Record<string, any> = {},
|
|
82
|
-
): Promise<void> {
|
|
83
|
-
req.log(
|
|
84
|
-
`${dimGrey(email)} ${required ? 'required' : 'optional'} permissions check [${dimGrey(
|
|
85
|
-
reqPermissions.join(', '),
|
|
86
|
-
)}]: ${granted ? green('GRANTED') : red('DENIED')}`,
|
|
87
|
-
meta,
|
|
88
|
-
)
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
async getEmailByToken(req: BackendRequest, adminToken?: string): Promise<string | undefined> {
|
|
92
|
-
if (!adminToken) return
|
|
93
|
-
|
|
94
|
-
try {
|
|
95
|
-
const auth = await this.getFirebaseAuth()
|
|
96
|
-
const decodedToken = await auth.verifyIdToken(adminToken)
|
|
97
|
-
const email = decodedToken?.email
|
|
98
|
-
req.log(`admin email: ${dimGrey(email)}`)
|
|
99
|
-
return email
|
|
100
|
-
} catch (err) {
|
|
101
|
-
// example:
|
|
102
|
-
// FirebaseAuthError: Firebase ID token has expired. Get a fresh ID token from your client app and try again (auth/id-token-expired).
|
|
103
|
-
if (
|
|
104
|
-
// err instanceof FirebaseAuthError && err.hasCode('id-token-expired')
|
|
105
|
-
(err as any)?.code?.includes('id-token-expired')
|
|
106
|
-
) {
|
|
107
|
-
return // skip logging, expected error
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
req.error(`getEmailByToken error:`, err)
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
@_Memo()
|
|
115
|
-
private async getFirebaseAuth(): Promise<Auth> {
|
|
116
|
-
return await this.loadFirebaseAuth()
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Current implementation is based on req=Request (from Express).
|
|
121
|
-
* Override if needed.
|
|
122
|
-
*/
|
|
123
|
-
getAdminToken(req: BackendRequest): string | undefined {
|
|
124
|
-
return (
|
|
125
|
-
req.cookies?.[this.cfg.adminTokenKey] ||
|
|
126
|
-
req.header(this.cfg.adminTokenKey) ||
|
|
127
|
-
req.header('x-admin-token')
|
|
128
|
-
)
|
|
129
|
-
}
|
|
130
|
-
|
|
131
32
|
async isAdmin(req: BackendRequest | undefined): Promise<boolean> {
|
|
132
33
|
if (!req) return false
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
return !!(await this.getEmailPermissions(email))
|
|
34
|
+
const { permissions } = await this.resolveAdmin(req)
|
|
35
|
+
return !!permissions
|
|
136
36
|
}
|
|
137
37
|
|
|
138
38
|
async getAdminInfo(req: BackendRequest): Promise<AdminInfo | undefined> {
|
|
@@ -144,6 +44,24 @@ export class BaseAdminService {
|
|
|
144
44
|
// await this.reqPermissions(req)
|
|
145
45
|
// }
|
|
146
46
|
|
|
47
|
+
// convenience method
|
|
48
|
+
async hasPermission(
|
|
49
|
+
req: BackendRequest,
|
|
50
|
+
reqPermission: string,
|
|
51
|
+
meta?: AnyObject,
|
|
52
|
+
): Promise<boolean> {
|
|
53
|
+
const adminInfo = await this.hasPermissions(req, [reqPermission], meta)
|
|
54
|
+
return !!adminInfo
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async requirePermission(
|
|
58
|
+
req: BackendRequest,
|
|
59
|
+
reqPermission: string,
|
|
60
|
+
meta?: AnyObject,
|
|
61
|
+
): Promise<AdminInfo> {
|
|
62
|
+
return await this.requirePermissions(req, [reqPermission], meta)
|
|
63
|
+
}
|
|
64
|
+
|
|
147
65
|
/**
|
|
148
66
|
* Returns AdminInfo if it has all required permissions.
|
|
149
67
|
* Otherwise returns undefined
|
|
@@ -151,73 +69,119 @@ export class BaseAdminService {
|
|
|
151
69
|
async hasPermissions(
|
|
152
70
|
req: BackendRequest,
|
|
153
71
|
reqPermissions: string[] = [],
|
|
154
|
-
meta:
|
|
72
|
+
meta: AnyObject = {},
|
|
155
73
|
): Promise<AdminInfo | undefined> {
|
|
156
74
|
if (!this.cfg.authEnabled) return adminInfoDisabled()
|
|
157
75
|
|
|
158
|
-
const
|
|
159
|
-
const email
|
|
160
|
-
|
|
161
|
-
if (!hasPermissions) return
|
|
76
|
+
const admin = await this.resolveAdmin(req)
|
|
77
|
+
const { email, permissions } = admin
|
|
78
|
+
if (!email || !permissions) return
|
|
162
79
|
|
|
163
|
-
const
|
|
80
|
+
const result = this.checkPermissions(admin, reqPermissions)
|
|
164
81
|
|
|
165
|
-
void this.onPermissionCheck(req, email
|
|
82
|
+
void this.onPermissionCheck(req, email, reqPermissions, false, result.granted, meta)
|
|
166
83
|
|
|
167
|
-
if (!granted) return
|
|
84
|
+
if (!result.granted) return
|
|
168
85
|
|
|
169
86
|
return {
|
|
170
|
-
email
|
|
171
|
-
permissions: Array.from(
|
|
87
|
+
email,
|
|
88
|
+
permissions: Array.from(permissions),
|
|
172
89
|
}
|
|
173
90
|
}
|
|
174
91
|
|
|
175
92
|
async requirePermissions(
|
|
176
93
|
req: BackendRequest,
|
|
177
94
|
reqPermissions: string[] = [],
|
|
178
|
-
meta:
|
|
95
|
+
meta: AnyObject = {},
|
|
179
96
|
andComparison = true,
|
|
180
97
|
): Promise<AdminInfo> {
|
|
181
98
|
if (!this.cfg.authEnabled) return adminInfoDisabled()
|
|
182
99
|
|
|
100
|
+
const admin = await this.resolveAdmin(req)
|
|
101
|
+
const result = this.checkPermissions(admin, reqPermissions, { andComparison })
|
|
102
|
+
|
|
103
|
+
if (result.email) {
|
|
104
|
+
// Log only the granted permissions (differs from reqPermissions only on OR-comparison)
|
|
105
|
+
const checkedPermissions = result.granted ? result.grantedPermissions : reqPermissions
|
|
106
|
+
void this.onPermissionCheck(req, result.email, checkedPermissions, true, result.granted, meta)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return this.requireGranted(result)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Doesn't check permissions, nor `authEnabled`. See `checkPermissions`.
|
|
114
|
+
*/
|
|
115
|
+
async resolveAdmin(req: BackendRequest): Promise<ResolvedAdmin> {
|
|
183
116
|
const adminToken = this.getAdminToken(req)
|
|
184
117
|
const email = await this.getEmailByToken(req, adminToken)
|
|
118
|
+
if (!email) return {}
|
|
185
119
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
120
|
+
const permissions = await this.getEmailPermissions(email)
|
|
121
|
+
return { email, permissions }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Doesn't log or throw. See `requireGranted`.
|
|
126
|
+
*/
|
|
127
|
+
checkPermissions(
|
|
128
|
+
admin: ResolvedAdmin,
|
|
129
|
+
reqPermissions: string[] = [],
|
|
130
|
+
opt: CheckPermissionsOptions = {},
|
|
131
|
+
): CheckPermissionsResult {
|
|
132
|
+
const { andComparison = true } = opt
|
|
133
|
+
const { email, permissions: hasPermissions } = admin
|
|
134
|
+
const isAdmin = !!hasPermissions
|
|
135
|
+
|
|
136
|
+
if (!this.cfg.authEnabled) {
|
|
137
|
+
return {
|
|
138
|
+
email,
|
|
139
|
+
isAdmin,
|
|
140
|
+
granted: true,
|
|
141
|
+
reqPermissions,
|
|
142
|
+
grantedPermissions: [],
|
|
143
|
+
authDisabled: true,
|
|
144
|
+
}
|
|
192
145
|
}
|
|
193
146
|
|
|
194
|
-
const hasPermissions = await this.getEmailPermissions(email)
|
|
195
147
|
const grantedPermissions = hasPermissions
|
|
196
148
|
? reqPermissions.filter(p => hasPermissions.has(p))
|
|
197
149
|
: []
|
|
198
150
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
void this.onPermissionCheck(req, email, reqPermissions, true, granted, meta)
|
|
203
|
-
} else {
|
|
204
|
-
granted = !!hasPermissions && grantedPermissions.length > 0
|
|
205
|
-
if (granted) {
|
|
206
|
-
// Require the permission(s), but only the ones the user was actually granted. 1+ is required
|
|
207
|
-
void this.onPermissionCheck(req, email, grantedPermissions, true, granted, meta)
|
|
208
|
-
} else {
|
|
209
|
-
void this.onPermissionCheck(req, email, reqPermissions, true, granted, meta)
|
|
210
|
-
}
|
|
211
|
-
}
|
|
151
|
+
const granted = andComparison
|
|
152
|
+
? isAdmin && grantedPermissions.length === reqPermissions.length // All permissions granted
|
|
153
|
+
: isAdmin && grantedPermissions.length > 0 // 1+ is required
|
|
212
154
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
155
|
+
return {
|
|
156
|
+
email,
|
|
157
|
+
isAdmin,
|
|
158
|
+
granted,
|
|
159
|
+
reqPermissions,
|
|
160
|
+
grantedPermissions,
|
|
161
|
+
authDisabled: false,
|
|
220
162
|
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Throws 401 if there's no email, 403 if not granted.
|
|
167
|
+
*/
|
|
168
|
+
requireGranted(result: CheckPermissionsResult): AdminInfo {
|
|
169
|
+
if (result.authDisabled) return adminInfoDisabled()
|
|
170
|
+
|
|
171
|
+
const { email, granted, reqPermissions, grantedPermissions } = result
|
|
172
|
+
|
|
173
|
+
_assert(email, 'adminToken required', {
|
|
174
|
+
adminAuthRequired: true,
|
|
175
|
+
backendResponseStatusCode: 401,
|
|
176
|
+
userFriendly: true,
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
_assert(granted, `Admin permissions required: [${reqPermissions.join(', ')}]`, {
|
|
180
|
+
adminPermissionsRequired: reqPermissions,
|
|
181
|
+
email,
|
|
182
|
+
backendResponseStatusCode: 403,
|
|
183
|
+
userFriendly: true,
|
|
184
|
+
})
|
|
221
185
|
|
|
222
186
|
return {
|
|
223
187
|
email,
|
|
@@ -225,21 +189,80 @@ export class BaseAdminService {
|
|
|
225
189
|
}
|
|
226
190
|
}
|
|
227
191
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
192
|
+
/**
|
|
193
|
+
* Current implementation is based on req=Request (from Express).
|
|
194
|
+
* Override if needed.
|
|
195
|
+
*/
|
|
196
|
+
getAdminToken(req: BackendRequest): string | undefined {
|
|
197
|
+
return (
|
|
198
|
+
req.cookies?.[this.cfg.adminTokenKey] ||
|
|
199
|
+
req.header(this.cfg.adminTokenKey) ||
|
|
200
|
+
req.header('x-admin-token')
|
|
201
|
+
)
|
|
235
202
|
}
|
|
236
203
|
|
|
237
|
-
async
|
|
204
|
+
async getEmailByToken(req: BackendRequest, adminToken?: string): Promise<string | undefined> {
|
|
205
|
+
if (!adminToken) return
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
const auth = await this.getFirebaseAuth()
|
|
209
|
+
const decodedToken = await auth.verifyIdToken(adminToken)
|
|
210
|
+
const email = decodedToken?.email
|
|
211
|
+
req.log(`admin email: ${dimGrey(email)}`)
|
|
212
|
+
return email
|
|
213
|
+
} catch (err) {
|
|
214
|
+
// example:
|
|
215
|
+
// FirebaseAuthError: Firebase ID token has expired. Get a fresh ID token from your client app and try again (auth/id-token-expired).
|
|
216
|
+
if (
|
|
217
|
+
// err instanceof FirebaseAuthError && err.hasCode('id-token-expired')
|
|
218
|
+
(err as any)?.code?.includes('id-token-expired')
|
|
219
|
+
) {
|
|
220
|
+
return // skip logging, expected error
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
req.error(`getEmailByToken error:`, err)
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
@_Memo()
|
|
228
|
+
private async getFirebaseAuth(): Promise<Auth> {
|
|
229
|
+
return await this.loadFirebaseAuth()
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* To be extended.
|
|
234
|
+
*
|
|
235
|
+
* Returns undefined if it's not an Admin.
|
|
236
|
+
* Otherwise returns Set of permissions.
|
|
237
|
+
* Empty array means it IS and Admin, but has no permissions (except being an Admin).
|
|
238
|
+
*/
|
|
239
|
+
async getEmailPermissions(email?: string): Promise<Set<string> | undefined> {
|
|
240
|
+
if (!email) return
|
|
241
|
+
console.log(
|
|
242
|
+
`getEmailPermissions (${dimGrey(
|
|
243
|
+
email,
|
|
244
|
+
)}) returning undefined (please override the implementation)`,
|
|
245
|
+
)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* To be extended.
|
|
250
|
+
*/
|
|
251
|
+
// oxlint-disable-next-line max-params
|
|
252
|
+
protected async onPermissionCheck(
|
|
238
253
|
req: BackendRequest,
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
254
|
+
email: string,
|
|
255
|
+
reqPermissions: string[],
|
|
256
|
+
required: boolean,
|
|
257
|
+
granted: boolean,
|
|
258
|
+
meta: AnyObject = {},
|
|
259
|
+
): Promise<void> {
|
|
260
|
+
req.log(
|
|
261
|
+
`${dimGrey(email)} ${required ? 'required' : 'optional'} permissions check [${dimGrey(
|
|
262
|
+
reqPermissions.join(', '),
|
|
263
|
+
)}]: ${granted ? green('GRANTED') : red('DENIED')}`,
|
|
264
|
+
meta,
|
|
265
|
+
)
|
|
243
266
|
}
|
|
244
267
|
|
|
245
268
|
/**
|
|
@@ -280,3 +303,53 @@ export class BaseAdminService {
|
|
|
280
303
|
}
|
|
281
304
|
}
|
|
282
305
|
}
|
|
306
|
+
|
|
307
|
+
const adminInfoDisabled = (): AdminInfo => ({
|
|
308
|
+
email: 'authDisabled',
|
|
309
|
+
permissions: [],
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
export interface AdminServiceCfg {
|
|
313
|
+
/**
|
|
314
|
+
* @default 'admin_token'
|
|
315
|
+
*/
|
|
316
|
+
adminTokenKey?: string
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* If false - disables auth completely (useful for debugging locally, but never in production).
|
|
320
|
+
*
|
|
321
|
+
* @default true
|
|
322
|
+
*/
|
|
323
|
+
authEnabled?: boolean
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export interface AdminInfo {
|
|
327
|
+
email: string
|
|
328
|
+
permissions: string[]
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
export interface ResolvedAdmin {
|
|
332
|
+
/** undefined - no valid admin token */
|
|
333
|
+
email?: string
|
|
334
|
+
/** undefined - not an Admin */
|
|
335
|
+
permissions?: Set<string>
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
export interface CheckPermissionsOptions {
|
|
339
|
+
/**
|
|
340
|
+
* false - one granted permission is enough.
|
|
341
|
+
*
|
|
342
|
+
* @default true
|
|
343
|
+
*/
|
|
344
|
+
andComparison?: boolean
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export interface CheckPermissionsResult {
|
|
348
|
+
/** undefined - no valid admin token */
|
|
349
|
+
email?: string
|
|
350
|
+
isAdmin: boolean
|
|
351
|
+
granted: boolean
|
|
352
|
+
reqPermissions: string[]
|
|
353
|
+
grantedPermissions: string[]
|
|
354
|
+
authDisabled: boolean
|
|
355
|
+
}
|
|
@@ -73,7 +73,7 @@ export class BackendServer {
|
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
const cpus = os.cpus().length
|
|
76
|
-
const availableParallelism = os.availableParallelism
|
|
76
|
+
const availableParallelism = os.availableParallelism()
|
|
77
77
|
const { version, platform, arch } = process
|
|
78
78
|
console.log(
|
|
79
79
|
dimGrey(
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import os from 'node:os'
|
|
1
2
|
import { localTime } from '@naturalcycles/js-lib/datetime/localTime.js'
|
|
2
3
|
import { _filterNullishValues } from '@naturalcycles/js-lib/object/object.util.js'
|
|
3
4
|
import { memoryUsageFull, processSharedUtil } from '@naturalcycles/nodejs-lib'
|
|
@@ -5,6 +6,7 @@ import { getDeployInfo } from '../deploy/deployInfo.util.js'
|
|
|
5
6
|
import type { BackendRequestHandler } from './server.model.js'
|
|
6
7
|
|
|
7
8
|
const { versions, arch, platform } = process
|
|
9
|
+
const availableParallelismAtStart = os.availableParallelism()
|
|
8
10
|
const {
|
|
9
11
|
GAE_APPLICATION,
|
|
10
12
|
GAE_SERVICE,
|
|
@@ -16,6 +18,7 @@ const {
|
|
|
16
18
|
NODE_ENV,
|
|
17
19
|
NODE_OPTIONS,
|
|
18
20
|
UV_THREADPOOL_SIZE,
|
|
21
|
+
MALLOC_ARENA_MAX,
|
|
19
22
|
DEPLOY_BUILD_TIME,
|
|
20
23
|
BUILD_VERSION,
|
|
21
24
|
} = process.env
|
|
@@ -36,6 +39,8 @@ export function getServerStatusData(
|
|
|
36
39
|
deployBuildTime = localTime(ts).toPretty()
|
|
37
40
|
}
|
|
38
41
|
|
|
42
|
+
const availableParallelism = os.availableParallelism()
|
|
43
|
+
|
|
39
44
|
return _filterNullishValues({
|
|
40
45
|
nodeProcessStarted: getStartedStr(),
|
|
41
46
|
deployBuildTime,
|
|
@@ -54,10 +59,13 @@ export function getServerStatusData(
|
|
|
54
59
|
mem: memoryUsageFull(),
|
|
55
60
|
cpuAvg: processSharedUtil.cpuAvg(),
|
|
56
61
|
cpuInfo: processSharedUtil.cpuInfo(),
|
|
62
|
+
availableParallelismAtStart,
|
|
63
|
+
availableParallelism,
|
|
57
64
|
versions,
|
|
58
65
|
NODE_OPTIONS,
|
|
59
66
|
NODE_ENV,
|
|
60
67
|
UV_THREADPOOL_SIZE,
|
|
68
|
+
MALLOC_ARENA_MAX,
|
|
61
69
|
...extra,
|
|
62
70
|
})
|
|
63
71
|
}
|