mirror of
https://github.com/arthur-pbty/contact.git
synced 2026-06-03 23:36:30 +02:00
feat: add contact page with form handling and validation
- Implemented a contact page with a form for user inquiries. - Added validation for form fields using Zod schema. - Integrated PostgreSQL database for storing contact messages. - Created necessary API endpoints for form submission. - Added admin authentication and session management. - Developed CGU, cookies policy, privacy policy, and legal mentions pages. - Set up Docker configuration for PostgreSQL and application services. - Enhanced UI with responsive design and accessibility features.
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { createAdminToken, getAdminCookieName, getAdminCredentials } from "@/lib/admin-auth";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const body = (await request.json().catch(() => null)) as
|
||||
| { username?: string; password?: string }
|
||||
| null;
|
||||
|
||||
if (!body?.username || !body?.password) {
|
||||
return NextResponse.json({ message: "Identifiants manquants." }, { status: 400 });
|
||||
}
|
||||
|
||||
const credentials = getAdminCredentials();
|
||||
if (body.username !== credentials.username || body.password !== credentials.password) {
|
||||
return NextResponse.json({ message: "Identifiants invalides." }, { status: 401 });
|
||||
}
|
||||
|
||||
const token = createAdminToken(body.username);
|
||||
const response = NextResponse.json({ ok: true });
|
||||
response.cookies.set(getAdminCookieName(), token, {
|
||||
httpOnly: true,
|
||||
sameSite: "lax",
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
path: "/",
|
||||
maxAge: 60 * 60 * 12,
|
||||
});
|
||||
|
||||
return response;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getAdminCookieName } from "@/lib/admin-auth";
|
||||
|
||||
export async function POST() {
|
||||
const response = NextResponse.json({ ok: true });
|
||||
response.cookies.set(getAdminCookieName(), "", {
|
||||
httpOnly: true,
|
||||
sameSite: "lax",
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
path: "/",
|
||||
maxAge: 0,
|
||||
});
|
||||
|
||||
return response;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { isAdminAuthenticated } from "@/lib/admin-auth";
|
||||
|
||||
export async function GET() {
|
||||
const authenticated = await isAdminAuthenticated();
|
||||
if (!authenticated) {
|
||||
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import nodemailer from "nodemailer";
|
||||
import { NextResponse } from "next/server";
|
||||
import { isAdminAuthenticated } from "@/lib/admin-auth";
|
||||
import { getMessageById, markMessageReply } from "@/lib/db";
|
||||
|
||||
async function sendReplyEmail(input: { to: string; reply: string; senderName: string; originalMessage: string }) {
|
||||
const smtpHost = process.env.SMTP_HOST;
|
||||
const smtpPort = Number(process.env.SMTP_PORT || "587");
|
||||
const smtpUser = process.env.SMTP_USER;
|
||||
const smtpPass = process.env.SMTP_PASS;
|
||||
|
||||
if (!smtpHost || !smtpUser || !smtpPass) {
|
||||
throw new Error("SMTP non configure pour les reponses admin.");
|
||||
}
|
||||
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: smtpHost,
|
||||
port: Number.isNaN(smtpPort) ? 587 : smtpPort,
|
||||
secure: smtpPort === 465,
|
||||
auth: { user: smtpUser, pass: smtpPass },
|
||||
});
|
||||
|
||||
const fromEmail = process.env.CONTACT_FROM_EMAIL || smtpUser;
|
||||
const escapedOriginal = input.originalMessage
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">");
|
||||
const escapedReply = input.reply
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">");
|
||||
|
||||
await transporter.sendMail({
|
||||
from: fromEmail,
|
||||
to: input.to,
|
||||
subject: "Reponse a votre message",
|
||||
text: [
|
||||
`Bonjour ${input.senderName},`,
|
||||
"",
|
||||
"Merci pour votre message. Voici ma reponse:",
|
||||
"",
|
||||
input.reply,
|
||||
"",
|
||||
"---------------------------",
|
||||
"Votre message initial:",
|
||||
input.originalMessage,
|
||||
"---------------------------",
|
||||
"",
|
||||
"ArthurP",
|
||||
].join("\n"),
|
||||
html: `
|
||||
<div style="font-family: Inter, Segoe UI, Arial, sans-serif; color: #0f172a; line-height: 1.6;">
|
||||
<h2 style="margin: 0 0 12px;">Reponse a votre message</h2>
|
||||
<p style="margin: 0 0 16px;">Bonjour ${input.senderName},</p>
|
||||
<p style="margin: 0 0 8px;">Merci pour votre message. Voici ma reponse:</p>
|
||||
<div style="margin: 0 0 20px; padding: 12px 14px; background: #ecfeff; border: 1px solid #a5f3fc; border-radius: 10px; white-space: pre-wrap;">${escapedReply}</div>
|
||||
<p style="margin: 0 0 8px; font-weight: 600;">Votre message initial:</p>
|
||||
<div style="margin: 0; padding: 12px 14px; background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 10px; white-space: pre-wrap;">${escapedOriginal}</div>
|
||||
<p style="margin: 20px 0 0; font-size: 13px; color: #475569;">ArthurP</p>
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
}
|
||||
|
||||
export async function POST(
|
||||
request: Request,
|
||||
context: { params: Promise<{ id: string }> },
|
||||
) {
|
||||
const authenticated = await isAdminAuthenticated();
|
||||
if (!authenticated) {
|
||||
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
const { id } = await context.params;
|
||||
const messageId = Number(id);
|
||||
if (!Number.isInteger(messageId) || messageId <= 0) {
|
||||
return NextResponse.json({ message: "ID invalide." }, { status: 400 });
|
||||
}
|
||||
|
||||
const body = (await request.json().catch(() => null)) as { reply?: string } | null;
|
||||
const reply = body?.reply?.trim() || "";
|
||||
if (reply.length < 5) {
|
||||
return NextResponse.json({ message: "Reponse trop courte." }, { status: 400 });
|
||||
}
|
||||
|
||||
const existing = await getMessageById(messageId);
|
||||
if (!existing) {
|
||||
return NextResponse.json({ message: "Message introuvable." }, { status: 404 });
|
||||
}
|
||||
|
||||
try {
|
||||
await sendReplyEmail({
|
||||
to: existing.email,
|
||||
reply,
|
||||
senderName: existing.name,
|
||||
originalMessage: existing.message,
|
||||
});
|
||||
await markMessageReply(messageId, reply);
|
||||
return NextResponse.json({ ok: true });
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
message:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Impossible d'envoyer la reponse.",
|
||||
},
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { isAdminAuthenticated } from "@/lib/admin-auth";
|
||||
import { deleteMessage } from "@/lib/db";
|
||||
|
||||
export async function DELETE(
|
||||
_request: Request,
|
||||
context: { params: Promise<{ id: string }> },
|
||||
) {
|
||||
const authenticated = await isAdminAuthenticated();
|
||||
if (!authenticated) {
|
||||
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
const { id } = await context.params;
|
||||
const messageId = Number(id);
|
||||
if (!Number.isInteger(messageId) || messageId <= 0) {
|
||||
return NextResponse.json({ message: "ID invalide." }, { status: 400 });
|
||||
}
|
||||
|
||||
const deleted = await deleteMessage(messageId);
|
||||
if (!deleted) {
|
||||
return NextResponse.json({ message: "Message introuvable." }, { status: 404 });
|
||||
}
|
||||
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { isAdminAuthenticated } from "@/lib/admin-auth";
|
||||
import { setMessageStatus } from "@/lib/db";
|
||||
|
||||
export async function POST(
|
||||
request: Request,
|
||||
context: { params: Promise<{ id: string }> },
|
||||
) {
|
||||
const authenticated = await isAdminAuthenticated();
|
||||
if (!authenticated) {
|
||||
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
const { id } = await context.params;
|
||||
const messageId = Number(id);
|
||||
if (!Number.isInteger(messageId) || messageId <= 0) {
|
||||
return NextResponse.json({ message: "ID invalide." }, { status: 400 });
|
||||
}
|
||||
|
||||
const body = (await request.json().catch(() => null)) as { status?: string } | null;
|
||||
const status = body?.status;
|
||||
|
||||
if (status !== "pending" && status !== "replied") {
|
||||
return NextResponse.json({ message: "Statut invalide." }, { status: 400 });
|
||||
}
|
||||
|
||||
const updated = await setMessageStatus(messageId, status);
|
||||
if (!updated) {
|
||||
return NextResponse.json({ message: "Message introuvable." }, { status: 404 });
|
||||
}
|
||||
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { isAdminAuthenticated } from "@/lib/admin-auth";
|
||||
import { listMessages } from "@/lib/db";
|
||||
|
||||
export async function GET() {
|
||||
const authenticated = await isAdminAuthenticated();
|
||||
if (!authenticated) {
|
||||
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
const messages = await listMessages();
|
||||
|
||||
return NextResponse.json({
|
||||
messages: messages.map((row) => ({
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
email: row.email,
|
||||
project: row.project,
|
||||
requestType: row.request_type,
|
||||
message: row.message,
|
||||
createdAt: row.created_at,
|
||||
repliedAt: row.replied_at,
|
||||
adminReply: row.admin_reply,
|
||||
status: row.status,
|
||||
})),
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user