@plank-cms/plank 0.16.0 → 0.17.1
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-CryEl0yA.js +223 -0
- package/dist/admin/index.html +2 -2
- 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-PGIFNGM5.js → server-BCAMMFQR.js} +260 -65
- package/package.json +4 -4
- package/dist/admin/assets/index-BpfwJogs.css +0 -2
- package/dist/admin/assets/index-Cdi-vHcE.js +0 -223
|
@@ -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",
|
|
53
|
+
"media:read",
|
|
54
|
+
"media:write"
|
|
55
|
+
],
|
|
56
|
+
"Editor": [
|
|
57
|
+
"content-types:read",
|
|
58
|
+
"entries:read",
|
|
59
|
+
"entries:write",
|
|
60
|
+
"entries:delete",
|
|
52
61
|
"media:read",
|
|
53
62
|
"media:write"
|
|
63
|
+
],
|
|
64
|
+
"Viewer": [
|
|
65
|
+
"content-types:read",
|
|
66
|
+
"entries:read",
|
|
67
|
+
"media:read"
|
|
54
68
|
]
|
|
55
69
|
};
|
|
56
70
|
|
|
@@ -70,13 +84,17 @@ async function appliedMigrations(client) {
|
|
|
70
84
|
return new Set(rows.map((r2) => r2.filename));
|
|
71
85
|
}
|
|
72
86
|
async function seedDefaultRoles(client) {
|
|
73
|
-
|
|
74
|
-
if (parseInt(rows[0].count) > 0)
|
|
75
|
-
return;
|
|
87
|
+
let created = 0;
|
|
76
88
|
for (const [name, permissions] of Object.entries(DEFAULT_ROLE_PERMISSIONS)) {
|
|
77
|
-
await client.query(
|
|
89
|
+
const result = await client.query(`
|
|
90
|
+
INSERT INTO plank_roles (id, name, permissions)
|
|
91
|
+
VALUES ($1, $2, $3)
|
|
92
|
+
ON CONFLICT (name) DO NOTHING
|
|
93
|
+
`, [createId(), name, JSON.stringify(permissions)]);
|
|
94
|
+
created += result.rowCount ?? 0;
|
|
78
95
|
}
|
|
79
|
-
|
|
96
|
+
if (created > 0)
|
|
97
|
+
console.log(`[plank/db] Seeded missing default roles: ${created}.`);
|
|
80
98
|
}
|
|
81
99
|
async function migrate() {
|
|
82
100
|
const client = await pool_default.connect();
|
|
@@ -2469,7 +2487,7 @@ async function login(req, res) {
|
|
|
2469
2487
|
res.status(429).json({ error: "Too many login attempts. Try again in 15 minutes." });
|
|
2470
2488
|
return;
|
|
2471
2489
|
}
|
|
2472
|
-
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
|
|
2490
|
+
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
|
|
2473
2491
|
FROM plank_users
|
|
2474
2492
|
WHERE email = $1`, [email]);
|
|
2475
2493
|
const user = rows[0];
|
|
@@ -2477,6 +2495,10 @@ async function login(req, res) {
|
|
|
2477
2495
|
res.status(401).json({ error: "Invalid credentials" });
|
|
2478
2496
|
return;
|
|
2479
2497
|
}
|
|
2498
|
+
if (!user.enabled) {
|
|
2499
|
+
res.status(403).json({ error: "User is disabled" });
|
|
2500
|
+
return;
|
|
2501
|
+
}
|
|
2480
2502
|
await clearRateLimit("login", rateKey);
|
|
2481
2503
|
if (user.two_factor_enabled && user.two_factor_secret) {
|
|
2482
2504
|
const challengeToken = buildChallengeToken({
|
|
@@ -2520,9 +2542,13 @@ async function loginWithTwoFactor(req, res) {
|
|
|
2520
2542
|
res.status(429).json({ error: "Too many 2FA attempts. Try again in 15 minutes." });
|
|
2521
2543
|
return;
|
|
2522
2544
|
}
|
|
2523
|
-
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
|
|
2545
|
+
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
|
|
2524
2546
|
FROM plank_users WHERE id = $1`, [payload.sub]);
|
|
2525
2547
|
const user = rows[0];
|
|
2548
|
+
if (user && !user.enabled) {
|
|
2549
|
+
res.status(403).json({ error: "User is disabled" });
|
|
2550
|
+
return;
|
|
2551
|
+
}
|
|
2526
2552
|
if (!user || !user.two_factor_enabled || !user.two_factor_secret) {
|
|
2527
2553
|
res.status(401).json({ error: "2FA is not enabled for this account" });
|
|
2528
2554
|
return;
|
|
@@ -2602,8 +2628,13 @@ async function register(req, res) {
|
|
|
2602
2628
|
const { email, password } = parsed.data;
|
|
2603
2629
|
const hashed = await bcrypt.hash(password, 12);
|
|
2604
2630
|
const { rows: roleRows } = await pool_default.query("SELECT id, name FROM plank_roles WHERE name = $1", ["Super Admin"]);
|
|
2631
|
+
const superAdminRole = roleRows[0];
|
|
2632
|
+
if (!superAdminRole) {
|
|
2633
|
+
res.status(500).json({ error: "Super Admin role is not configured." });
|
|
2634
|
+
return;
|
|
2635
|
+
}
|
|
2605
2636
|
const id = createId();
|
|
2606
|
-
await pool_default.query("INSERT INTO plank_users (id, email, password, role_id) VALUES ($1, $2, $3, $4)", [id, email, hashed,
|
|
2637
|
+
await pool_default.query("INSERT INTO plank_users (id, email, password, role_id) VALUES ($1, $2, $3, $4)", [id, email, hashed, superAdminRole.id]);
|
|
2607
2638
|
res.status(201).json({ id, email });
|
|
2608
2639
|
}
|
|
2609
2640
|
|
|
@@ -2664,8 +2695,8 @@ async function authenticate(req, res, next) {
|
|
|
2664
2695
|
if (typeof payload.sv !== "number") {
|
|
2665
2696
|
return { ok: false };
|
|
2666
2697
|
}
|
|
2667
|
-
const { rows } = await pool_default.query("SELECT session_version FROM plank_users WHERE id = $1", [payload.sub]);
|
|
2668
|
-
if (!rows[0] || rows[0].session_version !== payload.sv) {
|
|
2698
|
+
const { rows } = await pool_default.query("SELECT session_version, enabled FROM plank_users WHERE id = $1", [payload.sub]);
|
|
2699
|
+
if (!rows[0] || !rows[0].enabled || rows[0].session_version !== payload.sv) {
|
|
2669
2700
|
return { ok: false };
|
|
2670
2701
|
}
|
|
2671
2702
|
return { ok: true, sessionVersion: rows[0].session_version };
|
|
@@ -2711,6 +2742,25 @@ async function authenticate(req, res, next) {
|
|
|
2711
2742
|
}
|
|
2712
2743
|
}
|
|
2713
2744
|
|
|
2745
|
+
// ../core/dist/lib/editorialMode.js
|
|
2746
|
+
async function isEditorialModeEnabled() {
|
|
2747
|
+
const raw = await getSetting("general", "editorial_mode");
|
|
2748
|
+
return String(raw ?? "false").toLowerCase() === "true";
|
|
2749
|
+
}
|
|
2750
|
+
|
|
2751
|
+
// ../core/dist/lib/appModes.js
|
|
2752
|
+
async function resolveAppModes() {
|
|
2753
|
+
return {
|
|
2754
|
+
editorial: await isEditorialModeEnabled()
|
|
2755
|
+
};
|
|
2756
|
+
}
|
|
2757
|
+
|
|
2758
|
+
// ../core/dist/middlewares/appModes.js
|
|
2759
|
+
async function attachAppModes(req, _res, next) {
|
|
2760
|
+
req.appModes = await resolveAppModes();
|
|
2761
|
+
next();
|
|
2762
|
+
}
|
|
2763
|
+
|
|
2714
2764
|
// ../core/dist/middlewares/authorize.js
|
|
2715
2765
|
function authorize(permission) {
|
|
2716
2766
|
return async (req, res, next) => {
|
|
@@ -3152,11 +3202,17 @@ async function syncManyToMany(entryId, tableName, field, targetIds) {
|
|
|
3152
3202
|
const placeholders = targetIds.map((_3, i2) => `($1, $${i2 + 2})`).join(", ");
|
|
3153
3203
|
await pool_default.query(`INSERT INTO ${quoteIdentifier(jt)} (source_id, target_id) VALUES ${placeholders} ON CONFLICT DO NOTHING`, [entryId, ...targetIds]);
|
|
3154
3204
|
}
|
|
3155
|
-
async function
|
|
3205
|
+
async function isContributorRole(roleId) {
|
|
3156
3206
|
if (!roleId)
|
|
3157
3207
|
return false;
|
|
3158
3208
|
const { rows } = await pool_default.query("SELECT name FROM plank_roles WHERE id = $1", [roleId]);
|
|
3159
|
-
return rows[0]?.name?.toLowerCase() === "
|
|
3209
|
+
return rows[0]?.name?.toLowerCase() === "contributor";
|
|
3210
|
+
}
|
|
3211
|
+
async function roleName(roleId) {
|
|
3212
|
+
if (!roleId)
|
|
3213
|
+
return "";
|
|
3214
|
+
const { rows } = await pool_default.query("SELECT name FROM plank_roles WHERE id = $1", [roleId]);
|
|
3215
|
+
return rows[0]?.name?.toLowerCase() ?? "";
|
|
3160
3216
|
}
|
|
3161
3217
|
var listEntries = async (req, res) => {
|
|
3162
3218
|
const ct = await findContentTypeBySlug(req.params.slug);
|
|
@@ -3166,7 +3222,6 @@ var listEntries = async (req, res) => {
|
|
|
3166
3222
|
}
|
|
3167
3223
|
assertSafeIdentifier(ct.tableName);
|
|
3168
3224
|
const quotedTableName = quoteIdentifier(ct.tableName);
|
|
3169
|
-
const isUser = await isUserRole(req.user?.roleId);
|
|
3170
3225
|
const page = Math.max(1, parseInt(String(req.query.page ?? 1)));
|
|
3171
3226
|
const limit = Math.min(100, Math.max(1, parseInt(String(req.query.limit ?? 20))));
|
|
3172
3227
|
const offset = (page - 1) * limit;
|
|
@@ -3177,19 +3232,15 @@ var listEntries = async (req, res) => {
|
|
|
3177
3232
|
const quotedSortField = quoteIdentifier(sortField);
|
|
3178
3233
|
const locale = req.query.locale ? String(req.query.locale) : void 0;
|
|
3179
3234
|
const fallbacks = req.query.fallback ? String(req.query.fallback).split(",") : [];
|
|
3180
|
-
const ownCollectionOnly = isUser && ct.kind === "collection";
|
|
3181
|
-
const whereClause = ownCollectionOnly ? "WHERE e.created_by = $3" : "";
|
|
3182
|
-
const countWhereClause = ownCollectionOnly ? "WHERE created_by = $1" : "";
|
|
3183
|
-
const listValues = ownCollectionOnly ? [limit, offset, req.user?.id ?? null] : [limit, offset];
|
|
3184
|
-
const countValues = ownCollectionOnly ? [req.user?.id ?? null] : [];
|
|
3185
3235
|
const [{ rows }, { rows: countRows }] = await Promise.all([
|
|
3186
|
-
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
|
|
3236
|
+
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,
|
|
3237
|
+
ed.first_name AS _editor_first_name, ed.last_name AS _editor_last_name, ed.avatar_url AS _editor_avatar_url
|
|
3187
3238
|
FROM ${quotedTableName} e
|
|
3188
3239
|
LEFT JOIN plank_users u ON u.id = e.created_by
|
|
3189
|
-
|
|
3240
|
+
LEFT JOIN plank_users ed ON ed.id = e.editor_id
|
|
3190
3241
|
ORDER BY e.${quotedSortField} ${sortDir}
|
|
3191
|
-
LIMIT $1 OFFSET $2`,
|
|
3192
|
-
pool_default.query(`SELECT COUNT(*) as count FROM ${quotedTableName}
|
|
3242
|
+
LIMIT $1 OFFSET $2`, [limit, offset]),
|
|
3243
|
+
pool_default.query(`SELECT COUNT(*) as count FROM ${quotedTableName}`)
|
|
3193
3244
|
]);
|
|
3194
3245
|
const provider = await getProvider();
|
|
3195
3246
|
function entryMatchesLocale(row, locale2) {
|
|
@@ -3213,6 +3264,10 @@ var listEntries = async (req, res) => {
|
|
|
3213
3264
|
if (key && !key.startsWith("http")) {
|
|
3214
3265
|
resolved._author_avatar_url = await provider.getUrl(key);
|
|
3215
3266
|
}
|
|
3267
|
+
const editorKey = resolved._editor_avatar_url;
|
|
3268
|
+
if (editorKey && !editorKey.startsWith("http")) {
|
|
3269
|
+
resolved._editor_avatar_url = await provider.getUrl(editorKey);
|
|
3270
|
+
}
|
|
3216
3271
|
return normalizeNavigationFields({ ...resolved, ...mmIds }, ct.fields);
|
|
3217
3272
|
}));
|
|
3218
3273
|
let total = parseInt(countRows[0].count);
|
|
@@ -3246,16 +3301,16 @@ var getEntry = async (req, res) => {
|
|
|
3246
3301
|
}
|
|
3247
3302
|
assertSafeIdentifier(ct.tableName);
|
|
3248
3303
|
const quotedTableName = quoteIdentifier(ct.tableName);
|
|
3249
|
-
const
|
|
3250
|
-
|
|
3304
|
+
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,
|
|
3305
|
+
ed.first_name AS _editor_first_name, ed.last_name AS _editor_last_name, ed.avatar_url AS _editor_avatar_url
|
|
3306
|
+
FROM ${quotedTableName} e
|
|
3307
|
+
LEFT JOIN plank_users u ON u.id = e.created_by
|
|
3308
|
+
LEFT JOIN plank_users ed ON ed.id = e.editor_id
|
|
3309
|
+
WHERE e.id = $1`, [req.params.id]);
|
|
3251
3310
|
if (!rows[0]) {
|
|
3252
3311
|
res.status(404).json({ error: "Entry not found" });
|
|
3253
3312
|
return;
|
|
3254
3313
|
}
|
|
3255
|
-
if (isUser && ct.kind === "collection" && rows[0].created_by !== req.user?.id) {
|
|
3256
|
-
res.status(403).json({ error: "Forbidden" });
|
|
3257
|
-
return;
|
|
3258
|
-
}
|
|
3259
3314
|
const locale = req.query.locale ? String(req.query.locale) : void 0;
|
|
3260
3315
|
const fallbacks = req.query.fallback ? String(req.query.fallback).split(",") : [];
|
|
3261
3316
|
const mmIds = await loadManyToManyIds(req.params.id, ct.tableName, ct.fields);
|
|
@@ -3264,6 +3319,9 @@ var getEntry = async (req, res) => {
|
|
|
3264
3319
|
const key = resolved._author_avatar_url;
|
|
3265
3320
|
if (key && !key.startsWith("http"))
|
|
3266
3321
|
resolved._author_avatar_url = await provider.getUrl(key);
|
|
3322
|
+
const editorKey = resolved._editor_avatar_url;
|
|
3323
|
+
if (editorKey && !editorKey.startsWith("http"))
|
|
3324
|
+
resolved._editor_avatar_url = await provider.getUrl(editorKey);
|
|
3267
3325
|
res.json(normalizeNavigationFields({ ...resolved, ...mmIds }, ct.fields));
|
|
3268
3326
|
};
|
|
3269
3327
|
var createEntry = async (req, res) => {
|
|
@@ -3275,9 +3333,9 @@ var createEntry = async (req, res) => {
|
|
|
3275
3333
|
validate(ct, req.body);
|
|
3276
3334
|
assertSafeIdentifier(ct.tableName);
|
|
3277
3335
|
const quotedTableName = quoteIdentifier(ct.tableName);
|
|
3278
|
-
const
|
|
3279
|
-
if (
|
|
3280
|
-
res.status(403).json({ error: "Single types are read-only for
|
|
3336
|
+
const isContributor = await isContributorRole(req.user?.roleId);
|
|
3337
|
+
if (isContributor && ct.kind === "single") {
|
|
3338
|
+
res.status(403).json({ error: "Single types are read-only for Contributor role" });
|
|
3281
3339
|
return;
|
|
3282
3340
|
}
|
|
3283
3341
|
const mmFields = ct.fields.filter((f2) => f2.type === "relation" && (f2.relationType ?? "many-to-one") === "many-to-many" && req.body[f2.name] !== void 0);
|
|
@@ -3385,13 +3443,16 @@ var updateEntry = async (req, res) => {
|
|
|
3385
3443
|
validate(ct, req.body);
|
|
3386
3444
|
assertSafeIdentifier(ct.tableName);
|
|
3387
3445
|
const quotedTableName = quoteIdentifier(ct.tableName);
|
|
3388
|
-
const
|
|
3389
|
-
|
|
3390
|
-
|
|
3446
|
+
const editorialMode = req.appModes?.editorial ?? false;
|
|
3447
|
+
const currentRole = await roleName(req.user?.roleId);
|
|
3448
|
+
const isContributor = currentRole === "contributor";
|
|
3449
|
+
const isEditor = currentRole === "editor";
|
|
3450
|
+
if (isContributor && ct.kind === "single") {
|
|
3451
|
+
res.status(403).json({ error: "Single types are read-only for Contributor role" });
|
|
3391
3452
|
return;
|
|
3392
3453
|
}
|
|
3393
|
-
if (
|
|
3394
|
-
const { rows: authorRows } = await pool_default.query(`SELECT created_by FROM ${quotedTableName} WHERE id = $1`, [req.params.id]);
|
|
3454
|
+
if ((isContributor || isEditor) && ct.kind === "collection") {
|
|
3455
|
+
const { rows: authorRows } = await pool_default.query(`SELECT created_by, status FROM ${quotedTableName} WHERE id = $1`, [req.params.id]);
|
|
3395
3456
|
if (!authorRows[0]) {
|
|
3396
3457
|
res.status(404).json({ error: "Entry not found" });
|
|
3397
3458
|
return;
|
|
@@ -3400,6 +3461,10 @@ var updateEntry = async (req, res) => {
|
|
|
3400
3461
|
res.status(403).json({ error: "Forbidden" });
|
|
3401
3462
|
return;
|
|
3402
3463
|
}
|
|
3464
|
+
if (editorialMode && isContributor && authorRows[0].status === "in_review") {
|
|
3465
|
+
res.status(403).json({ error: "Entry is currently in review and locked for contributor edits" });
|
|
3466
|
+
return;
|
|
3467
|
+
}
|
|
3403
3468
|
}
|
|
3404
3469
|
const mmFields = ct.fields.filter((f2) => f2.type === "relation" && (f2.relationType ?? "many-to-one") === "many-to-many" && req.body[f2.name] !== void 0);
|
|
3405
3470
|
const fields = ct.fields.filter((f2) => req.body[f2.name] !== void 0 && !isVirtualRelation(f2));
|
|
@@ -3449,9 +3514,9 @@ function buildSnapshotExpr(tableName) {
|
|
|
3449
3514
|
return `(SELECT ${strip} FROM ${quoteIdentifier(tableName)} t WHERE t.id = $1)`;
|
|
3450
3515
|
}
|
|
3451
3516
|
var patchEntryStatus = async (req, res) => {
|
|
3452
|
-
const { status, scheduled_for } = req.body;
|
|
3453
|
-
if (status !== "draft" && status !== "published" && status !== "scheduled") {
|
|
3454
|
-
res.status(400).json({ error: "status must be draft, published, or
|
|
3517
|
+
const { status, scheduled_for, editor_id, review_locked_by_editor, review_rejected } = req.body;
|
|
3518
|
+
if (status !== "draft" && status !== "published" && status !== "scheduled" && status !== "pending" && status !== "in_review") {
|
|
3519
|
+
res.status(400).json({ error: "status must be draft, published, scheduled, pending, or in_review" });
|
|
3455
3520
|
return;
|
|
3456
3521
|
}
|
|
3457
3522
|
if (status === "scheduled") {
|
|
@@ -3471,13 +3536,17 @@ var patchEntryStatus = async (req, res) => {
|
|
|
3471
3536
|
}
|
|
3472
3537
|
assertSafeIdentifier(ct.tableName);
|
|
3473
3538
|
const quotedTableName = quoteIdentifier(ct.tableName);
|
|
3474
|
-
const
|
|
3475
|
-
|
|
3476
|
-
|
|
3539
|
+
const editorialMode = req.appModes?.editorial ?? false;
|
|
3540
|
+
const currentRole = await roleName(req.user?.roleId);
|
|
3541
|
+
const isContributor = currentRole === "contributor";
|
|
3542
|
+
const isAdminRole = currentRole === "admin" || currentRole === "super admin";
|
|
3543
|
+
const isEditorRole = currentRole === "editor";
|
|
3544
|
+
if (isContributor && ct.kind === "single") {
|
|
3545
|
+
res.status(403).json({ error: "Single types are read-only for Contributor role" });
|
|
3477
3546
|
return;
|
|
3478
3547
|
}
|
|
3479
|
-
if (
|
|
3480
|
-
const { rows: authorRows } = await pool_default.query(`SELECT created_by FROM ${quotedTableName} WHERE id = $1`, [req.params.id]);
|
|
3548
|
+
if (isContributor && ct.kind === "collection") {
|
|
3549
|
+
const { rows: authorRows } = await pool_default.query(`SELECT created_by, review_locked_by_editor FROM ${quotedTableName} WHERE id = $1`, [req.params.id]);
|
|
3481
3550
|
if (!authorRows[0]) {
|
|
3482
3551
|
res.status(404).json({ error: "Entry not found" });
|
|
3483
3552
|
return;
|
|
@@ -3486,6 +3555,18 @@ var patchEntryStatus = async (req, res) => {
|
|
|
3486
3555
|
res.status(403).json({ error: "Forbidden" });
|
|
3487
3556
|
return;
|
|
3488
3557
|
}
|
|
3558
|
+
if (editorialMode && authorRows[0].review_locked_by_editor && status !== "pending") {
|
|
3559
|
+
res.status(403).json({ error: "Entry is currently locked for contributor edits" });
|
|
3560
|
+
return;
|
|
3561
|
+
}
|
|
3562
|
+
}
|
|
3563
|
+
if (editorialMode && isContributor && status === "published") {
|
|
3564
|
+
res.status(403).json({ error: "Contributors cannot publish in editorial mode" });
|
|
3565
|
+
return;
|
|
3566
|
+
}
|
|
3567
|
+
if (editorialMode && isContributor && status === "scheduled") {
|
|
3568
|
+
res.status(403).json({ error: "Contributors cannot schedule in editorial mode" });
|
|
3569
|
+
return;
|
|
3489
3570
|
}
|
|
3490
3571
|
let sql;
|
|
3491
3572
|
let values;
|
|
@@ -3496,6 +3577,7 @@ var patchEntryStatus = async (req, res) => {
|
|
|
3496
3577
|
published_data = ${buildSnapshotExpr(ct.tableName)},
|
|
3497
3578
|
published_at = NOW(),
|
|
3498
3579
|
scheduled_for = NULL,
|
|
3580
|
+
review_rejected = FALSE,
|
|
3499
3581
|
updated_at = NOW()
|
|
3500
3582
|
WHERE id = $1
|
|
3501
3583
|
RETURNING *
|
|
@@ -3506,11 +3588,66 @@ var patchEntryStatus = async (req, res) => {
|
|
|
3506
3588
|
UPDATE ${quotedTableName} SET
|
|
3507
3589
|
status = 'scheduled',
|
|
3508
3590
|
scheduled_for = $2,
|
|
3591
|
+
review_rejected = FALSE,
|
|
3509
3592
|
updated_at = NOW()
|
|
3510
3593
|
WHERE id = $1
|
|
3511
3594
|
RETURNING *
|
|
3512
3595
|
`;
|
|
3513
3596
|
values = [req.params.id, scheduled_for];
|
|
3597
|
+
} else if (status === "pending") {
|
|
3598
|
+
sql = `
|
|
3599
|
+
UPDATE ${quotedTableName} SET
|
|
3600
|
+
status = 'pending',
|
|
3601
|
+
review_rejected = COALESCE($2, FALSE),
|
|
3602
|
+
review_locked_by_editor = FALSE,
|
|
3603
|
+
updated_at = NOW()
|
|
3604
|
+
WHERE id = $1
|
|
3605
|
+
RETURNING *
|
|
3606
|
+
`;
|
|
3607
|
+
values = [req.params.id, typeof review_rejected === "boolean" ? review_rejected : false];
|
|
3608
|
+
} else if (status === "in_review") {
|
|
3609
|
+
if (!editorialMode) {
|
|
3610
|
+
res.status(403).json({ error: "In review status requires editorial mode" });
|
|
3611
|
+
return;
|
|
3612
|
+
}
|
|
3613
|
+
if (!isAdminRole && !isEditorRole) {
|
|
3614
|
+
res.status(403).json({ error: "Forbidden" });
|
|
3615
|
+
return;
|
|
3616
|
+
}
|
|
3617
|
+
const requestedEditorId = typeof editor_id === "string" && editor_id.trim().length > 0 ? editor_id : isEditorRole ? req.user?.id ?? null : null;
|
|
3618
|
+
if (isEditorRole && requestedEditorId && requestedEditorId !== req.user?.id) {
|
|
3619
|
+
res.status(403).json({ error: "Editors can only assign themselves" });
|
|
3620
|
+
return;
|
|
3621
|
+
}
|
|
3622
|
+
if (requestedEditorId) {
|
|
3623
|
+
const { rows: editorRows } = await pool_default.query(`SELECT r.name as role_name
|
|
3624
|
+
FROM plank_users u
|
|
3625
|
+
JOIN plank_roles r ON r.id = u.role_id
|
|
3626
|
+
WHERE u.id = $1`, [requestedEditorId]);
|
|
3627
|
+
const targetRole = editorRows[0]?.role_name?.toLowerCase();
|
|
3628
|
+
if (isAdminRole) {
|
|
3629
|
+
if (requestedEditorId !== req.user?.id && targetRole !== "editor") {
|
|
3630
|
+
res.status(403).json({ error: "Admins can assign only themselves or Editors" });
|
|
3631
|
+
return;
|
|
3632
|
+
}
|
|
3633
|
+
} else if (targetRole !== "editor") {
|
|
3634
|
+
res.status(403).json({ error: "Invalid editor assignee" });
|
|
3635
|
+
return;
|
|
3636
|
+
}
|
|
3637
|
+
}
|
|
3638
|
+
const nextEditorId = requestedEditorId ?? (isEditorRole ? req.user?.id ?? null : null);
|
|
3639
|
+
const lock = typeof review_locked_by_editor === "boolean" ? review_locked_by_editor : false;
|
|
3640
|
+
sql = `
|
|
3641
|
+
UPDATE ${quotedTableName} SET
|
|
3642
|
+
status = 'in_review',
|
|
3643
|
+
editor_id = COALESCE($2, editor_id),
|
|
3644
|
+
review_locked_by_editor = $3,
|
|
3645
|
+
review_rejected = FALSE,
|
|
3646
|
+
updated_at = NOW()
|
|
3647
|
+
WHERE id = $1
|
|
3648
|
+
RETURNING *
|
|
3649
|
+
`;
|
|
3650
|
+
values = [req.params.id, nextEditorId, lock];
|
|
3514
3651
|
} else {
|
|
3515
3652
|
sql = `
|
|
3516
3653
|
UPDATE ${quotedTableName} SET
|
|
@@ -3518,6 +3655,8 @@ var patchEntryStatus = async (req, res) => {
|
|
|
3518
3655
|
published_data = NULL,
|
|
3519
3656
|
published_at = NULL,
|
|
3520
3657
|
scheduled_for = NULL,
|
|
3658
|
+
review_rejected = FALSE,
|
|
3659
|
+
review_locked_by_editor = FALSE,
|
|
3521
3660
|
updated_at = NOW()
|
|
3522
3661
|
WHERE id = $1
|
|
3523
3662
|
RETURNING *
|
|
@@ -3542,12 +3681,14 @@ var deleteEntry = async (req, res) => {
|
|
|
3542
3681
|
}
|
|
3543
3682
|
assertSafeIdentifier(ct.tableName);
|
|
3544
3683
|
const quotedTableName = quoteIdentifier(ct.tableName);
|
|
3545
|
-
const
|
|
3546
|
-
|
|
3547
|
-
|
|
3684
|
+
const currentRole = await roleName(req.user?.roleId);
|
|
3685
|
+
const isContributor = currentRole === "contributor";
|
|
3686
|
+
const isEditor = currentRole === "editor";
|
|
3687
|
+
if (isContributor && ct.kind === "single") {
|
|
3688
|
+
res.status(403).json({ error: "Single types are read-only for Contributor role" });
|
|
3548
3689
|
return;
|
|
3549
3690
|
}
|
|
3550
|
-
if (
|
|
3691
|
+
if ((isContributor || isEditor) && ct.kind === "collection") {
|
|
3551
3692
|
const { rows: authorRows } = await pool_default.query(`SELECT created_by FROM ${quotedTableName} WHERE id = $1`, [req.params.id]);
|
|
3552
3693
|
if (!authorRows[0]) {
|
|
3553
3694
|
res.status(404).json({ error: "Entry not found" });
|
|
@@ -3576,13 +3717,15 @@ import { z as z4, flattenError as flattenError4 } from "zod";
|
|
|
3576
3717
|
var CreateUserSchema = z4.object({
|
|
3577
3718
|
email: z4.email(),
|
|
3578
3719
|
password: z4.string().min(8),
|
|
3579
|
-
roleId: z4.string().min(1)
|
|
3720
|
+
roleId: z4.string().min(1),
|
|
3721
|
+
enabled: z4.boolean().optional()
|
|
3580
3722
|
});
|
|
3581
3723
|
var UpdateUserSchema = z4.object({
|
|
3582
3724
|
email: z4.email().optional(),
|
|
3583
3725
|
roleId: z4.string().min(1).optional(),
|
|
3584
3726
|
firstName: z4.string().max(100).nullable().optional(),
|
|
3585
|
-
lastName: z4.string().max(100).nullable().optional()
|
|
3727
|
+
lastName: z4.string().max(100).nullable().optional(),
|
|
3728
|
+
enabled: z4.boolean().optional()
|
|
3586
3729
|
});
|
|
3587
3730
|
var ChangePasswordSchema = z4.object({
|
|
3588
3731
|
currentPassword: z4.string().min(1),
|
|
@@ -3636,7 +3779,7 @@ async function roleNameById(roleId) {
|
|
|
3636
3779
|
return rows[0]?.name ?? null;
|
|
3637
3780
|
}
|
|
3638
3781
|
async function listUsers(_req, res) {
|
|
3639
|
-
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
|
|
3782
|
+
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
|
|
3640
3783
|
FROM plank_users u
|
|
3641
3784
|
JOIN plank_roles r ON r.id = u.role_id
|
|
3642
3785
|
ORDER BY u.created_at DESC`);
|
|
@@ -3644,7 +3787,7 @@ async function listUsers(_req, res) {
|
|
|
3644
3787
|
}
|
|
3645
3788
|
async function getMe(req, res) {
|
|
3646
3789
|
const { rows } = await pool_default.query(`SELECT u.id, u.email, u.role_id, u.first_name, u.last_name, u.avatar_url,
|
|
3647
|
-
u.job_title, u.organization, u.country, u.two_factor_enabled, u.created_at,
|
|
3790
|
+
u.job_title, u.organization, u.country, u.two_factor_enabled, u.enabled, u.created_at,
|
|
3648
3791
|
r.name AS role_name, r.permissions
|
|
3649
3792
|
FROM plank_users u
|
|
3650
3793
|
JOIN plank_roles r ON r.id = u.role_id
|
|
@@ -3654,11 +3797,14 @@ async function getMe(req, res) {
|
|
|
3654
3797
|
return;
|
|
3655
3798
|
}
|
|
3656
3799
|
const resolved = await resolveAvatarUrl(rows[0]);
|
|
3800
|
+
const modes = req.appModes ?? await resolveAppModes();
|
|
3657
3801
|
res.json({
|
|
3658
3802
|
...resolved,
|
|
3659
3803
|
role: rows[0].role_name,
|
|
3660
3804
|
permissions: rows[0].permissions,
|
|
3661
|
-
|
|
3805
|
+
enabled: rows[0].enabled ?? true,
|
|
3806
|
+
two_factor_enabled: rows[0].two_factor_enabled,
|
|
3807
|
+
modes
|
|
3662
3808
|
});
|
|
3663
3809
|
}
|
|
3664
3810
|
async function getTwoFactorStatus(req, res) {
|
|
@@ -3873,17 +4019,20 @@ async function createUser(req, res) {
|
|
|
3873
4019
|
res.status(400).json({ errors: flattenError4(parsed.error, (i2) => i2.message) });
|
|
3874
4020
|
return;
|
|
3875
4021
|
}
|
|
3876
|
-
const { email, password, roleId } = parsed.data;
|
|
4022
|
+
const { email, password, roleId, enabled } = parsed.data;
|
|
3877
4023
|
const requesterRoleName = await roleNameById(req.user.roleId);
|
|
3878
4024
|
const targetRoleName = await roleNameById(roleId);
|
|
3879
4025
|
if (targetRoleName === "Super Admin" && requesterRoleName !== "Super Admin") {
|
|
3880
4026
|
res.status(403).json({ error: "Only Super Admin can assign Super Admin role" });
|
|
3881
4027
|
return;
|
|
3882
4028
|
}
|
|
4029
|
+
const editorialMode = req.appModes?.editorial ?? false;
|
|
4030
|
+
const isEditorialExclusiveRole = ["Editor", "Viewer"].includes(targetRoleName ?? "");
|
|
4031
|
+
const nextEnabled = editorialMode || !isEditorialExclusiveRole ? enabled ?? true : false;
|
|
3883
4032
|
const hashed = await bcrypt2.hash(password, 12);
|
|
3884
4033
|
const id = createId();
|
|
3885
|
-
await pool_default.query("INSERT INTO plank_users (id, email, password, role_id) VALUES ($1, $2, $3, $4)", [id, email, hashed, roleId]);
|
|
3886
|
-
res.status(201).json({ id, email, roleId });
|
|
4034
|
+
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]);
|
|
4035
|
+
res.status(201).json({ id, email, roleId, enabled: nextEnabled });
|
|
3887
4036
|
}
|
|
3888
4037
|
async function updateUser(req, res) {
|
|
3889
4038
|
const parsed = UpdateUserSchema.safeParse(req.body);
|
|
@@ -3891,7 +4040,7 @@ async function updateUser(req, res) {
|
|
|
3891
4040
|
res.status(400).json({ errors: flattenError4(parsed.error, (i2) => i2.message) });
|
|
3892
4041
|
return;
|
|
3893
4042
|
}
|
|
3894
|
-
const { email, roleId, firstName, lastName } = parsed.data;
|
|
4043
|
+
const { email, roleId, firstName, lastName, enabled } = parsed.data;
|
|
3895
4044
|
const requesterRoleName = await roleNameById(req.user.roleId);
|
|
3896
4045
|
const { rows: targetRows } = await pool_default.query("SELECT role_id FROM plank_users WHERE id = $1", [req.params.id]);
|
|
3897
4046
|
if (!targetRows[0]) {
|
|
@@ -3903,20 +4052,30 @@ async function updateUser(req, res) {
|
|
|
3903
4052
|
res.status(403).json({ error: "Only Super Admin can edit Super Admin users" });
|
|
3904
4053
|
return;
|
|
3905
4054
|
}
|
|
4055
|
+
const editorialMode = req.appModes?.editorial ?? false;
|
|
4056
|
+
let resolvedEnabled = enabled;
|
|
3906
4057
|
if (roleId) {
|
|
3907
4058
|
const nextRoleName = await roleNameById(roleId);
|
|
3908
4059
|
if (nextRoleName === "Super Admin" && requesterRoleName !== "Super Admin") {
|
|
3909
4060
|
res.status(403).json({ error: "Only Super Admin can assign Super Admin role" });
|
|
3910
4061
|
return;
|
|
3911
4062
|
}
|
|
4063
|
+
if (!editorialMode && ["Editor", "Viewer"].includes(nextRoleName ?? "")) {
|
|
4064
|
+
resolvedEnabled = false;
|
|
4065
|
+
}
|
|
3912
4066
|
}
|
|
3913
4067
|
const { rows } = await pool_default.query(`UPDATE plank_users
|
|
3914
4068
|
SET email = COALESCE($1, email),
|
|
3915
4069
|
role_id = COALESCE($2, role_id),
|
|
3916
4070
|
first_name = COALESCE($3, first_name),
|
|
3917
|
-
last_name = COALESCE($4, last_name)
|
|
3918
|
-
|
|
3919
|
-
|
|
4071
|
+
last_name = COALESCE($4, last_name),
|
|
4072
|
+
enabled = COALESCE($5, enabled),
|
|
4073
|
+
session_version = CASE
|
|
4074
|
+
WHEN $5 IS NOT NULL AND $5 = FALSE AND enabled = TRUE THEN session_version + 1
|
|
4075
|
+
ELSE session_version
|
|
4076
|
+
END
|
|
4077
|
+
WHERE id = $6
|
|
4078
|
+
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]);
|
|
3920
4079
|
if (!rows[0]) {
|
|
3921
4080
|
res.status(404).json({ error: "User not found" });
|
|
3922
4081
|
return;
|
|
@@ -4264,6 +4423,14 @@ async function getNamespaceSettings(req, res) {
|
|
|
4264
4423
|
const settings = await getSettings(namespace);
|
|
4265
4424
|
res.json(maskSettings(namespace, settings));
|
|
4266
4425
|
}
|
|
4426
|
+
async function getAppModes(req, res) {
|
|
4427
|
+
const modes = req.appModes ?? await resolveAppModes();
|
|
4428
|
+
res.json(modes);
|
|
4429
|
+
}
|
|
4430
|
+
async function getEditorialMode(_req, res) {
|
|
4431
|
+
const { editorial: enabled } = await resolveAppModes();
|
|
4432
|
+
res.json({ enabled });
|
|
4433
|
+
}
|
|
4267
4434
|
async function updateNamespaceSettings(req, res) {
|
|
4268
4435
|
const { namespace } = req.params;
|
|
4269
4436
|
const incoming = req.body;
|
|
@@ -4279,6 +4446,13 @@ async function updateNamespaceSettings(req, res) {
|
|
|
4279
4446
|
toSave[key] = value;
|
|
4280
4447
|
}
|
|
4281
4448
|
await setSettings(namespace, toSave);
|
|
4449
|
+
if (namespace === "general" && Object.prototype.hasOwnProperty.call(toSave, "editorial_mode") && String(toSave.editorial_mode).toLowerCase() === "false") {
|
|
4450
|
+
await pool_default.query(`UPDATE plank_users
|
|
4451
|
+
SET enabled = FALSE, session_version = session_version + 1
|
|
4452
|
+
WHERE role_id IN (
|
|
4453
|
+
SELECT id FROM plank_roles WHERE LOWER(name) IN ('editor', 'viewer')
|
|
4454
|
+
)`);
|
|
4455
|
+
}
|
|
4282
4456
|
const updated = await getSettings(namespace);
|
|
4283
4457
|
res.json(maskSettings(namespace, updated));
|
|
4284
4458
|
}
|
|
@@ -4286,6 +4460,7 @@ async function updateNamespaceSettings(req, res) {
|
|
|
4286
4460
|
// ../core/dist/routes/admin.js
|
|
4287
4461
|
var router2 = Router2();
|
|
4288
4462
|
router2.use(authenticate);
|
|
4463
|
+
router2.use(attachAppModes);
|
|
4289
4464
|
router2.get("/content-types", authorize("content-types:read"), listContentTypes);
|
|
4290
4465
|
router2.post("/content-types", authorize("content-types:write"), createContentType);
|
|
4291
4466
|
router2.get("/content-types/:slug", authorize("content-types:read"), getContentType);
|
|
@@ -4334,6 +4509,8 @@ router2.post("/media", authorize("media:write"), upload.array("files", 500), upl
|
|
|
4334
4509
|
router2.get("/media/:id/url", authorize("media:read"), getMediaUrl);
|
|
4335
4510
|
router2.patch("/media/:id", authorize("media:write"), updateMedia);
|
|
4336
4511
|
router2.delete("/media/:id", authorize("media:delete"), deleteMedia);
|
|
4512
|
+
router2.get("/modes", getAppModes);
|
|
4513
|
+
router2.get("/editorial-mode", getEditorialMode);
|
|
4337
4514
|
router2.get("/settings/:namespace", authorize("settings:overview:read"), getNamespaceSettings);
|
|
4338
4515
|
router2.put("/settings/:namespace", authorize("settings:overview:write"), updateNamespaceSettings);
|
|
4339
4516
|
router2.get("/webhooks", authorize("settings:webhooks:read"), listWebhooks);
|
|
@@ -4530,10 +4707,14 @@ async function resolveAuthorAvatars(entries) {
|
|
|
4530
4707
|
if (author?.avatar_url && !author.avatar_url.startsWith("http")) {
|
|
4531
4708
|
author.avatar_url = await provider.getUrl(author.avatar_url);
|
|
4532
4709
|
}
|
|
4710
|
+
const editor = entry.editor;
|
|
4711
|
+
if (editor?.avatar_url && !editor.avatar_url.startsWith("http")) {
|
|
4712
|
+
editor.avatar_url = await provider.getUrl(editor.avatar_url);
|
|
4713
|
+
}
|
|
4533
4714
|
}));
|
|
4534
4715
|
}
|
|
4535
4716
|
function serializeEntry(row, ct, statusParam, locale, fallbacks = []) {
|
|
4536
|
-
const { published_data, _author_first_name, _author_last_name, _author_avatar_url, _author_job_title, _author_organization, _author_country, ...rest } = row;
|
|
4717
|
+
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;
|
|
4537
4718
|
const source = statusParam === "published" && published_data ? published_data : rest;
|
|
4538
4719
|
const effective = { ...source };
|
|
4539
4720
|
if (locale) {
|
|
@@ -4584,6 +4765,14 @@ function serializeEntry(row, ct, statusParam, locale, fallbacks = []) {
|
|
|
4584
4765
|
organization: _author_organization ?? null,
|
|
4585
4766
|
country: _author_country ?? null
|
|
4586
4767
|
} : null;
|
|
4768
|
+
out.editor = _editor_first_name || _editor_last_name ? {
|
|
4769
|
+
first_name: _editor_first_name ?? null,
|
|
4770
|
+
last_name: _editor_last_name ?? null,
|
|
4771
|
+
avatar_url: _editor_avatar_url ?? null,
|
|
4772
|
+
job_title: _editor_job_title ?? null,
|
|
4773
|
+
organization: _editor_organization ?? null,
|
|
4774
|
+
country: _editor_country ?? null
|
|
4775
|
+
} : null;
|
|
4587
4776
|
return out;
|
|
4588
4777
|
}
|
|
4589
4778
|
var listPublicEntries = async (req, res) => {
|
|
@@ -4599,9 +4788,11 @@ var listPublicEntries = async (req, res) => {
|
|
|
4599
4788
|
const fallbacks2 = req.query.fallback ? String(req.query.fallback).split(",") : [];
|
|
4600
4789
|
const statusClause = statusParam2 === "published" || statusParam2 === "draft" ? `WHERE e.status = $1` : "";
|
|
4601
4790
|
const values = statusClause ? [statusParam2] : [];
|
|
4602
|
-
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
|
|
4791
|
+
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,
|
|
4792
|
+
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
|
|
4603
4793
|
FROM ${ct.tableName} e
|
|
4604
4794
|
LEFT JOIN plank_users u ON u.id = e.created_by
|
|
4795
|
+
LEFT JOIN plank_users ed ON ed.id = e.editor_id
|
|
4605
4796
|
${statusClause} LIMIT 1`, values);
|
|
4606
4797
|
if (!rows2[0]) {
|
|
4607
4798
|
res.status(404).json({ error: "Not found" });
|
|
@@ -4647,9 +4838,11 @@ var listPublicEntries = async (req, res) => {
|
|
|
4647
4838
|
const limitParam = filterValues.length + 1;
|
|
4648
4839
|
const offsetParam = filterValues.length + 2;
|
|
4649
4840
|
const [{ rows }, { rows: countRows }] = await Promise.all([
|
|
4650
|
-
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
|
|
4841
|
+
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,
|
|
4842
|
+
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
|
|
4651
4843
|
FROM ${ct.tableName} e
|
|
4652
4844
|
LEFT JOIN plank_users u ON u.id = e.created_by
|
|
4845
|
+
LEFT JOIN plank_users ed ON ed.id = e.editor_id
|
|
4653
4846
|
${where} ORDER BY e.${sortField} ${sortDir} LIMIT $${limitParam} OFFSET $${offsetParam}`, [...filterValues, limit, offset]),
|
|
4654
4847
|
pool_default.query(`SELECT COUNT(*) as count FROM ${ct.tableName} e ${where}`, filterValues)
|
|
4655
4848
|
]);
|
|
@@ -4671,9 +4864,11 @@ var getPublicEntry = async (req, res) => {
|
|
|
4671
4864
|
const statusParam = String(req.query.status ?? "published");
|
|
4672
4865
|
const statusClause = statusParam === "published" || statusParam === "draft" ? ` AND e.status = $2` : "";
|
|
4673
4866
|
const values = statusClause ? [req.params.id, statusParam] : [req.params.id];
|
|
4674
|
-
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
|
|
4867
|
+
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,
|
|
4868
|
+
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
|
|
4675
4869
|
FROM ${ct.tableName} e
|
|
4676
4870
|
LEFT JOIN plank_users u ON u.id = e.created_by
|
|
4871
|
+
LEFT JOIN plank_users ed ON ed.id = e.editor_id
|
|
4677
4872
|
WHERE e.id = $1${statusClause}`, values);
|
|
4678
4873
|
if (!rows[0]) {
|
|
4679
4874
|
res.status(404).json({ error: "Not found" });
|