@naturalcycles/backend-lib 9.80.0 → 9.81.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.
@@ -32,4 +32,27 @@ export interface DefaultAppCfg {
32
32
  bodyParserRawOptions?: Options;
33
33
  corsOptions?: CorsOptions;
34
34
  genericErrorMwCfg?: GenericErrorMiddlewareCfg;
35
+ /**
36
+ * Value for Express `trust proxy` setting, which defines how `req.ip`, `req.ips`,
37
+ * `req.protocol` and `req.hostname` are resolved from `X-Forwarded-*` headers.
38
+ *
39
+ * Defaults to `2` - trust exactly 2 hops: the socket peer (Google Front End) and the
40
+ * rightmost `X-Forwarded-For` entry (appended by the Google Cloud Load Balancer).
41
+ * This is the setup of Cloud Run behind a Load Balancer.
42
+ * Any further entries to the left could be injected by the client, so they are not trusted,
43
+ * which prevents IP address spoofing via `X-Forwarded-For` header.
44
+ *
45
+ * Do NOT set it to `true` in production - it trusts the leftmost `X-Forwarded-For` entry,
46
+ * which is fully client-controlled.
47
+ *
48
+ * Adjust it to match the hosting environment (number of proxies in front of the app).
49
+ *
50
+ * @default 2
51
+ */
52
+ trustProxy?: TrustProxy;
35
53
  }
54
+ /**
55
+ * Accepted values of Express `trust proxy` setting.
56
+ * See https://expressjs.com/en/guide/behind-proxies.html
57
+ */
58
+ export type TrustProxy = boolean | number | string | string[] | ((ip: string | undefined, hopIndex: number) => boolean);
@@ -5,6 +5,7 @@ import { logMiddleware } from '../server/logMiddleware.js';
5
5
  import { notFoundMiddleware } from '../server/notFoundMiddleware.js';
6
6
  import { requestTimeoutMiddleware } from '../server/requestTimeoutMiddleware.js';
7
7
  import { simpleRequestLoggerMiddleware } from '../server/simpleRequestLoggerMiddleware.js';
8
+ import { isCloudRun } from '../util.js';
8
9
  const isTest = process.env['APP_ENV'] === 'test';
9
10
  const isDev = process.env['APP_ENV'] === 'dev';
10
11
  const isGAE = !!process.env['GAE_INSTANCE'];
@@ -16,7 +17,7 @@ export async function createDefaultApp(cfg) {
16
17
  const app = express();
17
18
  app.disable('etag');
18
19
  app.disable('x-powered-by');
19
- app.set('trust proxy', true);
20
+ app.set('trust proxy', cfg.trustProxy ?? 2);
20
21
  // preHandlers
21
22
  useHandlers(app, cfg.preHandlers);
22
23
  app.use(logMiddleware());
@@ -43,8 +44,9 @@ export async function createDefaultApp(cfg) {
43
44
  maxAge: 86400,
44
45
  ...cfg.corsOptions,
45
46
  }));
46
- if (!isGAE && isTest) {
47
+ if (!isGAE && (isTest || isCloudRun())) {
47
48
  // compression is not used in AppEngine, because AppEngine provides it by default
49
+ // Cloud Run doesn't compress responses, so we do it ourselves
48
50
  app.use(compressionMiddleware());
49
51
  }
50
52
  // app.use(safeJsonMiddleware()) // optional
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@naturalcycles/backend-lib",
3
3
  "type": "module",
4
- "version": "9.80.0",
4
+ "version": "9.81.0",
5
5
  "dependencies": {
6
6
  "@naturalcycles/db-lib": "^10",
7
7
  "@naturalcycles/js-lib": "^15",
@@ -6,38 +6,59 @@
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
- <!-- 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
- />
9
+ <style>
10
+ body {
11
+ max-width: 45rem;
12
+ margin: 0 auto;
13
+ padding: 40px 1rem;
14
+ font-family: system-ui, sans-serif;
15
+ font-size: 1.15rem;
16
+ line-height: 1.5;
17
+ }
18
+ pre {
19
+ padding: 1rem 1.4rem;
20
+ overflow-x: auto;
21
+ background: #f5f7ff;
22
+ border: 1px solid #ccc;
23
+ border-radius: 5px;
24
+ }
25
+ button {
26
+ padding: 1rem 1.4rem;
27
+ font-size: 1.5rem;
28
+ color: #fff;
29
+ cursor: pointer;
30
+ background: #0d47a1;
31
+ border: none;
32
+ border-radius: 5px;
33
+ }
34
+ button:hover {
35
+ opacity: 0.8;
36
+ }
37
+ </style>
15
38
  </head>
16
39
  <body>
17
- <div id="app" style="padding: 40px 50px">
18
- <pre v-if="loading">Checking login...</pre>
19
- <div v-else>
20
- <div v-if="user">
21
- <p>Logged in as:</p>
22
- <pre><mark>{{ user.email }}</mark></pre>
23
- <pre><mark>{{ user.token }}</mark></pre>
24
- <button class="btn btn-primary btn-lg" @click="logout">Logout</button>
25
- </div>
26
- <div v-else>
27
- <button class="btn btn-primary btn-lg" @click="login" id="loginBtn">Login</button>
28
- </div>
29
- </div>
40
+ <pre id="loading">Checking login...</pre>
41
+
42
+ <div id="loggedIn" hidden>
43
+ <p>Logged in as:</p>
44
+ <pre><code id="email"></code></pre>
45
+ <pre><code id="token"></code></pre>
46
+ <button id="logoutButton">Logout</button>
47
+ </div>
48
+
49
+ <div id="loggedOut" hidden>
50
+ <button id="loginBtn">Login</button>
30
51
  </div>
31
52
 
32
53
  <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'
54
+ import { initializeApp } from 'https://www.gstatic.com/firebasejs/13.1.0/firebase-app.js'
35
55
  import {
36
56
  getAuth,
37
57
  GoogleAuthProvider,
38
58
  onAuthStateChanged,
39
59
  signInWithRedirect,
40
- } from 'https://www.gstatic.com/firebasejs/9.1.2/firebase-auth.js'
60
+ signOut,
61
+ } from 'https://www.gstatic.com/firebasejs/13.1.0/firebase-auth.js'
41
62
 
42
63
  const apiKey = '<%= firebaseApiKey %>'
43
64
  const authDomain = '<%= firebaseAuthDomain %>'
@@ -47,84 +68,59 @@
47
68
  alert(`Error: 'apiKey' or 'authDomain' is missing!`)
48
69
  }
