diff --git a/public/src/client/register.js b/public/src/client/register.js
index 62dbc41..28ca45e 100644
--- a/public/src/client/register.js
+++ b/public/src/client/register.js
@@ -1,209 +1,243 @@
'use strict';
-
define('forum/register', [
- 'translator', 'slugify', 'api', 'bootbox', 'forum/login', 'zxcvbn', 'jquery-form',
+ 'translator', 'slugify', 'api', 'bootbox', 'forum/login', 'zxcvbn', 'jquery-form',
], function (translator, slugify, api, bootbox, Login, zxcvbn) {
- const Register = {};
- let validationError = false;
- const successIcon = '';
-
- Register.init = function () {
- const username = $('#username');
- const password = $('#password');
- const password_confirm = $('#password-confirm');
- const register = $('#register');
-
- handleLanguageOverride();
-
- $('#content #noscript').val('false');
-
- const query = utils.params();
- if (query.token) {
- $('#token').val(query.token);
- }
-
- // Update the "others can mention you via" text
- username.on('keyup', function () {
- $('#yourUsername').text(this.value.length > 0 ? slugify(this.value) : 'username');
- });
-
- username.on('blur', function () {
- if (username.val().length) {
- validateUsername(username.val());
- }
- });
-
- password.on('blur', function () {
- if (password.val().length) {
- validatePassword(password.val(), password_confirm.val());
- }
- });
-
- password_confirm.on('blur', function () {
- if (password_confirm.val().length) {
- validatePasswordConfirm(password.val(), password_confirm.val());
- }
- });
-
- function validateForm(callback) {
- validationError = false;
- validatePassword(password.val(), password_confirm.val());
- validatePasswordConfirm(password.val(), password_confirm.val());
- validateUsername(username.val(), callback);
- }
-
- // Guard against caps lock
- Login.capsLockCheck(document.querySelector('#password'), document.querySelector('#caps-lock-warning'));
-
- register.on('click', function (e) {
- const registerBtn = $(this);
- const errorEl = $('#register-error-notify');
- errorEl.addClass('hidden');
- e.preventDefault();
- validateForm(function () {
- if (validationError) {
- return;
- }
-
- registerBtn.addClass('disabled');
-
- registerBtn.parents('form').ajaxSubmit({
- headers: {
- 'x-csrf-token': config.csrf_token,
- },
- success: function (data) {
- registerBtn.removeClass('disabled');
- if (!data) {
- return;
- }
- if (data.next) {
- const pathname = utils.urlToLocation(data.next).pathname;
-
- const params = utils.params({ url: data.next });
- params.registered = true;
- const qs = decodeURIComponent($.param(params));
-
- window.location.href = pathname + '?' + qs;
- } else if (data.message) {
- translator.translate(data.message, function (msg) {
- bootbox.alert(msg);
- ajaxify.go('/');
- });
- }
- },
- error: function (data) {
- translator.translate(data.responseText, config.defaultLang, function (translated) {
- if (data.status === 403 && data.responseText === 'Forbidden') {
- window.location.href = config.relative_path + '/register?error=csrf-invalid';
- } else {
- errorEl.find('p').text(translated);
- errorEl.removeClass('hidden');
- registerBtn.removeClass('disabled');
- }
- });
- },
- });
- });
- });
-
- // Set initial focus
- $('#username').focus();
- };
-
- function validateUsername(username, callback) {
- callback = callback || function () {};
-
- const username_notify = $('#username-notify');
- const userslug = slugify(username);
- if (username.length < ajaxify.data.minimumUsernameLength ||
- userslug.length < ajaxify.data.minimumUsernameLength) {
- showError(username_notify, '[[error:username-too-short]]');
- } else if (username.length > ajaxify.data.maximumUsernameLength) {
- showError(username_notify, '[[error:username-too-long]]');
- } else if (!utils.isUserNameValid(username) || !userslug) {
- showError(username_notify, '[[error:invalid-username]]');
- } else {
- Promise.allSettled([
- api.head(`/users/bySlug/${username}`, {}),
- api.head(`/groups/${username}`, {}),
- ]).then((results) => {
- if (results.every(obj => obj.status === 'rejected')) {
- showSuccess(username_notify, successIcon);
- } else {
- showError(username_notify, '[[error:username-taken]]');
- }
-
- callback();
- });
- }
- }
-
- function validatePassword(password, password_confirm) {
- const password_notify = $('#password-notify');
- const password_confirm_notify = $('#password-confirm-notify');
-
- try {
- utils.assertPasswordValidity(password, zxcvbn);
-
- if (password === $('#username').val()) {
- throw new Error('[[user:password_same_as_username]]');
- }
-
- showSuccess(password_notify, successIcon);
- } catch (err) {
- showError(password_notify, err.message);
- }
-
- if (password !== password_confirm && password_confirm !== '') {
- showError(password_confirm_notify, '[[user:change_password_error_match]]');
- }
- }
-
- function validatePasswordConfirm(password, password_confirm) {
- const password_notify = $('#password-notify');
- const password_confirm_notify = $('#password-confirm-notify');
-
- if (!password || password_notify.hasClass('alert-error')) {
- return;
- }
-
- if (password !== password_confirm) {
- showError(password_confirm_notify, '[[user:change_password_error_match]]');
- } else {
- showSuccess(password_confirm_notify, successIcon);
- }
- }
-
- function showError(element, msg) {
- translator.translate(msg, function (msg) {
- element.html(msg);
- element.parent()
- .removeClass('register-success')
- .addClass('register-danger');
- element.show();
- });
- validationError = true;
- }
-
- function showSuccess(element, msg) {
- translator.translate(msg, function (msg) {
- element.html(msg);
- element.parent()
- .removeClass('register-danger')
- .addClass('register-success');
- element.show();
- });
- }
-
- function handleLanguageOverride() {
- if (!app.user.uid && config.defaultLang !== config.userLang) {
- const formEl = $('[component="register/local"]');
- const langEl = $('');
-
- formEl.append(langEl);
- }
- }
-
- return Register;
+ const Register = {};
+ let validationError = false;
+ const successIcon = '';
+
+ Register.init = function () {
+ const username = $('#username');
+ const password = $('#password');
+ const password_confirm = $('#password-confirm');
+ const register = $('#register');
+
+ handleLanguageOverride();
+
+ $('#content #noscript').val('false');
+
+ const query = utils.params();
+ if (query.token) {
+ $('#token').val(query.token);
+ }
+
+ // Update the "others can mention you via" text
+ username.on('keyup', function () {
+ $('#yourUsername').text(this.value.length > 0 ? slugify(this.value) : 'username');
+ });
+
+ username.on('blur', function () {
+ if (username.val().length) {
+ validateUsername(username.val());
+ }
+ });
+
+ password.on('blur', function () {
+ if (password.val().length) {
+ validatePassword(password.val(), password_confirm.val());
+ }
+ });
+
+ password_confirm.on('blur', function () {
+ if (password_confirm.val().length) {
+ validatePasswordConfirm(password.val(), password_confirm.val());
+ }
+ });
+
+ function validateForm(callback) {
+ validationError = false;
+ validatePassword(password.val(), password_confirm.val());
+ validatePasswordConfirm(password.val(), password_confirm.val());
+ validateUsername(username.val(), callback);
+ }
+
+ // Guard against caps lock
+ Login.capsLockCheck(document.querySelector('#password'), document.querySelector('#caps-lock-warning'));
+
+ register.on('click', function (e) {
+ const registerBtn = $(this);
+ const errorEl = $('#register-error-notify');
+ errorEl.addClass('hidden');
+ e.preventDefault();
+ validateForm(function () {
+ if (validationError) {
+ return;
+ }
+
+ registerBtn.addClass('disabled');
+
+ registerBtn.parents('form').ajaxSubmit({
+ headers: {
+ 'x-csrf-token': config.csrf_token,
+ },
+ success: function (data) {
+ registerBtn.removeClass('disabled');
+ if (!data) {
+ return;
+ }
+ if (data.next) {
+ const pathname = utils.urlToLocation(data.next).pathname;
+
+ const params = utils.params({ url: data.next });
+ params.registered = true;
+ const qs = decodeURIComponent($.param(params));
+
+ window.location.href = pathname + '?' + qs;
+ } else if (data.message) {
+ translator.translate(data.message, function (msg) {
+ bootbox.alert(msg);
+ ajaxify.go('/');
+ });
+ }
+ },
+ error: function (data) {
+ translator.translate(data.responseText, config.defaultLang, function (translated) {
+ if (data.status === 403 && data.responseText === 'Forbidden') {
+ window.location.href = config.relative_path + '/register?error=csrf-invalid';
+ } else {
+ errorEl.find('p').text(translated);
+ errorEl.removeClass('hidden');
+ registerBtn.removeClass('disabled');
+ }
+ });
+ },
+ });
+ });
+ });
+
+ // Set initial focus
+ $('#username').focus();
+ };
+
+ /**
+ * Modified validateUsername: if the username is taken, try to generate an alternative suggestion.
+ */
+ function validateUsername(username, callback) {
+ callback = callback || function () {};
+
+ const username_notify = $('#username-notify');
+ const userslug = slugify(username);
+ if (username.length < ajaxify.data.minimumUsernameLength ||
+ userslug.length < ajaxify.data.minimumUsernameLength) {
+ showError(username_notify, '[[error:username-too-short]]');
+ return callback();
+ } else if (username.length > ajaxify.data.maximumUsernameLength) {
+ showError(username_notify, '[[error:username-too-long]]');
+ return callback();
+ } else if (!utils.isUserNameValid(username) || !userslug) {
+ showError(username_notify, '[[error:invalid-username]]');
+ return callback();
+ } else {
+ // Check if the username (or group) already exists
+ Promise.allSettled([
+ api.head(`/users/bySlug/${username}`, {}),
+ api.head(`/groups/${username}`, {}),
+ ]).then((results) => {
+ if (results.every(obj => obj.status === 'rejected')) {
+ // Username is available
+ showSuccess(username_notify, successIcon);
+ return callback();
+ } else {
+ // Username is taken – generate an alternative suggestion
+ const suffix = 'suffix';
+ let suggestedUsername = username + suffix;
+
+ // Check if the first suggested username is available
+ api.head(`/users/bySlug/${suggestedUsername}`, {})
+ .then(() => {
+ // If the suggestion is taken, try appending a counter until one is available
+ let counter = 1;
+ (function checkSuggestion() {
+ const newSuggestion = username + suffix + counter;
+ api.head(`/users/bySlug/${newSuggestion}`, {})
+ .then(() => {
+ // This suggestion is also taken, increment counter and try again
+ counter++;
+ checkSuggestion();
+ })
+ .catch(() => {
+ // Found an available suggestion
+ showError(username_notify, '[[error:username-taken]] - Try "' + newSuggestion + '"');
+ return callback();
+ });
+ })();
+ })
+ .catch(() => {
+ // The first suggestion is available
+ showError(username_notify, '[[error:username-taken]] - Try "' + suggestedUsername + '"');
+ return callback();
+ });
+ }
+ });
+ }
+ }
+
+ function validatePassword(password, password_confirm) {
+ const password_notify = $('#password-notify');
+ const password_confirm_notify = $('#password-confirm-notify');
+
+ try {
+ utils.assertPasswordValidity(password, zxcvbn);
+
+ if (password === $('#username').val()) {
+ throw new Error('[[user:password_same_as_username]]');
+ }
+
+ showSuccess(password_notify, successIcon);
+ } catch (err) {
+ showError(password_notify, err.message);
+ }
+
+ if (password !== password_confirm && password_confirm !== '') {
+ showError(password_confirm_notify, '[[user:change_password_error_match]]');
+ }
+ }
+
+ function validatePasswordConfirm(password, password_confirm) {
+ const password_notify = $('#password-notify');
+ const password_confirm_notify = $('#password-confirm-notify');
+
+ if (!password || password_notify.hasClass('alert-error')) {
+ return;
+ }
+
+ if (password !== password_confirm) {
+ showError(password_confirm_notify, '[[user:change_password_error_match]]');
+ } else {
+ showSuccess(password_confirm_notify, successIcon);
+ }
+ }
+
+ function showError(element, msg) {
+ translator.translate(msg, function (translatedMsg) {
+ element.html(translatedMsg);
+ element.parent()
+ .removeClass('register-success')
+ .addClass('register-danger');
+ element.show();
+ });
+ validationError = true;
+ }
+
+ function showSuccess(element, msg) {
+ translator.translate(msg, function (translatedMsg) {
+ element.html(translatedMsg);
+ element.parent()
+ .removeClass('register-danger')
+ .addClass('register-success');
+ element.show();
+ });
+ }
+
+ function handleLanguageOverride() {
+ if (!app.user.uid && config.defaultLang !== config.userLang) {
+ const formEl = $('[component="register/local"]');
+ const langEl = $('');
+
+ formEl.append(langEl);
+ }
+ }
+
+ return Register;
});
diff --git a/src/controllers/user.js b/src/controllers/user.js
index 673a8cf..19695a2 100644
--- a/src/controllers/user.js
+++ b/src/controllers/user.js
@@ -9,6 +9,50 @@ const accountHelpers = require('./accounts/helpers');
const userController = module.exports;
+// ======================================================================
+// NEW: updateUsername method added to check for taken usernames and suggest an alternative.
+// ======================================================================
+userController.updateUsername = async function (req, res, next) {
+ try {
+ // Make sure the user is logged in
+ if (!req.loggedIn) {
+ return res.status(401).json({ error: 'not-authorized' });
+ }
+
+ // Retrieve the new username from the request body
+ const newUsername = req.body.username;
+ if (!newUsername) {
+ return res.status(400).json({ error: '[[error:invalid-username]]' });
+ }
+
+ // Check if the username is already taken by another user.
+ const existingUid = await user.getUidByUsername(newUsername);
+ // Note: if existingUid exists and it doesn't belong to the current user, it is taken.
+ if (existingUid && parseInt(existingUid, 10) !== parseInt(req.uid, 10)) {
+ // Username is taken – generate an alternative suggestion.
+ const suffix = 'suffix'; // You can make this configurable.
+ let suggestedUsername = newUsername + suffix;
+ let counter = 1;
+ // Loop until we find an available suggestion.
+ while (await user.getUidByUsername(suggestedUsername)) {
+ suggestedUsername = newUsername + suffix + counter;
+ counter++;
+ }
+ // Return a 409 Conflict status along with an error code and the suggestion.
+ return res.status(409).json({ error: 'username-taken', suggestion: suggestedUsername });
+ }
+
+ // Username is available – update the username.
+ await user.updateUsername(req.uid, newUsername);
+ return res.json({ success: true, username: newUsername });
+ } catch (err) {
+ return next(err);
+ }
+};
+// ======================================================================
+// End of updateUsername method
+// ======================================================================
+
userController.getCurrentUser = async function (req, res) {
if (!req.loggedIn) {
return res.status(401).json('not-authorized');
@@ -113,6 +157,12 @@ function sendExport(filename, type, res, next) {
}
require('../promisify')(userController, [
- 'getCurrentUser', 'getUserByUID', 'getUserByUsername', 'getUserByEmail',
- 'exportPosts', 'exportUploads', 'exportProfile',
+ 'getCurrentUser',
+ 'getUserByUID',
+ 'getUserByUsername',
+ 'getUserByEmail',
+ 'updateUsername', // <<-- ADDED updateUsername here for promisification.
+ 'exportPosts',
+ 'exportUploads',
+ 'exportProfile',
]);