@plank-cms/plank 0.21.3 → 0.23.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-BSi0iXTe.css +2 -0
- package/dist/admin/assets/{index-Dwwt-EaN.js → index-C86iT0Bf.js} +38 -38
- package/dist/admin/index.html +2 -2
- package/dist/index.js +3 -3
- package/dist/migrations/030_public_author_slug.sql +79 -0
- package/dist/migrations/031_public_author_slug_uid_rebuild.sql +82 -0
- package/dist/{server-CW3GRBTB.js → server-M2KTICST.js} +241 -52
- package/package.json +4 -4
- package/dist/admin/assets/index-CE2YLL9F.css +0 -2
|
@@ -2948,7 +2948,20 @@ var ArraySubFieldSchema = z2.object({
|
|
|
2948
2948
|
});
|
|
2949
2949
|
var FieldSchema = z2.object({
|
|
2950
2950
|
name: z2.string().regex(/^[a-z][a-z0-9_]*$/, "Field name must be lowercase with underscores"),
|
|
2951
|
-
type: z2.enum([
|
|
2951
|
+
type: z2.enum([
|
|
2952
|
+
"string",
|
|
2953
|
+
"text",
|
|
2954
|
+
"richtext",
|
|
2955
|
+
"number",
|
|
2956
|
+
"boolean",
|
|
2957
|
+
"datetime",
|
|
2958
|
+
"media",
|
|
2959
|
+
"media-gallery",
|
|
2960
|
+
"relation",
|
|
2961
|
+
"uid",
|
|
2962
|
+
"array",
|
|
2963
|
+
"navigation"
|
|
2964
|
+
]),
|
|
2952
2965
|
required: z2.boolean().optional(),
|
|
2953
2966
|
subtype: z2.enum(["integer", "float"]).optional(),
|
|
2954
2967
|
relationType: z2.enum(["many-to-one", "one-to-one", "one-to-many", "many-to-many"]).optional(),
|
|
@@ -2974,6 +2987,9 @@ function isReservedSqlIdentifier(name) {
|
|
|
2974
2987
|
}
|
|
2975
2988
|
function findReservedIdentifierErrors(ct) {
|
|
2976
2989
|
const errors = [];
|
|
2990
|
+
if (ct.tableName === "authors") {
|
|
2991
|
+
errors.push('Content type slug "authors" is reserved by the public API.');
|
|
2992
|
+
}
|
|
2977
2993
|
if (isReservedSqlIdentifier(ct.tableName)) {
|
|
2978
2994
|
errors.push(`Table name "${ct.tableName}" is reserved by SQL. Choose a different content type slug.`);
|
|
2979
2995
|
}
|
|
@@ -3316,8 +3332,10 @@ var listEntries = async (req, res) => {
|
|
|
3316
3332
|
const searchFields = rawSearchFields.filter((name) => ct.fields.some((f2) => f2.name === name && textLikeTypes.includes(f2.type)));
|
|
3317
3333
|
const mainParams = [limit, offset];
|
|
3318
3334
|
const countParams = [];
|
|
3335
|
+
const statusParams = [];
|
|
3319
3336
|
const mainClauses = [];
|
|
3320
3337
|
const countClauses = [];
|
|
3338
|
+
const statusClauses = [];
|
|
3321
3339
|
const allowedStatuses = ["draft", "published", "scheduled", "pending", "in_review"];
|
|
3322
3340
|
const statusFilter = req.query.status ? String(req.query.status) : "";
|
|
3323
3341
|
if (statusFilter && allowedStatuses.includes(statusFilter)) {
|
|
@@ -3330,8 +3348,10 @@ var listEntries = async (req, res) => {
|
|
|
3330
3348
|
const term = `%${search}%`;
|
|
3331
3349
|
mainParams.push(term);
|
|
3332
3350
|
countParams.push(term);
|
|
3351
|
+
statusParams.push(term);
|
|
3333
3352
|
const mainIdx = mainParams.length;
|
|
3334
3353
|
const countIdx = countParams.length;
|
|
3354
|
+
const statusIdx = statusParams.length;
|
|
3335
3355
|
const searchMainConditions = searchFields.map((name) => {
|
|
3336
3356
|
assertSafeIdentifier(name);
|
|
3337
3357
|
return `e.${quoteIdentifier(name)}::text ILIKE $${mainIdx}`;
|
|
@@ -3339,12 +3359,17 @@ var listEntries = async (req, res) => {
|
|
|
3339
3359
|
const searchCountConditions = searchFields.map((name) => {
|
|
3340
3360
|
return `e.${quoteIdentifier(name)}::text ILIKE $${countIdx}`;
|
|
3341
3361
|
});
|
|
3362
|
+
const searchStatusConditions = searchFields.map((name) => {
|
|
3363
|
+
return `e.${quoteIdentifier(name)}::text ILIKE $${statusIdx}`;
|
|
3364
|
+
});
|
|
3342
3365
|
mainClauses.push(`(${searchMainConditions.join(" OR ")})`);
|
|
3343
3366
|
countClauses.push(`(${searchCountConditions.join(" OR ")})`);
|
|
3367
|
+
statusClauses.push(`(${searchStatusConditions.join(" OR ")})`);
|
|
3344
3368
|
}
|
|
3345
3369
|
const mainWhereClause = mainClauses.length > 0 ? `WHERE ${mainClauses.join(" AND ")}` : "";
|
|
3346
3370
|
const countWhereClause = countClauses.length > 0 ? `WHERE ${countClauses.join(" AND ")}` : "";
|
|
3347
|
-
const
|
|
3371
|
+
const statusWhereClause = statusClauses.length > 0 ? `WHERE ${statusClauses.join(" AND ")}` : "";
|
|
3372
|
+
const [{ rows }, { rows: countRows }, { rows: statusRows }] = await Promise.all([
|
|
3348
3373
|
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,
|
|
3349
3374
|
ed.first_name AS _editor_first_name, ed.last_name AS _editor_last_name, ed.avatar_url AS _editor_avatar_url
|
|
3350
3375
|
FROM ${quotedTableName} e
|
|
@@ -3353,7 +3378,11 @@ var listEntries = async (req, res) => {
|
|
|
3353
3378
|
${mainWhereClause}
|
|
3354
3379
|
ORDER BY e.${quotedSortField} ${sortDir}
|
|
3355
3380
|
LIMIT $1 OFFSET $2`, mainParams),
|
|
3356
|
-
pool_default.query(`SELECT COUNT(*) as count FROM ${quotedTableName} e ${countWhereClause}`, countParams)
|
|
3381
|
+
pool_default.query(`SELECT COUNT(*) as count FROM ${quotedTableName} e ${countWhereClause}`, countParams),
|
|
3382
|
+
pool_default.query(`SELECT DISTINCT e.status
|
|
3383
|
+
FROM ${quotedTableName} e
|
|
3384
|
+
${statusWhereClause}
|
|
3385
|
+
ORDER BY e.status`, statusParams)
|
|
3357
3386
|
]);
|
|
3358
3387
|
const provider = await getProvider();
|
|
3359
3388
|
function entryMatchesLocale(row, locale2) {
|
|
@@ -3392,7 +3421,13 @@ var listEntries = async (req, res) => {
|
|
|
3392
3421
|
} catch {
|
|
3393
3422
|
}
|
|
3394
3423
|
}
|
|
3395
|
-
res.json({
|
|
3424
|
+
res.json({
|
|
3425
|
+
data,
|
|
3426
|
+
total,
|
|
3427
|
+
page,
|
|
3428
|
+
limit,
|
|
3429
|
+
available_statuses: statusRows.map((row) => row.status).filter((value) => allowedStatuses.includes(value))
|
|
3430
|
+
});
|
|
3396
3431
|
};
|
|
3397
3432
|
async function loadManyToManyIds(entryId, tableName, fields) {
|
|
3398
3433
|
const mmFields = fields.filter((f2) => f2.type === "relation" && (f2.relationType ?? "many-to-one") === "many-to-many");
|
|
@@ -3406,6 +3441,28 @@ async function loadManyToManyIds(entryId, tableName, fields) {
|
|
|
3406
3441
|
}));
|
|
3407
3442
|
return result;
|
|
3408
3443
|
}
|
|
3444
|
+
async function loadHydratedEntry(entryId, tableName, fields, locale, fallbacks = []) {
|
|
3445
|
+
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,
|
|
3446
|
+
ed.first_name AS _editor_first_name, ed.last_name AS _editor_last_name, ed.avatar_url AS _editor_avatar_url
|
|
3447
|
+
FROM ${quoteIdentifier(tableName)} e
|
|
3448
|
+
LEFT JOIN plank_users u ON u.id = e.created_by
|
|
3449
|
+
LEFT JOIN plank_users ed ON ed.id = e.editor_id
|
|
3450
|
+
WHERE e.id = $1`, [entryId]);
|
|
3451
|
+
if (!rows[0])
|
|
3452
|
+
return null;
|
|
3453
|
+
const mmIds = await loadManyToManyIds(entryId, tableName, fields);
|
|
3454
|
+
const provider = await getProvider();
|
|
3455
|
+
const resolved = resolveLocalizedRow(rows[0], { fields }, locale, fallbacks);
|
|
3456
|
+
const authorKey = resolved._author_avatar_url;
|
|
3457
|
+
if (authorKey && !authorKey.startsWith("http")) {
|
|
3458
|
+
resolved._author_avatar_url = await provider.getUrl(authorKey);
|
|
3459
|
+
}
|
|
3460
|
+
const editorKey = resolved._editor_avatar_url;
|
|
3461
|
+
if (editorKey && !editorKey.startsWith("http")) {
|
|
3462
|
+
resolved._editor_avatar_url = await provider.getUrl(editorKey);
|
|
3463
|
+
}
|
|
3464
|
+
return normalizeNavigationFields({ ...resolved, ...mmIds }, fields);
|
|
3465
|
+
}
|
|
3409
3466
|
var getEntry = async (req, res) => {
|
|
3410
3467
|
const ct = await findContentTypeBySlug(req.params.slug);
|
|
3411
3468
|
if (!ct) {
|
|
@@ -3413,29 +3470,14 @@ var getEntry = async (req, res) => {
|
|
|
3413
3470
|
return;
|
|
3414
3471
|
}
|
|
3415
3472
|
assertSafeIdentifier(ct.tableName);
|
|
3416
|
-
const
|
|
3417
|
-
const
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
LEFT JOIN plank_users u ON u.id = e.created_by
|
|
3421
|
-
LEFT JOIN plank_users ed ON ed.id = e.editor_id
|
|
3422
|
-
WHERE e.id = $1`, [req.params.id]);
|
|
3423
|
-
if (!rows[0]) {
|
|
3473
|
+
const locale = req.query.locale ? String(req.query.locale) : void 0;
|
|
3474
|
+
const fallbacks = req.query.fallback ? String(req.query.fallback).split(",") : [];
|
|
3475
|
+
const entry = await loadHydratedEntry(req.params.id, ct.tableName, ct.fields, locale, fallbacks);
|
|
3476
|
+
if (!entry) {
|
|
3424
3477
|
res.status(404).json({ error: "Entry not found" });
|
|
3425
3478
|
return;
|
|
3426
3479
|
}
|
|
3427
|
-
|
|
3428
|
-
const fallbacks = req.query.fallback ? String(req.query.fallback).split(",") : [];
|
|
3429
|
-
const mmIds = await loadManyToManyIds(req.params.id, ct.tableName, ct.fields);
|
|
3430
|
-
const provider = await getProvider();
|
|
3431
|
-
const resolved = resolveLocalizedRow(rows[0], ct, locale, fallbacks);
|
|
3432
|
-
const key = resolved._author_avatar_url;
|
|
3433
|
-
if (key && !key.startsWith("http"))
|
|
3434
|
-
resolved._author_avatar_url = await provider.getUrl(key);
|
|
3435
|
-
const editorKey = resolved._editor_avatar_url;
|
|
3436
|
-
if (editorKey && !editorKey.startsWith("http"))
|
|
3437
|
-
resolved._editor_avatar_url = await provider.getUrl(editorKey);
|
|
3438
|
-
res.json(normalizeNavigationFields({ ...resolved, ...mmIds }, ct.fields));
|
|
3480
|
+
res.json(entry);
|
|
3439
3481
|
};
|
|
3440
3482
|
var createEntry = async (req, res) => {
|
|
3441
3483
|
const ct = await findContentTypeBySlug(req.params.slug);
|
|
@@ -3688,7 +3730,7 @@ var patchEntryStatus = async (req, res) => {
|
|
|
3688
3730
|
UPDATE ${quotedTableName} SET
|
|
3689
3731
|
status = 'published',
|
|
3690
3732
|
published_data = ${buildSnapshotExpr(ct.tableName)},
|
|
3691
|
-
published_at = NOW(),
|
|
3733
|
+
published_at = COALESCE(published_at, NOW()),
|
|
3692
3734
|
scheduled_for = NULL,
|
|
3693
3735
|
review_rejected = FALSE,
|
|
3694
3736
|
updated_at = NOW()
|
|
@@ -3762,7 +3804,7 @@ var patchEntryStatus = async (req, res) => {
|
|
|
3762
3804
|
sql = `
|
|
3763
3805
|
UPDATE ${quotedTableName} SET
|
|
3764
3806
|
status = 'in_review',
|
|
3765
|
-
editor_id =
|
|
3807
|
+
editor_id = $2,
|
|
3766
3808
|
review_locked_by_editor = $3,
|
|
3767
3809
|
review_rejected = FALSE,
|
|
3768
3810
|
updated_at = NOW()
|
|
@@ -3790,7 +3832,12 @@ var patchEntryStatus = async (req, res) => {
|
|
|
3790
3832
|
res.status(404).json({ error: "Entry not found" });
|
|
3791
3833
|
return;
|
|
3792
3834
|
}
|
|
3793
|
-
|
|
3835
|
+
const entry = await loadHydratedEntry(req.params.id, ct.tableName, ct.fields);
|
|
3836
|
+
if (!entry) {
|
|
3837
|
+
res.status(404).json({ error: "Entry not found" });
|
|
3838
|
+
return;
|
|
3839
|
+
}
|
|
3840
|
+
res.json(entry);
|
|
3794
3841
|
const webhookEvent = status === "published" ? "entry.published" : status === "draft" ? "entry.unpublished" : null;
|
|
3795
3842
|
if (webhookEvent)
|
|
3796
3843
|
triggerWebhooks(webhookEvent, { content_type: req.params.slug, entry_id: req.params.id });
|
|
@@ -3836,10 +3883,40 @@ var deleteEntry = async (req, res) => {
|
|
|
3836
3883
|
import bcrypt2 from "bcryptjs";
|
|
3837
3884
|
import { randomBytes as randomBytes6 } from "crypto";
|
|
3838
3885
|
import { z as z4, flattenError as flattenError4 } from "zod";
|
|
3886
|
+
|
|
3887
|
+
// ../core/dist/lib/publicAuthorSlug.js
|
|
3888
|
+
function slugifySegment(value) {
|
|
3889
|
+
return value.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "").replace(/^-+|-+$/g, "");
|
|
3890
|
+
}
|
|
3891
|
+
function baseSlugFromUser(input) {
|
|
3892
|
+
const fullName = [input.firstName, input.lastName].map((value) => value?.trim() ?? "").filter(Boolean).join(" ");
|
|
3893
|
+
const base = fullName || input.email.split("@")[0] || "author";
|
|
3894
|
+
return slugifySegment(base) || "author";
|
|
3895
|
+
}
|
|
3896
|
+
async function resolveUniquePublicAuthorSlug(input, excludeUserId) {
|
|
3897
|
+
const baseSlug = baseSlugFromUser(input);
|
|
3898
|
+
let slug = baseSlug;
|
|
3899
|
+
let suffix = 2;
|
|
3900
|
+
while (true) {
|
|
3901
|
+
const { rows } = await pool_default.query(`SELECT id
|
|
3902
|
+
FROM plank_users
|
|
3903
|
+
WHERE public_author_slug = $1
|
|
3904
|
+
AND ($2::text IS NULL OR id != $2)
|
|
3905
|
+
LIMIT 1`, [slug, excludeUserId ?? null]);
|
|
3906
|
+
if (!rows[0])
|
|
3907
|
+
return slug;
|
|
3908
|
+
slug = `${baseSlug}-${suffix}`;
|
|
3909
|
+
suffix += 1;
|
|
3910
|
+
}
|
|
3911
|
+
}
|
|
3912
|
+
|
|
3913
|
+
// ../core/dist/controllers/users.js
|
|
3839
3914
|
var CreateUserSchema = z4.object({
|
|
3840
3915
|
email: z4.email(),
|
|
3841
3916
|
password: z4.string().min(8),
|
|
3842
3917
|
roleId: z4.string().min(1),
|
|
3918
|
+
firstName: z4.string().trim().min(1).max(100),
|
|
3919
|
+
lastName: z4.string().trim().min(1).max(100),
|
|
3843
3920
|
enabled: z4.boolean().optional()
|
|
3844
3921
|
});
|
|
3845
3922
|
var UpdateUserSchema = z4.object({
|
|
@@ -3908,7 +3985,7 @@ async function listUsers(_req, res) {
|
|
|
3908
3985
|
res.json(rows);
|
|
3909
3986
|
}
|
|
3910
3987
|
async function getMe(req, res) {
|
|
3911
|
-
const { rows } = await pool_default.query(`SELECT u.id, u.email, u.role_id, u.first_name, u.last_name, u.avatar_url,
|
|
3988
|
+
const { rows } = await pool_default.query(`SELECT u.id, u.email, u.role_id, u.first_name, u.last_name, u.public_author_slug, u.avatar_url,
|
|
3912
3989
|
u.job_title, u.organization, u.country, u.two_factor_enabled, u.enabled, u.created_at,
|
|
3913
3990
|
r.name AS role_name, r.permissions
|
|
3914
3991
|
FROM plank_users u
|
|
@@ -3987,7 +4064,9 @@ async function disableTwoFactor(req, res) {
|
|
|
3987
4064
|
res.status(400).json({ errors: flattenError4(parsed.error, (i2) => i2.message) });
|
|
3988
4065
|
return;
|
|
3989
4066
|
}
|
|
3990
|
-
const { rows } = await pool_default.query("SELECT two_factor_enabled, two_factor_secret, password FROM plank_users WHERE id = $1", [
|
|
4067
|
+
const { rows } = await pool_default.query("SELECT two_factor_enabled, two_factor_secret, password FROM plank_users WHERE id = $1", [
|
|
4068
|
+
req.user.id
|
|
4069
|
+
]);
|
|
3991
4070
|
const user = rows[0];
|
|
3992
4071
|
if (!user) {
|
|
3993
4072
|
res.status(404).json({ error: "User not found" });
|
|
@@ -4025,7 +4104,9 @@ async function regenerateBackupCodes(req, res) {
|
|
|
4025
4104
|
res.status(400).json({ errors: flattenError4(parsed.error, (i2) => i2.message) });
|
|
4026
4105
|
return;
|
|
4027
4106
|
}
|
|
4028
|
-
const { rows } = await pool_default.query("SELECT two_factor_enabled, two_factor_secret, password FROM plank_users WHERE id = $1", [
|
|
4107
|
+
const { rows } = await pool_default.query("SELECT two_factor_enabled, two_factor_secret, password FROM plank_users WHERE id = $1", [
|
|
4108
|
+
req.user.id
|
|
4109
|
+
]);
|
|
4029
4110
|
const user = rows[0];
|
|
4030
4111
|
if (!user || !user.two_factor_enabled || !user.two_factor_secret) {
|
|
4031
4112
|
res.status(400).json({ error: "2FA is not enabled" });
|
|
@@ -4036,7 +4117,10 @@ async function regenerateBackupCodes(req, res) {
|
|
|
4036
4117
|
res.status(401).json({ error: "Current password is incorrect" });
|
|
4037
4118
|
return;
|
|
4038
4119
|
}
|
|
4039
|
-
const totpResult = d3({
|
|
4120
|
+
const totpResult = d3({
|
|
4121
|
+
token: parsed.data.code,
|
|
4122
|
+
secret: decrypt(user.two_factor_secret)
|
|
4123
|
+
});
|
|
4040
4124
|
if (!totpResult.valid) {
|
|
4041
4125
|
res.status(401).json({ error: "Invalid verification code" });
|
|
4042
4126
|
return;
|
|
@@ -4051,15 +4135,36 @@ async function updateMe(req, res) {
|
|
|
4051
4135
|
return;
|
|
4052
4136
|
}
|
|
4053
4137
|
const { firstName, lastName, jobTitle, organization, country } = parsed.data;
|
|
4138
|
+
const { rows: currentRows } = await pool_default.query("SELECT email, first_name, last_name FROM plank_users WHERE id = $1", [req.user.id]);
|
|
4139
|
+
if (!currentRows[0]) {
|
|
4140
|
+
res.status(404).json({ error: "User not found" });
|
|
4141
|
+
return;
|
|
4142
|
+
}
|
|
4143
|
+
const nextFirstName = firstName ?? currentRows[0].first_name;
|
|
4144
|
+
const nextLastName = lastName ?? currentRows[0].last_name;
|
|
4145
|
+
const publicAuthorSlug = await resolveUniquePublicAuthorSlug({
|
|
4146
|
+
email: currentRows[0].email,
|
|
4147
|
+
firstName: nextFirstName,
|
|
4148
|
+
lastName: nextLastName
|
|
4149
|
+
}, req.user.id);
|
|
4054
4150
|
const { rows } = await pool_default.query(`UPDATE plank_users
|
|
4055
4151
|
SET first_name = COALESCE($1, first_name),
|
|
4056
4152
|
last_name = COALESCE($2, last_name),
|
|
4057
4153
|
job_title = COALESCE($3, job_title),
|
|
4058
4154
|
organization = COALESCE($4, organization),
|
|
4059
|
-
country = COALESCE($5, country)
|
|
4060
|
-
|
|
4061
|
-
|
|
4062
|
-
|
|
4155
|
+
country = COALESCE($5, country),
|
|
4156
|
+
public_author_slug = $6
|
|
4157
|
+
WHERE id = $7
|
|
4158
|
+
RETURNING id, email, role_id, first_name, last_name, public_author_slug, avatar_url,
|
|
4159
|
+
job_title, organization, country, created_at`, [
|
|
4160
|
+
firstName ?? null,
|
|
4161
|
+
lastName ?? null,
|
|
4162
|
+
jobTitle ?? null,
|
|
4163
|
+
organization ?? null,
|
|
4164
|
+
country ?? null,
|
|
4165
|
+
publicAuthorSlug,
|
|
4166
|
+
req.user.id
|
|
4167
|
+
]);
|
|
4063
4168
|
if (!rows[0]) {
|
|
4064
4169
|
res.status(404).json({ error: "User not found" });
|
|
4065
4170
|
return;
|
|
@@ -4105,7 +4210,9 @@ async function confirmAvatar(req, res) {
|
|
|
4105
4210
|
res.json({ avatarUrl, user: await resolveAvatarUrl(rows[0]) });
|
|
4106
4211
|
}
|
|
4107
4212
|
async function deleteAvatar(req, res) {
|
|
4108
|
-
const { rows } = await pool_default.query("SELECT avatar_url FROM plank_users WHERE id = $1", [
|
|
4213
|
+
const { rows } = await pool_default.query("SELECT avatar_url FROM plank_users WHERE id = $1", [
|
|
4214
|
+
req.user.id
|
|
4215
|
+
]);
|
|
4109
4216
|
const current = rows[0]?.avatar_url;
|
|
4110
4217
|
if (current && !current.startsWith("http")) {
|
|
4111
4218
|
const provider = await getProvider();
|
|
@@ -4141,7 +4248,7 @@ async function createUser(req, res) {
|
|
|
4141
4248
|
res.status(400).json({ errors: flattenError4(parsed.error, (i2) => i2.message) });
|
|
4142
4249
|
return;
|
|
4143
4250
|
}
|
|
4144
|
-
const { email, password, roleId, enabled } = parsed.data;
|
|
4251
|
+
const { email, password, roleId, firstName, lastName, enabled } = parsed.data;
|
|
4145
4252
|
const requesterRoleName = await roleNameById(req.user.roleId);
|
|
4146
4253
|
const targetRoleName = await roleNameById(roleId);
|
|
4147
4254
|
if (targetRoleName === "Super Admin" && requesterRoleName !== "Super Admin") {
|
|
@@ -4153,8 +4260,19 @@ async function createUser(req, res) {
|
|
|
4153
4260
|
const nextEnabled = editorialMode || !isEditorialExclusiveRole ? enabled ?? true : false;
|
|
4154
4261
|
const hashed = await bcrypt2.hash(password, 12);
|
|
4155
4262
|
const id = createId();
|
|
4156
|
-
|
|
4157
|
-
|
|
4263
|
+
const publicAuthorSlug = await resolveUniquePublicAuthorSlug({ email, firstName, lastName });
|
|
4264
|
+
await pool_default.query(`INSERT INTO plank_users
|
|
4265
|
+
(id, email, password, role_id, first_name, last_name, public_author_slug, enabled)
|
|
4266
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`, [id, email, hashed, roleId, firstName, lastName, publicAuthorSlug, nextEnabled]);
|
|
4267
|
+
res.status(201).json({
|
|
4268
|
+
id,
|
|
4269
|
+
email,
|
|
4270
|
+
roleId,
|
|
4271
|
+
first_name: firstName,
|
|
4272
|
+
last_name: lastName,
|
|
4273
|
+
public_author_slug: publicAuthorSlug,
|
|
4274
|
+
enabled: nextEnabled
|
|
4275
|
+
});
|
|
4158
4276
|
}
|
|
4159
4277
|
async function updateUser(req, res) {
|
|
4160
4278
|
const parsed = UpdateUserSchema.safeParse(req.body);
|
|
@@ -4186,18 +4304,41 @@ async function updateUser(req, res) {
|
|
|
4186
4304
|
resolvedEnabled = false;
|
|
4187
4305
|
}
|
|
4188
4306
|
}
|
|
4307
|
+
const targetUserId = String(req.params.id);
|
|
4308
|
+
const { rows: currentRows } = await pool_default.query("SELECT email, first_name, last_name FROM plank_users WHERE id = $1", [targetUserId]);
|
|
4309
|
+
if (!currentRows[0]) {
|
|
4310
|
+
res.status(404).json({ error: "User not found" });
|
|
4311
|
+
return;
|
|
4312
|
+
}
|
|
4313
|
+
const nextFirstName = firstName ?? currentRows[0].first_name;
|
|
4314
|
+
const nextLastName = lastName ?? currentRows[0].last_name;
|
|
4315
|
+
const nextEmail = email ?? currentRows[0].email;
|
|
4316
|
+
const publicAuthorSlug = await resolveUniquePublicAuthorSlug({
|
|
4317
|
+
email: nextEmail,
|
|
4318
|
+
firstName: nextFirstName,
|
|
4319
|
+
lastName: nextLastName
|
|
4320
|
+
}, targetUserId);
|
|
4189
4321
|
const { rows } = await pool_default.query(`UPDATE plank_users
|
|
4190
4322
|
SET email = COALESCE($1, email),
|
|
4191
4323
|
role_id = COALESCE($2, role_id),
|
|
4192
4324
|
first_name = COALESCE($3, first_name),
|
|
4193
4325
|
last_name = COALESCE($4, last_name),
|
|
4194
4326
|
enabled = COALESCE($5, enabled),
|
|
4327
|
+
public_author_slug = $6,
|
|
4195
4328
|
session_version = CASE
|
|
4196
4329
|
WHEN $5 IS NOT NULL AND $5 = FALSE AND enabled = TRUE THEN session_version + 1
|
|
4197
4330
|
ELSE session_version
|
|
4198
4331
|
END
|
|
4199
|
-
WHERE id = $
|
|
4200
|
-
RETURNING id, email, role_id, first_name, last_name, enabled, created_at`, [
|
|
4332
|
+
WHERE id = $7
|
|
4333
|
+
RETURNING id, email, role_id, first_name, last_name, public_author_slug, enabled, created_at`, [
|
|
4334
|
+
email ?? null,
|
|
4335
|
+
roleId ?? null,
|
|
4336
|
+
firstName ?? null,
|
|
4337
|
+
lastName ?? null,
|
|
4338
|
+
resolvedEnabled ?? null,
|
|
4339
|
+
publicAuthorSlug,
|
|
4340
|
+
req.params.id
|
|
4341
|
+
]);
|
|
4201
4342
|
if (!rows[0]) {
|
|
4202
4343
|
res.status(404).json({ error: "User not found" });
|
|
4203
4344
|
return;
|
|
@@ -4638,6 +4779,14 @@ async function getAppModes(req, res) {
|
|
|
4638
4779
|
const modes = req.appModes ?? await resolveAppModes();
|
|
4639
4780
|
res.json(modes);
|
|
4640
4781
|
}
|
|
4782
|
+
async function getClientSettings(_req, res) {
|
|
4783
|
+
const settings = await getSettings("general");
|
|
4784
|
+
res.json({
|
|
4785
|
+
timezone: settings.timezone ?? "UTC",
|
|
4786
|
+
locales: settings.locales ?? '["en"]',
|
|
4787
|
+
default_locale: settings.default_locale ?? "en"
|
|
4788
|
+
});
|
|
4789
|
+
}
|
|
4641
4790
|
async function getEditorialMode(_req, res) {
|
|
4642
4791
|
const { editorial: enabled } = await resolveAppModes();
|
|
4643
4792
|
res.json({ enabled });
|
|
@@ -4721,6 +4870,7 @@ router2.get("/media/:id/url", authorize("media:read"), getMediaUrl);
|
|
|
4721
4870
|
router2.patch("/media/:id", authorize("media:write"), updateMedia);
|
|
4722
4871
|
router2.delete("/media/:id", authorize("media:delete"), deleteMedia);
|
|
4723
4872
|
router2.get("/modes", getAppModes);
|
|
4873
|
+
router2.get("/client-settings", getClientSettings);
|
|
4724
4874
|
router2.get("/editorial-mode", getEditorialMode);
|
|
4725
4875
|
router2.get("/settings/:namespace", authorize("settings:overview:read"), getNamespaceSettings);
|
|
4726
4876
|
router2.put("/settings/:namespace", authorize("settings:overview:write"), updateNamespaceSettings);
|
|
@@ -5098,6 +5248,23 @@ async function resolveAuthorAvatars(entries) {
|
|
|
5098
5248
|
}
|
|
5099
5249
|
}));
|
|
5100
5250
|
}
|
|
5251
|
+
async function resolvePublicAuthorAvatar(author) {
|
|
5252
|
+
if (!author.avatar_url || author.avatar_url.startsWith("http"))
|
|
5253
|
+
return author;
|
|
5254
|
+
const provider = await getProvider();
|
|
5255
|
+
return { ...author, avatar_url: await provider.getUrl(author.avatar_url) };
|
|
5256
|
+
}
|
|
5257
|
+
function serializePublicAuthor(row) {
|
|
5258
|
+
return {
|
|
5259
|
+
first_name: row.first_name,
|
|
5260
|
+
last_name: row.last_name,
|
|
5261
|
+
avatar_url: row.avatar_url,
|
|
5262
|
+
job_title: row.job_title,
|
|
5263
|
+
organization: row.organization,
|
|
5264
|
+
country: row.country,
|
|
5265
|
+
slug: row.public_author_slug
|
|
5266
|
+
};
|
|
5267
|
+
}
|
|
5101
5268
|
function serializeEntry(row, ct, statusParam, locale, fallbacks = []) {
|
|
5102
5269
|
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;
|
|
5103
5270
|
const source = statusParam === "published" && published_data ? published_data : rest;
|
|
@@ -5148,7 +5315,8 @@ function serializeEntry(row, ct, statusParam, locale, fallbacks = []) {
|
|
|
5148
5315
|
avatar_url: _author_avatar_url ?? null,
|
|
5149
5316
|
job_title: _author_job_title ?? null,
|
|
5150
5317
|
organization: _author_organization ?? null,
|
|
5151
|
-
country: _author_country ?? null
|
|
5318
|
+
country: _author_country ?? null,
|
|
5319
|
+
slug: row._author_slug ?? null
|
|
5152
5320
|
} : null;
|
|
5153
5321
|
out.editor = _editor_first_name || _editor_last_name ? {
|
|
5154
5322
|
first_name: _editor_first_name ?? null,
|
|
@@ -5156,7 +5324,8 @@ function serializeEntry(row, ct, statusParam, locale, fallbacks = []) {
|
|
|
5156
5324
|
avatar_url: _editor_avatar_url ?? null,
|
|
5157
5325
|
job_title: _editor_job_title ?? null,
|
|
5158
5326
|
organization: _editor_organization ?? null,
|
|
5159
|
-
country: _editor_country ?? null
|
|
5327
|
+
country: _editor_country ?? null,
|
|
5328
|
+
slug: row._editor_slug ?? null
|
|
5160
5329
|
} : null;
|
|
5161
5330
|
return out;
|
|
5162
5331
|
}
|
|
@@ -5178,8 +5347,8 @@ var listPublicEntries = async (req, res) => {
|
|
|
5178
5347
|
const fallbacks2 = req.query.fallback ? String(req.query.fallback).split(",") : [];
|
|
5179
5348
|
const statusClause = statusParam2 === "published" || statusParam2 === "draft" ? `WHERE e.status = $1` : "";
|
|
5180
5349
|
const values = statusClause ? [statusParam2] : [];
|
|
5181
|
-
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,
|
|
5182
|
-
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
|
|
5350
|
+
const { rows: rows2 } = await pool_default.query(`SELECT e.*, u.first_name AS _author_first_name, u.last_name AS _author_last_name, u.public_author_slug AS _author_slug, u.avatar_url AS _author_avatar_url, u.job_title AS _author_job_title, u.organization AS _author_organization, u.country AS _author_country,
|
|
5351
|
+
ed.first_name AS _editor_first_name, ed.last_name AS _editor_last_name, ed.public_author_slug AS _editor_slug, ed.avatar_url AS _editor_avatar_url, ed.job_title AS _editor_job_title, ed.organization AS _editor_organization, ed.country AS _editor_country
|
|
5183
5352
|
FROM ${ct.tableName} e
|
|
5184
5353
|
LEFT JOIN plank_users u ON u.id = e.created_by
|
|
5185
5354
|
LEFT JOIN plank_users ed ON ed.id = e.editor_id
|
|
@@ -5217,6 +5386,11 @@ var listPublicEntries = async (req, res) => {
|
|
|
5217
5386
|
filterClauses.push(`e.status = $${filterValues.length + 1}`);
|
|
5218
5387
|
filterValues.push(statusParam);
|
|
5219
5388
|
}
|
|
5389
|
+
const authorSlug = typeof req.query.author === "string" ? req.query.author.trim() : "";
|
|
5390
|
+
if (authorSlug) {
|
|
5391
|
+
filterClauses.push(`u.public_author_slug = $${filterValues.length + 1}`);
|
|
5392
|
+
filterValues.push(authorSlug);
|
|
5393
|
+
}
|
|
5220
5394
|
const rawSort = String(req.query.sort ?? "created_at");
|
|
5221
5395
|
const sortField = knownFields.has(rawSort) || systemSortFields.has(rawSort) ? rawSort : "created_at";
|
|
5222
5396
|
assertSafeIdentifier(sortField);
|
|
@@ -5239,8 +5413,8 @@ var listPublicEntries = async (req, res) => {
|
|
|
5239
5413
|
filterClauses.push(parsedFilter.operator === "nin" ? `NOT (e.${fieldName} = ANY($${filterValues.length + 1}))` : `e.${fieldName} = ANY($${filterValues.length + 1})`);
|
|
5240
5414
|
filterValues.push(coercedValues);
|
|
5241
5415
|
}
|
|
5242
|
-
for (const [key
|
|
5243
|
-
if (key === "page" || key === "limit" || key === "status" || key === "sort" || key === "order" || key === "locale" || key === "fallback" || key === "fields" || key === "select" || key === "exclude" || key === "filters" || key.startsWith("filters["))
|
|
5416
|
+
for (const [key] of Object.entries(req.query)) {
|
|
5417
|
+
if (key === "page" || key === "limit" || key === "status" || key === "sort" || key === "order" || key === "locale" || key === "fallback" || key === "fields" || key === "select" || key === "exclude" || key === "author" || key === "filters" || key.startsWith("filters["))
|
|
5244
5418
|
continue;
|
|
5245
5419
|
if (knownFields.has(key))
|
|
5246
5420
|
continue;
|
|
@@ -5251,13 +5425,16 @@ var listPublicEntries = async (req, res) => {
|
|
|
5251
5425
|
const limitParam = filterValues.length + 1;
|
|
5252
5426
|
const offsetParam = filterValues.length + 2;
|
|
5253
5427
|
const [{ rows }, { rows: countRows }] = await Promise.all([
|
|
5254
|
-
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,
|
|
5255
|
-
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
|
|
5428
|
+
pool_default.query(`SELECT e.*, u.first_name AS _author_first_name, u.last_name AS _author_last_name, u.public_author_slug AS _author_slug, u.avatar_url AS _author_avatar_url, u.job_title AS _author_job_title, u.organization AS _author_organization, u.country AS _author_country,
|
|
5429
|
+
ed.first_name AS _editor_first_name, ed.last_name AS _editor_last_name, ed.public_author_slug AS _editor_slug, ed.avatar_url AS _editor_avatar_url, ed.job_title AS _editor_job_title, ed.organization AS _editor_organization, ed.country AS _editor_country
|
|
5256
5430
|
FROM ${ct.tableName} e
|
|
5257
5431
|
LEFT JOIN plank_users u ON u.id = e.created_by
|
|
5258
5432
|
LEFT JOIN plank_users ed ON ed.id = e.editor_id
|
|
5259
5433
|
${where} ORDER BY e.${sortField} ${sortDir} LIMIT $${limitParam} OFFSET $${offsetParam}`, [...filterValues, limit, offset]),
|
|
5260
|
-
pool_default.query(`SELECT COUNT(*) as count
|
|
5434
|
+
pool_default.query(`SELECT COUNT(*) as count
|
|
5435
|
+
FROM ${ct.tableName} e
|
|
5436
|
+
LEFT JOIN plank_users u ON u.id = e.created_by
|
|
5437
|
+
${where}`, filterValues)
|
|
5261
5438
|
]);
|
|
5262
5439
|
const data = rows.map((row) => serializeEntry(row, ct, statusParam, locale, fallbacks));
|
|
5263
5440
|
await Promise.all([
|
|
@@ -5287,8 +5464,8 @@ var getPublicEntry = async (req, res) => {
|
|
|
5287
5464
|
const statusParam = String(req.query.status ?? "published");
|
|
5288
5465
|
const statusClause = statusParam === "published" || statusParam === "draft" ? ` AND e.status = $2` : "";
|
|
5289
5466
|
const values = statusClause ? [req.params.id, statusParam] : [req.params.id];
|
|
5290
|
-
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,
|
|
5291
|
-
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
|
|
5467
|
+
const { rows } = await pool_default.query(`SELECT e.*, u.first_name AS _author_first_name, u.last_name AS _author_last_name, u.public_author_slug AS _author_slug, u.avatar_url AS _author_avatar_url, u.job_title AS _author_job_title, u.organization AS _author_organization, u.country AS _author_country,
|
|
5468
|
+
ed.first_name AS _editor_first_name, ed.last_name AS _editor_last_name, ed.public_author_slug AS _editor_slug, ed.avatar_url AS _editor_avatar_url, ed.job_title AS _editor_job_title, ed.organization AS _editor_organization, ed.country AS _editor_country
|
|
5292
5469
|
FROM ${ct.tableName} e
|
|
5293
5470
|
LEFT JOIN plank_users u ON u.id = e.created_by
|
|
5294
5471
|
LEFT JOIN plank_users ed ON ed.id = e.editor_id
|
|
@@ -5307,10 +5484,22 @@ var getPublicEntry = async (req, res) => {
|
|
|
5307
5484
|
]);
|
|
5308
5485
|
res.json(selectEntryFields(entry, ct, selection));
|
|
5309
5486
|
};
|
|
5487
|
+
var getPublicAuthor = async (req, res) => {
|
|
5488
|
+
const { rows } = await pool_default.query(`SELECT public_author_slug, first_name, last_name, avatar_url, job_title, organization, country
|
|
5489
|
+
FROM plank_users
|
|
5490
|
+
WHERE public_author_slug = $1
|
|
5491
|
+
LIMIT 1`, [req.params.slug]);
|
|
5492
|
+
if (!rows[0]) {
|
|
5493
|
+
res.status(404).json({ error: "Not found" });
|
|
5494
|
+
return;
|
|
5495
|
+
}
|
|
5496
|
+
res.json(await resolvePublicAuthorAvatar(serializePublicAuthor(rows[0])));
|
|
5497
|
+
};
|
|
5310
5498
|
|
|
5311
5499
|
// ../core/dist/routes/public.js
|
|
5312
5500
|
var router3 = Router3();
|
|
5313
5501
|
router3.use(apiToken);
|
|
5502
|
+
router3.get("/authors/:slug", getPublicAuthor);
|
|
5314
5503
|
router3.get("/:slug", listPublicEntries);
|
|
5315
5504
|
router3.get("/:slug/:id", getPublicEntry);
|
|
5316
5505
|
var public_default = router3;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plank-cms/plank",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.0",
|
|
4
4
|
"description": "Self-hosted headless CMS. Deploy in minutes on your own infrastructure.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -55,9 +55,9 @@
|
|
|
55
55
|
"devDependencies": {
|
|
56
56
|
"@types/fs-extra": "^11.0.4",
|
|
57
57
|
"tsup": "^8.5.0",
|
|
58
|
-
"@plank-cms/core": "0.
|
|
59
|
-
"@plank-cms/
|
|
60
|
-
"@plank-cms/
|
|
58
|
+
"@plank-cms/core": "0.23.0",
|
|
59
|
+
"@plank-cms/schema": "0.23.0",
|
|
60
|
+
"@plank-cms/db": "0.23.0"
|
|
61
61
|
},
|
|
62
62
|
"scripts": {
|
|
63
63
|
"build": "tsup",
|