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:
121
app/api/admin/candidatures/[id]/approve/route.ts
Normal file
121
app/api/admin/candidatures/[id]/approve/route.ts
Normal 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.",
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user