fix(admin): remove manual reasons and repair code creation
Independent Staging Quality Gate / validate (push) Has been cancelled
Independent Staging Quality Gate / publish (push) Has been cancelled

This commit is contained in:
Jesse_Chen
2026-08-16 16:49:05 +08:00
parent 9df43e8112
commit c712787ec2
24 changed files with 180 additions and 206 deletions
@@ -27,7 +27,6 @@ const mutationSchema = z.object({
email: z.string().trim().email().optional(),
userId: z.string().uuid().optional(),
roleCode: z.enum(["owner", "model_admin", "billing_admin", "operations", "support", "auditor"]),
reason: z.string().trim().min(1).max(500),
}).refine((value) => Boolean(value.email || value.userId), {
message: "email_or_user_id_required",
});
@@ -90,7 +89,7 @@ async function mutate(request: Request, assign: boolean) {
if (!targetUserId) return NextResponse.json({ error: "用户不存在" }, { status: 404 });
const rows = await queryAdminRows<{ user_id: string; role_code: string; assigned: boolean }>(
"select * from public.admin_manage_role($1, $2, $3, $4, $5, $6)",
[session.user.id, targetUserId, parsed.data.roleCode, assign, parsed.data.reason, requestId(request)],
[session.user.id, targetUserId, parsed.data.roleCode, assign, "admin_console_manage_role", requestId(request)],
);
return NextResponse.json({ data: rows[0] });
} catch (error) {
+3 -10
View File
@@ -12,14 +12,10 @@ import {
export const runtime = "nodejs";
const paramsSchema = z.object({ id: z.string().uuid() });
const revokeCodeSchema = z.object({
reason: z.string().trim().min(1).max(500),
});
const updateCodeSchema = z
.object({
note: z.string().trim().max(500).nullable().optional(),
expiresAt: z.string().datetime({ offset: true }).nullable().optional(),
reason: z.string().trim().min(1).max(500),
})
.refine((value) => "note" in value || "expiresAt" in value, {
message: "至少提供一个可修改字段",
@@ -52,7 +48,7 @@ export async function PATCH(
p_note: body.note ?? null,
p_set_expires_at: "expiresAt" in body,
p_expires_at: body.expiresAt ?? null,
p_reason: body.reason,
p_reason: "admin_console_update_redemption_code",
},
);
return NextResponse.json({ data: rows[0] });
@@ -71,15 +67,12 @@ export async function DELETE(
"billing.adjustments.write",
);
const parsed = paramsSchema.safeParse(await context.params);
const parsedBody = revokeCodeSchema.safeParse(
await request.json().catch(() => null),
);
if (!parsed.success || !parsedBody.success) return invalidQueryResponse();
if (!parsed.success) return invalidQueryResponse();
const rows = await runCodeRpc(
"admin_revoke_redemption_code",
session,
requestId(request),
{ p_code_id: parsed.data.id, p_reason: parsedBody.data.reason },
{ p_code_id: parsed.data.id, p_reason: "admin_console_revoke_redemption_code" },
);
return NextResponse.json({ data: rows[0] });
} catch (error) {
+1 -2
View File
@@ -28,7 +28,6 @@ const createCodesSchema = z.object({
count: z.number().int().min(1).max(100),
expiresAt: z.string().datetime({ offset: true }).nullable().optional(),
note: z.string().trim().max(500).nullable().optional(),
reason: z.string().trim().min(1).max(500),
});
type CodeRow = {
@@ -147,7 +146,7 @@ export async function POST(request: Request) {
"admin_create_redemption_codes",
session,
operationRequestId,
{ p_codes: records, p_reason: parsed.data.reason },
{ p_codes: records, p_reason: "admin_console_create_redemption_codes" },
);
const byMask = new Map<string, RedemptionCodeRecord>(
stored.map((record) => [record.mask, record]),
@@ -14,7 +14,6 @@ export const runtime = "nodejs";
const resetSchema = z.object({
userId: z.string().uuid(),
confirmation: z.literal("RESET"),
reason: z.string().trim().min(1).max(500),
});
type ResetRow = {
@@ -40,7 +39,7 @@ export async function POST(request: Request) {
[
session.user.id,
parsed.data.userId,
parsed.data.reason,
"admin_console_reset_customer",
requestId(request),
],
);
@@ -5,9 +5,9 @@ import { pageOffset, queryAdminRows } from "@/lib/admin/database";
import { adminErrorResponse, invalidQueryResponse, parseListQuery, requestId, requireAdminMutation } from "@/lib/admin/http";
export const runtime="nodejs";
const schema=z.discriminatedUnion("action",[
z.object({action:z.literal("save"),id:z.string().uuid().nullable().optional(),flagKey:z.string().regex(/^[a-z][a-z0-9._-]{1,99}$/),enabled:z.boolean(),rolloutPercentage:z.number().int().min(0).max(100),config:z.record(z.string(),z.unknown()).default({}),expectedVersion:z.number().int().positive().nullable().optional(),reason:z.string().trim().min(1).max(500)}).strict(),
z.object({action:z.literal("publish"),id:z.string().uuid(),expectedVersion:z.number().int().positive(),reason:z.string().trim().min(1).max(500)}).strict(),
z.object({action:z.literal("save"),id:z.string().uuid().nullable().optional(),flagKey:z.string().regex(/^[a-z][a-z0-9._-]{1,99}$/),enabled:z.boolean(),rolloutPercentage:z.number().int().min(0).max(100),config:z.record(z.string(),z.unknown()).default({}),expectedVersion:z.number().int().positive().nullable().optional()}).strict(),
z.object({action:z.literal("publish"),id:z.string().uuid(),expectedVersion:z.number().int().positive()}).strict(),
]);
type Row={id:string;flag_key:string;version:number;enabled:boolean;rollout_percentage:number;config:Record<string,unknown>;status:string;created_at:Date;published_at:Date|null;total_count:string};
export async function GET(request:Request){try{await requirePermission("admin.access");const p=parseListQuery(request);if(!p.success)return invalidQueryResponse(p.error.flatten());const q=p.data.q?`%${p.data.q}%`:null;const rows=await queryAdminRows<Row>(`select f.*,count(*) over()::text total_count from public.feature_flags f where ($1::text is null or f.flag_key ilike $1) and ($2::text is null or f.status=$2) order by f.created_at desc limit $3 offset $4`,[q,p.data.status??null,p.data.pageSize,pageOffset(p.data.page,p.data.pageSize)]);return NextResponse.json({data:rows.map(r=>({id:r.id,flagKey:r.flag_key,version:r.version,enabled:r.enabled,rolloutPercentage:r.rollout_percentage,config:r.config,status:r.status,createdAt:r.created_at.toISOString(),publishedAt:r.published_at?.toISOString()??null})),total:Number(rows[0]?.total_count??0)});}catch(e){return adminErrorResponse(e)}}
export async function POST(request:Request){try{const b=schema.safeParse(await request.json().catch(()=>null));if(!b.success)return invalidQueryResponse(b.error.flatten());const session=await requireAdminMutation(request,"ops.flags.write");const rid=requestId(request);if(b.data.action==="publish"){const rows=await queryAdminRows<{id:string}>("select public.admin_publish_feature_flag($1,$2,$3,$4,$5) id",[session.user.id,b.data.id,b.data.expectedVersion,b.data.reason,rid]);return NextResponse.json({data:{id:rows[0]!.id,requestId:rid}})}const v=b.data;const rows=await queryAdminRows<{id:string}>("select public.admin_save_feature_flag($1,$2,$3,$4,$5,$6::jsonb,$7,$8,$9) id",[session.user.id,v.id??null,v.flagKey,v.enabled,v.rolloutPercentage,JSON.stringify(v.config),v.expectedVersion??null,v.reason,rid]);return NextResponse.json({data:{id:rows[0]!.id,requestId:rid}})}catch(e){return adminErrorResponse(e)}}
export async function POST(request:Request){try{const b=schema.safeParse(await request.json().catch(()=>null));if(!b.success)return invalidQueryResponse(b.error.flatten());const session=await requireAdminMutation(request,"ops.flags.write");const rid=requestId(request);if(b.data.action==="publish"){const rows=await queryAdminRows<{id:string}>("select public.admin_publish_feature_flag($1,$2,$3,$4,$5) id",[session.user.id,b.data.id,b.data.expectedVersion,"admin_console_publish_feature_flag",rid]);return NextResponse.json({data:{id:rows[0]!.id,requestId:rid}})}const v=b.data;const rows=await queryAdminRows<{id:string}>("select public.admin_save_feature_flag($1,$2,$3,$4,$5,$6::jsonb,$7,$8,$9) id",[session.user.id,v.id??null,v.flagKey,v.enabled,v.rolloutPercentage,JSON.stringify(v.config),v.expectedVersion??null,"admin_console_save_feature_flag",rid]);return NextResponse.json({data:{id:rows[0]!.id,requestId:rid}})}catch(e){return adminErrorResponse(e)}}
+1 -2
View File
@@ -17,7 +17,6 @@ const adjustmentSchema = z.object({
id: z.string().uuid(),
action: z.enum(["retry_grant", "compensate", "record_refund"]),
expectedVersion: z.number().int().min(0),
reason: z.string().trim().min(1).max(500),
});
type Row = {
@@ -125,7 +124,7 @@ export async function POST(request: Request) {
parsed.data.id,
parsed.data.action,
parsed.data.expectedVersion,
parsed.data.reason,
"admin_console_adjust_order",
requestId(request),
],
);
+2 -4
View File
@@ -32,12 +32,10 @@ const saveSchema = z.object({
sortOrder: z.number().int(),
oneTimePerUser: z.boolean(),
entitlements: z.array(entitlementSchema).min(1),
reason: z.string().trim().min(1).max(500),
}).strict();
const publishSchema = z.object({
action: z.literal("publish"),
id: z.string().uuid(),
reason: z.string().trim().min(1).max(500),
}).strict();
const mutationSchema = z.discriminatedUnion("action", [saveSchema, publishSchema]);
@@ -91,7 +89,7 @@ export async function POST(request: Request) {
const session = await requireAdminMutation(request, permission);
const rid = requestId(request);
if (body.data.action === "publish") {
const rows = await queryAdminRows<{ id: string }>("select public.admin_publish_product($1,$2,$3,$4) id", [session.user.id, body.data.id, body.data.reason, rid]);
const rows = await queryAdminRows<{ id: string }>("select public.admin_publish_product($1,$2,$3,$4) id", [session.user.id, body.data.id, "admin_console_publish_product", rid]);
return NextResponse.json({ data: { id: rows[0]!.id, requestId: rid } });
}
const value = body.data;
@@ -99,7 +97,7 @@ export async function POST(request: Request) {
select public.admin_save_product_draft($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14::jsonb,$15,$16) id
`, [session.user.id, value.id ?? null, value.code, value.name, value.description, value.productType,
value.billingPeriod, value.intervalCount, value.priceCents, value.currency, value.enabled, value.sortOrder,
value.oneTimePerUser, JSON.stringify(value.entitlements), value.reason, rid]);
value.oneTimePerUser, JSON.stringify(value.entitlements), "admin_console_save_product", rid]);
return NextResponse.json({ data: { id: rows[0]!.id, requestId: rid } });
} catch (error) { return adminErrorResponse(error); }
}
@@ -10,7 +10,6 @@ const mutationSchema = z.object({
action: z.enum(["extend", "revoke"]),
days: z.number().int().min(1).max(3660).optional(),
expectedEndsAt: z.string().datetime(),
reason: z.string().trim().min(1).max(500),
}).strict();
type Row = { id:string; user_id:string; email:string|null; product_code:string; product_version:number; status:string; starts_at:Date; ends_at:Date; created_at:Date; total_count:string };
export async function GET(request: Request) {
@@ -29,7 +28,7 @@ export async function POST(request: Request){
try{
const body=mutationSchema.safeParse(await request.json().catch(()=>null)); if(!body.success)return invalidQueryResponse(body.error.flatten());
const session=await requireAdminMutation(request,"billing.adjustments.write"); const rid=requestId(request);
const rows=await queryAdminRows<{id:string}>("select public.admin_adjust_subscription($1,$2,$3,$4,$5,$6,$7) id",[session.user.id,body.data.id,body.data.action,body.data.days??null,body.data.expectedEndsAt,body.data.reason,rid]);
const rows=await queryAdminRows<{id:string}>("select public.admin_adjust_subscription($1,$2,$3,$4,$5,$6,$7) id",[session.user.id,body.data.id,body.data.action,body.data.days??null,body.data.expectedEndsAt,"admin_console_adjust_subscription",rid]);
return NextResponse.json({data:{id:rows[0]!.id,requestId:rid}});
}catch(error){return adminErrorResponse(error);}
}