@plank-cms/plank 0.22.0 → 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-dWzhX4Ev.js → index-C86iT0Bf.js} +2 -2
- 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-M2KTICST.js} +176 -27
- 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-C86iT0Bf.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.23.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-M2KTICST.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;
|
|
@@ -5138,6 +5248,23 @@ async function resolveAuthorAvatars(entries) {
|
|
|
5138
5248
|
}
|
|
5139
5249
|
}));
|
|
5140
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
|
+
}
|
|
5141
5268
|
function serializeEntry(row, ct, statusParam, locale, fallbacks = []) {
|
|
5142
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;
|
|
5143
5270
|
const source = statusParam === "published" && published_data ? published_data : rest;
|
|
@@ -5188,7 +5315,8 @@ function serializeEntry(row, ct, statusParam, locale, fallbacks = []) {
|
|
|
5188
5315
|
avatar_url: _author_avatar_url ?? null,
|
|
5189
5316
|
job_title: _author_job_title ?? null,
|
|
5190
5317
|
organization: _author_organization ?? null,
|
|
5191
|
-
country: _author_country ?? null
|
|
5318
|
+
country: _author_country ?? null,
|
|
5319
|
+
slug: row._author_slug ?? null
|
|
5192
5320
|
} : null;
|
|
5193
5321
|
out.editor = _editor_first_name || _editor_last_name ? {
|
|
5194
5322
|
first_name: _editor_first_name ?? null,
|
|
@@ -5196,7 +5324,8 @@ function serializeEntry(row, ct, statusParam, locale, fallbacks = []) {
|
|
|
5196
5324
|
avatar_url: _editor_avatar_url ?? null,
|
|
5197
5325
|
job_title: _editor_job_title ?? null,
|
|
5198
5326
|
organization: _editor_organization ?? null,
|
|
5199
|
-
country: _editor_country ?? null
|
|
5327
|
+
country: _editor_country ?? null,
|
|
5328
|
+
slug: row._editor_slug ?? null
|
|
5200
5329
|
} : null;
|
|
5201
5330
|
return out;
|
|
5202
5331
|
}
|
|
@@ -5218,8 +5347,8 @@ var listPublicEntries = async (req, res) => {
|
|
|
5218
5347
|
const fallbacks2 = req.query.fallback ? String(req.query.fallback).split(",") : [];
|
|
5219
5348
|
const statusClause = statusParam2 === "published" || statusParam2 === "draft" ? `WHERE e.status = $1` : "";
|
|
5220
5349
|
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
|
|
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
|
|
5223
5352
|
FROM ${ct.tableName} e
|
|
5224
5353
|
LEFT JOIN plank_users u ON u.id = e.created_by
|
|
5225
5354
|
LEFT JOIN plank_users ed ON ed.id = e.editor_id
|
|
@@ -5257,6 +5386,11 @@ var listPublicEntries = async (req, res) => {
|
|
|
5257
5386
|
filterClauses.push(`e.status = $${filterValues.length + 1}`);
|
|
5258
5387
|
filterValues.push(statusParam);
|
|
5259
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
|
+
}
|
|
5260
5394
|
const rawSort = String(req.query.sort ?? "created_at");
|
|
5261
5395
|
const sortField = knownFields.has(rawSort) || systemSortFields.has(rawSort) ? rawSort : "created_at";
|
|
5262
5396
|
assertSafeIdentifier(sortField);
|
|
@@ -5279,8 +5413,8 @@ var listPublicEntries = async (req, res) => {
|
|
|
5279
5413
|
filterClauses.push(parsedFilter.operator === "nin" ? `NOT (e.${fieldName} = ANY($${filterValues.length + 1}))` : `e.${fieldName} = ANY($${filterValues.length + 1})`);
|
|
5280
5414
|
filterValues.push(coercedValues);
|
|
5281
5415
|
}
|
|
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["))
|
|
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["))
|
|
5284
5418
|
continue;
|
|
5285
5419
|
if (knownFields.has(key))
|
|
5286
5420
|
continue;
|
|
@@ -5291,13 +5425,16 @@ var listPublicEntries = async (req, res) => {
|
|
|
5291
5425
|
const limitParam = filterValues.length + 1;
|
|
5292
5426
|
const offsetParam = filterValues.length + 2;
|
|
5293
5427
|
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
|
|
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
|
|
5296
5430
|
FROM ${ct.tableName} e
|
|
5297
5431
|
LEFT JOIN plank_users u ON u.id = e.created_by
|
|
5298
5432
|
LEFT JOIN plank_users ed ON ed.id = e.editor_id
|
|
5299
5433
|
${where} ORDER BY e.${sortField} ${sortDir} LIMIT $${limitParam} OFFSET $${offsetParam}`, [...filterValues, limit, offset]),
|
|
5300
|
-
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)
|
|
5301
5438
|
]);
|
|
5302
5439
|
const data = rows.map((row) => serializeEntry(row, ct, statusParam, locale, fallbacks));
|
|
5303
5440
|
await Promise.all([
|
|
@@ -5327,8 +5464,8 @@ var getPublicEntry = async (req, res) => {
|
|
|
5327
5464
|
const statusParam = String(req.query.status ?? "published");
|
|
5328
5465
|
const statusClause = statusParam === "published" || statusParam === "draft" ? ` AND e.status = $2` : "";
|
|
5329
5466
|
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
|
|
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
|
|
5332
5469
|
FROM ${ct.tableName} e
|
|
5333
5470
|
LEFT JOIN plank_users u ON u.id = e.created_by
|
|
5334
5471
|
LEFT JOIN plank_users ed ON ed.id = e.editor_id
|
|
@@ -5347,10 +5484,22 @@ var getPublicEntry = async (req, res) => {
|
|
|
5347
5484
|
]);
|
|
5348
5485
|
res.json(selectEntryFields(entry, ct, selection));
|
|
5349
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
|
+
};
|
|
5350
5498
|
|
|
5351
5499
|
// ../core/dist/routes/public.js
|
|
5352
5500
|
var router3 = Router3();
|
|
5353
5501
|
router3.use(apiToken);
|
|
5502
|
+
router3.get("/authors/:slug", getPublicAuthor);
|
|
5354
5503
|
router3.get("/:slug", listPublicEntries);
|
|
5355
5504
|
router3.get("/:slug/:id", getPublicEntry);
|
|
5356
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/
|
|
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",
|