feat(plan4): API CRUD /api/admin/users (TDD)

This commit is contained in:
2026-06-08 23:23:10 +02:00
parent 9303aab78e
commit f27b62a403
3 changed files with 150 additions and 0 deletions
+47
View File
@@ -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<string, string>
const data: Record<string, string> = {}
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 })
}
+61
View File
@@ -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<typeof getServerSession>
const mockFindMany = prisma.user.findMany as jest.MockedFunction<typeof prisma.user.findMany>
const mockCreate = prisma.user.create as jest.MockedFunction<typeof prisma.user.create>
const mockHash = bcrypt.hash as jest.MockedFunction<typeof bcrypt.hash>
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)
})
})
+42
View File
@@ -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<string, string>
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 })
}