feat: 新增受保护链接并将控制台合并为资源访问管理

- 新增 ProtectedLink/LinkVisitRecord,/link/{slug} 门禁页 + 一次性令牌跳转
- 复用邮箱验证体系,验证码邮件区分下载/链接模板
- 控制台下载资源与受保护链接合并为「资源访问管理」单页(页签切换)
- 引用扫描支持 /link/ 链接,角色模板/设置/文档同步更新
- 版本 1.3.0
This commit is contained in:
shirainbown
2026-08-09 14:28:06 +08:00
parent 1c8dad158f
commit 54dbba9ea2
37 changed files with 2659 additions and 223 deletions
+118
View File
@@ -4,7 +4,11 @@ import type {
DownloadResourceList,
DownloadResourceSpec,
DownloadRecord,
LinkVisitRecord,
ListResult,
ProtectedLink,
ProtectedLinkList,
ProtectedLinkSpec,
ReferencesMap,
} from '@/types';
@@ -125,3 +129,117 @@ export async function refreshReferences(): Promise<ReferencesMap> {
);
return data;
}
// ---------- 受保护链接 ----------
interface LinkVo {
name: string;
slug: string;
displayName: string;
description?: string;
targetUrl?: string;
requireEmailVerify?: boolean;
enabled?: boolean;
visitUrl?: string;
creationTimestamp?: string;
stats?: {
visitCount?: number;
visitorCount?: number;
referenceCount?: number;
};
}
function normalizeLink(vo: LinkVo): ProtectedLink {
return {
metadata: { name: vo.name, creationTimestamp: vo.creationTimestamp },
spec: {
slug: vo.slug,
displayName: vo.displayName,
description: vo.description,
targetUrl: vo.targetUrl,
requireEmailVerify: vo.requireEmailVerify,
enabled: vo.enabled,
},
status: { visitCount: vo.stats?.visitCount ?? 0 },
stats: {
visitorCount: vo.stats?.visitorCount ?? 0,
referenceCount: vo.stats?.referenceCount ?? 0,
},
};
}
export async function listLinks(): Promise<ProtectedLinkList> {
const { data } = await axiosInstance.get<LinkVo[]>(
`${API_BASE}/protected-links`
);
const items = (data ?? []).map(normalizeLink);
return { items, total: items.length };
}
export async function createLink(spec: ProtectedLinkSpec): Promise<ProtectedLink> {
const { data } = await axiosInstance.post<LinkVo>(
`${API_BASE}/protected-links`,
spec
);
return normalizeLink(data);
}
export async function getLink(name: string): Promise<ProtectedLink> {
const { data } = await axiosInstance.get<LinkVo>(
`${API_BASE}/protected-links/${name}`
);
return normalizeLink(data);
}
export async function updateLink(
name: string,
spec: ProtectedLinkSpec
): Promise<ProtectedLink> {
const { data } = await axiosInstance.put<LinkVo>(
`${API_BASE}/protected-links/${name}`,
spec
);
return normalizeLink(data);
}
export async function deleteLink(name: string): Promise<void> {
await axiosInstance.delete(`${API_BASE}/protected-links/${name}`);
}
export async function listLinkRecords(params: {
linkSlug?: string;
page?: number;
size?: number;
}): Promise<ListResult<LinkVisitRecord>> {
const { data } = await axiosInstance.get<ListResult<LinkVisitRecord>>(
`${API_BASE}/link-visit-records`,
{ params }
);
return data;
}
export async function deleteLinkRecord(name: string): Promise<void> {
await axiosInstance.delete(`${API_BASE}/link-visit-records/${name}`);
}
// CSV 导出走浏览器直接导航(console 端点接受 session cookie
export function linkRecordsExportUrl(linkSlug?: string): string {
const query = linkSlug
? `?linkSlug=${encodeURIComponent(linkSlug)}`
: '';
return `${API_BASE}/link-visit-records/-/export${query}`;
}
export async function getLinkReferences(): Promise<ReferencesMap> {
const { data } = await axiosInstance.get<ReferencesMap>(
`${API_BASE}/link-references`
);
return data;
}
export async function refreshLinkReferences(): Promise<ReferencesMap> {
const { data } = await axiosInstance.post<ReferencesMap>(
`${API_BASE}/link-references/-/refresh`
);
return data;
}