From b40f59bec6d6509599af8e620dc979046568143b Mon Sep 17 00:00:00 2001 From: Julien Froidefond Date: Sat, 28 Feb 2026 11:06:42 +0100 Subject: [PATCH] refactor: convert admin user management to Server Actions - Add src/app/actions/admin.ts with updateUserRoles, deleteUser, resetUserPassword - Update EditUserDialog, DeleteUserDialog, ResetPasswordDialog to use Server Actions - Remove admin users API routes (PATCH/DELETE/PUT) --- docs/server-actions-plan.md | 3 + src/app/actions/admin.ts | 72 ++++++++++++++++ .../admin/users/[userId]/password/route.ts | 59 ------------- src/app/api/admin/users/[userId]/route.ts | 83 ------------------- src/components/admin/DeleteUserDialog.tsx | 10 +-- src/components/admin/EditUserDialog.tsx | 12 +-- src/components/admin/ResetPasswordDialog.tsx | 12 +-- 7 files changed, 87 insertions(+), 164 deletions(-) create mode 100644 src/app/actions/admin.ts delete mode 100644 src/app/api/admin/users/[userId]/password/route.ts delete mode 100644 src/app/api/admin/users/[userId]/route.ts diff --git a/docs/server-actions-plan.md b/docs/server-actions-plan.md index 8797af8..9a1bbaf 100644 --- a/docs/server-actions-plan.md +++ b/docs/server-actions-plan.md @@ -15,6 +15,9 @@ | `POST /api/komga/config` | `saveKomgaConfig()` | ✅ Done | | `PUT /api/user/password` | `changePassword()` | ✅ Done | | `POST /api/auth/register` | `registerUser()` | ✅ Done | +| `PATCH /api/admin/users/[userId]` | `updateUserRoles()` | ✅ Done | +| `DELETE /api/admin/users/[userId]` | `deleteUser()` | ✅ Done | +| `PUT /api/admin/users/[userId]/password` | `resetUserPassword()` | ✅ Done | --- diff --git a/src/app/actions/admin.ts b/src/app/actions/admin.ts new file mode 100644 index 0000000..6645803 --- /dev/null +++ b/src/app/actions/admin.ts @@ -0,0 +1,72 @@ +"use server"; + +import { AdminService } from "@/lib/services/admin.service"; +import { ERROR_CODES } from "@/constants/errorCodes"; +import { AppError } from "@/utils/errors"; +import { AuthServerService } from "@/lib/services/auth-server.service"; + +/** + * Met à jour les rôles d'un utilisateur + */ +export async function updateUserRoles( + userId: string, + roles: string[] +): Promise<{ success: boolean; message: string }> { + try { + if (roles.length === 0) { + return { success: false, message: "L'utilisateur doit avoir au moins un rôle" }; + } + + await AdminService.updateUserRoles(userId, roles); + + return { success: true, message: "Rôles mis à jour" }; + } catch (error) { + if (error instanceof AppError) { + return { success: false, message: error.message }; + } + return { success: false, message: "Erreur lors de la mise à jour des rôles" }; + } +} + +/** + * Supprime un utilisateur + */ +export async function deleteUser( + userId: string +): Promise<{ success: boolean; message: string }> { + try { + await AdminService.deleteUser(userId); + return { success: true, message: "Utilisateur supprimé" }; + } catch (error) { + if (error instanceof AppError) { + return { success: false, message: error.message }; + } + return { success: false, message: "Erreur lors de la suppression" }; + } +} + +/** + * Réinitialise le mot de passe d'un utilisateur + */ +export async function resetUserPassword( + userId: string, + newPassword: string +): Promise<{ success: boolean; message: string }> { + try { + if (!AuthServerService.isPasswordStrong(newPassword)) { + return { + success: false, + message: "Le mot de passe doit contenir au moins 8 caractères, une majuscule et un chiffre", + }; + } + + await AdminService.resetUserPassword(userId, newPassword); + + return { success: true, message: "Mot de passe réinitialisé" }; + } catch (error) { + if (error instanceof AppError) { + return { success: false, message: error.message }; + } + return { success: false, message: "Erreur lors de la réinitialisation du mot de passe" }; + } +} diff --git a/src/app/api/admin/users/[userId]/password/route.ts b/src/app/api/admin/users/[userId]/password/route.ts deleted file mode 100644 index 2d70029..0000000 --- a/src/app/api/admin/users/[userId]/password/route.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { AdminService } from "@/lib/services/admin.service"; -import { AppError } from "@/utils/errors"; -import { AuthServerService } from "@/lib/services/auth-server.service"; -import logger from "@/lib/logger"; - -export async function PUT( - request: NextRequest, - { params }: { params: Promise<{ userId: string }> } -) { - try { - const { userId } = await params; - const body = await request.json(); - const { newPassword } = body; - - if (!newPassword) { - return NextResponse.json({ error: "Nouveau mot de passe manquant" }, { status: 400 }); - } - - // Vérifier que le mot de passe est fort - if (!AuthServerService.isPasswordStrong(newPassword)) { - return NextResponse.json( - { - error: "Le mot de passe doit contenir au moins 8 caractères, une majuscule et un chiffre", - }, - { status: 400 } - ); - } - - await AdminService.resetUserPassword(userId, newPassword); - - return NextResponse.json({ success: true }); - } catch (error) { - logger.error({ err: error }, "Erreur lors de la réinitialisation du mot de passe:"); - - if (error instanceof AppError) { - return NextResponse.json( - { error: error.message, code: error.code }, - { - status: - error.code === "AUTH_FORBIDDEN" - ? 403 - : error.code === "AUTH_UNAUTHENTICATED" - ? 401 - : error.code === "AUTH_USER_NOT_FOUND" - ? 404 - : error.code === "ADMIN_CANNOT_RESET_OWN_PASSWORD" - ? 400 - : 500, - } - ); - } - - return NextResponse.json( - { error: "Erreur lors de la réinitialisation du mot de passe" }, - { status: 500 } - ); - } -} diff --git a/src/app/api/admin/users/[userId]/route.ts b/src/app/api/admin/users/[userId]/route.ts deleted file mode 100644 index 4a419f5..0000000 --- a/src/app/api/admin/users/[userId]/route.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { AdminService } from "@/lib/services/admin.service"; -import { AppError } from "@/utils/errors"; -import logger from "@/lib/logger"; - -export async function PATCH( - request: NextRequest, - { params }: { params: Promise<{ userId: string }> } -) { - try { - const { userId } = await params; - const body = await request.json(); - const { roles } = body; - - if (!roles || !Array.isArray(roles)) { - return NextResponse.json({ error: "Rôles invalides" }, { status: 400 }); - } - - await AdminService.updateUserRoles(userId, roles); - - return NextResponse.json({ success: true }); - } catch (error) { - logger.error({ err: error }, "Erreur lors de la mise à jour de l'utilisateur:"); - - if (error instanceof AppError) { - return NextResponse.json( - { error: error.message, code: error.code }, - { - status: - error.code === "AUTH_FORBIDDEN" - ? 403 - : error.code === "AUTH_UNAUTHENTICATED" - ? 401 - : error.code === "AUTH_USER_NOT_FOUND" - ? 404 - : 500, - } - ); - } - - return NextResponse.json( - { error: "Erreur lors de la mise à jour de l'utilisateur" }, - { status: 500 } - ); - } -} - -export async function DELETE( - request: NextRequest, - { params }: { params: Promise<{ userId: string }> } -) { - try { - const { userId } = await params; - await AdminService.deleteUser(userId); - - return NextResponse.json({ success: true }); - } catch (error) { - logger.error({ err: error }, "Erreur lors de la suppression de l'utilisateur:"); - - if (error instanceof AppError) { - return NextResponse.json( - { error: error.message, code: error.code }, - { - status: - error.code === "AUTH_FORBIDDEN" - ? 403 - : error.code === "AUTH_UNAUTHENTICATED" - ? 401 - : error.code === "AUTH_USER_NOT_FOUND" - ? 404 - : error.code === "ADMIN_CANNOT_DELETE_SELF" - ? 400 - : 500, - } - ); - } - - return NextResponse.json( - { error: "Erreur lors de la suppression de l'utilisateur" }, - { status: 500 } - ); - } -} diff --git a/src/components/admin/DeleteUserDialog.tsx b/src/components/admin/DeleteUserDialog.tsx index 64d465a..de431af 100644 --- a/src/components/admin/DeleteUserDialog.tsx +++ b/src/components/admin/DeleteUserDialog.tsx @@ -13,6 +13,7 @@ import { } from "@/components/ui/alert-dialog"; import { useToast } from "@/components/ui/use-toast"; import type { AdminUserData } from "@/lib/services/admin.service"; +import { deleteUser } from "@/app/actions/admin"; interface DeleteUserDialogProps { user: AdminUserData; @@ -29,13 +30,10 @@ export function DeleteUserDialog({ user, open, onOpenChange, onSuccess }: Delete setIsLoading(true); try { - const response = await fetch(`/api/admin/users/${user.id}`, { - method: "DELETE", - }); + const result = await deleteUser(user.id); - if (!response.ok) { - const data = await response.json(); - throw new Error(data.error || "Erreur lors de la suppression"); + if (!result.success) { + throw new Error(result.message); } toast({ diff --git a/src/components/admin/EditUserDialog.tsx b/src/components/admin/EditUserDialog.tsx index 0b2b619..e400b54 100644 --- a/src/components/admin/EditUserDialog.tsx +++ b/src/components/admin/EditUserDialog.tsx @@ -14,6 +14,7 @@ import { Checkbox } from "@/components/ui/checkbox"; import { Label } from "@/components/ui/label"; import { useToast } from "@/components/ui/use-toast"; import type { AdminUserData } from "@/lib/services/admin.service"; +import { updateUserRoles } from "@/app/actions/admin"; interface EditUserDialogProps { user: AdminUserData; @@ -51,15 +52,10 @@ export function EditUserDialog({ user, open, onOpenChange, onSuccess }: EditUser setIsLoading(true); try { - const response = await fetch(`/api/admin/users/${user.id}`, { - method: "PATCH", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ roles: selectedRoles }), - }); + const result = await updateUserRoles(user.id, selectedRoles); - if (!response.ok) { - const data = await response.json(); - throw new Error(data.error || "Erreur lors de la mise à jour"); + if (!result.success) { + throw new Error(result.message); } toast({ diff --git a/src/components/admin/ResetPasswordDialog.tsx b/src/components/admin/ResetPasswordDialog.tsx index 622899a..e22ff95 100644 --- a/src/components/admin/ResetPasswordDialog.tsx +++ b/src/components/admin/ResetPasswordDialog.tsx @@ -15,6 +15,7 @@ import { Label } from "@/components/ui/label"; import { useToast } from "@/components/ui/use-toast"; import { Lock } from "lucide-react"; import type { AdminUserData } from "@/lib/services/admin.service"; +import { resetUserPassword } from "@/app/actions/admin"; interface ResetPasswordDialogProps { user: AdminUserData; @@ -65,15 +66,10 @@ export function ResetPasswordDialog({ setIsLoading(true); try { - const response = await fetch(`/api/admin/users/${user.id}/password`, { - method: "PUT", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ newPassword }), - }); + const result = await resetUserPassword(user.id, newPassword); - if (!response.ok) { - const data = await response.json(); - throw new Error(data.error || "Erreur lors de la réinitialisation"); + if (!result.success) { + throw new Error(result.message); } toast({