Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions workers/admin-panel/src/api/domains.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,12 @@ export interface DomainsResponse {
readonly reason?: UnconfiguredReason;
/** Script name the answer is about, so the UI never has to guess. */
readonly script?: string;
/**
* The Cloudflare account the answer was read from. Not a secret — it appears
* in every dashboard URL — and exposed only so the UI can build the deep
* link to the Worker's Custom Domains screen. Absent when unconfigured.
*/
readonly accountId?: string;
readonly hosts?: readonly HostBinding[];
/** Sources that could not be read, named individually. */
readonly failures?: readonly { readonly source: string; readonly message: string }[];
Expand Down Expand Up @@ -245,6 +251,7 @@ export const discoverDomains = async (env: Env, db: D1Database): Promise<Domains
return {
configured: true,
script,
accountId: credentials.accountId,
hosts,
...(result.failures.length > 0 ? { failures: result.failures } : {}),
...(result.skippedZones === undefined ? {} : { skippedZones: result.skippedZones }),
Expand Down
11 changes: 11 additions & 0 deletions workers/admin-panel/src/domains.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@
beforeEach(async () => {
await applyD1Migrations(testEnv.DB, TEST_MIGRATIONS);
for (const table of ['audit_log', 'routes', 'settings', 'mcp_tokens', 'users']) {
await testEnv.DB.prepare(`DELETE FROM ${table}`).run();

Check warning on line 149 in workers/admin-panel/src/domains.test.ts

View workflow job for this annotation

GitHub Actions / check

eslint(no-await-in-loop)

workers/admin-panel/src/domains.test.ts:149:5: Unexpected `await` inside a loop.
}
__resetDomainCache();
});
Expand Down Expand Up @@ -216,6 +216,17 @@
expect(raw).not.toContain('read-only-token');
});

it('exposes the account id, which is not a secret, only when configured', async () => {
// The account id is what the UI builds the dashboard deep link from; unlike
// the token it appears in every dashboard URL already.
const appEnv = configured({ workersDev: true });
expect((await domains(appEnv, await adminAuth(appEnv))).accountId).toBe('acct-1');

// Unconfigured responses name the reason but not the account.
const emptyEnv = envWith({ CF_ACCOUNT_ID: undefined, CF_API_TOKEN: undefined });
expect((await domains(emptyEnv, await adminAuth(emptyEnv))).accountId).toBeUndefined();
});

it('marks a discovered host with the route ids that claim it', async () => {
const appEnv = configured({
customDomains: [{ hostname: 'mirror.example.com', service: SCRIPT }],
Expand Down
2 changes: 2 additions & 0 deletions workers/admin-panel/web/src/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,8 @@ export interface DomainsResponse {
readonly configured: boolean;
readonly reason?: UnconfiguredReason;
readonly script?: string;
/** 读到答案的 Cloudflare 账户。非机密(所有 dash URL 都带着),仅供拼深链;未配置时省略。 */
readonly accountId?: string;
readonly hosts?: readonly HostBinding[];
readonly failures?: readonly { readonly source: string; readonly message: string }[];
readonly skippedZones?: readonly string[];
Expand Down
1 change: 1 addition & 0 deletions workers/admin-panel/web/src/lib/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -592,6 +592,7 @@ export const t = {
description: '从 Cloudflare 账号读出真正能打到反代的 hostname,并和路由表对一遍。',
refresh: '重新读取',
refreshing: '读取中…',
goBind: '去绑定',
scriptNote: (script: string) => `查询的是 Worker「${script}」的绑定。`,
readOnlyNote: '只读查询,不写数据库也不进审计日志。',
columns: {
Expand Down
27 changes: 26 additions & 1 deletion workers/admin-panel/web/src/views/domains-view.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as React from 'react';
import { GlobeIcon, RefreshCwIcon, TriangleAlertIcon } from 'lucide-react';
import { ExternalLinkIcon, GlobeIcon, RefreshCwIcon, TriangleAlertIcon } from 'lucide-react';
import { Alert, AlertAction, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
Expand Down Expand Up @@ -82,6 +82,13 @@ const errorTitle = (error: LoadError): string => {
/** kinds 表按 BindingKind 三键全覆盖,但服务端将来加新枚举时别渲染出 undefined。 */
const kindLabel = (kind: BindingKind): string => t.domains.kinds[kind] ?? kind;

/**
* Worker 在 Cloudflare 控制台的 Domains & Routes 标签页深链。accountId 非机密
* (本就出现在每个 dash URL 里),响应里专门为拼这个链接而暴露。
*/
const bindUrl = (accountId: string, script: string): string =>
`https://dash.cloudflare.com/${accountId}/workers/services/view/${encodeURIComponent(script)}/production/domains`;

export const DomainsView = () => {
const [data, setData] = React.useState<DomainsResponse | null>(null);
const [error, setError] = React.useState<LoadError | null>(null);
Expand Down Expand Up @@ -111,6 +118,24 @@ export const DomainsView = () => {
<CardTitle>{t.domains.title}</CardTitle>
<CardDescription>{t.domains.description}</CardDescription>
<CardAction>
{/* 去控制台绑域名是这页读数的下一步;缺 accountId(未配置凭据)就不显示,
不猜账户。外链用 render 透传成 <a>,Base UI 的按钮样式跟着走。 */}
{data?.configured === true && data.accountId !== undefined && (
<Button
variant="outline"
size="sm"
render={
<a
href={bindUrl(data.accountId, data.script ?? 'jouska')}
target="_blank"
rel="noreferrer"
/>
}
>
<ExternalLinkIcon />
{t.domains.goBind}
</Button>
)}
<Button variant="outline" size="sm" disabled={refreshing} onClick={() => void load()}>
{refreshing ? <Spinner /> : <RefreshCwIcon />}
{refreshing ? t.domains.refreshing : t.domains.refresh}
Expand Down
Loading