feat: add admin dashboard and core API routes with Docker configuration
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user