49
70
 
50
- // Initialize Firebase
51
- initializeApp({
52
- apiKey,
53
- authDomain,
54
- })
55
-
56
- const auth = getAuth()
57
- const provider = new GoogleAuthProvider()
58
-
59
- onAuthStateChanged(auth, user => {
60
- // console.log('onAuthStateChanged, user: ', JSON.stringify(user, null, 2))
61
- // console.log('onAuthStateChanged, user: ', user)
62
- onUser(user)
63
- })
64
-
65
71
  const qs = Object.fromEntries(new URLSearchParams(location.search))
66
- const { autoLogin, logout, adminTokenKey = 'admin_token' } = qs
72
+ const { autoLogin, adminTokenKey = 'admin_token' } = qs
67
73
  const returnUrl = toSafeReturnUrl(qs.returnUrl)
68
74
  // console.log(qs)
69
75
 
70
- const app = createApp({
71
- data() {
72
- return {
73
- loading: 'Loading...',
74
- user: undefined,
75
- }
76
- },
77
- methods: {
78
- login: async function () {
79
- try {
80
- await signInWithRedirect(auth, provider)
81
- } catch (err) {
82
- logError(err)
83
- }
84
- },
76
+ // Initialize Firebase
77
+ const auth = getAuth(
78
+ initializeApp({
79
+ apiKey,
80
+ authDomain,
81
+ }),
82
+ )
83
+ const provider = new GoogleAuthProvider()
85
84
 
86
- logout: async function () {
87
- try {
88
- await auth.signOut()
85
+ const el = {
86
+ loading: document.getElementById('loading'),
87
+ loggedIn: document.getElementById('loggedIn'),
88
+ loggedOut: document.getElementById('loggedOut'),
89
+ email: document.getElementById('email'),
90
+ token: document.getElementById('token'),
91
+ }
89
92
 
90
- await postToken('logout') // magic string
93
+ document.getElementById('loginBtn').addEventListener('click', login)
94
+ document.getElementById('logoutButton').addEventListener('click', logout)
91
95
 
92
- if (logout && returnUrl) {
93
- alert('Logged out, redurecting back...')
94
- location.href = returnUrl
95
- }
96
- } catch (err) {
97
- logError(err)
98
- }
99
- },
100
- },
101
- }).mount('#app')
96
+ onAuthStateChanged(auth, user => {
97
+ // console.log('onAuthStateChanged, user: ', JSON.stringify(user, null, 2))
98
+ // console.log('onAuthStateChanged, user: ', user)
99
+ void onUser(user)
100
+ })
102
101
 
103
- if (logout) app.logout()
102
+ if (qs.logout) void logout()
104
103
 
105
104
  ////
106
105
 
