Add a retargetable, human-readable vanity link namespace /l/<alias> that sits alongside the untouched /share/... routes. - New share_aliases table (workspace-scoped, UNIQUE(workspace_id, alias), page_id nullable ON DELETE SET NULL so the address outlives its target). - ShareAliasRepo + ShareAliasService (create / no-op / 409 reassign guard / availability / request-time readable-target resolution through the single existing share boundary). - Public ShareAliasRedirectController (GET /l/:alias) issues a 302 (never 301, the target is mutable) to the canonical /share/:key/p/:slug page; unknown / dangling / no-longer-readable aliases serve the SPA index with no leak. 'l/:alias' excluded from the global /api prefix. - Authenticated ShareAliasController (set/remove/availability/for-page). - Shared ASCII-only normalize/validate util (server + client copies). - Client: Custom address block in the share modal (live normalize + debounced availability + copy + reassign confirmation dialog). - Unit tests: util, repo SQL-shape, service semantics, migration/entity sanity (server jest) + client alias util (vitest). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
93 lines
2.4 KiB
TypeScript
93 lines
2.4 KiB
TypeScript
import api from "@/lib/api-client";
|
|
import { IPage } from "@/features/page/types/page.types";
|
|
|
|
import {
|
|
ICreateShare,
|
|
IShare,
|
|
IShareAlias,
|
|
IShareAliasAvailability,
|
|
ISetShareAlias,
|
|
ISharedItem,
|
|
ISharedPage,
|
|
ISharedPageTree,
|
|
IShareForPage,
|
|
IShareInfoInput,
|
|
IUpdateShare,
|
|
} from "@/features/share/types/share.types.ts";
|
|
import { IPagination, QueryParams } from "@/lib/types.ts";
|
|
|
|
export async function getShares(
|
|
params?: QueryParams,
|
|
): Promise<IPagination<ISharedItem>> {
|
|
const req = await api.post("/shares", params);
|
|
return req.data;
|
|
}
|
|
|
|
export async function createShare(data: ICreateShare): Promise<any> {
|
|
const req = await api.post<any>("/shares/create", data);
|
|
return req.data;
|
|
}
|
|
|
|
export async function getShareInfo(shareId: string): Promise<IShare> {
|
|
const req = await api.post<IShare>("/shares/info", { shareId });
|
|
return req.data;
|
|
}
|
|
|
|
export async function updateShare(data: IUpdateShare): Promise<any> {
|
|
const req = await api.post<any>("/shares/update", data);
|
|
return req.data;
|
|
}
|
|
|
|
export async function getShareForPage(pageId: string): Promise<IShareForPage> {
|
|
const req = await api.post<any>("/shares/for-page", { pageId });
|
|
return req.data;
|
|
}
|
|
|
|
export async function getSharePageInfo(
|
|
shareInput: Partial<IShareInfoInput>,
|
|
): Promise<ISharedPage> {
|
|
const req = await api.post<ISharedPage>("/shares/page-info", shareInput);
|
|
return req.data;
|
|
}
|
|
|
|
export async function deleteShare(shareId: string): Promise<void> {
|
|
await api.post("/shares/delete", { shareId });
|
|
}
|
|
|
|
export async function getSharedPageTree(
|
|
shareId: string,
|
|
): Promise<ISharedPageTree> {
|
|
const req = await api.post<ISharedPageTree>("/shares/tree", { shareId });
|
|
return req.data;
|
|
}
|
|
|
|
export async function getShareAliasForPage(
|
|
pageId: string,
|
|
): Promise<IShareAlias | null> {
|
|
const req = await api.post<IShareAlias | null>("/share-aliases/for-page", {
|
|
pageId,
|
|
});
|
|
return req.data;
|
|
}
|
|
|
|
export async function setShareAlias(
|
|
data: ISetShareAlias,
|
|
): Promise<IShareAlias> {
|
|
const req = await api.post<IShareAlias>("/share-aliases/set", data);
|
|
return req.data;
|
|
}
|
|
|
|
export async function removeShareAlias(aliasId: string): Promise<void> {
|
|
await api.post("/share-aliases/remove", { aliasId });
|
|
}
|
|
|
|
export async function checkShareAliasAvailability(
|
|
alias: string,
|
|
): Promise<IShareAliasAvailability> {
|
|
const req = await api.post<IShareAliasAvailability>(
|
|
"/share-aliases/availability",
|
|
{ alias },
|
|
);
|
|
return req.data;
|
|
}
|