Files
Jyotisha/frontend/src/app/api/admin/codes/route.ts
T
linmeng 1b8d6fcce6
Staging Backend Quality Gate (push the reviewed main SHA to staging to auto-deploy) / validate (push) Failing after 5m10s
Staging Backend Quality Gate (push the reviewed main SHA to staging to auto-deploy) / publish-and-deploy (push) Has been skipped
feat: add configurable payments and Gitea staging delivery
Add database-backed administrators, configurable Alipay packages, idempotent payment settlement and platform reporting. Standardize Gitea workflows on the xiaoxin runner so reviewed staging commits are tested, packaged, and deployed by immutable image digest.
2026-07-27 20:19:05 +08:00

109 lines
3.7 KiB
TypeScript

import { NextResponse } from "next/server";
import { z } from "zod";
import {
createAdminSupabaseClient,
isAdminUser,
} from "@/lib/supabase/admin";
import {
generateRedeemCode,
hashRedeemCode,
maskRedeemCode,
} from "@/lib/supabase/codes";
import {
isSupabaseConfigurationError,
} from "@/lib/supabase/config";
import { createServerSupabaseClient } from "@/lib/supabase/server";
export const runtime = "nodejs";
const createCodesSchema = z.object({
credits: z.number().int().positive().max(1_000_000),
count: z.number().int().min(1).max(100),
expiresAt: z.string().datetime({ offset: true }).optional(),
note: z.string().trim().max(500).optional(),
});
async function requireAdmin() {
const supabase = await createServerSupabaseClient();
const { data: { user }, error } = await supabase.auth.getUser();
if (error || !user) return { response: NextResponse.json({ error: "请先登录" }, { status: 401 }) };
if (!(await isAdminUser(user))) {
return { response: NextResponse.json({ error: "无管理员权限" }, { status: 403 }) };
}
return { user };
}
export async function GET() {
try {
const auth = await requireAdmin();
if ("response" in auth) return auth.response;
const admin = createAdminSupabaseClient();
const { data, error } = await admin
.from("redemption_codes")
.select("id,code_mask,credits,expires_at,note,created_at,redeemed_by,redeemed_email,redeemed_at")
.order("created_at", { ascending: false })
.limit(100);
if (error) {
return NextResponse.json({ error: "暂时无法读取兑换码列表" }, { status: 500 });
}
return NextResponse.json({
codes: data.map((code) => ({
id: code.id,
mask: code.code_mask,
credits: code.credits,
expiresAt: code.expires_at,
note: code.note,
createdAt: code.created_at,
redeemedBy: code.redeemed_by,
redeemedEmail: code.redeemed_email,
redeemedAt: code.redeemed_at,
})),
});
} catch (error) {
if (isSupabaseConfigurationError(error)) {
return NextResponse.json({ error: "Supabase 或管理员白名单尚未配置", code: "SUPABASE_NOT_CONFIGURED" }, { status: 503 });
}
return NextResponse.json({ error: "兑换码管理服务暂时不可用" }, { status: 500 });
}
}
export async function POST(request: Request) {
try {
const auth = await requireAdmin();
if ("response" in auth) return auth.response;
const parsed = createCodesSchema.safeParse(await request.json().catch(() => null));
if (!parsed.success) {
return NextResponse.json({ error: "兑换码参数不正确" }, { status: 400 });
}
const { credits, count, expiresAt, note } = parsed.data;
const codes = Array.from({ length: count }, generateRedeemCode);
const admin = createAdminSupabaseClient();
const { error } = await admin.from("redemption_codes").insert(codes.map((code) => ({
code_hash: hashRedeemCode(code),
code_mask: maskRedeemCode(code),
credits,
expires_at: expiresAt ?? null,
note: note || null,
created_by: auth.user.id,
})));
if (error) {
return NextResponse.json({ error: "生成兑换码失败,请重试" }, { status: 500 });
}
return NextResponse.json({
codes: codes.map((code) => ({ code, credits, expiresAt: expiresAt ?? null, note: note || null })),
}, { status: 201 });
} catch (error) {
if (isSupabaseConfigurationError(error)) {
return NextResponse.json({ error: "Supabase 或管理员白名单尚未配置", code: "SUPABASE_NOT_CONFIGURED" }, { status: 503 });
}
return NextResponse.json({ error: "兑换码管理服务暂时不可用" }, { status: 500 });
}
}