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

View file

@ -27,9 +27,24 @@ import (
// journal, the status machine and the optimistic lock are enforced in one place and
// a panel action is indistinguishable from an API one in the audit trail.
// requireThirdPartyVerificationVisible refuses every third-party verification
// route while the feature is hidden (TELESRV_HIDE_THIRD_PARTY_VERIFICATION,
// default true), regardless of session permissions -- the feature is not
// fully finished and may cause unstable server behavior, so hiding it is
// enforced here, not just by the panel dropping its nav entry.
func (s *server) requireThirdPartyVerificationVisible(handler http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if s.cfg.HideThirdPartyVerification {
http.NotFound(w, r)
return
}
handler(w, r)
}
}
// botVerificationRead mounts a route behind a session and botverification.review.
func (s *server) botVerificationRead(handler http.HandlerFunc) http.Handler {
return s.requireAuthAPI(s.requirePermission(permissionBotVerificationReview, handler))
return s.requireAuthAPI(s.requirePermission(permissionBotVerificationReview, s.requireThirdPartyVerificationVisible(handler)))
}
// botVerificationManage mounts a route behind a session and botverification.manage.
@ -38,7 +53,7 @@ func (s *server) botVerificationRead(handler http.HandlerFunc) http.Handler {
// a verifier and working its queue are different jobs, so an operator may hold
// either without the other.
func (s *server) botVerificationManage(handler http.HandlerFunc) http.Handler {
return s.requireAuthAPI(s.requirePermission(permissionBotVerificationManage, handler))
return s.requireAuthAPI(s.requirePermission(permissionBotVerificationManage, s.requireThirdPartyVerificationVisible(handler)))
}
// ---------------------------------------------------------------------------

View file

@ -76,6 +76,12 @@ type uiConfig struct {
// entry, so introducing the permission model never locks an operator out of a
// panel that worked before.
Permissions []string
// HideThirdPartyVerification mirrors config.HideThirdPartyVerification
// (TELESRV_HIDE_THIRD_PARTY_VERIFICATION, default true): while true, every
// botverification.* route refuses with 404 regardless of session
// permissions, and the session/login response tells the frontend to hide
// the "Third-party marks" nav entry and its routes.
HideThirdPartyVerification bool
}
// loadConfig 通过 internal/config.Load() 加载 .env 配置文件与环境变量,
@ -103,14 +109,15 @@ func loadConfig() (uiConfig, error) {
sum := sha256.Sum256([]byte(appCfg.AdminSessionKey))
return uiConfig{
Addr: appCfg.AdminUIAddr,
PostgresDSN: appCfg.PostgresDSN,
AdminAPIURL: adminAPIURL(adminAPIAddr),
AdminAPIToken: appCfg.AdminAPIToken,
Password: appCfg.AdminUIPassword,
Token: appCfg.AdminUIToken,
SessionKey: sum[:],
Permissions: appCfg.AdminUIPermissions,
Addr: appCfg.AdminUIAddr,
PostgresDSN: appCfg.PostgresDSN,
AdminAPIURL: adminAPIURL(adminAPIAddr),
AdminAPIToken: appCfg.AdminAPIToken,
Password: appCfg.AdminUIPassword,
Token: appCfg.AdminUIToken,
SessionKey: sum[:],
Permissions: appCfg.AdminUIPermissions,
HideThirdPartyVerification: appCfg.HideThirdPartyVerification,
}, nil
}

View file

@ -251,9 +251,10 @@ func (s *server) handleAPILogin(w http.ResponseWriter, r *http.Request) {
})
setCSRFCookie(w, csrfToken, sessionTTL)
writeJSON(w, http.StatusOK, map[string]any{
"actor": "admin",
"permissions": permissions.List(),
"csrf_token": csrfToken,
"actor": "admin",
"permissions": permissions.List(),
"csrf_token": csrfToken,
"hide_third_party_verification": s.cfg.HideThirdPartyVerification,
})
}
@ -277,8 +278,9 @@ func (s *server) handleAPILogout(w http.ResponseWriter, _ *http.Request) {
// than letting them walk into a 403.
func (s *server) handleSession(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, map[string]any{
"actor": actorFromContext(r.Context()),
"permissions": permissionsFromContext(r.Context()).List(),
"actor": actorFromContext(r.Context()),
"permissions": permissionsFromContext(r.Context()).List(),
"hide_third_party_verification": s.cfg.HideThirdPartyVerification,
})
}

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 & {