feat(share): custom /l/:alias pretty links (share_aliases table) (#205)
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>
This commit is contained in:
@@ -4,6 +4,9 @@ import { IPage } from "@/features/page/types/page.types";
|
||||
import {
|
||||
ICreateShare,
|
||||
IShare,
|
||||
IShareAlias,
|
||||
IShareAliasAvailability,
|
||||
ISetShareAlias,
|
||||
ISharedItem,
|
||||
ISharedPage,
|
||||
ISharedPageTree,
|
||||
@@ -57,3 +60,33 @@ export async function getSharedPageTree(
|
||||
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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user