@plank-cms/plank 0.15.3 → 0.17.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/admin/assets/index-BUxIRJB1.css +2 -0
- package/dist/admin/assets/index-D7pl1GEz.js +223 -0
- package/dist/admin/index.html +3 -3
- package/dist/index.js +2 -2
- package/dist/migrations/027_roles_contributor_editor.sql +23 -0
- package/dist/migrations/028_editorial_mode_base.sql +49 -0
- package/dist/{server-TQTYSOGD.js → server-P4JVP3GT.js} +328 -68
- package/package.json +4 -4
- package/dist/admin/assets/index-fAjS4w6T.js +0 -223
- package/dist/admin/assets/index-ojuLCat6.css +0 -2
|
@@ -45,12 +45,26 @@ var DEFAULT_ROLE_PERMISSIONS = {
|
|
|
45
45
|
"settings:webhooks:write",
|
|
46
46
|
"settings:webhooks:delete"
|
|
47
47
|
],
|
|
48
|
-
"
|
|
48
|
+
"Contributor": [
|
|
49
49
|
"content-types:read",
|
|
50
50
|
"entries:read",
|
|
51
51
|
"entries:write",
|
|
52
|
+
"entries:delete",
|
|
52
53
|
"media:read",
|
|
53
54
|
"media:write"
|
|
55
|
+
],
|
|
56
|
+
"Editor": [
|
|
57
|
+
"content-types:read",
|
|
58
|
+
"entries:read",
|
|
59
|
+
"entries:write",
|
|
60
|
+
"entries:delete",
|
|
61
|
+
"media:read",
|
|
62
|
+
"media:write"
|
|
63
|
+
],
|
|
64
|
+
"Viewer": [
|
|
65
|
+
"content-types:read",
|
|
66
|
+
"entries:read",
|
|
67
|
+
"media:read"
|
|
54
68
|
]
|
|
55
69
|
};
|
|
56
70
|
|
|
@@ -289,7 +303,7 @@ async function createTable(contentType) {
|
|
|
289
303
|
await pool_default.query(sql);
|
|
290
304
|
try {
|
|
291
305
|
await pool_default.query(`CREATE INDEX IF NOT EXISTS idx_${contentType.tableName}_localized_gin ON ${quotedTableName} USING gin (localized)`);
|
|
292
|
-
} catch
|
|
306
|
+
} catch {
|
|
293
307
|
}
|
|
294
308
|
for (const field of contentType.fields) {
|
|
295
309
|
if (field.type === "relation" && (field.relationType ?? "many-to-one") === "many-to-many" && field.relatedTable) {
|
|
@@ -397,10 +411,10 @@ async function syncAllTables() {
|
|
|
397
411
|
existingColumns.add("localized");
|
|
398
412
|
try {
|
|
399
413
|
await pool_default.query(`CREATE INDEX IF NOT EXISTS idx_${ct.tableName}_localized_gin ON ${quotedTableName} USING gin (localized)`);
|
|
400
|
-
} catch
|
|
414
|
+
} catch {
|
|
401
415
|
}
|
|
402
416
|
console.log(`[plank] Added missing column "localized" to table "${ct.tableName}"`);
|
|
403
|
-
} catch
|
|
417
|
+
} catch {
|
|
404
418
|
}
|
|
405
419
|
}
|
|
406
420
|
for (const field of ct.fields) {
|
|
@@ -423,6 +437,18 @@ async function syncAllTables() {
|
|
|
423
437
|
// ../schema/dist/validator.js
|
|
424
438
|
var MAX_NAVIGATION_DEPTH = 3;
|
|
425
439
|
var MAX_NAVIGATION_ITEMS_PER_LEVEL = 20;
|
|
440
|
+
function isMixedArrayValue(value) {
|
|
441
|
+
if (typeof value !== "object" || value === null)
|
|
442
|
+
return false;
|
|
443
|
+
const v3 = value;
|
|
444
|
+
if (v3.kind === "string")
|
|
445
|
+
return typeof v3.value === "string";
|
|
446
|
+
if (v3.kind === "number")
|
|
447
|
+
return typeof v3.value === "number" && !isNaN(v3.value);
|
|
448
|
+
if (v3.kind === "boolean")
|
|
449
|
+
return typeof v3.value === "boolean";
|
|
450
|
+
return false;
|
|
451
|
+
}
|
|
426
452
|
function validateNavigationItems(value, fieldName, errors, path = fieldName, depth = 1) {
|
|
427
453
|
if (!Array.isArray(value)) {
|
|
428
454
|
errors.push(`Field "${path}" must be an array`);
|
|
@@ -535,6 +561,36 @@ function validate(contentType, payload) {
|
|
|
535
561
|
const subEmpty = subValue === void 0 || subValue === null || subValue === "";
|
|
536
562
|
if (subField.required && subEmpty) {
|
|
537
563
|
errors.push(`Field "${field.name}[${i2}].${subField.name}" is required`);
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
if (subEmpty)
|
|
567
|
+
continue;
|
|
568
|
+
if (subField.type === "string" || subField.type === "text" || subField.type === "richtext") {
|
|
569
|
+
if (typeof subValue !== "string") {
|
|
570
|
+
errors.push(`Field "${field.name}[${i2}].${subField.name}" must be a string`);
|
|
571
|
+
}
|
|
572
|
+
} else if (subField.type === "number") {
|
|
573
|
+
if (typeof subValue !== "number" || isNaN(subValue)) {
|
|
574
|
+
errors.push(`Field "${field.name}[${i2}].${subField.name}" must be a number`);
|
|
575
|
+
} else if (subField.subtype !== "float" && !Number.isInteger(subValue)) {
|
|
576
|
+
errors.push(`Field "${field.name}[${i2}].${subField.name}" must be an integer`);
|
|
577
|
+
}
|
|
578
|
+
} else if (subField.type === "boolean") {
|
|
579
|
+
if (typeof subValue !== "boolean") {
|
|
580
|
+
errors.push(`Field "${field.name}[${i2}].${subField.name}" must be a boolean`);
|
|
581
|
+
}
|
|
582
|
+
} else if (subField.type === "datetime") {
|
|
583
|
+
if (!(subValue instanceof Date) && isNaN(Date.parse(String(subValue)))) {
|
|
584
|
+
errors.push(`Field "${field.name}[${i2}].${subField.name}" must be a valid date`);
|
|
585
|
+
}
|
|
586
|
+
} else if (subField.type === "media") {
|
|
587
|
+
if (typeof subValue !== "string" || !subValue.trim()) {
|
|
588
|
+
errors.push(`Field "${field.name}[${i2}].${subField.name}" must be a non-empty string URL`);
|
|
589
|
+
}
|
|
590
|
+
} else if (subField.type === "mixed") {
|
|
591
|
+
if (!isMixedArrayValue(subValue)) {
|
|
592
|
+
errors.push(`Field "${field.name}[${i2}].${subField.name}" must be a mixed value object with kind/value`);
|
|
593
|
+
}
|
|
538
594
|
}
|
|
539
595
|
}
|
|
540
596
|
}
|
|
@@ -2103,7 +2159,7 @@ var localProvider = {
|
|
|
2103
2159
|
const base = await publicUrl();
|
|
2104
2160
|
return { url: `${base}/uploads/${key}`, key };
|
|
2105
2161
|
},
|
|
2106
|
-
async uploadRaw(buffer, exactKey,
|
|
2162
|
+
async uploadRaw(buffer, exactKey, _mimeType) {
|
|
2107
2163
|
const base_dir = await uploadsDir();
|
|
2108
2164
|
const dir = join3(base_dir, exactKey.split("/").slice(0, -1).join("/"));
|
|
2109
2165
|
await mkdir(dir, { recursive: true });
|
|
@@ -2427,7 +2483,7 @@ async function login(req, res) {
|
|
|
2427
2483
|
res.status(429).json({ error: "Too many login attempts. Try again in 15 minutes." });
|
|
2428
2484
|
return;
|
|
2429
2485
|
}
|
|
2430
|
-
const { rows } = await pool_default.query(`SELECT id, email, password, role_id, first_name, last_name, avatar_url, job_title, organization, country, two_factor_enabled, two_factor_secret, session_version
|
|
2486
|
+
const { rows } = await pool_default.query(`SELECT id, email, password, role_id, first_name, last_name, avatar_url, job_title, organization, country, two_factor_enabled, two_factor_secret, enabled, session_version
|
|
2431
2487
|
FROM plank_users
|
|
2432
2488
|
WHERE email = $1`, [email]);
|
|
2433
2489
|
const user = rows[0];
|
|
@@ -2435,6 +2491,10 @@ async function login(req, res) {
|
|
|
2435
2491
|
res.status(401).json({ error: "Invalid credentials" });
|
|
2436
2492
|
return;
|
|
2437
2493
|
}
|
|
2494
|
+
if (!user.enabled) {
|
|
2495
|
+
res.status(403).json({ error: "User is disabled" });
|
|
2496
|
+
return;
|
|
2497
|
+
}
|
|
2438
2498
|
await clearRateLimit("login", rateKey);
|
|
2439
2499
|
if (user.two_factor_enabled && user.two_factor_secret) {
|
|
2440
2500
|
const challengeToken = buildChallengeToken({
|
|
@@ -2478,9 +2538,13 @@ async function loginWithTwoFactor(req, res) {
|
|
|
2478
2538
|
res.status(429).json({ error: "Too many 2FA attempts. Try again in 15 minutes." });
|
|
2479
2539
|
return;
|
|
2480
2540
|
}
|
|
2481
|
-
const { rows } = await pool_default.query(`SELECT id, email, role_id, first_name, last_name, avatar_url, job_title, organization, country, two_factor_enabled, two_factor_secret, password, session_version
|
|
2541
|
+
const { rows } = await pool_default.query(`SELECT id, email, role_id, first_name, last_name, avatar_url, job_title, organization, country, two_factor_enabled, two_factor_secret, password, enabled, session_version
|
|
2482
2542
|
FROM plank_users WHERE id = $1`, [payload.sub]);
|
|
2483
2543
|
const user = rows[0];
|
|
2544
|
+
if (user && !user.enabled) {
|
|
2545
|
+
res.status(403).json({ error: "User is disabled" });
|
|
2546
|
+
return;
|
|
2547
|
+
}
|
|
2484
2548
|
if (!user || !user.two_factor_enabled || !user.two_factor_secret) {
|
|
2485
2549
|
res.status(401).json({ error: "2FA is not enabled for this account" });
|
|
2486
2550
|
return;
|
|
@@ -2622,8 +2686,8 @@ async function authenticate(req, res, next) {
|
|
|
2622
2686
|
if (typeof payload.sv !== "number") {
|
|
2623
2687
|
return { ok: false };
|
|
2624
2688
|
}
|
|
2625
|
-
const { rows } = await pool_default.query("SELECT session_version FROM plank_users WHERE id = $1", [payload.sub]);
|
|
2626
|
-
if (!rows[0] || rows[0].session_version !== payload.sv) {
|
|
2689
|
+
const { rows } = await pool_default.query("SELECT session_version, enabled FROM plank_users WHERE id = $1", [payload.sub]);
|
|
2690
|
+
if (!rows[0] || !rows[0].enabled || rows[0].session_version !== payload.sv) {
|
|
2627
2691
|
return { ok: false };
|
|
2628
2692
|
}
|
|
2629
2693
|
return { ok: true, sessionVersion: rows[0].session_version };
|
|
@@ -2669,6 +2733,25 @@ async function authenticate(req, res, next) {
|
|
|
2669
2733
|
}
|
|
2670
2734
|
}
|
|
2671
2735
|
|
|
2736
|
+
// ../core/dist/lib/editorialMode.js
|
|
2737
|
+
async function isEditorialModeEnabled() {
|
|
2738
|
+
const raw = await getSetting("general", "editorial_mode");
|
|
2739
|
+
return String(raw ?? "false").toLowerCase() === "true";
|
|
2740
|
+
}
|
|
2741
|
+
|
|
2742
|
+
// ../core/dist/lib/appModes.js
|
|
2743
|
+
async function resolveAppModes() {
|
|
2744
|
+
return {
|
|
2745
|
+
editorial: await isEditorialModeEnabled()
|
|
2746
|
+
};
|
|
2747
|
+
}
|
|
2748
|
+
|
|
2749
|
+
// ../core/dist/middlewares/appModes.js
|
|
2750
|
+
async function attachAppModes(req, _res, next) {
|
|
2751
|
+
req.appModes = await resolveAppModes();
|
|
2752
|
+
next();
|
|
2753
|
+
}
|
|
2754
|
+
|
|
2672
2755
|
// ../core/dist/middlewares/authorize.js
|
|
2673
2756
|
function authorize(permission) {
|
|
2674
2757
|
return async (req, res, next) => {
|
|
@@ -2770,7 +2853,7 @@ var RESERVED_SQL_IDENTIFIERS = /* @__PURE__ */ new Set([
|
|
|
2770
2853
|
]);
|
|
2771
2854
|
var ArraySubFieldSchema = z2.object({
|
|
2772
2855
|
name: z2.string().regex(/^[a-z][a-z0-9_]*$/, "Sub-field name must be lowercase with underscores"),
|
|
2773
|
-
type: z2.enum(["string", "text", "richtext", "number", "boolean", "datetime", "media"]),
|
|
2856
|
+
type: z2.enum(["string", "text", "richtext", "number", "boolean", "datetime", "media", "mixed"]),
|
|
2774
2857
|
required: z2.boolean().optional(),
|
|
2775
2858
|
subtype: z2.enum(["integer", "float"]).optional(),
|
|
2776
2859
|
allowedTypes: z2.array(z2.enum(["image", "video", "audio", "document"])).optional(),
|
|
@@ -3110,11 +3193,17 @@ async function syncManyToMany(entryId, tableName, field, targetIds) {
|
|
|
3110
3193
|
const placeholders = targetIds.map((_3, i2) => `($1, $${i2 + 2})`).join(", ");
|
|
3111
3194
|
await pool_default.query(`INSERT INTO ${quoteIdentifier(jt)} (source_id, target_id) VALUES ${placeholders} ON CONFLICT DO NOTHING`, [entryId, ...targetIds]);
|
|
3112
3195
|
}
|
|
3113
|
-
async function
|
|
3196
|
+
async function isContributorRole(roleId) {
|
|
3114
3197
|
if (!roleId)
|
|
3115
3198
|
return false;
|
|
3116
3199
|
const { rows } = await pool_default.query("SELECT name FROM plank_roles WHERE id = $1", [roleId]);
|
|
3117
|
-
return rows[0]?.name?.toLowerCase() === "
|
|
3200
|
+
return rows[0]?.name?.toLowerCase() === "contributor";
|
|
3201
|
+
}
|
|
3202
|
+
async function roleName(roleId) {
|
|
3203
|
+
if (!roleId)
|
|
3204
|
+
return "";
|
|
3205
|
+
const { rows } = await pool_default.query("SELECT name FROM plank_roles WHERE id = $1", [roleId]);
|
|
3206
|
+
return rows[0]?.name?.toLowerCase() ?? "";
|
|
3118
3207
|
}
|
|
3119
3208
|
var listEntries = async (req, res) => {
|
|
3120
3209
|
const ct = await findContentTypeBySlug(req.params.slug);
|
|
@@ -3124,7 +3213,6 @@ var listEntries = async (req, res) => {
|
|
|
3124
3213
|
}
|
|
3125
3214
|
assertSafeIdentifier(ct.tableName);
|
|
3126
3215
|
const quotedTableName = quoteIdentifier(ct.tableName);
|
|
3127
|
-
const isUser = await isUserRole(req.user?.roleId);
|
|
3128
3216
|
const page = Math.max(1, parseInt(String(req.query.page ?? 1)));
|
|
3129
3217
|
const limit = Math.min(100, Math.max(1, parseInt(String(req.query.limit ?? 20))));
|
|
3130
3218
|
const offset = (page - 1) * limit;
|
|
@@ -3135,19 +3223,15 @@ var listEntries = async (req, res) => {
|
|
|
3135
3223
|
const quotedSortField = quoteIdentifier(sortField);
|
|
3136
3224
|
const locale = req.query.locale ? String(req.query.locale) : void 0;
|
|
3137
3225
|
const fallbacks = req.query.fallback ? String(req.query.fallback).split(",") : [];
|
|
3138
|
-
const ownCollectionOnly = isUser && ct.kind === "collection";
|
|
3139
|
-
const whereClause = ownCollectionOnly ? "WHERE e.created_by = $3" : "";
|
|
3140
|
-
const countWhereClause = ownCollectionOnly ? "WHERE created_by = $1" : "";
|
|
3141
|
-
const listValues = ownCollectionOnly ? [limit, offset, req.user?.id ?? null] : [limit, offset];
|
|
3142
|
-
const countValues = ownCollectionOnly ? [req.user?.id ?? null] : [];
|
|
3143
3226
|
const [{ rows }, { rows: countRows }] = await Promise.all([
|
|
3144
|
-
pool_default.query(`SELECT e.*, u.first_name AS _author_first_name, u.last_name AS _author_last_name, u.avatar_url AS _author_avatar_url
|
|
3227
|
+
pool_default.query(`SELECT e.*, u.first_name AS _author_first_name, u.last_name AS _author_last_name, u.avatar_url AS _author_avatar_url,
|
|
3228
|
+
ed.first_name AS _editor_first_name, ed.last_name AS _editor_last_name, ed.avatar_url AS _editor_avatar_url
|
|
3145
3229
|
FROM ${quotedTableName} e
|
|
3146
3230
|
LEFT JOIN plank_users u ON u.id = e.created_by
|
|
3147
|
-
|
|
3231
|
+
LEFT JOIN plank_users ed ON ed.id = e.editor_id
|
|
3148
3232
|
ORDER BY e.${quotedSortField} ${sortDir}
|
|
3149
|
-
LIMIT $1 OFFSET $2`,
|
|
3150
|
-
pool_default.query(`SELECT COUNT(*) as count FROM ${quotedTableName}
|
|
3233
|
+
LIMIT $1 OFFSET $2`, [limit, offset]),
|
|
3234
|
+
pool_default.query(`SELECT COUNT(*) as count FROM ${quotedTableName}`)
|
|
3151
3235
|
]);
|
|
3152
3236
|
const provider = await getProvider();
|
|
3153
3237
|
function entryMatchesLocale(row, locale2) {
|
|
@@ -3155,7 +3239,7 @@ var listEntries = async (req, res) => {
|
|
|
3155
3239
|
return true;
|
|
3156
3240
|
const localized = row.localized && typeof row.localized === "object" ? row.localized : {};
|
|
3157
3241
|
const locales = Object.keys(localized).filter((k3) => !k3.startsWith("_"));
|
|
3158
|
-
const meta = localized._meta
|
|
3242
|
+
const meta = localized._meta ?? {};
|
|
3159
3243
|
const enabled = meta.enabled ?? locales.length > 0;
|
|
3160
3244
|
const primary = meta.primary;
|
|
3161
3245
|
if (enabled) {
|
|
@@ -3171,6 +3255,10 @@ var listEntries = async (req, res) => {
|
|
|
3171
3255
|
if (key && !key.startsWith("http")) {
|
|
3172
3256
|
resolved._author_avatar_url = await provider.getUrl(key);
|
|
3173
3257
|
}
|
|
3258
|
+
const editorKey = resolved._editor_avatar_url;
|
|
3259
|
+
if (editorKey && !editorKey.startsWith("http")) {
|
|
3260
|
+
resolved._editor_avatar_url = await provider.getUrl(editorKey);
|
|
3261
|
+
}
|
|
3174
3262
|
return normalizeNavigationFields({ ...resolved, ...mmIds }, ct.fields);
|
|
3175
3263
|
}));
|
|
3176
3264
|
let total = parseInt(countRows[0].count);
|
|
@@ -3179,7 +3267,7 @@ var listEntries = async (req, res) => {
|
|
|
3179
3267
|
const { rows: allRows } = await pool_default.query(`SELECT localized FROM ${quotedTableName}`);
|
|
3180
3268
|
const matching = allRows.filter((r2) => entryMatchesLocale(r2, locale));
|
|
3181
3269
|
total = matching.length;
|
|
3182
|
-
} catch
|
|
3270
|
+
} catch {
|
|
3183
3271
|
}
|
|
3184
3272
|
}
|
|
3185
3273
|
res.json({ data, total, page, limit });
|
|
@@ -3204,16 +3292,16 @@ var getEntry = async (req, res) => {
|
|
|
3204
3292
|
}
|
|
3205
3293
|
assertSafeIdentifier(ct.tableName);
|
|
3206
3294
|
const quotedTableName = quoteIdentifier(ct.tableName);
|
|
3207
|
-
const
|
|
3208
|
-
|
|
3295
|
+
const { rows } = await pool_default.query(`SELECT e.*, u.first_name AS _author_first_name, u.last_name AS _author_last_name, u.avatar_url AS _author_avatar_url,
|
|
3296
|
+
ed.first_name AS _editor_first_name, ed.last_name AS _editor_last_name, ed.avatar_url AS _editor_avatar_url
|
|
3297
|
+
FROM ${quotedTableName} e
|
|
3298
|
+
LEFT JOIN plank_users u ON u.id = e.created_by
|
|
3299
|
+
LEFT JOIN plank_users ed ON ed.id = e.editor_id
|
|
3300
|
+
WHERE e.id = $1`, [req.params.id]);
|
|
3209
3301
|
if (!rows[0]) {
|
|
3210
3302
|
res.status(404).json({ error: "Entry not found" });
|
|
3211
3303
|
return;
|
|
3212
3304
|
}
|
|
3213
|
-
if (isUser && ct.kind === "collection" && rows[0].created_by !== req.user?.id) {
|
|
3214
|
-
res.status(403).json({ error: "Forbidden" });
|
|
3215
|
-
return;
|
|
3216
|
-
}
|
|
3217
3305
|
const locale = req.query.locale ? String(req.query.locale) : void 0;
|
|
3218
3306
|
const fallbacks = req.query.fallback ? String(req.query.fallback).split(",") : [];
|
|
3219
3307
|
const mmIds = await loadManyToManyIds(req.params.id, ct.tableName, ct.fields);
|
|
@@ -3222,6 +3310,9 @@ var getEntry = async (req, res) => {
|
|
|
3222
3310
|
const key = resolved._author_avatar_url;
|
|
3223
3311
|
if (key && !key.startsWith("http"))
|
|
3224
3312
|
resolved._author_avatar_url = await provider.getUrl(key);
|
|
3313
|
+
const editorKey = resolved._editor_avatar_url;
|
|
3314
|
+
if (editorKey && !editorKey.startsWith("http"))
|
|
3315
|
+
resolved._editor_avatar_url = await provider.getUrl(editorKey);
|
|
3225
3316
|
res.json(normalizeNavigationFields({ ...resolved, ...mmIds }, ct.fields));
|
|
3226
3317
|
};
|
|
3227
3318
|
var createEntry = async (req, res) => {
|
|
@@ -3233,9 +3324,9 @@ var createEntry = async (req, res) => {
|
|
|
3233
3324
|
validate(ct, req.body);
|
|
3234
3325
|
assertSafeIdentifier(ct.tableName);
|
|
3235
3326
|
const quotedTableName = quoteIdentifier(ct.tableName);
|
|
3236
|
-
const
|
|
3237
|
-
if (
|
|
3238
|
-
res.status(403).json({ error: "Single types are read-only for
|
|
3327
|
+
const isContributor = await isContributorRole(req.user?.roleId);
|
|
3328
|
+
if (isContributor && ct.kind === "single") {
|
|
3329
|
+
res.status(403).json({ error: "Single types are read-only for Contributor role" });
|
|
3239
3330
|
return;
|
|
3240
3331
|
}
|
|
3241
3332
|
const mmFields = ct.fields.filter((f2) => f2.type === "relation" && (f2.relationType ?? "many-to-one") === "many-to-many" && req.body[f2.name] !== void 0);
|
|
@@ -3343,13 +3434,16 @@ var updateEntry = async (req, res) => {
|
|
|
3343
3434
|
validate(ct, req.body);
|
|
3344
3435
|
assertSafeIdentifier(ct.tableName);
|
|
3345
3436
|
const quotedTableName = quoteIdentifier(ct.tableName);
|
|
3346
|
-
const
|
|
3347
|
-
|
|
3348
|
-
|
|
3437
|
+
const editorialMode = req.appModes?.editorial ?? false;
|
|
3438
|
+
const currentRole = await roleName(req.user?.roleId);
|
|
3439
|
+
const isContributor = currentRole === "contributor";
|
|
3440
|
+
const isEditor = currentRole === "editor";
|
|
3441
|
+
if (isContributor && ct.kind === "single") {
|
|
3442
|
+
res.status(403).json({ error: "Single types are read-only for Contributor role" });
|
|
3349
3443
|
return;
|
|
3350
3444
|
}
|
|
3351
|
-
if (
|
|
3352
|
-
const { rows: authorRows } = await pool_default.query(`SELECT created_by FROM ${quotedTableName} WHERE id = $1`, [req.params.id]);
|
|
3445
|
+
if ((isContributor || isEditor) && ct.kind === "collection") {
|
|
3446
|
+
const { rows: authorRows } = await pool_default.query(`SELECT created_by, status FROM ${quotedTableName} WHERE id = $1`, [req.params.id]);
|
|
3353
3447
|
if (!authorRows[0]) {
|
|
3354
3448
|
res.status(404).json({ error: "Entry not found" });
|
|
3355
3449
|
return;
|
|
@@ -3358,6 +3452,10 @@ var updateEntry = async (req, res) => {
|
|
|
3358
3452
|
res.status(403).json({ error: "Forbidden" });
|
|
3359
3453
|
return;
|
|
3360
3454
|
}
|
|
3455
|
+
if (editorialMode && isContributor && authorRows[0].status === "in_review") {
|
|
3456
|
+
res.status(403).json({ error: "Entry is currently in review and locked for contributor edits" });
|
|
3457
|
+
return;
|
|
3458
|
+
}
|
|
3361
3459
|
}
|
|
3362
3460
|
const mmFields = ct.fields.filter((f2) => f2.type === "relation" && (f2.relationType ?? "many-to-one") === "many-to-many" && req.body[f2.name] !== void 0);
|
|
3363
3461
|
const fields = ct.fields.filter((f2) => req.body[f2.name] !== void 0 && !isVirtualRelation(f2));
|
|
@@ -3407,9 +3505,9 @@ function buildSnapshotExpr(tableName) {
|
|
|
3407
3505
|
return `(SELECT ${strip} FROM ${quoteIdentifier(tableName)} t WHERE t.id = $1)`;
|
|
3408
3506
|
}
|
|
3409
3507
|
var patchEntryStatus = async (req, res) => {
|
|
3410
|
-
const { status, scheduled_for } = req.body;
|
|
3411
|
-
if (status !== "draft" && status !== "published" && status !== "scheduled") {
|
|
3412
|
-
res.status(400).json({ error: "status must be draft, published, or
|
|
3508
|
+
const { status, scheduled_for, editor_id, review_locked_by_editor, review_rejected } = req.body;
|
|
3509
|
+
if (status !== "draft" && status !== "published" && status !== "scheduled" && status !== "pending" && status !== "in_review") {
|
|
3510
|
+
res.status(400).json({ error: "status must be draft, published, scheduled, pending, or in_review" });
|
|
3413
3511
|
return;
|
|
3414
3512
|
}
|
|
3415
3513
|
if (status === "scheduled") {
|
|
@@ -3429,13 +3527,17 @@ var patchEntryStatus = async (req, res) => {
|
|
|
3429
3527
|
}
|
|
3430
3528
|
assertSafeIdentifier(ct.tableName);
|
|
3431
3529
|
const quotedTableName = quoteIdentifier(ct.tableName);
|
|
3432
|
-
const
|
|
3433
|
-
|
|
3434
|
-
|
|
3530
|
+
const editorialMode = req.appModes?.editorial ?? false;
|
|
3531
|
+
const currentRole = await roleName(req.user?.roleId);
|
|
3532
|
+
const isContributor = currentRole === "contributor";
|
|
3533
|
+
const isAdminRole = currentRole === "admin" || currentRole === "super admin";
|
|
3534
|
+
const isEditorRole = currentRole === "editor";
|
|
3535
|
+
if (isContributor && ct.kind === "single") {
|
|
3536
|
+
res.status(403).json({ error: "Single types are read-only for Contributor role" });
|
|
3435
3537
|
return;
|
|
3436
3538
|
}
|
|
3437
|
-
if (
|
|
3438
|
-
const { rows: authorRows } = await pool_default.query(`SELECT created_by FROM ${quotedTableName} WHERE id = $1`, [req.params.id]);
|
|
3539
|
+
if (isContributor && ct.kind === "collection") {
|
|
3540
|
+
const { rows: authorRows } = await pool_default.query(`SELECT created_by, review_locked_by_editor FROM ${quotedTableName} WHERE id = $1`, [req.params.id]);
|
|
3439
3541
|
if (!authorRows[0]) {
|
|
3440
3542
|
res.status(404).json({ error: "Entry not found" });
|
|
3441
3543
|
return;
|
|
@@ -3444,6 +3546,18 @@ var patchEntryStatus = async (req, res) => {
|
|
|
3444
3546
|
res.status(403).json({ error: "Forbidden" });
|
|
3445
3547
|
return;
|
|
3446
3548
|
}
|
|
3549
|
+
if (editorialMode && authorRows[0].review_locked_by_editor && status !== "pending") {
|
|
3550
|
+
res.status(403).json({ error: "Entry is currently locked for contributor edits" });
|
|
3551
|
+
return;
|
|
3552
|
+
}
|
|
3553
|
+
}
|
|
3554
|
+
if (editorialMode && isContributor && status === "published") {
|
|
3555
|
+
res.status(403).json({ error: "Contributors cannot publish in editorial mode" });
|
|
3556
|
+
return;
|
|
3557
|
+
}
|
|
3558
|
+
if (editorialMode && isContributor && status === "scheduled") {
|
|
3559
|
+
res.status(403).json({ error: "Contributors cannot schedule in editorial mode" });
|
|
3560
|
+
return;
|
|
3447
3561
|
}
|
|
3448
3562
|
let sql;
|
|
3449
3563
|
let values;
|
|
@@ -3454,6 +3568,7 @@ var patchEntryStatus = async (req, res) => {
|
|
|
3454
3568
|
published_data = ${buildSnapshotExpr(ct.tableName)},
|
|
3455
3569
|
published_at = NOW(),
|
|
3456
3570
|
scheduled_for = NULL,
|
|
3571
|
+
review_rejected = FALSE,
|
|
3457
3572
|
updated_at = NOW()
|
|
3458
3573
|
WHERE id = $1
|
|
3459
3574
|
RETURNING *
|
|
@@ -3464,11 +3579,66 @@ var patchEntryStatus = async (req, res) => {
|
|
|
3464
3579
|
UPDATE ${quotedTableName} SET
|
|
3465
3580
|
status = 'scheduled',
|
|
3466
3581
|
scheduled_for = $2,
|
|
3582
|
+
review_rejected = FALSE,
|
|
3467
3583
|
updated_at = NOW()
|
|
3468
3584
|
WHERE id = $1
|
|
3469
3585
|
RETURNING *
|
|
3470
3586
|
`;
|
|
3471
3587
|
values = [req.params.id, scheduled_for];
|
|
3588
|
+
} else if (status === "pending") {
|
|
3589
|
+
sql = `
|
|
3590
|
+
UPDATE ${quotedTableName} SET
|
|
3591
|
+
status = 'pending',
|
|
3592
|
+
review_rejected = COALESCE($2, FALSE),
|
|
3593
|
+
review_locked_by_editor = FALSE,
|
|
3594
|
+
updated_at = NOW()
|
|
3595
|
+
WHERE id = $1
|
|
3596
|
+
RETURNING *
|
|
3597
|
+
`;
|
|
3598
|
+
values = [req.params.id, typeof review_rejected === "boolean" ? review_rejected : false];
|
|
3599
|
+
} else if (status === "in_review") {
|
|
3600
|
+
if (!editorialMode) {
|
|
3601
|
+
res.status(403).json({ error: "In review status requires editorial mode" });
|
|
3602
|
+
return;
|
|
3603
|
+
}
|
|
3604
|
+
if (!isAdminRole && !isEditorRole) {
|
|
3605
|
+
res.status(403).json({ error: "Forbidden" });
|
|
3606
|
+
return;
|
|
3607
|
+
}
|
|
3608
|
+
const requestedEditorId = typeof editor_id === "string" && editor_id.trim().length > 0 ? editor_id : isEditorRole ? req.user?.id ?? null : null;
|
|
3609
|
+
if (isEditorRole && requestedEditorId && requestedEditorId !== req.user?.id) {
|
|
3610
|
+
res.status(403).json({ error: "Editors can only assign themselves" });
|
|
3611
|
+
return;
|
|
3612
|
+
}
|
|
3613
|
+
if (requestedEditorId) {
|
|
3614
|
+
const { rows: editorRows } = await pool_default.query(`SELECT r.name as role_name
|
|
3615
|
+
FROM plank_users u
|
|
3616
|
+
JOIN plank_roles r ON r.id = u.role_id
|
|
3617
|
+
WHERE u.id = $1`, [requestedEditorId]);
|
|
3618
|
+
const targetRole = editorRows[0]?.role_name?.toLowerCase();
|
|
3619
|
+
if (isAdminRole) {
|
|
3620
|
+
if (requestedEditorId !== req.user?.id && targetRole !== "editor") {
|
|
3621
|
+
res.status(403).json({ error: "Admins can assign only themselves or Editors" });
|
|
3622
|
+
return;
|
|
3623
|
+
}
|
|
3624
|
+
} else if (targetRole !== "editor") {
|
|
3625
|
+
res.status(403).json({ error: "Invalid editor assignee" });
|
|
3626
|
+
return;
|
|
3627
|
+
}
|
|
3628
|
+
}
|
|
3629
|
+
const nextEditorId = requestedEditorId ?? (isEditorRole ? req.user?.id ?? null : null);
|
|
3630
|
+
const lock = typeof review_locked_by_editor === "boolean" ? review_locked_by_editor : false;
|
|
3631
|
+
sql = `
|
|
3632
|
+
UPDATE ${quotedTableName} SET
|
|
3633
|
+
status = 'in_review',
|
|
3634
|
+
editor_id = COALESCE($2, editor_id),
|
|
3635
|
+
review_locked_by_editor = $3,
|
|
3636
|
+
review_rejected = FALSE,
|
|
3637
|
+
updated_at = NOW()
|
|
3638
|
+
WHERE id = $1
|
|
3639
|
+
RETURNING *
|
|
3640
|
+
`;
|
|
3641
|
+
values = [req.params.id, nextEditorId, lock];
|
|
3472
3642
|
} else {
|
|
3473
3643
|
sql = `
|
|
3474
3644
|
UPDATE ${quotedTableName} SET
|
|
@@ -3476,6 +3646,8 @@ var patchEntryStatus = async (req, res) => {
|
|
|
3476
3646
|
published_data = NULL,
|
|
3477
3647
|
published_at = NULL,
|
|
3478
3648
|
scheduled_for = NULL,
|
|
3649
|
+
review_rejected = FALSE,
|
|
3650
|
+
review_locked_by_editor = FALSE,
|
|
3479
3651
|
updated_at = NOW()
|
|
3480
3652
|
WHERE id = $1
|
|
3481
3653
|
RETURNING *
|
|
@@ -3500,12 +3672,14 @@ var deleteEntry = async (req, res) => {
|
|
|
3500
3672
|
}
|
|
3501
3673
|
assertSafeIdentifier(ct.tableName);
|
|
3502
3674
|
const quotedTableName = quoteIdentifier(ct.tableName);
|
|
3503
|
-
const
|
|
3504
|
-
|
|
3505
|
-
|
|
3675
|
+
const currentRole = await roleName(req.user?.roleId);
|
|
3676
|
+
const isContributor = currentRole === "contributor";
|
|
3677
|
+
const isEditor = currentRole === "editor";
|
|
3678
|
+
if (isContributor && ct.kind === "single") {
|
|
3679
|
+
res.status(403).json({ error: "Single types are read-only for Contributor role" });
|
|
3506
3680
|
return;
|
|
3507
3681
|
}
|
|
3508
|
-
if (
|
|
3682
|
+
if ((isContributor || isEditor) && ct.kind === "collection") {
|
|
3509
3683
|
const { rows: authorRows } = await pool_default.query(`SELECT created_by FROM ${quotedTableName} WHERE id = $1`, [req.params.id]);
|
|
3510
3684
|
if (!authorRows[0]) {
|
|
3511
3685
|
res.status(404).json({ error: "Entry not found" });
|
|
@@ -3534,13 +3708,15 @@ import { z as z4, flattenError as flattenError4 } from "zod";
|
|
|
3534
3708
|
var CreateUserSchema = z4.object({
|
|
3535
3709
|
email: z4.email(),
|
|
3536
3710
|
password: z4.string().min(8),
|
|
3537
|
-
roleId: z4.string().min(1)
|
|
3711
|
+
roleId: z4.string().min(1),
|
|
3712
|
+
enabled: z4.boolean().optional()
|
|
3538
3713
|
});
|
|
3539
3714
|
var UpdateUserSchema = z4.object({
|
|
3540
3715
|
email: z4.email().optional(),
|
|
3541
3716
|
roleId: z4.string().min(1).optional(),
|
|
3542
3717
|
firstName: z4.string().max(100).nullable().optional(),
|
|
3543
|
-
lastName: z4.string().max(100).nullable().optional()
|
|
3718
|
+
lastName: z4.string().max(100).nullable().optional(),
|
|
3719
|
+
enabled: z4.boolean().optional()
|
|
3544
3720
|
});
|
|
3545
3721
|
var ChangePasswordSchema = z4.object({
|
|
3546
3722
|
currentPassword: z4.string().min(1),
|
|
@@ -3594,7 +3770,7 @@ async function roleNameById(roleId) {
|
|
|
3594
3770
|
return rows[0]?.name ?? null;
|
|
3595
3771
|
}
|
|
3596
3772
|
async function listUsers(_req, res) {
|
|
3597
|
-
const { rows } = await pool_default.query(`SELECT u.id, u.email, u.role_id, r.name as role_name, u.first_name, u.last_name, u.created_at
|
|
3773
|
+
const { rows } = await pool_default.query(`SELECT u.id, u.email, u.role_id, r.name as role_name, u.first_name, u.last_name, u.enabled, u.created_at
|
|
3598
3774
|
FROM plank_users u
|
|
3599
3775
|
JOIN plank_roles r ON r.id = u.role_id
|
|
3600
3776
|
ORDER BY u.created_at DESC`);
|
|
@@ -3602,7 +3778,7 @@ async function listUsers(_req, res) {
|
|
|
3602
3778
|
}
|
|
3603
3779
|
async function getMe(req, res) {
|
|
3604
3780
|
const { rows } = await pool_default.query(`SELECT u.id, u.email, u.role_id, u.first_name, u.last_name, u.avatar_url,
|
|
3605
|
-
u.job_title, u.organization, u.country, u.two_factor_enabled, u.created_at,
|
|
3781
|
+
u.job_title, u.organization, u.country, u.two_factor_enabled, u.enabled, u.created_at,
|
|
3606
3782
|
r.name AS role_name, r.permissions
|
|
3607
3783
|
FROM plank_users u
|
|
3608
3784
|
JOIN plank_roles r ON r.id = u.role_id
|
|
@@ -3612,11 +3788,14 @@ async function getMe(req, res) {
|
|
|
3612
3788
|
return;
|
|
3613
3789
|
}
|
|
3614
3790
|
const resolved = await resolveAvatarUrl(rows[0]);
|
|
3791
|
+
const modes = req.appModes ?? await resolveAppModes();
|
|
3615
3792
|
res.json({
|
|
3616
3793
|
...resolved,
|
|
3617
3794
|
role: rows[0].role_name,
|
|
3618
3795
|
permissions: rows[0].permissions,
|
|
3619
|
-
|
|
3796
|
+
enabled: rows[0].enabled ?? true,
|
|
3797
|
+
two_factor_enabled: rows[0].two_factor_enabled,
|
|
3798
|
+
modes
|
|
3620
3799
|
});
|
|
3621
3800
|
}
|
|
3622
3801
|
async function getTwoFactorStatus(req, res) {
|
|
@@ -3831,17 +4010,20 @@ async function createUser(req, res) {
|
|
|
3831
4010
|
res.status(400).json({ errors: flattenError4(parsed.error, (i2) => i2.message) });
|
|
3832
4011
|
return;
|
|
3833
4012
|
}
|
|
3834
|
-
const { email, password, roleId } = parsed.data;
|
|
4013
|
+
const { email, password, roleId, enabled } = parsed.data;
|
|
3835
4014
|
const requesterRoleName = await roleNameById(req.user.roleId);
|
|
3836
4015
|
const targetRoleName = await roleNameById(roleId);
|
|
3837
4016
|
if (targetRoleName === "Super Admin" && requesterRoleName !== "Super Admin") {
|
|
3838
4017
|
res.status(403).json({ error: "Only Super Admin can assign Super Admin role" });
|
|
3839
4018
|
return;
|
|
3840
4019
|
}
|
|
4020
|
+
const editorialMode = req.appModes?.editorial ?? false;
|
|
4021
|
+
const isEditorialExclusiveRole = ["Editor", "Viewer"].includes(targetRoleName ?? "");
|
|
4022
|
+
const nextEnabled = editorialMode || !isEditorialExclusiveRole ? enabled ?? true : false;
|
|
3841
4023
|
const hashed = await bcrypt2.hash(password, 12);
|
|
3842
4024
|
const id = createId();
|
|
3843
|
-
await pool_default.query("INSERT INTO plank_users (id, email, password, role_id) VALUES ($1, $2, $3, $4)", [id, email, hashed, roleId]);
|
|
3844
|
-
res.status(201).json({ id, email, roleId });
|
|
4025
|
+
await pool_default.query("INSERT INTO plank_users (id, email, password, role_id, enabled) VALUES ($1, $2, $3, $4, $5)", [id, email, hashed, roleId, nextEnabled]);
|
|
4026
|
+
res.status(201).json({ id, email, roleId, enabled: nextEnabled });
|
|
3845
4027
|
}
|
|
3846
4028
|
async function updateUser(req, res) {
|
|
3847
4029
|
const parsed = UpdateUserSchema.safeParse(req.body);
|
|
@@ -3849,7 +4031,7 @@ async function updateUser(req, res) {
|
|
|
3849
4031
|
res.status(400).json({ errors: flattenError4(parsed.error, (i2) => i2.message) });
|
|
3850
4032
|
return;
|
|
3851
4033
|
}
|
|
3852
|
-
const { email, roleId, firstName, lastName } = parsed.data;
|
|
4034
|
+
const { email, roleId, firstName, lastName, enabled } = parsed.data;
|
|
3853
4035
|
const requesterRoleName = await roleNameById(req.user.roleId);
|
|
3854
4036
|
const { rows: targetRows } = await pool_default.query("SELECT role_id FROM plank_users WHERE id = $1", [req.params.id]);
|
|
3855
4037
|
if (!targetRows[0]) {
|
|
@@ -3861,20 +4043,30 @@ async function updateUser(req, res) {
|
|
|
3861
4043
|
res.status(403).json({ error: "Only Super Admin can edit Super Admin users" });
|
|
3862
4044
|
return;
|
|
3863
4045
|
}
|
|
4046
|
+
const editorialMode = req.appModes?.editorial ?? false;
|
|
4047
|
+
let resolvedEnabled = enabled;
|
|
3864
4048
|
if (roleId) {
|
|
3865
4049
|
const nextRoleName = await roleNameById(roleId);
|
|
3866
4050
|
if (nextRoleName === "Super Admin" && requesterRoleName !== "Super Admin") {
|
|
3867
4051
|
res.status(403).json({ error: "Only Super Admin can assign Super Admin role" });
|
|
3868
4052
|
return;
|
|
3869
4053
|
}
|
|
4054
|
+
if (!editorialMode && ["Editor", "Viewer"].includes(nextRoleName ?? "")) {
|
|
4055
|
+
resolvedEnabled = false;
|
|
4056
|
+
}
|
|
3870
4057
|
}
|
|
3871
4058
|
const { rows } = await pool_default.query(`UPDATE plank_users
|
|
3872
4059
|
SET email = COALESCE($1, email),
|
|
3873
4060
|
role_id = COALESCE($2, role_id),
|
|
3874
4061
|
first_name = COALESCE($3, first_name),
|
|
3875
|
-
last_name = COALESCE($4, last_name)
|
|
3876
|
-
|
|
3877
|
-
|
|
4062
|
+
last_name = COALESCE($4, last_name),
|
|
4063
|
+
enabled = COALESCE($5, enabled),
|
|
4064
|
+
session_version = CASE
|
|
4065
|
+
WHEN $5 IS NOT NULL AND $5 = FALSE AND enabled = TRUE THEN session_version + 1
|
|
4066
|
+
ELSE session_version
|
|
4067
|
+
END
|
|
4068
|
+
WHERE id = $6
|
|
4069
|
+
RETURNING id, email, role_id, first_name, last_name, enabled, created_at`, [email ?? null, roleId ?? null, firstName ?? null, lastName ?? null, resolvedEnabled ?? null, req.params.id]);
|
|
3878
4070
|
if (!rows[0]) {
|
|
3879
4071
|
res.status(404).json({ error: "User not found" });
|
|
3880
4072
|
return;
|
|
@@ -4222,6 +4414,14 @@ async function getNamespaceSettings(req, res) {
|
|
|
4222
4414
|
const settings = await getSettings(namespace);
|
|
4223
4415
|
res.json(maskSettings(namespace, settings));
|
|
4224
4416
|
}
|
|
4417
|
+
async function getAppModes(req, res) {
|
|
4418
|
+
const modes = req.appModes ?? await resolveAppModes();
|
|
4419
|
+
res.json(modes);
|
|
4420
|
+
}
|
|
4421
|
+
async function getEditorialMode(_req, res) {
|
|
4422
|
+
const { editorial: enabled } = await resolveAppModes();
|
|
4423
|
+
res.json({ enabled });
|
|
4424
|
+
}
|
|
4225
4425
|
async function updateNamespaceSettings(req, res) {
|
|
4226
4426
|
const { namespace } = req.params;
|
|
4227
4427
|
const incoming = req.body;
|
|
@@ -4237,6 +4437,13 @@ async function updateNamespaceSettings(req, res) {
|
|
|
4237
4437
|
toSave[key] = value;
|
|
4238
4438
|
}
|
|
4239
4439
|
await setSettings(namespace, toSave);
|
|
4440
|
+
if (namespace === "general" && Object.prototype.hasOwnProperty.call(toSave, "editorial_mode") && String(toSave.editorial_mode).toLowerCase() === "false") {
|
|
4441
|
+
await pool_default.query(`UPDATE plank_users
|
|
4442
|
+
SET enabled = FALSE, session_version = session_version + 1
|
|
4443
|
+
WHERE role_id IN (
|
|
4444
|
+
SELECT id FROM plank_roles WHERE LOWER(name) IN ('editor', 'viewer')
|
|
4445
|
+
)`);
|
|
4446
|
+
}
|
|
4240
4447
|
const updated = await getSettings(namespace);
|
|
4241
4448
|
res.json(maskSettings(namespace, updated));
|
|
4242
4449
|
}
|
|
@@ -4244,6 +4451,7 @@ async function updateNamespaceSettings(req, res) {
|
|
|
4244
4451
|
// ../core/dist/routes/admin.js
|
|
4245
4452
|
var router2 = Router2();
|
|
4246
4453
|
router2.use(authenticate);
|
|
4454
|
+
router2.use(attachAppModes);
|
|
4247
4455
|
router2.get("/content-types", authorize("content-types:read"), listContentTypes);
|
|
4248
4456
|
router2.post("/content-types", authorize("content-types:write"), createContentType);
|
|
4249
4457
|
router2.get("/content-types/:slug", authorize("content-types:read"), getContentType);
|
|
@@ -4292,6 +4500,8 @@ router2.post("/media", authorize("media:write"), upload.array("files", 500), upl
|
|
|
4292
4500
|
router2.get("/media/:id/url", authorize("media:read"), getMediaUrl);
|
|
4293
4501
|
router2.patch("/media/:id", authorize("media:write"), updateMedia);
|
|
4294
4502
|
router2.delete("/media/:id", authorize("media:delete"), deleteMedia);
|
|
4503
|
+
router2.get("/modes", getAppModes);
|
|
4504
|
+
router2.get("/editorial-mode", getEditorialMode);
|
|
4295
4505
|
router2.get("/settings/:namespace", authorize("settings:overview:read"), getNamespaceSettings);
|
|
4296
4506
|
router2.put("/settings/:namespace", authorize("settings:overview:write"), updateNamespaceSettings);
|
|
4297
4507
|
router2.get("/webhooks", authorize("settings:webhooks:read"), listWebhooks);
|
|
@@ -4353,6 +4563,29 @@ function normalizeNavigationItems2(value) {
|
|
|
4353
4563
|
return normalized;
|
|
4354
4564
|
});
|
|
4355
4565
|
}
|
|
4566
|
+
function normalizeArrayItemsBySchema(value, field) {
|
|
4567
|
+
if (field.type !== "array" || !Array.isArray(value))
|
|
4568
|
+
return value;
|
|
4569
|
+
const subFields = field.arrayFields ?? [];
|
|
4570
|
+
if (subFields.length === 0)
|
|
4571
|
+
return value;
|
|
4572
|
+
return value.map((item) => {
|
|
4573
|
+
if (typeof item !== "object" || item === null || Array.isArray(item))
|
|
4574
|
+
return item;
|
|
4575
|
+
const raw = item;
|
|
4576
|
+
const normalized = {};
|
|
4577
|
+
for (const subField of subFields) {
|
|
4578
|
+
if (subField.name in raw)
|
|
4579
|
+
normalized[subField.name] = raw[subField.name];
|
|
4580
|
+
}
|
|
4581
|
+
for (const [key, val] of Object.entries(raw)) {
|
|
4582
|
+
if (key in normalized)
|
|
4583
|
+
continue;
|
|
4584
|
+
normalized[key] = val;
|
|
4585
|
+
}
|
|
4586
|
+
return normalized;
|
|
4587
|
+
});
|
|
4588
|
+
}
|
|
4356
4589
|
async function resolveMediaFields(entries, ct) {
|
|
4357
4590
|
const singleFields = ct.fields.filter((f2) => f2.type === "media").map((f2) => f2.name);
|
|
4358
4591
|
const galleryFields = ct.fields.filter((f2) => f2.type === "media-gallery").map((f2) => f2.name);
|
|
@@ -4465,14 +4698,19 @@ async function resolveAuthorAvatars(entries) {
|
|
|
4465
4698
|
if (author?.avatar_url && !author.avatar_url.startsWith("http")) {
|
|
4466
4699
|
author.avatar_url = await provider.getUrl(author.avatar_url);
|
|
4467
4700
|
}
|
|
4701
|
+
const editor = entry.editor;
|
|
4702
|
+
if (editor?.avatar_url && !editor.avatar_url.startsWith("http")) {
|
|
4703
|
+
editor.avatar_url = await provider.getUrl(editor.avatar_url);
|
|
4704
|
+
}
|
|
4468
4705
|
}));
|
|
4469
4706
|
}
|
|
4470
4707
|
function serializeEntry(row, ct, statusParam, locale, fallbacks = []) {
|
|
4471
|
-
const { published_data, _author_first_name, _author_last_name, _author_avatar_url, _author_job_title, _author_organization, _author_country, ...rest } = row;
|
|
4708
|
+
const { published_data, _author_first_name, _author_last_name, _author_avatar_url, _author_job_title, _author_organization, _author_country, _editor_first_name, _editor_last_name, _editor_avatar_url, _editor_job_title, _editor_organization, _editor_country, ...rest } = row;
|
|
4472
4709
|
const source = statusParam === "published" && published_data ? published_data : rest;
|
|
4473
4710
|
const effective = { ...source };
|
|
4474
4711
|
if (locale) {
|
|
4475
|
-
const
|
|
4712
|
+
const sourceObj = source;
|
|
4713
|
+
const localizedContainer = source && typeof source === "object" && sourceObj.localized && typeof sourceObj.localized === "object" ? sourceObj.localized : row.localized && typeof row.localized === "object" ? row.localized : {};
|
|
4476
4714
|
const localizableTypes = /* @__PURE__ */ new Set(["string", "text", "richtext", "uid"]);
|
|
4477
4715
|
for (const f2 of ct.fields) {
|
|
4478
4716
|
if (!localizableTypes.has(f2.type))
|
|
@@ -4496,7 +4734,15 @@ function serializeEntry(row, ct, statusParam, locale, fallbacks = []) {
|
|
|
4496
4734
|
for (const field of ct.fields) {
|
|
4497
4735
|
if (!(field.name in effective))
|
|
4498
4736
|
continue;
|
|
4499
|
-
|
|
4737
|
+
if (field.type === "navigation") {
|
|
4738
|
+
out[field.name] = normalizeNavigationItems2(effective[field.name]);
|
|
4739
|
+
continue;
|
|
4740
|
+
}
|
|
4741
|
+
if (field.type === "array") {
|
|
4742
|
+
out[field.name] = normalizeArrayItemsBySchema(effective[field.name], field);
|
|
4743
|
+
continue;
|
|
4744
|
+
}
|
|
4745
|
+
out[field.name] = effective[field.name];
|
|
4500
4746
|
}
|
|
4501
4747
|
out.status = row.status;
|
|
4502
4748
|
out.published_at = row.published_at ?? null;
|
|
@@ -4510,6 +4756,14 @@ function serializeEntry(row, ct, statusParam, locale, fallbacks = []) {
|
|
|
4510
4756
|
organization: _author_organization ?? null,
|
|
4511
4757
|
country: _author_country ?? null
|
|
4512
4758
|
} : null;
|
|
4759
|
+
out.editor = _editor_first_name || _editor_last_name ? {
|
|
4760
|
+
first_name: _editor_first_name ?? null,
|
|
4761
|
+
last_name: _editor_last_name ?? null,
|
|
4762
|
+
avatar_url: _editor_avatar_url ?? null,
|
|
4763
|
+
job_title: _editor_job_title ?? null,
|
|
4764
|
+
organization: _editor_organization ?? null,
|
|
4765
|
+
country: _editor_country ?? null
|
|
4766
|
+
} : null;
|
|
4513
4767
|
return out;
|
|
4514
4768
|
}
|
|
4515
4769
|
var listPublicEntries = async (req, res) => {
|
|
@@ -4525,9 +4779,11 @@ var listPublicEntries = async (req, res) => {
|
|
|
4525
4779
|
const fallbacks2 = req.query.fallback ? String(req.query.fallback).split(",") : [];
|
|
4526
4780
|
const statusClause = statusParam2 === "published" || statusParam2 === "draft" ? `WHERE e.status = $1` : "";
|
|
4527
4781
|
const values = statusClause ? [statusParam2] : [];
|
|
4528
|
-
const { rows: rows2 } = await pool_default.query(`SELECT e.*, u.first_name AS _author_first_name, u.last_name AS _author_last_name, u.avatar_url AS _author_avatar_url, u.job_title AS _author_job_title, u.organization AS _author_organization, u.country AS _author_country
|
|
4782
|
+
const { rows: rows2 } = await pool_default.query(`SELECT e.*, u.first_name AS _author_first_name, u.last_name AS _author_last_name, u.avatar_url AS _author_avatar_url, u.job_title AS _author_job_title, u.organization AS _author_organization, u.country AS _author_country,
|
|
4783
|
+
ed.first_name AS _editor_first_name, ed.last_name AS _editor_last_name, ed.avatar_url AS _editor_avatar_url, ed.job_title AS _editor_job_title, ed.organization AS _editor_organization, ed.country AS _editor_country
|
|
4529
4784
|
FROM ${ct.tableName} e
|
|
4530
4785
|
LEFT JOIN plank_users u ON u.id = e.created_by
|
|
4786
|
+
LEFT JOIN plank_users ed ON ed.id = e.editor_id
|
|
4531
4787
|
${statusClause} LIMIT 1`, values);
|
|
4532
4788
|
if (!rows2[0]) {
|
|
4533
4789
|
res.status(404).json({ error: "Not found" });
|
|
@@ -4573,9 +4829,11 @@ var listPublicEntries = async (req, res) => {
|
|
|
4573
4829
|
const limitParam = filterValues.length + 1;
|
|
4574
4830
|
const offsetParam = filterValues.length + 2;
|
|
4575
4831
|
const [{ rows }, { rows: countRows }] = await Promise.all([
|
|
4576
|
-
pool_default.query(`SELECT e.*, u.first_name AS _author_first_name, u.last_name AS _author_last_name, u.avatar_url AS _author_avatar_url, u.job_title AS _author_job_title, u.organization AS _author_organization, u.country AS _author_country
|
|
4832
|
+
pool_default.query(`SELECT e.*, u.first_name AS _author_first_name, u.last_name AS _author_last_name, u.avatar_url AS _author_avatar_url, u.job_title AS _author_job_title, u.organization AS _author_organization, u.country AS _author_country,
|
|
4833
|
+
ed.first_name AS _editor_first_name, ed.last_name AS _editor_last_name, ed.avatar_url AS _editor_avatar_url, ed.job_title AS _editor_job_title, ed.organization AS _editor_organization, ed.country AS _editor_country
|
|
4577
4834
|
FROM ${ct.tableName} e
|
|
4578
4835
|
LEFT JOIN plank_users u ON u.id = e.created_by
|
|
4836
|
+
LEFT JOIN plank_users ed ON ed.id = e.editor_id
|
|
4579
4837
|
${where} ORDER BY e.${sortField} ${sortDir} LIMIT $${limitParam} OFFSET $${offsetParam}`, [...filterValues, limit, offset]),
|
|
4580
4838
|
pool_default.query(`SELECT COUNT(*) as count FROM ${ct.tableName} e ${where}`, filterValues)
|
|
4581
4839
|
]);
|
|
@@ -4597,9 +4855,11 @@ var getPublicEntry = async (req, res) => {
|
|
|
4597
4855
|
const statusParam = String(req.query.status ?? "published");
|
|
4598
4856
|
const statusClause = statusParam === "published" || statusParam === "draft" ? ` AND e.status = $2` : "";
|
|
4599
4857
|
const values = statusClause ? [req.params.id, statusParam] : [req.params.id];
|
|
4600
|
-
const { rows } = await pool_default.query(`SELECT e.*, u.first_name AS _author_first_name, u.last_name AS _author_last_name, u.avatar_url AS _author_avatar_url, u.job_title AS _author_job_title, u.organization AS _author_organization, u.country AS _author_country
|
|
4858
|
+
const { rows } = await pool_default.query(`SELECT e.*, u.first_name AS _author_first_name, u.last_name AS _author_last_name, u.avatar_url AS _author_avatar_url, u.job_title AS _author_job_title, u.organization AS _author_organization, u.country AS _author_country,
|
|
4859
|
+
ed.first_name AS _editor_first_name, ed.last_name AS _editor_last_name, ed.avatar_url AS _editor_avatar_url, ed.job_title AS _editor_job_title, ed.organization AS _editor_organization, ed.country AS _editor_country
|
|
4601
4860
|
FROM ${ct.tableName} e
|
|
4602
4861
|
LEFT JOIN plank_users u ON u.id = e.created_by
|
|
4862
|
+
LEFT JOIN plank_users ed ON ed.id = e.editor_id
|
|
4603
4863
|
WHERE e.id = $1${statusClause}`, values);
|
|
4604
4864
|
if (!rows[0]) {
|
|
4605
4865
|
res.status(404).json({ error: "Not found" });
|