feat: user profile self-service + admin password reset
- Add PUT /api/auth/profile for users to change their own email/password - Add POST /api/admin/users/:id/reset-password for admin password resets - Add updateProfileSchema and resetPasswordSchema validation (Zod) - Create Profile.jsx page with email change and password change forms - Add Reset Password button with inline form to ManageUsers in Admin panel - Add /profile route and Account nav link in Layout Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,7 @@ const {
|
||||
createHomeownerSchema,
|
||||
updateHomeownerSchema,
|
||||
reportQuerySchema,
|
||||
resetPasswordSchema,
|
||||
validateBody,
|
||||
validateQuery,
|
||||
} = require('../utils/validation');
|
||||
@@ -860,4 +861,34 @@ router.get('/overtime', async (req, res) => {
|
||||
// ─────────────── GET /api/timesheets/overtime ───────────────
|
||||
// (mounted at /api/timesheets/overtime in timesheets router)
|
||||
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// ADMIN PASSWORD RESET
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
// POST /api/admin/users/:id/reset-password
|
||||
router.post('/users/:id/reset-password', validateBody(resetPasswordSchema), async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { newPassword } = req.validated;
|
||||
|
||||
const user = await req.prisma.user.findUnique({ where: { id } });
|
||||
if (!user) {
|
||||
return res.status(404).json({ error: 'User not found' });
|
||||
}
|
||||
|
||||
const passwordHash = await bcrypt.hash(newPassword, 12);
|
||||
|
||||
await req.prisma.user.update({
|
||||
where: { id },
|
||||
data: { passwordHash, refreshToken: null },
|
||||
});
|
||||
|
||||
res.json({ message: 'Password reset successfully' });
|
||||
} catch (err) {
|
||||
console.error('Admin password reset error:', err);
|
||||
res.status(500).json({ error: 'Failed to reset password' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -12,6 +12,7 @@ const {
|
||||
loginSchema,
|
||||
registerSchema,
|
||||
refreshSchema,
|
||||
updateProfileSchema,
|
||||
validateBody,
|
||||
} = require('../utils/validation');
|
||||
|
||||
@@ -207,4 +208,54 @@ router.post('/logout', authenticate, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// ─────────────── PUT /api/auth/profile ───────────────
|
||||
router.put('/profile', authenticate, validateBody(updateProfileSchema), async (req, res) => {
|
||||
try {
|
||||
const { currentPassword, newEmail, newPassword } = req.validated;
|
||||
|
||||
const user = await req.prisma.user.findUnique({ where: { id: req.user.id } });
|
||||
if (!user) {
|
||||
return res.status(404).json({ error: 'User not found' });
|
||||
}
|
||||
|
||||
const passwordValid = await bcrypt.compare(currentPassword, user.passwordHash);
|
||||
if (!passwordValid) {
|
||||
return res.status(401).json({ error: 'Current password is incorrect' });
|
||||
}
|
||||
|
||||
const updateData = {};
|
||||
|
||||
if (newEmail) {
|
||||
const email = newEmail.toLowerCase();
|
||||
if (email !== user.email) {
|
||||
const existing = await req.prisma.user.findUnique({ where: { email } });
|
||||
if (existing) {
|
||||
return res.status(409).json({ error: 'Email already in use' });
|
||||
}
|
||||
updateData.email = email;
|
||||
}
|
||||
}
|
||||
|
||||
if (newPassword) {
|
||||
updateData.passwordHash = await bcrypt.hash(newPassword, 12);
|
||||
}
|
||||
|
||||
if (Object.keys(updateData).length === 0) {
|
||||
return res.status(400).json({ error: 'No changes to apply' });
|
||||
}
|
||||
|
||||
const updated = await req.prisma.user.update({
|
||||
where: { id: req.user.id },
|
||||
data: updateData,
|
||||
select: { id: true, email: true, name: true, role: true, isActive: true, createdAt: true },
|
||||
});
|
||||
|
||||
res.json({ user: updated });
|
||||
} catch (err) {
|
||||
console.error('Profile update error:', err);
|
||||
res.status(500).json({ error: 'Failed to update profile' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -90,6 +90,21 @@ const reportQuerySchema = z.object({
|
||||
status: z.enum(['draft', 'submitted', 'approved', 'rejected']).optional(),
|
||||
});
|
||||
|
||||
|
||||
// ──────────────────────────── Profile / Password Reset ────────────────────────────
|
||||
|
||||
const updateProfileSchema = z.object({
|
||||
currentPassword: z.string().min(1, "Current password is required").max(128),
|
||||
newEmail: z.string().email("Invalid email address").max(255).optional(),
|
||||
newPassword: z.string().min(8, "Password must be at least 8 characters").max(128).optional(),
|
||||
}).refine((data) => data.newEmail || data.newPassword, {
|
||||
message: "At least one of newEmail or newPassword must be provided",
|
||||
});
|
||||
|
||||
const resetPasswordSchema = z.object({
|
||||
newPassword: z.string().min(8, "Password must be at least 8 characters").max(128),
|
||||
});
|
||||
|
||||
// ──────────────────────────── Helpers ────────────────────────────
|
||||
|
||||
/**
|
||||
@@ -142,6 +157,8 @@ module.exports = {
|
||||
createHomeownerSchema,
|
||||
updateHomeownerSchema,
|
||||
reportQuerySchema,
|
||||
updateProfileSchema,
|
||||
resetPasswordSchema,
|
||||
validateBody,
|
||||
validateQuery,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user