Add display name support for user management and update related components
All checks were successful
Build & Push Docker Image / build (push) Successful in 6m2s
All checks were successful
Build & Push Docker Image / build (push) Successful in 6m2s
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 = ?
|
||||
|
||||
Reference in New Issue
Block a user