Add display name support for user management and update related components
All checks were successful
Build & Push Docker Image / build (push) Successful in 6m2s

This commit is contained in:
2026-02-27 16:29:23 +01:00
parent d781022b63
commit 9be9938f02
14 changed files with 165 additions and 63 deletions

View File

@@ -22,12 +22,17 @@ const router = Router();
// POST /api/auth/register
router.post('/register', async (req, res) => {
try {
const { name, email, password } = req.body;
const { username, display_name, email, password } = req.body;
if (!name || !email || !password) {
if (!username || !display_name || !email || !password) {
return res.status(400).json({ error: 'All fields are required' });
}
const usernameRegex = /^[a-zA-Z0-9_-]{3,30}$/;
if (!usernameRegex.test(username)) {
return res.status(400).json({ error: 'Username may only contain letters, numbers, _ and - (330 chars)' });
}
if (password.length < 6) {
return res.status(400).json({ error: 'Password must be at least 6 characters long' });
}
@@ -38,6 +43,11 @@ router.post('/register', async (req, res) => {
return res.status(409).json({ error: 'Email is already in use' });
}
const existingUsername = await db.get('SELECT id FROM users WHERE LOWER(name) = LOWER(?)', [username]);
if (existingUsername) {
return res.status(409).json({ error: 'Username is already taken' });
}
const hash = bcrypt.hashSync(password, 12);
// If SMTP is configured, require email verification
@@ -46,8 +56,8 @@ router.post('/register', async (req, res) => {
const expires = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString();
await db.run(
'INSERT INTO users (name, email, password_hash, email_verified, verification_token, verification_token_expires) VALUES (?, ?, ?, 0, ?, ?)',
[name, email.toLowerCase(), hash, verificationToken, expires]
'INSERT INTO users (name, display_name, email, password_hash, email_verified, verification_token, verification_token_expires) VALUES (?, ?, ?, ?, 0, ?, ?)',
[username, display_name, email.toLowerCase(), hash, verificationToken, expires]
);
// Build verification URL
@@ -61,19 +71,19 @@ router.post('/register', async (req, res) => {
try { appName = JSON.parse(brandingSetting.value).appName || appName; } catch {}
}
await sendVerificationEmail(email.toLowerCase(), name, verifyUrl, appName);
await sendVerificationEmail(email.toLowerCase(), display_name, verifyUrl, appName);
return res.status(201).json({ needsVerification: true, message: 'Verification email has been sent' });
}
// No SMTP configured register and login immediately (legacy behaviour)
const result = await db.run(
'INSERT INTO users (name, email, password_hash, email_verified) VALUES (?, ?, ?, 1)',
[name, email.toLowerCase(), hash]
'INSERT INTO users (name, display_name, email, password_hash, email_verified) VALUES (?, ?, ?, ?, 1)',
[username, display_name, email.toLowerCase(), hash]
);
const token = generateToken(result.lastInsertRowid);
const user = await db.get('SELECT id, name, email, role, theme, language, avatar_color, avatar_image FROM users WHERE id = ?', [result.lastInsertRowid]);
const user = await db.get('SELECT id, name, display_name, email, role, theme, language, avatar_color, avatar_image FROM users WHERE id = ?', [result.lastInsertRowid]);
res.status(201).json({ token, user });
} catch (err) {
@@ -129,7 +139,7 @@ router.post('/resend-verification', async (req, res) => {
}
const db = getDb();
const user = await db.get('SELECT id, name, email_verified FROM users WHERE email = ?', [email.toLowerCase()]);
const user = await db.get('SELECT id, name, display_name, email_verified FROM users WHERE email = ?', [email.toLowerCase()]);
if (!user || user.email_verified) {
// Don't reveal whether account exists
@@ -153,7 +163,7 @@ router.post('/resend-verification', async (req, res) => {
try { appName = JSON.parse(brandingSetting.value).appName || appName; } catch {}
}
await sendVerificationEmail(email.toLowerCase(), user.name, verifyUrl, appName);
await sendVerificationEmail(email.toLowerCase(), user.display_name || user.name, verifyUrl, appName);
res.json({ message: 'If an account exists, a new email has been sent.' });
} catch (err) {
@@ -200,7 +210,7 @@ router.get('/me', authenticateToken, (req, res) => {
// PUT /api/auth/profile
router.put('/profile', authenticateToken, async (req, res) => {
try {
const { name, email, theme, language, avatar_color } = req.body;
const { name, display_name, email, theme, language, avatar_color } = req.body;
const db = getDb();
if (email && email !== req.user.email) {
@@ -210,18 +220,30 @@ router.put('/profile', authenticateToken, async (req, res) => {
}
}
if (name && name !== req.user.name) {
const usernameRegex = /^[a-zA-Z0-9_-]{3,30}$/;
if (!usernameRegex.test(name)) {
return res.status(400).json({ error: 'Username may only contain letters, numbers, _ and - (330 chars)' });
}
const existingUsername = await db.get('SELECT id FROM users WHERE LOWER(name) = LOWER(?) AND id != ?', [name, req.user.id]);
if (existingUsername) {
return res.status(409).json({ error: 'Username is already taken' });
}
}
await db.run(`
UPDATE users SET
name = COALESCE(?, name),
display_name = COALESCE(?, display_name),
email = COALESCE(?, email),
theme = COALESCE(?, theme),
language = COALESCE(?, language),
avatar_color = COALESCE(?, avatar_color),
updated_at = CURRENT_TIMESTAMP
WHERE id = ?
`, [name, email?.toLowerCase(), theme, language, avatar_color, req.user.id]);
`, [name, display_name, email?.toLowerCase(), theme, language, avatar_color, req.user.id]);
const updated = await db.get('SELECT id, name, email, role, theme, language, avatar_color, avatar_image FROM users WHERE id = ?', [req.user.id]);
const updated = await db.get('SELECT id, name, display_name, email, role, theme, language, avatar_color, avatar_image FROM users WHERE id = ?', [req.user.id]);
res.json({ user: updated });
} catch (err) {
console.error('Profile update error:', err);
@@ -290,7 +312,7 @@ router.post('/avatar', authenticateToken, async (req, res) => {
fs.writeFileSync(filepath, buffer);
await db.run('UPDATE users SET avatar_image = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?', [filename, req.user.id]);
const updated = await db.get('SELECT id, name, email, role, theme, language, avatar_color, avatar_image FROM users WHERE id = ?', [req.user.id]);
const updated = await db.get('SELECT id, name, display_name, email, role, theme, language, avatar_color, avatar_image FROM users WHERE id = ?', [req.user.id]);
res.json({ user: updated });
} catch (err) {
@@ -309,7 +331,7 @@ router.delete('/avatar', authenticateToken, async (req, res) => {
if (fs.existsSync(oldPath)) fs.unlinkSync(oldPath);
}
await db.run('UPDATE users SET avatar_image = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?', [req.user.id]);
const updated = await db.get('SELECT id, name, email, role, theme, language, avatar_color, avatar_image FROM users WHERE id = ?', [req.user.id]);
const updated = await db.get('SELECT id, name, display_name, email, role, theme, language, avatar_color, avatar_image FROM users WHERE id = ?', [req.user.id]);
res.json({ user: updated });
} catch (err) {
console.error('Avatar delete error:', err);