feat(csms): 添加 OCPP 鉴权
This commit is contained in:
@@ -15,9 +15,10 @@ import {
|
||||
Spinner,
|
||||
Table,
|
||||
TextField,
|
||||
Tooltip,
|
||||
} from "@heroui/react";
|
||||
import { ArrowLeft, Pencil, ArrowRotateRight } from "@gravity-ui/icons";
|
||||
import { api } from "@/lib/api";
|
||||
import { ArrowLeft, Pencil, ArrowRotateRight, Key, Copy, Check } from "@gravity-ui/icons";
|
||||
import { api, type ChargePointPasswordReset } from "@/lib/api";
|
||||
import { useSession } from "@/lib/auth-client";
|
||||
import dayjs from "@/lib/dayjs";
|
||||
import InfoSection from "@/components/info-section";
|
||||
@@ -105,6 +106,29 @@ export default function ChargePointDetailPage({ params }: { params: Promise<{ id
|
||||
feePerKwh: "0",
|
||||
});
|
||||
|
||||
// reset password
|
||||
const [resetBusy, setResetBusy] = useState(false);
|
||||
const [resetResult, setResetResult] = useState<ChargePointPasswordReset | null>(null);
|
||||
const [resetCopied, setResetCopied] = useState(false);
|
||||
|
||||
const handleResetPassword = async () => {
|
||||
if (!cp) return;
|
||||
setResetBusy(true);
|
||||
try {
|
||||
const result = await api.chargePoints.resetPassword(cp.id);
|
||||
setResetResult(result);
|
||||
} finally {
|
||||
setResetBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCopyResetPassword = (text: string) => {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
setResetCopied(true);
|
||||
setTimeout(() => setResetCopied(false), 2000);
|
||||
});
|
||||
};
|
||||
|
||||
const { isFetching: refreshing, ...cpQuery } = useQuery({
|
||||
queryKey: ["chargePoint", id],
|
||||
queryFn: () => api.chargePoints.get(id),
|
||||
@@ -237,10 +261,21 @@ export default function ChargePointDetailPage({ params }: { params: Promise<{ id
|
||||
<ArrowRotateRight className={`size-4 ${refreshing ? "animate-spin" : ""}`} />
|
||||
</Button>
|
||||
{isAdmin && (
|
||||
<Button size="sm" variant="secondary" onPress={openEdit}>
|
||||
<Pencil className="size-4" />
|
||||
编辑
|
||||
</Button>
|
||||
<>
|
||||
<Tooltip>
|
||||
<Tooltip.Content>重置 OCPP 连接密码</Tooltip.Content>
|
||||
<Tooltip.Trigger>
|
||||
<Button size="sm" variant="ghost" isDisabled={resetBusy} onPress={handleResetPassword}>
|
||||
{resetBusy ? <Spinner size="sm" /> : <Key className="size-4" />}
|
||||
重置密码
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
</Tooltip>
|
||||
<Button size="sm" variant="secondary" onPress={openEdit}>
|
||||
<Pencil className="size-4" />
|
||||
编辑
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -552,6 +587,57 @@ export default function ChargePointDetailPage({ params }: { params: Promise<{ id
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Reset password result modal */}
|
||||
{isAdmin && (
|
||||
<Modal
|
||||
isOpen={resetResult !== null}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) { setResetResult(null); setResetCopied(false); }
|
||||
}}
|
||||
>
|
||||
<Modal.Backdrop>
|
||||
<Modal.Container scroll="outside">
|
||||
<Modal.Dialog className="sm:max-w-md">
|
||||
<Modal.Header>
|
||||
<Modal.Heading>密码已重置 — 请保存新密码</Modal.Heading>
|
||||
</Modal.Header>
|
||||
<Modal.Body className="space-y-4">
|
||||
<p className="text-sm text-warning font-medium">
|
||||
⚠️ 此密码仅显示一次,关闭后无法再次查看。旧密码已立即失效,请更新固件配置。
|
||||
</p>
|
||||
<div className="space-y-1.5">
|
||||
<p className="text-xs text-muted font-medium">新 OCPP Basic Auth 密码</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<code className="flex-1 rounded-md bg-surface-secondary px-3 py-2 text-sm font-mono text-foreground select-all">
|
||||
{resetResult?.plainPassword}
|
||||
</code>
|
||||
<Tooltip>
|
||||
<Tooltip.Content>{resetCopied ? "已复制" : "复制密码"}</Tooltip.Content>
|
||||
<Tooltip.Trigger>
|
||||
<Button
|
||||
isIconOnly
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onPress={() => resetResult && handleCopyResetPassword(resetResult.plainPassword)}
|
||||
>
|
||||
{resetCopied ? <Check className="size-4 text-success" /> : <Copy className="size-4" />}
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</Modal.Body>
|
||||
<Modal.Footer className="flex justify-end">
|
||||
<Button onPress={() => { setResetResult(null); setResetCopied(false); }}>
|
||||
我已保存密码
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal.Dialog>
|
||||
</Modal.Container>
|
||||
</Modal.Backdrop>
|
||||
</Modal>
|
||||
)}
|
||||
|
||||
{/* Edit modal */}
|
||||
{isAdmin && (
|
||||
<Modal
|
||||
|
||||
Reference in New Issue
Block a user