@plank-cms/plank 0.22.0 → 0.24.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-dWzhX4Ev.js → index-BHOPEoNL.js} +3 -3
- package/dist/admin/index.html +1 -1
- 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-YVELAF42.js → server-FYUZINCY.js} +184 -29
- package/package.json +4 -4
package/dist/admin/index.html
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
href="https://fonts.googleapis.com/css2?family=Google+Sans:ital,opsz,wght@0,17..18,400..700;1,17..18,400..700&display=swap"
|
|
13
13
|
rel="stylesheet"
|
|
14
14
|
/>
|
|
15
|
-
<script type="module" crossorigin src="/admin/assets/index-
|
|
15
|
+
<script type="module" crossorigin src="/admin/assets/index-BHOPEoNL.js"></script>
|
|
16
16
|
<link rel="stylesheet" crossorigin href="/admin/assets/index-BSi0iXTe.css">
|
|
17
17
|
</head>
|
|
18
18
|
<body>
|
package/dist/index.js
CHANGED
|
@@ -7,7 +7,7 @@ import { randomBytes } from "crypto";
|
|
|
7
7
|
import { resolve, join } from "path";
|
|
8
8
|
import fs from "fs-extra";
|
|
9
9
|
import { execa } from "execa";
|
|
10
|
-
var PACKAGE_VERSION = "0.
|
|
10
|
+
var PACKAGE_VERSION = "0.24.0";
|
|
11
11
|
function generateSecret() {
|
|
12
12
|
return randomBytes(32).toString("hex");
|
|
13
13
|
}
|
|
@@ -101,7 +101,7 @@ import { dirname, join as join2, resolve as resolve2 } from "path";
|
|
|
101
101
|
async function start() {
|
|
102
102
|
config({ path: resolve2(process.cwd(), ".env") });
|
|
103
103
|
process.env.PLANK_ADMIN_DIST = join2(dirname(fileURLToPath(import.meta.url)), "admin");
|
|
104
|
-
const { start: startServer } = await import("./server-
|
|
104
|
+
const { start: startServer } = await import("./server-FYUZINCY.js");
|
|
105
105
|
await startServer();
|
|
106
106
|
}
|
|
107
107
|
|
|
@@ -135,7 +135,7 @@ async function publishScheduled() {
|
|
|
135
135
|
`UPDATE ${table_name} SET
|
|
136
136
|
status = 'published',
|
|
137
137
|
published_data = ${snapshotExpr(table_name)},
|
|
138
|
-
published_at = NOW(),
|
|
138
|
+
published_at = COALESCE(published_at, NOW()),
|
|
139
139
|
scheduled_for = NULL,
|
|
140
140
|
updated_at = NOW()
|
|
141
141
|
WHERE id = $1`,
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
ALTER TABLE plank_users
|
|
2
|
+
ADD COLUMN IF NOT EXISTS public_author_slug VARCHAR(255);
|
|
3
|
+
|
|
4
|
+
DO $$
|
|
5
|
+
DECLARE
|
|
6
|
+
usr RECORD;
|
|
7
|
+
base_slug TEXT;
|
|
8
|
+
next_slug TEXT;
|
|
9
|
+
suffix_num INTEGER;
|
|
10
|
+
BEGIN
|
|
11
|
+
FOR usr IN
|
|
12
|
+
SELECT id, email, first_name, last_name
|
|
13
|
+
FROM plank_users
|
|
14
|
+
WHERE public_author_slug IS NULL OR btrim(public_author_slug) = ''
|
|
15
|
+
ORDER BY created_at, id
|
|
16
|
+
LOOP
|
|
17
|
+
base_slug := lower(
|
|
18
|
+
trim(
|
|
19
|
+
both '-'
|
|
20
|
+
FROM regexp_replace(
|
|
21
|
+
regexp_replace(
|
|
22
|
+
regexp_replace(
|
|
23
|
+
coalesce(
|
|
24
|
+
nullif(trim(concat_ws(' ', usr.first_name, usr.last_name)), ''),
|
|
25
|
+
nullif(split_part(usr.email, '@', 1), ''),
|
|
26
|
+
'author'
|
|
27
|
+
),
|
|
28
|
+
'[áàäâãåÁÀÄÂÃÅ]',
|
|
29
|
+
'a',
|
|
30
|
+
'g'
|
|
31
|
+
),
|
|
32
|
+
'[éèëêÉÈËÊ]',
|
|
33
|
+
'e',
|
|
34
|
+
'g'
|
|
35
|
+
),
|
|
36
|
+
'\s+',
|
|
37
|
+
'-',
|
|
38
|
+
'g'
|
|
39
|
+
)
|
|
40
|
+
)
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
base_slug := regexp_replace(base_slug, '[íìïîÍÌÏÎ]', 'i', 'g');
|
|
44
|
+
base_slug := regexp_replace(base_slug, '[óòöôõÓÒÖÔÕ]', 'o', 'g');
|
|
45
|
+
base_slug := regexp_replace(base_slug, '[úùüûÚÙÜÛ]', 'u', 'g');
|
|
46
|
+
base_slug := regexp_replace(base_slug, '[ñÑ]', 'n', 'g');
|
|
47
|
+
base_slug := regexp_replace(base_slug, '[çÇ]', 'c', 'g');
|
|
48
|
+
base_slug := regexp_replace(base_slug, '[^a-z0-9-]', '', 'g');
|
|
49
|
+
base_slug := trim(both '-' FROM base_slug);
|
|
50
|
+
|
|
51
|
+
IF base_slug IS NULL OR base_slug = '' THEN
|
|
52
|
+
base_slug := 'author';
|
|
53
|
+
END IF;
|
|
54
|
+
|
|
55
|
+
next_slug := base_slug;
|
|
56
|
+
suffix_num := 2;
|
|
57
|
+
|
|
58
|
+
WHILE EXISTS (
|
|
59
|
+
SELECT 1
|
|
60
|
+
FROM plank_users
|
|
61
|
+
WHERE public_author_slug = next_slug
|
|
62
|
+
AND id <> usr.id
|
|
63
|
+
) LOOP
|
|
64
|
+
next_slug := base_slug || '-' || suffix_num;
|
|
65
|
+
suffix_num := suffix_num + 1;
|
|
66
|
+
END LOOP;
|
|
67
|
+
|
|
68
|
+
UPDATE plank_users
|
|
69
|
+
SET public_author_slug = next_slug
|
|
70
|
+
WHERE id = usr.id;
|
|
71
|
+
END LOOP;
|
|
72
|
+
END;
|
|
73
|
+
$$;
|
|
74
|
+
|
|
75
|
+
ALTER TABLE plank_users
|
|
76
|
+
ALTER COLUMN public_author_slug SET NOT NULL;
|
|
77
|
+
|
|
78
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_plank_users_public_author_slug
|
|
79
|
+
ON plank_users(public_author_slug);
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
DO $$
|
|
2
|
+
DECLARE
|
|
3
|
+
usr RECORD;
|
|
4
|
+
base_slug TEXT;
|
|
5
|
+
next_slug TEXT;
|
|
6
|
+
suffix_num INTEGER;
|
|
7
|
+
BEGIN
|
|
8
|
+
FOR usr IN
|
|
9
|
+
SELECT id, email, first_name, last_name
|
|
10
|
+
FROM plank_users
|
|
11
|
+
ORDER BY created_at, id
|
|
12
|
+
LOOP
|
|
13
|
+
base_slug := lower(
|
|
14
|
+
trim(
|
|
15
|
+
both '-'
|
|
16
|
+
FROM regexp_replace(
|
|
17
|
+
regexp_replace(
|
|
18
|
+
regexp_replace(
|
|
19
|
+
coalesce(
|
|
20
|
+
nullif(trim(concat_ws(' ', usr.first_name, usr.last_name)), ''),
|
|
21
|
+
nullif(split_part(usr.email, '@', 1), ''),
|
|
22
|
+
'author'
|
|
23
|
+
),
|
|
24
|
+
'[áàäâãåÁÀÄÂÃÅ]',
|
|
25
|
+
'a',
|
|
26
|
+
'g'
|
|
27
|
+
),
|
|
28
|
+
'[éèëêÉÈËÊ]',
|
|
29
|
+
'e',
|
|
30
|
+
'g'
|
|
31
|
+
),
|
|
32
|
+
'\s+',
|
|
33
|
+
'-',
|
|
34
|
+
'g'
|
|
35
|
+
)
|
|
36
|
+
)
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
base_slug := regexp_replace(base_slug, '[íìïîÍÌÏÎ]', 'i', 'g');
|
|
40
|
+
base_slug := regexp_replace(base_slug, '[óòöôõÓÒÖÔÕ]', 'o', 'g');
|
|
41
|
+
base_slug := regexp_replace(base_slug, '[úùüûÚÙÜÛ]', 'u', 'g');
|
|
42
|
+
base_slug := regexp_replace(base_slug, '[ñÑ]', 'n', 'g');
|
|
43
|
+
base_slug := regexp_replace(base_slug, '[çÇ]', 'c', 'g');
|
|
44
|
+
base_slug := regexp_replace(base_slug, '[^a-z0-9-]', '', 'g');
|
|
45
|
+
base_slug := trim(both '-' FROM base_slug);
|
|
46
|
+
|
|
47
|
+
IF base_slug IS NULL OR base_slug = '' THEN
|
|
48
|
+
base_slug := 'author';
|
|
49
|
+
END IF;
|
|
50
|
+
|
|
51
|
+
next_slug := base_slug || '--' || usr.id;
|
|
52
|
+
|
|
53
|
+
UPDATE plank_users
|
|
54
|
+
SET public_author_slug = next_slug
|
|
55
|
+
WHERE id = usr.id;
|
|
56
|
+
END LOOP;
|
|
57
|
+
|
|
58
|
+
FOR usr IN
|
|
59
|
+
SELECT id, public_author_slug
|
|
60
|
+
FROM plank_users
|
|
61
|
+
ORDER BY created_at, id
|
|
62
|
+
LOOP
|
|
63
|
+
base_slug := split_part(usr.public_author_slug, '--', 1);
|
|
64
|
+
next_slug := base_slug;
|
|
65
|
+
suffix_num := 2;
|
|
66
|
+
|
|
67
|
+
WHILE EXISTS (
|
|
68
|
+
SELECT 1
|
|
69
|
+
FROM plank_users
|
|
70
|
+
WHERE public_author_slug = next_slug
|
|
71
|
+
AND id <> usr.id
|
|
72
|
+
) LOOP
|
|
73
|
+
next_slug := base_slug || '-' || suffix_num;
|
|
74
|
+
suffix_num := suffix_num + 1;
|
|
75
|
+
END LOOP;
|
|
76
|
+
|
|
77
|
+
UPDATE plank_users
|
|
78
|
+
SET public_author_slug = next_slug
|
|
79
|
+
WHERE id = usr.id;
|
|
80
|
+
END LOOP;
|
|
81
|
+
END;
|
|
82
|
+
$$;
|
|
@@ -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
|
}
|
|
@@ -3714,7 +3730,7 @@ var patchEntryStatus = async (req, res) => {
|
|
|
3714
3730
|
UPDATE ${quotedTableName} SET
|
|
3715
3731
|
status = 'published',
|
|
3716
3732
|
published_data = ${buildSnapshotExpr(ct.tableName)},
|
|
3717
|
-
published_at = NOW(),
|
|
3733
|
+
published_at = COALESCE(published_at, NOW()),
|
|
3718
3734
|
scheduled_for = NULL,
|
|
3719
3735
|
review_rejected = FALSE,
|
|
3720
3736
|
updated_at = NOW()
|
|
@@ -3867,10 +3883,40 @@ var deleteEntry = async (req, res) => {
|
|
|
3867
3883
|
import bcrypt2 from "bcryptjs";
|
|
3868
3884
|
import { randomBytes as randomBytes6 } from "crypto";
|
|
3869
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
|
|
3870
3914
|
var CreateUserSchema = z4.object({
|
|
3871
3915
|
email: z4.email(),
|
|
3872
3916
|
password: z4.string().min(8),
|
|
3873
3917
|
roleId: z4.string().min(1),
|
|
3918
|
+
firstName: z4.string().trim().min(1).max(100),
|
|
3919
|
+
lastName: z4.string().trim().min(1).max(100),
|
|
3874
3920
|
enabled: z4.boolean().optional()
|
|
3875
3921
|
});
|
|
3876
3922
|
var UpdateUserSchema = z4.object({
|
|
@@ -3939,7 +3985,7 @@ async function listUsers(_req, res) {
|
|
|
3939
3985
|
res.json(rows);
|
|
3940
3986
|
}
|
|
3941
3987
|
async function getMe(req, res) {
|
|
3942
|
-
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,
|
|
3943
3989
|
u.job_title, u.organization, u.country, u.two_factor_enabled, u.enabled, u.created_at,
|
|
3944
3990
|
r.name AS role_name, r.permissions
|
|
3945
3991
|
FROM plank_users u
|
|
@@ -4018,7 +4064,9 @@ async function disableTwoFactor(req, res) {
|
|
|
4018
4064
|
res.status(400).json({ errors: flattenError4(parsed.error, (i2) => i2.message) });
|
|
4019
4065
|
return;
|
|
4020
4066
|
}
|
|
4021
|
-
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
|
+
]);
|
|
4022
4070
|
const user = rows[0];
|
|
4023
4071
|
if (!user) {
|
|
4024
4072
|
res.status(404).json({ error: "User not found" });
|
|
@@ -4056,7 +4104,9 @@ async function regenerateBackupCodes(req, res) {
|
|
|
4056
4104
|
res.status(400).json({ errors: flattenError4(parsed.error, (i2) => i2.message) });
|
|
4057
4105
|
return;
|
|
4058
4106
|
}
|
|
4059
|
-
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
|
+
]);
|
|
4060
4110
|
const user = rows[0];
|
|
4061
4111
|
if (!user || !user.two_factor_enabled || !user.two_factor_secret) {
|
|
4062
4112
|
res.status(400).json({ error: "2FA is not enabled" });
|
|
@@ -4067,7 +4117,10 @@ async function regenerateBackupCodes(req, res) {
|
|
|
4067
4117
|
res.status(401).json({ error: "Current password is incorrect" });
|
|
4068
4118
|
return;
|
|
4069
4119
|
}
|
|
4070
|
-
const totpResult = d3({
|
|
4120
|
+
const totpResult = d3({
|
|
4121
|
+
token: parsed.data.code,
|
|
4122
|
+
secret: decrypt(user.two_factor_secret)
|
|
4123
|
+
});
|
|
4071
4124
|
if (!totpResult.valid) {
|
|
4072
4125
|
res.status(401).json({ error: "Invalid verification code" });
|
|
4073
4126
|
return;
|
|
@@ -4082,15 +4135,36 @@ async function updateMe(req, res) {
|
|
|
4082
4135
|
return;
|
|
4083
4136
|
}
|
|
4084
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);
|
|
4085
4150
|
const { rows } = await pool_default.query(`UPDATE plank_users
|
|
4086
4151
|
SET first_name = COALESCE($1, first_name),
|
|
4087
4152
|
last_name = COALESCE($2, last_name),
|
|
4088
4153
|
job_title = COALESCE($3, job_title),
|
|
4089
4154
|
organization = COALESCE($4, organization),
|
|
4090
|
-
country = COALESCE($5, country)
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
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
|
+
]);
|
|
4094
4168
|
if (!rows[0]) {
|
|
4095
4169
|
res.status(404).json({ error: "User not found" });
|
|
4096
4170
|
return;
|
|
@@ -4136,7 +4210,9 @@ async function confirmAvatar(req, res) {
|
|
|
4136
4210
|
res.json({ avatarUrl, user: await resolveAvatarUrl(rows[0]) });
|
|
4137
4211
|
}
|
|
4138
4212
|
async function deleteAvatar(req, res) {
|
|
4139
|
-
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
|
+
]);
|
|
4140
4216
|
const current = rows[0]?.avatar_url;
|
|
4141
4217
|
if (current && !current.startsWith("http")) {
|
|
4142
4218
|
const provider = await getProvider();
|
|
@@ -4172,7 +4248,7 @@ async function createUser(req, res) {
|
|
|
4172
4248
|
res.status(400).json({ errors: flattenError4(parsed.error, (i2) => i2.message) });
|
|
4173
4249
|
return;
|
|
4174
4250
|
}
|
|
4175
|
-
const { email, password, roleId, enabled } = parsed.data;
|
|
4251
|
+
const { email, password, roleId, firstName, lastName, enabled } = parsed.data;
|
|
4176
4252
|
const requesterRoleName = await roleNameById(req.user.roleId);
|
|
4177
4253
|
const targetRoleName = await roleNameById(roleId);
|
|
4178
4254
|
if (targetRoleName === "Super Admin" && requesterRoleName !== "Super Admin") {
|
|
@@ -4184,8 +4260,19 @@ async function createUser(req, res) {
|
|
|
4184
4260
|
const nextEnabled = editorialMode || !isEditorialExclusiveRole ? enabled ?? true : false;
|
|
4185
4261
|
const hashed = await bcrypt2.hash(password, 12);
|
|
4186
4262
|
const id = createId();
|
|
4187
|
-
|
|
4188
|
-
|
|
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
|
+
});
|
|
4189
4276
|
}
|
|
4190
4277
|
async function updateUser(req, res) {
|
|
4191
4278
|
const parsed = UpdateUserSchema.safeParse(req.body);
|
|
@@ -4217,18 +4304,41 @@ async function updateUser(req, res) {
|
|
|
4217
4304
|
resolvedEnabled = false;
|
|
4218
4305
|
}
|
|
4219
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);
|
|
4220
4321
|
const { rows } = await pool_default.query(`UPDATE plank_users
|
|
4221
4322
|
SET email = COALESCE($1, email),
|
|
4222
4323
|
role_id = COALESCE($2, role_id),
|
|
4223
4324
|
first_name = COALESCE($3, first_name),
|
|
4224
4325
|
last_name = COALESCE($4, last_name),
|
|
4225
4326
|
enabled = COALESCE($5, enabled),
|
|
4327
|
+
public_author_slug = $6,
|
|
4226
4328
|
session_version = CASE
|
|
4227
4329
|
WHEN $5 IS NOT NULL AND $5 = FALSE AND enabled = TRUE THEN session_version + 1
|
|
4228
4330
|
ELSE session_version
|
|
4229
4331
|
END
|
|
4230
|
-
WHERE id = $
|
|
4231
|
-
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
|
+
]);
|
|
4232
4342
|
if (!rows[0]) {
|
|
4233
4343
|
res.status(404).json({ error: "User not found" });
|
|
4234
4344
|
return;
|
|
@@ -4670,11 +4780,17 @@ async function getAppModes(req, res) {
|
|
|
4670
4780
|
res.json(modes);
|
|
4671
4781
|
}
|
|
4672
4782
|
async function getClientSettings(_req, res) {
|
|
4673
|
-
const settings = await
|
|
4783
|
+
const [settings, previewSettings] = await Promise.all([
|
|
4784
|
+
getSettings("general"),
|
|
4785
|
+
getSettings("preview")
|
|
4786
|
+
]);
|
|
4674
4787
|
res.json({
|
|
4675
4788
|
timezone: settings.timezone ?? "UTC",
|
|
4676
4789
|
locales: settings.locales ?? '["en"]',
|
|
4677
|
-
default_locale: settings.default_locale ?? "en"
|
|
4790
|
+
default_locale: settings.default_locale ?? "en",
|
|
4791
|
+
preview_enabled: previewSettings.enabled ?? "false",
|
|
4792
|
+
preview_url_template: previewSettings.url_template ?? "",
|
|
4793
|
+
preview_slug_field: previewSettings.slug_field ?? "slug"
|
|
4678
4794
|
});
|
|
4679
4795
|
}
|
|
4680
4796
|
async function getEditorialMode(_req, res) {
|
|
@@ -5138,6 +5254,23 @@ async function resolveAuthorAvatars(entries) {
|
|
|
5138
5254
|
}
|
|
5139
5255
|
}));
|
|
5140
5256
|
}
|
|
5257
|
+
async function resolvePublicAuthorAvatar(author) {
|
|
5258
|
+
if (!author.avatar_url || author.avatar_url.startsWith("http"))
|
|
5259
|
+
return author;
|
|
5260
|
+
const provider = await getProvider();
|
|
5261
|
+
return { ...author, avatar_url: await provider.getUrl(author.avatar_url) };
|
|
5262
|
+
}
|
|
5263
|
+
function serializePublicAuthor(row) {
|
|
5264
|
+
return {
|
|
5265
|
+
first_name: row.first_name,
|
|
5266
|
+
last_name: row.last_name,
|
|
5267
|
+
avatar_url: row.avatar_url,
|
|
5268
|
+
job_title: row.job_title,
|
|
5269
|
+
organization: row.organization,
|
|
5270
|
+
country: row.country,
|
|
5271
|
+
slug: row.public_author_slug
|
|
5272
|
+
};
|
|
5273
|
+
}
|
|
5141
5274
|
function serializeEntry(row, ct, statusParam, locale, fallbacks = []) {
|
|
5142
5275
|
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;
|
|
5143
5276
|
const source = statusParam === "published" && published_data ? published_data : rest;
|
|
@@ -5188,7 +5321,8 @@ function serializeEntry(row, ct, statusParam, locale, fallbacks = []) {
|
|
|
5188
5321
|
avatar_url: _author_avatar_url ?? null,
|
|
5189
5322
|
job_title: _author_job_title ?? null,
|
|
5190
5323
|
organization: _author_organization ?? null,
|
|
5191
|
-
country: _author_country ?? null
|
|
5324
|
+
country: _author_country ?? null,
|
|
5325
|
+
slug: row._author_slug ?? null
|
|
5192
5326
|
} : null;
|
|
5193
5327
|
out.editor = _editor_first_name || _editor_last_name ? {
|
|
5194
5328
|
first_name: _editor_first_name ?? null,
|
|
@@ -5196,7 +5330,8 @@ function serializeEntry(row, ct, statusParam, locale, fallbacks = []) {
|
|
|
5196
5330
|
avatar_url: _editor_avatar_url ?? null,
|
|
5197
5331
|
job_title: _editor_job_title ?? null,
|
|
5198
5332
|
organization: _editor_organization ?? null,
|
|
5199
|
-
country: _editor_country ?? null
|
|
5333
|
+
country: _editor_country ?? null,
|
|
5334
|
+
slug: row._editor_slug ?? null
|
|
5200
5335
|
} : null;
|
|
5201
5336
|
return out;
|
|
5202
5337
|
}
|
|
@@ -5218,8 +5353,8 @@ var listPublicEntries = async (req, res) => {
|
|
|
5218
5353
|
const fallbacks2 = req.query.fallback ? String(req.query.fallback).split(",") : [];
|
|
5219
5354
|
const statusClause = statusParam2 === "published" || statusParam2 === "draft" ? `WHERE e.status = $1` : "";
|
|
5220
5355
|
const values = statusClause ? [statusParam2] : [];
|
|
5221
|
-
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,
|
|
5222
|
-
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
|
|
5356
|
+
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,
|
|
5357
|
+
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
|
|
5223
5358
|
FROM ${ct.tableName} e
|
|
5224
5359
|
LEFT JOIN plank_users u ON u.id = e.created_by
|
|
5225
5360
|
LEFT JOIN plank_users ed ON ed.id = e.editor_id
|
|
@@ -5257,6 +5392,11 @@ var listPublicEntries = async (req, res) => {
|
|
|
5257
5392
|
filterClauses.push(`e.status = $${filterValues.length + 1}`);
|
|
5258
5393
|
filterValues.push(statusParam);
|
|
5259
5394
|
}
|
|
5395
|
+
const authorSlug = typeof req.query.author === "string" ? req.query.author.trim() : "";
|
|
5396
|
+
if (authorSlug) {
|
|
5397
|
+
filterClauses.push(`u.public_author_slug = $${filterValues.length + 1}`);
|
|
5398
|
+
filterValues.push(authorSlug);
|
|
5399
|
+
}
|
|
5260
5400
|
const rawSort = String(req.query.sort ?? "created_at");
|
|
5261
5401
|
const sortField = knownFields.has(rawSort) || systemSortFields.has(rawSort) ? rawSort : "created_at";
|
|
5262
5402
|
assertSafeIdentifier(sortField);
|
|
@@ -5279,8 +5419,8 @@ var listPublicEntries = async (req, res) => {
|
|
|
5279
5419
|
filterClauses.push(parsedFilter.operator === "nin" ? `NOT (e.${fieldName} = ANY($${filterValues.length + 1}))` : `e.${fieldName} = ANY($${filterValues.length + 1})`);
|
|
5280
5420
|
filterValues.push(coercedValues);
|
|
5281
5421
|
}
|
|
5282
|
-
for (const [key
|
|
5283
|
-
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["))
|
|
5422
|
+
for (const [key] of Object.entries(req.query)) {
|
|
5423
|
+
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["))
|
|
5284
5424
|
continue;
|
|
5285
5425
|
if (knownFields.has(key))
|
|
5286
5426
|
continue;
|
|
@@ -5291,13 +5431,16 @@ var listPublicEntries = async (req, res) => {
|
|
|
5291
5431
|
const limitParam = filterValues.length + 1;
|
|
5292
5432
|
const offsetParam = filterValues.length + 2;
|
|
5293
5433
|
const [{ rows }, { rows: countRows }] = await Promise.all([
|
|
5294
|
-
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,
|
|
5295
|
-
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
|
|
5434
|
+
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,
|
|
5435
|
+
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
|
|
5296
5436
|
FROM ${ct.tableName} e
|
|
5297
5437
|
LEFT JOIN plank_users u ON u.id = e.created_by
|
|
5298
5438
|
LEFT JOIN plank_users ed ON ed.id = e.editor_id
|
|
5299
5439
|
${where} ORDER BY e.${sortField} ${sortDir} LIMIT $${limitParam} OFFSET $${offsetParam}`, [...filterValues, limit, offset]),
|
|
5300
|
-
pool_default.query(`SELECT COUNT(*) as count
|
|
5440
|
+
pool_default.query(`SELECT COUNT(*) as count
|
|
5441
|
+
FROM ${ct.tableName} e
|
|
5442
|
+
LEFT JOIN plank_users u ON u.id = e.created_by
|
|
5443
|
+
${where}`, filterValues)
|
|
5301
5444
|
]);
|
|
5302
5445
|
const data = rows.map((row) => serializeEntry(row, ct, statusParam, locale, fallbacks));
|
|
5303
5446
|
await Promise.all([
|
|
@@ -5327,8 +5470,8 @@ var getPublicEntry = async (req, res) => {
|
|
|
5327
5470
|
const statusParam = String(req.query.status ?? "published");
|
|
5328
5471
|
const statusClause = statusParam === "published" || statusParam === "draft" ? ` AND e.status = $2` : "";
|
|
5329
5472
|
const values = statusClause ? [req.params.id, statusParam] : [req.params.id];
|
|
5330
|
-
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,
|
|
5331
|
-
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
|
|
5473
|
+
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,
|
|
5474
|
+
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
|
|
5332
5475
|
FROM ${ct.tableName} e
|
|
5333
5476
|
LEFT JOIN plank_users u ON u.id = e.created_by
|
|
5334
5477
|
LEFT JOIN plank_users ed ON ed.id = e.editor_id
|
|
@@ -5347,10 +5490,22 @@ var getPublicEntry = async (req, res) => {
|
|
|
5347
5490
|
]);
|
|
5348
5491
|
res.json(selectEntryFields(entry, ct, selection));
|
|
5349
5492
|
};
|
|
5493
|
+
var getPublicAuthor = async (req, res) => {
|
|
5494
|
+
const { rows } = await pool_default.query(`SELECT public_author_slug, first_name, last_name, avatar_url, job_title, organization, country
|
|
5495
|
+
FROM plank_users
|
|
5496
|
+
WHERE public_author_slug = $1
|
|
5497
|
+
LIMIT 1`, [req.params.slug]);
|
|
5498
|
+
if (!rows[0]) {
|
|
5499
|
+
res.status(404).json({ error: "Not found" });
|
|
5500
|
+
return;
|
|
5501
|
+
}
|
|
5502
|
+
res.json(await resolvePublicAuthorAvatar(serializePublicAuthor(rows[0])));
|
|
5503
|
+
};
|
|
5350
5504
|
|
|
5351
5505
|
// ../core/dist/routes/public.js
|
|
5352
5506
|
var router3 = Router3();
|
|
5353
5507
|
router3.use(apiToken);
|
|
5508
|
+
router3.get("/authors/:slug", getPublicAuthor);
|
|
5354
5509
|
router3.get("/:slug", listPublicEntries);
|
|
5355
5510
|
router3.get("/:slug/:id", getPublicEntry);
|
|
5356
5511
|
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.24.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/
|
|
59
|
-
"@plank-cms/
|
|
60
|
-
"@plank-cms/schema": "0.
|
|
58
|
+
"@plank-cms/core": "0.24.0",
|
|
59
|
+
"@plank-cms/db": "0.24.0",
|
|
60
|
+
"@plank-cms/schema": "0.24.0"
|
|
61
61
|
},
|
|
62
62
|
"scripts": {
|
|
63
63
|
"build": "tsup",
|