Files
zeekayandMarc Seitz 5b8a26b789 fix: harden root domain redirects - validation, plan gating, cleanup
- Change redirect status from 301 to 302 (non-permanent, user-configurable)
- Remove backfill UPDATE entries from migration SQL
- Remove unnecessary sync-redirects cron endpoint
- Add URL validation: SSRF protection, edge config keyword blocklist,
  protocol check, URL sanitization via shared validateRedirectUrl helper
- Add plan gating: require Business plan or higher for redirect URLs
  (enforced on both API endpoints and UI)
- Clean up Redis redirect URLs on team deletion
- Clear redirect URLs (Postgres + Redis) on subscription cancellation
  and on plan downgrade below Business
- Split redis.ts to avoid Prisma import in edge middleware context
- Gate DomainCard redirect UI based on plan (show upgrade prompt)

Co-authored-by: Marc Seitz <[email protected]>
2026-02-22 08:24:29 +00:00

42 lines
997 B
TypeScript

import { redis } from "@/lib/redis";
const DOMAIN_REDIRECT_PREFIX = "domain:redirect";
const PLANS_WITH_REDIRECTS = new Set([
"business",
"datarooms",
"datarooms-plus",
"datarooms-premium",
]);
export function planSupportsRedirects(plan: string): boolean {
const normalized = plan.replace("+old", "");
return PLANS_WITH_REDIRECTS.has(normalized);
}
export function getRedisKey(domain: string): string {
return `${DOMAIN_REDIRECT_PREFIX}:${domain.toLowerCase()}`;
}
export async function getDomainRedirectUrl(
domain: string,
): Promise<string | null> {
return redis.get<string>(getRedisKey(domain));
}
export async function setDomainRedirectUrl(
domain: string,
redirectUrl: string | null,
): Promise<void> {
const key = getRedisKey(domain);
if (redirectUrl) {
await redis.set(key, redirectUrl);
} else {
await redis.del(key);
}
}
export async function deleteDomainRedirectUrl(domain: string): Promise<void> {
await redis.del(getRedisKey(domain));
}