107
106
  async function onUser(user) {
108
107
  try {
109
108
  // alert('onUser')
110
- app.user = user
111
- app.loading = false
109
+ render(user)
112
110
  if (!user) {
113
- if (autoLogin) app.login()
111
+ if (autoLogin) void login()
114
112
  } else {
115
- const token = await auth.currentUser.getIdToken()
113
+ const token = await user.getIdToken()
116
114
 
117
115
  // alert('idToken')
118
116
  // console.log(idToken)
119
- app.user = Object.assign({}, app.user, {
120
- token,
121
- })
117
+ render(user, token)
122
118
 
123
119
  await postToken(token)
124
120
 
125
121
  // Redirect if needed
126
122
  if (returnUrl) {
127
- // alert(`Logged in as ${app.user.email}, redirecting back...`)
123
+ // alert(`Logged in as ${user.email}, redirecting back...`)
128
124
  location.href = returnUrl
129
125
  }
130
126
  }
@@ -133,6 +129,37 @@
133
129
  }
134
130
  }
135
131
 
132
+ function render(user, token = '') {
133
+ el.loading.hidden = true
134
+ el.loggedIn.hidden = !user
135
+ el.loggedOut.hidden = !!user
136
+ el.email.textContent = user?.email || ''
137
+ el.token.textContent = token
138
+ }
139
+
140
+ async function login() {
141
+ try {
142
+ await signInWithRedirect(auth, provider)
143
+ } catch (err) {
144
+ logError(err)
145
+ }
146
+ }
147
+
148
+ async function logout() {
149
+ try {
150
+ await signOut(auth)
151
+
152
+ await postToken('logout') // magic string
153
+
154
+ if (qs.logout && returnUrl) {
155
+ alert('Logged out, redirecting back...')
156
+ location.href = returnUrl
157
+ }
158
+ } catch (err) {
159
+ logError(err)
160
+ }
161
+ }
162
+
136
163
  function logError(err) {
137
164
  console.error(err)
138
165
  alert('Error\n ' + JSON.stringify(err, null, 2))
@@ -14,6 +14,7 @@ import type {
14
14
  BackendRequestHandler,
15
15
  } from '../server/server.model.js'
16
16
  import { simpleRequestLoggerMiddleware } from '../server/simpleRequestLoggerMiddleware.js'
17
+ import { isCloudRun } from '../util.js'
17
18
 
18
19
  const isTest = process.env['APP_ENV'] === 'test'
19
20
  const isDev = process.env['APP_ENV'] === 'dev'
@@ -30,7 +31,7 @@ export async function createDefaultApp(cfg: DefaultAppCfg): Promise<BackendAppli
30
31
 
31
32
  app.disable('etag')
32
33
  app.disable('x-powered-by')
33
- app.set('trust proxy', true)
34
+ app.set('trust proxy', cfg.trustProxy ?? 2)
34
35
 
35
36
  // preHandlers
36
37
  useHandlers(app, cfg.preHandlers)
@@ -69,8 +70,9 @@ export async function createDefaultApp(cfg: DefaultAppCfg): Promise<BackendAppli
69
70
  }),
70
71
  )
71
72
 
72
- if (!isGAE && isTest) {
73
+ if (!isGAE && (isTest || isCloudRun())) {
73
74
  // compression is not used in AppEngine, because AppEngine provides it by default
75
+ // Cloud Run doesn't compress responses, so we do it ourselves
74
76
  app.use(compressionMiddleware())
75
77
  }
76
78
 
@@ -195,4 +197,34 @@ export interface DefaultAppCfg {
195
197
  corsOptions?: CorsOptions
196
198
 
197
199
  genericErrorMwCfg?: GenericErrorMiddlewareCfg
200
+
201
+ /**
202
+ * Value for Express `trust proxy` setting, which defines how `req.ip`, `req.ips`,
203
+ * `req.protocol` and `req.hostname` are resolved from `X-Forwarded-*` headers.
204
+ *
205
+ * Defaults to `2` - trust exactly 2 hops: the socket peer (Google Front End) and the
206
+ * rightmost `X-Forwarded-For` entry (appended by the Google Cloud Load Balancer).
207
+ * This is the setup of Cloud Run behind a Load Balancer.
208
+ * Any further entries to the left could be injected by the client, so they are not trusted,
209
+ * which prevents IP address spoofing via `X-Forwarded-For` header.
210
+ *
211
+ * Do NOT set it to `true` in production - it trusts the leftmost `X-Forwarded-For` entry,
212
+ * which is fully client-controlled.
213
+ *
214
+ * Adjust it to match the hosting environment (number of proxies in front of the app).
215
+ *
216
+ * @default 2
217
+ */
218
+ trustProxy?: TrustProxy
198
219
  }
220
+
221
+ /**
222
+ * Accepted values of Express `trust proxy` setting.
223
+ * See https://expressjs.com/en/guide/behind-proxies.html
224
+ */
225
+ export type TrustProxy =
226
+ | boolean
227
+ | number
228
+ | string
229
+ | string[]
230
+ | ((ip: string | undefined, hopIndex: number) => boolean)