Files
Jyotisha/frontend/src/lib/admin/codes.ts
T
Jesse_Chen 1cbdb774e0
Independent Staging Quality Gate / validate (push) Successful in 13m41s
Independent Staging Quality Gate / publish (push) Successful in 9m41s
fix(admin): pass verified identity role to code RPCs
2026-08-16 20:40:25 +08:00

150 lines
4.0 KiB
TypeScript

import "server-only";
import { queryAdminRows } from "./database";
import type { AdminSession } from "./auth";
export type RedemptionCodeRecord = {
id: string;
mask: string;
credits: number;
expiresAt: string | null;
note: string | null;
createdAt: string;
redeemedBy: string | null;
redeemedEmail: string | null;
redeemedAt: string | null;
revokedBy: string | null;
revokedAt: string | null;
status: "available" | "expired" | "redeemed" | "revoked";
};
type RpcCodeRow = {
id: string;
code_mask: string;
credits: number;
expires_at: Date | string | null;
note: string | null;
created_at: Date | string;
redeemed_by: string | null;
redeemed_email: string | null;
redeemed_at: Date | string | null;
revoked_by: string | null;
revoked_at: Date | string | null;
};
type CodeRpcArgs = {
admin_create_redemption_codes: {
p_codes: unknown;
p_reason: string;
};
admin_update_redemption_code: {
p_code_id: string;
p_set_note: boolean;
p_note: string | null;
p_set_expires_at: boolean;
p_expires_at: string | null;
p_reason: string;
};
admin_revoke_redemption_code: {
p_code_id: string;
p_reason: string;
};
};
function isoTimestamp(value: Date | string | null): string | null {
return value instanceof Date ? value.toISOString() : value;
}
export function codeStatus(row: RpcCodeRow): RedemptionCodeRecord["status"] {
const expiresAt = isoTimestamp(row.expires_at);
if (row.redeemed_at) return "redeemed";
if (row.revoked_at) return "revoked";
if (expiresAt && Date.parse(expiresAt) <= Date.now()) return "expired";
return "available";
}
export function mapCode(row: RpcCodeRow): RedemptionCodeRecord {
const expiresAt = isoTimestamp(row.expires_at);
const redeemedAt = isoTimestamp(row.redeemed_at);
const revokedAt = isoTimestamp(row.revoked_at);
return {
id: row.id,
mask: row.code_mask,
credits: row.credits,
expiresAt,
note: row.note,
createdAt: isoTimestamp(row.created_at)!,
redeemedBy: row.redeemed_by,
redeemedEmail: row.redeemed_email,
redeemedAt,
revokedBy: row.revoked_by,
revokedAt,
status: codeStatus({
...row,
expires_at: expiresAt,
created_at: isoTimestamp(row.created_at)!,
redeemed_at: redeemedAt,
revoked_at: revokedAt,
}),
};
}
export async function runCodeRpc<T extends keyof CodeRpcArgs>(
functionName: T,
session: Pick<AdminSession, "user">,
id: string,
args: CodeRpcArgs[T],
): Promise<RedemptionCodeRecord[]> {
const common = [
session.user.id,
session.user.email,
"admin",
id,
];
let rows: RpcCodeRow[] = [];
switch (functionName) {
case "admin_create_redemption_codes": {
const input = args as CodeRpcArgs["admin_create_redemption_codes"];
rows = await queryAdminRows<RpcCodeRow>(
`select * from public.admin_create_redemption_codes(
$1::uuid,$2::text,$3::text,$4::text,$5::jsonb,$6::text
)`,
[...common, JSON.stringify(input.p_codes), input.p_reason],
);
break;
}
case "admin_update_redemption_code": {
const input = args as CodeRpcArgs["admin_update_redemption_code"];
rows = await queryAdminRows<RpcCodeRow>(
`select * from public.admin_update_redemption_code(
$1::uuid,$2::text,$3::text,$4::text,$5::uuid,
$6::boolean,$7::text,$8::boolean,$9::timestamptz,$10::text
)`,
[
...common,
input.p_code_id,
input.p_set_note,
input.p_note,
input.p_set_expires_at,
input.p_expires_at,
input.p_reason,
],
);
break;
}
case "admin_revoke_redemption_code": {
const input = args as CodeRpcArgs["admin_revoke_redemption_code"];
rows = await queryAdminRows<RpcCodeRow>(
`select * from public.admin_revoke_redemption_code(
$1::uuid,$2::text,$3::text,$4::text,$5::uuid,$6::text
)`,
[...common, input.p_code_id, input.p_reason],
);
break;
}
}
return rows.map(mapCode);
}