@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.
Files changed (37) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/dist/admin/admin.mw.d.ts +5 -0
  3. package/dist/admin/admin.mw.js +3 -3
  4. package/dist/admin/base.admin.service.d.ts +11 -1
  5. package/dist/admin/base.admin.service.js +34 -0
  6. package/dist/admin/login.html +57 -44
  7. package/dist/db/httpDB.d.ts +3 -3
  8. package/dist/db/httpDB.js +1 -4
  9. package/dist/db/httpDBRequestHandler.d.ts +2 -1
  10. package/dist/deploy/deploy.util.js +2 -2
  11. package/dist/deploy/deployHealthCheck.js +1 -1
  12. package/dist/index.d.ts +0 -10
  13. package/dist/sentry/sentry.shared.service.js +3 -3
  14. package/dist/server/deployInfo.util.js +1 -1
  15. package/dist/server/handlers/reqValidation.mw.js +1 -0
  16. package/dist/server/handlers/statusHandler.d.ts +2 -7
  17. package/dist/server/handlers/statusHandler.js +10 -12
  18. package/dist/server/startServer.js +1 -1
  19. package/dist/server/startServer.model.d.ts +1 -1
  20. package/dist/testing/express.test.service.d.ts +1 -1
  21. package/package.json +2 -2
  22. package/src/admin/admin.mw.ts +11 -5
  23. package/src/admin/base.admin.service.ts +40 -2
  24. package/src/admin/login.html +57 -44
  25. package/src/db/httpDB.ts +3 -9
  26. package/src/db/httpDBRequestHandler.ts +1 -1
  27. package/src/deploy/deploy.util.ts +2 -2
  28. package/src/deploy/deployHealthCheck.ts +1 -1
  29. package/src/index.ts +0 -11
  30. package/src/sentry/sentry.shared.service.ts +3 -3
  31. package/src/server/deployInfo.util.ts +1 -1
  32. package/src/server/handlers/bodyParserTimeout.mw.ts +1 -2
  33. package/src/server/handlers/reqValidation.mw.ts +1 -0
  34. package/src/server/handlers/statusHandler.ts +9 -20
  35. package/src/server/startServer.model.ts +1 -1
  36. package/src/server/startServer.ts +2 -2
  37. 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
 
@@ -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;
@@ -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;
@@ -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
- <script src="https://www.gstatic.com/firebasejs/6.3.5/firebase-app.js"></script>
10
- <script src="https://www.gstatic.com/firebasejs/6.3.5/firebase-auth.js"></script>
11
- <script src="https://unpkg.com/vue@2.6.10/dist/vue.min.js"></script>
12
-
13
- <link href="https://unpkg.com/bootstrap@4.1.3/dist/css/bootstrap.min.css" rel="stylesheet" />
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
- const qs = parseQuery(location.search)
41
- const { autoLogin, logout, returnUrl, adminTokenKey = 'admin_token' } = qs
42
- console.log(qs)
50
+ // Initialize Firebase
51
+ initializeApp({
52
+ apiKey,
53
+ authDomain,
54
+ })
43
55
 
44
- const provider = new firebase.auth[authProvider]()
56
+ const auth = getAuth()
57
+ const provider = new GoogleAuthProvider()
45
58
 
