makearmy-app/app/api/support/badges/route.ts

37 lines
1.4 KiB
TypeScript
Raw Permalink Normal View History

feat(support): Ko-fi end-to-end linking + badges (derive-on-read, no cron) - Add Ko-fi webhook (/api/webhooks/kofi) with upsert by (provider, external_user_id) • Computes renews_at = timestamp + 1 calendar month + 1 day • Preserves first started_at; stores raw payload; canonicalizes by email when available - Add Ko-fi claim flow • POST /api/support/kofi/claim/start — sends verification email via SMTP • GET /api/support/kofi/claim/verify — finalizes link (sets app_user), redirects to /portal/account • POST /api/support/kofi/unlink — clears app_user on Ko-fi rows - Add derive-on-read membership logic • /lib/memberships.ts — single source of truth for badges & “active” state • /api/support/badges — thin wrapper that returns per-provider badges - Account UI • components/account/SupporterBadges.tsx — renders provider badges (Ko-fi now; extensible) • components/account/ConnectKofi.tsx — “Link Ko-fi” form (email → verify link) • components/account/LinkStatus.tsx — success/error banner on return • app/portal/account/AccountPanel.tsx — integrates badges, link panel, and banner - Config/env • Requires: DIRECTUS_URL, DIRECTUS_TOKEN_ADMIN_SUPPORTER, KOFI_VERIFY_TOKEN • SMTP: SMTP_HOST, SMTP_PORT, SMTP_SECURE, SMTP_USER, SMTP_PASS, EMAIL_FROM • APP_ORIGIN used to build absolute verify URLs - Misc • Fixed import to use @/lib/memberships • No cron required; UI derives active state via status === active && renews_at >= now Refs: beta readiness for Ko-fi supporters
2025-10-19 17:51:04 -04:00
// /app/api/support/badges/route.ts
import { NextRequest, NextResponse } from "next/server";
2025-10-19 18:21:18 -04:00
import { fetchMembershipBadges } from "@/lib/memberships";
feat(support): Ko-fi end-to-end linking + badges (derive-on-read, no cron) - Add Ko-fi webhook (/api/webhooks/kofi) with upsert by (provider, external_user_id) • Computes renews_at = timestamp + 1 calendar month + 1 day • Preserves first started_at; stores raw payload; canonicalizes by email when available - Add Ko-fi claim flow • POST /api/support/kofi/claim/start — sends verification email via SMTP • GET /api/support/kofi/claim/verify — finalizes link (sets app_user), redirects to /portal/account • POST /api/support/kofi/unlink — clears app_user on Ko-fi rows - Add derive-on-read membership logic • /lib/memberships.ts — single source of truth for badges & “active” state • /api/support/badges — thin wrapper that returns per-provider badges - Account UI • components/account/SupporterBadges.tsx — renders provider badges (Ko-fi now; extensible) • components/account/ConnectKofi.tsx — “Link Ko-fi” form (email → verify link) • components/account/LinkStatus.tsx — success/error banner on return • app/portal/account/AccountPanel.tsx — integrates badges, link panel, and banner - Config/env • Requires: DIRECTUS_URL, DIRECTUS_TOKEN_ADMIN_SUPPORTER, KOFI_VERIFY_TOKEN • SMTP: SMTP_HOST, SMTP_PORT, SMTP_SECURE, SMTP_USER, SMTP_PASS, EMAIL_FROM • APP_ORIGIN used to build absolute verify URLs - Misc • Fixed import to use @/lib/memberships • No cron required; UI derives active state via status === active && renews_at >= now Refs: beta readiness for Ko-fi supporters
2025-10-19 17:51:04 -04:00
// Replace this with your real auth lookup if/when you wire it in
async function getCurrentUser(req: NextRequest): Promise<{ id?: string; email?: string } | null> {
const uid = req.headers.get("x-user-id") || undefined;
const email = req.headers.get("x-user-email") || undefined;
return uid || email ? { id: uid, email } : null;
}
export async function GET(req: NextRequest) {
try {
const me = await getCurrentUser(req);
// Accept explicit query params so the component can work without special headers
const emailParam = (req.nextUrl.searchParams.get("email") || "").trim().toLowerCase() || undefined;
const userIdParam = (req.nextUrl.searchParams.get("userId") || "").trim() || undefined;
if (!emailParam && !userIdParam && !me?.id && !me?.email) {
return NextResponse.json({ error: "Provide email or userId" }, { status: 400 });
}
const badges = await fetchMembershipBadges({
userId: userIdParam || me?.id,
email: emailParam || me?.email,
});
return NextResponse.json({ badges });
} catch (e: any) {
return NextResponse.json(
{ error: "badges_fetch_failed", detail: String(e?.message || e) },
{ status: 500 }
);
}
}