Add user profile panel with self-service password change and dark theme UserMenu
This commit is contained in:
@@ -258,6 +258,137 @@ function createAuthRouter(db, logAudit) {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/auth/profile
|
||||
*
|
||||
* Returns the full profile for the currently authenticated user.
|
||||
* Queries the database for up-to-date account details including
|
||||
* creation date and last login timestamp.
|
||||
*
|
||||
* @returns {object} 200 - { id, username, email, group, created_at, last_login }
|
||||
* @returns {object} 401 - { error: 'Account is disabled' } (clears session cookie)
|
||||
* @returns {object} 500 - { error: 'Failed to fetch profile' }
|
||||
*/
|
||||
router.get('/profile', requireAuth(db), async (req, res) => {
|
||||
try {
|
||||
const user = await new Promise((resolve, reject) => {
|
||||
db.get(
|
||||
'SELECT id, username, email, user_group, created_at, last_login, is_active FROM users WHERE id = ?',
|
||||
[req.user.id],
|
||||
(err, row) => {
|
||||
if (err) reject(err);
|
||||
else resolve(row);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
if (!user || !user.is_active) {
|
||||
res.clearCookie('session_id');
|
||||
return res.status(401).json({ error: 'Account is disabled' });
|
||||
}
|
||||
|
||||
res.json({
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
group: user.user_group,
|
||||
created_at: user.created_at,
|
||||
last_login: user.last_login
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Profile fetch error:', err);
|
||||
res.status(500).json({ error: 'Failed to fetch profile' });
|
||||
}
|
||||
});
|
||||
|
||||
// Rate limiter for password change — 5 attempts per 15-minute window, keyed by session cookie
|
||||
const passwordChangeLimiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: 5,
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
keyGenerator: (req) => req.cookies?.session_id || req.ip,
|
||||
message: { error: 'Too many password change attempts. Please try again later.' }
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/auth/change-password
|
||||
*
|
||||
* Allows the authenticated user to change their own password.
|
||||
* Rate-limited to 5 attempts per 15-minute window per session.
|
||||
*
|
||||
* @body {string} currentPassword - The user's current password
|
||||
* @body {string} newPassword - The desired new password (min 8 characters)
|
||||
* @returns {object} 200 - { message: 'Password changed successfully' }
|
||||
* @returns {object} 400 - { error: 'Current password and new password are required' } | { error: 'New password must be at least 8 characters' }
|
||||
* @returns {object} 401 - { error: 'Account is disabled' } | { error: 'Current password is incorrect' }
|
||||
* @returns {object} 429 - { error: 'Too many password change attempts. Please try again later.' }
|
||||
* @returns {object} 500 - { error: 'Failed to change password' }
|
||||
*/
|
||||
router.post('/change-password', requireAuth(db), passwordChangeLimiter, async (req, res) => {
|
||||
const { currentPassword, newPassword } = req.body;
|
||||
|
||||
if (!currentPassword || !newPassword) {
|
||||
return res.status(400).json({ error: 'Current password and new password are required' });
|
||||
}
|
||||
|
||||
if (newPassword.length < 8) {
|
||||
return res.status(400).json({ error: 'New password must be at least 8 characters' });
|
||||
}
|
||||
|
||||
try {
|
||||
// Fetch user's password hash and active status
|
||||
const user = await new Promise((resolve, reject) => {
|
||||
db.get(
|
||||
'SELECT password_hash, is_active FROM users WHERE id = ?',
|
||||
[req.user.id],
|
||||
(err, row) => {
|
||||
if (err) reject(err);
|
||||
else resolve(row);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
if (!user || !user.is_active) {
|
||||
return res.status(401).json({ error: 'Account is disabled' });
|
||||
}
|
||||
|
||||
// Verify current password
|
||||
const validPassword = await bcrypt.compare(currentPassword, user.password_hash);
|
||||
if (!validPassword) {
|
||||
return res.status(401).json({ error: 'Current password is incorrect' });
|
||||
}
|
||||
|
||||
// Hash new password and update
|
||||
const newHash = await bcrypt.hash(newPassword, 10);
|
||||
await new Promise((resolve, reject) => {
|
||||
db.run(
|
||||
'UPDATE users SET password_hash = ? WHERE id = ?',
|
||||
[newHash, req.user.id],
|
||||
(err) => {
|
||||
if (err) reject(err);
|
||||
else resolve();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
logAudit(db, {
|
||||
userId: req.user.id,
|
||||
username: req.user.username,
|
||||
action: 'password_change',
|
||||
entityType: 'auth',
|
||||
entityId: null,
|
||||
details: null,
|
||||
ipAddress: req.ip
|
||||
});
|
||||
|
||||
res.json({ message: 'Password changed successfully' });
|
||||
} catch (err) {
|
||||
console.error('Password change error:', err);
|
||||
res.status(500).json({ error: 'Failed to change password' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/auth/cleanup-sessions
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user