feat: add admin dashboard and core API routes with Docker configuration
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import prisma from '../../../../../lib/prisma'
|
||||
import { isAdmin } from '@/lib/auth-utils'
|
||||
|
||||
// PATCH /api/admin/reports/[id] — Update report status
|
||||
export async function PATCH(
|
||||
@@ -7,21 +8,10 @@ export async function PATCH(
|
||||
context: { params: Promise<{ id: string }> }
|
||||
): Promise<Response> {
|
||||
try {
|
||||
const userId = request.headers.get('x-user-id')
|
||||
const { id } = await context.params
|
||||
const { status } = await request.json()
|
||||
|
||||
if (!userId) {
|
||||
return NextResponse.json({ error: 'Utilisateur non identifié' }, { status: 401 })
|
||||
}
|
||||
|
||||
// Check if user is admin
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: { role: true }
|
||||
})
|
||||
|
||||
if (!user || user.role !== 'ADMIN') {
|
||||
if (!(await isAdmin(request))) {
|
||||
return NextResponse.json({ error: 'Accès réservé aux administrateurs' }, { status: 403 })
|
||||
}
|
||||
|
||||
|
||||
@@ -1,22 +1,11 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import prisma from '@/lib/prisma'
|
||||
import { isAdmin } from '@/lib/auth-utils'
|
||||
|
||||
// GET /api/admin/reports — List all reports for moderation
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const userId = request.headers.get('x-user-id')
|
||||
|
||||
if (!userId) {
|
||||
return NextResponse.json({ error: 'Utilisateur non identifié' }, { status: 401 })
|
||||
}
|
||||
|
||||
// Check if user is admin
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: { role: true }
|
||||
})
|
||||
|
||||
if (!user || user.role !== 'ADMIN') {
|
||||
if (!(await isAdmin(request))) {
|
||||
return NextResponse.json({ error: 'Accès réservé aux administrateurs' }, { status: 403 })
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,37 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import prisma from '../../../../../lib/prisma';
|
||||
import { getAuthUser } from '@/lib/auth-utils';
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const authUser = await getAuthUser(request);
|
||||
if (!authUser) {
|
||||
return NextResponse.json({ error: 'Authentification requise' }, { status: 401 });
|
||||
}
|
||||
|
||||
const { id } = await params;
|
||||
|
||||
if (!id) {
|
||||
return NextResponse.json({ error: 'ID Business requis' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Verify ownership or ADMIN role
|
||||
const business = await prisma.business.findUnique({
|
||||
where: { id },
|
||||
select: { id: true, ownerId: true }
|
||||
});
|
||||
|
||||
if (!business) {
|
||||
return NextResponse.json({ error: 'Entreprise non trouvée' }, { status: 404 });
|
||||
}
|
||||
|
||||
if (business.ownerId !== authUser.id && authUser.role !== 'ADMIN') {
|
||||
return NextResponse.json({ error: 'Accès non autorisé' }, { status: 403 });
|
||||
}
|
||||
|
||||
// 1. Get total views from AnalyticsEvent
|
||||
const totalViews = await prisma.analyticsEvent.count({
|
||||
where: {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { isIP } from 'node:net';
|
||||
import prisma from '@/lib/prisma';
|
||||
import { getGeoInfo } from '@/lib/geo';
|
||||
|
||||
@@ -7,14 +8,15 @@ export async function POST(request: NextRequest) {
|
||||
const body = await request.json();
|
||||
const { type, path, label, value, metadata } = body;
|
||||
|
||||
if (!type || !path) {
|
||||
if (!type || !path || typeof type !== 'string' || typeof path !== 'string') {
|
||||
return NextResponse.json({ error: 'Type et Path requis' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Extract detailed info from headers
|
||||
const userAgent = request.headers.get('user-agent') || '';
|
||||
const referrer = request.headers.get('referer') || '';
|
||||
const ip = request.headers.get('x-forwarded-for')?.split(',')[0] || '127.0.0.1';
|
||||
const userAgent = (request.headers.get('user-agent') || '').slice(0, 500);
|
||||
const referrer = (request.headers.get('referer') || '').slice(0, 500);
|
||||
const rawForwardedIp = request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() || '127.0.0.1';
|
||||
const ip = isIP(rawForwardedIp) !== 0 ? rawForwardedIp : '127.0.0.1';
|
||||
|
||||
// Basic User-Agent parsing
|
||||
const isMobile = /mobile/i.test(userAgent);
|
||||
@@ -33,22 +35,22 @@ export async function POST(request: NextRequest) {
|
||||
else if (/iphone|ipad/i.test(userAgent)) os = 'iOS';
|
||||
|
||||
// Simple Country detection
|
||||
const { country, city } = await getGeoInfo(ip, request.headers);
|
||||
const { country } = await getGeoInfo(ip, request.headers);
|
||||
|
||||
const event = await prisma.analyticsEvent.create({
|
||||
data: {
|
||||
type,
|
||||
path,
|
||||
label: label || null,
|
||||
value: value || null,
|
||||
metadata: metadata || {},
|
||||
type: type.slice(0, 100),
|
||||
path: path.slice(0, 500),
|
||||
label: typeof label === 'string' ? label.slice(0, 200) : null,
|
||||
value: typeof value === 'number' ? value : null,
|
||||
metadata: metadata && typeof metadata === 'object' ? metadata : {},
|
||||
ip,
|
||||
country,
|
||||
browser,
|
||||
os,
|
||||
device,
|
||||
referrer,
|
||||
userId: metadata?.userId || null,
|
||||
userId: typeof metadata?.userId === 'string' ? metadata.userId : null,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -5,6 +5,15 @@ import crypto from 'crypto';
|
||||
|
||||
import { verifyCsrfToken, verifyOrigin } from '@/lib/csrf';
|
||||
|
||||
function escapeHtml(value: string): string {
|
||||
return value
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
// CSRF Protection
|
||||
const isOriginValid = verifyOrigin(req);
|
||||
@@ -52,13 +61,14 @@ export async function POST(req: NextRequest) {
|
||||
where: { id: 'singleton' }
|
||||
});
|
||||
|
||||
const siteName = settings?.siteName || 'Afrohub';
|
||||
const siteName = escapeHtml(settings?.siteName || 'Afrohub');
|
||||
const safeUserName = escapeHtml(user.name || '');
|
||||
const subject = settings?.resetPasswordSubject || 'Réinitialisation de votre mot de passe - Afrohub';
|
||||
|
||||
let html = settings?.resetPasswordTemplate || `
|
||||
<div style="font-family: sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #e5e7eb; border-radius: 8px;">
|
||||
<h2 style="color: #0d9488; text-align: center;">${siteName}</h2>
|
||||
<p>Bonjour ${user.name},</p>
|
||||
<p>Bonjour ${safeUserName},</p>
|
||||
<p>Vous avez demandé la réinitialisation de votre mot de passe. Cliquez sur le bouton ci-dessous pour choisir un nouveau mot de passe :</p>
|
||||
<div style="text-align: center; margin: 30px 0;">
|
||||
<a href="${resetUrl}" style="background-color: #0d9488; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px; font-weight: bold;">Réinitialiser mon mot de passe</a>
|
||||
@@ -71,7 +81,7 @@ export async function POST(req: NextRequest) {
|
||||
|
||||
// Replace placeholders
|
||||
html = html
|
||||
.replace(/{name}/g, user.name)
|
||||
.replace(/{name}/g, safeUserName)
|
||||
.replace(/{resetUrl}/g, resetUrl)
|
||||
.replace(/{siteName}/g, siteName);
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import prisma from '@/lib/prisma';
|
||||
import bcrypt from 'bcryptjs';
|
||||
|
||||
import { verifyCsrfToken, verifyOrigin } from '@/lib/csrf';
|
||||
import { signUserToken, setUserSessionCookie } from '@/lib/auth-utils';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
// CSRF Protection
|
||||
@@ -75,11 +76,34 @@ export async function POST(request: NextRequest) {
|
||||
);
|
||||
}
|
||||
|
||||
// Omit password from response
|
||||
const { password: _, ...userWithoutPassword } = user;
|
||||
// Sign and set HttpOnly session cookie
|
||||
const token = await signUserToken({
|
||||
userId: user.id,
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
});
|
||||
await setUserSessionCookie(token);
|
||||
|
||||
// Return only safe user fields (never leak password, resetToken, verificationToken)
|
||||
const safeUser = {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
avatar: user.avatar,
|
||||
bio: user.bio,
|
||||
location: user.location,
|
||||
phone: user.phone,
|
||||
countryId: user.countryId,
|
||||
isSuspended: user.isSuspended,
|
||||
suspensionReason: user.suspensionReason,
|
||||
emailVerified: user.emailVerified,
|
||||
createdAt: user.createdAt,
|
||||
updatedAt: user.updatedAt,
|
||||
};
|
||||
|
||||
return NextResponse.json(
|
||||
{ message: 'Connexion réussie', user: userWithoutPassword },
|
||||
{ message: 'Connexion réussie', user: safeUser },
|
||||
{ status: 200 }
|
||||
);
|
||||
|
||||
|
||||
13
app/api/auth/logout/route.ts
Normal file
13
app/api/auth/logout/route.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { clearUserSessionCookie } from '@/lib/auth-utils';
|
||||
|
||||
export async function POST() {
|
||||
try {
|
||||
await clearUserSessionCookie();
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Logout error:', error);
|
||||
return NextResponse.json({ error: 'Erreur lors de la déconnexion' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,15 @@ import crypto from 'crypto';
|
||||
|
||||
import { verifyCsrfToken, verifyOrigin } from '@/lib/csrf';
|
||||
|
||||
function escapeHtml(str: string): string {
|
||||
return str
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
// CSRF Protection
|
||||
const isOriginValid = verifyOrigin(request);
|
||||
@@ -19,7 +28,7 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
try {
|
||||
const rawBody = await request.json();
|
||||
const name = rawBody.name;
|
||||
const name = typeof rawBody.name === 'string' ? rawBody.name.trim() : '';
|
||||
const email = rawBody.email?.toLowerCase().trim();
|
||||
const password = rawBody.password;
|
||||
|
||||
@@ -31,9 +40,9 @@ export async function POST(request: NextRequest) {
|
||||
);
|
||||
}
|
||||
|
||||
if (password.length < 6) {
|
||||
if (password.length < 8) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Le mot de passe doit contenir au moins 6 caractères' },
|
||||
{ error: 'Le mot de passe doit contenir au moins 8 caractères' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
@@ -74,14 +83,16 @@ export async function POST(request: NextRequest) {
|
||||
where: { id: 'singleton' }
|
||||
});
|
||||
|
||||
const siteName = settings?.siteName || 'Afrohub';
|
||||
const safeName = escapeHtml(name);
|
||||
const safeEmail = escapeHtml(email);
|
||||
const siteName = escapeHtml(settings?.siteName || 'Afrohub');
|
||||
const subject = settings?.verifyEmailSubject || 'Vérification de votre compte - Afrohub';
|
||||
const verifyUrl = `${process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'}/api/auth/verify?token=${verificationToken}`;
|
||||
|
||||
let html = settings?.verifyEmailTemplate || `
|
||||
<div style="font-family: sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #e5e7eb; border-radius: 8px;">
|
||||
<h2 style="color: #0d9488; text-align: center;">${siteName}</h2>
|
||||
<p>Bonjour ${name},</p>
|
||||
<p>Bonjour ${safeName},</p>
|
||||
<p>Merci de vous être inscrit sur ${siteName}. Veuillez vérifier votre adresse email en cliquant sur le bouton ci-dessous :</p>
|
||||
<div style="text-align: center; margin: 30px 0;">
|
||||
<a href="${verifyUrl}" style="background-color: #0d9488; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px; font-weight: bold;">Vérifier mon compte</a>
|
||||
@@ -89,9 +100,9 @@ export async function POST(request: NextRequest) {
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Replace placeholders
|
||||
// Replace placeholders with HTML-escaped values
|
||||
html = html
|
||||
.replace(/{name}/g, name)
|
||||
.replace(/{name}/g, safeName)
|
||||
.replace(/{verifyUrl}/g, verifyUrl)
|
||||
.replace(/{siteName}/g, siteName);
|
||||
|
||||
@@ -107,7 +118,7 @@ export async function POST(request: NextRequest) {
|
||||
// Send admin notification email
|
||||
try {
|
||||
const adminEmail = settings?.contactEmail || 'support@afrohub.com';
|
||||
const adminSubject = `[Admin] Nouvelle inscription sur ${siteName} : ${name}`;
|
||||
const adminSubject = `[Admin] Nouvelle inscription sur ${settings?.siteName || 'Afrohub'} : ${name}`;
|
||||
const adminHtml = `
|
||||
<div style="font-family: sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #e5e7eb; border-radius: 8px;">
|
||||
<h2 style="color: #4f46e5; text-align: center;">Notification Admin - Inscription</h2>
|
||||
@@ -116,11 +127,11 @@ export async function POST(request: NextRequest) {
|
||||
<table style="width: 100%; border-collapse: collapse; margin-top: 15px;">
|
||||
<tr>
|
||||
<td style="padding: 8px; border-bottom: 1px solid #e5e7eb; font-weight: bold; width: 35%;">Nom complet :</td>
|
||||
<td style="padding: 8px; border-bottom: 1px solid #e5e7eb;">${name}</td>
|
||||
<td style="padding: 8px; border-bottom: 1px solid #e5e7eb;">${safeName}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 8px; border-bottom: 1px solid #e5e7eb; font-weight: bold;">Adresse email :</td>
|
||||
<td style="padding: 8px; border-bottom: 1px solid #e5e7eb;"><a href="mailto:${email}">${email}</a></td>
|
||||
<td style="padding: 8px; border-bottom: 1px solid #e5e7eb;"><a href="mailto:${safeEmail}">${safeEmail}</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 8px; border-bottom: 1px solid #e5e7eb; font-weight: bold;">Date d'inscription :</td>
|
||||
@@ -149,11 +160,25 @@ export async function POST(request: NextRequest) {
|
||||
console.error('Failed to send registration email(s):', mailError);
|
||||
}
|
||||
|
||||
// Omit password from response
|
||||
const { password: _, ...userWithoutPassword } = user;
|
||||
// Return only safe user fields (never leak password, verificationToken, resetToken)
|
||||
const safeUser = {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
avatar: user.avatar,
|
||||
bio: user.bio,
|
||||
location: user.location,
|
||||
phone: user.phone,
|
||||
countryId: user.countryId,
|
||||
isSuspended: user.isSuspended,
|
||||
emailVerified: user.emailVerified,
|
||||
createdAt: user.createdAt,
|
||||
updatedAt: user.updatedAt,
|
||||
};
|
||||
|
||||
return NextResponse.json(
|
||||
{ message: 'Compte créé avec succès', user: userWithoutPassword },
|
||||
{ message: 'Compte créé avec succès', user: safeUser },
|
||||
{ status: 201 }
|
||||
);
|
||||
|
||||
|
||||
@@ -4,6 +4,15 @@ import crypto from 'crypto';
|
||||
|
||||
import { verifyCsrfToken, verifyOrigin } from '@/lib/csrf';
|
||||
|
||||
function escapeHtml(value: string): string {
|
||||
return value
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
// CSRF Protection
|
||||
const isOriginValid = verifyOrigin(request);
|
||||
@@ -61,7 +70,8 @@ export async function POST(request: NextRequest) {
|
||||
where: { id: 'singleton' }
|
||||
});
|
||||
|
||||
const siteName = settings?.siteName || 'Afrohub';
|
||||
const siteName = escapeHtml(settings?.siteName || 'Afrohub');
|
||||
const safeUserName = escapeHtml(user.name || '');
|
||||
const subject = settings?.verifyEmailSubject || 'Vérification de votre compte - Afrohub';
|
||||
const verifyUrl = `${process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'}/api/auth/verify?token=${verificationToken}`;
|
||||
|
||||
@@ -78,7 +88,7 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
// Replace placeholders
|
||||
html = html
|
||||
.replace(/{name}/g, user.name)
|
||||
.replace(/{name}/g, safeUserName)
|
||||
.replace(/{verifyUrl}/g, verifyUrl)
|
||||
.replace(/{siteName}/g, siteName);
|
||||
|
||||
|
||||
@@ -18,10 +18,17 @@ export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const { token, password } = await request.json();
|
||||
|
||||
if (!token || !password) {
|
||||
if (!token || !password || typeof password !== 'string') {
|
||||
return NextResponse.json({ error: 'Token et mot de passe sont requis' }, { status: 400 });
|
||||
}
|
||||
|
||||
if (password.length < 8) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Le mot de passe doit contenir au moins 8 caractères' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const user = await prisma.user.findFirst({
|
||||
where: {
|
||||
resetToken: token,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import prisma from '../../../../lib/prisma'
|
||||
|
||||
type Params = { params: Promise<{ id: string }> }
|
||||
import { isAdmin } from '@/lib/auth-utils'
|
||||
|
||||
// GET /api/blog/[id]
|
||||
export async function GET(
|
||||
@@ -19,12 +18,16 @@ export async function GET(
|
||||
}
|
||||
}
|
||||
|
||||
// PATCH /api/blog/[id]
|
||||
// PATCH /api/blog/[id] (Admin only)
|
||||
export async function PATCH(
|
||||
request: NextRequest,
|
||||
context: { params: Promise<{ id: string }> }
|
||||
): Promise<Response> {
|
||||
try {
|
||||
if (!(await isAdmin(request))) {
|
||||
return NextResponse.json({ error: 'Accès réservé aux administrateurs' }, { status: 403 })
|
||||
}
|
||||
|
||||
const { id } = await context.params
|
||||
const body = await request.json()
|
||||
if (body.title && body.slug === undefined) {
|
||||
@@ -39,12 +42,16 @@ export async function PATCH(
|
||||
}
|
||||
}
|
||||
|
||||
// DELETE /api/blog/[id]
|
||||
// DELETE /api/blog/[id] (Admin only)
|
||||
export async function DELETE(
|
||||
_request: NextRequest,
|
||||
request: NextRequest,
|
||||
context: { params: Promise<{ id: string }> }
|
||||
): Promise<Response> {
|
||||
try {
|
||||
if (!(await isAdmin(request))) {
|
||||
return NextResponse.json({ error: 'Accès réservé aux administrateurs' }, { status: 403 })
|
||||
}
|
||||
|
||||
const { id } = await context.params
|
||||
await prisma.blogPost.delete({ where: { id } })
|
||||
return NextResponse.json({ success: true })
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import prisma from '@/lib/prisma'
|
||||
import { generateSlug } from '@/lib/utils'
|
||||
import { isAdmin } from '@/lib/auth-utils'
|
||||
|
||||
// GET /api/blog
|
||||
export async function GET() {
|
||||
@@ -21,9 +22,13 @@ export async function GET() {
|
||||
}
|
||||
}
|
||||
|
||||
// POST /api/blog
|
||||
// POST /api/blog (Admin only)
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
if (!(await isAdmin(request))) {
|
||||
return NextResponse.json({ error: 'Accès réservé aux administrateurs' }, { status: 403 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
if (!body.slug && body.title) {
|
||||
body.slug = generateSlug(body.title)
|
||||
|
||||
@@ -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 }
|
||||
});
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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' }
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
})
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import prisma from '../../../../../lib/prisma'
|
||||
import { getAuthUser } from '@/lib/auth-utils'
|
||||
|
||||
// PATCH /api/conversations/[id]/archive — Toggle archive status for the user
|
||||
export async function PATCH(
|
||||
@@ -7,18 +8,18 @@ export async function PATCH(
|
||||
context: { params: Promise<{ id: string }> }
|
||||
): Promise<Response> {
|
||||
try {
|
||||
const userId = request.headers.get('x-user-id')
|
||||
const authUser = await getAuthUser(request)
|
||||
const { id } = await context.params
|
||||
const { archived } = await request.json()
|
||||
|
||||
if (!userId) {
|
||||
if (!authUser) {
|
||||
return NextResponse.json({ error: 'Utilisateur non identifié' }, { status: 401 })
|
||||
}
|
||||
|
||||
const updatedParticipant = await prisma.conversationParticipant.update({
|
||||
where: {
|
||||
userId_conversationId: {
|
||||
userId: userId,
|
||||
userId: authUser.id,
|
||||
conversationId: id
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import prisma from '../../../../lib/prisma'
|
||||
import { getAuthUser } from '@/lib/auth-utils'
|
||||
|
||||
// GET /api/conversations/[id] — Get messages for a conversation
|
||||
export async function GET(
|
||||
@@ -7,13 +8,15 @@ export async function GET(
|
||||
context: { params: Promise<{ id: string }> }
|
||||
): Promise<Response> {
|
||||
try {
|
||||
const userId = request.headers.get('x-user-id')
|
||||
const authUser = await getAuthUser(request)
|
||||
const { id } = await context.params
|
||||
|
||||
if (!userId) {
|
||||
if (!authUser) {
|
||||
return NextResponse.json({ error: 'Utilisateur non identifié' }, { status: 401 })
|
||||
}
|
||||
|
||||
const userId = authUser.id
|
||||
|
||||
// Verify user is a participant
|
||||
const isParticipant = await prisma.conversationParticipant.findUnique({
|
||||
where: {
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import prisma from '@/lib/prisma'
|
||||
import { getAuthUser } from '@/lib/auth-utils'
|
||||
|
||||
// GET /api/conversations — List all conversations for the 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 userId = authUser.id
|
||||
const { searchParams } = new URL(request.url)
|
||||
const businessId = searchParams.get('businessId')
|
||||
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import prisma from '@/lib/prisma';
|
||||
import { getAuthUser } from '@/lib/auth-utils';
|
||||
|
||||
// GET /api/favorites - Get user's favorites
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const userId = request.headers.get('x-user-id');
|
||||
if (!userId) {
|
||||
const authUser = await getAuthUser(request);
|
||||
if (!authUser) {
|
||||
return NextResponse.json({ error: 'Utilisateur non identifié' }, { status: 401 });
|
||||
}
|
||||
|
||||
const favorites = await prisma.favorite.findMany({
|
||||
where: { userId },
|
||||
where: { userId: authUser.id },
|
||||
include: {
|
||||
business: {
|
||||
include: {
|
||||
@@ -32,17 +33,19 @@ export async function GET(request: NextRequest) {
|
||||
// POST /api/favorites - Toggle favorite
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const userId = request.headers.get('x-user-id');
|
||||
const authUser = await getAuthUser(request);
|
||||
const body = await request.json();
|
||||
const { businessId } = body;
|
||||
|
||||
if (!userId) {
|
||||
if (!authUser) {
|
||||
return NextResponse.json({ error: 'Utilisateur non identifié' }, { status: 401 });
|
||||
}
|
||||
if (!businessId) {
|
||||
return NextResponse.json({ error: 'businessId requis' }, { status: 400 });
|
||||
}
|
||||
|
||||
const userId = authUser.id;
|
||||
|
||||
// Resolve business ID (in case slug was passed)
|
||||
const business = await prisma.business.findFirst({
|
||||
where: {
|
||||
|
||||
@@ -1,9 +1,43 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { GoogleGenAI } from '@google/genai'
|
||||
import { getAuthUser } from '@/lib/auth-utils'
|
||||
|
||||
const RATE_LIMIT_WINDOW_MS = 60 * 1000 // 1 minute
|
||||
const MAX_REQUESTS_PER_WINDOW = 10
|
||||
const rateLimitMap = new Map<string, { count: number; resetAt: number }>()
|
||||
|
||||
function checkRateLimit(key: string): boolean {
|
||||
const now = Date.now()
|
||||
const entry = rateLimitMap.get(key)
|
||||
if (!entry || now > entry.resetAt) {
|
||||
rateLimitMap.set(key, { count: 1, resetAt: now + RATE_LIMIT_WINDOW_MS })
|
||||
return true
|
||||
}
|
||||
if (entry.count >= MAX_REQUESTS_PER_WINDOW) {
|
||||
return false
|
||||
}
|
||||
entry.count += 1
|
||||
return true
|
||||
}
|
||||
|
||||
// POST /api/gemini — AI content generation
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const authUser = await getAuthUser(request)
|
||||
if (!authUser) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Authentification requise' },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
if (!checkRateLimit(authUser.id)) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Trop de requêtes IA. Veuillez patienter une minute.' },
|
||||
{ status: 429 }
|
||||
)
|
||||
}
|
||||
|
||||
const apiKey = process.env.GEMINI_API_KEY
|
||||
if (!apiKey || apiKey === 'PLACEHOLDER_API_KEY') {
|
||||
return NextResponse.json(
|
||||
@@ -14,7 +48,10 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
const ai = new GoogleGenAI({ apiKey })
|
||||
const body = await request.json()
|
||||
const { action, name, category, keywords } = body
|
||||
const action = typeof body.action === 'string' ? body.action : ''
|
||||
const name = typeof body.name === 'string' ? body.name.trim().slice(0, 100) : ''
|
||||
const category = typeof body.category === 'string' ? body.category.trim().slice(0, 100) : ''
|
||||
const keywords = typeof body.keywords === 'string' ? body.keywords.trim().slice(0, 300) : ''
|
||||
|
||||
if (action === 'description') {
|
||||
const prompt = `
|
||||
|
||||
@@ -1,15 +1,5 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export async function GET() {
|
||||
return NextResponse.json(
|
||||
{
|
||||
status: 'ok',
|
||||
service: 'afrohub-front',
|
||||
timestamp: new Date().toISOString(),
|
||||
uptime: process.uptime(),
|
||||
},
|
||||
{ status: 200 }
|
||||
);
|
||||
return NextResponse.json({ status: 'ok', timestamp: new Date().toISOString() });
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import prisma from '../../../../lib/prisma'
|
||||
|
||||
type Params = { params: Promise<{ id: string }> }
|
||||
import { isAdmin } from '@/lib/auth-utils'
|
||||
|
||||
// GET /api/interviews/[id]
|
||||
export async function GET(
|
||||
@@ -19,12 +18,16 @@ export async function GET(
|
||||
}
|
||||
}
|
||||
|
||||
// PATCH /api/interviews/[id]
|
||||
// PATCH /api/interviews/[id] (Admin only)
|
||||
export async function PATCH(
|
||||
request: NextRequest,
|
||||
context: { params: Promise<{ id: string }> }
|
||||
): Promise<Response> {
|
||||
try {
|
||||
if (!(await isAdmin(request))) {
|
||||
return NextResponse.json({ error: 'Accès réservé aux administrateurs' }, { status: 403 })
|
||||
}
|
||||
|
||||
const { id } = await context.params
|
||||
const body = await request.json()
|
||||
const interview = await prisma.interview.update({ where: { id }, data: body })
|
||||
@@ -35,12 +38,16 @@ export async function PATCH(
|
||||
}
|
||||
}
|
||||
|
||||
// DELETE /api/interviews/[id]
|
||||
// DELETE /api/interviews/[id] (Admin only)
|
||||
export async function DELETE(
|
||||
_request: NextRequest,
|
||||
request: NextRequest,
|
||||
context: { params: Promise<{ id: string }> }
|
||||
): Promise<Response> {
|
||||
try {
|
||||
if (!(await isAdmin(request))) {
|
||||
return NextResponse.json({ error: 'Accès réservé aux administrateurs' }, { status: 403 })
|
||||
}
|
||||
|
||||
const { id } = await context.params
|
||||
await prisma.interview.delete({ where: { id } })
|
||||
return NextResponse.json({ success: true })
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import prisma from '@/lib/prisma'
|
||||
import { isAdmin } from '@/lib/auth-utils'
|
||||
|
||||
// GET /api/interviews
|
||||
export async function GET() {
|
||||
@@ -20,9 +21,13 @@ export async function GET() {
|
||||
}
|
||||
}
|
||||
|
||||
// POST /api/interviews
|
||||
// POST /api/interviews (Admin only)
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
if (!(await isAdmin(request))) {
|
||||
return NextResponse.json({ error: 'Accès réservé aux administrateurs' }, { status: 403 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const interview = await prisma.interview.create({ data: body })
|
||||
return NextResponse.json(interview, { status: 201 })
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import prisma from '../../../../../lib/prisma'
|
||||
import { getAuthUser } from '@/lib/auth-utils'
|
||||
|
||||
// POST /api/messages/[id]/report — Report a message for moderation
|
||||
export async function POST(
|
||||
@@ -7,14 +8,16 @@ export async function POST(
|
||||
context: { params: Promise<{ id: string }> }
|
||||
): Promise<Response> {
|
||||
try {
|
||||
const userId = request.headers.get('x-user-id')
|
||||
const authUser = await getAuthUser(request)
|
||||
const { id } = await context.params
|
||||
const { reason } = await request.json()
|
||||
|
||||
if (!userId) {
|
||||
if (!authUser) {
|
||||
return NextResponse.json({ error: 'Utilisateur non identifié' }, { status: 401 })
|
||||
}
|
||||
|
||||
const userId = authUser.id
|
||||
|
||||
// Verify message exists
|
||||
const message = await prisma.message.findUnique({
|
||||
where: { id }
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import prisma from '@/lib/prisma'
|
||||
import { getAuthUser } from '@/lib/auth-utils'
|
||||
|
||||
// POST /api/messages — Send a message or start a conversation
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const userId = request.headers.get('x-user-id')
|
||||
const authUser = await getAuthUser(request)
|
||||
const { businessId, conversationId, content } = await request.json()
|
||||
|
||||
if (!userId) {
|
||||
if (!authUser) {
|
||||
return NextResponse.json({ error: 'Utilisateur non identifié' }, { status: 401 })
|
||||
}
|
||||
|
||||
const userId = authUser.id
|
||||
|
||||
if (!content) {
|
||||
return NextResponse.json({ error: 'Contenu manquant' }, { status: 400 })
|
||||
}
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import prisma from '@/lib/prisma'
|
||||
import { getAuthUser } from '@/lib/auth-utils'
|
||||
|
||||
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 userId = authUser.id
|
||||
|
||||
const unreadCount = await prisma.message.count({
|
||||
where: {
|
||||
conversation: {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import prisma from '../../../../lib/prisma'
|
||||
|
||||
type Params = { params: Promise<{ id: string }> }
|
||||
import { getAuthUser } from '@/lib/auth-utils'
|
||||
|
||||
// GET /api/offers/[id]
|
||||
export async function GET(
|
||||
@@ -22,17 +21,43 @@ export async function GET(
|
||||
}
|
||||
}
|
||||
|
||||
// PATCH /api/offers/[id]
|
||||
// PATCH /api/offers/[id] (Owner or Admin only)
|
||||
export async function PATCH(
|
||||
request: NextRequest,
|
||||
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.offer.findUnique({
|
||||
where: { id },
|
||||
include: { business: { select: { ownerId: true } } }
|
||||
})
|
||||
|
||||
if (!existing) {
|
||||
return NextResponse.json({ error: 'Offre non trouvée' }, { status: 404 })
|
||||
}
|
||||
|
||||
if (existing.business.ownerId !== authUser.id && authUser.role !== 'ADMIN') {
|
||||
return NextResponse.json({ error: 'Non autorisé' }, { status: 403 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const { title, description, price, type, imageUrl } = body
|
||||
|
||||
const offer = await prisma.offer.update({
|
||||
where: { id },
|
||||
data: body,
|
||||
data: {
|
||||
...(title !== undefined ? { title } : {}),
|
||||
...(description !== undefined ? { description } : {}),
|
||||
...(price !== undefined ? { price } : {}),
|
||||
...(type !== undefined ? { type } : {}),
|
||||
...(imageUrl !== undefined ? { imageUrl } : {}),
|
||||
},
|
||||
include: { business: true },
|
||||
})
|
||||
return NextResponse.json(offer)
|
||||
@@ -42,13 +67,31 @@ export async function PATCH(
|
||||
}
|
||||
}
|
||||
|
||||
// DELETE /api/offers/[id]
|
||||
// DELETE /api/offers/[id] (Owner or Admin only)
|
||||
export async function DELETE(
|
||||
_request: NextRequest,
|
||||
request: NextRequest,
|
||||
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.offer.findUnique({
|
||||
where: { id },
|
||||
include: { business: { select: { ownerId: true } } }
|
||||
})
|
||||
|
||||
if (!existing) {
|
||||
return NextResponse.json({ error: 'Offre non trouvée' }, { status: 404 })
|
||||
}
|
||||
|
||||
if (existing.business.ownerId !== authUser.id && authUser.role !== 'ADMIN') {
|
||||
return NextResponse.json({ error: 'Non autorisé' }, { status: 403 })
|
||||
}
|
||||
|
||||
await prisma.offer.delete({ where: { id } })
|
||||
return NextResponse.json({ success: true })
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import prisma from '@/lib/prisma'
|
||||
import { getAuthUser } from '@/lib/auth-utils'
|
||||
|
||||
// GET /api/offers — optionally filter by businessId
|
||||
export async function GET(request: NextRequest) {
|
||||
@@ -22,11 +23,16 @@ export async function GET(request: NextRequest) {
|
||||
}
|
||||
}
|
||||
|
||||
// POST /api/offers
|
||||
// POST /api/offers (Owner or Admin only)
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const authUser = await getAuthUser(request)
|
||||
if (!authUser) {
|
||||
return NextResponse.json({ error: 'Authentification requise' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const { businessId } = body
|
||||
const { businessId, title, description, price, type, imageUrl } = body
|
||||
|
||||
if (!businessId) {
|
||||
return NextResponse.json({ error: 'businessId manquant' }, { status: 400 })
|
||||
@@ -44,6 +50,10 @@ export async function POST(request: NextRequest) {
|
||||
return NextResponse.json({ error: 'Entreprise non trouvée' }, { status: 404 })
|
||||
}
|
||||
|
||||
if (business.ownerId !== authUser.id && authUser.role !== 'ADMIN') {
|
||||
return NextResponse.json({ error: 'Non autorisé : vous n\'êtes pas propriétaire de cette entreprise' }, { status: 403 })
|
||||
}
|
||||
|
||||
// Fetch the specific limit for this tier from the database
|
||||
const planDetails = await prisma.pricingPlan.findUnique({
|
||||
where: { tier: business.plan }
|
||||
@@ -63,9 +73,16 @@ export async function POST(request: NextRequest) {
|
||||
}, { status: 403 })
|
||||
}
|
||||
|
||||
// 3. Create offer
|
||||
// 3. Create offer with whitelisted fields
|
||||
const offer = await prisma.offer.create({
|
||||
data: body,
|
||||
data: {
|
||||
businessId,
|
||||
title: title || '',
|
||||
description: description || '',
|
||||
price: price ?? '',
|
||||
type: type || 'PRODUCT',
|
||||
imageUrl: imageUrl || '',
|
||||
},
|
||||
include: { business: true },
|
||||
})
|
||||
return NextResponse.json(offer, { status: 201 })
|
||||
|
||||
@@ -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: ratingId } = 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 { reply } = await req.json();
|
||||
|
||||
|
||||
@@ -1,37 +1,52 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import prisma from '@/lib/prisma'
|
||||
import { getAuthUser, USER_SAFE_SELECT } from '@/lib/auth-utils'
|
||||
|
||||
// PATCH /api/users/me — Update personal user profile
|
||||
export async function PATCH(
|
||||
request: NextRequest,
|
||||
context: { params: Promise<any> }
|
||||
): Promise<Response> {
|
||||
// GET /api/users/me — Get current authenticated user profile
|
||||
export async function GET(request: NextRequest): Promise<Response> {
|
||||
try {
|
||||
const userId = request.headers.get('x-user-id')
|
||||
const data = await request.json()
|
||||
|
||||
if (!userId) {
|
||||
const authUser = await getAuthUser(request)
|
||||
if (!authUser) {
|
||||
return NextResponse.json({ error: 'Utilisateur non identifié' }, { status: 401 })
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: authUser.id },
|
||||
select: USER_SAFE_SELECT
|
||||
})
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'Utilisateur introuvable' }, { status: 404 })
|
||||
}
|
||||
|
||||
return NextResponse.json(user)
|
||||
} catch (error) {
|
||||
console.error('GET /api/users/me error:', error)
|
||||
return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
// PATCH /api/users/me — Update personal user profile
|
||||
export async function PATCH(
|
||||
request: NextRequest
|
||||
): Promise<Response> {
|
||||
try {
|
||||
const authUser = await getAuthUser(request)
|
||||
if (!authUser) {
|
||||
return NextResponse.json({ error: 'Utilisateur non identifié' }, { status: 401 })
|
||||
}
|
||||
|
||||
const data = await request.json()
|
||||
|
||||
const updatedUser = await prisma.user.update({
|
||||
where: { id: userId },
|
||||
where: { id: authUser.id },
|
||||
data: {
|
||||
name: data.name,
|
||||
phone: data.phone,
|
||||
bio: data.bio,
|
||||
location: data.location,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
role: true,
|
||||
phone: true,
|
||||
bio: true,
|
||||
location: true,
|
||||
avatar: true
|
||||
}
|
||||
select: USER_SAFE_SELECT
|
||||
})
|
||||
|
||||
return NextResponse.json(updatedUser)
|
||||
|
||||
@@ -1,27 +1,25 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import prisma from '@/lib/prisma';
|
||||
import { getAuthUser, USER_SAFE_SELECT } from '@/lib/auth-utils';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const email = searchParams.get('email');
|
||||
const authUser = await getAuthUser(request);
|
||||
if (!authUser) {
|
||||
return NextResponse.json({ error: 'Authentification requise' }, { status: 401 });
|
||||
}
|
||||
|
||||
if (!email) {
|
||||
return NextResponse.json({ error: 'Email requis' }, { status: 400 });
|
||||
const { searchParams } = new URL(request.url);
|
||||
const email = searchParams.get('email')?.toLowerCase().trim() || authUser.email.toLowerCase();
|
||||
|
||||
// Seul l'utilisateur lui-même ou un administrateur peut consulter ce profil par email
|
||||
if (authUser.role !== 'ADMIN' && authUser.email.toLowerCase() !== email) {
|
||||
return NextResponse.json({ error: 'Accès non autorisé' }, { status: 403 });
|
||||
}
|
||||
|
||||
try {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { email },
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
role: true,
|
||||
avatar: true,
|
||||
phone: true,
|
||||
bio: true,
|
||||
location: true
|
||||
}
|
||||
select: USER_SAFE_SELECT
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import bcrypt from 'bcryptjs'
|
||||
import prisma from '@/lib/prisma'
|
||||
import { isAdmin, USER_SAFE_SELECT } from '@/lib/auth-utils'
|
||||
|
||||
// GET /api/users
|
||||
// GET /api/users (Admin only)
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
if (!(await isAdmin(request))) {
|
||||
@@ -23,7 +24,7 @@ export async function GET(request: NextRequest) {
|
||||
}
|
||||
}
|
||||
|
||||
// POST /api/users (Admin only)
|
||||
// POST /api/users (Admin only — hashes password and whitelists fields)
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
if (!(await isAdmin(request))) {
|
||||
@@ -31,12 +32,33 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
|
||||
// Note: For admins, we might want to allow creating users directly,
|
||||
// but we should still ensure password hashing if provided.
|
||||
// For now, we just restrict it to admins to prevent the public leak.
|
||||
const email = body.email?.toLowerCase().trim()
|
||||
const name = body.name?.trim()
|
||||
const rawPassword = body.password
|
||||
|
||||
if (!email || !name || !rawPassword || rawPassword.length < 8) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Nom, email et mot de passe (min. 8 caractères) requis' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const hashedPassword = await bcrypt.hash(rawPassword, 10)
|
||||
const allowedRole = ['VISITOR', 'ENTREPRENEUR', 'ADMIN'].includes(body.role)
|
||||
? body.role
|
||||
: 'VISITOR'
|
||||
|
||||
const user = await prisma.user.create({
|
||||
data: body,
|
||||
data: {
|
||||
name,
|
||||
email,
|
||||
password: hashedPassword,
|
||||
role: allowedRole,
|
||||
phone: body.phone || null,
|
||||
bio: body.bio || null,
|
||||
location: body.location || null,
|
||||
emailVerified: true,
|
||||
},
|
||||
select: USER_SAFE_SELECT
|
||||
})
|
||||
return NextResponse.json(user, { status: 201 })
|
||||
|
||||
Reference in New Issue
Block a user