Files
gitmost/apps/client/src/features/share/services/share-service.ts
claude code agent 227 fdeede003b 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>
2026-06-26 06:28:26 +03:00

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;
}