@naturalcycles/backend-lib 2.60.0 → 2.60.4
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 +3 -3
- 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 +3 -3
- package/dist/db/httpDB.js +1 -4
- package/dist/db/httpDBRequestHandler.d.ts +2 -1
- package/dist/deploy/deploy.util.js +2 -2
- package/dist/deploy/deployHealthCheck.js +1 -1
- package/dist/index.d.ts +0 -10
- package/dist/sentry/sentry.shared.service.js +3 -3
- package/dist/server/deployInfo.util.js +1 -1
- 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 +1 -1
- package/dist/server/startServer.model.d.ts +1 -1
- package/dist/testing/express.test.service.d.ts +1 -1
- package/package.json +2 -2
- package/src/admin/admin.mw.ts +11 -5
- package/src/admin/base.admin.service.ts +40 -2
- package/src/admin/login.html +57 -44
- package/src/db/httpDB.ts +3 -9
- package/src/db/httpDBRequestHandler.ts +1 -1
- package/src/deploy/deploy.util.ts +2 -2
- package/src/deploy/deployHealthCheck.ts +1 -1
- package/src/index.ts +0 -11
- package/src/sentry/sentry.shared.service.ts +3 -3
- package/src/server/deployInfo.util.ts +1 -1
- package/src/server/handlers/bodyParserTimeout.mw.ts +1 -2
- package/src/server/handlers/reqValidation.mw.ts +1 -0
- package/src/server/handlers/statusHandler.ts +9 -20
- package/src/server/startServer.model.ts +1 -1
- package/src/server/startServer.ts +2 -2
- package/src/testing/express.test.service.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,31 @@
|
|
|
1
|
+
## [2.60.4](https://github.com/NaturalCycles/backend-lib/compare/v2.60.3...v2.60.4) (2021-10-21)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* deps, use process.uptime in statusHandler ([8d3389d](https://github.com/NaturalCycles/backend-lib/commit/8d3389d8908954af9a375a6d7fb95aefc1b08e5f))
|
|
7
|
+
|
|
8
|
+
## [2.60.3](https://github.com/NaturalCycles/backend-lib/compare/v2.60.2...v2.60.3) (2021-10-15)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* deps (firebase-admin@10) ([50fe05b](https://github.com/NaturalCycles/backend-lib/commit/50fe05be92cb4345b809096d3b45b1b099bc2ea5))
|
|
14
|
+
|
|
15
|
+
## [2.60.2](https://github.com/NaturalCycles/backend-lib/compare/v2.60.1...v2.60.2) (2021-10-09)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Bug Fixes
|
|
19
|
+
|
|
20
|
+
* login.html by inroducing an `/admin/login` endpoint ([6e16d0f](https://github.com/NaturalCycles/backend-lib/commit/6e16d0fd8f26d35dd562687a29a120b32d9f2717))
|
|
21
|
+
|
|
22
|
+
## [2.60.1](https://github.com/NaturalCycles/backend-lib/compare/v2.60.0...v2.60.1) (2021-10-04)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
### Bug Fixes
|
|
26
|
+
|
|
27
|
+
* deps ([ef0fa52](https://github.com/NaturalCycles/backend-lib/commit/ef0fa52fabd9f905592f69cfeed651011faf6578))
|
|
28
|
+
|
|
1
29
|
# [2.60.0](https://github.com/NaturalCycles/backend-lib/compare/v2.59.3...v2.60.0) (2021-09-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
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.getLoginHtmlRedirect = exports.loginHtml = exports.requireAdminPermissions = exports.createAdminMiddleware = void 0;
|
|
4
|
+
const fs = require("fs");
|
|
4
5
|
const js_lib_1 = require("@naturalcycles/js-lib");
|
|
5
6
|
const nodejs_lib_1 = require("@naturalcycles/nodejs-lib");
|
|
6
7
|
const ejs = require("ejs");
|
|
7
|
-
const fs = require("fs");
|
|
8
8
|
const log = (0, nodejs_lib_1.Debug)('nc:backend-lib:admin');
|
|
9
9
|
function createAdminMiddleware(adminService, cfgDefaults = {}) {
|
|
10
10
|
return (reqPermissions, cfg) => requireAdminPermissions(adminService, reqPermissions, {
|
|
@@ -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,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { JsonSchemaRootObject, ObjectWithId } from '@naturalcycles/js-lib';
|
|
2
|
+
import { BaseCommonDB, CommonDB, CommonDBOptions, CommonDBSaveOptions, CommonDBStreamOptions, DBQuery, RunQueryResult } from '@naturalcycles/db-lib';
|
|
2
3
|
import { GetGotOptions, ReadableTyped } from '@naturalcycles/nodejs-lib';
|
|
3
4
|
export interface HttpDBCfg extends GetGotOptions {
|
|
4
5
|
prefixUrl: string;
|
|
@@ -13,7 +14,7 @@ export declare class HttpDB extends BaseCommonDB implements CommonDB {
|
|
|
13
14
|
private got;
|
|
14
15
|
ping(): Promise<void>;
|
|
15
16
|
getTables(): Promise<string[]>;
|
|
16
|
-
getTableSchema<ROW extends ObjectWithId>(table: string): Promise<
|
|
17
|
+
getTableSchema<ROW extends ObjectWithId>(table: string): Promise<JsonSchemaRootObject<ROW>>;
|
|
17
18
|
resetCache(table?: string): Promise<void>;
|
|
18
19
|
getByIds<ROW extends ObjectWithId>(table: string, ids: string[], opt?: CommonDBOptions): Promise<ROW[]>;
|
|
19
20
|
runQuery<ROW extends ObjectWithId>(query: DBQuery<ROW>, opt?: CommonDBOptions): Promise<RunQueryResult<ROW>>;
|
|
@@ -21,6 +22,5 @@ export declare class HttpDB extends BaseCommonDB implements CommonDB {
|
|
|
21
22
|
saveBatch<ROW extends ObjectWithId>(table: string, rows: ROW[], opt?: CommonDBSaveOptions): Promise<void>;
|
|
22
23
|
deleteByIds(table: string, ids: string[], opt?: CommonDBOptions): Promise<number>;
|
|
23
24
|
deleteByQuery<ROW extends ObjectWithId>(query: DBQuery<ROW>, opt?: CommonDBOptions): Promise<number>;
|
|
24
|
-
createTable(_schema: CommonSchema, _opt?: CommonDBCreateOptions): Promise<void>;
|
|
25
25
|
streamQuery<ROW extends ObjectWithId>(_q: DBQuery<ROW>, _opt?: CommonDBStreamOptions): ReadableTyped<ROW>;
|
|
26
26
|
}
|
package/dist/db/httpDB.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.HttpDB = void 0;
|
|
4
|
+
const stream_1 = require("stream");
|
|
4
5
|
const db_lib_1 = require("@naturalcycles/db-lib");
|
|
5
6
|
const nodejs_lib_1 = require("@naturalcycles/nodejs-lib");
|
|
6
|
-
const stream_1 = require("stream");
|
|
7
7
|
/**
|
|
8
8
|
* Implementation of CommonDB that proxies all requests via HTTP to "httpDBRequestHandler".
|
|
9
9
|
*/
|
|
@@ -89,9 +89,6 @@ class HttpDB extends db_lib_1.BaseCommonDB {
|
|
|
89
89
|
})
|
|
90
90
|
.json();
|
|
91
91
|
}
|
|
92
|
-
async createTable(_schema, _opt) {
|
|
93
|
-
console.warn(`createTable not implemented`);
|
|
94
|
-
}
|
|
95
92
|
streamQuery(_q, _opt) {
|
|
96
93
|
console.warn(`streamQuery not implemented`);
|
|
97
94
|
return stream_1.Readable.from([]);
|
|
@@ -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;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.validateGAEServiceName = exports.createAppYaml = exports.createAndSaveAppYaml = exports.createDeployInfo = exports.createAndSaveDeployInfo = void 0;
|
|
4
|
+
const fs = require("fs");
|
|
4
5
|
const js_lib_1 = require("@naturalcycles/js-lib");
|
|
5
6
|
const colors_1 = require("@naturalcycles/nodejs-lib/dist/colors");
|
|
6
7
|
const time_lib_1 = require("@naturalcycles/time-lib");
|
|
7
|
-
const fs = require("fs");
|
|
8
8
|
const yaml = require("js-yaml");
|
|
9
9
|
const APP_YAML_DEFAULT = () => ({
|
|
10
10
|
runtime: 'nodejs14',
|
|
@@ -101,10 +101,10 @@ function createAppYaml(backendCfg, deployInfo, projectDir, appYamlPassEnv = '')
|
|
|
101
101
|
}
|
|
102
102
|
// appYamlPassEnv
|
|
103
103
|
require('dotenv').config(); // ensure .env is read
|
|
104
|
-
// eslint-disable-next-line unicorn/no-array-reduce
|
|
105
104
|
const passEnv = appYamlPassEnv
|
|
106
105
|
.split(',')
|
|
107
106
|
.filter(Boolean)
|
|
107
|
+
// eslint-disable-next-line unicorn/no-array-reduce
|
|
108
108
|
.reduce((map, key) => {
|
|
109
109
|
const v = process.env[key];
|
|
110
110
|
if (!v) {
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.deployHealthCheck = exports.deployHealthCheckYargsOptions = void 0;
|
|
4
|
+
const util_1 = require("util");
|
|
4
5
|
const js_lib_1 = require("@naturalcycles/js-lib");
|
|
5
6
|
const nodejs_lib_1 = require("@naturalcycles/nodejs-lib");
|
|
6
7
|
const colors_1 = require("@naturalcycles/nodejs-lib/dist/colors");
|
|
7
8
|
const exec_1 = require("@naturalcycles/nodejs-lib/dist/exec");
|
|
8
|
-
const util_1 = require("util");
|
|
9
9
|
const request_log_util_1 = require("../server/request.log.util");
|
|
10
10
|
exports.deployHealthCheckYargsOptions = {
|
|
11
11
|
thresholdHealthy: {
|
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
|
-
}
|
|
@@ -17,16 +17,16 @@ class SentrySharedService {
|
|
|
17
17
|
// Reasons:
|
|
18
18
|
// 1. Can be useful is this module is imported but never actually used
|
|
19
19
|
// 2. Works around memory leak when used with Jest
|
|
20
|
-
const
|
|
20
|
+
const sentry = require('@sentry/node');
|
|
21
21
|
if (this.sentryServiceCfg.dsn) {
|
|
22
22
|
// Sentry enabled
|
|
23
23
|
log('SentryService init...');
|
|
24
24
|
}
|
|
25
|
-
|
|
25
|
+
sentry.init({
|
|
26
26
|
maxValueLength: 2000,
|
|
27
27
|
...this.sentryServiceCfg,
|
|
28
28
|
});
|
|
29
|
-
return
|
|
29
|
+
return sentry;
|
|
30
30
|
}
|
|
31
31
|
getRequestHandler() {
|
|
32
32
|
return this.sentry().Handlers.requestHandler();
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.getDeployInfo = void 0;
|
|
4
|
-
const js_lib_1 = require("@naturalcycles/js-lib");
|
|
5
4
|
const fs = require("fs");
|
|
5
|
+
const js_lib_1 = require("@naturalcycles/js-lib");
|
|
6
6
|
exports.getDeployInfo = (0, js_lib_1._memoFn)((projectDir) => {
|
|
7
7
|
const deployInfoPath = `${projectDir}/deployInfo.json`;
|
|
8
8
|
try {
|
|
@@ -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})`;
|
|
@@ -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)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/// <reference types="node" />
|
|
2
|
-
import { Got } from '@naturalcycles/nodejs-lib';
|
|
3
2
|
import { Server } from 'http';
|
|
3
|
+
import { Got } from '@naturalcycles/nodejs-lib';
|
|
4
4
|
import { RequestHandlerCfg } from '..';
|
|
5
5
|
interface DestroyableServer extends Server {
|
|
6
6
|
destroy(): void;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@naturalcycles/backend-lib",
|
|
3
|
-
"version": "2.60.
|
|
3
|
+
"version": "2.60.4",
|
|
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
|
@@ -1,8 +1,8 @@
|
|
|
1
|
+
import * as fs from 'fs'
|
|
1
2
|
import { Admin401ErrorData, HttpError, _memoFn } from '@naturalcycles/js-lib'
|
|
2
3
|
import { Debug } from '@naturalcycles/nodejs-lib'
|
|
3
4
|
import * as ejs from 'ejs'
|
|
4
5
|
import { RequestHandler } from 'express'
|
|
5
|
-
import * as fs from 'fs'
|
|
6
6
|
import { BaseAdminService } from './base.admin.service'
|
|
7
7
|
import { FirebaseSharedServiceCfg } from './firebase.shared.service'
|
|
8
8
|
|
|
@@ -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,17 +1,15 @@
|
|
|
1
|
+
import { Readable } from 'stream'
|
|
2
|
+
import { JsonSchemaRootObject, ObjectWithId } from '@naturalcycles/js-lib'
|
|
1
3
|
import {
|
|
2
4
|
BaseCommonDB,
|
|
3
5
|
CommonDB,
|
|
4
|
-
CommonDBCreateOptions,
|
|
5
6
|
CommonDBOptions,
|
|
6
7
|
CommonDBSaveOptions,
|
|
7
8
|
CommonDBStreamOptions,
|
|
8
|
-
CommonSchema,
|
|
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'
|
|
14
|
-
import { Readable } from 'stream'
|
|
15
13
|
|
|
16
14
|
export interface HttpDBCfg extends GetGotOptions {
|
|
17
15
|
prefixUrl: string
|
|
@@ -42,7 +40,7 @@ export class HttpDB extends BaseCommonDB implements CommonDB {
|
|
|
42
40
|
|
|
43
41
|
override async getTableSchema<ROW extends ObjectWithId>(
|
|
44
42
|
table: string,
|
|
45
|
-
): Promise<
|
|
43
|
+
): Promise<JsonSchemaRootObject<ROW>> {
|
|
46
44
|
return await this.got(`${table}/schema`).json()
|
|
47
45
|
}
|
|
48
46
|
|
|
@@ -134,10 +132,6 @@ export class HttpDB extends BaseCommonDB implements CommonDB {
|
|
|
134
132
|
.json()
|
|
135
133
|
}
|
|
136
134
|
|
|
137
|
-
override async createTable(_schema: CommonSchema, _opt?: CommonDBCreateOptions): Promise<void> {
|
|
138
|
-
console.warn(`createTable not implemented`)
|
|
139
|
-
}
|
|
140
|
-
|
|
141
135
|
override streamQuery<ROW extends ObjectWithId>(
|
|
142
136
|
_q: DBQuery<ROW>,
|
|
143
137
|
_opt?: CommonDBStreamOptions,
|
|
@@ -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 '..'
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
+
import * as fs from 'fs'
|
|
1
2
|
import { _mapValues, _merge, _truncate } from '@naturalcycles/js-lib'
|
|
2
3
|
import { dimGrey, white } from '@naturalcycles/nodejs-lib/dist/colors'
|
|
3
4
|
import { dayjs } from '@naturalcycles/time-lib'
|
|
4
|
-
import * as fs from 'fs'
|
|
5
5
|
import * as yaml from 'js-yaml'
|
|
6
6
|
import type * as simpleGitLib from 'simple-git/promise'
|
|
7
7
|
import { BackendCfg } from './backend.cfg.util'
|
|
@@ -149,10 +149,10 @@ export function createAppYaml(
|
|
|
149
149
|
|
|
150
150
|
// appYamlPassEnv
|
|
151
151
|
require('dotenv').config() // ensure .env is read
|
|
152
|
-
// eslint-disable-next-line unicorn/no-array-reduce
|
|
153
152
|
const passEnv = appYamlPassEnv
|
|
154
153
|
.split(',')
|
|
155
154
|
.filter(Boolean)
|
|
155
|
+
// eslint-disable-next-line unicorn/no-array-reduce
|
|
156
156
|
.reduce((map, key) => {
|
|
157
157
|
const v = process.env[key]
|
|
158
158
|
if (!v) {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
+
import { inspect, InspectOptions } from 'util'
|
|
1
2
|
import { pDelay, _filterFalsyValues, _ms, _since } from '@naturalcycles/js-lib'
|
|
2
3
|
import { getGot } from '@naturalcycles/nodejs-lib'
|
|
3
4
|
import { dimGrey, red } from '@naturalcycles/nodejs-lib/dist/colors'
|
|
4
5
|
import { execCommand } from '@naturalcycles/nodejs-lib/dist/exec'
|
|
5
|
-
import { inspect, InspectOptions } from 'util'
|
|
6
6
|
import { coloredHttpCode } from '../server/request.log.util'
|
|
7
7
|
|
|
8
8
|
export interface DeployHealthCheckOptions {
|
package/src/index.ts
CHANGED
|
@@ -20,19 +20,19 @@ export class SentrySharedService {
|
|
|
20
20
|
// Reasons:
|
|
21
21
|
// 1. Can be useful is this module is imported but never actually used
|
|
22
22
|
// 2. Works around memory leak when used with Jest
|
|
23
|
-
const
|
|
23
|
+
const sentry = require('@sentry/node') as typeof SentryLib
|
|
24
24
|
|
|
25
25
|
if (this.sentryServiceCfg.dsn) {
|
|
26
26
|
// Sentry enabled
|
|
27
27
|
log('SentryService init...')
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
sentry.init({
|
|
31
31
|
maxValueLength: 2000, // default is 250 characters
|
|
32
32
|
...this.sentryServiceCfg,
|
|
33
33
|
})
|
|
34
34
|
|
|
35
|
-
return
|
|
35
|
+
return sentry
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
getRequestHandler(): RequestHandler {
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { HttpError } from '@naturalcycles/js-lib'
|
|
2
|
-
import { RequestHandler } from 'express'
|
|
3
|
-
import { Request } from 'express'
|
|
2
|
+
import { RequestHandler, Request } from 'express'
|
|
4
3
|
import { respondWithError } from '../error.util'
|
|
5
4
|
|
|
6
5
|
export interface BodyParserTimeoutCfg {
|
|
@@ -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()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { Server } from 'http'
|
|
1
2
|
import { _Memo, _ms } from '@naturalcycles/js-lib'
|
|
2
3
|
import { boldGrey, dimGrey, white } from '@naturalcycles/nodejs-lib/dist/colors'
|
|
3
|
-
import { Server } from 'http'
|
|
4
4
|
import { log } from '../log'
|
|
5
5
|
import { StartServerCfg, StartServerData } from './startServer.model'
|
|
6
6
|
|
|
@@ -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) => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { getGot, Got } from '@naturalcycles/nodejs-lib'
|
|
2
1
|
import { Server } from 'http'
|
|
3
2
|
import { AddressInfo } from 'net'
|
|
3
|
+
import { getGot, Got } from '@naturalcycles/nodejs-lib'
|
|
4
4
|
import { createDefaultApp, RequestHandlerCfg } from '..'
|
|
5
5
|
|
|
6
6
|
interface DestroyableServer extends Server {
|