@naturalcycles/backend-lib 2.60.1 → 2.61.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/CHANGELOG.md +28 -0
- package/dist/admin/admin.mw.d.ts +5 -0
- package/dist/admin/admin.mw.js +2 -2
- package/dist/admin/base.admin.service.d.ts +11 -1
- package/dist/admin/base.admin.service.js +34 -0
- package/dist/admin/login.html +57 -44
- package/dist/db/httpDB.d.ts +2 -2
- package/dist/db/httpDBRequestHandler.d.ts +2 -1
- package/dist/index.d.ts +0 -10
- package/dist/server/handlers/reqValidation.mw.js +1 -0
- package/dist/server/handlers/statusHandler.d.ts +2 -7
- package/dist/server/handlers/statusHandler.js +10 -12
- package/dist/server/startServer.js +16 -12
- package/dist/server/startServer.model.d.ts +4 -9
- package/package.json +2 -2
- package/src/admin/admin.mw.ts +10 -4
- package/src/admin/base.admin.service.ts +40 -2
- package/src/admin/login.html +57 -44
- package/src/db/httpDB.ts +1 -2
- package/src/db/httpDBRequestHandler.ts +1 -1
- package/src/index.ts +0 -11
- package/src/server/handlers/reqValidation.mw.ts +1 -0
- package/src/server/handlers/statusHandler.ts +9 -20
- package/src/server/startServer.model.ts +4 -10
- package/src/server/startServer.ts +16 -16
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,31 @@
|
|
|
1
|
+
# [2.61.0](https://github.com/NaturalCycles/backend-lib/compare/v2.60.4...v2.61.0) (2021-10-23)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* clarify startServer cfg, use process.uptime() ([9185603](https://github.com/NaturalCycles/backend-lib/commit/9185603bec23f5bcccfe764125d8af1efcfc3c11))
|
|
7
|
+
|
|
8
|
+
## [2.60.4](https://github.com/NaturalCycles/backend-lib/compare/v2.60.3...v2.60.4) (2021-10-21)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* deps, use process.uptime in statusHandler ([8d3389d](https://github.com/NaturalCycles/backend-lib/commit/8d3389d8908954af9a375a6d7fb95aefc1b08e5f))
|
|
14
|
+
|
|
15
|
+
## [2.60.3](https://github.com/NaturalCycles/backend-lib/compare/v2.60.2...v2.60.3) (2021-10-15)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Bug Fixes
|
|
19
|
+
|
|
20
|
+
* deps (firebase-admin@10) ([50fe05b](https://github.com/NaturalCycles/backend-lib/commit/50fe05be92cb4345b809096d3b45b1b099bc2ea5))
|
|
21
|
+
|
|
22
|
+
## [2.60.2](https://github.com/NaturalCycles/backend-lib/compare/v2.60.1...v2.60.2) (2021-10-09)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
### Bug Fixes
|
|
26
|
+
|
|
27
|
+
* login.html by inroducing an `/admin/login` endpoint ([6e16d0f](https://github.com/NaturalCycles/backend-lib/commit/6e16d0fd8f26d35dd562687a29a120b32d9f2717))
|
|
28
|
+
|
|
1
29
|
## [2.60.1](https://github.com/NaturalCycles/backend-lib/compare/v2.60.0...v2.60.1) (2021-10-04)
|
|
2
30
|
|
|
3
31
|
|
package/dist/admin/admin.mw.d.ts
CHANGED
|
@@ -12,6 +12,11 @@ export interface RequireAdminCfg {
|
|
|
12
12
|
*/
|
|
13
13
|
apiHost?: string;
|
|
14
14
|
urlStartsWith?: string;
|
|
15
|
+
/**
|
|
16
|
+
* Defaults to `true`.
|
|
17
|
+
* Set to `false` to debug login issues.
|
|
18
|
+
*/
|
|
19
|
+
autoLogin?: boolean;
|
|
15
20
|
}
|
|
16
21
|
export declare type AdminMiddleware = (reqPermissions?: string[], cfg?: RequireAdminCfg) => RequestHandler;
|
|
17
22
|
export declare function createAdminMiddleware(adminService: BaseAdminService, cfgDefaults?: RequireAdminCfg): AdminMiddleware;
|
package/dist/admin/admin.mw.js
CHANGED
|
@@ -20,7 +20,7 @@ exports.createAdminMiddleware = createAdminMiddleware;
|
|
|
20
20
|
* Otherwise will just pass.
|
|
21
21
|
*/
|
|
22
22
|
function requireAdminPermissions(adminService, reqPermissions = [], cfg = {}) {
|
|
23
|
-
const { loginHtmlPath = '/login.html', urlStartsWith, apiHost } = cfg;
|
|
23
|
+
const { loginHtmlPath = '/login.html', urlStartsWith, apiHost, autoLogin = true } = cfg;
|
|
24
24
|
return async (req, res, next) => {
|
|
25
25
|
if (urlStartsWith && !req.url.startsWith(urlStartsWith))
|
|
26
26
|
return next();
|
|
@@ -31,7 +31,7 @@ function requireAdminPermissions(adminService, reqPermissions = [], cfg = {}) {
|
|
|
31
31
|
catch (err) {
|
|
32
32
|
if (err instanceof js_lib_1.HttpError && err.data.adminAuthRequired) {
|
|
33
33
|
// Redirect to login.html
|
|
34
|
-
const href = `${loginHtmlPath}?autoLogin=1&returnUrl=\${encodeURIComponent(location.href)}${apiHost ? '&apiHost=' + apiHost : ''}`;
|
|
34
|
+
const href = `${loginHtmlPath}?${autoLogin ? 'autoLogin=1&' : ''}returnUrl=\${encodeURIComponent(location.href)}${apiHost ? '&apiHost=' + apiHost : ''}`;
|
|
35
35
|
res.status(401).send(getLoginHtmlRedirect(href));
|
|
36
36
|
}
|
|
37
37
|
else {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Request } from 'express';
|
|
1
|
+
import { Request, RequestHandler } from 'express';
|
|
2
2
|
import type * as FirebaseAdmin from 'firebase-admin';
|
|
3
3
|
export interface AdminServiceCfg {
|
|
4
4
|
/**
|
|
@@ -52,4 +52,14 @@ export declare class BaseAdminService {
|
|
|
52
52
|
requirePermissions(req: Request, reqPermissions?: string[], meta?: Record<string, any>, andComparison?: boolean): Promise<AdminInfo>;
|
|
53
53
|
hasPermission(req: Request, reqPermission: string, meta?: Record<string, any>): Promise<boolean>;
|
|
54
54
|
requirePermission(req: Request, reqPermission: string, meta?: Record<string, any>): Promise<AdminInfo>;
|
|
55
|
+
/**
|
|
56
|
+
* Install it on POST /admin/login url
|
|
57
|
+
*
|
|
58
|
+
* It takes a POST request with `Authentication` header, that contains `accessToken` from Firebase Auth.
|
|
59
|
+
* Backend doesn't validate the token, but only does `setCookie` (secure, httpOnly), returns http 204 (ok, empty response).
|
|
60
|
+
* Frontend (login.html page) will then proceed with redirecting to `returnUrl`.
|
|
61
|
+
*
|
|
62
|
+
* Same endpoint is used to logout, but the `Authentication` header should contain `logout` magic string.
|
|
63
|
+
*/
|
|
64
|
+
getFirebaseAuthLoginHandler(): RequestHandler;
|
|
55
65
|
}
|
|
@@ -153,5 +153,39 @@ class BaseAdminService {
|
|
|
153
153
|
async requirePermission(req, reqPermission, meta) {
|
|
154
154
|
return await this.requirePermissions(req, [reqPermission], meta);
|
|
155
155
|
}
|
|
156
|
+
/**
|
|
157
|
+
* Install it on POST /admin/login url
|
|
158
|
+
*
|
|
159
|
+
* It takes a POST request with `Authentication` header, that contains `accessToken` from Firebase Auth.
|
|
160
|
+
* Backend doesn't validate the token, but only does `setCookie` (secure, httpOnly), returns http 204 (ok, empty response).
|
|
161
|
+
* Frontend (login.html page) will then proceed with redirecting to `returnUrl`.
|
|
162
|
+
*
|
|
163
|
+
* Same endpoint is used to logout, but the `Authentication` header should contain `logout` magic string.
|
|
164
|
+
*/
|
|
165
|
+
getFirebaseAuthLoginHandler() {
|
|
166
|
+
return async (req, res) => {
|
|
167
|
+
const token = req.header('authentication');
|
|
168
|
+
(0, js_lib_1._assert)(token, `401 Unauthenticated`, {
|
|
169
|
+
userFriendly: true,
|
|
170
|
+
httpStatusCode: 401,
|
|
171
|
+
});
|
|
172
|
+
let maxAge = 1000 * 60 * 60 * 24 * 30; // 30 days
|
|
173
|
+
// Special case
|
|
174
|
+
if (token === 'logout') {
|
|
175
|
+
// delete the cookie
|
|
176
|
+
maxAge = 0;
|
|
177
|
+
}
|
|
178
|
+
res
|
|
179
|
+
.cookie(this.cfg.adminTokenKey, token, {
|
|
180
|
+
maxAge,
|
|
181
|
+
sameSite: 'lax',
|
|
182
|
+
// comment these 2 lines to debug on localhost
|
|
183
|
+
httpOnly: true,
|
|
184
|
+
secure: true,
|
|
185
|
+
})
|
|
186
|
+
.status(204)
|
|
187
|
+
.end();
|
|
188
|
+
};
|
|
189
|
+
}
|
|
156
190
|
}
|
|
157
191
|
exports.BaseAdminService = BaseAdminService;
|
package/dist/admin/login.html
CHANGED
|
@@ -6,11 +6,12 @@
|
|
|
6
6
|
<meta charset="utf-8" />
|
|
7
7
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
<
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
<!-- CSS only -->
|
|
10
|
+
<link
|
|
11
|
+
href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.2/dist/css/bootstrap.min.css"
|
|
12
|
+
rel="stylesheet"
|
|
13
|
+
crossorigin="anonymous"
|
|
14
|
+
/>
|
|
14
15
|
</head>
|
|
15
16
|
<body>
|
|
16
17
|
<div id="app" style="padding: 40px 50px">
|
|
@@ -28,33 +29,54 @@
|
|
|
28
29
|
</div>
|
|
29
30
|
</div>
|
|
30
31
|
|
|
31
|
-
<script>
|
|
32
|
+
<script type="module">
|
|
33
|
+
import { createApp } from 'https://cdn.jsdelivr.net/npm/vue@3.2.20/dist/vue.esm-browser.prod.js'
|
|
34
|
+
import { initializeApp } from 'https://www.gstatic.com/firebasejs/9.1.2/firebase-app.js'
|
|
35
|
+
import {
|
|
36
|
+
getAuth,
|
|
37
|
+
GoogleAuthProvider,
|
|
38
|
+
onAuthStateChanged,
|
|
39
|
+
signInWithRedirect,
|
|
40
|
+
} from 'https://www.gstatic.com/firebasejs/9.1.2/firebase-auth.js'
|
|
41
|
+
|
|
32
42
|
const apiKey = '<%= firebaseApiKey %>'
|
|
33
43
|
const authDomain = '<%= firebaseAuthDomain %>'
|
|
34
|
-
const authProvider = '<%= firebaseAuthProvider %>'
|
|
44
|
+
// const authProvider = '<%= firebaseAuthProvider %>'
|
|
35
45
|
|
|
36
46
|
if (!apiKey || !authDomain) {
|
|
37
47
|
alert(`Error: 'apiKey' or 'authDomain' is missing!`)
|
|
38
48
|
}
|
|
39
49
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
50
|
+
// Initialize Firebase
|
|
51
|
+
initializeApp({
|
|
52
|
+
apiKey,
|
|
53
|
+
authDomain,
|
|
54
|
+
})
|
|
43
55
|
|
|
44
|
-
const
|
|
56
|
+
const auth = getAuth()
|
|
57
|
+
const provider = new GoogleAuthProvider()
|
|
45
58
|
|
|
46
|
-
|
|
47
|
-
|
|
59
|
+
onAuthStateChanged(auth, user => {
|
|
60
|
+
// console.log('onAuthStateChanged, user: ', JSON.stringify(user, null, 2))
|
|
61
|
+
console.log('onAuthStateChanged, user: ', user)
|
|
62
|
+
onUser(user)
|
|
63
|
+
})
|
|
48
64
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
},
|
|
65
|
+
const qs = Object.fromEntries(new URLSearchParams(location.search))
|
|
66
|
+
const { autoLogin, logout, returnUrl, adminTokenKey = 'admin_token' } = qs
|
|
67
|
+
console.log(qs)
|
|
53
68
|
|
|
69
|
+
const app = createApp({
|
|
70
|
+
data() {
|
|
71
|
+
return {
|
|
72
|
+
loading: 'Loading...',
|
|
73
|
+
user: undefined,
|
|
74
|
+
}
|
|
75
|
+
},
|
|
54
76
|
methods: {
|
|
55
77
|
login: async function () {
|
|
56
78
|
try {
|
|
57
|
-
await
|
|
79
|
+
await signInWithRedirect(auth, provider)
|
|
58
80
|
} catch (err) {
|
|
59
81
|
logError(err)
|
|
60
82
|
}
|
|
@@ -62,7 +84,9 @@
|
|
|
62
84
|
|
|
63
85
|
logout: async function () {
|
|
64
86
|
try {
|
|
65
|
-
await
|
|
87
|
+
await auth.signOut()
|
|
88
|
+
|
|
89
|
+
await postToken('logout') // magic string
|
|
66
90
|
|
|
67
91
|
if (logout && returnUrl) {
|
|
68
92
|
alert('Logged out, redurecting back...')
|
|
@@ -73,20 +97,7 @@
|
|
|
73
97
|
}
|
|
74
98
|
},
|
|
75
99
|
},
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
// Initialize Firebase
|
|
79
|
-
const config = {
|
|
80
|
-
apiKey,
|
|
81
|
-
authDomain,
|
|
82
|
-
}
|
|
83
|
-
firebase.initializeApp(config)
|
|
84
|
-
|
|
85
|
-
firebase.auth().onAuthStateChanged(user => {
|
|
86
|
-
// console.log('onAuthStateChanged, user: ', JSON.stringify(user, null, 2))
|
|
87
|
-
console.log('onAuthStateChanged, user: ', user)
|
|
88
|
-
onUser(user)
|
|
89
|
-
})
|
|
100
|
+
}).mount('#app')
|
|
90
101
|
|
|
91
102
|
if (logout) app.logout()
|
|
92
103
|
|
|
@@ -100,13 +111,16 @@
|
|
|
100
111
|
if (!user) {
|
|
101
112
|
if (autoLogin) app.login()
|
|
102
113
|
} else {
|
|
103
|
-
const token = await
|
|
114
|
+
const token = await auth.currentUser.getIdToken()
|
|
115
|
+
|
|
104
116
|
// alert('idToken')
|
|
105
117
|
// console.log(idToken)
|
|
106
118
|
app.user = Object.assign({}, app.user, {
|
|
107
119
|
token,
|
|
108
120
|
})
|
|
109
121
|
|
|
122
|
+
await postToken(token)
|
|
123
|
+
|
|
110
124
|
// Redirect if needed
|
|
111
125
|
if (returnUrl) {
|
|
112
126
|
// alert(`Logged in as ${app.user.email}, redirecting back...`)
|
|
@@ -118,20 +132,19 @@
|
|
|
118
132
|
}
|
|
119
133
|
}
|
|
120
134
|
|
|
121
|
-
function parseQuery(queryString) {
|
|
122
|
-
const query = {}
|
|
123
|
-
const pairs = (queryString[0] === '?' ? queryString.substr(1) : queryString).split('&')
|
|
124
|
-
for (let i = 0; i < pairs.length; i++) {
|
|
125
|
-
const pair = pairs[i].split('=')
|
|
126
|
-
query[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1] || '')
|
|
127
|
-
}
|
|
128
|
-
return query
|
|
129
|
-
}
|
|
130
|
-
|
|
131
135
|
function logError(err) {
|
|
132
136
|
console.error(err)
|
|
133
137
|
alert('Error\n ' + JSON.stringify(err, null, 2))
|
|
134
138
|
}
|
|
139
|
+
|
|
140
|
+
async function postToken(token) {
|
|
141
|
+
await fetch(`/admin/login`, {
|
|
142
|
+
method: 'post',
|
|
143
|
+
headers: {
|
|
144
|
+
Authentication: token,
|
|
145
|
+
},
|
|
146
|
+
})
|
|
147
|
+
}
|
|
135
148
|
</script>
|
|
136
149
|
</body>
|
|
137
150
|
</html>
|
package/dist/db/httpDB.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { JsonSchemaRootObject } from '@naturalcycles/js-lib';
|
|
2
|
-
import { BaseCommonDB, CommonDB, CommonDBOptions, CommonDBSaveOptions, CommonDBStreamOptions, DBQuery,
|
|
1
|
+
import { JsonSchemaRootObject, ObjectWithId } from '@naturalcycles/js-lib';
|
|
2
|
+
import { BaseCommonDB, CommonDB, CommonDBOptions, CommonDBSaveOptions, CommonDBStreamOptions, DBQuery, RunQueryResult } from '@naturalcycles/db-lib';
|
|
3
3
|
import { GetGotOptions, ReadableTyped } from '@naturalcycles/nodejs-lib';
|
|
4
4
|
export interface HttpDBCfg extends GetGotOptions {
|
|
5
5
|
prefixUrl: string;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { CommonDB, CommonDBOptions, CommonDBSaveOptions, DBQuery
|
|
1
|
+
import { CommonDB, CommonDBOptions, CommonDBSaveOptions, DBQuery } from '@naturalcycles/db-lib';
|
|
2
|
+
import { ObjectWithId } from '@naturalcycles/js-lib';
|
|
2
3
|
import { Router } from 'express';
|
|
3
4
|
export interface GetByIdsInput {
|
|
4
5
|
table: string;
|
package/dist/index.d.ts
CHANGED
|
@@ -32,13 +32,3 @@ import { BackendServer, startServer } from './server/startServer';
|
|
|
32
32
|
import { StartServerCfg, StartServerData } from './server/startServer.model';
|
|
33
33
|
export type { MethodOverrideCfg, SentrySharedServiceCfg, RequestHandlerWithPath, RequestHandlerCfg, DefaultAppCfg, StartServerCfg, StartServerData, EnvSharedServiceCfg, BaseEnv, AdminMiddleware, AdminServiceCfg, AdminInfo, RequireAdminCfg, SecureHeaderMiddlewareCfg, BodyParserTimeoutCfg, RequestTimeoutCfg, SimpleRequestLoggerCfg, ReqValidationOptions, };
|
|
34
34
|
export { BackendServer, SentrySharedService, EnvSharedService, reqValidation, notFoundHandler, genericErrorHandler, methodOverride, sentryErrorHandler, createDefaultApp, startServer, catchWrapper, getDefaultRouter, isGAE, statusHandler, statusHandlerData, okHandler, getDeployInfo, onFinished, respondWithError, logRequest, FirebaseSharedService, createAdminMiddleware, BaseAdminService, loginHtml, createSecureHeaderMiddleware, bodyParserTimeout, clearBodyParserTimeout, requestTimeout, simpleRequestLogger, coloredHttpCode, getRequestContextProperty, setRequestContextProperty, requestContextMiddleware, requestIdMiddleware, REQUEST_ID_KEY, validateBody, validateParams, validateQuery, };
|
|
35
|
-
declare global {
|
|
36
|
-
namespace NodeJS {
|
|
37
|
-
interface ProcessEnv {
|
|
38
|
-
PORT?: string;
|
|
39
|
-
GAE_APPLICATION?: string;
|
|
40
|
-
GAE_SERVICE?: string;
|
|
41
|
-
GAE_VERSION?: string;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
}
|
|
@@ -11,6 +11,7 @@ function reqValidation(reqProperty, schema, opt = {}) {
|
|
|
11
11
|
if (opt.redactPaths) {
|
|
12
12
|
redact(opt.redactPaths, req[reqProperty], error);
|
|
13
13
|
error.data.joiValidationErrorItems.length = 0; // clears the array
|
|
14
|
+
delete error.data.annotation;
|
|
14
15
|
}
|
|
15
16
|
return next(new js_lib_1.HttpError(error.message, {
|
|
16
17
|
httpStatusCode: 400,
|
|
@@ -1,8 +1,3 @@
|
|
|
1
1
|
import { RequestHandler } from 'express';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
*/
|
|
5
|
-
declare type ServerStartedCallback = () => number | undefined;
|
|
6
|
-
export declare function statusHandler(serverStartedCallback?: ServerStartedCallback, projectDir?: string, extra?: any): RequestHandler;
|
|
7
|
-
export declare function statusHandlerData(serverStartedCallback?: ServerStartedCallback, projectDir?: string, extra?: any): Record<string, any>;
|
|
8
|
-
export {};
|
|
2
|
+
export declare function statusHandler(projectDir?: string, extra?: any): RequestHandler;
|
|
3
|
+
export declare function statusHandlerData(projectDir?: string, extra?: any): Record<string, any>;
|
|
@@ -5,29 +5,28 @@ const js_lib_1 = require("@naturalcycles/js-lib");
|
|
|
5
5
|
const nodejs_lib_1 = require("@naturalcycles/nodejs-lib");
|
|
6
6
|
const time_lib_1 = require("@naturalcycles/time-lib");
|
|
7
7
|
const deployInfo_util_1 = require("../deployInfo.util");
|
|
8
|
-
const now = Date.now();
|
|
9
|
-
const defaultServerStartedCallback = () => now;
|
|
10
8
|
const { versions } = process;
|
|
11
|
-
|
|
9
|
+
const { GAE_APPLICATION, GAE_SERVICE, GAE_VERSION } = process.env;
|
|
10
|
+
function statusHandler(projectDir, extra) {
|
|
12
11
|
return async (req, res) => {
|
|
13
|
-
res.json(statusHandlerData(
|
|
12
|
+
res.json(statusHandlerData(projectDir, extra));
|
|
14
13
|
};
|
|
15
14
|
}
|
|
16
15
|
exports.statusHandler = statusHandler;
|
|
17
|
-
function statusHandlerData(
|
|
16
|
+
function statusHandlerData(projectDir = process.cwd(), extra) {
|
|
18
17
|
const { APP_ENV } = process.env;
|
|
19
18
|
const { gitRev, gitBranch, prod, ts } = (0, deployInfo_util_1.getDeployInfo)(projectDir);
|
|
20
19
|
const deployBuildTimeUTC = time_lib_1.dayjs.unix(ts).toPretty();
|
|
21
20
|
const buildInfo = [time_lib_1.dayjs.unix(ts).toCompactTime(), gitBranch, gitRev].filter(Boolean).join('_');
|
|
22
21
|
return (0, js_lib_1._filterFalsyValues)({
|
|
23
|
-
started: getStartedStr(
|
|
22
|
+
started: getStartedStr(),
|
|
24
23
|
deployBuildTimeUTC,
|
|
25
24
|
APP_ENV,
|
|
26
25
|
prod,
|
|
27
26
|
buildInfo,
|
|
28
|
-
GAE_APPLICATION
|
|
29
|
-
GAE_SERVICE
|
|
30
|
-
GAE_VERSION
|
|
27
|
+
GAE_APPLICATION,
|
|
28
|
+
GAE_SERVICE,
|
|
29
|
+
GAE_VERSION,
|
|
31
30
|
mem: (0, nodejs_lib_1.memoryUsageFull)(),
|
|
32
31
|
cpuAvg: nodejs_lib_1.processSharedUtil.cpuAvg(),
|
|
33
32
|
// resourceUsage: process.resourceUsage?.(),
|
|
@@ -36,9 +35,8 @@ function statusHandlerData(serverStartedCallback = defaultServerStartedCallback,
|
|
|
36
35
|
});
|
|
37
36
|
}
|
|
38
37
|
exports.statusHandlerData = statusHandlerData;
|
|
39
|
-
function getStartedStr(
|
|
40
|
-
|
|
41
|
-
return 'not started yet';
|
|
38
|
+
function getStartedStr() {
|
|
39
|
+
const serverStarted = time_lib_1.dayjs.utc().subtract(process.uptime(), 's');
|
|
42
40
|
const s1 = (0, time_lib_1.dayjs)(serverStarted).toPretty();
|
|
43
41
|
const s2 = (0, time_lib_1.dayjs)(serverStarted).fromNow();
|
|
44
42
|
return `${s1} UTC (${s2})`;
|
|
@@ -10,7 +10,7 @@ class BackendServer {
|
|
|
10
10
|
this.cfg = cfg;
|
|
11
11
|
}
|
|
12
12
|
async start() {
|
|
13
|
-
const {
|
|
13
|
+
const { port: cfgPort, expressApp } = this.cfg;
|
|
14
14
|
// 1. Register error handlers, etc.
|
|
15
15
|
process.on('uncaughtException', err => {
|
|
16
16
|
log_1.log.error('uncaughtException:', err);
|
|
@@ -22,7 +22,7 @@ class BackendServer {
|
|
|
22
22
|
process.once('SIGTERM', () => this.stop());
|
|
23
23
|
// sentryService.install()
|
|
24
24
|
// 2. Start Express Server
|
|
25
|
-
const port = Number(process.env
|
|
25
|
+
const port = Number(process.env['PORT']) || cfgPort || 8080;
|
|
26
26
|
this.server = await new Promise((resolve, reject) => {
|
|
27
27
|
const server = expressApp.listen(port, (err) => {
|
|
28
28
|
if (err)
|
|
@@ -32,15 +32,21 @@ class BackendServer {
|
|
|
32
32
|
});
|
|
33
33
|
// This is to fix GCP LoadBalancer race condition
|
|
34
34
|
this.server.keepAliveTimeout = 600 * 1000; // 10 minutes
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
|
|
35
|
+
let address = `http://localhost:${port}`; // default
|
|
36
|
+
const addr = this.server.address();
|
|
37
|
+
if (addr) {
|
|
38
|
+
if (typeof addr === 'string') {
|
|
39
|
+
address = addr;
|
|
40
|
+
}
|
|
41
|
+
else if (addr.address !== '::') {
|
|
42
|
+
address = `http://${addr.address}:${port}`;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
(0, log_1.log)(`serverStarted on ${(0, colors_1.white)(address)} in ${(0, colors_1.dimGrey)((0, js_lib_1._ms)(process.uptime() * 1000))}`);
|
|
38
46
|
return {
|
|
39
47
|
port,
|
|
40
|
-
bootstrapStartedAt,
|
|
41
|
-
serverStartedAt,
|
|
42
|
-
bootstrapMillis,
|
|
43
48
|
server: this.server,
|
|
49
|
+
address,
|
|
44
50
|
};
|
|
45
51
|
}
|
|
46
52
|
/**
|
|
@@ -53,9 +59,7 @@ class BackendServer {
|
|
|
53
59
|
(0, log_1.log)((0, colors_1.boldGrey)('Forceful shutdown after timeout'));
|
|
54
60
|
process.exit(1);
|
|
55
61
|
}, this.cfg.forceShutdownTimeout || 3000);
|
|
56
|
-
|
|
57
|
-
void this.cfg.onShutdown();
|
|
58
|
-
}
|
|
62
|
+
void this.cfg.onShutdown?.();
|
|
59
63
|
try {
|
|
60
64
|
if (this.server) {
|
|
61
65
|
await new Promise(r => this.server.close(r));
|
|
@@ -64,7 +68,7 @@ class BackendServer {
|
|
|
64
68
|
process.exit(0);
|
|
65
69
|
}
|
|
66
70
|
catch (err) {
|
|
67
|
-
|
|
71
|
+
console.error(err);
|
|
68
72
|
process.exit(1);
|
|
69
73
|
}
|
|
70
74
|
}
|
|
@@ -6,12 +6,6 @@ export interface StartServerCfg {
|
|
|
6
6
|
* @default process.env.PORT || 8080
|
|
7
7
|
*/
|
|
8
8
|
port?: number;
|
|
9
|
-
/**
|
|
10
|
-
* Unix millisecond timestamp of when bootstrap has started.
|
|
11
|
-
*
|
|
12
|
-
* @default to Date.now()
|
|
13
|
-
*/
|
|
14
|
-
bootstrapStartedAt?: number;
|
|
15
9
|
expressApp: Application;
|
|
16
10
|
/**
|
|
17
11
|
* Server will wait for promise to resolve until shutting down.
|
|
@@ -25,8 +19,9 @@ export interface StartServerCfg {
|
|
|
25
19
|
}
|
|
26
20
|
export interface StartServerData {
|
|
27
21
|
port: number;
|
|
28
|
-
bootstrapStartedAt: number;
|
|
29
|
-
serverStartedAt: number;
|
|
30
|
-
bootstrapMillis: number;
|
|
31
22
|
server: Server;
|
|
23
|
+
/**
|
|
24
|
+
* "Processed" server.address() as a string, ready to Cmd+click in MacOS Terminal
|
|
25
|
+
*/
|
|
26
|
+
address: string;
|
|
32
27
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@naturalcycles/backend-lib",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.61.0",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"prepare": "husky install",
|
|
6
6
|
"docs-serve": "vuepress dev docs",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"ejs": "^3.0.1",
|
|
32
32
|
"express": "^4.16.4",
|
|
33
33
|
"express-promise-router": "^4.0.0",
|
|
34
|
-
"firebase-admin": "^
|
|
34
|
+
"firebase-admin": "^10.0.0",
|
|
35
35
|
"fs-extra": "^10.0.0",
|
|
36
36
|
"helmet": "^4.0.0",
|
|
37
37
|
"js-yaml": "^4.0.0",
|
package/src/admin/admin.mw.ts
CHANGED
|
@@ -21,6 +21,12 @@ export interface RequireAdminCfg {
|
|
|
21
21
|
apiHost?: string
|
|
22
22
|
|
|
23
23
|
urlStartsWith?: string
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Defaults to `true`.
|
|
27
|
+
* Set to `false` to debug login issues.
|
|
28
|
+
*/
|
|
29
|
+
autoLogin?: boolean
|
|
24
30
|
}
|
|
25
31
|
|
|
26
32
|
export type AdminMiddleware = (reqPermissions?: string[], cfg?: RequireAdminCfg) => RequestHandler
|
|
@@ -47,7 +53,7 @@ export function requireAdminPermissions(
|
|
|
47
53
|
reqPermissions: string[] = [],
|
|
48
54
|
cfg: RequireAdminCfg = {},
|
|
49
55
|
): RequestHandler {
|
|
50
|
-
const { loginHtmlPath = '/login.html', urlStartsWith, apiHost } = cfg
|
|
56
|
+
const { loginHtmlPath = '/login.html', urlStartsWith, apiHost, autoLogin = true } = cfg
|
|
51
57
|
|
|
52
58
|
return async (req, res, next) => {
|
|
53
59
|
if (urlStartsWith && !req.url.startsWith(urlStartsWith)) return next()
|
|
@@ -58,9 +64,9 @@ export function requireAdminPermissions(
|
|
|
58
64
|
} catch (err) {
|
|
59
65
|
if (err instanceof HttpError && (err.data as Admin401ErrorData).adminAuthRequired) {
|
|
60
66
|
// Redirect to login.html
|
|
61
|
-
const href = `${loginHtmlPath}
|
|
62
|
-
|
|
63
|
-
}`
|
|
67
|
+
const href = `${loginHtmlPath}?${
|
|
68
|
+
autoLogin ? 'autoLogin=1&' : ''
|
|
69
|
+
}returnUrl=\${encodeURIComponent(location.href)}${apiHost ? '&apiHost=' + apiHost : ''}`
|
|
64
70
|
res.status(401).send(getLoginHtmlRedirect(href))
|
|
65
71
|
} else {
|
|
66
72
|
return next(err)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { Admin401ErrorData, Admin403ErrorData, HttpError } from '@naturalcycles/js-lib'
|
|
1
|
+
import { _assert, Admin401ErrorData, Admin403ErrorData, HttpError } from '@naturalcycles/js-lib'
|
|
2
2
|
import { Debug, inspectAny } from '@naturalcycles/nodejs-lib'
|
|
3
3
|
import { dimGrey, green, red } from '@naturalcycles/nodejs-lib/dist/colors'
|
|
4
|
-
import { Request } from 'express'
|
|
4
|
+
import { Request, RequestHandler } from 'express'
|
|
5
5
|
import type * as FirebaseAdmin from 'firebase-admin'
|
|
6
6
|
|
|
7
7
|
const log = Debug('nc:backend-lib:admin')
|
|
@@ -228,4 +228,42 @@ export class BaseAdminService {
|
|
|
228
228
|
): Promise<AdminInfo> {
|
|
229
229
|
return await this.requirePermissions(req, [reqPermission], meta)
|
|
230
230
|
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Install it on POST /admin/login url
|
|
234
|
+
*
|
|
235
|
+
* It takes a POST request with `Authentication` header, that contains `accessToken` from Firebase Auth.
|
|
236
|
+
* Backend doesn't validate the token, but only does `setCookie` (secure, httpOnly), returns http 204 (ok, empty response).
|
|
237
|
+
* Frontend (login.html page) will then proceed with redirecting to `returnUrl`.
|
|
238
|
+
*
|
|
239
|
+
* Same endpoint is used to logout, but the `Authentication` header should contain `logout` magic string.
|
|
240
|
+
*/
|
|
241
|
+
getFirebaseAuthLoginHandler(): RequestHandler {
|
|
242
|
+
return async (req, res) => {
|
|
243
|
+
const token = req.header('authentication')
|
|
244
|
+
_assert(token, `401 Unauthenticated`, {
|
|
245
|
+
userFriendly: true,
|
|
246
|
+
httpStatusCode: 401,
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
let maxAge = 1000 * 60 * 60 * 24 * 30 // 30 days
|
|
250
|
+
|
|
251
|
+
// Special case
|
|
252
|
+
if (token === 'logout') {
|
|
253
|
+
// delete the cookie
|
|
254
|
+
maxAge = 0
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
res
|
|
258
|
+
.cookie(this.cfg.adminTokenKey, token, {
|
|
259
|
+
maxAge,
|
|
260
|
+
sameSite: 'lax', // can be: none, lax, strict
|
|
261
|
+
// comment these 2 lines to debug on localhost
|
|
262
|
+
httpOnly: true,
|
|
263
|
+
secure: true,
|
|
264
|
+
})
|
|
265
|
+
.status(204)
|
|
266
|
+
.end()
|
|
267
|
+
}
|
|
268
|
+
}
|
|
231
269
|
}
|
package/src/admin/login.html
CHANGED
|
@@ -6,11 +6,12 @@
|
|
|
6
6
|
<meta charset="utf-8" />
|
|
7
7
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
<
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
<!-- CSS only -->
|
|
10
|
+
<link
|
|
11
|
+
href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.2/dist/css/bootstrap.min.css"
|
|
12
|
+
rel="stylesheet"
|
|
13
|
+
crossorigin="anonymous"
|
|
14
|
+
/>
|
|
14
15
|
</head>
|
|
15
16
|
<body>
|
|
16
17
|
<div id="app" style="padding: 40px 50px">
|
|
@@ -28,33 +29,54 @@
|
|
|
28
29
|
</div>
|
|
29
30
|
</div>
|
|
30
31
|
|
|
31
|
-
<script>
|
|
32
|
+
<script type="module">
|
|
33
|
+
import { createApp } from 'https://cdn.jsdelivr.net/npm/vue@3.2.20/dist/vue.esm-browser.prod.js'
|
|
34
|
+
import { initializeApp } from 'https://www.gstatic.com/firebasejs/9.1.2/firebase-app.js'
|
|
35
|
+
import {
|
|
36
|
+
getAuth,
|
|
37
|
+
GoogleAuthProvider,
|
|
38
|
+
onAuthStateChanged,
|
|
39
|
+
signInWithRedirect,
|
|
40
|
+
} from 'https://www.gstatic.com/firebasejs/9.1.2/firebase-auth.js'
|
|
41
|
+
|
|
32
42
|
const apiKey = '<%= firebaseApiKey %>'
|
|
33
43
|
const authDomain = '<%= firebaseAuthDomain %>'
|
|
34
|
-
const authProvider = '<%= firebaseAuthProvider %>'
|
|
44
|
+
// const authProvider = '<%= firebaseAuthProvider %>'
|
|
35
45
|
|
|
36
46
|
if (!apiKey || !authDomain) {
|
|
37
47
|
alert(`Error: 'apiKey' or 'authDomain' is missing!`)
|
|
38
48
|
}
|
|
39
49
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
50
|
+
// Initialize Firebase
|
|
51
|
+
initializeApp({
|
|
52
|
+
apiKey,
|
|
53
|
+
authDomain,
|
|
54
|
+
})
|
|
43
55
|
|
|
44
|
-
const
|
|
56
|
+
const auth = getAuth()
|
|
57
|
+
const provider = new GoogleAuthProvider()
|
|
45
58
|
|
|
46
|
-
|
|
47
|
-
|
|
59
|
+
onAuthStateChanged(auth, user => {
|
|
60
|
+
// console.log('onAuthStateChanged, user: ', JSON.stringify(user, null, 2))
|
|
61
|
+
console.log('onAuthStateChanged, user: ', user)
|
|
62
|
+
onUser(user)
|
|
63
|
+
})
|
|
48
64
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
},
|
|
65
|
+
const qs = Object.fromEntries(new URLSearchParams(location.search))
|
|
66
|
+
const { autoLogin, logout, returnUrl, adminTokenKey = 'admin_token' } = qs
|
|
67
|
+
console.log(qs)
|
|
53
68
|
|
|
69
|
+
const app = createApp({
|
|
70
|
+
data() {
|
|
71
|
+
return {
|
|
72
|
+
loading: 'Loading...',
|
|
73
|
+
user: undefined,
|
|
74
|
+
}
|
|
75
|
+
},
|
|
54
76
|
methods: {
|
|
55
77
|
login: async function () {
|
|
56
78
|
try {
|
|
57
|
-
await
|
|
79
|
+
await signInWithRedirect(auth, provider)
|
|
58
80
|
} catch (err) {
|
|
59
81
|
logError(err)
|
|
60
82
|
}
|
|
@@ -62,7 +84,9 @@
|
|
|
62
84
|
|
|
63
85
|
logout: async function () {
|
|
64
86
|
try {
|
|
65
|
-
await
|
|
87
|
+
await auth.signOut()
|
|
88
|
+
|
|
89
|
+
await postToken('logout') // magic string
|
|
66
90
|
|
|
67
91
|
if (logout && returnUrl) {
|
|
68
92
|
alert('Logged out, redurecting back...')
|
|
@@ -73,20 +97,7 @@
|
|
|
73
97
|
}
|
|
74
98
|
},
|
|
75
99
|
},
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
// Initialize Firebase
|
|
79
|
-
const config = {
|
|
80
|
-
apiKey,
|
|
81
|
-
authDomain,
|
|
82
|
-
}
|
|
83
|
-
firebase.initializeApp(config)
|
|
84
|
-
|
|
85
|
-
firebase.auth().onAuthStateChanged(user => {
|
|
86
|
-
// console.log('onAuthStateChanged, user: ', JSON.stringify(user, null, 2))
|
|
87
|
-
console.log('onAuthStateChanged, user: ', user)
|
|
88
|
-
onUser(user)
|
|
89
|
-
})
|
|
100
|
+
}).mount('#app')
|
|
90
101
|
|
|
91
102
|
if (logout) app.logout()
|
|
92
103
|
|
|
@@ -100,13 +111,16 @@
|
|
|
100
111
|
if (!user) {
|
|
101
112
|
if (autoLogin) app.login()
|
|
102
113
|
} else {
|
|
103
|
-
const token = await
|
|
114
|
+
const token = await auth.currentUser.getIdToken()
|
|
115
|
+
|
|
104
116
|
// alert('idToken')
|
|
105
117
|
// console.log(idToken)
|
|
106
118
|
app.user = Object.assign({}, app.user, {
|
|
107
119
|
token,
|
|
108
120
|
})
|
|
109
121
|
|
|
122
|
+
await postToken(token)
|
|
123
|
+
|
|
110
124
|
// Redirect if needed
|
|
111
125
|
if (returnUrl) {
|
|
112
126
|
// alert(`Logged in as ${app.user.email}, redirecting back...`)
|
|
@@ -118,20 +132,19 @@
|
|
|
118
132
|
}
|
|
119
133
|
}
|
|
120
134
|
|
|
121
|
-
function parseQuery(queryString) {
|
|
122
|
-
const query = {}
|
|
123
|
-
const pairs = (queryString[0] === '?' ? queryString.substr(1) : queryString).split('&')
|
|
124
|
-
for (let i = 0; i < pairs.length; i++) {
|
|
125
|
-
const pair = pairs[i].split('=')
|
|
126
|
-
query[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1] || '')
|
|
127
|
-
}
|
|
128
|
-
return query
|
|
129
|
-
}
|
|
130
|
-
|
|
131
135
|
function logError(err) {
|
|
132
136
|
console.error(err)
|
|
133
137
|
alert('Error\n ' + JSON.stringify(err, null, 2))
|
|
134
138
|
}
|
|
139
|
+
|
|
140
|
+
async function postToken(token) {
|
|
141
|
+
await fetch(`/admin/login`, {
|
|
142
|
+
method: 'post',
|
|
143
|
+
headers: {
|
|
144
|
+
Authentication: token,
|
|
145
|
+
},
|
|
146
|
+
})
|
|
147
|
+
}
|
|
135
148
|
</script>
|
|
136
149
|
</body>
|
|
137
150
|
</html>
|
package/src/db/httpDB.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Readable } from 'stream'
|
|
2
|
-
import { JsonSchemaRootObject } from '@naturalcycles/js-lib'
|
|
2
|
+
import { JsonSchemaRootObject, ObjectWithId } from '@naturalcycles/js-lib'
|
|
3
3
|
import {
|
|
4
4
|
BaseCommonDB,
|
|
5
5
|
CommonDB,
|
|
@@ -7,7 +7,6 @@ import {
|
|
|
7
7
|
CommonDBSaveOptions,
|
|
8
8
|
CommonDBStreamOptions,
|
|
9
9
|
DBQuery,
|
|
10
|
-
ObjectWithId,
|
|
11
10
|
RunQueryResult,
|
|
12
11
|
} from '@naturalcycles/db-lib'
|
|
13
12
|
import { getGot, GetGotOptions, Got, ReadableTyped } from '@naturalcycles/nodejs-lib'
|
|
@@ -4,13 +4,13 @@ import {
|
|
|
4
4
|
CommonDBSaveOptions,
|
|
5
5
|
DBQuery,
|
|
6
6
|
InMemoryDB,
|
|
7
|
-
ObjectWithId,
|
|
8
7
|
} from '@naturalcycles/db-lib'
|
|
9
8
|
import {
|
|
10
9
|
commonDBOptionsSchema,
|
|
11
10
|
commonDBSaveOptionsSchema,
|
|
12
11
|
dbQuerySchema,
|
|
13
12
|
} from '@naturalcycles/db-lib/dist/validation'
|
|
13
|
+
import { ObjectWithId } from '@naturalcycles/js-lib'
|
|
14
14
|
import { anyObjectSchema, arraySchema, objectSchema, stringSchema } from '@naturalcycles/nodejs-lib'
|
|
15
15
|
import { Router } from 'express'
|
|
16
16
|
import { getDefaultRouter, reqValidation } from '..'
|
package/src/index.ts
CHANGED
|
@@ -4,27 +4,16 @@ import { dayjs } from '@naturalcycles/time-lib'
|
|
|
4
4
|
import { RequestHandler } from 'express'
|
|
5
5
|
import { getDeployInfo } from '../deployInfo.util'
|
|
6
6
|
|
|
7
|
-
/**
|
|
8
|
-
* @returns unix timestamp in millis
|
|
9
|
-
*/
|
|
10
|
-
type ServerStartedCallback = () => number | undefined
|
|
11
|
-
|
|
12
|
-
const now = Date.now()
|
|
13
|
-
const defaultServerStartedCallback = () => now
|
|
14
7
|
const { versions } = process
|
|
8
|
+
const { GAE_APPLICATION, GAE_SERVICE, GAE_VERSION } = process.env
|
|
15
9
|
|
|
16
|
-
export function statusHandler(
|
|
17
|
-
serverStartedCallback?: ServerStartedCallback,
|
|
18
|
-
projectDir?: string,
|
|
19
|
-
extra?: any,
|
|
20
|
-
): RequestHandler {
|
|
10
|
+
export function statusHandler(projectDir?: string, extra?: any): RequestHandler {
|
|
21
11
|
return async (req, res) => {
|
|
22
|
-
res.json(statusHandlerData(
|
|
12
|
+
res.json(statusHandlerData(projectDir, extra))
|
|
23
13
|
}
|
|
24
14
|
}
|
|
25
15
|
|
|
26
16
|
export function statusHandlerData(
|
|
27
|
-
serverStartedCallback: ServerStartedCallback = defaultServerStartedCallback,
|
|
28
17
|
projectDir: string = process.cwd(),
|
|
29
18
|
extra?: any,
|
|
30
19
|
): Record<string, any> {
|
|
@@ -34,14 +23,14 @@ export function statusHandlerData(
|
|
|
34
23
|
const buildInfo = [dayjs.unix(ts).toCompactTime(), gitBranch, gitRev].filter(Boolean).join('_')
|
|
35
24
|
|
|
36
25
|
return _filterFalsyValues({
|
|
37
|
-
started: getStartedStr(
|
|
26
|
+
started: getStartedStr(),
|
|
38
27
|
deployBuildTimeUTC,
|
|
39
28
|
APP_ENV,
|
|
40
29
|
prod,
|
|
41
30
|
buildInfo,
|
|
42
|
-
GAE_APPLICATION
|
|
43
|
-
GAE_SERVICE
|
|
44
|
-
GAE_VERSION
|
|
31
|
+
GAE_APPLICATION,
|
|
32
|
+
GAE_SERVICE,
|
|
33
|
+
GAE_VERSION,
|
|
45
34
|
mem: memoryUsageFull(),
|
|
46
35
|
cpuAvg: processSharedUtil.cpuAvg(),
|
|
47
36
|
// resourceUsage: process.resourceUsage?.(),
|
|
@@ -50,8 +39,8 @@ export function statusHandlerData(
|
|
|
50
39
|
})
|
|
51
40
|
}
|
|
52
41
|
|
|
53
|
-
function getStartedStr(
|
|
54
|
-
|
|
42
|
+
function getStartedStr(): string {
|
|
43
|
+
const serverStarted = dayjs.utc().subtract(process.uptime(), 's')
|
|
55
44
|
|
|
56
45
|
const s1 = dayjs(serverStarted).toPretty()
|
|
57
46
|
const s2 = dayjs(serverStarted).fromNow()
|
|
@@ -7,13 +7,6 @@ export interface StartServerCfg {
|
|
|
7
7
|
*/
|
|
8
8
|
port?: number
|
|
9
9
|
|
|
10
|
-
/**
|
|
11
|
-
* Unix millisecond timestamp of when bootstrap has started.
|
|
12
|
-
*
|
|
13
|
-
* @default to Date.now()
|
|
14
|
-
*/
|
|
15
|
-
bootstrapStartedAt?: number
|
|
16
|
-
|
|
17
10
|
expressApp: Application
|
|
18
11
|
|
|
19
12
|
/**
|
|
@@ -30,8 +23,9 @@ export interface StartServerCfg {
|
|
|
30
23
|
|
|
31
24
|
export interface StartServerData {
|
|
32
25
|
port: number
|
|
33
|
-
bootstrapStartedAt: number
|
|
34
|
-
serverStartedAt: number
|
|
35
|
-
bootstrapMillis: number
|
|
36
26
|
server: Server
|
|
27
|
+
/**
|
|
28
|
+
* "Processed" server.address() as a string, ready to Cmd+click in MacOS Terminal
|
|
29
|
+
*/
|
|
30
|
+
address: string
|
|
37
31
|
}
|
|
@@ -10,7 +10,7 @@ export class BackendServer {
|
|
|
10
10
|
server?: Server
|
|
11
11
|
|
|
12
12
|
async start(): Promise<StartServerData> {
|
|
13
|
-
const {
|
|
13
|
+
const { port: cfgPort, expressApp } = this.cfg
|
|
14
14
|
|
|
15
15
|
// 1. Register error handlers, etc.
|
|
16
16
|
process.on('uncaughtException', err => {
|
|
@@ -27,7 +27,7 @@ export class BackendServer {
|
|
|
27
27
|
// sentryService.install()
|
|
28
28
|
|
|
29
29
|
// 2. Start Express Server
|
|
30
|
-
const port = Number(process.env
|
|
30
|
+
const port = Number(process.env['PORT']) || cfgPort || 8080
|
|
31
31
|
|
|
32
32
|
this.server = await new Promise<Server>((resolve, reject) => {
|
|
33
33
|
const server = expressApp.listen(port, (err?: Error) => {
|
|
@@ -39,21 +39,23 @@ export class BackendServer {
|
|
|
39
39
|
// This is to fix GCP LoadBalancer race condition
|
|
40
40
|
this.server.keepAliveTimeout = 600 * 1000 // 10 minutes
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
let address = `http://localhost:${port}` // default
|
|
43
43
|
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
)
|
|
49
|
-
|
|
44
|
+
const addr = this.server.address()
|
|
45
|
+
if (addr) {
|
|
46
|
+
if (typeof addr === 'string') {
|
|
47
|
+
address = addr
|
|
48
|
+
} else if (addr.address !== '::') {
|
|
49
|
+
address = `http://${addr.address}:${port}`
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
log(`serverStarted on ${white(address)} in ${dimGrey(_ms(process.uptime() * 1000))}`)
|
|
50
54
|
|
|
51
55
|
return {
|
|
52
56
|
port,
|
|
53
|
-
bootstrapStartedAt,
|
|
54
|
-
serverStartedAt,
|
|
55
|
-
bootstrapMillis,
|
|
56
57
|
server: this.server,
|
|
58
|
+
address,
|
|
57
59
|
}
|
|
58
60
|
}
|
|
59
61
|
|
|
@@ -70,9 +72,7 @@ export class BackendServer {
|
|
|
70
72
|
process.exit(1)
|
|
71
73
|
}, this.cfg.forceShutdownTimeout || 3000)
|
|
72
74
|
|
|
73
|
-
|
|
74
|
-
void this.cfg.onShutdown()
|
|
75
|
-
}
|
|
75
|
+
void this.cfg.onShutdown?.()
|
|
76
76
|
|
|
77
77
|
try {
|
|
78
78
|
if (this.server) {
|
|
@@ -81,7 +81,7 @@ export class BackendServer {
|
|
|
81
81
|
log(dimGrey('Shutdown completed.'))
|
|
82
82
|
process.exit(0)
|
|
83
83
|
} catch (err) {
|
|
84
|
-
|
|
84
|
+
console.error(err)
|
|
85
85
|
process.exit(1)
|
|
86
86
|
}
|
|
87
87
|
}
|