@flowerforce/flowerbase 1.7.4 → 1.7.5-beta.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/auth/controller.d.ts.map +1 -1
- package/dist/auth/controller.js +14 -4
- package/dist/auth/plugins/jwt.d.ts.map +1 -1
- package/dist/auth/plugins/jwt.js +8 -5
- package/dist/features/functions/controller.d.ts.map +1 -1
- package/dist/features/functions/controller.js +126 -28
- package/dist/services/mongodb-atlas/index.d.ts.map +1 -1
- package/dist/services/mongodb-atlas/index.js +102 -34
- package/dist/utils/initializer/exposeRoutes.d.ts.map +1 -1
- package/dist/utils/initializer/exposeRoutes.js +2 -0
- package/package.json +1 -1
- package/src/auth/controller.ts +17 -6
- package/src/auth/plugins/jwt.test.ts +1 -1
- package/src/auth/plugins/jwt.ts +5 -8
- package/src/features/functions/controller.ts +149 -31
- package/src/services/mongodb-atlas/index.ts +247 -142
- package/src/utils/initializer/exposeRoutes.ts +2 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"controller.d.ts","sourceRoot":"","sources":["../../src/auth/controller.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;
|
|
1
|
+
{"version":3,"file":"controller.d.ts","sourceRoot":"","sources":["../../src/auth/controller.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AAgBzC;;;;GAIG;AACH,wBAAsB,cAAc,CAAC,GAAG,EAAE,eAAe,iBA0LxD"}
|
package/dist/auth/controller.js
CHANGED
|
@@ -16,6 +16,12 @@ const state_1 = require("../state");
|
|
|
16
16
|
const crypto_1 = require("../utils/crypto");
|
|
17
17
|
const utils_1 = require("./utils");
|
|
18
18
|
const HANDLER_TYPE = 'preHandler';
|
|
19
|
+
const unauthorizedSessionError = {
|
|
20
|
+
message: 'Unauthorized',
|
|
21
|
+
error: 'unauthorized',
|
|
22
|
+
errorCode: 'InvalidSession',
|
|
23
|
+
error_code: 'InvalidSession'
|
|
24
|
+
};
|
|
19
25
|
/**
|
|
20
26
|
* Controller for handling user authentication, profile retrieval, and session management.
|
|
21
27
|
* @testable
|
|
@@ -96,11 +102,13 @@ function authController(app) {
|
|
|
96
102
|
}, function (req, res) {
|
|
97
103
|
return __awaiter(this, void 0, void 0, function* () {
|
|
98
104
|
if (req.user.typ !== 'refresh') {
|
|
99
|
-
|
|
105
|
+
res.code(401).send(unauthorizedSessionError);
|
|
106
|
+
return;
|
|
100
107
|
}
|
|
101
108
|
const authHeader = req.headers.authorization;
|
|
102
109
|
if (!(authHeader === null || authHeader === void 0 ? void 0 : authHeader.startsWith('Bearer '))) {
|
|
103
|
-
|
|
110
|
+
res.code(401).send(unauthorizedSessionError);
|
|
111
|
+
return;
|
|
104
112
|
}
|
|
105
113
|
const refreshToken = authHeader.slice('Bearer '.length).trim();
|
|
106
114
|
const refreshTokenHash = (0, crypto_1.hashToken)(refreshToken);
|
|
@@ -110,11 +118,13 @@ function authController(app) {
|
|
|
110
118
|
expiresAt: { $gt: new Date() }
|
|
111
119
|
});
|
|
112
120
|
if (!storedToken) {
|
|
113
|
-
|
|
121
|
+
res.code(401).send(unauthorizedSessionError);
|
|
122
|
+
return;
|
|
114
123
|
}
|
|
115
124
|
const auth_user = yield (db === null || db === void 0 ? void 0 : db.collection(authCollection).findOne({ _id: new this.mongo.ObjectId(req.user.sub) }));
|
|
116
125
|
if (!auth_user) {
|
|
117
|
-
|
|
126
|
+
res.code(401).send(unauthorizedSessionError);
|
|
127
|
+
return;
|
|
118
128
|
}
|
|
119
129
|
const user = userCollection && constants_1.AUTH_CONFIG.user_id_field
|
|
120
130
|
? (yield db.collection(userCollection).findOne({ [constants_1.AUTH_CONFIG.user_id_field]: req.user.sub }))
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"jwt.d.ts","sourceRoot":"","sources":["../../../src/auth/plugins/jwt.ts"],"names":[],"mappings":"AAKA,KAAK,OAAO,GAAG;IACb,MAAM,EAAE,MAAM,CAAA;CACf,CAAA;AAkBD;;;;;;;GAOG;iUAC8C,OAAO;AAAxD,
|
|
1
|
+
{"version":3,"file":"jwt.d.ts","sourceRoot":"","sources":["../../../src/auth/plugins/jwt.ts"],"names":[],"mappings":"AAKA,KAAK,OAAO,GAAG;IACb,MAAM,EAAE,MAAM,CAAA;CACf,CAAA;AAkBD;;;;;;;GAOG;iUAC8C,OAAO;AAAxD,wBA4GE"}
|
package/dist/auth/plugins/jwt.js
CHANGED
|
@@ -95,15 +95,18 @@ exports.default = (0, fastify_plugin_1.default)(function (fastify, opts) {
|
|
|
95
95
|
});
|
|
96
96
|
fastify.decorate('createAccessToken', function (user) {
|
|
97
97
|
const id = user._id.toString();
|
|
98
|
-
const userData = isRecord(user.user_data) ? Object.assign({}, user.user_data) : {};
|
|
99
98
|
const customData = isRecord(user.custom_data)
|
|
100
|
-
? Object.assign({}, user.custom_data) : Object.assign({},
|
|
101
|
-
const
|
|
99
|
+
? Object.assign({}, user.custom_data) : (isRecord(user.user_data) ? Object.assign({}, user.user_data) : {});
|
|
100
|
+
const authData = {
|
|
101
|
+
_id: id,
|
|
102
|
+
id,
|
|
103
|
+
email: typeof user.email === 'string' ? user.email : customData.email
|
|
104
|
+
};
|
|
102
105
|
return this.jwt.sign({
|
|
103
106
|
typ: 'access',
|
|
104
107
|
id,
|
|
105
|
-
data:
|
|
106
|
-
user_data:
|
|
108
|
+
data: authData,
|
|
109
|
+
user_data: customData,
|
|
107
110
|
custom_data: customData
|
|
108
111
|
}, {
|
|
109
112
|
iss: BAAS_ID,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"controller.d.ts","sourceRoot":"","sources":["../../../src/features/functions/controller.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAA;
|
|
1
|
+
{"version":3,"file":"controller.d.ts","sourceRoot":"","sources":["../../../src/features/functions/controller.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAA;AAoFhD;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,EAAE,kBAqPjC,CAAA"}
|
|
@@ -8,11 +8,21 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
8
8
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
9
|
});
|
|
10
10
|
};
|
|
11
|
+
var __rest = (this && this.__rest) || function (s, e) {
|
|
12
|
+
var t = {};
|
|
13
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
14
|
+
t[p] = s[p];
|
|
15
|
+
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
|
16
|
+
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
|
17
|
+
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
|
18
|
+
t[p[i]] = s[p[i]];
|
|
19
|
+
}
|
|
20
|
+
return t;
|
|
21
|
+
};
|
|
11
22
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
23
|
exports.functionsController = void 0;
|
|
13
24
|
const bson_1 = require("bson");
|
|
14
25
|
const services_1 = require("../../services");
|
|
15
|
-
const state_1 = require("../../state");
|
|
16
26
|
const context_1 = require("../../utils/context");
|
|
17
27
|
const utils_1 = require("./utils");
|
|
18
28
|
const normalizeUser = (payload) => {
|
|
@@ -47,6 +57,20 @@ const isReturnedError = (value) => {
|
|
|
47
57
|
return typeof candidate.message === 'string' && typeof candidate.name === 'string';
|
|
48
58
|
};
|
|
49
59
|
const serializeEjson = (value) => JSON.stringify(bson_1.EJSON.serialize(value, { relaxed: false }));
|
|
60
|
+
const isRecord = (value) => !!value && typeof value === 'object' && !Array.isArray(value);
|
|
61
|
+
const sharedWatchStreams = new Map();
|
|
62
|
+
let watchSubscriberCounter = 0;
|
|
63
|
+
const parseWatchFilter = (args) => {
|
|
64
|
+
var _a;
|
|
65
|
+
if (!isRecord(args))
|
|
66
|
+
return undefined;
|
|
67
|
+
const candidate = (_a = (isRecord(args.filter) ? args.filter : undefined)) !== null && _a !== void 0 ? _a : (isRecord(args.query) ? args.query : undefined);
|
|
68
|
+
return candidate ? candidate : undefined;
|
|
69
|
+
};
|
|
70
|
+
const isReadableDocumentResult = (value) => !!value &&
|
|
71
|
+
typeof value === 'object' &&
|
|
72
|
+
!Array.isArray(value) &&
|
|
73
|
+
Object.keys(value).length > 0;
|
|
50
74
|
/**
|
|
51
75
|
* > Creates a pre handler for every query
|
|
52
76
|
* @param app -> the fastify instance
|
|
@@ -55,7 +79,6 @@ const serializeEjson = (value) => JSON.stringify(bson_1.EJSON.serialize(value, {
|
|
|
55
79
|
*/
|
|
56
80
|
const functionsController = (app_1, _a) => __awaiter(void 0, [app_1, _a], void 0, function* (app, { functionsList, rules }) {
|
|
57
81
|
app.addHook('preHandler', app.jwtAuthentication);
|
|
58
|
-
const streams = {};
|
|
59
82
|
app.post('/call', {
|
|
60
83
|
schema: {
|
|
61
84
|
tags: ['Functions']
|
|
@@ -140,10 +163,9 @@ const functionsController = (app_1, _a) => __awaiter(void 0, [app_1, _a], void 0
|
|
|
140
163
|
throw new Error('Access token required');
|
|
141
164
|
}
|
|
142
165
|
const { baas_request, stitch_request } = query;
|
|
143
|
-
const
|
|
144
|
-
const
|
|
145
|
-
const
|
|
146
|
-
const services = state_1.StateManager.select('services');
|
|
166
|
+
const decodedConfig = JSON.parse(Buffer.from(baas_request || stitch_request || '', 'base64').toString('utf8'));
|
|
167
|
+
const config = bson_1.EJSON.deserialize(decodedConfig);
|
|
168
|
+
const [_a] = config.arguments, { database, collection } = _a, watchArgs = __rest(_a, ["database", "collection"]);
|
|
147
169
|
const headers = {
|
|
148
170
|
'Content-Type': 'text/event-stream',
|
|
149
171
|
'Cache-Control': 'no-cache',
|
|
@@ -154,31 +176,107 @@ const functionsController = (app_1, _a) => __awaiter(void 0, [app_1, _a], void 0
|
|
|
154
176
|
};
|
|
155
177
|
res.raw.writeHead(200, headers);
|
|
156
178
|
res.raw.flushHeaders();
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
179
|
+
const streamKey = `${database}::${collection}`;
|
|
180
|
+
const subscriberId = `${Date.now()}-${watchSubscriberCounter++}`;
|
|
181
|
+
const extraFilter = parseWatchFilter(watchArgs);
|
|
182
|
+
const mongoClient = app.mongo.client;
|
|
183
|
+
let hub = sharedWatchStreams.get(streamKey);
|
|
184
|
+
if (!hub) {
|
|
185
|
+
const stream = mongoClient.db(database).collection(collection).watch([], {
|
|
186
|
+
fullDocument: 'whenAvailable'
|
|
164
187
|
});
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
}
|
|
171
|
-
|
|
188
|
+
hub = {
|
|
189
|
+
database,
|
|
190
|
+
collection,
|
|
191
|
+
stream,
|
|
192
|
+
subscribers: new Map()
|
|
193
|
+
};
|
|
194
|
+
sharedWatchStreams.set(streamKey, hub);
|
|
172
195
|
}
|
|
173
|
-
|
|
196
|
+
const ensureHubListeners = (currentHub) => {
|
|
197
|
+
if (currentHub.listenersBound) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
const closeHub = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
201
|
+
currentHub.stream.off('change', onHubChange);
|
|
202
|
+
currentHub.stream.off('error', onHubError);
|
|
203
|
+
sharedWatchStreams.delete(streamKey);
|
|
204
|
+
try {
|
|
205
|
+
yield currentHub.stream.close();
|
|
206
|
+
}
|
|
207
|
+
catch (_a) {
|
|
208
|
+
// Ignore stream close errors.
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
const onHubChange = (change) => __awaiter(void 0, void 0, void 0, function* () {
|
|
212
|
+
const subscribers = Array.from(currentHub.subscribers.values());
|
|
213
|
+
yield Promise.all(subscribers.map((subscriber) => __awaiter(void 0, void 0, void 0, function* () {
|
|
214
|
+
var _a, _b, _c;
|
|
215
|
+
const subscriberRes = subscriber.response;
|
|
216
|
+
if (subscriberRes.writableEnded || subscriberRes.destroyed) {
|
|
217
|
+
currentHub.subscribers.delete(subscriber.id);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
const docId = (_b = (_a = change === null || change === void 0 ? void 0 : change.documentKey) === null || _a === void 0 ? void 0 : _a._id) !== null && _b !== void 0 ? _b : (_c = change === null || change === void 0 ? void 0 : change.fullDocument) === null || _c === void 0 ? void 0 : _c._id;
|
|
221
|
+
if (typeof docId === 'undefined')
|
|
222
|
+
return;
|
|
223
|
+
const readQuery = subscriber.extraFilter
|
|
224
|
+
? { $and: [subscriber.extraFilter, { _id: docId }] }
|
|
225
|
+
: { _id: docId };
|
|
226
|
+
try {
|
|
227
|
+
const readableDoc = yield services_1.services['mongodb-atlas'](app, {
|
|
228
|
+
user: subscriber.user,
|
|
229
|
+
rules
|
|
230
|
+
})
|
|
231
|
+
.db(currentHub.database)
|
|
232
|
+
.collection(currentHub.collection)
|
|
233
|
+
.findOne(readQuery);
|
|
234
|
+
if (!isReadableDocumentResult(readableDoc))
|
|
235
|
+
return;
|
|
236
|
+
subscriberRes.write(`data: ${serializeEjson(change)}\n\n`);
|
|
237
|
+
}
|
|
238
|
+
catch (error) {
|
|
239
|
+
subscriberRes.write(`event: error\ndata: ${formatFunctionExecutionError(error)}\n\n`);
|
|
240
|
+
subscriberRes.end();
|
|
241
|
+
currentHub.subscribers.delete(subscriber.id);
|
|
242
|
+
}
|
|
243
|
+
})));
|
|
244
|
+
if (!currentHub.subscribers.size) {
|
|
245
|
+
yield closeHub();
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
const onHubError = (error) => __awaiter(void 0, void 0, void 0, function* () {
|
|
249
|
+
for (const subscriber of currentHub.subscribers.values()) {
|
|
250
|
+
const subscriberRes = subscriber.response;
|
|
251
|
+
if (!subscriberRes.writableEnded && !subscriberRes.destroyed) {
|
|
252
|
+
subscriberRes.write(`event: error\ndata: ${formatFunctionExecutionError(error)}\n\n`);
|
|
253
|
+
subscriberRes.end();
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
currentHub.subscribers.clear();
|
|
257
|
+
yield closeHub();
|
|
258
|
+
});
|
|
259
|
+
currentHub.stream.on('change', onHubChange);
|
|
260
|
+
currentHub.stream.on('error', onHubError);
|
|
261
|
+
currentHub.listenersBound = true;
|
|
262
|
+
};
|
|
263
|
+
ensureHubListeners(hub);
|
|
264
|
+
const subscriber = {
|
|
265
|
+
id: subscriberId,
|
|
174
266
|
user,
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
267
|
+
response: res.raw,
|
|
268
|
+
extraFilter
|
|
269
|
+
};
|
|
270
|
+
hub.subscribers.set(subscriberId, subscriber);
|
|
271
|
+
req.raw.on('close', () => {
|
|
272
|
+
const currentHub = sharedWatchStreams.get(streamKey);
|
|
273
|
+
if (!currentHub)
|
|
274
|
+
return;
|
|
275
|
+
currentHub.subscribers.delete(subscriberId);
|
|
276
|
+
if (!currentHub.subscribers.size) {
|
|
277
|
+
void currentHub.stream.close();
|
|
278
|
+
sharedWatchStreams.delete(streamKey);
|
|
279
|
+
}
|
|
182
280
|
});
|
|
183
281
|
}));
|
|
184
282
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/services/mongodb-atlas/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/services/mongodb-atlas/index.ts"],"names":[],"mappings":"AAwBA,OAAO,EAGL,oBAAoB,EAErB,MAAM,SAAS,CAAA;AAk1ChB,QAAA,MAAM,YAAY,EAAE,oBA6BlB,CAAA;AAEF,eAAe,YAAY,CAAA"}
|
|
@@ -113,6 +113,53 @@ const normalizeFindOneAndUpdateOptions = (options) => {
|
|
|
113
113
|
return Object.assign(Object.assign({}, rest), { returnDocument: returnNewDocument ? 'after' : 'before' });
|
|
114
114
|
};
|
|
115
115
|
const buildAndQuery = (clauses) => clauses.length ? { $and: clauses } : {};
|
|
116
|
+
const toWatchMatchFilter = (value) => {
|
|
117
|
+
if (Array.isArray(value)) {
|
|
118
|
+
return value.map((item) => toWatchMatchFilter(item));
|
|
119
|
+
}
|
|
120
|
+
if (!isPlainObject(value))
|
|
121
|
+
return value;
|
|
122
|
+
return Object.entries(value).reduce((acc, [key, current]) => {
|
|
123
|
+
if (key.startsWith('$')) {
|
|
124
|
+
acc[key] = toWatchMatchFilter(current);
|
|
125
|
+
return acc;
|
|
126
|
+
}
|
|
127
|
+
acc[`fullDocument.${key}`] = toWatchMatchFilter(current);
|
|
128
|
+
return acc;
|
|
129
|
+
}, {});
|
|
130
|
+
};
|
|
131
|
+
const resolveWatchArgs = (pipelineOrOptions, options) => {
|
|
132
|
+
var _a;
|
|
133
|
+
const inputPipeline = Array.isArray(pipelineOrOptions) ? pipelineOrOptions : [];
|
|
134
|
+
const rawOptions = (_a = (Array.isArray(pipelineOrOptions) ? options : pipelineOrOptions)) !== null && _a !== void 0 ? _a : {};
|
|
135
|
+
if (!isPlainObject(rawOptions)) {
|
|
136
|
+
return {
|
|
137
|
+
pipeline: inputPipeline,
|
|
138
|
+
options: options,
|
|
139
|
+
extraMatches: []
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
const _b = rawOptions, { filter: watchFilter, ids } = _b, watchOptions = __rest(_b, ["filter", "ids"]);
|
|
143
|
+
const extraMatches = [];
|
|
144
|
+
if (typeof watchFilter !== 'undefined') {
|
|
145
|
+
extraMatches.push({ $match: toWatchMatchFilter(watchFilter) });
|
|
146
|
+
}
|
|
147
|
+
if (Array.isArray(ids)) {
|
|
148
|
+
extraMatches.push({
|
|
149
|
+
$match: {
|
|
150
|
+
$or: [
|
|
151
|
+
{ 'documentKey._id': { $in: ids } },
|
|
152
|
+
{ 'fullDocument._id': { $in: ids } }
|
|
153
|
+
]
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
pipeline: inputPipeline,
|
|
159
|
+
options: watchOptions,
|
|
160
|
+
extraMatches
|
|
161
|
+
};
|
|
162
|
+
};
|
|
116
163
|
const hasAtomicOperators = (data) => Object.keys(data).some((key) => key.startsWith('$'));
|
|
117
164
|
const normalizeUpdatePayload = (data) => hasAtomicOperators(data) ? data : { $set: data };
|
|
118
165
|
const hasOperatorExpressions = (value) => isPlainObject(value) && Object.keys(value).some((key) => key.startsWith('$'));
|
|
@@ -316,22 +363,22 @@ const getOperators = (collection, { rules, collName, user, run_as_system, monito
|
|
|
316
363
|
};
|
|
317
364
|
return {
|
|
318
365
|
/**
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
366
|
+
* Finds a single document in a MongoDB collection with optional role-based filtering and validation.
|
|
367
|
+
*
|
|
368
|
+
* @param {Filter<Document>} query - The MongoDB query used to match the document.
|
|
369
|
+
* @param {Document} [projection] - Optional projection to select returned fields.
|
|
370
|
+
* @param {FindOneOptions} [options] - Optional settings for the findOne operation.
|
|
371
|
+
* @returns {Promise<Document | {} | null>} A promise resolving to the document if found and permitted, an empty object if access is denied, or `null` if not found.
|
|
372
|
+
*
|
|
373
|
+
* @description
|
|
374
|
+
* If `run_as_system` is enabled, the function behaves like a standard `collection.findOne(query)` with no access checks.
|
|
375
|
+
* Otherwise:
|
|
376
|
+
* - Merges the provided query with any access control filters using `getFormattedQuery`.
|
|
377
|
+
* - Attempts to find the document using the formatted query.
|
|
378
|
+
* - Determines the user's role via `getWinningRole`.
|
|
379
|
+
* - Validates the result using `checkValidation` to ensure read permission.
|
|
380
|
+
* - If validation fails, returns an empty object; otherwise returns the validated document.
|
|
381
|
+
*/
|
|
335
382
|
findOne: (...args_1) => __awaiter(void 0, [...args_1], void 0, function* (query = {}, projectionOrOptions, options) {
|
|
336
383
|
var _a;
|
|
337
384
|
try {
|
|
@@ -804,32 +851,35 @@ const getOperators = (collection, { rules, collName, user, run_as_system, monito
|
|
|
804
851
|
*
|
|
805
852
|
* This allows fine-grained control over what change events a user can observe, based on roles and filters.
|
|
806
853
|
*/
|
|
807
|
-
watch: (
|
|
854
|
+
watch: (pipelineOrOptions = [], options) => {
|
|
808
855
|
try {
|
|
856
|
+
const { pipeline, options: watchOptions, extraMatches } = resolveWatchArgs(pipelineOrOptions, options);
|
|
809
857
|
if (!run_as_system) {
|
|
810
858
|
(0, utils_3.checkDenyOperation)(normalizedRules, collection.collectionName, model_1.CRUD_OPERATIONS.READ);
|
|
811
859
|
// Apply access filters to initial change stream pipeline
|
|
812
860
|
const formattedQuery = (0, utils_3.getFormattedQuery)(filters, {}, user);
|
|
813
|
-
const
|
|
814
|
-
|
|
815
|
-
|
|
861
|
+
const watchFormattedQuery = formattedQuery.map((condition) => toWatchMatchFilter(condition));
|
|
862
|
+
const firstStep = watchFormattedQuery.length
|
|
863
|
+
? {
|
|
864
|
+
$match: {
|
|
865
|
+
$and: watchFormattedQuery
|
|
866
|
+
}
|
|
816
867
|
}
|
|
817
|
-
|
|
818
|
-
const formattedPipeline = [
|
|
819
|
-
|
|
820
|
-
...pipeline
|
|
821
|
-
].filter(Boolean);
|
|
822
|
-
const result = collection.watch(formattedPipeline, options);
|
|
868
|
+
: undefined;
|
|
869
|
+
const formattedPipeline = [firstStep, ...extraMatches, ...pipeline].filter(Boolean);
|
|
870
|
+
const result = collection.watch(formattedPipeline, watchOptions);
|
|
823
871
|
const originalOn = result.on.bind(result);
|
|
824
872
|
/**
|
|
825
873
|
* Validates a change event against the user's roles.
|
|
826
874
|
*
|
|
827
875
|
* @param {Document} change - A change event from the ChangeStream.
|
|
828
|
-
* @returns {Promise<{ status: boolean, document: Document, updatedFieldsStatus: boolean, updatedFields: Document }>}
|
|
876
|
+
* @returns {Promise<{ status: boolean, document: Document, updatedFieldsStatus: boolean, updatedFields: Document, hasFullDocument: boolean, hasWinningRole: boolean }>}
|
|
829
877
|
*/
|
|
830
|
-
const isValidChange = (
|
|
878
|
+
const isValidChange = (change) => __awaiter(void 0, void 0, void 0, function* () {
|
|
879
|
+
const { fullDocument, updateDescription } = change;
|
|
880
|
+
const hasFullDocument = !!fullDocument;
|
|
831
881
|
const winningRole = (0, utils_2.getWinningRole)(fullDocument, user, roles);
|
|
832
|
-
const
|
|
882
|
+
const fullDocumentValidation = winningRole
|
|
833
883
|
? yield (0, machines_1.checkValidation)(winningRole, {
|
|
834
884
|
type: 'read',
|
|
835
885
|
roles,
|
|
@@ -837,6 +887,7 @@ const getOperators = (collection, { rules, collName, user, run_as_system, monito
|
|
|
837
887
|
expansions: {}
|
|
838
888
|
}, user)
|
|
839
889
|
: fallbackAccess(fullDocument);
|
|
890
|
+
const { status, document } = fullDocumentValidation;
|
|
840
891
|
const { status: updatedFieldsStatus, document: updatedFields } = winningRole
|
|
841
892
|
? yield (0, machines_1.checkValidation)(winningRole, {
|
|
842
893
|
type: 'read',
|
|
@@ -845,15 +896,32 @@ const getOperators = (collection, { rules, collName, user, run_as_system, monito
|
|
|
845
896
|
expansions: {}
|
|
846
897
|
}, user)
|
|
847
898
|
: fallbackAccess(updateDescription === null || updateDescription === void 0 ? void 0 : updateDescription.updatedFields);
|
|
848
|
-
return {
|
|
899
|
+
return {
|
|
900
|
+
status,
|
|
901
|
+
document,
|
|
902
|
+
updatedFieldsStatus,
|
|
903
|
+
updatedFields,
|
|
904
|
+
hasFullDocument,
|
|
905
|
+
hasWinningRole: !!winningRole
|
|
906
|
+
};
|
|
849
907
|
});
|
|
850
908
|
// Override the .on() method to apply validation before emitting events
|
|
851
909
|
result.on = (eventType, listener) => {
|
|
852
910
|
return originalOn(eventType, (change) => __awaiter(void 0, void 0, void 0, function* () {
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
return;
|
|
911
|
+
var _a, _b, _c, _d;
|
|
912
|
+
const { document, updatedFieldsStatus, updatedFields, hasFullDocument, hasWinningRole } = yield isValidChange(change);
|
|
856
913
|
const filteredChange = Object.assign(Object.assign({}, change), { fullDocument: document, updateDescription: Object.assign(Object.assign({}, change.updateDescription), { updatedFields: updatedFieldsStatus ? updatedFields : {} }) });
|
|
914
|
+
console.log('[flowerbase watch] delivered change', {
|
|
915
|
+
collection: collName,
|
|
916
|
+
operationType: change === null || change === void 0 ? void 0 : change.operationType,
|
|
917
|
+
eventType,
|
|
918
|
+
hasFullDocument,
|
|
919
|
+
hasWinningRole,
|
|
920
|
+
updatedFieldsStatus,
|
|
921
|
+
documentKey: ((_c = (_b = (_a = change === null || change === void 0 ? void 0 : change.documentKey) === null || _a === void 0 ? void 0 : _a._id) === null || _b === void 0 ? void 0 : _b.toString) === null || _c === void 0 ? void 0 : _c.call(_b)) ||
|
|
922
|
+
((_d = change === null || change === void 0 ? void 0 : change.documentKey) === null || _d === void 0 ? void 0 : _d._id) ||
|
|
923
|
+
null
|
|
924
|
+
});
|
|
857
925
|
listener(filteredChange);
|
|
858
926
|
}));
|
|
859
927
|
};
|
|
@@ -861,7 +929,7 @@ const getOperators = (collection, { rules, collName, user, run_as_system, monito
|
|
|
861
929
|
return result;
|
|
862
930
|
}
|
|
863
931
|
// System mode: no filtering applied
|
|
864
|
-
const result = collection.watch(pipeline,
|
|
932
|
+
const result = collection.watch([...extraMatches, ...pipeline], watchOptions);
|
|
865
933
|
emitMongoEvent('watch');
|
|
866
934
|
return result;
|
|
867
935
|
}
|
|
@@ -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;AAOzC;;;;GAIG;AACH,eAAO,MAAM,YAAY,GAAU,SAAS,eAAe,
|
|
1
|
+
{"version":3,"file":"exposeRoutes.d.ts","sourceRoot":"","sources":["../../../src/utils/initializer/exposeRoutes.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AAOzC;;;;GAIG;AACH,eAAO,MAAM,YAAY,GAAU,SAAS,eAAe,kBAqF1D,CAAA"}
|
|
@@ -59,6 +59,7 @@ const exposeRoutes = (fastify) => __awaiter(void 0, void 0, void 0, function* ()
|
|
|
59
59
|
const db = fastify.mongo.client.db(constants_1.DB_NAME);
|
|
60
60
|
const { email, password } = req.body;
|
|
61
61
|
const hashedPassword = yield (0, crypto_1.hashPassword)(password);
|
|
62
|
+
const now = new Date();
|
|
62
63
|
const users = db.collection(authCollection).find();
|
|
63
64
|
const list = yield (users === null || users === void 0 ? void 0 : users.toArray());
|
|
64
65
|
if (list === null || list === void 0 ? void 0 : list.length) {
|
|
@@ -71,6 +72,7 @@ const exposeRoutes = (fastify) => __awaiter(void 0, void 0, void 0, function* ()
|
|
|
71
72
|
email: email,
|
|
72
73
|
password: hashedPassword,
|
|
73
74
|
status: 'confirmed',
|
|
75
|
+
createdAt: now,
|
|
74
76
|
custom_data: {}
|
|
75
77
|
});
|
|
76
78
|
yield (db === null || db === void 0 ? void 0 : db.collection(authCollection).updateOne({
|
package/package.json
CHANGED
package/src/auth/controller.ts
CHANGED
|
@@ -4,9 +4,16 @@ import { AUTH_CONFIG, DB_NAME, DEFAULT_CONFIG } from '../constants'
|
|
|
4
4
|
import { StateManager } from '../state'
|
|
5
5
|
import { hashToken } from '../utils/crypto'
|
|
6
6
|
import { SessionCreatedDto } from './dtos'
|
|
7
|
-
import { AUTH_ENDPOINTS
|
|
7
|
+
import { AUTH_ENDPOINTS } from './utils'
|
|
8
8
|
|
|
9
9
|
const HANDLER_TYPE = 'preHandler'
|
|
10
|
+
const unauthorizedSessionError = {
|
|
11
|
+
message: 'Unauthorized',
|
|
12
|
+
error: 'unauthorized',
|
|
13
|
+
errorCode: 'InvalidSession',
|
|
14
|
+
error_code: 'InvalidSession'
|
|
15
|
+
} as const
|
|
16
|
+
type UnauthorizedSessionReply = typeof unauthorizedSessionError
|
|
10
17
|
|
|
11
18
|
/**
|
|
12
19
|
* Controller for handling user authentication, profile retrieval, and session management.
|
|
@@ -90,7 +97,7 @@ export async function authController(app: FastifyInstance) {
|
|
|
90
97
|
* @param {import('fastify').FastifyReply} res - The response object.
|
|
91
98
|
* @returns {Promise<SessionCreatedDto>} A promise resolving with the newly created session data.
|
|
92
99
|
*/
|
|
93
|
-
app.post<{ Reply: SessionCreatedDto }>(
|
|
100
|
+
app.post<{ Reply: SessionCreatedDto | UnauthorizedSessionReply }>(
|
|
94
101
|
AUTH_ENDPOINTS.SESSION,
|
|
95
102
|
{
|
|
96
103
|
schema: {
|
|
@@ -99,12 +106,14 @@ export async function authController(app: FastifyInstance) {
|
|
|
99
106
|
},
|
|
100
107
|
async function (req, res) {
|
|
101
108
|
if (req.user.typ !== 'refresh') {
|
|
102
|
-
|
|
109
|
+
res.code(401).send(unauthorizedSessionError)
|
|
110
|
+
return
|
|
103
111
|
}
|
|
104
112
|
|
|
105
113
|
const authHeader = req.headers.authorization
|
|
106
114
|
if (!authHeader?.startsWith('Bearer ')) {
|
|
107
|
-
|
|
115
|
+
res.code(401).send(unauthorizedSessionError)
|
|
116
|
+
return
|
|
108
117
|
}
|
|
109
118
|
const refreshToken = authHeader.slice('Bearer '.length).trim()
|
|
110
119
|
const refreshTokenHash = hashToken(refreshToken)
|
|
@@ -114,7 +123,8 @@ export async function authController(app: FastifyInstance) {
|
|
|
114
123
|
expiresAt: { $gt: new Date() }
|
|
115
124
|
})
|
|
116
125
|
if (!storedToken) {
|
|
117
|
-
|
|
126
|
+
res.code(401).send(unauthorizedSessionError)
|
|
127
|
+
return
|
|
118
128
|
}
|
|
119
129
|
|
|
120
130
|
const auth_user = await db
|
|
@@ -122,7 +132,8 @@ export async function authController(app: FastifyInstance) {
|
|
|
122
132
|
.findOne({ _id: new this.mongo.ObjectId(req.user.sub) })
|
|
123
133
|
|
|
124
134
|
if (!auth_user) {
|
|
125
|
-
|
|
135
|
+
res.code(401).send(unauthorizedSessionError)
|
|
136
|
+
return
|
|
126
137
|
}
|
|
127
138
|
|
|
128
139
|
const user = userCollection && AUTH_CONFIG.user_id_field
|
|
@@ -140,7 +140,7 @@ describe('jwtAuthentication', () => {
|
|
|
140
140
|
expect(decoded.id).toBe(authId.toHexString())
|
|
141
141
|
expect(decoded.sub).toBe(authId.toHexString())
|
|
142
142
|
expect(decoded.data._id).toBe(authId.toHexString())
|
|
143
|
-
expect(decoded.user_data._id).toBe(
|
|
143
|
+
expect(decoded.user_data._id).toBe(linkedId.toHexString())
|
|
144
144
|
expect(decoded.custom_data._id).toBe(linkedId.toHexString())
|
|
145
145
|
expect(decoded.custom_data.role).toBe('owner')
|
|
146
146
|
})
|
package/src/auth/plugins/jwt.ts
CHANGED
|
@@ -101,24 +101,21 @@ export default fp(async function (fastify, opts: Options) {
|
|
|
101
101
|
|
|
102
102
|
fastify.decorate('createAccessToken', function (user: WithId<Document>) {
|
|
103
103
|
const id = user._id.toString()
|
|
104
|
-
const userData = isRecord(user.user_data) ? { ...user.user_data } : {}
|
|
105
104
|
const customData = isRecord(user.custom_data)
|
|
106
105
|
? { ...user.custom_data }
|
|
107
|
-
: { ...
|
|
108
|
-
const
|
|
109
|
-
...customData,
|
|
110
|
-
...userData,
|
|
106
|
+
: (isRecord(user.user_data) ? { ...user.user_data } : {})
|
|
107
|
+
const authData = {
|
|
111
108
|
_id: id,
|
|
112
109
|
id,
|
|
113
|
-
email: typeof user.email === 'string' ? user.email :
|
|
110
|
+
email: typeof user.email === 'string' ? user.email : customData.email
|
|
114
111
|
}
|
|
115
112
|
|
|
116
113
|
return this.jwt.sign(
|
|
117
114
|
{
|
|
118
115
|
typ: 'access',
|
|
119
116
|
id,
|
|
120
|
-
data:
|
|
121
|
-
user_data:
|
|
117
|
+
data: authData,
|
|
118
|
+
user_data: customData,
|
|
122
119
|
custom_data: customData
|
|
123
120
|
},
|
|
124
121
|
{
|