mirror of
https://github.com/wahyd4/heygo.git
synced 2026-08-08 21:05:48 +10:00
132 lines
3.8 KiB
TypeScript
132 lines
3.8 KiB
TypeScript
import { getCurrentUser } from '../auth';
|
|
import type { Env } from '../env';
|
|
import { renderCustomLinkHtml } from '../lib/custom-link';
|
|
import {
|
|
htmlResponse,
|
|
privateAliasNotFoundResponse,
|
|
privateHtmlResponse,
|
|
privateLoginRequiredResponse,
|
|
privateRedirectResponse,
|
|
publicBadRequestResponse,
|
|
publicNotFoundResponse,
|
|
withPrivateNoStoreHeaders,
|
|
} from '../lib/responses';
|
|
import { resolveTemplateUrl, TemplateResolutionError } from '../lib/templates';
|
|
|
|
type LinkRow = {
|
|
id: string;
|
|
alias: string;
|
|
link_type: 'redirect' | 'custom';
|
|
target_url: string | null;
|
|
content_markdown: string | null;
|
|
};
|
|
|
|
const PUBLIC_LINK_BY_ID_QUERY = `SELECT id, alias, link_type, target_url, content_markdown
|
|
FROM links
|
|
WHERE id=? AND scope='public' AND status='active'
|
|
LIMIT 1`;
|
|
|
|
const PRIVATE_LINK_BY_ID_QUERY = `SELECT id, alias, link_type, target_url, content_markdown
|
|
FROM links
|
|
WHERE id=? AND scope='private' AND owner_user_id=? AND status='active'
|
|
LIMIT 1`;
|
|
|
|
const CLICK_COUNT_UPDATE = `UPDATE links SET click_count = click_count + 1, updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE id=?`;
|
|
const CLICK_DAILY_UPSERT = `INSERT INTO click_daily (link_id, day, count) VALUES (?, strftime('%Y-%m-%d', 'now'), 1)
|
|
ON CONFLICT (link_id, day) DO UPDATE SET count = count + 1`;
|
|
|
|
export async function handleLinkGoRoute(
|
|
request: Request,
|
|
env: Env,
|
|
ctx: Pick<ExecutionContext, 'waitUntil'>,
|
|
): Promise<Response | null> {
|
|
const url = new URL(request.url);
|
|
const match = url.pathname.match(/^\/links\/([^/]+)\/go$/);
|
|
if (!match) {
|
|
return null;
|
|
}
|
|
|
|
const linkId = decodePathSegment(match[1]);
|
|
if (!linkId) {
|
|
return publicNotFoundResponse();
|
|
}
|
|
|
|
const publicLink = await env.DB.prepare(PUBLIC_LINK_BY_ID_QUERY).bind(linkId).first<LinkRow>();
|
|
if (publicLink) {
|
|
return resolveGoResponse(publicLink, false, request, env, ctx);
|
|
}
|
|
|
|
const user = await getCurrentUser(request, env);
|
|
if (!user) {
|
|
return privateLoginRequiredResponse(env.APP_BASE_URL);
|
|
}
|
|
|
|
const privateLink = await env.DB.prepare(PRIVATE_LINK_BY_ID_QUERY).bind(linkId, user.id).first<LinkRow>();
|
|
if (!privateLink) {
|
|
return privateAliasNotFoundResponse(env.APP_BASE_URL);
|
|
}
|
|
|
|
return resolveGoResponse(privateLink, true, request, env, ctx);
|
|
}
|
|
|
|
async function resolveGoResponse(
|
|
link: LinkRow,
|
|
isPrivate: boolean,
|
|
request: Request,
|
|
env: Env,
|
|
ctx: Pick<ExecutionContext, 'waitUntil'>,
|
|
): Promise<Response> {
|
|
const url = new URL(request.url);
|
|
|
|
ctx.waitUntil(recordClick(env.DB, link.id));
|
|
|
|
if (link.link_type === 'redirect') {
|
|
if (!link.target_url) {
|
|
return isPrivate ? privateAliasNotFoundResponse(env.APP_BASE_URL) : publicNotFoundResponse();
|
|
}
|
|
|
|
try {
|
|
const location = resolveTemplateUrl({
|
|
targetUrl: link.target_url,
|
|
query: url.searchParams,
|
|
});
|
|
|
|
return isPrivate ? privateRedirectResponse(location, 302) : Response.redirect(location, 302);
|
|
} catch (error) {
|
|
if (error instanceof TemplateResolutionError) {
|
|
return isPrivate
|
|
? withPrivateNoStoreHeaders(publicBadRequestResponse(error.message))
|
|
: publicBadRequestResponse(error.message);
|
|
}
|
|
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
if (link.link_type === 'custom') {
|
|
const html = renderCustomLinkHtml(link);
|
|
return isPrivate ? privateHtmlResponse(html, { status: 200 }) : htmlResponse(html, { status: 200 });
|
|
}
|
|
|
|
return isPrivate ? privateAliasNotFoundResponse(env.APP_BASE_URL) : publicNotFoundResponse();
|
|
}
|
|
|
|
function decodePathSegment(value: string): string | null {
|
|
try {
|
|
return decodeURIComponent(value);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async function recordClick(db: D1Database, linkId: string): Promise<void> {
|
|
try {
|
|
await db.batch([
|
|
db.prepare(CLICK_COUNT_UPDATE).bind(linkId),
|
|
db.prepare(CLICK_DAILY_UPSERT).bind(linkId),
|
|
]);
|
|
} catch {
|
|
// Analytics must never block or break link resolution.
|
|
}
|
|
}
|