added ability to disable third-party verification

This commit is contained in:
onysd 2026-08-06 05:27:48 +03:00
parent 4f0fa895c1
commit 36350f83dc
17 changed files with 211 additions and 69 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -23,7 +23,7 @@
})();
</script>
<script type="module" crossorigin src="/assets/index-CP1sn_7z.js"></script>
<script type="module" crossorigin src="/assets/index-BUraMCki.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-XySYVUb7.css">
</head>
<body>

View file

@ -41,7 +41,7 @@ export function App() {
}
return (
<PermissionsProvider permissions={session.permissions ?? []}>
<PermissionsProvider permissions={session.permissions ?? []} hideThirdPartyVerification={session.hide_third_party_verification ?? true}>
<Shell actor={session.actor} route={route} navigate={navigate} onLogout={() => setSession(null)}>
<Routes route={route} navigate={navigate} />
</Shell>

View file

@ -19,7 +19,7 @@ import {
} from "lucide-react";
import { useEffect, useState, type ReactNode } from "react";
import { api } from "../api";
import { permissionBotVerificationReview, permissionVerificationReview, useCan } from "../permissions";
import { permissionBotVerificationReview, permissionVerificationReview, useCan, useThirdPartyVerificationHidden } from "../permissions";
import { type Navigate, type RouteState, routeTitle } from "../routing";
import { ThemeSwitch } from "../theme";
import { AppLink } from "./AppLink";
@ -58,6 +58,9 @@ export function Shell({
// Same reasoning for the third-party queue, which has its own right: the two
// sections are granted independently, so one entry can be visible without the other.
const canReviewBotVerification = useCan(permissionBotVerificationReview);
// Third-party verification is additionally hidden by default (not fully
// finished) regardless of what the session was granted -- see permissions.tsx.
const thirdPartyVerificationHidden = useThirdPartyVerificationHidden();
const messagesActive = route.path.startsWith("/messages");
const [messagesOpen, setMessagesOpen] = useState(messagesActive);
@ -92,7 +95,7 @@ export function Shell({
{canReviewVerification && (
<NavLink icon={<BadgeCheck size={16} />} href="/verification" route={route} navigate={navigate}>{"Verification"}</NavLink>
)}
{canReviewBotVerification && (
{canReviewBotVerification && !thirdPartyVerificationHidden && (
<NavLink icon={<Stamp size={16} />} href="/bot-verification" route={route} navigate={navigate}>{"Third-party marks"}</NavLink>
)}
<NavLink icon={<AtSign size={16} />} href="/collectible-usernames" route={route} navigate={navigate}>{"NFT Usernames"}</NavLink>

View file

@ -27,6 +27,7 @@ import { VerificationDetailPage } from "./VerificationDetailPage";
import { VerificationPage } from "./VerificationPage";
import {
PermissionGate,
ThirdPartyVerificationHiddenGate,
permissionBotVerificationReview,
permissionVerificationReview
} from "../permissions";
@ -45,16 +46,20 @@ export function Routes({ route, navigate }: { route: RouteState; navigate: Navig
const botVerificationRequestID = route.path.match(/^\/bot-verification\/(\d+)$/)?.[1];
if (botVerificationRequestID) {
return (
<PermissionGate permission={permissionBotVerificationReview}>
<BotVerificationRequestPage id={botVerificationRequestID} navigate={navigate} />
</PermissionGate>
<ThirdPartyVerificationHiddenGate>
<PermissionGate permission={permissionBotVerificationReview}>
<BotVerificationRequestPage id={botVerificationRequestID} navigate={navigate} />
</PermissionGate>
</ThirdPartyVerificationHiddenGate>
);
}
if (route.path === "/bot-verification") {
return (
<PermissionGate permission={permissionBotVerificationReview}>
<BotVerificationPage navigate={navigate} />
</PermissionGate>
<ThirdPartyVerificationHiddenGate>
<PermissionGate permission={permissionBotVerificationReview}>
<BotVerificationPage navigate={navigate} />
</PermissionGate>
</ThirdPartyVerificationHiddenGate>
);
}
// The detail match has to be tested before the exact "/verification" branch, and

View file

@ -17,20 +17,32 @@ export const permissionBotVerificationManage = "botverification.manage";
// section the session may not use is hidden instead of rendered into a 403. This
// is a convenience for the operator, not a security boundary: every route is
// checked again server-side.
const PermissionsContext = createContext<readonly string[]>([]);
type SessionFlags = {
permissions: readonly string[];
// Mirrors AdminSession.hide_third_party_verification. Deliberately NOT folded
// into the permission list: it applies regardless of what the session was
// granted (even "*"), because the feature is not fully finished rather than
// merely restricted.
hideThirdPartyVerification: boolean;
};
const PermissionsContext = createContext<SessionFlags>({ permissions: [], hideThirdPartyVerification: true });
export function PermissionsProvider({
permissions,
hideThirdPartyVerification = true,
children
}: {
permissions: readonly string[];
hideThirdPartyVerification?: boolean;
children: ReactNode;
}) {
return <PermissionsContext.Provider value={permissions}>{children}</PermissionsContext.Provider>;
const value = useMemo(() => ({ permissions, hideThirdPartyVerification }), [permissions, hideThirdPartyVerification]);
return <PermissionsContext.Provider value={value}>{children}</PermissionsContext.Provider>;
}
export function usePermissions(): { permissions: readonly string[]; can: (permission: string) => boolean } {
const permissions = useContext(PermissionsContext);
const { permissions } = useContext(PermissionsContext);
return useMemo(
() => ({
permissions,
@ -44,6 +56,13 @@ export function useCan(permission: string): boolean {
return usePermissions().can(permission);
}
// useThirdPartyVerificationHidden reports the server's
// TELESRV_HIDE_THIRD_PARTY_VERIFICATION setting (default true). Unlike
// useCan, this is never overridden by a "*" session -- see SessionFlags.
export function useThirdPartyVerificationHidden(): boolean {
return useContext(PermissionsContext).hideThirdPartyVerification;
}
// PermissionGate is what a direct URL hits: without the right the operator gets
// an explanation naming the missing permission, not an empty table that looks
// like "no data".
@ -70,3 +89,26 @@ export function PermissionDenied({ permission }: { permission: string }) {
</PageFrame>
);
}
// ThirdPartyVerificationHiddenGate is what a direct URL to a third-party
// verification page hits while the feature is hidden -- distinct from
// PermissionGate because no permission grant (not even "*") changes this.
export function ThirdPartyVerificationHiddenGate({ children }: { children: ReactNode }) {
const hidden = useThirdPartyVerificationHidden();
if (!hidden) {
return <>{children}</>;
}
return (
<PageFrame title={"Feature hidden"} eyebrow={"Console / Third-party marks"}>
<Alert>{"Third-party bot verification is hidden on this server (TELESRV_HIDE_THIRD_PARTY_VERIFICATION=true)."}</Alert>
<section className="section-block">
<div className="entity-head">
<div>
<div className="entity-title"><ShieldOff size={16} /> {"Not fully finished"}</div>
<div className="entity-subtitle">{"This feature may cause unstable server behavior and is hidden by default. Set TELESRV_HIDE_THIRD_PARTY_VERIFICATION=false to re-enable it."}</div>
</div>
</div>
</section>
</PageFrame>
);
}

View file

@ -718,6 +718,13 @@ export type AdminSession = {
actor: string;
// The right set the signed session was issued with; ["*"] means everything.
permissions?: string[] | null;
// Mirrors the server's TELESRV_HIDE_THIRD_PARTY_VERIFICATION (default true):
// while true, the panel drops the "Third-party marks" nav entry and its
// routes, regardless of what permissions the session carries -- the feature
// is not fully finished. The server also refuses the underlying routes with
// 404, so this is a UI convenience on top of a real enforcement, not the
// enforcement itself.
hide_third_party_verification?: boolean;
};
export type AdminLoginResult = AdminSession & {