@xeplr/base-apis 2.0.6 → 3.0.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/README.md +17 -9
- package/express.js +17 -10
- package/index.js +2 -0
- package/lib/remoteAuth.js +43 -5
- package/lib/sendTenantFile.js +35 -0
- package/lib/uploadRoute.js +11 -2
- package/package.json +12 -4
package/README.md
CHANGED
|
@@ -67,7 +67,8 @@ createApp(3000, 'myapp', {
|
|
|
67
67
|
| `remoteAuth(config)` | the default gate, as middleware |
|
|
68
68
|
| `gateRequiredEnv` | `['AUTH_URL']` |
|
|
69
69
|
| `checkEnv(names, options)` | missing-variable check |
|
|
70
|
-
| `uploadRoute(options)` | a standalone "receive a file, return its path" router |
|
|
70
|
+
| `uploadRoute(options)` | a standalone "receive a file, return its path" router — into the requester's own `uploads/` unless told otherwise |
|
|
71
|
+
| `sendTenantFile(res, name, { download? })` | serve a stored file back, only from the requesting tenant's own folder (404 another company's name, 400 a path out, 400 no tenant) |
|
|
71
72
|
| `sse` | `{ handler, createHandler, publish, defaultAuthorizeTopics }` |
|
|
72
73
|
| `runCluster(options)`, `broadcast(msg)`, `isPrimary()`, `isWorker()` | Node cluster helpers |
|
|
73
74
|
| `createHttpServer` | `{ init(port, appName, handler) }` — a plain `http.Server` |
|
|
@@ -116,24 +117,27 @@ Why on by default: a service that forgot to configure auth used to boot clean, p
|
|
|
116
117
|
|
|
117
118
|
`remoteAuth` sends the caller's `Authorization: Bearer …` **and the request it is serving** (`x-verify-method`, `x-verify-path`) to `url + endpoint` (`/auth/api/verify`). Auth answers who the caller is and whether they may call that API, so **every request is checked for access, not only for a signed-in token**; the answer's `user` and `access` go on `req.user` / `req.access`. See `@xeplr/auth`'s "Every request is checked".
|
|
118
119
|
|
|
120
|
+
**And whether the caller belongs to the company and workspace the request names.** When the app registered tenancy (`@xeplr/db`'s `registerMTs`) and the request carries those headers, the gate sends them as `x-verify-tenant` (`{"l1":"<company>","l2":"<workspace>"}`), and auth refuses a caller without a membership for each (`403 NOT_A_MEMBER`, Super Admin excepted) — before the app sees the request. So `mtMiddleware`'s tenant, which every query and every file path (`@xeplr/db`'s `dataDir`) reads, can be trusted on every request of every app, and no route has to add `mtMembershipMiddleware` itself. An auth that does not answer `tenant: 'checked'` (older than `@xeplr/auth` 1.4) is refused (503), never taken as a member.
|
|
121
|
+
|
|
119
122
|
| situation | response |
|
|
120
123
|
|---|---|
|
|
121
124
|
| path starts with a `publicPaths` entry | skipped |
|
|
122
125
|
| no `Bearer` token | 401 `No token provided` |
|
|
123
|
-
| auth answers `403` with `code` `DISABLED`, `HIDDEN` or `
|
|
126
|
+
| auth answers `403` with `code` `DISABLED`, `HIDDEN`, `UNLICENSED` or `NOT_A_MEMBER` | 403 with that `code`: signed in, but not allowed this API (`UNLICENSED`: its module is not in the caller's licence; `NOT_A_MEMBER`: not in the company or workspace the request names) |
|
|
127
|
+
| the request names a company, and auth answers without `tenant: 'checked'` | **503**: an auth that ignored the question is not an answer |
|
|
124
128
|
| auth answers 401 / other 403 | same status, `Invalid or expired token` |
|
|
125
129
|
| auth answers 200 **without** `state: 'enabled'` (an older auth, or an `endpoint` such as `/me` that only says who the caller is) | **503**: "did not say no" is not "said yes" |
|
|
126
130
|
| auth unreachable, times out (`timeoutMs`, 5000), answers anything else, or answers non-JSON | **503 `Authentication service unavailable` — never let through** |
|
|
127
131
|
|
|
128
132
|
- **Validated over HTTP, not in-process** — so a service needs no `@xeplr/auth` dependency (it would be a cycle), no JWT secret and no Redis. The cost is a hop per request.
|
|
129
133
|
- **A burst makes one call.** Requests with the same token, method and path that arrive while a check is on its way share its answer: ten at once, one call to auth. This holds with `cacheSeconds: 0` too.
|
|
130
|
-
- **`cacheSeconds` (default 5)** caches each answer (allowed or refused) per token, method and
|
|
134
|
+
- **`cacheSeconds` (default 5)** caches each answer (allowed or refused) per token, method, path **and company named**, per process — another company with the same token is asked about afresh; a revoked token or a changed mapping takes that long to reach here. `0` checks every request.
|
|
131
135
|
- **`X-New-Token` is forwarded** (with `Access-Control-Expose-Headers`) when the auth service slides a token, to every request in the burst that shared the check, so none is rejected because the token changed. That answer is **not cached** — otherwise the next requests would be answered from memory without telling the client to switch, and it would be logged out when the window closed.
|
|
132
136
|
|
|
133
137
|
### Uploads
|
|
134
138
|
|
|
135
139
|
```js
|
|
136
|
-
createApp(port, 'myapp', { upload: {
|
|
140
|
+
createApp(port, 'myapp', { upload: { allowedTypes: ['image/*'], maxSize: 2e6 } })
|
|
137
141
|
```
|
|
138
142
|
|
|
139
143
|
| option | default |
|
|
@@ -141,15 +145,19 @@ createApp(port, 'myapp', { upload: { destination: './uploads', allowedTypes: ['i
|
|
|
141
145
|
| `route` | `/internal/upload` — `POST`, multipart, behind the gate |
|
|
142
146
|
| `fieldName` | `file` |
|
|
143
147
|
| `maxFiles` | `10` |
|
|
144
|
-
| `destination` | `
|
|
148
|
+
| `destination` | **the requester's own `uploads/`** under `XEPLR_DATA_DIR` (`@xeplr/db`'s `dataDir`), asked per upload — never one folder for every company. A request with no tenant is refused. A folder named here is the app's own, and the app serves it |
|
|
145
149
|
| `allowedTypes` | `['*']` |
|
|
146
150
|
| `maxSize` | `5242880` (5 MB) |
|
|
147
|
-
| `serveRoute` | `/uploads` — static
|
|
151
|
+
| `serveRoute` | `/uploads` — `GET /uploads/:name`, **behind the gate**, from the requester's own `uploads/` (`sendTenantFile`). It used to be `express.static` over the whole folder before the gate: every company's files to anyone with a name. Only with the default `destination` |
|
|
148
152
|
| `auth` | a function passed to `@xeplr/utils`' `FileUploader` |
|
|
149
153
|
|
|
150
154
|
Returns `{ filename, originalName, size, mimetype }`, or an array of them for several files; 400 when none arrived. Files are stored under a generated id plus the original extension.
|
|
151
155
|
|
|
152
|
-
`uploadRoute({ destination, maxSize, allowedTypes, fieldName, auth })` is the same as a router of its own: `POST /` (single file) → `{ filePath, fileName, originalName, size, mimetype }`.
|
|
156
|
+
`uploadRoute({ destination, maxSize, allowedTypes, fieldName, auth })` is the same as a router of its own: `POST /` (single file) → `{ filePath, fileName, originalName, size, mimetype }`. With no `destination`, into the requester's own `uploads/`.
|
|
157
|
+
|
|
158
|
+
**Serving a stored file back:** `sendTenantFile(res, 'uploads/' + name)` — the one way. It reads only the requesting tenant's own folder: another company's name is 404, a path out (`../`, a link, a full path elsewhere) is 400, a request with no tenant is 400. Never `express.static` over a shared folder.
|
|
159
|
+
|
|
160
|
+
**From 2.x (3.0.0):** an upload with no folder named goes into the requester's own folder (it was `UPLOAD_DIR` / `./uploads`, shared), and `createApp`'s uploads are no longer served before the gate. Set `XEPLR_DATA_DIR` (`@xeplr/db`'s `dataRequiredEnv`) and register the application (`registerApplication`).
|
|
153
161
|
|
|
154
162
|
## genericRoute
|
|
155
163
|
|
|
@@ -266,7 +274,7 @@ broadcast({ type: 'cache-clear' }) // from a worker: relayed to every other wo
|
|
|
266
274
|
| variable | |
|
|
267
275
|
|---|---|
|
|
268
276
|
| `AUTH_URL` | the auth service base URL — **required** for the default gate |
|
|
269
|
-
| `
|
|
277
|
+
| `XEPLR_DATA_DIR` | the root of every tenant's files: an upload with no folder named goes in the requester's `uploads/` under it (`@xeplr/db` `dataRequiredEnv`) |
|
|
270
278
|
| `NODE_ENV` | anything but `production` puts the logger in dev mode |
|
|
271
279
|
|
|
272
280
|
## Tests
|
|
@@ -275,7 +283,7 @@ broadcast({ type: 'cache-clear' }) // from a worker: relayed to every other wo
|
|
|
275
283
|
npm test
|
|
276
284
|
```
|
|
277
285
|
|
|
278
|
-
The auth gate against a stand-in auth service (refusal to boot, 401 / 503, public paths, caching, `X-New-Token`), SSE publish and subscribe, and ticket-authorised SSE.
|
|
286
|
+
The auth gate against a stand-in auth service (refusal to boot, 401 / 503, public paths, caching, `X-New-Token`), SSE publish and subscribe, and ticket-authorised SSE. `tenant-gate.test.js`: the gate naming the company and workspace, refusing a non-member before the app sees the request, refusing an auth that did not check, caching per company; a real multipart upload landing in the company's own `uploads/` and served back only to it; `createApp`'s uploads no longer public; `sendTenantFile` refusing another company's name, a path out and no tenant.
|
|
279
287
|
|
|
280
288
|
## License
|
|
281
289
|
|
package/express.js
CHANGED
|
@@ -37,12 +37,12 @@ var logs = require('@xeplr/logs');
|
|
|
37
37
|
* @param {object} [options.log.emailService] - Email service instance
|
|
38
38
|
* @param {object} [options.upload] - File upload config
|
|
39
39
|
* @param {string} [options.upload.route='/internal/upload'] - Upload endpoint path
|
|
40
|
-
* @param {string} [options.upload.destination
|
|
40
|
+
* @param {string} [options.upload.destination] - A folder of the app's own; default: the requester's own uploads/ under XEPLR_DATA_DIR
|
|
41
41
|
* @param {string[]} [options.upload.allowedTypes=['*']] - MIME types
|
|
42
42
|
* @param {number} [options.upload.maxSize=5242880] - Max file size in bytes
|
|
43
43
|
* @param {string} [options.upload.fieldName='file'] - Form field name
|
|
44
44
|
* @param {number} [options.upload.maxFiles=10] - Max files per request
|
|
45
|
-
* @param {string} [options.upload.serveRoute='/uploads'] -
|
|
45
|
+
* @param {string} [options.upload.serveRoute='/uploads'] - Where an upload is fetched back, signed in, from the requester's own uploads/ (default destination only)
|
|
46
46
|
* @returns {{ app: Express, server: http.Server }}
|
|
47
47
|
*/
|
|
48
48
|
function createApp(port, appName, options) {
|
|
@@ -86,13 +86,6 @@ function createApp(port, appName, options) {
|
|
|
86
86
|
app.use(express.static(options.staticDir));
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
// ── File upload (static serve before auth, upload route after auth) ──
|
|
90
|
-
if (options.upload) {
|
|
91
|
-
var serveRoute = options.upload.serveRoute || '/uploads';
|
|
92
|
-
var destination = path.resolve(options.upload.destination || process.env.UPLOAD_DIR || './uploads');
|
|
93
|
-
app.use(serveRoute, express.static(destination));
|
|
94
|
-
}
|
|
95
|
-
|
|
96
89
|
// ── Auth gate ──
|
|
97
90
|
//
|
|
98
91
|
// ON BY DEFAULT. A service that forgets to configure auth must not answer
|
|
@@ -146,7 +139,10 @@ function createApp(port, appName, options) {
|
|
|
146
139
|
if (options.upload) {
|
|
147
140
|
var FileUploader = require('@xeplr/utils').FileUploader;
|
|
148
141
|
var uploadCfg = options.upload;
|
|
149
|
-
|
|
142
|
+
// Named: that folder. Not named: the requesting tenant's own uploads/
|
|
143
|
+
// under XEPLR_DATA_DIR, asked per upload — never one folder for every
|
|
144
|
+
// company (lib/uploadRoute.js tenantUploads).
|
|
145
|
+
var destination = uploadCfg.destination ? path.resolve(uploadCfg.destination) : require('./lib/uploadRoute').tenantUploads;
|
|
150
146
|
var uploader = new FileUploader({
|
|
151
147
|
auth: uploadCfg.auth || null,
|
|
152
148
|
allowedTypes: uploadCfg.allowedTypes || ['*'],
|
|
@@ -158,6 +154,17 @@ function createApp(port, appName, options) {
|
|
|
158
154
|
var fieldName = uploadCfg.fieldName || 'file';
|
|
159
155
|
var maxFiles = uploadCfg.maxFiles || 10;
|
|
160
156
|
|
|
157
|
+
// SERVED BACK SIGNED IN, from the requester's own uploads/ only. This was
|
|
158
|
+
// express.static over the whole folder, BEFORE the gate: every company's
|
|
159
|
+
// files to anyone who had a name. Now behind the gate (which checks the
|
|
160
|
+
// caller belongs to the company named) and read in that company's folder.
|
|
161
|
+
// With a folder of the app's own (`destination`), the app serves it.
|
|
162
|
+
if (!uploadCfg.destination) {
|
|
163
|
+
var serveRoute = uploadCfg.serveRoute || '/uploads';
|
|
164
|
+
var sendTenantFile = require('./lib/sendTenantFile');
|
|
165
|
+
app.get(serveRoute + '/:name', function(req, res) { sendTenantFile(res, 'uploads/' + req.params.name); });
|
|
166
|
+
}
|
|
167
|
+
|
|
161
168
|
app.post(uploadRoute, uploader.array(fieldName, maxFiles), function(req, res) {
|
|
162
169
|
if (!req.files || req.files.length === 0) {
|
|
163
170
|
return res.status(400).json({ error: 'No files uploaded' });
|
package/index.js
CHANGED
|
@@ -15,6 +15,8 @@ module.exports = {
|
|
|
15
15
|
remoteAuth: require('./lib/remoteAuth'),
|
|
16
16
|
genericController: require('./lib/genericController'),
|
|
17
17
|
uploadRoute: require('./lib/uploadRoute'),
|
|
18
|
+
// Serve a stored file back, only from the requesting tenant's own folder.
|
|
19
|
+
sendTenantFile: require('./lib/sendTenantFile'),
|
|
18
20
|
runCluster: require('./lib/cluster').runCluster,
|
|
19
21
|
broadcast: require('./lib/cluster').broadcast,
|
|
20
22
|
isPrimary: require('./lib/cluster').isPrimary,
|
package/lib/remoteAuth.js
CHANGED
|
@@ -37,7 +37,33 @@ var DEFAULT_TIMEOUT_MS = 5000;
|
|
|
37
37
|
// The 403 codes auth gives for a signed-in caller this API is refused to. Any
|
|
38
38
|
// other 403 is a token problem. UNLICENSED: the API's module is not in the
|
|
39
39
|
// caller's licence (@xeplr/auth's licence.use).
|
|
40
|
-
|
|
40
|
+
// NOT_A_MEMBER: the request names a company or workspace the caller does not
|
|
41
|
+
// belong to (see tenantOf).
|
|
42
|
+
var REFUSALS = ['DISABLED', 'HIDDEN', 'UNLICENSED', 'NOT_A_MEMBER'];
|
|
43
|
+
var LEVELS = ['l1', 'l2', 'l3', 'l4'];
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* THE COMPANY AND WORKSPACE THIS REQUEST NAMES, for auth to check membership
|
|
47
|
+
* of — in this gate, so it is checked for every request of every app built on
|
|
48
|
+
* createApp, and no route can forget it (@xeplr/db's mtMiddleware would
|
|
49
|
+
* otherwise trust whatever the header says). By level, as auth keeps
|
|
50
|
+
* memberships: '{"l1":"<company>","l2":"<workspace>"}', or null when the app
|
|
51
|
+
* registered no tenancy or the request names none. Read from the app's own
|
|
52
|
+
* registerMTs(), per request.
|
|
53
|
+
*/
|
|
54
|
+
function tenantOf(req) {
|
|
55
|
+
var mt;
|
|
56
|
+
try { mt = require('@xeplr/db').getMtConfig(); } catch (e) { return null; }
|
|
57
|
+
if (!mt || !mt.enabled) return null;
|
|
58
|
+
var named = {};
|
|
59
|
+
var any = false;
|
|
60
|
+
LEVELS.forEach(function (level) {
|
|
61
|
+
var slot = mt.slots && mt.slots[level];
|
|
62
|
+
var value = slot && req.headers[slot.header];
|
|
63
|
+
if (value) { named[level] = String(value); any = true; }
|
|
64
|
+
});
|
|
65
|
+
return any ? JSON.stringify(named) : null;
|
|
66
|
+
}
|
|
41
67
|
|
|
42
68
|
/**
|
|
43
69
|
* @param {object} config
|
|
@@ -99,11 +125,13 @@ function remoteAuth(config) {
|
|
|
99
125
|
}
|
|
100
126
|
|
|
101
127
|
// One call to auth → an outcome every waiter can apply.
|
|
102
|
-
async function ask(header, method, path) {
|
|
128
|
+
async function ask(header, method, path, tenant) {
|
|
103
129
|
var response;
|
|
130
|
+
var headers = { authorization: header, 'x-verify-method': method, 'x-verify-path': path };
|
|
131
|
+
if (tenant) headers['x-verify-tenant'] = tenant;
|
|
104
132
|
try {
|
|
105
133
|
response = await fetch(url + endpoint, {
|
|
106
|
-
headers:
|
|
134
|
+
headers: headers,
|
|
107
135
|
signal: AbortSignal.timeout(timeoutMs)
|
|
108
136
|
});
|
|
109
137
|
} catch (err) {
|
|
@@ -153,6 +181,12 @@ function remoteAuth(config) {
|
|
|
153
181
|
if (!body || body.state !== 'enabled') {
|
|
154
182
|
return { kind: 'unavailable', why: 'auth answered without an access state; update @xeplr/auth' };
|
|
155
183
|
}
|
|
184
|
+
// Asked about a company and not told it was checked: an older auth that
|
|
185
|
+
// ignored the question. Refused for the same reason — "did not say no" is
|
|
186
|
+
// not "is a member".
|
|
187
|
+
if (tenant && body.tenant !== 'checked') {
|
|
188
|
+
return { kind: 'unavailable', why: 'auth did not check membership of the company named; update @xeplr/auth to 1.4 or later' };
|
|
189
|
+
}
|
|
156
190
|
|
|
157
191
|
return { kind: 'ok', newToken: newToken, user: body.user, access: body.access };
|
|
158
192
|
}
|
|
@@ -188,7 +222,10 @@ function remoteAuth(config) {
|
|
|
188
222
|
var token = header.slice('Bearer '.length);
|
|
189
223
|
var method = req.method;
|
|
190
224
|
var path = (req.baseUrl || '') + req.path;
|
|
191
|
-
var
|
|
225
|
+
var tenant = tenantOf(req);
|
|
226
|
+
// The answer is about this token, this request AND this company: another
|
|
227
|
+
// company named with the same token is asked about afresh.
|
|
228
|
+
var key = token + '\n' + method + ' ' + path + '\n' + (tenant || '');
|
|
192
229
|
|
|
193
230
|
if (cacheSeconds > 0) {
|
|
194
231
|
var hit = cache.get(key);
|
|
@@ -197,7 +234,7 @@ function remoteAuth(config) {
|
|
|
197
234
|
|
|
198
235
|
var inFlight = pending.get(key);
|
|
199
236
|
if (!inFlight) {
|
|
200
|
-
inFlight = ask(header, method, path);
|
|
237
|
+
inFlight = ask(header, method, path, tenant);
|
|
201
238
|
pending.set(key, inFlight);
|
|
202
239
|
inFlight.then(function (outcome) {
|
|
203
240
|
pending.delete(key);
|
|
@@ -220,3 +257,4 @@ function remoteAuth(config) {
|
|
|
220
257
|
}
|
|
221
258
|
|
|
222
259
|
module.exports = remoteAuth;
|
|
260
|
+
module.exports.tenantOf = tenantOf;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// SEND A FILE BACK — only from the requesting tenant's own folder.
|
|
2
|
+
//
|
|
3
|
+
// The one way an app serves a stored file: the name is resolved inside the
|
|
4
|
+
// tenant's folder under XEPLR_DATA_DIR (@xeplr/db's dataDir), so another
|
|
5
|
+
// company's name finds nothing here, and a path that climbs out (../, a link,
|
|
6
|
+
// an absolute path elsewhere) is refused before anything is opened. The
|
|
7
|
+
// tenant is the request's, checked by the gate (createApp asks auth whether
|
|
8
|
+
// the caller belongs to it) — never what the client names.
|
|
9
|
+
//
|
|
10
|
+
// router.get('/files/:name', function(req, res) { sendTenantFile(res, 'uploads/' + req.params.name); });
|
|
11
|
+
//
|
|
12
|
+
// 404 when there is nothing there; 400 when the name is not one in the
|
|
13
|
+
// tenant's folder. Never express.static over a shared folder.
|
|
14
|
+
var fs = require('fs');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param res the Express response
|
|
18
|
+
* @param name a name in the tenant's folder, e.g. 'report-assets/logo.png'
|
|
19
|
+
* @param options { download: true | '<file name>' } to send as an attachment
|
|
20
|
+
*/
|
|
21
|
+
function sendTenantFile(res, name, options) {
|
|
22
|
+
options = options || {};
|
|
23
|
+
var file;
|
|
24
|
+
try {
|
|
25
|
+
file = require('@xeplr/db').dataDir.resolve(String(name || ''), 'The file');
|
|
26
|
+
} catch (err) {
|
|
27
|
+
var status = err.code === 'DATA_DIR_UNSET' ? 500 : 400;
|
|
28
|
+
return res.status(status).json({ error: status === 500 ? 'Files are not set up on this server' : err.message, code: err.code });
|
|
29
|
+
}
|
|
30
|
+
if (!fs.existsSync(file) || !fs.statSync(file).isFile()) return res.status(404).json({ error: 'No such file' });
|
|
31
|
+
if (options.download) return res.download(file, options.download === true ? undefined : options.download);
|
|
32
|
+
return res.sendFile(file);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
module.exports = sendTenantFile;
|
package/lib/uploadRoute.js
CHANGED
|
@@ -18,7 +18,10 @@ var FileUploader = require('@xeplr/utils/lib/fileUploader');
|
|
|
18
18
|
* → { filePath, fileName, originalName, size, mimetype }
|
|
19
19
|
*
|
|
20
20
|
* @param {object} [options]
|
|
21
|
-
* @param {string} [options.destination
|
|
21
|
+
* @param {string|Function} [options.destination] - where to keep the file: a
|
|
22
|
+
* folder, or (req) => folder. DEFAULT: the requesting tenant's own
|
|
23
|
+
* `uploads/` folder under XEPLR_DATA_DIR (@xeplr/db's dataDir) — never one
|
|
24
|
+
* folder for every company. A request with no tenant is refused.
|
|
22
25
|
* @param {number} [options.maxSize=5MB]
|
|
23
26
|
* @param {string[]} [options.allowedTypes=['*']]
|
|
24
27
|
* @param {string} [options.fieldName='file']
|
|
@@ -31,7 +34,7 @@ function uploadRoute(options) {
|
|
|
31
34
|
var fieldName = options.fieldName || 'file';
|
|
32
35
|
|
|
33
36
|
var uploader = new FileUploader({
|
|
34
|
-
destination: options.destination,
|
|
37
|
+
destination: options.destination || tenantUploads,
|
|
35
38
|
maxSize: options.maxSize,
|
|
36
39
|
allowedTypes: options.allowedTypes
|
|
37
40
|
});
|
|
@@ -53,4 +56,10 @@ function uploadRoute(options) {
|
|
|
53
56
|
return router;
|
|
54
57
|
}
|
|
55
58
|
|
|
59
|
+
/** The requesting tenant's uploads/ — asked per upload, inside its context. */
|
|
60
|
+
function tenantUploads() {
|
|
61
|
+
return require('@xeplr/db').dataDir.resolve('uploads', 'The uploads folder');
|
|
62
|
+
}
|
|
63
|
+
|
|
56
64
|
module.exports = uploadRoute;
|
|
65
|
+
module.exports.tenantUploads = tenantUploads;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xeplr/base-apis",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"description": "Base API bootstrapper — Express and plain HTTP server factories with middleware injection",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -31,23 +31,31 @@
|
|
|
31
31
|
"access": "public"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
+
"@xeplr/logs": "^1.0.0",
|
|
35
|
+
"@xeplr/utils": "^1.1.0",
|
|
34
36
|
"cookie-parser": "^1.4.7",
|
|
35
37
|
"cors": "^2.8.6",
|
|
36
38
|
"debug": "^4.4.3",
|
|
37
|
-
"http-errors": "^2.0.1"
|
|
38
|
-
"@xeplr/logs": "^1.0.0",
|
|
39
|
-
"@xeplr/utils": "^1.0.0"
|
|
39
|
+
"http-errors": "^2.0.1"
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
42
|
+
"@xeplr/db": "^1.1.0",
|
|
42
43
|
"express": "^4.0.0 || ^5.0.0",
|
|
43
44
|
"multer": "^2.3.0"
|
|
44
45
|
},
|
|
45
46
|
"peerDependenciesMeta": {
|
|
46
47
|
"multer": {
|
|
47
48
|
"optional": true
|
|
49
|
+
},
|
|
50
|
+
"@xeplr/db": {
|
|
51
|
+
"optional": true
|
|
48
52
|
}
|
|
49
53
|
},
|
|
50
54
|
"scripts": {
|
|
51
55
|
"test": "node --test test/*.test.js"
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"@xeplr/db": "^1.1.0",
|
|
59
|
+
"multer": "^2.5.0"
|
|
52
60
|
}
|
|
53
61
|
}
|