diff --git a/install/package.json b/install/package.json
index a477f60e06..76954ea502 100644
--- a/install/package.json
+++ b/install/package.json
@@ -107,10 +107,10 @@
"nodebb-plugin-ntfy": "1.7.7",
"nodebb-plugin-spam-be-gone": "2.2.2",
"nodebb-rewards-essentials": "1.0.0",
- "nodebb-theme-harmony": "1.2.87",
+ "nodebb-theme-harmony": "1.2.88",
"nodebb-theme-lavender": "7.1.16",
- "nodebb-theme-peace": "2.2.21",
- "nodebb-theme-persona": "13.3.53",
+ "nodebb-theme-peace": "2.2.22",
+ "nodebb-theme-persona": "13.3.54",
"nodebb-widget-essentials": "7.0.31",
"nodemailer": "6.9.16",
"nprogress": "0.2.0",
diff --git a/public/language/en-GB/admin/settings/reputation.json b/public/language/en-GB/admin/settings/reputation.json
index 479069e3a4..a4fde0d0af 100644
--- a/public/language/en-GB/admin/settings/reputation.json
+++ b/public/language/en-GB/admin/settings/reputation.json
@@ -20,7 +20,6 @@
"min-rep-chat": "Minimum reputation to send chat messages",
"min-rep-post-links": "Minimum reputation to post links",
"min-rep-flag": "Minimum reputation to flag posts",
- "min-rep-website": "Minimum reputation to add \"Website\" to user profile",
"min-rep-aboutme": "Minimum reputation to add \"About me\" to user profile",
"min-rep-signature": "Minimum reputation to add \"Signature\" to user profile",
"min-rep-profile-picture": "Minimum reputation to add \"Profile Picture\" to user profile",
diff --git a/public/language/en-GB/error.json b/public/language/en-GB/error.json
index 5ddc42d596..5f69e9ffc5 100644
--- a/public/language/en-GB/error.json
+++ b/public/language/en-GB/error.json
@@ -211,6 +211,7 @@
"not-enough-reputation-custom-field": "You need %1 reputation for %2",
"custom-user-field-value-too-long": "Custom field value too long, %1",
"custom-user-field-select-value-invalid": "Custom field selected option is invalid, %1",
+ "custom-user-field-invalid-text": "Custom field text is invalid, %1",
"custom-user-field-invalid-link": "Custom field link is invalid, %1",
"custom-user-field-invalid-number": "Custom field number is invalid, %1",
"custom-user-field-invalid-date": "Custom field date is invalid, %1",
diff --git a/public/openapi/components/schemas/UserObject.yaml b/public/openapi/components/schemas/UserObject.yaml
index d86cecbaca..1d0dee6081 100644
--- a/public/openapi/components/schemas/UserObject.yaml
+++ b/public/openapi/components/schemas/UserObject.yaml
@@ -41,19 +41,11 @@ UserObject:
type: string
description: This is either username or fullname depending on forum and user settings
example: Dragon Fruit
- location:
- type: string
- example: 'Toronto, Canada'
- nullable: true
birthday:
type: string
description: A birthdate given in an ISO format parseable by the Date object
example: 03/27/2020
nullable: true
- website:
- type: string
- example: 'https://example.org'
- nullable: true
aboutme:
type: string
example: |
@@ -172,9 +164,7 @@ UserObject:
- joindate
- lastonline
- picture
- - location
- birthday
- - website
- aboutme
- signature
- uploadedpicture
@@ -245,16 +235,10 @@ UserObjectFull:
type: string
description: This is either username or fullname depending on forum and user settings
example: Dragon Fruit
- location:
- type: string
- example: 'Toronto, Canada'
birthday:
type: string
description: A birthdate given in an ISO format parseable by the Date object
example: 03/27/2020
- website:
- type: string
- example: 'https://example.org'
aboutme:
type: string
example: |
@@ -508,10 +492,6 @@ UserObjectFull:
- name
- visibility
- public
- websiteLink:
- type: string
- websiteName:
- type: string
username:disableEdit:
type: number
email:disableEdit:
diff --git a/public/openapi/read/admin/manage/users.yaml b/public/openapi/read/admin/manage/users.yaml
index 80dbed1728..148a44f56c 100644
--- a/public/openapi/read/admin/manage/users.yaml
+++ b/public/openapi/read/admin/manage/users.yaml
@@ -15,6 +15,9 @@ get:
type: array
items:
$ref: ../../../components/schemas/UserObject.yaml#/UserObjectACP
+ customUserFields:
+ type: array
+ description: array of custom user fields
page:
type: number
pageCount:
diff --git a/public/openapi/read/user/userslug/edit.yaml b/public/openapi/read/user/userslug/edit.yaml
index e7fdb560a0..32c476c99f 100644
--- a/public/openapi/read/user/userslug/edit.yaml
+++ b/public/openapi/read/user/userslug/edit.yaml
@@ -37,8 +37,6 @@ get:
type: boolean
allowAccountDelete:
type: boolean
- allowWebsite:
- type: boolean
allowAboutMe:
type: boolean
allowSignature:
diff --git a/public/src/admin/manage/users.js b/public/src/admin/manage/users.js
index 0f8dffaa29..a3bca173b0 100644
--- a/public/src/admin/manage/users.js
+++ b/public/src/admin/manage/users.js
@@ -43,12 +43,15 @@ define('admin/manage/users', [
{ label: '[[admin/manage/users:export-field-followercount]]', field: 'followerCount', selected: false },
{ label: '[[admin/manage/users:export-field-followingcount]]', field: 'followingCount', selected: false },
{ label: '[[admin/manage/users:export-field-fullname]]', field: 'fullname', selected: false },
- { label: '[[admin/manage/users:export-field-website]]', field: 'website', selected: false },
- { label: '[[admin/manage/users:export-field-location]]', field: 'location', selected: false },
{ label: '[[admin/manage/users:export-field-birthday]]', field: 'birthday', selected: false },
{ label: '[[admin/manage/users:export-field-signature]]', field: 'signature', selected: false },
{ label: '[[admin/manage/users:export-field-aboutme]]', field: 'aboutme', selected: false },
- ];
+ ].concat(ajaxify.data.customUserFields.map(field => ({
+ label: field.name,
+ field: field.key,
+ selected: false,
+ })));
+
const options = defaultFields.map((field, i) => (`
diff --git a/src/controllers/accounts/edit.js b/src/controllers/accounts/edit.js
index c687279698..2326361fdc 100644
--- a/src/controllers/accounts/edit.js
+++ b/src/controllers/accounts/edit.js
@@ -38,7 +38,6 @@ editController.get = async function (req, res, next) {
userData.maximumProfileImageSize = meta.config.maximumProfileImageSize;
userData.allowMultipleBadges = meta.config.allowMultipleBadges === 1;
userData.allowAccountDelete = meta.config.allowAccountDelete === 1;
- userData.allowWebsite = !isSelf || !!meta.config['reputation:disabled'] || reputation >= meta.config['min:rep:website'];
userData.allowAboutMe = !isSelf || !!meta.config['reputation:disabled'] || reputation >= meta.config['min:rep:aboutme'];
userData.allowSignature = canUseSignature && (!isSelf || !!meta.config['reputation:disabled'] || reputation >= meta.config['min:rep:signature']);
userData.profileImageDimension = meta.config.profileImageDimension;
diff --git a/src/controllers/accounts/helpers.js b/src/controllers/accounts/helpers.js
index 89e7013411..72ef70b9bd 100644
--- a/src/controllers/accounts/helpers.js
+++ b/src/controllers/accounts/helpers.js
@@ -104,12 +104,7 @@ helpers.getUserDataByUserSlug = async function (userslug, callerUID, query = {})
userData.banned = Boolean(userData.banned);
userData.muted = parseInt(userData.mutedUntil, 10) > Date.now();
- userData.website = escape(userData.website);
- userData.websiteLink = !userData.website.startsWith('http') ? `http://${userData.website}` : userData.website;
- userData.websiteName = userData.website.replace(validator.escape('http://'), '').replace(validator.escape('https://'), '');
-
userData.fullname = escape(userData.fullname);
- userData.location = escape(userData.location);
userData.signature = escape(userData.signature);
userData.birthday = validator.escape(String(userData.birthday || ''));
userData.moderationNote = validator.escape(String(userData.moderationNote || ''));
@@ -148,6 +143,9 @@ helpers.getCustomUserFields = async function (userData) {
if (f.type === 'select-multi' && userValue) {
userValue = JSON.parse(userValue || '[]');
}
+ if (f.type === 'input-link' && userValue) {
+ f.linkValue = validator.escape(String(userValue.replace('http://', '').replace('https://', '')));
+ }
f['select-options'] = f['select-options'].split('\n').filter(Boolean).map(
opt => ({
value: opt,
diff --git a/src/controllers/admin/users.js b/src/controllers/admin/users.js
index a372342640..db1be70157 100644
--- a/src/controllers/admin/users.js
+++ b/src/controllers/admin/users.js
@@ -94,9 +94,10 @@ async function getUsers(req, res) {
const set = buildSet();
const uids = await getUids(set);
- const [count, users] = await Promise.all([
+ const [count, users, customUserFields] = await Promise.all([
getCount(set),
loadUserInfo(req.uid, uids),
+ getCustomUserFields(),
]);
await render(req, res, {
@@ -106,9 +107,15 @@ async function getUsers(req, res) {
resultsPerPage: resultsPerPage,
reverse: reverse,
sortBy: sortBy,
+ customUserFields,
});
}
+async function getCustomUserFields() {
+ const keys = await db.getSortedSetRange('user-custom-fields', 0, -1);
+ return (await db.getObjects(keys.map(k => `user-custom-field:${k}`))).filter(Boolean);
+}
+
usersController.search = async function (req, res) {
const sortDirection = req.query.sortDirection || 'desc';
const reverse = sortDirection === 'desc';
diff --git a/src/upgrades/3.11.0/default-custom-profile-fields.js b/src/upgrades/3.11.0/default-custom-profile-fields.js
new file mode 100644
index 0000000000..1739586f94
--- /dev/null
+++ b/src/upgrades/3.11.0/default-custom-profile-fields.js
@@ -0,0 +1,44 @@
+/* eslint-disable no-await-in-loop */
+
+'use strict';
+
+const db = require('../../database');
+
+module.exports = {
+ name: 'Add website and location as custom profile fields',
+ timestamp: Date.UTC(2024, 10, 25),
+ method: async function () {
+ const minRepWebsite = parseInt(await db.getObjectField('config', 'min:rep:website'), 10) || 0;
+
+ const website = {
+ icon: 'fa-solid fa-globe',
+ key: 'website',
+ 'min:rep': minRepWebsite,
+ name: '[[user:website]]',
+ 'select-options': '',
+ type: 'input-link',
+ };
+
+ const location = {
+ icon: 'fa-solid fa-map-pin',
+ key: 'location',
+ 'min:rep': 0,
+ name: '[[user:location]]',
+ 'select-options': '',
+ type: 'input-text',
+ };
+
+ await db.sortedSetAdd(
+ `user-custom-fields`,
+ [0, 1],
+ ['website', 'location']
+ );
+
+ await db.setObjectBulk([
+ [`user-custom-field:website`, website],
+ [`user-custom-field:location`, location],
+ ]);
+
+ await db.deleteObjectField('config', 'min:rep:website');
+ },
+};
diff --git a/src/user/data.js b/src/user/data.js
index 037b13f887..491501a6d6 100644
--- a/src/user/data.js
+++ b/src/user/data.js
@@ -21,7 +21,7 @@ const intFields = [
module.exports = function (User) {
const fieldWhitelist = [
'uid', 'username', 'userslug', 'email', 'email:confirmed', 'joindate',
- 'lastonline', 'picture', 'icon:bgColor', 'fullname', 'location', 'birthday', 'website',
+ 'lastonline', 'picture', 'icon:bgColor', 'fullname', 'birthday',
'aboutme', 'signature', 'uploadedpicture', 'profileviews', 'reputation',
'postcount', 'topiccount', 'lastposttime', 'banned', 'banned:expire',
'status', 'flags', 'followerCount', 'followingCount', 'cover:url',
diff --git a/src/user/profile.js b/src/user/profile.js
index ebf17f354e..7869f14410 100644
--- a/src/user/profile.js
+++ b/src/user/profile.js
@@ -77,11 +77,9 @@ module.exports = function (User) {
async function validateData(callerUid, data) {
await isEmailValid(data);
await isUsernameAvailable(data, data.uid);
- await isWebsiteValid(callerUid, data);
await isAboutMeValid(callerUid, data);
await isSignatureValid(callerUid, data);
isFullnameValid(data);
- isLocationValid(data);
isBirthdayValid(data);
isGroupTitleValid(data);
await validateCustomFields(data);
@@ -113,6 +111,10 @@ module.exports = function (User) {
throw new Error(tx.compile(
'error:custom-user-field-invalid-number', field.name
));
+ } else if (value && type === 'input-text' && validator.isURL(value)) {
+ throw new Error(tx.compile(
+ 'error:custom-user-field-invalid-text', field.name
+ ));
} else if (value && type === 'input-date' && !validator.isDate(value)) {
throw new Error(tx.compile(
'error:custom-user-field-invalid-date', field.name
@@ -197,16 +199,6 @@ module.exports = function (User) {
}
User.checkUsername = async username => isUsernameAvailable({ username });
- async function isWebsiteValid(callerUid, data) {
- if (!data.website) {
- return;
- }
- if (data.website.length > 255) {
- throw new Error('[[error:invalid-website]]');
- }
- await User.checkMinReputation(callerUid, data.uid, 'min:rep:website');
- }
-
async function isAboutMeValid(callerUid, data) {
if (!data.aboutme) {
return;
@@ -235,12 +227,6 @@ module.exports = function (User) {
}
}
- function isLocationValid(data) {
- if (data.location && (validator.isURL(data.location) || data.location.length > 255)) {
- throw new Error('[[error:invalid-location]]');
- }
- }
-
function isBirthdayValid(data) {
if (!data.birthday) {
return;
diff --git a/src/views/admin/settings/reputation.tpl b/src/views/admin/settings/reputation.tpl
index daedbd76cb..10e72b6f47 100644
--- a/src/views/admin/settings/reputation.tpl
+++ b/src/views/admin/settings/reputation.tpl
@@ -73,10 +73,6 @@
-
-
-
-