@airoom/nextmin-node 2.0.2 → 2.0.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/README.md +151 -0
- package/dist/api/apiRouter.d.ts +20 -0
- package/dist/api/apiRouter.js +122 -7
- package/dist/api/router/mountCrudRoutes.js +69 -18
- package/dist/api/router/setupAuthRoutes.js +475 -38
- package/dist/api/router/setupChatWidgetRoutes.d.ts +3 -0
- package/dist/api/router/setupChatWidgetRoutes.js +207 -0
- package/dist/api/router/setupFileRoutes.js +221 -16
- package/dist/api/router/utils.d.ts +2 -1
- package/dist/api/router/utils.js +8 -6
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +145 -64
- package/dist/database/DatabaseAdapter.d.ts +1 -0
- package/dist/database/NMAdapter.d.ts +6 -0
- package/dist/database/NMAdapter.js +437 -86
- package/dist/database/QueryEngine.js +7 -4
- package/dist/files/FileStorageAdapter.d.ts +1 -0
- package/dist/files/LocalFileStorageAdapter.d.ts +1 -0
- package/dist/files/LocalFileStorageAdapter.js +25 -3
- package/dist/files/S3FileStorageAdapter.d.ts +3 -1
- package/dist/files/S3FileStorageAdapter.js +72 -11
- package/dist/files/filename.js +6 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3 -1
- package/dist/models/BaseModel.d.ts +4 -0
- package/dist/policy/authorize.d.ts +1 -1
- package/dist/policy/authorize.js +64 -13
- package/dist/schemas/Users.json +20 -10
- package/dist/services/IndexingService.d.ts +24 -0
- package/dist/services/IndexingService.js +555 -0
- package/dist/services/LocalAIService.d.ts +34 -0
- package/dist/services/LocalAIService.js +471 -0
- package/dist/services/OpenAIService.d.ts +16 -0
- package/dist/services/OpenAIService.js +586 -0
- package/dist/utils/SchemaLoader.d.ts +1 -1
- package/dist/utils/SchemaLoader.js +40 -4
- package/package.json +12 -4
|
@@ -126,22 +126,25 @@ class QueryEngine {
|
|
|
126
126
|
}
|
|
127
127
|
if (!attr)
|
|
128
128
|
continue;
|
|
129
|
+
const a = Array.isArray(attr) ? attr[0] : attr;
|
|
130
|
+
const attrType = a && String(a.type || '').toLowerCase();
|
|
131
|
+
const isStringField = attrType === 'string' || attrType === 'text';
|
|
129
132
|
const v = Array.isArray(vRaw) ? vRaw.map(String).join(',') : String(vRaw);
|
|
130
133
|
if (!v?.length)
|
|
131
134
|
continue;
|
|
132
135
|
if (v.includes(',')) {
|
|
133
|
-
filter[effectiveKey] = { $in: v.split(',').map(s => this.coerceValue(s.trim())) };
|
|
136
|
+
filter[effectiveKey] = { $in: v.split(',').map(s => isStringField ? s.trim() : this.coerceValue(s.trim())) };
|
|
134
137
|
}
|
|
135
138
|
else if (v.startsWith('!')) {
|
|
136
|
-
filter[effectiveKey] = { $ne: this.coerceValue(v.slice(1)) };
|
|
139
|
+
filter[effectiveKey] = { $ne: isStringField ? v.slice(1) : this.coerceValue(v.slice(1)) };
|
|
137
140
|
}
|
|
138
141
|
else if (/^(gt|gte|lt|lte):/i.test(v)) {
|
|
139
142
|
const [op, rest] = v.split(':', 2);
|
|
140
143
|
const map = { gt: '$gt', gte: '$gte', lt: '$lt', lte: '$lte' };
|
|
141
|
-
filter[effectiveKey] = { [map[op.toLowerCase()]]: this.coerceValue(rest) };
|
|
144
|
+
filter[effectiveKey] = { [map[op.toLowerCase()]]: isStringField ? rest : this.coerceValue(rest) };
|
|
142
145
|
}
|
|
143
146
|
else {
|
|
144
|
-
filter[effectiveKey] = this.coerceValue(v);
|
|
147
|
+
filter[effectiveKey] = isStringField ? v : this.coerceValue(v);
|
|
145
148
|
}
|
|
146
149
|
}
|
|
147
150
|
return filter;
|
|
@@ -36,6 +36,7 @@ export interface FileStorageAdapter {
|
|
|
36
36
|
delete(key: string): Promise<{
|
|
37
37
|
deleted: boolean;
|
|
38
38
|
}>;
|
|
39
|
+
exists?(key: string): Promise<boolean>;
|
|
39
40
|
/** Optional helpers */
|
|
40
41
|
getPublicUrl?(key: string): string | null;
|
|
41
42
|
getSignedUrl?(key: string, expiresInSeconds?: number): Promise<string>;
|
|
@@ -8,6 +8,7 @@ export declare class LocalFileStorageAdapter implements FileStorageAdapter {
|
|
|
8
8
|
private uploadDir;
|
|
9
9
|
private publicUrlPrefix;
|
|
10
10
|
constructor(options: LocalFileStorageOptions);
|
|
11
|
+
exists(key: string): Promise<boolean>;
|
|
11
12
|
upload(input: UploadPayload): Promise<UploadResult>;
|
|
12
13
|
download(key: string): Promise<{
|
|
13
14
|
body: any;
|
|
@@ -18,11 +18,33 @@ class LocalFileStorageAdapter {
|
|
|
18
18
|
fs_1.default.mkdirSync(this.uploadDir, { recursive: true });
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
|
+
async exists(key) {
|
|
22
|
+
const filePath = path_1.default.join(this.uploadDir, key);
|
|
23
|
+
return fs_1.default.existsSync(filePath);
|
|
24
|
+
}
|
|
21
25
|
async upload(input) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
26
|
+
let uniqueName = input.key;
|
|
27
|
+
if (!uniqueName) {
|
|
28
|
+
if (input.originalFilename) {
|
|
29
|
+
const lastDot = input.originalFilename.lastIndexOf('.');
|
|
30
|
+
const hasExt = lastDot > 0 && lastDot < input.originalFilename.length - 1;
|
|
31
|
+
const nameWithoutExt = hasExt ? input.originalFilename.slice(0, lastDot) : input.originalFilename;
|
|
32
|
+
const ext = hasExt ? input.originalFilename.slice(lastDot).toLowerCase() : '';
|
|
33
|
+
const randomId = crypto_1.default.randomBytes(4).toString('hex');
|
|
34
|
+
const cleanName = (0, filename_1.slugify)(nameWithoutExt) || randomId;
|
|
35
|
+
uniqueName = `${cleanName}${ext}`;
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
uniqueName = crypto_1.default.randomBytes(8).toString('hex');
|
|
39
|
+
}
|
|
40
|
+
}
|
|
25
41
|
const filePath = path_1.default.join(this.uploadDir, uniqueName);
|
|
42
|
+
if (fs_1.default.existsSync(filePath)) {
|
|
43
|
+
const err = new Error(`File '${uniqueName}' already exists in storage. Please rename the file manually and try again.`);
|
|
44
|
+
err.statusCode = 409;
|
|
45
|
+
err.code = 'FILE_EXISTS';
|
|
46
|
+
throw err;
|
|
47
|
+
}
|
|
26
48
|
const writeStream = fs_1.default.createWriteStream(filePath);
|
|
27
49
|
// Convert body to stream if it is a Buffer
|
|
28
50
|
const readStream = new stream_1.Readable();
|
|
@@ -20,6 +20,7 @@ export declare class S3FileStorageAdapter implements FileStorageAdapter {
|
|
|
20
20
|
private readonly forcePathStyle;
|
|
21
21
|
private readonly defaultACL?;
|
|
22
22
|
private readonly publicBaseUrl?;
|
|
23
|
+
private readonly credentials?;
|
|
23
24
|
constructor(opts: S3AdapterOptions);
|
|
24
25
|
upload(input: UploadPayload): Promise<UploadResult>;
|
|
25
26
|
download(key: string): Promise<{
|
|
@@ -30,6 +31,7 @@ export declare class S3FileStorageAdapter implements FileStorageAdapter {
|
|
|
30
31
|
delete(key: string): Promise<{
|
|
31
32
|
deleted: boolean;
|
|
32
33
|
}>;
|
|
34
|
+
exists(key: string): Promise<boolean>;
|
|
33
35
|
getPublicUrl(key: string): string;
|
|
34
|
-
private
|
|
36
|
+
private buildKey;
|
|
35
37
|
}
|
|
@@ -16,24 +16,49 @@ class S3FileStorageAdapter {
|
|
|
16
16
|
this.forcePathStyle = opts.forcePathStyle ?? true; // sane default for MinIO
|
|
17
17
|
this.defaultACL = opts.defaultACL;
|
|
18
18
|
this.publicBaseUrl = opts.publicBaseUrl;
|
|
19
|
+
this.credentials = opts.credentials;
|
|
19
20
|
this.s3 = new client_s3_1.S3Client({
|
|
20
21
|
region: this.regionStr,
|
|
21
22
|
endpoint: this.endpoint,
|
|
22
23
|
forcePathStyle: this.forcePathStyle,
|
|
23
24
|
credentials: opts.credentials,
|
|
25
|
+
requestChecksumCalculation: 'WHEN_REQUIRED',
|
|
24
26
|
});
|
|
25
27
|
}
|
|
26
28
|
async upload(input) {
|
|
27
29
|
if (!input.body)
|
|
28
30
|
throw new Error('S3 upload: "body" is required');
|
|
29
|
-
const key = input.key ?? this.
|
|
31
|
+
const key = input.key ?? this.buildKey(input.originalFilename);
|
|
32
|
+
const alreadyExists = await this.exists(key);
|
|
33
|
+
if (alreadyExists) {
|
|
34
|
+
const filename = key.split('/').pop() || key;
|
|
35
|
+
const err = new Error(`File '${filename}' already exists in storage. Please rename the file manually and try again.`);
|
|
36
|
+
err.statusCode = 409;
|
|
37
|
+
err.code = 'FILE_EXISTS';
|
|
38
|
+
throw err;
|
|
39
|
+
}
|
|
40
|
+
const sanitizedMetadata = {};
|
|
41
|
+
if (input.metadata) {
|
|
42
|
+
for (const [k, v] of Object.entries(input.metadata)) {
|
|
43
|
+
if (typeof v === 'string') {
|
|
44
|
+
// Replace non-ascii spaces (e.g. narrow no-break space U+202F, no-break space U+00A0)
|
|
45
|
+
// and keep only printable ASCII characters (0x20 to 0x7E) to satisfy S3 metadata specs.
|
|
46
|
+
sanitizedMetadata[k] = v
|
|
47
|
+
.replace(/[\u200B-\u200D\uFEFF\u202F\u00A0]/g, ' ')
|
|
48
|
+
.replace(/[^\x20-\x7E]/g, '');
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
sanitizedMetadata[k] = String(v);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
30
55
|
// Build params without ACL by default
|
|
31
56
|
const params = {
|
|
32
57
|
Bucket: this.bucket,
|
|
33
58
|
Key: key,
|
|
34
59
|
Body: input.body,
|
|
35
60
|
ContentType: input.contentType,
|
|
36
|
-
Metadata:
|
|
61
|
+
Metadata: sanitizedMetadata,
|
|
37
62
|
CacheControl: input.contentType?.startsWith('image/')
|
|
38
63
|
? 'public, max-age=31536000, immutable'
|
|
39
64
|
: 'public, max-age=86400',
|
|
@@ -43,7 +68,27 @@ class S3FileStorageAdapter {
|
|
|
43
68
|
if (this.endpoint && this.defaultACL) {
|
|
44
69
|
params.ACL = this.defaultACL;
|
|
45
70
|
}
|
|
46
|
-
|
|
71
|
+
try {
|
|
72
|
+
await this.s3.send(new client_s3_1.PutObjectCommand(params));
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
console.error('[R2 UPLOAD ERROR] PutObject failed:', {
|
|
76
|
+
bucket: this.bucket,
|
|
77
|
+
key: key,
|
|
78
|
+
contentType: input.contentType,
|
|
79
|
+
endpoint: this.endpoint,
|
|
80
|
+
region: this.regionStr,
|
|
81
|
+
forcePathStyle: this.forcePathStyle,
|
|
82
|
+
defaultACL: this.defaultACL,
|
|
83
|
+
hasCredentials: !!this.credentials,
|
|
84
|
+
accessKeyIdLength: this.credentials?.accessKeyId?.length,
|
|
85
|
+
secretAccessKeyLength: this.credentials?.secretAccessKey?.length,
|
|
86
|
+
accessKeyIdStart: this.credentials?.accessKeyId?.slice(0, 4),
|
|
87
|
+
secretAccessKeyStart: this.credentials?.secretAccessKey?.slice(0, 4),
|
|
88
|
+
});
|
|
89
|
+
console.error('[R2 UPLOAD ERROR] Detailed Error:', err);
|
|
90
|
+
throw err;
|
|
91
|
+
}
|
|
47
92
|
const url = this.getPublicUrl(key);
|
|
48
93
|
return {
|
|
49
94
|
provider: 's3',
|
|
@@ -73,6 +118,18 @@ class S3FileStorageAdapter {
|
|
|
73
118
|
await this.s3.send(new client_s3_1.DeleteObjectCommand({ Bucket: this.bucket, Key: key }));
|
|
74
119
|
return { deleted: true };
|
|
75
120
|
}
|
|
121
|
+
async exists(key) {
|
|
122
|
+
try {
|
|
123
|
+
await this.s3.send(new client_s3_1.HeadObjectCommand({ Bucket: this.bucket, Key: key }));
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
if (err.name === 'NotFound' || err.name === 'NoSuchKey' || err.$metadata?.httpStatusCode === 404) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
76
133
|
getPublicUrl(key) {
|
|
77
134
|
const encKey = encodeKeySegments(key);
|
|
78
135
|
if (this.publicBaseUrl) {
|
|
@@ -86,19 +143,23 @@ class S3FileStorageAdapter {
|
|
|
86
143
|
return `https://${this.bucket}.s3.${this.regionStr}.amazonaws.com/${encKey}`;
|
|
87
144
|
}
|
|
88
145
|
// ---- private helpers ----
|
|
89
|
-
|
|
146
|
+
buildKey(originalFilename) {
|
|
90
147
|
const ts = new Date();
|
|
91
148
|
const y = ts.getUTCFullYear();
|
|
92
149
|
const m = String(ts.getUTCMonth() + 1).padStart(2, '0');
|
|
93
150
|
const d = String(ts.getUTCDate()).padStart(2, '0');
|
|
151
|
+
if (originalFilename) {
|
|
152
|
+
const lastDot = originalFilename.lastIndexOf('.');
|
|
153
|
+
const hasExt = lastDot > 0 && lastDot < originalFilename.length - 1;
|
|
154
|
+
const nameWithoutExt = hasExt ? originalFilename.slice(0, lastDot) : originalFilename;
|
|
155
|
+
const ext = hasExt ? originalFilename.slice(lastDot).toLowerCase() : '';
|
|
156
|
+
const randomId = Math.random().toString(36).slice(2, 10);
|
|
157
|
+
const cleanName = (0, filename_1.slugify)(nameWithoutExt) || randomId;
|
|
158
|
+
const filename = `${cleanName}${ext}`;
|
|
159
|
+
return `uploads/${y}/${m}/${d}/${filename}`;
|
|
160
|
+
}
|
|
94
161
|
const randomId = Math.random().toString(36).slice(2, 10);
|
|
95
|
-
|
|
96
|
-
const filename = originalFilename
|
|
97
|
-
? (0, filename_1.filenameWithRandomId)(originalFilename, randomId)
|
|
98
|
-
: randomId;
|
|
99
|
-
// Path: uploads/YYYY/MM/DD/filename
|
|
100
|
-
const fullPath = `uploads/${y}/${m}/${d}/${filename}`;
|
|
101
|
-
return fullPath;
|
|
162
|
+
return `uploads/${y}/${m}/${d}/${randomId}`;
|
|
102
163
|
}
|
|
103
164
|
}
|
|
104
165
|
exports.S3FileStorageAdapter = S3FileStorageAdapter;
|
package/dist/files/filename.js
CHANGED
|
@@ -16,13 +16,15 @@ function sanitizeFilename(name) {
|
|
|
16
16
|
* Example: "My Photo (2024).jpg" → "my-photo-2024.jpg"
|
|
17
17
|
*/
|
|
18
18
|
function slugify(text) {
|
|
19
|
+
if (!text)
|
|
20
|
+
return '';
|
|
19
21
|
return text
|
|
20
22
|
.toLowerCase()
|
|
21
23
|
.trim()
|
|
22
24
|
// Replace spaces and underscores with hyphens
|
|
23
25
|
.replace(/[\s_]+/g, '-')
|
|
24
|
-
// Remove special characters except hyphens, dots, and alphanumeric
|
|
25
|
-
.replace(/[^\
|
|
26
|
+
// Remove special characters except hyphens, dots, and alphanumeric/Unicode letters & marks
|
|
27
|
+
.replace(/[^\p{L}\p{N}\p{M}\-_.]+/gu, '')
|
|
26
28
|
// Replace multiple consecutive hyphens with single hyphen
|
|
27
29
|
.replace(/-+/g, '-')
|
|
28
30
|
// Remove leading/trailing hyphens
|
|
@@ -40,8 +42,8 @@ function filenameWithRandomId(originalName, randomId) {
|
|
|
40
42
|
const ext = hasExt ? originalName.slice(lastDot) : '';
|
|
41
43
|
// Slugify the name part
|
|
42
44
|
const slugifiedName = slugify(nameWithoutExt);
|
|
43
|
-
// Combine: slugified-name-randomid.ext
|
|
44
|
-
const result = `${slugifiedName}-${randomId}${ext}`;
|
|
45
|
+
// Combine: slugified-name-randomid.ext (or randomid.ext if empty)
|
|
46
|
+
const result = slugifiedName ? `${slugifiedName}-${randomId}${ext}` : `${randomId}${ext}`;
|
|
45
47
|
return result;
|
|
46
48
|
}
|
|
47
49
|
function withTimestampPrefix(name) {
|
package/dist/index.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export * from './files/FileStorageAdapter';
|
|
|
8
8
|
export * from './files/S3FileStorageAdapter';
|
|
9
9
|
export * from './files/LocalFileStorageAdapter';
|
|
10
10
|
export * from './files/filename';
|
|
11
|
+
export { reindex } from './services/IndexingService';
|
|
11
12
|
/**
|
|
12
13
|
* Compose the public router:
|
|
13
14
|
* - JSON & urlencoded body parsers
|
package/dist/index.js
CHANGED
|
@@ -17,7 +17,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
17
17
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
18
18
|
};
|
|
19
19
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
-
exports.generateApiKey = exports.Events = exports.events = exports.QueryEngine = exports.NMAdapter = void 0;
|
|
20
|
+
exports.reindex = exports.generateApiKey = exports.Events = exports.events = exports.QueryEngine = exports.NMAdapter = void 0;
|
|
21
21
|
exports.createNextMinRouter = createNextMinRouter;
|
|
22
22
|
const express_1 = __importDefault(require("express"));
|
|
23
23
|
const body_parser_1 = __importDefault(require("body-parser"));
|
|
@@ -36,6 +36,8 @@ __exportStar(require("./files/FileStorageAdapter"), exports);
|
|
|
36
36
|
__exportStar(require("./files/S3FileStorageAdapter"), exports);
|
|
37
37
|
__exportStar(require("./files/LocalFileStorageAdapter"), exports);
|
|
38
38
|
__exportStar(require("./files/filename"), exports);
|
|
39
|
+
var IndexingService_1 = require("./services/IndexingService");
|
|
40
|
+
Object.defineProperty(exports, "reindex", { enumerable: true, get: function () { return IndexingService_1.reindex; } });
|
|
39
41
|
function isMultipart(req) {
|
|
40
42
|
const ct = (req.headers['content-type'] || '').toLowerCase();
|
|
41
43
|
return ct.startsWith('multipart/form-data');
|
|
@@ -53,6 +53,10 @@ export interface Schema {
|
|
|
53
53
|
actions?: CustomAction[];
|
|
54
54
|
access?: any;
|
|
55
55
|
aggregates?: AggregateSpec[];
|
|
56
|
+
publicRegisterFields?: string[];
|
|
57
|
+
roleDefaultStatuses?: Record<string, string>;
|
|
58
|
+
authServices?: string[];
|
|
59
|
+
phoneVerification?: boolean;
|
|
56
60
|
}
|
|
57
61
|
export interface AggregateSpec {
|
|
58
62
|
sourceModel: string;
|
|
@@ -21,5 +21,5 @@ export declare function applyReadMaskOne(doc: any, mask?: string[]): any;
|
|
|
21
21
|
export declare function applyReadMaskMany(docs: any[], mask?: string[]): any[];
|
|
22
22
|
export declare function stripWriteDeny(payload: any, deny?: string[]): any;
|
|
23
23
|
export declare function mergeCreateDefaults(payload: any, defs?: Record<string, any>): any;
|
|
24
|
-
export declare function enforceRestrictions(payload: any, restrictions?: Record<string, any>, _ctx?: PolicyContext): void
|
|
24
|
+
export declare function enforceRestrictions(payload: any, restrictions?: Record<string, any>, _ctx?: PolicyContext, routerCtx?: any): Promise<void>;
|
|
25
25
|
export declare function andFilter(a?: Record<string, any>, b?: Record<string, any>): Record<string, any>;
|
package/dist/policy/authorize.js
CHANGED
|
@@ -59,22 +59,51 @@ function asBool(v) {
|
|
|
59
59
|
return typeof v === 'boolean' ? v : undefined;
|
|
60
60
|
}
|
|
61
61
|
function ownerKey(schemaPolicy) {
|
|
62
|
-
const by = schemaPolicy?.conditions?.owner?.by;
|
|
62
|
+
const by = schemaPolicy?.conditions?.owner?.by || schemaPolicy?.access?.conditions?.owner?.by;
|
|
63
63
|
return typeof by === 'string' && by.trim() ? by.trim() : 'id';
|
|
64
64
|
}
|
|
65
|
-
function ownerFilter(schemaPolicy, ctx) {
|
|
66
|
-
const key = ownerKey(schemaPolicy);
|
|
65
|
+
function ownerFilter(schemaPolicy, ctx, modelNameLC) {
|
|
67
66
|
if (!ctx.userId)
|
|
68
67
|
return {};
|
|
68
|
+
const isUserModel = modelNameLC === 'users' || modelNameLC === 'user';
|
|
69
|
+
if (isUserModel) {
|
|
70
|
+
if (ctx.role === 'admin') {
|
|
71
|
+
return {
|
|
72
|
+
or: [
|
|
73
|
+
{ id: ctx.userId },
|
|
74
|
+
{ createdBy: ctx.userId }
|
|
75
|
+
]
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
return { id: ctx.userId };
|
|
79
|
+
}
|
|
80
|
+
const key = ownerKey(schemaPolicy);
|
|
69
81
|
return { [key]: ctx.userId };
|
|
70
82
|
}
|
|
71
|
-
function isOwnerDoc(schemaPolicy, ctx, doc) {
|
|
83
|
+
function isOwnerDoc(schemaPolicy, ctx, doc, modelNameLC) {
|
|
72
84
|
if (!doc || !ctx.userId)
|
|
73
85
|
return false;
|
|
86
|
+
const isUserModel = modelNameLC === 'users' || modelNameLC === 'user';
|
|
87
|
+
if (isUserModel) {
|
|
88
|
+
const isSelf = String(doc.id ?? doc._id ?? '') === String(ctx.userId);
|
|
89
|
+
const isCreator = doc.createdBy && String(typeof doc.createdBy === 'object' ? (doc.createdBy.id ?? doc.createdBy._id ?? doc.createdBy) : doc.createdBy) === String(ctx.userId);
|
|
90
|
+
if (ctx.role === 'admin') {
|
|
91
|
+
return isSelf || isCreator;
|
|
92
|
+
}
|
|
93
|
+
return isSelf;
|
|
94
|
+
}
|
|
74
95
|
const key = ownerKey(schemaPolicy);
|
|
75
96
|
const val = doc[key];
|
|
76
97
|
if (val == null)
|
|
77
98
|
return false;
|
|
99
|
+
if (Array.isArray(val)) {
|
|
100
|
+
return val.some((v) => {
|
|
101
|
+
if (v == null)
|
|
102
|
+
return false;
|
|
103
|
+
const candidate = typeof v === 'object' ? (v.id ?? v._id ?? String(v)) : String(v);
|
|
104
|
+
return String(candidate) === String(ctx.userId);
|
|
105
|
+
});
|
|
106
|
+
}
|
|
78
107
|
const candidate = typeof val === 'object' ? (val.id ?? val._id ?? String(val)) : String(val);
|
|
79
108
|
return String(candidate) === String(ctx.userId);
|
|
80
109
|
}
|
|
@@ -121,6 +150,20 @@ function authorize(modelNameLC, action, schemaPolicy, ctx, doc) {
|
|
|
121
150
|
const allowed = !!schemaPolicy?.allowedMethods?.[action];
|
|
122
151
|
if (!allowed)
|
|
123
152
|
return { ...EMPTY_DECISION, allow: false };
|
|
153
|
+
// Bypass all access rules for superadmin and admin roles
|
|
154
|
+
if (ctx.isSuperadmin || ctx.role === 'superadmin' || ctx.role === 'admin') {
|
|
155
|
+
const sensitiveMask = computeSensitiveMask(schemaPolicy);
|
|
156
|
+
return {
|
|
157
|
+
allow: true,
|
|
158
|
+
readMask: sensitiveMask,
|
|
159
|
+
writeDeny: [],
|
|
160
|
+
createDefaults: {},
|
|
161
|
+
restrictions: {},
|
|
162
|
+
queryFilter: {},
|
|
163
|
+
exposePrivate: true,
|
|
164
|
+
sensitiveMask,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
124
167
|
const access = schemaPolicy?.access || {};
|
|
125
168
|
const baseReadMask = [].concat(access?.readMask || []);
|
|
126
169
|
// Auto-mask private attributes
|
|
@@ -228,7 +271,7 @@ function authorize(modelNameLC, action, schemaPolicy, ctx, doc) {
|
|
|
228
271
|
if (action === 'read') {
|
|
229
272
|
if (doc) {
|
|
230
273
|
return finalize({
|
|
231
|
-
allow: isOwnerDoc(schemaPolicy, ctx, doc),
|
|
274
|
+
allow: isOwnerDoc(schemaPolicy, ctx, doc, modelNameLC),
|
|
232
275
|
readMask: effectiveReadMask,
|
|
233
276
|
writeDeny: effectiveWriteDeny,
|
|
234
277
|
createDefaults: {},
|
|
@@ -244,13 +287,13 @@ function authorize(modelNameLC, action, schemaPolicy, ctx, doc) {
|
|
|
244
287
|
writeDeny: effectiveWriteDeny,
|
|
245
288
|
createDefaults: {},
|
|
246
289
|
restrictions: {},
|
|
247
|
-
queryFilter: ownerFilter(schemaPolicy, ctx),
|
|
290
|
+
queryFilter: ownerFilter(schemaPolicy, ctx, modelNameLC),
|
|
248
291
|
exposePrivate: bypass,
|
|
249
292
|
sensitiveMask,
|
|
250
293
|
});
|
|
251
294
|
}
|
|
252
295
|
return finalize({
|
|
253
|
-
allow: isOwnerDoc(schemaPolicy, ctx, doc),
|
|
296
|
+
allow: isOwnerDoc(schemaPolicy, ctx, doc, modelNameLC),
|
|
254
297
|
readMask: effectiveReadMask,
|
|
255
298
|
writeDeny: effectiveWriteDeny,
|
|
256
299
|
createDefaults: {},
|
|
@@ -289,7 +332,7 @@ function authorize(modelNameLC, action, schemaPolicy, ctx, doc) {
|
|
|
289
332
|
if (action === 'read') {
|
|
290
333
|
if (doc) {
|
|
291
334
|
return finalize({
|
|
292
|
-
allow: isOwnerDoc(schemaPolicy, ctx, doc),
|
|
335
|
+
allow: isOwnerDoc(schemaPolicy, ctx, doc, modelNameLC),
|
|
293
336
|
readMask: effectiveReadMask,
|
|
294
337
|
writeDeny: effectiveWriteDeny,
|
|
295
338
|
createDefaults: {},
|
|
@@ -305,13 +348,13 @@ function authorize(modelNameLC, action, schemaPolicy, ctx, doc) {
|
|
|
305
348
|
writeDeny: effectiveWriteDeny,
|
|
306
349
|
createDefaults: {},
|
|
307
350
|
restrictions: {},
|
|
308
|
-
queryFilter: ownerFilter(schemaPolicy, ctx),
|
|
351
|
+
queryFilter: ownerFilter(schemaPolicy, ctx, modelNameLC),
|
|
309
352
|
exposePrivate: bypass,
|
|
310
353
|
sensitiveMask,
|
|
311
354
|
});
|
|
312
355
|
}
|
|
313
356
|
return finalize({
|
|
314
|
-
allow: isOwnerDoc(schemaPolicy, ctx, doc),
|
|
357
|
+
allow: isOwnerDoc(schemaPolicy, ctx, doc, modelNameLC),
|
|
315
358
|
readMask: effectiveReadMask,
|
|
316
359
|
writeDeny: effectiveReadMask, // (typo guard: will be ignored by callers; leave as is)
|
|
317
360
|
createDefaults: {},
|
|
@@ -360,12 +403,20 @@ function mergeCreateDefaults(payload, defs) {
|
|
|
360
403
|
return payload;
|
|
361
404
|
return { ...defs, ...payload };
|
|
362
405
|
}
|
|
363
|
-
function enforceRestrictions(payload, restrictions, _ctx) {
|
|
406
|
+
async function enforceRestrictions(payload, restrictions, _ctx, routerCtx) {
|
|
364
407
|
if (!payload || !restrictions)
|
|
365
408
|
return;
|
|
366
409
|
for (const [field, blocked] of Object.entries(restrictions)) {
|
|
367
|
-
|
|
368
|
-
|
|
410
|
+
let val = payload[field];
|
|
411
|
+
if (val != null) {
|
|
412
|
+
if (field === 'role' && routerCtx?.normalizeRoleName) {
|
|
413
|
+
const resolvedRole = await routerCtx.normalizeRoleName(val);
|
|
414
|
+
if (resolvedRole)
|
|
415
|
+
val = resolvedRole;
|
|
416
|
+
}
|
|
417
|
+
if (Array.isArray(blocked) && blocked.map(String).includes(String(val))) {
|
|
418
|
+
throw new Error(`Restricted value for field: ${field}`);
|
|
419
|
+
}
|
|
369
420
|
}
|
|
370
421
|
}
|
|
371
422
|
}
|
package/dist/schemas/Users.json
CHANGED
|
@@ -68,13 +68,18 @@
|
|
|
68
68
|
"default": "user",
|
|
69
69
|
"required": true,
|
|
70
70
|
"safe": true
|
|
71
|
+
},
|
|
72
|
+
"createdBy": {
|
|
73
|
+
"type": "ObjectId",
|
|
74
|
+
"ref": "Users",
|
|
75
|
+
"private": true
|
|
71
76
|
}
|
|
72
77
|
},
|
|
73
78
|
"allowedMethods": {
|
|
74
79
|
"create": true,
|
|
75
80
|
"read": true,
|
|
76
81
|
"update": true,
|
|
77
|
-
"delete":
|
|
82
|
+
"delete": true
|
|
78
83
|
},
|
|
79
84
|
"access": {
|
|
80
85
|
"public": {
|
|
@@ -87,14 +92,14 @@
|
|
|
87
92
|
"create": false,
|
|
88
93
|
"read": "owner",
|
|
89
94
|
"update": "owner",
|
|
90
|
-
"delete":
|
|
95
|
+
"delete": "owner"
|
|
91
96
|
},
|
|
92
97
|
"roles": {
|
|
93
98
|
"admin": {
|
|
94
99
|
"create": true,
|
|
95
100
|
"read": true,
|
|
96
|
-
"update":
|
|
97
|
-
"delete":
|
|
101
|
+
"update": "owner",
|
|
102
|
+
"delete": "owner"
|
|
98
103
|
},
|
|
99
104
|
"superadmin": {
|
|
100
105
|
"create": true,
|
|
@@ -141,17 +146,22 @@
|
|
|
141
146
|
"status": "pending"
|
|
142
147
|
},
|
|
143
148
|
"roles.admin": {
|
|
144
|
-
"status": "active"
|
|
149
|
+
"status": "active",
|
|
150
|
+
"createdBy": "$CTX.userId"
|
|
145
151
|
},
|
|
146
152
|
"roles.superadmin": {
|
|
147
|
-
"status": "active"
|
|
153
|
+
"status": "active",
|
|
154
|
+
"createdBy": "$CTX.userId"
|
|
148
155
|
}
|
|
149
156
|
},
|
|
150
157
|
"restrictions": {
|
|
151
|
-
"roles
|
|
152
|
-
"
|
|
153
|
-
"
|
|
154
|
-
|
|
158
|
+
"roles": {
|
|
159
|
+
"admin": {
|
|
160
|
+
"role": [
|
|
161
|
+
"superadmin",
|
|
162
|
+
"admin"
|
|
163
|
+
]
|
|
164
|
+
}
|
|
155
165
|
}
|
|
156
166
|
}
|
|
157
167
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { ChatWidgetConfig } from '../api/apiRouter';
|
|
2
|
+
export declare class IndexingService {
|
|
3
|
+
private chatWidget;
|
|
4
|
+
private force;
|
|
5
|
+
private dbAdapter?;
|
|
6
|
+
private itemKeyToId;
|
|
7
|
+
constructor(chatWidget: ChatWidgetConfig, force?: boolean, dbAdapter?: any);
|
|
8
|
+
private getManifestPath;
|
|
9
|
+
private readManifest;
|
|
10
|
+
private writeManifest;
|
|
11
|
+
private getScrapedPagesDir;
|
|
12
|
+
private writeScrapedPageLocal;
|
|
13
|
+
private deleteScrapedPageLocal;
|
|
14
|
+
private fetchSitemapUrls;
|
|
15
|
+
crawlSite(siteUrl: string, startPaths?: string[], concurrency?: number): Promise<Map<string, string>>;
|
|
16
|
+
private extractInternalLinks;
|
|
17
|
+
private indexDatabaseSchemas;
|
|
18
|
+
runIndex(): Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
export declare function reindex(config: {
|
|
21
|
+
chatWidget: ChatWidgetConfig;
|
|
22
|
+
force?: boolean;
|
|
23
|
+
dbAdapter?: any;
|
|
24
|
+
}): Promise<void>;
|