feat: add admin dashboard and core API routes with Docker configuration

This commit is contained in:
2026-10-29 19:46:37 +01:00
parent 2fdabe1dfd
commit 4f4856d9dc
89 changed files with 1984 additions and 1034 deletions

View File

@@ -1,17 +1,20 @@
import { NextRequest, NextResponse } from 'next/server';
import prisma from '@/lib/prisma';
import { getAuthUser } from '@/lib/auth-utils';
export async function POST(
req: NextRequest,
context: { params: Promise<{ id: string }> }
) {
const { id: paramId } = await context.params;
const userId = req.headers.get('x-user-id');
const authUser = await getAuthUser(req);
if (!userId) {
if (!authUser) {
return NextResponse.json({ error: 'Utilisateur non identifié' }, { status: 401 });
}
const userId = authUser.id;
try {
const { value, comment } = await req.json();
@@ -27,7 +30,7 @@ export async function POST(
{ slug: paramId }
]
},
select: { id: true }
select: { id: true, ownerId: true }
});
if (!business) {
@@ -35,7 +38,7 @@ export async function POST(
}
const businessId = business.id;
const ownerId = (business as any).ownerId;
const ownerId = business.ownerId;
// 2. Security: Don't allow owner to rate themselves
if (userId === ownerId) {
@@ -85,7 +88,7 @@ export async function POST(
// 3. Aggregate all ratings for this business
const stats = await prisma.rating.aggregate({
where: { businessId },
where: { businessId, status: 'APPROVED' },
_avg: { value: true },
_count: { value: true }
});

View File

@@ -1,17 +1,20 @@
import { NextRequest, NextResponse } from 'next/server';
import prisma from '@/lib/prisma';
import { getAuthUser } from '@/lib/auth-utils';
export async function GET(
req: NextRequest,
context: { params: Promise<{ id: string }> }
) {
const { id: paramId } = await context.params;
const userId = req.headers.get('x-user-id');
const authUser = await getAuthUser(req);
if (!userId) {
if (!authUser) {
return NextResponse.json({ rating: null });
}
const userId = authUser.id;
try {
// Resolve actual Business UUID if paramId is a slug
const business = await prisma.business.findFirst({

View File

@@ -2,8 +2,6 @@ import { NextRequest, NextResponse } from 'next/server'
import prisma from '@/lib/prisma'
import { USER_SAFE_SELECT, getAuthUser } from '@/lib/auth-utils'
type Params = { params: Promise<{ id: string }> }
// GET /api/businesses/[id]
export async function GET(
request: NextRequest,
@@ -50,11 +48,56 @@ export async function PATCH(
context: { params: Promise<{ id: string }> }
): Promise<Response> {
try {
const authUser = await getAuthUser(request)
if (!authUser) {
return NextResponse.json({ error: 'Authentification requise' }, { status: 401 })
}
const { id } = await context.params
const existing = await prisma.business.findUnique({
where: { id },
select: { ownerId: true }
})
if (!existing) {
return NextResponse.json({ error: 'Boutique non trouvée' }, { status: 404 })
}
const isAdmin = authUser.role === 'ADMIN'
const isOwner = existing.ownerId === authUser.id
if (!isOwner && !isAdmin) {
return NextResponse.json({ error: 'Non autorisé' }, { status: 403 })
}
const body = await request.json()
// Whitelist allowed fields for owners; sensitive fields require ADMIN role
const allowedOwnerFields = [
'name', 'slug', 'category', 'categoryId', 'suggestedCategory',
'location', 'countryId', 'city', 'description', 'logoUrl',
'coverUrl', 'coverPosition', 'coverZoom', 'videoUrl',
'contactEmail', 'contactPhone', 'websiteUrl', 'socialLinks',
'showEmail', 'showPhone', 'showSocials', 'tags',
'founderName', 'founderImageUrl', 'keyMetric', 'isActive'
]
const allowedAdminFields = [
...allowedOwnerFields,
'verified', 'plan', 'isFeatured', 'isHomeFeatured',
'isSuspended', 'suspensionReason', 'metaTitle', 'metaDescription'
]
const permittedKeys = isAdmin ? allowedAdminFields : allowedOwnerFields
const updateData: Record<string, any> = {}
for (const key of permittedKeys) {
if (body[key] !== undefined) {
updateData[key] = body[key]
}
}
const business = await prisma.business.update({
where: { id },
data: body,
data: updateData,
include: {
owner: {
select: USER_SAFE_SELECT
@@ -75,8 +118,12 @@ export async function DELETE(
context: { params: Promise<{ id: string }> }
): Promise<Response> {
try {
const authUser = await getAuthUser(request)
if (!authUser) {
return NextResponse.json({ error: 'Authentification requise' }, { status: 401 })
}
const { id } = await context.params
const headerUserId = request.headers.get('x-user-id')
// 1. Find business to check owner
const business = await prisma.business.findUnique({
@@ -89,9 +136,8 @@ export async function DELETE(
}
// 2. Auth check (Owner or Admin)
const user = headerUserId ? await prisma.user.findUnique({ where: { id: headerUserId } }) : null
const isAdmin = user?.role === 'ADMIN'
const isOwner = business.ownerId === headerUserId
const isAdmin = authUser.role === 'ADMIN'
const isOwner = business.ownerId === authUser.id
if (!isOwner && !isAdmin) {
return NextResponse.json({ error: 'Non autorisé' }, { status: 403 })
@@ -101,13 +147,13 @@ export async function DELETE(
await prisma.business.delete({ where: { id } })
// 4. Update user role if they don't have other businesses
if (headerUserId && !isAdmin) {
if (!isAdmin) {
const otherBusinesses = await prisma.business.findFirst({
where: { ownerId: headerUserId }
where: { ownerId: authUser.id }
})
if (!otherBusinesses) {
await prisma.user.update({
where: { id: headerUserId },
where: { id: authUser.id },
data: { role: 'VISITOR' }
})
}

View File

@@ -1,17 +1,18 @@
import { NextRequest, NextResponse } from 'next/server'
import prisma from '@/lib/prisma'
import { getAuthUser } from '@/lib/auth-utils'
// GET /api/businesses/me — Fetch business for the current user
export async function GET(request: NextRequest) {
try {
const userId = request.headers.get('x-user-id')
const authUser = await getAuthUser(request)
if (!userId) {
if (!authUser) {
return NextResponse.json({ error: 'Utilisateur non identifié' }, { status: 401 })
}
const business = await prisma.business.findFirst({
where: { ownerId: userId },
where: { ownerId: authUser.id },
include: { offers: true }
})

View File

@@ -91,16 +91,15 @@ export async function GET(request: NextRequest) {
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const headerUserId = request.headers.get('x-user-id')
const { ownerId: bodyOwnerId, ...data } = body
const ownerId = bodyOwnerId || headerUserId
if (!ownerId) {
return NextResponse.json({ error: 'ownerId est requis (via body ou x-user-id header)' }, { status: 400 })
const authUser = await getAuthUser(request)
if (!authUser) {
return NextResponse.json({ error: 'Authentification requise' }, { status: 401 })
}
const body = await request.json()
const { ownerId: _ignoredOwnerId, ...data } = body
const ownerId = authUser.id
// 1. Sanitize & Validate data
const cleanData: any = {
name: data.name || "",
@@ -124,11 +123,7 @@ export async function POST(request: NextRequest) {
showEmail: data.showEmail ?? true,
showPhone: data.showPhone ?? true,
showSocials: data.showSocials ?? true,
verified: data.verified ?? false,
viewCount: data.viewCount ?? 0,
rating: data.rating ?? 0,
tags: Array.isArray(data.tags) ? data.tags : [],
isFeatured: data.isFeatured ?? false,
founderName: data.founderName || null,
founderImageUrl: data.founderImageUrl || null,
keyMetric: data.keyMetric || null,
@@ -190,11 +185,13 @@ export async function POST(request: NextRequest) {
}
});
// Automatic role upgrade
await prisma.user.update({
where: { id: ownerId },
data: { role: 'ENTREPRENEUR' }
});
// Automatic role upgrade (only if VISITOR, never downgrade ADMIN)
if (authUser.role === 'VISITOR') {
await prisma.user.update({
where: { id: ownerId },
data: { role: 'ENTREPRENEUR' }
});
}
}
console.log('Business saved successfully:', business.id);