feat: add admin profile editing capability

Added admin endpoint and functionality for editing user profiles from the
report management interface:

Backend Changes:
- Added PUT /api/users/:id endpoint for admin-only profile editing
- Reuses existing updateUserSettings logic with slug uniqueness validation
- Protected with adminGuard and csrfProtection
- Proper audit logging for admin profile edits

Frontend Changes:
- Added adminUpdateUser() method to UserService
- Updated editProfile() in admin-reports to load profile and allow bio editing
- Uses prompt for quick bio editing (can be expanded to full form later)
- Shows success/error toasts and refreshes report list after edit

Admins can now quickly edit reported user profiles directly from the
report management interface.
This commit is contained in:
2026-02-19 20:05:57 -08:00
committed by Naomi Carrigan
parent 6e884b9ae8
commit c154ef1897
3 changed files with 61 additions and 3 deletions
+34
View File
@@ -255,6 +255,40 @@ const usersRoutes: FastifyPluginAsync = async (app) => {
return user;
}
);
app.put<{ Params: { id: string }; Body: UpdateUserSettingsBody; Reply: User | { error: string } }>(
"/:id",
{
preValidation: [app.authenticate, adminGuard],
preHandler: [app.csrfProtection],
},
async (request, reply) => {
const { id } = request.params;
const updates = request.body;
// If slug is being updated, check if it's unique
if (updates.slug) {
const existingUser = await userService.getUserBySlug(updates.slug);
if (existingUser && existingUser.id !== id) {
return reply.code(400).send({ error: "Slug already taken" });
}
}
const updatedUser = await userService.updateUserSettings(id, updates);
if (!updatedUser) {
return reply.code(404).send({ error: "User not found" });
}
await AuditService.logFromRequest(request, {
action: AuditAction.entryUpdate,
category: AuditCategory.admin,
targetUserId: id,
details: `Admin updated profile for user: ${updatedUser.username}`,
});
return updatedUser;
}
);
};
export default usersRoutes;
@@ -1529,10 +1529,30 @@ export class AdminReportsComponent implements OnInit {
editProfile(report: ProfileReportWithUsers): void {
const userId = report.reportedUser.id;
const username = report.reportedUser.username;
// Navigate to profile page using ObjectId
this.router.navigate(['/profile', userId]);
this.toastService.success('Navigate to profile to edit');
// Load the full profile first to get current values
this.userService.getProfile(userId).subscribe({
next: (profile) => {
const newBio = prompt(`Edit bio for ${username}:`, profile.bio || '');
if (newBio !== null) {
this.userService.adminUpdateUser(userId, { bio: newBio }).subscribe({
next: () => {
this.toastService.success('Profile updated successfully');
this.loadReports();
this.closeProfileReviewModal();
},
error: (err) => {
this.toastService.error(err.message ?? 'Failed to update profile');
}
});
}
},
error: (err) => {
this.toastService.error(err.message ?? 'Failed to load profile');
}
});
}
makeProfilePrivate(report: ProfileReportWithUsers): void {
@@ -85,4 +85,8 @@ export class UserService {
makeProfilePrivate(userId: string): Observable<User> {
return this.api.post<User>(`/users/${userId}/make-private`, {});
}
adminUpdateUser(userId: string, settings: UpdateUserSettingsRequest): Observable<User> {
return this.api.put<User>(`/users/${userId}`, settings);
}
}