feat(admin): bot management (list, verify, create, delete)
- Add a Bots admin tab: list/search bots with a dedicated read query (users.is_bot, excluded from the accounts list), showing owner and system-vs-user type - Create system bots from the admin via a new bot.create command that reuses the existing bot provisioning flow; the token is shown once - Delete user-created bots via a new bot.delete command backed by a dedicated Postgres DeleteBotAccount (revokes sessions, purges private state, releases username, drops the bots row, tombstones the user); system service bots are rejected - Verified badge toggling reuses the existing set-verified command - All write paths go through the dry-run/confirm + audit command pipeline - Rebuild dist bundle
This commit is contained in:
parent
ad9d535edc
commit
9e45da69ef
22 changed files with 985 additions and 12 deletions
108
cmd/telesrv-admin/web/src/pages/BotDetailPage.tsx
Normal file
108
cmd/telesrv-admin/web/src/pages/BotDetailPage.tsx
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
import { ArrowLeft, BadgeCheck, Trash2 } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { api, errorMessage } from "../api";
|
||||
import { ActionButton } from "../components/ActionButton";
|
||||
import { Alert, AuditTable, Badge, LoadingSurface, PageFrame, SectionHead, SplitLayout, Summary } from "../components/ui";
|
||||
import { useI18n } from "../i18n";
|
||||
import { displayUsername, formatDate } from "../lib/format";
|
||||
import type { Navigate } from "../routing";
|
||||
import type { BotDetail } from "../types";
|
||||
|
||||
export function BotDetailPage({ id, navigate }: { id: number; navigate: Navigate }) {
|
||||
const { t } = useI18n();
|
||||
const [detail, setDetail] = useState<BotDetail | null>(null);
|
||||
const [error, setError] = useState("");
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
async function load() {
|
||||
setBusy(true);
|
||||
setError("");
|
||||
try {
|
||||
setDetail(await api.bot(id));
|
||||
} catch (err) {
|
||||
setError(errorMessage(err));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
void load();
|
||||
}, [id]);
|
||||
|
||||
if (error) {
|
||||
return <Alert>{error}</Alert>;
|
||||
}
|
||||
if (!detail) {
|
||||
return <LoadingSurface label={busy ? t("bots.loadingDetail") : t("account.waitingData")} />;
|
||||
}
|
||||
|
||||
const bot = detail.Bot;
|
||||
return (
|
||||
<PageFrame
|
||||
title={t("bots.detailTitle", { id: bot.ID })}
|
||||
eyebrow={t("bots.profile")}
|
||||
actions={<button className="btn icon-text" onClick={() => navigate("/bots")}><ArrowLeft size={15} /> {t("common.backToList")}</button>}
|
||||
>
|
||||
<SplitLayout
|
||||
main={
|
||||
<div className="stacked-sections">
|
||||
<section className="entity-head">
|
||||
<div>
|
||||
<div className="entity-title">{bot.FirstName || t("bots.unnamed")}</div>
|
||||
<div className="entity-subtitle">{displayUsername(bot.Username) || t("account.noUsername")}</div>
|
||||
</div>
|
||||
<div className="entity-badges">
|
||||
<Badge tone={bot.System ? "warn" : "neutral"}>{bot.System ? t("bots.system") : t("bots.user")}</Badge>
|
||||
{bot.Verified ? <Badge tone="good">{t("common.verified")}</Badge> : <Badge>{t("account.notVerified")}</Badge>}
|
||||
</div>
|
||||
</section>
|
||||
<div className="summary-grid">
|
||||
<Summary label={t("bots.botID")} value={String(bot.ID)} mono />
|
||||
<Summary label={t("bots.owner")} value={bot.OwnerUserID > 0 ? `${bot.OwnerUserID} ${displayUsername(detail.OwnerUsername)}`.trim() : t("common.none")} />
|
||||
<Summary label={t("bots.type")} value={bot.System ? t("bots.system") : t("bots.user")} />
|
||||
<Summary label={t("common.updatedAt")} value={formatDate(bot.UpdatedAt) || "-"} />
|
||||
<Summary label={t("account.createdAt")} value={formatDate(bot.CreatedAt) || "-"} />
|
||||
</div>
|
||||
{detail.About && <p className="about-text">{detail.About}</p>}
|
||||
{detail.Description && detail.Description.trim() !== detail.About.trim() && <p className="about-text">{detail.Description}</p>}
|
||||
<section className="section-block">
|
||||
<SectionHead title={t("account.recentAdminOps")} text={t("account.recent30Audit")} />
|
||||
<AuditTable rows={detail.AuditLogs} />
|
||||
</section>
|
||||
</div>
|
||||
}
|
||||
side={
|
||||
<section className="action-dock">
|
||||
<div className="dock-title">{t("bots.actionDock")}</div>
|
||||
<div className="action-stack">
|
||||
<ActionButton
|
||||
label={bot.Verified ? t("account.clearVerified") : t("account.setVerified")}
|
||||
icon={<BadgeCheck size={15} />}
|
||||
tone="neutral"
|
||||
path="/api/actions/set-verified"
|
||||
payload={() => ({ user_id: bot.ID, verified: !bot.Verified })}
|
||||
onDone={load}
|
||||
/>
|
||||
</div>
|
||||
{bot.System ? (
|
||||
<p className="bot-create-note">{t("bots.systemHint")}</p>
|
||||
) : (
|
||||
<div className="danger-zone">
|
||||
<ActionButton
|
||||
label={t("bots.delete")}
|
||||
icon={<Trash2 size={15} />}
|
||||
tone="danger"
|
||||
path="/api/actions/delete-bot"
|
||||
payload={() => ({ bot_user_id: bot.ID })}
|
||||
onDone={() => navigate("/bots")}
|
||||
/>
|
||||
<p className="bot-create-note">{t("bots.deleteHint")}</p>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
}
|
||||
/>
|
||||
</PageFrame>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue