diff --git a/src/app/api/admin/users/[id]/route.ts b/src/app/api/admin/users/[id]/route.ts new file mode 100644 index 0000000..5daad54 --- /dev/null +++ b/src/app/api/admin/users/[id]/route.ts @@ -0,0 +1,47 @@ +import { NextRequest, NextResponse } from 'next/server' +import { getServerSession } from 'next-auth' +import { authOptions } from '@/lib/auth' +import { prisma } from '@/lib/prisma' +import bcrypt from 'bcryptjs' + +async function requireAdmin() { + const session = await getServerSession(authOptions) + if (!session || session.user.role !== 'admin') return null + return session +} + +export async function PUT(req: NextRequest, { params }: { params: { id: string } }) { + const session = await requireAdmin() + if (!session) return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) + + const body = await req.json().catch(() => ({})) + const { name, role, password } = body as Record + + const data: Record = {} + if (name) data.name = name + if (role) data.role = role + if (password) data.password = await bcrypt.hash(password, 12) + + if (Object.keys(data).length === 0) { + return NextResponse.json({ error: 'Aucun champ à mettre à jour' }, { status: 400 }) + } + + const user = await prisma.user.update({ + where: { id: params.id }, + data, + select: { id: true, email: true, name: true, role: true, createdAt: true }, + }) + return NextResponse.json(user) +} + +export async function DELETE(req: NextRequest, { params }: { params: { id: string } }) { + const session = await requireAdmin() + if (!session) return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) + + if (params.id === session.user.id) { + return NextResponse.json({ error: 'Impossible de se supprimer soi-même' }, { status: 400 }) + } + + await prisma.user.delete({ where: { id: params.id } }) + return NextResponse.json({ ok: true }) +} diff --git a/src/app/api/admin/users/route.test.ts b/src/app/api/admin/users/route.test.ts new file mode 100644 index 0000000..ec013b2 --- /dev/null +++ b/src/app/api/admin/users/route.test.ts @@ -0,0 +1,61 @@ +import { GET, POST } from './route' +import { getServerSession } from 'next-auth' +import { prisma } from '@/lib/prisma' +import bcrypt from 'bcryptjs' + +jest.mock('next-auth') +jest.mock('@/lib/prisma', () => ({ prisma: { user: { findMany: jest.fn(), create: jest.fn() } } })) +jest.mock('bcryptjs') + +const mockSession = getServerSession as jest.MockedFunction +const mockFindMany = prisma.user.findMany as jest.MockedFunction +const mockCreate = prisma.user.create as jest.MockedFunction +const mockHash = bcrypt.hash as jest.MockedFunction + +const adminSession = { user: { id: 'admin-id', role: 'admin' } } +const gestSession = { user: { id: 'gest-id', role: 'gestionnaire' } } + +describe('GET /api/admin/users', () => { + it('retourne 403 si non admin', async () => { + mockSession.mockResolvedValue(gestSession as never) + const res = await GET(new Request('http://localhost') as never) + expect(res.status).toBe(403) + }) + + it('retourne la liste des utilisateurs', async () => { + mockSession.mockResolvedValue(adminSession as never) + mockFindMany.mockResolvedValue([ + { id: '1', email: 'a@b.com', name: 'Alice', role: 'admin', createdAt: new Date() } as never, + ]) + const res = await GET(new Request('http://localhost') as never) + const body = await res.json() + expect(body).toHaveLength(1) + expect(body[0].email).toBe('a@b.com') + expect(body[0].password).toBeUndefined() + }) +}) + +describe('POST /api/admin/users', () => { + it('retourne 403 si non admin', async () => { + mockSession.mockResolvedValue(gestSession as never) + const res = await POST(new Request('http://localhost', { method: 'POST', body: '{}' }) as never) + expect(res.status).toBe(403) + }) + + it('crée un utilisateur avec mot de passe hashé', async () => { + mockSession.mockResolvedValue(adminSession as never) + mockHash.mockResolvedValue('hashed' as never) + mockCreate.mockResolvedValue({ id: 'new', email: 'b@c.com', name: 'Bob', role: 'gestionnaire', createdAt: new Date() } as never) + const body = JSON.stringify({ email: 'b@c.com', name: 'Bob', password: 'pass123', role: 'gestionnaire' }) + const res = await POST(new Request('http://localhost', { method: 'POST', body, headers: { 'Content-Type': 'application/json' } }) as never) + expect(res.status).toBe(201) + expect(mockHash).toHaveBeenCalledWith('pass123', 12) + }) + + it('retourne 400 si champs manquants', async () => { + mockSession.mockResolvedValue(adminSession as never) + const body = JSON.stringify({ email: 'b@c.com' }) + const res = await POST(new Request('http://localhost', { method: 'POST', body, headers: { 'Content-Type': 'application/json' } }) as never) + expect(res.status).toBe(400) + }) +}) diff --git a/src/app/api/admin/users/route.ts b/src/app/api/admin/users/route.ts new file mode 100644 index 0000000..7669e90 --- /dev/null +++ b/src/app/api/admin/users/route.ts @@ -0,0 +1,42 @@ +import { NextRequest, NextResponse } from 'next/server' +import { getServerSession } from 'next-auth' +import { authOptions } from '@/lib/auth' +import { prisma } from '@/lib/prisma' +import bcrypt from 'bcryptjs' + +async function requireAdmin() { + const session = await getServerSession(authOptions) + if (!session || session.user.role !== 'admin') return null + return session +} + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export async function GET(_req: NextRequest) { + const session = await requireAdmin() + if (!session) return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) + + const users = await prisma.user.findMany({ + orderBy: { createdAt: 'asc' }, + select: { id: true, email: true, name: true, role: true, createdAt: true }, + }) + return NextResponse.json(users) +} + +export async function POST(req: NextRequest) { + const session = await requireAdmin() + if (!session) return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) + + const body = await req.json().catch(() => ({})) + const { email, name, password, role } = body as Record + + if (!email || !name || !password || !role) { + return NextResponse.json({ error: 'email, name, password et role requis' }, { status: 400 }) + } + + const hashed = await bcrypt.hash(password, 12) + const user = await prisma.user.create({ + data: { email, name, password: hashed, role }, + select: { id: true, email: true, name: true, role: true, createdAt: true }, + }) + return NextResponse.json(user, { status: 201 }) +}