From 9be9938f02525783bf206e59f3d6a05931f2fb34 Mon Sep 17 00:00:00 2001 From: Michelle Date: Fri, 27 Feb 2026 16:29:23 +0100 Subject: [PATCH] Add display name support for user management and update related components --- server/config/database.js | 6 +++++ server/middleware/auth.js | 2 +- server/routes/admin.js | 22 ++++++++++----- server/routes/auth.js | 52 +++++++++++++++++++++++++----------- server/routes/rooms.js | 22 +++++++-------- src/components/Navbar.jsx | 8 +++--- src/components/Sidebar.jsx | 4 +-- src/contexts/AuthContext.jsx | 4 +-- src/i18n/de.json | 11 +++++++- src/i18n/en.json | 11 +++++++- src/pages/Admin.jsx | 30 +++++++++++++++------ src/pages/Register.jsx | 29 +++++++++++++++----- src/pages/RoomDetail.jsx | 8 +++--- src/pages/Settings.jsx | 19 +++++++++++-- 14 files changed, 165 insertions(+), 63 deletions(-) diff --git a/server/config/database.js b/server/config/database.js index e318f22..e4469d6 100644 --- a/server/config/database.js +++ b/server/config/database.js @@ -127,6 +127,7 @@ export async function initDatabase() { CREATE TABLE IF NOT EXISTS users ( id SERIAL PRIMARY KEY, name TEXT NOT NULL, + display_name TEXT DEFAULT '', email TEXT UNIQUE NOT NULL, password_hash TEXT NOT NULL, role TEXT DEFAULT 'user' CHECK(role IN ('user', 'admin')), @@ -213,6 +214,7 @@ export async function initDatabase() { CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, + display_name TEXT DEFAULT '', email TEXT UNIQUE NOT NULL, password_hash TEXT NOT NULL, role TEXT DEFAULT 'user' CHECK(role IN ('user', 'admin')), @@ -343,6 +345,10 @@ export async function initDatabase() { if (!(await db.columnExists('federation_invitations', 'allow_recording'))) { await db.exec('ALTER TABLE federation_invitations ADD COLUMN allow_recording INTEGER DEFAULT 1'); } + if (!(await db.columnExists('users', 'display_name'))) { + await db.exec("ALTER TABLE users ADD COLUMN display_name TEXT DEFAULT ''"); + await db.exec("UPDATE users SET display_name = name WHERE display_name = ''"); + } // ── Default admin ─────────────────────────────────────────────────────── const adminEmail = process.env.ADMIN_EMAIL || 'admin@example.com'; diff --git a/server/middleware/auth.js b/server/middleware/auth.js index 5818d25..7f60a4c 100644 --- a/server/middleware/auth.js +++ b/server/middleware/auth.js @@ -14,7 +14,7 @@ export async function authenticateToken(req, res, next) { try { const decoded = jwt.verify(token, JWT_SECRET); const db = getDb(); - const user = await db.get('SELECT id, name, email, role, theme, language, avatar_color, avatar_image FROM users WHERE id = ?', [decoded.userId]); + const user = await db.get('SELECT id, name, display_name, email, role, theme, language, avatar_color, avatar_image FROM users WHERE id = ?', [decoded.userId]); if (!user) { return res.status(401).json({ error: 'User not found' }); } diff --git a/server/routes/admin.js b/server/routes/admin.js index b745c1d..8c252b6 100644 --- a/server/routes/admin.js +++ b/server/routes/admin.js @@ -8,12 +8,17 @@ const router = Router(); // POST /api/admin/users - Create user (admin) router.post('/users', authenticateToken, requireAdmin, async (req, res) => { try { - const { name, email, password, role } = req.body; + const { name, display_name, email, password, role } = req.body; - if (!name || !email || !password) { + if (!name || !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(name)) { + return res.status(400).json({ error: 'Username may only contain letters, numbers, _ and - (3–30 chars)' }); + } + if (password.length < 6) { return res.status(400).json({ error: 'Password must be at least 6 characters long' }); } @@ -26,13 +31,18 @@ router.post('/users', authenticateToken, requireAdmin, 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(?)', [name]); + if (existingUsername) { + return res.status(409).json({ error: 'Username is already taken' }); + } + const hash = bcrypt.hashSync(password, 12); const result = await db.run( - 'INSERT INTO users (name, email, password_hash, role) VALUES (?, ?, ?, ?)', - [name, email.toLowerCase(), hash, validRole] + 'INSERT INTO users (name, display_name, email, password_hash, role, email_verified) VALUES (?, ?, ?, ?, ?, 1)', + [name, display_name, email.toLowerCase(), hash, validRole] ); - const user = await db.get('SELECT id, name, email, role, created_at FROM users WHERE id = ?', [result.lastInsertRowid]); + const user = await db.get('SELECT id, name, display_name, email, role, created_at FROM users WHERE id = ?', [result.lastInsertRowid]); res.status(201).json({ user }); } catch (err) { console.error('Create user error:', err); @@ -45,7 +55,7 @@ router.get('/users', authenticateToken, requireAdmin, async (req, res) => { try { const db = getDb(); const users = await db.all(` - SELECT id, name, email, role, language, theme, avatar_color, avatar_image, created_at, + SELECT id, name, display_name, email, role, language, theme, avatar_color, avatar_image, created_at, (SELECT COUNT(*) FROM rooms WHERE rooms.user_id = users.id) as room_count FROM users ORDER BY created_at DESC diff --git a/server/routes/auth.js b/server/routes/auth.js index d5f1cbb..682cb6e 100644 --- a/server/routes/auth.js +++ b/server/routes/auth.js @@ -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 - (3–30 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 - (3–30 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); diff --git a/server/routes/rooms.js b/server/routes/rooms.js index 06e8469..37c6d27 100644 --- a/server/routes/rooms.js +++ b/server/routes/rooms.js @@ -27,7 +27,7 @@ router.get('/', authenticateToken, async (req, res) => { try { const db = getDb(); const ownRooms = await db.all(` - SELECT r.*, u.name as owner_name, 0 as shared + SELECT r.*, COALESCE(NULLIF(u.display_name,''), u.name) as owner_name, 0 as shared FROM rooms r JOIN users u ON r.user_id = u.id WHERE r.user_id = ? @@ -35,7 +35,7 @@ router.get('/', authenticateToken, async (req, res) => { `, [req.user.id]); const sharedRooms = await db.all(` - SELECT r.*, u.name as owner_name, 1 as shared + SELECT r.*, COALESCE(NULLIF(u.display_name,''), u.name) as owner_name, 1 as shared FROM rooms r JOIN users u ON r.user_id = u.id JOIN room_shares rs ON rs.room_id = r.id @@ -60,11 +60,11 @@ router.get('/users/search', authenticateToken, async (req, res) => { const db = getDb(); const searchTerm = `%${q}%`; const users = await db.all(` - SELECT id, name, email, avatar_color, avatar_image + SELECT id, name, display_name, email, avatar_color, avatar_image FROM users - WHERE (name LIKE ? OR email LIKE ?) AND id != ? + WHERE (name LIKE ? OR display_name LIKE ? OR email LIKE ?) AND id != ? LIMIT 10 - `, [searchTerm, searchTerm, req.user.id]); + `, [searchTerm, searchTerm, searchTerm, req.user.id]); res.json({ users }); } catch (err) { console.error('Search users error:', err); @@ -77,7 +77,7 @@ router.get('/:uid', authenticateToken, async (req, res) => { try { const db = getDb(); const room = await db.get(` - SELECT r.*, u.name as owner_name + SELECT r.*, COALESCE(NULLIF(u.display_name,''), u.name) as owner_name FROM rooms r JOIN users u ON r.user_id = u.id WHERE r.uid = ? @@ -98,7 +98,7 @@ router.get('/:uid', authenticateToken, async (req, res) => { // Get shared users const sharedUsers = await db.all(` - SELECT u.id, u.name, u.email, u.avatar_color, u.avatar_image + SELECT u.id, u.name, u.display_name, u.email, u.avatar_color, u.avatar_image FROM room_shares rs JOIN users u ON rs.user_id = u.id WHERE rs.room_id = ? @@ -254,7 +254,7 @@ router.get('/:uid/shares', authenticateToken, async (req, res) => { return res.status(404).json({ error: 'Room not found or no permission' }); } const shares = await db.all(` - SELECT u.id, u.name, u.email, u.avatar_color, u.avatar_image + SELECT u.id, u.name, u.display_name, u.email, u.avatar_color, u.avatar_image FROM room_shares rs JOIN users u ON rs.user_id = u.id WHERE rs.room_id = ? @@ -288,7 +288,7 @@ router.post('/:uid/shares', authenticateToken, async (req, res) => { } await db.run('INSERT INTO room_shares (room_id, user_id) VALUES (?, ?)', [room.id, user_id]); const shares = await db.all(` - SELECT u.id, u.name, u.email, u.avatar_color, u.avatar_image + SELECT u.id, u.name, u.display_name, u.email, u.avatar_color, u.avatar_image FROM room_shares rs JOIN users u ON rs.user_id = u.id WHERE rs.room_id = ? @@ -310,7 +310,7 @@ router.delete('/:uid/shares/:userId', authenticateToken, async (req, res) => { } await db.run('DELETE FROM room_shares WHERE room_id = ? AND user_id = ?', [room.id, parseInt(req.params.userId)]); const shares = await db.all(` - SELECT u.id, u.name, u.email, u.avatar_color, u.avatar_image + SELECT u.id, u.name, u.display_name, u.email, u.avatar_color, u.avatar_image FROM room_shares rs JOIN users u ON rs.user_id = u.id WHERE rs.room_id = ? @@ -420,7 +420,7 @@ router.get('/:uid/public', async (req, res) => { const db = getDb(); const room = await db.get(` SELECT r.uid, r.name, r.welcome_message, r.access_code, r.record_meeting, r.max_participants, r.anyone_can_start, - u.name as owner_name + COALESCE(NULLIF(u.display_name,''), u.name) as owner_name FROM rooms r JOIN users u ON r.user_id = u.id WHERE r.uid = ? diff --git a/src/components/Navbar.jsx b/src/components/Navbar.jsx index 069b705..e297785 100644 --- a/src/components/Navbar.jsx +++ b/src/components/Navbar.jsx @@ -27,8 +27,8 @@ export default function Navbar({ onMenuClick }) { navigate('/'); }; - const initials = user?.name - ? user.name + const initials = (user?.display_name || user?.name) + ? (user.display_name || user.name) .split(' ') .map(n => n[0]) .join('') @@ -72,14 +72,14 @@ export default function Navbar({ onMenuClick }) { )} - {user?.name} + {user?.display_name || user?.name} {dropdownOpen && (
-

{user?.name}

+

{user?.display_name || user?.name}

{user?.email}

diff --git a/src/components/Sidebar.jsx b/src/components/Sidebar.jsx index 9147544..4f7fac4 100644 --- a/src/components/Sidebar.jsx +++ b/src/components/Sidebar.jsx @@ -106,10 +106,10 @@ export default function Sidebar({ open, onClose }) { className="w-9 h-9 rounded-full flex items-center justify-center text-white text-sm font-bold flex-shrink-0" style={{ backgroundColor: user?.avatar_color || '#6366f1' }} > - {user?.name?.[0]?.toUpperCase() || '?'} + {(user?.display_name || user?.name)?.[0]?.toUpperCase() || '?'}
-

{user?.name}

+

{user?.display_name || user?.name}

{user?.email}

diff --git a/src/contexts/AuthContext.jsx b/src/contexts/AuthContext.jsx index c34d68e..30ec413 100644 --- a/src/contexts/AuthContext.jsx +++ b/src/contexts/AuthContext.jsx @@ -28,8 +28,8 @@ export function AuthProvider({ children }) { return res.data.user; }, []); - const register = useCallback(async (name, email, password) => { - const res = await api.post('/auth/register', { name, email, password }); + const register = useCallback(async (username, displayName, email, password) => { + const res = await api.post('/auth/register', { username, display_name: displayName, email, password }); if (res.data.needsVerification) { return { needsVerification: true }; } diff --git a/src/i18n/de.json b/src/i18n/de.json index 72cc07b..90894ca 100644 --- a/src/i18n/de.json +++ b/src/i18n/de.json @@ -72,7 +72,16 @@ "verifyFailed": "Verifizierung fehlgeschlagen", "verifyFailedTitle": "Verifizierung fehlgeschlagen", "verifyTokenMissing": "Kein Verifizierungstoken vorhanden.", - "emailNotVerified": "E-Mail-Adresse noch nicht verifiziert. Bitte prüfe dein Postfach." + "emailNotVerified": "E-Mail-Adresse noch nicht verifiziert. Bitte prüfe dein Postfach.", + "username": "Benutzername", + "usernamePlaceholder": "z.B. maxmuster", + "usernameHint": "Nur Buchstaben, Zahlen, _ und - erlaubt (3–30 Zeichen)", + "displayName": "Anzeigename", + "displayNamePlaceholder": "Max Mustermann", + "usernameTaken": "Benutzername ist bereits vergeben", + "usernameInvalid": "Benutzername darf nur Buchstaben, Zahlen, _ und - enthalten (3–30 Zeichen)", + "usernameRequired": "Benutzername ist erforderlich", + "displayNameRequired": "Anzeigename ist erforderlich" }, "home": { "poweredBy": "Powered by BigBlueButton", diff --git a/src/i18n/en.json b/src/i18n/en.json index 553ca29..d3ea2ae 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -72,7 +72,16 @@ "verifyFailed": "Verification failed", "verifyFailedTitle": "Verification failed", "verifyTokenMissing": "No verification token provided.", - "emailNotVerified": "Email not yet verified. Please check your inbox." + "emailNotVerified": "Email not yet verified. Please check your inbox.", + "username": "Username", + "usernamePlaceholder": "e.g. johndoe", + "usernameHint": "Letters, numbers, _ and - only (3–30 chars)", + "displayName": "Display Name", + "displayNamePlaceholder": "John Doe", + "usernameTaken": "Username is already taken", + "usernameInvalid": "Username may only contain letters, numbers, _ and - (3–30 chars)", + "usernameRequired": "Username is required", + "displayNameRequired": "Display name is required" }, "home": { "poweredBy": "Powered by BigBlueButton", diff --git a/src/pages/Admin.jsx b/src/pages/Admin.jsx index 4e60804..17b8dd1 100644 --- a/src/pages/Admin.jsx +++ b/src/pages/Admin.jsx @@ -25,7 +25,7 @@ export default function Admin() { const [newPassword, setNewPassword] = useState(''); const [showCreateUser, setShowCreateUser] = useState(false); const [creatingUser, setCreatingUser] = useState(false); - const [newUser, setNewUser] = useState({ name: '', email: '', password: '', role: 'user' }); + const [newUser, setNewUser] = useState({ name: '', display_name: '', email: '', password: '', role: 'user' }); // Branding state const [editAppName, setEditAppName] = useState(''); @@ -163,7 +163,7 @@ export default function Admin() { await api.post('/admin/users', newUser); toast.success(t('admin.userCreated')); setShowCreateUser(false); - setNewUser({ name: '', email: '', password: '', role: 'user' }); + setNewUser({ name: '', display_name: '', email: '', password: '', role: 'user' }); fetchUsers(); } catch (err) { toast.error(err.response?.data?.error || t('admin.userCreateFailed')); @@ -173,7 +173,7 @@ export default function Admin() { }; const filteredUsers = users.filter(u => - u.name.toLowerCase().includes(search.toLowerCase()) || + (u.display_name || u.name).toLowerCase().includes(search.toLowerCase()) || u.email.toLowerCase().includes(search.toLowerCase()) ); @@ -371,12 +371,12 @@ export default function Admin() { className="w-full h-full object-cover" /> ) : ( - u.name[0]?.toUpperCase() + (u.display_name || u.name)[0]?.toUpperCase() )}
-

{u.name}

-

{u.email}

+

{u.display_name || u.name}

+

@{u.name} · {u.email}

@@ -490,7 +490,7 @@ export default function Admin() {

{t('admin.createUserTitle')}

- +
setNewUser({ ...newUser, name: e.target.value })} className="input-field pl-11" - placeholder={t('auth.namePlaceholder')} + placeholder={t('auth.usernamePlaceholder')} required />
+

{t('auth.usernameHint')}

+
+
+ +
+ + setNewUser({ ...newUser, display_name: e.target.value })} + className="input-field pl-11" + placeholder={t('auth.displayNamePlaceholder')} + /> +
diff --git a/src/pages/Register.jsx b/src/pages/Register.jsx index 8b77630..05006f8 100644 --- a/src/pages/Register.jsx +++ b/src/pages/Register.jsx @@ -7,7 +7,8 @@ import BrandLogo from '../components/BrandLogo'; import toast from 'react-hot-toast'; export default function Register() { - const [name, setName] = useState(''); + const [username, setUsername] = useState(''); + const [displayName, setDisplayName] = useState(''); const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [confirmPassword, setConfirmPassword] = useState(''); @@ -32,7 +33,7 @@ export default function Register() { setLoading(true); try { - const result = await register(name, email, password); + const result = await register(username, displayName, email, password); if (result?.needsVerification) { setNeedsVerification(true); toast.success(t('auth.verificationSent')); @@ -87,15 +88,31 @@ export default function Register() {
- +
setName(e.target.value)} + value={username} + onChange={e => setUsername(e.target.value)} className="input-field pl-11" - placeholder={t('auth.namePlaceholder')} + placeholder={t('auth.usernamePlaceholder')} + required + /> +
+

{t('auth.usernameHint')}

+
+ +
+ +
+ + setDisplayName(e.target.value)} + className="input-field pl-11" + placeholder={t('auth.displayNamePlaceholder')} required />
diff --git a/src/pages/RoomDetail.jsx b/src/pages/RoomDetail.jsx index 58b17a5..43a87fe 100644 --- a/src/pages/RoomDetail.jsx +++ b/src/pages/RoomDetail.jsx @@ -574,11 +574,11 @@ export default function RoomDetail() { {u.avatar_image ? ( ) : ( - u.name.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2) + (u.display_name || u.name).split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2) )}
-
{u.name}
+
{u.display_name || u.name}
{u.email}
@@ -600,11 +600,11 @@ export default function RoomDetail() { {u.avatar_image ? ( ) : ( - u.name.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2) + (u.display_name || u.name).split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2) )}
-
{u.name}
+
{u.display_name || u.name}
{u.email}
diff --git a/src/pages/Settings.jsx b/src/pages/Settings.jsx index f732ba8..2195050 100644 --- a/src/pages/Settings.jsx +++ b/src/pages/Settings.jsx @@ -14,6 +14,7 @@ export default function Settings() { const [profile, setProfile] = useState({ name: user?.name || '', + display_name: user?.display_name || '', email: user?.email || '', }); const [passwords, setPasswords] = useState({ @@ -52,6 +53,7 @@ export default function Settings() { try { const res = await api.put('/auth/profile', { name: profile.name, + display_name: profile.display_name, email: profile.email, theme, language, @@ -190,7 +192,7 @@ export default function Settings() { className="w-16 h-16 rounded-full flex items-center justify-center text-white text-xl font-bold" style={{ backgroundColor: user?.avatar_color || '#6366f1' }} > - {user?.name?.[0]?.toUpperCase() || '?'} + {(user?.display_name || user?.name)?.[0]?.toUpperCase() || '?'} )}