@flowerforce/flowerbase 1.8.1 → 1.8.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/README.md +103 -19
- package/dist/features/functions/utils.d.ts +1 -1
- package/dist/features/functions/utils.d.ts.map +1 -1
- package/dist/features/functions/utils.js +1 -1
- package/dist/utils/initializer/exposeRoutes.d.ts.map +1 -1
- package/dist/utils/initializer/exposeRoutes.js +40 -6
- package/dist/utils/roles/machines/write/B/index.d.ts.map +1 -1
- package/dist/utils/roles/machines/write/B/index.js +3 -0
- package/dist/utils/rules-matcher/utils.d.ts.map +1 -1
- package/dist/utils/rules-matcher/utils.js +7 -0
- package/package.json +1 -1
- package/src/features/functions/__tests__/utils.test.ts +33 -0
- package/src/features/functions/utils.ts +6 -1
- package/src/utils/__tests__/WRITE_STEP_B_STATES.test.ts +5 -6
- package/src/utils/__tests__/exposeRoutes.test.ts +26 -1
- package/src/utils/__tests__/rulesMatcherUtils.test.ts +31 -1
- package/src/utils/initializer/exposeRoutes.ts +39 -5
- package/src/utils/roles/machines/write/B/index.ts +3 -0
- package/src/utils/rules-matcher/utils.ts +14 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
## 1.8.3 (2026-04-02)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### 🩹 Fixes
|
|
5
|
+
|
|
6
|
+
- updateOne options ([5b16467](https://github.com/flowerforce/flowerbase/commit/5b16467))
|
|
7
|
+
|
|
8
|
+
## 1.8.2 (2026-03-31)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### 🩹 Fixes
|
|
12
|
+
|
|
13
|
+
- apply_when ([f2bcef2](https://github.com/flowerforce/flowerbase/commit/f2bcef2))
|
|
14
|
+
|
|
15
|
+
- forwarded host ([#52](https://github.com/flowerforce/flowerbase/pull/52))
|
|
16
|
+
|
|
1
17
|
## 1.8.1 (2026-03-27)
|
|
2
18
|
|
|
3
19
|
|
package/README.md
CHANGED
|
@@ -557,6 +557,45 @@ MONIT_ALLOW_EDIT=true
|
|
|
557
557
|
- If `MONIT_ALLOWED_IPS` is set, only those IPs can reach `/monit` (ensure `req.ip` reflects your proxy setup / `trustProxy`).
|
|
558
558
|
- You can disable **invoke** or **edit** with `MONIT_ALLOW_INVOKE=false` and/or `MONIT_ALLOW_EDIT=false`.
|
|
559
559
|
|
|
560
|
+
### 🔐 Reverse Proxy and SSL Termination
|
|
561
|
+
|
|
562
|
+
When Flowerbase runs behind Nginx or HAProxy with SSL termination, make sure the proxy forwards the public scheme/host/port headers.
|
|
563
|
+
|
|
564
|
+
Flowerbase uses these headers for `GET /api/client/<version>/app/:appId/location`, and Realm-style clients use that response to build auth URLs (including `/login`).
|
|
565
|
+
|
|
566
|
+
- Header priority for host and scheme is: `Forwarded` first, then `X-Forwarded-*`, then `Host`.
|
|
567
|
+
- If `X-Forwarded-Port` is set and host has no explicit port, Flowerbase appends it.
|
|
568
|
+
|
|
569
|
+
#### Nginx
|
|
570
|
+
|
|
571
|
+
```nginx
|
|
572
|
+
location / {
|
|
573
|
+
proxy_pass http://flowerbase_upstream;
|
|
574
|
+
proxy_set_header Host $http_host;
|
|
575
|
+
proxy_set_header X-Forwarded-Proto $scheme;
|
|
576
|
+
proxy_set_header X-Forwarded-Host $http_host;
|
|
577
|
+
proxy_set_header X-Forwarded-Port $server_port;
|
|
578
|
+
}
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
#### HAProxy
|
|
582
|
+
|
|
583
|
+
```haproxy
|
|
584
|
+
frontend fe_https
|
|
585
|
+
bind *:443 ssl crt /etc/haproxy/certs/site.pem
|
|
586
|
+
mode http
|
|
587
|
+
default_backend be_flowerbase
|
|
588
|
+
|
|
589
|
+
backend be_flowerbase
|
|
590
|
+
mode http
|
|
591
|
+
server app1 127.0.0.1:3000 check
|
|
592
|
+
http-request set-header Host %[req.hdr(host)]
|
|
593
|
+
http-request set-header X-Forwarded-Proto https if { ssl_fc }
|
|
594
|
+
http-request set-header X-Forwarded-Proto http if !{ ssl_fc }
|
|
595
|
+
http-request set-header X-Forwarded-Host %[req.hdr(host)]
|
|
596
|
+
http-request set-header X-Forwarded-Port %[dst_port]
|
|
597
|
+
```
|
|
598
|
+
|
|
560
599
|
|
|
561
600
|
<a id="build"></a>
|
|
562
601
|
## 🚀 Build & Deploy the Server
|
|
@@ -577,35 +616,80 @@ Once deployed, you'll receive a public URL (e.g. https://your-app-name.up.exampl
|
|
|
577
616
|
|
|
578
617
|
>This URL should be used as the base URL in your frontend application, as explained in the next section.
|
|
579
618
|
|
|
580
|
-
## 🌐 Frontend Setup –
|
|
619
|
+
## 🌐 Frontend Setup – `@flowerforce/flowerbase-client` (Recommended)
|
|
581
620
|
|
|
582
|
-
|
|
583
|
-
This serves as a sample setup — similar logic can be applied using other official Realm SDKs **(e.g. React Native, Node, or Flutter)**.
|
|
584
|
-
|
|
585
|
-
### 📦 Install Realm SDK
|
|
621
|
+
For frontend and mobile projects, you can use the dedicated Flowerbase client:
|
|
586
622
|
|
|
587
623
|
```bash
|
|
588
|
-
npm install
|
|
624
|
+
npm install @flowerforce/flowerbase-client
|
|
589
625
|
```
|
|
590
626
|
|
|
591
|
-
### ⚙️ Configure
|
|
592
|
-
|
|
593
|
-
Create a file to initialize and export the Realm App instance:
|
|
627
|
+
### ⚙️ Configure client app
|
|
594
628
|
|
|
595
629
|
```ts
|
|
596
|
-
|
|
630
|
+
import { App, Credentials } from '@flowerforce/flowerbase-client'
|
|
597
631
|
|
|
598
|
-
|
|
632
|
+
const app = new App({
|
|
633
|
+
id: 'your-app-id',
|
|
634
|
+
baseUrl: 'https://your-deployed-backend-url.com',
|
|
635
|
+
timeout: 10000
|
|
636
|
+
})
|
|
599
637
|
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
638
|
+
await app.logIn(Credentials.emailPassword('user@example.com', 'secret'))
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
### 📦 Common client operations
|
|
642
|
+
|
|
643
|
+
```ts
|
|
644
|
+
const user = app.currentUser
|
|
645
|
+
if (!user) throw new Error('User not logged in')
|
|
646
|
+
|
|
647
|
+
const profile = await user.functions.getProfile()
|
|
605
648
|
|
|
606
|
-
|
|
649
|
+
const todos = user.mongoClient('mongodb-atlas')
|
|
650
|
+
.db('my-db')
|
|
651
|
+
.collection('todos')
|
|
607
652
|
|
|
653
|
+
await todos.insertOne({ title: 'Ship docs update', done: false })
|
|
654
|
+
const openTodos = await todos.find({ done: false })
|
|
608
655
|
```
|
|
609
656
|
|
|
610
|
-
|
|
611
|
-
|
|
657
|
+
`@flowerforce/flowerbase-client` supports:
|
|
658
|
+
- local-userpass / anon-user / custom-function authentication
|
|
659
|
+
- function calls (`user.functions.<name>(...)`)
|
|
660
|
+
- MongoDB operations via `user.mongoClient('mongodb-atlas')`
|
|
661
|
+
- change streams with `watch()` async iterator
|
|
662
|
+
- BSON/EJSON interoperability (`ObjectId`, `Date`, etc.)
|
|
663
|
+
|
|
664
|
+
## 💡 Use Cases by Feature
|
|
665
|
+
|
|
666
|
+
### 🔐 Authentication
|
|
667
|
+
- Registration and login flows for SaaS dashboards using `local-userpass`.
|
|
668
|
+
- Guest sessions for trial users with `anon-user`, then account upgrade with full registration.
|
|
669
|
+
- Delegated enterprise login with `custom-function` auth when credentials must be validated by external identity logic.
|
|
670
|
+
|
|
671
|
+
### 🔒 Rules
|
|
672
|
+
- Multi-tenant isolation where each user can only read/write documents of their own workspace.
|
|
673
|
+
- Field-level protection to hide private fields (for example billing or internal notes) from non-admin users.
|
|
674
|
+
|
|
675
|
+
### ⚙️ Functions
|
|
676
|
+
- Centralized business logic (pricing, counters, workflows) called from web and mobile clients.
|
|
677
|
+
- Privileged server-side tasks invoked with `run_as_system` to perform safe internal operations.
|
|
678
|
+
|
|
679
|
+
### 🔔 Triggers
|
|
680
|
+
- Audit logging on insert/update/delete events into an activity collection.
|
|
681
|
+
- Scheduled jobs (for example nightly cleanup, reminder generation, data aggregation).
|
|
682
|
+
- Auth lifecycle reactions (welcome email on user creation, cleanup on user deletion).
|
|
683
|
+
|
|
684
|
+
### 🌐 HTTP Endpoints
|
|
685
|
+
- Public webhook ingestion from third-party systems.
|
|
686
|
+
- Protected custom APIs for backoffice actions not exposed as direct database operations.
|
|
687
|
+
|
|
688
|
+
### 📡 `flowerbase-client`
|
|
689
|
+
- Real-time UI updates in task boards using `collection.watch()` change streams.
|
|
690
|
+
- Frontend data access with Realm-style API surface to minimize integration complexity.
|
|
691
|
+
- Shared client usage across web and React Native projects with consistent auth/session behavior.
|
|
692
|
+
|
|
693
|
+
### 🖥 Monitoring UI
|
|
694
|
+
- Live inspection of function invocations, endpoint calls, and trigger executions in staging/production.
|
|
695
|
+
- Fast troubleshooting with event stream filters and user/session search tools.
|
|
@@ -18,7 +18,7 @@ export declare const executeQuery: ({ currentMethod, query, update, filter, proj
|
|
|
18
18
|
countDocuments: () => Promise<number>;
|
|
19
19
|
deleteOne: () => Promise<import("mongodb").DeleteResult>;
|
|
20
20
|
insertOne: () => Promise<import("mongodb").InsertOneResult<Document>>;
|
|
21
|
-
updateOne: () => Promise<
|
|
21
|
+
updateOne: () => Promise<import("mongodb").UpdateResult<Document>>;
|
|
22
22
|
findOneAndUpdate: () => Promise<Document | null>;
|
|
23
23
|
aggregate: () => Promise<Document[]>;
|
|
24
24
|
insertMany: () => Promise<import("mongodb").InsertManyResult<Document>>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../../src/features/functions/utils.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAA;AAGlC,OAAO,EAAE,kBAAkB,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAE3D;;;GAGG;AACH,eAAO,MAAM,aAAa,GAAU,gBAAuB,KAAG,OAAO,CAAC,SAAS,CAwB9E,CAAA;AAED;;;;;GAKG;AACH,eAAO,MAAM,YAAY,GAAU,2HAYhC,kBAAkB;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../../src/features/functions/utils.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAA;AAGlC,OAAO,EAAE,kBAAkB,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAE3D;;;GAGG;AACH,eAAO,MAAM,aAAa,GAAU,gBAAuB,KAAG,OAAO,CAAC,SAAS,CAwB9E,CAAA;AAED;;;;;GAKG;AACH,eAAO,MAAM,YAAY,GAAU,2HAYhC,kBAAkB;;;;;;;;;;;;;EAsGpB,CAAA"}
|
|
@@ -100,7 +100,7 @@ const executeQuery = (_a) => __awaiter(void 0, [_a], void 0, function* ({ curren
|
|
|
100
100
|
countDocuments: () => currentMethod(bson_1.EJSON.deserialize(resolvedQuery), parsedOptions),
|
|
101
101
|
deleteOne: () => currentMethod(bson_1.EJSON.deserialize(resolvedQuery), parsedOptions),
|
|
102
102
|
insertOne: () => currentMethod(bson_1.EJSON.deserialize(document)),
|
|
103
|
-
updateOne: () => currentMethod(bson_1.EJSON.deserialize(resolvedQuery), bson_1.EJSON.deserialize(resolvedUpdate)),
|
|
103
|
+
updateOne: () => currentMethod(bson_1.EJSON.deserialize(resolvedQuery), bson_1.EJSON.deserialize(resolvedUpdate), parsedOptions),
|
|
104
104
|
findOneAndUpdate: () => currentMethod(bson_1.EJSON.deserialize(resolvedQuery), bson_1.EJSON.deserialize(resolvedUpdate), parsedOptions),
|
|
105
105
|
aggregate: () => __awaiter(void 0, void 0, void 0, function* () {
|
|
106
106
|
return (yield currentMethod(bson_1.EJSON.deserialize(pipeline), {}, // TODO -> ADD OPTIONS
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"exposeRoutes.d.ts","sourceRoot":"","sources":["../../../src/utils/initializer/exposeRoutes.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;
|
|
1
|
+
{"version":3,"file":"exposeRoutes.d.ts","sourceRoot":"","sources":["../../../src/utils/initializer/exposeRoutes.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AAmCzC;;;;GAIG;AACH,eAAO,MAAM,YAAY,GAAU,SAAS,eAAe,kBA2F1D,CAAA"}
|
|
@@ -15,6 +15,36 @@ const utils_1 = require("../../auth/utils");
|
|
|
15
15
|
const constants_1 = require("../../constants");
|
|
16
16
|
const handleUserRegistration_model_1 = require("../../shared/models/handleUserRegistration.model");
|
|
17
17
|
const crypto_1 = require("../crypto");
|
|
18
|
+
const parseFirstHeaderValue = (header) => {
|
|
19
|
+
var _a;
|
|
20
|
+
if (!header)
|
|
21
|
+
return undefined;
|
|
22
|
+
const raw = Array.isArray(header) ? header[0] : header;
|
|
23
|
+
return ((_a = raw === null || raw === void 0 ? void 0 : raw.split(',')[0]) === null || _a === void 0 ? void 0 : _a.trim()) || undefined;
|
|
24
|
+
};
|
|
25
|
+
const parseForwardedHeader = (header) => {
|
|
26
|
+
var _a;
|
|
27
|
+
if (!header)
|
|
28
|
+
return {};
|
|
29
|
+
const segment = (_a = header.split(',')[0]) === null || _a === void 0 ? void 0 : _a.trim();
|
|
30
|
+
if (!segment)
|
|
31
|
+
return {};
|
|
32
|
+
const tokens = segment.split(';').map((item) => item.trim());
|
|
33
|
+
const protoToken = tokens.find((item) => item.toLowerCase().startsWith('proto='));
|
|
34
|
+
const hostToken = tokens.find((item) => item.toLowerCase().startsWith('host='));
|
|
35
|
+
const clean = (value) => value === null || value === void 0 ? void 0 : value.replace(/^"|"$/g, '');
|
|
36
|
+
return {
|
|
37
|
+
proto: clean(protoToken === null || protoToken === void 0 ? void 0 : protoToken.split('=')[1]),
|
|
38
|
+
host: clean(hostToken === null || hostToken === void 0 ? void 0 : hostToken.split('=')[1])
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
const hasExplicitPort = (host) => {
|
|
42
|
+
if (/^\[[^\]]+]\:\d+$/.test(host))
|
|
43
|
+
return true;
|
|
44
|
+
if (/^[^:]+\:\d+$/.test(host))
|
|
45
|
+
return true;
|
|
46
|
+
return false;
|
|
47
|
+
};
|
|
18
48
|
/**
|
|
19
49
|
* > Used to expose all app routes
|
|
20
50
|
* @param fastify -> the fastify instance
|
|
@@ -27,12 +57,16 @@ const exposeRoutes = (fastify) => __awaiter(void 0, void 0, void 0, function* ()
|
|
|
27
57
|
tags: ['System']
|
|
28
58
|
}
|
|
29
59
|
}, (req) => __awaiter(void 0, void 0, void 0, function* () {
|
|
30
|
-
var _a, _b, _c;
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
const
|
|
34
|
-
const
|
|
35
|
-
const
|
|
60
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
61
|
+
const forwarded = parseForwardedHeader(parseFirstHeaderValue(req.headers.forwarded));
|
|
62
|
+
const forwardedProto = parseFirstHeaderValue(req.headers['x-forwarded-proto']);
|
|
63
|
+
const forwardedHost = parseFirstHeaderValue(req.headers['x-forwarded-host']);
|
|
64
|
+
const forwardedPort = parseFirstHeaderValue(req.headers['x-forwarded-port']);
|
|
65
|
+
const schema = (_c = (_b = (_a = forwarded.proto) !== null && _a !== void 0 ? _a : forwardedProto) !== null && _b !== void 0 ? _b : constants_1.DEFAULT_CONFIG === null || constants_1.DEFAULT_CONFIG === void 0 ? void 0 : constants_1.DEFAULT_CONFIG.HTTPS_SCHEMA) !== null && _c !== void 0 ? _c : 'http';
|
|
66
|
+
let host = (_f = (_e = (_d = forwarded.host) !== null && _d !== void 0 ? _d : forwardedHost) !== null && _e !== void 0 ? _e : req.headers.host) !== null && _f !== void 0 ? _f : `localhost:${(_g = constants_1.DEFAULT_CONFIG === null || constants_1.DEFAULT_CONFIG === void 0 ? void 0 : constants_1.DEFAULT_CONFIG.PORT) !== null && _g !== void 0 ? _g : 3000}`;
|
|
67
|
+
if (forwardedPort && !hasExplicitPort(host)) {
|
|
68
|
+
host = `${host}:${forwardedPort}`;
|
|
69
|
+
}
|
|
36
70
|
const wsSchema = 'wss';
|
|
37
71
|
return {
|
|
38
72
|
deployment_model: 'LOCAL',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../../src/utils/roles/machines/write/B/index.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAA;AAGxC,eAAO,MAAM,aAAa,EAAE,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../../src/utils/roles/machines/write/B/index.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAA;AAGxC,eAAO,MAAM,aAAa,EAAE,MAmE3B,CAAA"}
|
|
@@ -44,6 +44,9 @@ exports.STEP_B_STATES = {
|
|
|
44
44
|
});
|
|
45
45
|
const check = yield (0, commonValidators_1.evaluateTopLevelPermissionsFn)(context, 'write');
|
|
46
46
|
if (check) {
|
|
47
|
+
if (context.params.type === 'write') {
|
|
48
|
+
return endValidation({ success: true });
|
|
49
|
+
}
|
|
47
50
|
return next('evaluateTopLevelInsert');
|
|
48
51
|
}
|
|
49
52
|
if (check === false) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../../src/utils/rules-matcher/utils.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,SAAS,EAAE,iBAAiB,EAAe,MAAM,aAAa,CAAA;AAmEvE;;GAEG;AACH,QAAA,MAAM,iBAAiB,EAAE,
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../../src/utils/rules-matcher/utils.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,SAAS,EAAE,iBAAiB,EAAe,MAAM,aAAa,CAAA;AAmEvE;;GAEG;AACH,QAAA,MAAM,iBAAiB,EAAE,iBAkOxB,CAAA;AAED;;GAEG;AACH,eAAO,MAAM,SAAS,EAAE,SAyDvB,CAAA;AAID,eAAe,iBAAiB,CAAA"}
|
|
@@ -212,6 +212,13 @@ const rulesMatcherUtils = {
|
|
|
212
212
|
return true;
|
|
213
213
|
return block['$or'].some((item) => rulesMatcherUtils.checkRule(item, data, options));
|
|
214
214
|
}
|
|
215
|
+
if (!Array.isArray(block) && block && typeof block === 'object') {
|
|
216
|
+
const keys = Object.keys(block);
|
|
217
|
+
const hasLogicalOperators = keys.some((key) => key === '$and' || key === '$or');
|
|
218
|
+
if (!hasLogicalOperators && keys.length > 1) {
|
|
219
|
+
return keys.every((key) => rulesMatcherUtils.checkRule({ [key]: block[key] }, data, options));
|
|
220
|
+
}
|
|
221
|
+
}
|
|
215
222
|
const res = rulesMatcherUtils.rule(block, data, options);
|
|
216
223
|
return res.valid;
|
|
217
224
|
},
|
package/package.json
CHANGED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { executeQuery } from '../utils'
|
|
2
|
+
|
|
3
|
+
describe('executeQuery', () => {
|
|
4
|
+
it('passes parsed options to updateOne', async () => {
|
|
5
|
+
const currentMethod = jest.fn().mockResolvedValue({
|
|
6
|
+
acknowledged: true,
|
|
7
|
+
matchedCount: 0,
|
|
8
|
+
modifiedCount: 0,
|
|
9
|
+
upsertedCount: 1
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
const operators = await executeQuery({
|
|
13
|
+
currentMethod,
|
|
14
|
+
query: { ownerUserId: 'user-1' },
|
|
15
|
+
update: {
|
|
16
|
+
$set: { locale: 'it-IT' },
|
|
17
|
+
$setOnInsert: { scope: 'workspace' }
|
|
18
|
+
},
|
|
19
|
+
options: { upsert: true }
|
|
20
|
+
} as any)
|
|
21
|
+
|
|
22
|
+
await operators.updateOne()
|
|
23
|
+
|
|
24
|
+
expect(currentMethod).toHaveBeenCalledWith(
|
|
25
|
+
{ ownerUserId: 'user-1' },
|
|
26
|
+
{
|
|
27
|
+
$set: { locale: 'it-IT' },
|
|
28
|
+
$setOnInsert: { scope: 'workspace' }
|
|
29
|
+
},
|
|
30
|
+
{ upsert: true }
|
|
31
|
+
)
|
|
32
|
+
})
|
|
33
|
+
})
|
|
@@ -122,7 +122,12 @@ export const executeQuery = async ({
|
|
|
122
122
|
(currentMethod as ReturnType<GetOperatorsFunction>['insertOne'])(
|
|
123
123
|
EJSON.deserialize(document)
|
|
124
124
|
),
|
|
125
|
-
updateOne: () =>
|
|
125
|
+
updateOne: () =>
|
|
126
|
+
(currentMethod as ReturnType<GetOperatorsFunction>['updateOne'])(
|
|
127
|
+
EJSON.deserialize(resolvedQuery),
|
|
128
|
+
EJSON.deserialize(resolvedUpdate),
|
|
129
|
+
parsedOptions
|
|
130
|
+
),
|
|
126
131
|
findOneAndUpdate: () =>
|
|
127
132
|
(currentMethod as ReturnType<GetOperatorsFunction>['findOneAndUpdate'])(
|
|
128
133
|
EJSON.deserialize(resolvedQuery),
|
|
@@ -77,9 +77,9 @@ describe('WRITE STEP_B_STATES', () => {
|
|
|
77
77
|
expect(endValidation).toHaveBeenCalledWith({ success: false })
|
|
78
78
|
})
|
|
79
79
|
|
|
80
|
-
it('
|
|
80
|
+
it('ends validation when write=true on update requests', async () => {
|
|
81
81
|
;(evaluateTopLevelPermissionsFn as jest.Mock).mockResolvedValueOnce(true)
|
|
82
|
-
const context = {} as MachineContext
|
|
82
|
+
const context = { params: { type: 'write' } } as MachineContext
|
|
83
83
|
await evaluateTopLevelWrite({
|
|
84
84
|
context,
|
|
85
85
|
endValidation,
|
|
@@ -87,13 +87,12 @@ describe('WRITE STEP_B_STATES', () => {
|
|
|
87
87
|
next,
|
|
88
88
|
initialStep: null
|
|
89
89
|
})
|
|
90
|
-
expect(
|
|
90
|
+
expect(endValidation).toHaveBeenCalledWith({ success: true })
|
|
91
91
|
})
|
|
92
92
|
|
|
93
|
-
it('routes to insert check when write=true
|
|
93
|
+
it('routes to insert check when write=true on insert requests', async () => {
|
|
94
94
|
;(evaluateTopLevelPermissionsFn as jest.Mock).mockResolvedValueOnce(true)
|
|
95
|
-
|
|
96
|
-
const context = {} as MachineContext
|
|
95
|
+
const context = { params: { type: 'insert' } } as MachineContext
|
|
97
96
|
await evaluateTopLevelWrite({
|
|
98
97
|
context,
|
|
99
98
|
endValidation,
|
|
@@ -38,7 +38,10 @@ describe('exposeRoutes', () => {
|
|
|
38
38
|
const appId = 'it'
|
|
39
39
|
const response = await config.app!.inject({
|
|
40
40
|
method: 'GET',
|
|
41
|
-
url: `${API_VERSION}/app/${appId}/location
|
|
41
|
+
url: `${API_VERSION}/app/${appId}/location`,
|
|
42
|
+
headers: {
|
|
43
|
+
host: 'localhost:3000'
|
|
44
|
+
}
|
|
42
45
|
})
|
|
43
46
|
|
|
44
47
|
expect(response.statusCode).toBe(200)
|
|
@@ -50,6 +53,28 @@ describe('exposeRoutes', () => {
|
|
|
50
53
|
})
|
|
51
54
|
})
|
|
52
55
|
|
|
56
|
+
it('GET location should prioritize forwarded host and port', async () => {
|
|
57
|
+
const appId = 'it'
|
|
58
|
+
const response = await config.app!.inject({
|
|
59
|
+
method: 'GET',
|
|
60
|
+
url: `${API_VERSION}/app/${appId}/location`,
|
|
61
|
+
headers: {
|
|
62
|
+
host: 'internal-flowerbase:3000',
|
|
63
|
+
'x-forwarded-proto': 'https',
|
|
64
|
+
'x-forwarded-host': 'api.example.com',
|
|
65
|
+
'x-forwarded-port': '8443'
|
|
66
|
+
}
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
expect(response.statusCode).toBe(200)
|
|
70
|
+
expect(JSON.parse(response.body)).toEqual({
|
|
71
|
+
deployment_model: 'LOCAL',
|
|
72
|
+
location: 'IE',
|
|
73
|
+
hostname: 'https://api.example.com:8443',
|
|
74
|
+
ws_hostname: 'wss://api.example.com:8443'
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
|
+
|
|
53
78
|
it('exposeRoutes should handle errors in the catch block', async () => {
|
|
54
79
|
const mockedApp = Fastify()
|
|
55
80
|
// Forced fail on get method
|
|
@@ -9,7 +9,8 @@ const {
|
|
|
9
9
|
getPath,
|
|
10
10
|
forceNumber,
|
|
11
11
|
getDefaultStringValue,
|
|
12
|
-
getTypeOf
|
|
12
|
+
getTypeOf,
|
|
13
|
+
checkRule
|
|
13
14
|
} = rulesMatcherUtils
|
|
14
15
|
|
|
15
16
|
describe('rulesMatcherUtils', () => {
|
|
@@ -64,4 +65,33 @@ describe('rulesMatcherUtils', () => {
|
|
|
64
65
|
expect(getPath('^data.name')).toBe('data.name')
|
|
65
66
|
expect(getPath('$data.name')).toBe('$data.name')
|
|
66
67
|
})
|
|
68
|
+
|
|
69
|
+
it('evaluates plain objects with multiple keys as AND conditions', () => {
|
|
70
|
+
const data = {
|
|
71
|
+
'%%root': { accountId: 'acc-1' },
|
|
72
|
+
'%%user': { custom_data: { accountId: 'acc-1', role: 'admin' } }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
expect(
|
|
76
|
+
checkRule(
|
|
77
|
+
{
|
|
78
|
+
'%%root.accountId': '$ref:%%user.custom_data.accountId',
|
|
79
|
+
'%%user.custom_data.role': 'admin'
|
|
80
|
+
} as any,
|
|
81
|
+
data,
|
|
82
|
+
{}
|
|
83
|
+
)
|
|
84
|
+
).toBe(true)
|
|
85
|
+
|
|
86
|
+
expect(
|
|
87
|
+
checkRule(
|
|
88
|
+
{
|
|
89
|
+
'%%root.accountId': '$ref:%%user.custom_data.accountId',
|
|
90
|
+
'%%user.custom_data.role': 'staff'
|
|
91
|
+
} as any,
|
|
92
|
+
data,
|
|
93
|
+
{}
|
|
94
|
+
)
|
|
95
|
+
).toBe(false)
|
|
96
|
+
})
|
|
67
97
|
})
|
|
@@ -6,6 +6,34 @@ import { API_VERSION, AUTH_CONFIG, AUTH_DB_NAME, DEFAULT_CONFIG } from '../../co
|
|
|
6
6
|
import { PROVIDER } from '../../shared/models/handleUserRegistration.model'
|
|
7
7
|
import { hashPassword } from '../crypto'
|
|
8
8
|
|
|
9
|
+
const parseFirstHeaderValue = (header: string | string[] | undefined): string | undefined => {
|
|
10
|
+
if (!header) return undefined
|
|
11
|
+
const raw = Array.isArray(header) ? header[0] : header
|
|
12
|
+
return raw?.split(',')[0]?.trim() || undefined
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const parseForwardedHeader = (header: string | undefined): { proto?: string; host?: string } => {
|
|
16
|
+
if (!header) return {}
|
|
17
|
+
const segment = header.split(',')[0]?.trim()
|
|
18
|
+
if (!segment) return {}
|
|
19
|
+
|
|
20
|
+
const tokens = segment.split(';').map((item) => item.trim())
|
|
21
|
+
const protoToken = tokens.find((item) => item.toLowerCase().startsWith('proto='))
|
|
22
|
+
const hostToken = tokens.find((item) => item.toLowerCase().startsWith('host='))
|
|
23
|
+
const clean = (value?: string) => value?.replace(/^"|"$/g, '')
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
proto: clean(protoToken?.split('=')[1]),
|
|
27
|
+
host: clean(hostToken?.split('=')[1])
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const hasExplicitPort = (host: string): boolean => {
|
|
32
|
+
if (/^\[[^\]]+]\:\d+$/.test(host)) return true
|
|
33
|
+
if (/^[^:]+\:\d+$/.test(host)) return true
|
|
34
|
+
return false
|
|
35
|
+
}
|
|
36
|
+
|
|
9
37
|
/**
|
|
10
38
|
* > Used to expose all app routes
|
|
11
39
|
* @param fastify -> the fastify instance
|
|
@@ -18,11 +46,17 @@ export const exposeRoutes = async (fastify: FastifyInstance) => {
|
|
|
18
46
|
tags: ['System']
|
|
19
47
|
}
|
|
20
48
|
}, async (req) => {
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
const
|
|
49
|
+
const forwarded = parseForwardedHeader(parseFirstHeaderValue(req.headers.forwarded))
|
|
50
|
+
const forwardedProto = parseFirstHeaderValue(req.headers['x-forwarded-proto'])
|
|
51
|
+
const forwardedHost = parseFirstHeaderValue(req.headers['x-forwarded-host'])
|
|
52
|
+
const forwardedPort = parseFirstHeaderValue(req.headers['x-forwarded-port'])
|
|
53
|
+
const schema = forwarded.proto ?? forwardedProto ?? DEFAULT_CONFIG?.HTTPS_SCHEMA ?? 'http'
|
|
54
|
+
let host = forwarded.host ?? forwardedHost ?? req.headers.host ?? `localhost:${DEFAULT_CONFIG?.PORT ?? 3000}`
|
|
55
|
+
|
|
56
|
+
if (forwardedPort && !hasExplicitPort(host)) {
|
|
57
|
+
host = `${host}:${forwardedPort}`
|
|
58
|
+
}
|
|
59
|
+
|
|
26
60
|
const wsSchema = 'wss'
|
|
27
61
|
|
|
28
62
|
return {
|
|
@@ -37,6 +37,9 @@ export const STEP_B_STATES: States = {
|
|
|
37
37
|
})
|
|
38
38
|
const check = await evaluateTopLevelPermissionsFn(context, 'write')
|
|
39
39
|
if (check) {
|
|
40
|
+
if (context.params.type === 'write') {
|
|
41
|
+
return endValidation({ success: true })
|
|
42
|
+
}
|
|
40
43
|
return next('evaluateTopLevelInsert')
|
|
41
44
|
}
|
|
42
45
|
if (check === false) {
|
|
@@ -267,6 +267,20 @@ const rulesMatcherUtils: RulesMatcherUtils = {
|
|
|
267
267
|
)
|
|
268
268
|
}
|
|
269
269
|
|
|
270
|
+
if (!Array.isArray(block) && block && typeof block === 'object') {
|
|
271
|
+
const keys = Object.keys(block)
|
|
272
|
+
const hasLogicalOperators = keys.some((key) => key === '$and' || key === '$or')
|
|
273
|
+
if (!hasLogicalOperators && keys.length > 1) {
|
|
274
|
+
return keys.every((key) =>
|
|
275
|
+
rulesMatcherUtils.checkRule(
|
|
276
|
+
{ [key]: (block as Record<string, unknown>)[key] } as any,
|
|
277
|
+
data,
|
|
278
|
+
options
|
|
279
|
+
)
|
|
280
|
+
)
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
270
284
|
const res = rulesMatcherUtils.rule(block, data, options)
|
|
271
285
|
return res.valid
|
|
272
286
|
},
|