make view activity works!

This commit is contained in:
2026-06-20 17:14:51 +10:00
parent 09ab959b08
commit 91b16aba53
8 changed files with 350 additions and 18 deletions
+70
View File
@@ -3,6 +3,7 @@ import { withPrivateNoStoreHeaders } from './lib/responses';
import { handleAuthApi } from './routes/api.auth';
import { handleDevAuth } from './routes/api.dev-auth';
import { handleLinksApi } from './routes/api.links';
import { handleLinkGoRoute } from './routes/link-go';
import { handlePromotionsApi } from './routes/api.promotions';
import { handlePrivateShortlink, isHeygoPrivateHost } from './routes/private-redirect';
import { handlePublicShortlink, isHeygoPublicHost, isReservedPublicPath } from './routes/redirect';
@@ -27,10 +28,74 @@ function withPrivateHostNoStoreHeaders(url: URL, response: Response, privateHost
return isHeygoPrivateHost(url.hostname, privateHost) ? withPrivateNoStoreHeaders(response) : response;
}
function isHtmlNavigationRequest(request: Request): boolean {
const accept = request.headers.get('accept') ?? '';
const secFetchDest = request.headers.get('sec-fetch-dest') ?? '';
return accept.includes('text/html') || secFetchDest === 'document';
}
function spaHashUrl(appBaseUrl: string, route: string): string {
return `${appBaseUrl}/#${route}`;
}
function spaRedirectForAppPath(url: URL, appBaseUrl: string): Response | null {
const pathname = url.pathname.replace(/\/+$/, '') || '/';
if (pathname === '/app') {
return Response.redirect(spaHashUrl(appBaseUrl, '/'), 302);
}
if (pathname === '/app/private') {
return Response.redirect(spaHashUrl(appBaseUrl, '/my-links'), 302);
}
if (pathname === '/app/login') {
return Response.redirect(spaHashUrl(appBaseUrl, '/login'), 302);
}
if (pathname === '/app/dev-login') {
return Response.redirect(spaHashUrl(appBaseUrl, '/dev-login'), 302);
}
if (pathname === '/app/admin') {
return Response.redirect(spaHashUrl(appBaseUrl, '/admin'), 302);
}
if (pathname === '/app/profile') {
return Response.redirect(spaHashUrl(appBaseUrl, '/profile'), 302);
}
if (pathname === '/app/settings') {
return Response.redirect(spaHashUrl(appBaseUrl, '/settings'), 302);
}
return null;
}
async function serveSpaShell(request: Request, env: Env): Promise<Response | null> {
if (!env.ASSETS) {
return null;
}
const url = new URL(request.url);
if (url.pathname.startsWith('/assets/')) {
return env.ASSETS.fetch(request);
}
if (url.pathname !== '/' || !isHtmlNavigationRequest(request)) {
return null;
}
const assetUrl = new URL('/index.html', url);
return env.ASSETS.fetch(new Request(assetUrl, request));
}
export default {
async fetch(request, env, ctx): Promise<Response> {
const url = new URL(request.url);
const appPathRedirect = spaRedirectForAppPath(url, env.APP_BASE_URL);
if (appPathRedirect) {
return withPrivateHostNoStoreHeaders(url, appPathRedirect, env.PRIVATE_HOST);
}
const spaShellResponse = await serveSpaShell(request, env);
if (spaShellResponse && isHeygoPublicHost(url.hostname, env.PUBLIC_HOST)) {
return withPrivateHostNoStoreHeaders(url, spaShellResponse, env.PRIVATE_HOST);
}
if (url.pathname === '/api/health') {
return withPrivateHostNoStoreHeaders(url, json({ ok: true, service: 'heygo-worker' }), env.PRIVATE_HOST);
}
@@ -55,6 +120,11 @@ export default {
return withPrivateHostNoStoreHeaders(url, json({ error: 'Not found' }, { status: 404 }), env.PRIVATE_HOST);
}
const linkGoResponse = await handleLinkGoRoute(request, env, ctx);
if (linkGoResponse) {
return withPrivateHostNoStoreHeaders(url, linkGoResponse, env.PRIVATE_HOST);
}
// In local dev (PUBLIC_HOST=*), public and private share the same host.
// Try public first; if it returns 404 (alias not found), fall through to
// private so authenticated users can resolve their personal shortlinks.
+131
View File
@@ -0,0 +1,131 @@
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.
}
}