@naturalcycles/backend-lib 9.80.0 → 9.82.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/db/httpDB.js +2 -0
- package/dist/express/createDefaultApp.d.ts +23 -0
- package/dist/express/createDefaultApp.js +4 -2
- package/dist/upload/getUploadedFiles.d.ts +1 -1
- package/package.json +2 -2
- package/src/admin/login.html +103 -76
- package/src/db/httpDB.ts +2 -0
- package/src/express/createDefaultApp.ts +34 -2
- package/src/upload/getUploadedFiles.ts +1 -1
package/dist/db/httpDB.js
CHANGED
|
@@ -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',
|
|
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.
|
|
4
|
+
"version": "9.82.0",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@naturalcycles/db-lib": "^10",
|
|
7
7
|
"@naturalcycles/js-lib": "^15",
|
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
"directory": "packages/backend-lib"
|
|
77
77
|
},
|
|
78
78
|
"engines": {
|
|
79
|
-
"node": ">=
|
|
79
|
+
"node": ">=26.12.1"
|
|
80
80
|
},
|
|
81
81
|
"description": "Standard library for making Express.js based backend services",
|
|
82
82
|
"author": "Natural Cycles Team",
|
package/src/admin/login.html
CHANGED
|
@@ -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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
<
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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 {
|
|
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
|
-
|
|
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,
|
|
72
|
+
const { autoLogin, adminTokenKey = 'admin_token' } = qs
|
|
67
73
|
const returnUrl = toSafeReturnUrl(qs.returnUrl)
|
|
68
74
|
// console.log(qs)
|
|
69
75
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
93
|
+
document.getElementById('loginBtn').addEventListener('click', login)
|
|
94
|
+
document.getElementById('logoutButton').addEventListener('click', logout)
|
|
91
95
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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)
|
|
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
|
-
|
|
111
|
-
app.loading = false
|
|
109
|
+
render(user)
|
|
112
110
|
if (!user) {
|
|
113
|
-
if (autoLogin)
|
|
111
|
+
if (autoLogin) void login()
|
|
114
112
|
} else {
|
|
115
|
-
const token = await
|
|
113
|
+
const token = await user.getIdToken()
|
|
116
114
|
|
|
117
115
|
// alert('idToken')
|
|
118
116
|
// console.log(idToken)
|
|
119
|
-
|
|
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 ${
|
|
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))
|
package/src/db/httpDB.ts
CHANGED
|
@@ -30,6 +30,8 @@ export class HttpDB extends BaseCommonDB implements CommonDB {
|
|
|
30
30
|
transactions: false,
|
|
31
31
|
patchByQuery: false,
|
|
32
32
|
increment: false,
|
|
33
|
+
// httpDBRequestHandler's options schema strips readAt
|
|
34
|
+
timeMachine: false,
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
constructor(public cfg: HttpDBCfg) {
|
|
@@ -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',
|
|
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)
|