feat(integration): 为集成站点与开奖管理新增 AdminPermissionGate 权限控制
使用 AdminPermissionGate 包裹集成站点与开奖相关组件,根据权限进行访问控制。 新增集成管理与开奖管理相关权限常量。 更新相关 UI 组件以适配权限校验逻辑,提升系统安全性与用户体验。 增强国际化支持,在英文、尼泊尔语与中文语言包中新增集成相关文案。
This commit is contained in:
@@ -6,6 +6,7 @@ import { ChevronRight } from "lucide-react";
|
||||
|
||||
import { ModuleScaffold } from "@/components/admin/module-scaffold";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { PRD_INTEGRATION_ACCESS_ANY } from "@/lib/admin-prd";
|
||||
import { useAdminProfile } from "@/stores/admin-session";
|
||||
import { adminHasAnyPermission } from "@/lib/admin-permissions";
|
||||
|
||||
@@ -45,7 +46,7 @@ const HUB_CARDS: HubCard[] = [
|
||||
href: "/admin/config/integration-sites",
|
||||
titleKey: "hub.integrationTitle",
|
||||
descKey: "hub.integrationDesc",
|
||||
requiredAny: ["prd.integration.view", "prd.integration.manage"],
|
||||
requiredAny: PRD_INTEGRATION_ACCESS_ANY,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ import type { AdminDrawFinanceSummaryData } from "@/types/api/admin-draw-finance
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { useAdminCurrencyCatalog } from "@/hooks/use-admin-currency-catalog";
|
||||
import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter";
|
||||
import { useConfirmAction } from "@/hooks/use-confirm-action";
|
||||
import { useExportLabels } from "@/hooks/use-export-labels";
|
||||
import { formatAdminMinorUnits } from "@/lib/money";
|
||||
@@ -44,6 +45,7 @@ export function DrawFinanceConsole({ drawId }: { drawId: string }): React.ReactE
|
||||
PRD_PAYOUT_REVIEW,
|
||||
]);
|
||||
const [data, setData] = useState<AdminDrawFinanceSummaryData | null>(null);
|
||||
const formatTs = useAdminDateTimeFormatter();
|
||||
const exportLabels = useExportLabels("drawFinance", { drawNo: data?.draw_no ?? drawId });
|
||||
const [err, setErr] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -219,8 +221,8 @@ export function DrawFinanceConsole({ drawId }: { drawId: string }): React.ReactE
|
||||
<TableCell className="text-center tabular-nums text-xs">
|
||||
{formatMoney(b.total_jackpot_payout_amount)}
|
||||
</TableCell>
|
||||
<TableCell className="font-mono text-[11px] text-muted-foreground">
|
||||
{b.finished_at ?? "—"}
|
||||
<TableCell className="whitespace-nowrap text-xs text-muted-foreground">
|
||||
{formatTs(b.finished_at)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { adminHasAnyPermission } from "@/lib/admin-permissions";
|
||||
import { useAdminProfile } from "@/stores/admin-session";
|
||||
@@ -103,6 +104,7 @@ export function DrawResultsConsole({ drawId }: { drawId: string }) {
|
||||
|
||||
function BatchTable({ batch }: { batch: AdminDrawBatchRow }) {
|
||||
const { t } = useTranslation("draws");
|
||||
const formatDt = useAdminDateTimeFormatter();
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
@@ -112,7 +114,7 @@ function BatchTable({ batch }: { batch: AdminDrawBatchRow }) {
|
||||
source: batch.source_type === "manual" ? t("manualEntry") : t("rng"),
|
||||
})}{" "}
|
||||
· {t("rngSummary", { hash: batch.rng_seed_hash ?? "—" })} ·{" "}
|
||||
{t("confirmedAt", { time: batch.confirmed_at ?? "—" })}
|
||||
{t("confirmedAt", { time: formatDt(batch.confirmed_at) })}
|
||||
</p>
|
||||
</CardHeader>
|
||||
<CardContent className="overflow-x-auto pt-0">
|
||||
|
||||
@@ -36,11 +36,15 @@ import {
|
||||
} from "@/components/ui/table";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { adminHasAnyPermission } from "@/lib/admin-permissions";
|
||||
import { getAdminPageBundle } from "@/lib/admin-permission-bundles";
|
||||
import { useAdminProfile } from "@/stores/admin-session";
|
||||
import { LotteryApiBizError } from "@/types/api/errors";
|
||||
import type {
|
||||
AdminIntegrationSiteCreatePayload,
|
||||
AdminIntegrationSiteConnectivityResult,
|
||||
AdminIntegrationSiteUpdatePayload,
|
||||
AdminIntegrationSiteRow,
|
||||
AdminIntegrationSiteDetail,
|
||||
AdminIntegrationSiteSecrets,
|
||||
AdminIntegrationSiteWithSecrets,
|
||||
} from "@/types/api/admin-integration-site";
|
||||
@@ -86,7 +90,7 @@ function textToOrigins(text: string): string[] {
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function rowToForm(row: AdminIntegrationSiteRow & Partial<FormState>): FormState {
|
||||
function rowToForm(row: AdminIntegrationSiteDetail): FormState {
|
||||
return {
|
||||
code: row.code,
|
||||
name: row.name,
|
||||
@@ -103,7 +107,12 @@ function rowToForm(row: AdminIntegrationSiteRow & Partial<FormState>): FormState
|
||||
};
|
||||
}
|
||||
|
||||
function formToPayload(form: FormState, includeCode: boolean) {
|
||||
function formToPayload(form: FormState, includeCode: true): AdminIntegrationSiteCreatePayload;
|
||||
function formToPayload(form: FormState, includeCode: false): AdminIntegrationSiteUpdatePayload;
|
||||
function formToPayload(
|
||||
form: FormState,
|
||||
includeCode: boolean,
|
||||
): AdminIntegrationSiteCreatePayload | AdminIntegrationSiteUpdatePayload {
|
||||
const base = {
|
||||
name: form.name.trim(),
|
||||
currency_code: form.currency_code.trim() || "NPR",
|
||||
@@ -128,11 +137,10 @@ function formToPayload(form: FormState, includeCode: boolean) {
|
||||
export function IntegrationSitesConsole() {
|
||||
const { t } = useTranslation("config");
|
||||
const profile = useAdminProfile();
|
||||
const canView = adminHasAnyPermission(profile?.permissions, [
|
||||
"prd.integration.view",
|
||||
"prd.integration.manage",
|
||||
]);
|
||||
const canManage = adminHasAnyPermission(profile?.permissions, ["prd.integration.manage"]);
|
||||
const canManage = adminHasAnyPermission(
|
||||
profile?.permissions,
|
||||
getAdminPageBundle("integration-sites", "manage"),
|
||||
);
|
||||
|
||||
const [items, setItems] = useState<AdminIntegrationSiteRow[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -158,12 +166,6 @@ export function IntegrationSitesConsole() {
|
||||
const [exportBusyId, setExportBusyId] = useState<number | null>(null);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
if (!canView) {
|
||||
setItems([]);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await getAdminIntegrationSites();
|
||||
@@ -175,7 +177,7 @@ export function IntegrationSitesConsole() {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [canView, t]);
|
||||
}, [t]);
|
||||
|
||||
useEffect(() => {
|
||||
queueMicrotask(() => {
|
||||
@@ -334,14 +336,6 @@ export function IntegrationSitesConsole() {
|
||||
}
|
||||
}
|
||||
|
||||
if (!canView) {
|
||||
return (
|
||||
<AdminPageCard title={t("integrationSites.title")}>
|
||||
<p className="text-sm text-muted-foreground">{t("integrationSites.noPermission")}</p>
|
||||
</AdminPageCard>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<AdminPageCard
|
||||
@@ -377,13 +371,12 @@ export function IntegrationSitesConsole() {
|
||||
<TableCell>{row.name}</TableCell>
|
||||
<TableCell>
|
||||
<AdminStatusBadge
|
||||
tone={row.status === 1 ? "success" : "muted"}
|
||||
label={
|
||||
row.status === 1
|
||||
? t("integrationSites.statusEnabled")
|
||||
: t("integrationSites.statusDisabled")
|
||||
}
|
||||
/>
|
||||
tone={row.status === 1 ? "success" : "neutral"}
|
||||
>
|
||||
{row.status === 1
|
||||
? t("integrationSites.statusEnabled")
|
||||
: t("integrationSites.statusDisabled")}
|
||||
</AdminStatusBadge>
|
||||
</TableCell>
|
||||
<TableCell className="max-w-[240px] truncate text-xs text-muted-foreground">
|
||||
{row.wallet_api_url ?? "—"}
|
||||
@@ -407,9 +400,16 @@ export function IntegrationSitesConsole() {
|
||||
>
|
||||
{t("integrationSites.exportParams")}
|
||||
</Button>
|
||||
<Button type="button" variant="outline" size="sm" onClick={() => void openEdit(row)}>
|
||||
{t("integrationSites.edit")}
|
||||
</Button>
|
||||
{canManage ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => void openEdit(row)}
|
||||
>
|
||||
{t("integrationSites.edit")}
|
||||
</Button>
|
||||
) : null}
|
||||
{canManage ? (
|
||||
<Button
|
||||
type="button"
|
||||
|
||||
@@ -286,9 +286,10 @@ export function JackpotPoolsConsole({ embedded = false }: JackpotPoolsConsolePro
|
||||
<Label>{t("adjustmentDirection")}</Label>
|
||||
<Select
|
||||
value={adj.direction}
|
||||
onValueChange={(value: "increase" | "decrease") =>
|
||||
updateAdjustmentDraft(p.id, { direction: value })
|
||||
}
|
||||
onValueChange={(value: "increase" | "decrease" | null) => {
|
||||
if (value === null) return;
|
||||
updateAdjustmentDraft(p.id, { direction: value });
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-full min-w-0 sm:max-w-[12rem]">
|
||||
<SelectValue>
|
||||
|
||||
@@ -305,7 +305,10 @@ export function PlayersConsole(): React.ReactElement {
|
||||
{canChooseSite ? (
|
||||
<div className="admin-list-field">
|
||||
<Label className="sm:w-20 sm:shrink-0">{t("filterSite")}</Label>
|
||||
<Select value={siteCode || "__all__"} onValueChange={(v) => setSiteCode(v === "__all__" ? "" : v)}>
|
||||
<Select
|
||||
value={siteCode || "__all__"}
|
||||
onValueChange={(v) => setSiteCode(v === "__all__" ? "" : v ?? "")}
|
||||
>
|
||||
<SelectTrigger className="w-full sm:w-[12rem]">
|
||||
<SelectValue placeholder={t("filterAllSites")} />
|
||||
</SelectTrigger>
|
||||
|
||||
@@ -73,6 +73,8 @@ import {
|
||||
} from "@/components/ui/table";
|
||||
import { useAdminCurrencyCatalog } from "@/hooks/use-admin-currency-catalog";
|
||||
import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter";
|
||||
import { formatAdminInstant } from "@/lib/admin-datetime";
|
||||
import { getAdminRequestLocale } from "@/lib/admin-locale";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { formatAdminMinorUnits } from "@/lib/money";
|
||||
import { LotteryApiBizError } from "@/types/api/errors";
|
||||
@@ -249,6 +251,10 @@ function normalizeFilenamePart(value: string): string {
|
||||
return value.trim().replace(/[\\/:*?"<>|\s]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
||||
}
|
||||
|
||||
function formatExportInstant(iso: string | null | undefined): ExportCell {
|
||||
return formatAdminInstant(iso, { locale: getAdminRequestLocale() });
|
||||
}
|
||||
|
||||
function toCsvValue(value: ExportCell): string {
|
||||
if (value == null) {
|
||||
return "";
|
||||
@@ -369,7 +375,7 @@ function drawRowsFromSummary(summary: AdminDrawFinanceSummaryData): ExportRow[]
|
||||
total_win_count: batch.total_win_count,
|
||||
total_payout_amount: batch.total_payout_amount,
|
||||
total_jackpot_payout_amount: batch.total_jackpot_payout_amount,
|
||||
finished_at: batch.finished_at,
|
||||
finished_at: formatExportInstant(batch.finished_at),
|
||||
})),
|
||||
];
|
||||
}
|
||||
@@ -575,8 +581,8 @@ export function ReportsConsole() {
|
||||
status: item.status,
|
||||
external_ref_no: item.external_ref_no,
|
||||
fail_reason: item.fail_reason,
|
||||
created_at: item.created_at,
|
||||
finished_at: item.finished_at,
|
||||
created_at: formatExportInstant(item.created_at),
|
||||
finished_at: formatExportInstant(item.finished_at),
|
||||
}));
|
||||
setResult({
|
||||
key: "player_transfer",
|
||||
@@ -625,7 +631,7 @@ export function ReportsConsole() {
|
||||
ticket_no: item.ticket_no,
|
||||
play_code: item.play_code,
|
||||
player_id: item.player_id,
|
||||
created_at: item.created_at,
|
||||
created_at: formatExportInstant(item.created_at),
|
||||
})),
|
||||
];
|
||||
setResult({
|
||||
@@ -737,7 +743,7 @@ export function ReportsConsole() {
|
||||
target_id: item.target_id,
|
||||
ip: item.ip,
|
||||
user_agent: item.user_agent,
|
||||
created_at: item.created_at,
|
||||
created_at: formatExportInstant(item.created_at),
|
||||
}));
|
||||
setResult({
|
||||
key: "admin_audit",
|
||||
|
||||
@@ -194,7 +194,7 @@ export function PlayerTicketsConsole(): React.ReactElement {
|
||||
onValueChange={(v) =>
|
||||
setDraft((current) => ({
|
||||
...current,
|
||||
siteCode: v === "__all__" ? "" : v,
|
||||
siteCode: v === "__all__" ? "" : (v ?? ""),
|
||||
}))
|
||||
}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user