mirror of
https://github.com/wahyd4/heygo.git
synced 2026-08-09 05:15:52 +10:00
feat: add terraform IaC, dev/prod environments, and multi-host worker support
- Terraform configs for D1 databases, KV namespaces, Worker custom domains - wrangler.dev.jsonc and wrangler.prod.jsonc for environment-specific deployments - Worker code refactored to use env vars for host checking (PUBLIC_HOST, PRIVATE_HOST) - Configurable app URLs and cookie domain via env vars - Deploy and migrate npm scripts for dev/prod - Updated all tests with new env fixtures - Deployment guide in README
This commit is contained in:
+10
-8
@@ -29,21 +29,23 @@ export function isAuthProviderEnabled(provider: string): provider is EnabledAuth
|
||||
return ENABLED_PROVIDER_SET.has(provider);
|
||||
}
|
||||
|
||||
export function getSessionCookieAttributes(requestUrl: string | URL): string[] {
|
||||
const url = typeof requestUrl === 'string' ? new URL(requestUrl) : requestUrl;
|
||||
export function getSessionCookieAttributes(
|
||||
requestUrl: string | URL,
|
||||
cookieDomain: string,
|
||||
): string[] {
|
||||
// The request URL is retained so callers can derive a host-specific domain in
|
||||
// the future; the shared Domain attribute now comes from the COOKIE_DOMAIN
|
||||
// env var so each deployment (dev/prod/local) controls its own scope.
|
||||
void requestUrl;
|
||||
const attributes = ['HttpOnly', 'Secure', 'SameSite=Lax', 'Path=/'];
|
||||
|
||||
if (isHeygoProductionHost(url.hostname)) {
|
||||
attributes.push('Domain=.heygo.cc');
|
||||
if (cookieDomain) {
|
||||
attributes.push(`Domain=${cookieDomain}`);
|
||||
}
|
||||
|
||||
return attributes;
|
||||
}
|
||||
|
||||
function isHeygoProductionHost(hostname: string): boolean {
|
||||
return hostname === 'heygo.cc' || hostname.endsWith('.heygo.cc');
|
||||
}
|
||||
|
||||
export class AuthError extends Error {
|
||||
readonly status: number;
|
||||
|
||||
|
||||
@@ -2,4 +2,8 @@ export interface Env {
|
||||
DB: D1Database;
|
||||
PUBLIC_LINK_CACHE?: KVNamespace;
|
||||
ASSETS?: Fetcher;
|
||||
PUBLIC_HOST: string;
|
||||
PRIVATE_HOST: string;
|
||||
APP_BASE_URL: string;
|
||||
COOKIE_DOMAIN: string;
|
||||
}
|
||||
|
||||
+9
-9
@@ -21,8 +21,8 @@ function json(body: unknown, init: ResponseInit = {}) {
|
||||
});
|
||||
}
|
||||
|
||||
function withPrivateHostNoStoreHeaders(url: URL, response: Response): Response {
|
||||
return isHeygoPrivateHost(url) ? withPrivateNoStoreHeaders(response) : response;
|
||||
function withPrivateHostNoStoreHeaders(url: URL, response: Response, privateHost: string): Response {
|
||||
return isHeygoPrivateHost(url.hostname, privateHost) ? withPrivateNoStoreHeaders(response) : response;
|
||||
}
|
||||
|
||||
export default {
|
||||
@@ -30,29 +30,29 @@ export default {
|
||||
const url = new URL(request.url);
|
||||
|
||||
if (url.pathname === '/api/health') {
|
||||
return withPrivateHostNoStoreHeaders(url, json({ ok: true, service: 'heygo-worker' }));
|
||||
return withPrivateHostNoStoreHeaders(url, json({ ok: true, service: 'heygo-worker' }), env.PRIVATE_HOST);
|
||||
}
|
||||
|
||||
if (url.pathname.startsWith('/api/')) {
|
||||
const apiResponse = await handleLinksApi(request, env);
|
||||
if (apiResponse) {
|
||||
return withPrivateHostNoStoreHeaders(url, apiResponse);
|
||||
return withPrivateHostNoStoreHeaders(url, apiResponse, env.PRIVATE_HOST);
|
||||
}
|
||||
const promotionsResponse = await handlePromotionsApi(request, env);
|
||||
if (promotionsResponse) {
|
||||
return withPrivateHostNoStoreHeaders(url, promotionsResponse);
|
||||
return withPrivateHostNoStoreHeaders(url, promotionsResponse, env.PRIVATE_HOST);
|
||||
}
|
||||
return withPrivateHostNoStoreHeaders(url, json({ error: 'Not found' }, { status: 404 }));
|
||||
return withPrivateHostNoStoreHeaders(url, json({ error: 'Not found' }, { status: 404 }), env.PRIVATE_HOST);
|
||||
}
|
||||
|
||||
if (isHeygoPublicHost(url) && !isReservedPublicPath(url.pathname)) {
|
||||
if (isHeygoPublicHost(url.hostname, env.PUBLIC_HOST) && !isReservedPublicPath(url.pathname)) {
|
||||
return handlePublicShortlink(request, env, ctx);
|
||||
}
|
||||
|
||||
if (isHeygoPrivateHost(url) && !isReservedPublicPath(url.pathname)) {
|
||||
if (isHeygoPrivateHost(url.hostname, env.PRIVATE_HOST) && !isReservedPublicPath(url.pathname)) {
|
||||
return handlePrivateShortlink(request, env, ctx);
|
||||
}
|
||||
|
||||
return withPrivateHostNoStoreHeaders(url, json({ error: 'Not found' }, { status: 404 }));
|
||||
return withPrivateHostNoStoreHeaders(url, json({ error: 'Not found' }, { status: 404 }), env.PRIVATE_HOST);
|
||||
},
|
||||
} satisfies ExportedHandler<Env>;
|
||||
|
||||
+20
-6
@@ -61,19 +61,33 @@ export function publicBadRequestResponse(message = 'Invalid short link'): Respon
|
||||
);
|
||||
}
|
||||
|
||||
export const PRIVATE_LOGIN_URL = 'https://heygo.cc/app/login';
|
||||
export const PRIVATE_APP_URL = 'https://heygo.cc/app/private';
|
||||
/**
|
||||
* Build the app login URL for a given environment's APP_BASE_URL.
|
||||
* Kept as a function (not a constant) so each deployment's base URL is honored.
|
||||
*/
|
||||
export function privateLoginUrl(appBaseUrl: string): string {
|
||||
return `${appBaseUrl}/app/login`;
|
||||
}
|
||||
|
||||
export function privateLoginRequiredResponse(): Response {
|
||||
/**
|
||||
* Build the private links app URL for a given environment's APP_BASE_URL.
|
||||
*/
|
||||
export function privateAppUrl(appBaseUrl: string): string {
|
||||
return `${appBaseUrl}/app/private`;
|
||||
}
|
||||
|
||||
export function privateLoginRequiredResponse(appBaseUrl: string): Response {
|
||||
const loginUrl = privateLoginUrl(appBaseUrl);
|
||||
return privateHtmlResponse(
|
||||
`<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Login required</title></head><body><h1>Not found</h1><p><a href="${PRIVATE_LOGIN_URL}">Login to use your private links</a>.</p></body></html>`,
|
||||
`<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Login required</title></head><body><h1>Not found</h1><p><a href="${loginUrl}">Login to use your private links</a>.</p></body></html>`,
|
||||
{ status: 404 },
|
||||
);
|
||||
}
|
||||
|
||||
export function privateAliasNotFoundResponse(): Response {
|
||||
export function privateAliasNotFoundResponse(appBaseUrl: string): Response {
|
||||
const appUrl = privateAppUrl(appBaseUrl);
|
||||
return privateHtmlResponse(
|
||||
`<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Not found</title></head><body><h1>Not found</h1><p>This private link does not exist.</p><p><a href="${PRIVATE_APP_URL}">Create this private link</a>.</p></body></html>`,
|
||||
`<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Not found</title></head><body><h1>Not found</h1><p>This private link does not exist.</p><p><a href="${appUrl}">Create this private link</a>.</p></body></html>`,
|
||||
{ status: 404 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ import { getCurrentUser } from '../auth';
|
||||
import { validateAlias } from '../lib/aliases';
|
||||
import { renderCustomLinkHtml } from '../lib/custom-link';
|
||||
import {
|
||||
PRIVATE_APP_URL,
|
||||
privateAliasNotFoundResponse,
|
||||
privateAppUrl,
|
||||
privateHtmlResponse,
|
||||
privateLoginRequiredResponse,
|
||||
privateRedirectResponse,
|
||||
@@ -30,8 +30,8 @@ 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=?`;
|
||||
|
||||
export function isHeygoPrivateHost(url: URL): boolean {
|
||||
return url.hostname === 'my.heygo.cc';
|
||||
export function isHeygoPrivateHost(hostname: string, privateHost: string): boolean {
|
||||
return hostname === privateHost;
|
||||
}
|
||||
|
||||
export async function handlePrivateShortlink(
|
||||
@@ -45,9 +45,9 @@ export async function handlePrivateShortlink(
|
||||
if (url.pathname === '/') {
|
||||
const user = await getCurrentUser(request, env);
|
||||
if (!user) {
|
||||
return privateLoginRequiredResponse();
|
||||
return privateLoginRequiredResponse(env.APP_BASE_URL);
|
||||
}
|
||||
return privateRedirectResponse(PRIVATE_APP_URL, 302);
|
||||
return privateRedirectResponse(privateAppUrl(env.APP_BASE_URL), 302);
|
||||
}
|
||||
|
||||
const pathParts = parseAliasPath(url.pathname);
|
||||
@@ -62,21 +62,21 @@ export async function handlePrivateShortlink(
|
||||
|
||||
const user = await getCurrentUser(request, env);
|
||||
if (!user) {
|
||||
return privateLoginRequiredResponse();
|
||||
return privateLoginRequiredResponse(env.APP_BASE_URL);
|
||||
}
|
||||
|
||||
const link = await env.DB.prepare(PRIVATE_LINK_QUERY)
|
||||
.bind(user.id, aliasValidation.value)
|
||||
.first<PrivateLinkRow>();
|
||||
if (!link) {
|
||||
return privateAliasNotFoundResponse();
|
||||
return privateAliasNotFoundResponse(env.APP_BASE_URL);
|
||||
}
|
||||
|
||||
ctx.waitUntil(recordClick(env.DB, link.id));
|
||||
|
||||
if (link.link_type === 'redirect') {
|
||||
if (!link.target_url) {
|
||||
return privateAliasNotFoundResponse();
|
||||
return privateAliasNotFoundResponse(env.APP_BASE_URL);
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -100,15 +100,15 @@ export async function handlePrivateShortlink(
|
||||
return privateHtmlResponse(renderCustomLinkHtml(link), { status: 200 });
|
||||
}
|
||||
|
||||
return privateAliasNotFoundResponse();
|
||||
return privateAliasNotFoundResponse(env.APP_BASE_URL);
|
||||
}
|
||||
|
||||
async function privateNotFoundOrLogin(request: Request, env: Env): Promise<Response> {
|
||||
const user = await getCurrentUser(request, env);
|
||||
if (!user) {
|
||||
return privateLoginRequiredResponse();
|
||||
return privateLoginRequiredResponse(env.APP_BASE_URL);
|
||||
}
|
||||
return privateAliasNotFoundResponse();
|
||||
return privateAliasNotFoundResponse(env.APP_BASE_URL);
|
||||
}
|
||||
|
||||
async function recordClick(db: D1Database, linkId: string): Promise<void> {
|
||||
|
||||
@@ -26,8 +26,8 @@ export function isReservedPublicPath(pathname: string): boolean {
|
||||
|| pathname === '/admin' || pathname.startsWith('/admin/');
|
||||
}
|
||||
|
||||
export function isHeygoPublicHost(url: URL): boolean {
|
||||
return url.hostname === 'heygo.cc';
|
||||
export function isHeygoPublicHost(hostname: string, publicHost: string): boolean {
|
||||
return hostname === publicHost;
|
||||
}
|
||||
|
||||
export async function handlePublicShortlink(
|
||||
|
||||
Reference in New Issue
Block a user