feat: add admin panel to manage candidatures and approve with Stripe link

- /admin page with secret-key authentication
- List all candidatures with details (expandable cards)
- Approve: updates status + generates Stripe checkout URL + sends email
- Reject: updates status
- Checkout URL displayed on screen for manual copy if Resend not configured
- Protected by ADMIN_SECRET env var

https://claude.ai/code/session_01H2aRGDaKgarPvhay2HxN6Y
This commit is contained in:
Claude
2026-02-09 19:39:33 +00:00
parent db51e21075
commit c4934f5669
4 changed files with 539 additions and 0 deletions

View File

@@ -0,0 +1,121 @@
import { NextResponse } from "next/server";
import { createAdminClient } from "@/lib/supabase/server";
import { stripe } from "@/lib/stripe/client";
import { getBaseUrl } from "@/lib/utils";
export const runtime = "nodejs";
// POST /api/admin/candidatures/[id]/approve - Approuver une candidature
export async function POST(
request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params;
const body = await request.json();
const { secret } = body;
if (!process.env.ADMIN_SECRET || secret !== process.env.ADMIN_SECRET) {
return NextResponse.json({ error: "Non autorisé." }, { status: 401 });
}
const supabase = createAdminClient();
// Récupérer la candidature
const { data: candidature, error: fetchError } = await supabase
.from("candidatures")
.select("*")
.eq("id", id)
.single();
if (fetchError || !candidature) {
return NextResponse.json({ error: "Candidature introuvable." }, { status: 404 });
}
// Mettre à jour le statut
const { error: updateError } = await supabase
.from("candidatures")
.update({ status: "approved" } as never)
.eq("id", id);
if (updateError) {
return NextResponse.json({ error: updateError.message }, { status: 500 });
}
// Générer le lien de paiement Stripe
let checkoutUrl: string | null = null;
if (process.env.STRIPE_SECRET_KEY && process.env.STRIPE_PRICE_ID) {
try {
const baseUrl = getBaseUrl();
const email = (candidature as Record<string, unknown>).email as string;
const candidatureId = (candidature as Record<string, unknown>).id as string;
// Créer ou récupérer le customer Stripe
const customers = await stripe.customers.list({ email, limit: 1 });
let customerId: string;
if (customers.data.length > 0) {
customerId = customers.data[0].id;
} else {
const customer = await stripe.customers.create({
email,
metadata: { candidature_id: candidatureId },
});
customerId = customer.id;
}
const session = await stripe.checkout.sessions.create({
customer: customerId,
mode: "subscription",
payment_method_types: ["card"],
line_items: [{ price: process.env.STRIPE_PRICE_ID, quantity: 1 }],
metadata: { candidature_id: candidatureId, email },
success_url: `${baseUrl}/merci?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${baseUrl}/candidature`,
allow_promotion_codes: true,
billing_address_collection: "required",
});
checkoutUrl = session.url;
// Envoyer le lien par email si Resend est configuré
if (process.env.RESEND_API_KEY && process.env.RESEND_API_KEY !== "re_your-api-key") {
try {
const { Resend } = await import("resend");
const resend = new Resend(process.env.RESEND_API_KEY);
const firstname = (candidature as Record<string, unknown>).firstname as string;
await resend.emails.send({
from: "HookLab <noreply@hooklab.fr>",
to: email,
subject: "Ta candidature HookLab est acceptée !",
html: `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<h1 style="color: #6D5EF6;">Félicitations ${firstname} !</h1>
<p>Ta candidature au programme HookLab a été <strong>acceptée</strong> !</p>
<p>Pour finaliser ton inscription et accéder au programme, clique sur le bouton ci-dessous :</p>
<div style="text-align: center; margin: 30px 0;">
<a href="${checkoutUrl}" style="background: #6D5EF6; color: white; padding: 14px 32px; border-radius: 12px; text-decoration: none; font-weight: bold; display: inline-block;">
Finaliser mon inscription (490€/mois)
</a>
</div>
<p style="color: #888; font-size: 13px;">Le paiement est sécurisé via Stripe. Tu peux payer en 2 mensualités de 490€.</p>
<p>À très vite,<br/>L'équipe HookLab</p>
</div>
`,
});
} catch (emailError) {
console.error("Erreur envoi email approbation:", emailError);
}
}
} catch (stripeError) {
console.error("Erreur Stripe:", stripeError);
}
}
return NextResponse.json({
success: true,
checkoutUrl,
message: checkoutUrl
? "Candidature approuvée. Lien de paiement généré."
: "Candidature approuvée. Stripe non configuré, pas de lien de paiement.",
});
}