46
- const app = new Vue({
47
- el: '#app',
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
- data: {
50
- loading: 'Loading...',
51
- user: undefined,
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 firebase.auth().signInWithRedirect(provider)
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 firebase.auth().signOut()
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 firebase.auth().currentUser.getIdToken(true)
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>
@@ -1,4 +1,5 @@
1
- import { BaseCommonDB, CommonDB, CommonDBCreateOptions, CommonDBOptions, CommonDBSaveOptions, CommonDBStreamOptions, CommonSchema, DBQuery, ObjectWithId, RunQueryResult } from '@naturalcycles/db-lib';
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<CommonSchema<ROW>>;
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, ObjectWithId } from '@naturalcycles/db-lib';
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 Sentry = require('@sentry/node');
20
+ const sentry = require('@sentry/node');
21
21
  if (this.sentryServiceCfg.dsn) {
22
22
  // Sentry enabled
23
23
  log('SentryService init...');
24
24
  }
25
- Sentry.init({
25
+ sentry.init({
26
26
  maxValueLength: 2000,
27
27
  ...this.sentryServiceCfg,
28
28
  });
29
- return Sentry;
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
- * @returns unix timestamp in millis
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
- function statusHandler(serverStartedCallback, projectDir, extra) {
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(serverStartedCallback, projectDir, extra));
12
+ res.json(statusHandlerData(projectDir, extra));
14
13
  };
15
14
  }
16
15
  exports.statusHandler = statusHandler;
17
- function statusHandlerData(serverStartedCallback = defaultServerStartedCallback, projectDir = process.cwd(), extra) {
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(serverStartedCallback()),
22
+ started: getStartedStr(),
24
23
  deployBuildTimeUTC,
25
24
  APP_ENV,
26
25
  prod,
27
26
  buildInfo,
28
- GAE_APPLICATION: process.env.GAE_APPLICATION,
29
- GAE_SERVICE: process.env.GAE_SERVICE,
30
- GAE_VERSION: process.env.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(serverStarted) {
40
- if (!serverStarted)
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.PORT) || cfgPort || 8080;
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 { Application } from 'express';
3
2
  import { Server } from 'http';
3
+ import { Application } from 'express';
4
4
  export interface StartServerCfg {
5
5
  /**
6
6
  * @default process.env.PORT || 8080
@@ -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.0",
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": "^9.0.0",
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",
@@ -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}?autoLogin=1&returnUrl=\${encodeURIComponent(location.href)}${
62
- apiHost ? '&apiHost=' + apiHost : ''
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
  }
@@ -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
- <script src="https://www.gstatic.com/firebasejs/6.3.5/firebase-app.js"></script>
10
- <script src="https://www.gstatic.com/firebasejs/6.3.5/firebase-auth.js"></script>
11
- <script src="https://unpkg.com/vue@2.6.10/dist/vue.min.js"></script>
12
-
13
- <link href="https://unpkg.com/bootstrap@4.1.3/dist/css/bootstrap.min.css" rel="stylesheet" />
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
- const qs = parseQuery(location.search)
41
- const { autoLogin, logout, returnUrl, adminTokenKey = 'admin_token' } = qs
42
- console.log(qs)
50
+ // Initialize Firebase
51
+ initializeApp({
52
+ apiKey,
53
+ authDomain,
54
+ })
43
55
 
44
- const provider = new firebase.auth[authProvider]()
56
+ const auth = getAuth()
57
+ const provider = new GoogleAuthProvider()
45
58
 
46
- const app = new Vue({
47
- el: '#app',
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
- data: {
50
- loading: 'Loading...',
51
- user: undefined,
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 firebase.auth().signInWithRedirect(provider)
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 firebase.auth().signOut()
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 firebase.auth().currentUser.getIdToken(true)
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<CommonSchema<ROW>> {
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
@@ -112,14 +112,3 @@ export {
112
112
  validateParams,
113
113
  validateQuery,
114
114
  }
115
-
116
- declare global {
117
- namespace NodeJS {
118
- interface ProcessEnv {
119
- PORT?: string
120
- GAE_APPLICATION?: string
121
- GAE_SERVICE?: string
122
- GAE_VERSION?: string
123
- }
124
- }
125
- }
@@ -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 Sentry = require('@sentry/node') as typeof SentryLib
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
- Sentry.init({
30
+ sentry.init({
31
31
  maxValueLength: 2000, // default is 250 characters
32
32
  ...this.sentryServiceCfg,
33
33
  })
34
34
 
35
- return Sentry
35
+ return sentry
36
36
  }
37
37
 
38
38
  getRequestHandler(): RequestHandler {
@@ -1,5 +1,5 @@
1
- import { _memoFn } from '@naturalcycles/js-lib'
2
1
  import * as fs from 'fs'
2
+ import { _memoFn } from '@naturalcycles/js-lib'
3
3
  import type { DeployInfo } from '../deploy/deploy.model'
4
4
 
5
5
  export const getDeployInfo = _memoFn((projectDir: string): DeployInfo => {
@@ -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 {
@@ -23,6 +23,7 @@ export function reqValidation(
23
23
  if (opt.redactPaths) {
24
24
  redact(opt.redactPaths, req[reqProperty], error)
25
25
  error.data.joiValidationErrorItems.length = 0 // clears the array
26
+ delete error.data.annotation
26
27
  }
27
28
 
28
29
  return next(
@@ -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(serverStartedCallback, projectDir, extra))
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(serverStartedCallback()),
26
+ started: getStartedStr(),
38
27
  deployBuildTimeUTC,
39
28
  APP_ENV,
40
29
  prod,
41
30
  buildInfo,
42
- GAE_APPLICATION: process.env.GAE_APPLICATION,
43
- GAE_SERVICE: process.env.GAE_SERVICE,
44
- GAE_VERSION: process.env.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(serverStarted?: number): string {
54
- if (!serverStarted) return 'not started yet'
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,5 +1,5 @@
1
- import { Application } from 'express'
2
1
  import { Server } from 'http'
2
+ import { Application } from 'express'
3
3
 
4
4
  export interface StartServerCfg {
5
5
  /**
@@ -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.PORT) || cfgPort || 8080
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 {