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:
Puechberty Arthur
2026-04-01 19:21:21 +02:00
parent 50553d5e92
commit f63eeb2e84
36 changed files with 2267 additions and 155 deletions
+98
View File
@@ -0,0 +1,98 @@
import { createHmac, timingSafeEqual } from "crypto";
import { cookies } from "next/headers";
const COOKIE_NAME = "admin_session";
const SESSION_TTL_SECONDS = 60 * 60 * 12;
type SessionPayload = {
sub: string;
exp: number;
};
function getSecret() {
const secret = process.env.ADMIN_SESSION_SECRET;
if (!secret) {
throw new Error("ADMIN_SESSION_SECRET is missing.");
}
return secret;
}
function encodeBase64Url(value: string) {
return Buffer.from(value).toString("base64url");
}
function decodeBase64Url(value: string) {
return Buffer.from(value, "base64url").toString();
}
function sign(value: string) {
return createHmac("sha256", getSecret()).update(value).digest("base64url");
}
function safeEqual(a: string, b: string) {
const aBuffer = Buffer.from(a);
const bBuffer = Buffer.from(b);
if (aBuffer.length !== bBuffer.length) {
return false;
}
return timingSafeEqual(aBuffer, bBuffer);
}
export function createAdminToken(username: string) {
const payload: SessionPayload = {
sub: username,
exp: Math.floor(Date.now() / 1000) + SESSION_TTL_SECONDS,
};
const encodedPayload = encodeBase64Url(JSON.stringify(payload));
const signature = sign(encodedPayload);
return `${encodedPayload}.${signature}`;
}
function verifyToken(token: string): SessionPayload | null {
const [payloadPart, signaturePart] = token.split(".");
if (!payloadPart || !signaturePart) {
return null;
}
const expectedSignature = sign(payloadPart);
if (!safeEqual(signaturePart, expectedSignature)) {
return null;
}
try {
const payload = JSON.parse(decodeBase64Url(payloadPart)) as SessionPayload;
if (!payload?.sub || !payload?.exp) {
return null;
}
if (payload.exp < Math.floor(Date.now() / 1000)) {
return null;
}
return payload;
} catch {
return null;
}
}
export async function isAdminAuthenticated() {
const cookieStore = await cookies();
const token = cookieStore.get(COOKIE_NAME)?.value;
if (!token) {
return false;
}
return Boolean(verifyToken(token));
}
export function getAdminCredentials() {
return {
username: process.env.ADMIN_USERNAME || "admin",
password: process.env.ADMIN_PASSWORD || "change-me",
};
}
export function getAdminCookieName() {
return COOKIE_NAME;
}