♻️ rename destinationDomain to destinationHost to include the Scheme

This commit is contained in:
Fred KISSIE
2026-09-18 23:55:51 +02:00
parent 57b80bf74e
commit 97c91fbdc9
13 changed files with 65 additions and 51 deletions
+4 -4
View File
@@ -4421,12 +4421,12 @@
"redirectDeleteConfirm": "Confirm Delete Redirect", "redirectDeleteConfirm": "Confirm Delete Redirect",
"redirectQuestionRemove": "Are you sure you want to remove this redirect?", "redirectQuestionRemove": "Are you sure you want to remove this redirect?",
"redirectMessageRemove": "Once removed, requests matching this redirect will no longer be forwarded.", "redirectMessageRemove": "Once removed, requests matching this redirect will no longer be forwarded.",
"redirectDestinationDomain": "Destination Domain", "redirectDestinationHost": "Destination",
"redirectDestinationDomainDescription": "The domain requests are sent to, such as example.com", "redirectDestinationHostDescription": "Where requests are sent, including the scheme, such as https://example.com",
"redirectDestinationDomainRequired": "Enter a destination domain", "redirectDestinationHostRequired": "Enter a destination",
"redirectSameDomainAsSource": "Same domain as source", "redirectSameDomainAsSource": "Same domain as source",
"redirectSameDomainAsSourceDescription": "Keep the destination on the source domain and only change the path", "redirectSameDomainAsSourceDescription": "Keep the destination on the source domain and only change the path",
"redirectDestinationDomainInvalid": "Enter a valid domain, such as example.com", "redirectDestinationHostInvalid": "Enter a valid destination with a scheme, such as https://example.com",
"redirectMatchPathDescription": "Which incoming paths this redirect applies to", "redirectMatchPathDescription": "Which incoming paths this redirect applies to",
"redirectRewritePathDescription": "Optionally change the path before redirecting. Leave unset to keep the original path.", "redirectRewritePathDescription": "Optionally change the path before redirecting. Leave unset to keep the original path.",
"redirectRewritePathRequired": "Enter a rewrite path, or choose Strip Prefix", "redirectRewritePathRequired": "Enter a rewrite path, or choose Strip Prefix",
+1 -1
View File
@@ -243,7 +243,7 @@ export const redirects = pgTable("redirects", {
niceId: text("niceId").notNull(), niceId: text("niceId").notNull(),
name: varchar("name").notNull(), name: varchar("name").notNull(),
subdomain: varchar("subdomain"), subdomain: varchar("subdomain"),
destinationDomain: varchar("destinationDomain").notNull(), destinationHost: varchar("destinationHost").notNull(), // scheme://host[:port]
pathMatchType: varchar("pathMatchType") pathMatchType: varchar("pathMatchType")
.$type<"exact" | "prefix" | "regex">() .$type<"exact" | "prefix" | "regex">()
.notNull() .notNull()
+2 -2
View File
@@ -60,7 +60,7 @@ export type RedirectByHost = {
orgId: string; orgId: string;
matchPath: string | null; matchPath: string | null;
pathMatchType: string; pathMatchType: string;
destinationDomain: string; destinationHost: string;
rewritePath: string | null; rewritePath: string | null;
rewritePathType: string | null; rewritePathType: string | null;
permanent: boolean; permanent: boolean;
@@ -102,7 +102,7 @@ export async function getRedirectsByHost(
orgId: redirects.orgId, orgId: redirects.orgId,
matchPath: redirects.matchPath, matchPath: redirects.matchPath,
pathMatchType: redirects.pathMatchType, pathMatchType: redirects.pathMatchType,
destinationDomain: redirects.destinationDomain, destinationHost: redirects.destinationHost,
rewritePath: redirects.rewritePath, rewritePath: redirects.rewritePath,
rewritePathType: redirects.rewritePathType, rewritePathType: redirects.rewritePathType,
permanent: redirects.permanent, permanent: redirects.permanent,
+1 -1
View File
@@ -259,7 +259,7 @@ export const redirects = sqliteTable("redirects", {
niceId: text("niceId").notNull(), niceId: text("niceId").notNull(),
name: text("name").notNull(), name: text("name").notNull(),
subdomain: text("subdomain"), subdomain: text("subdomain"),
destinationDomain: text("destinationDomain").notNull(), destinationHost: text("destinationHost").notNull(), // scheme://host[:port]
pathMatchType: text("pathMatchType") pathMatchType: text("pathMatchType")
.$type<"exact" | "prefix" | "regex">() .$type<"exact" | "prefix" | "regex">()
.notNull() .notNull()
+3 -3
View File
@@ -1060,8 +1060,8 @@ async function findRedirect(
} }
/** /**
* Destination for a configured redirect: the request's scheme and query are * Destination for a configured redirect: the scheme and host come from the
* kept, the host is swapped for the destination domain and the path is run * redirect's destination, the request's query is kept and the path is run
* through the redirect's rewrite rules (if any). * through the redirect's rewrite rules (if any).
*/ */
function buildRedirectUrl( function buildRedirectUrl(
@@ -1083,7 +1083,7 @@ function buildRedirectUrl(
// originalRequestURL is validated as a URL, so this is only defensive // originalRequestURL is validated as a URL, so this is only defensive
} }
return `${request.scheme}://${redirect.destinationDomain}${newPath}${search}`; return `${redirect.destinationHost}${newPath}${search}`;
} }
// Like a notAllowed login bounce, but the destination is the configured // Like a notAllowed login bounce, but the destination is the configured
+4 -4
View File
@@ -10,7 +10,7 @@ import { fromError } from "zod-validation-error";
import { OpenAPITags, registry } from "@server/openApi"; import { OpenAPITags, registry } from "@server/openApi";
import { and, eq } from "drizzle-orm"; import { and, eq } from "drizzle-orm";
import { import {
redirectDestinationDomainSchema, redirectDestinationHostSchema,
redirectMatchPathSchema, redirectMatchPathSchema,
redirectPathMatchTypeSchema, redirectPathMatchTypeSchema,
redirectRewritePathSchema, redirectRewritePathSchema,
@@ -36,7 +36,7 @@ const bodySchema = z
resourceId: z.number().int().positive().optional().nullable(), resourceId: z.number().int().positive().optional().nullable(),
domainId: z.string().nonempty().optional().nullable(), domainId: z.string().nonempty().optional().nullable(),
subdomain: z.string().nonempty().optional().nullable(), subdomain: z.string().nonempty().optional().nullable(),
destinationDomain: redirectDestinationDomainSchema, destinationHost: redirectDestinationHostSchema,
pathMatchType: redirectPathMatchTypeSchema.optional(), pathMatchType: redirectPathMatchTypeSchema.optional(),
matchPath: redirectMatchPathSchema.optional().nullable(), matchPath: redirectMatchPathSchema.optional().nullable(),
rewritePath: redirectRewritePathSchema.optional().nullable(), rewritePath: redirectRewritePathSchema.optional().nullable(),
@@ -126,7 +126,7 @@ export async function createRedirect(
resourceId, resourceId,
domainId, domainId,
subdomain, subdomain,
destinationDomain, destinationHost,
pathMatchType, pathMatchType,
matchPath, matchPath,
rewritePath, rewritePath,
@@ -200,7 +200,7 @@ export async function createRedirect(
resourceId: resourceId ?? null, resourceId: resourceId ?? null,
domainId: domainId ?? null, domainId: domainId ?? null,
subdomain: subdomain ?? null, subdomain: subdomain ?? null,
destinationDomain, destinationHost,
pathMatchType: pathMatchType ?? "regex", pathMatchType: pathMatchType ?? "regex",
matchPath: matchPath ?? null, matchPath: matchPath ?? null,
rewritePath: rewritePath ?? null, rewritePath: rewritePath ?? null,
+2 -2
View File
@@ -17,7 +17,7 @@ export type GetRedirectResponse = {
niceId: string; niceId: string;
name: string; name: string;
subdomain: string | null; subdomain: string | null;
destinationDomain: string; destinationHost: string;
pathMatchType: "exact" | "prefix" | "regex"; pathMatchType: "exact" | "prefix" | "regex";
matchPath: string | null; matchPath: string | null;
rewritePath: string | null; rewritePath: string | null;
@@ -43,7 +43,7 @@ const redirectColumns = {
niceId: redirects.niceId, niceId: redirects.niceId,
name: redirects.name, name: redirects.name,
subdomain: redirects.subdomain, subdomain: redirects.subdomain,
destinationDomain: redirects.destinationDomain, destinationHost: redirects.destinationHost,
pathMatchType: redirects.pathMatchType, pathMatchType: redirects.pathMatchType,
matchPath: redirects.matchPath, matchPath: redirects.matchPath,
rewritePath: redirects.rewritePath, rewritePath: redirects.rewritePath,
+3 -3
View File
@@ -17,7 +17,7 @@ export type ListRedirectsResponse = PaginatedResponse<{
niceId: string; niceId: string;
name: string; name: string;
subdomain: string | null; subdomain: string | null;
destinationDomain: string; destinationHost: string;
pathMatchType: "exact" | "prefix" | "regex"; pathMatchType: "exact" | "prefix" | "regex";
matchPath: string | null; matchPath: string | null;
rewritePath: string | null; rewritePath: string | null;
@@ -130,7 +130,7 @@ export async function listRedirects(
or( or(
like(sql`LOWER(${redirects.name})`, term), like(sql`LOWER(${redirects.name})`, term),
like(sql`LOWER(${redirects.matchPath})`, term), like(sql`LOWER(${redirects.matchPath})`, term),
like(sql`LOWER(${redirects.destinationDomain})`, term) like(sql`LOWER(${redirects.destinationHost})`, term)
)! )!
); );
} }
@@ -142,7 +142,7 @@ export async function listRedirects(
niceId: redirects.niceId, niceId: redirects.niceId,
name: redirects.name, name: redirects.name,
subdomain: redirects.subdomain, subdomain: redirects.subdomain,
destinationDomain: redirects.destinationDomain, destinationHost: redirects.destinationHost,
pathMatchType: redirects.pathMatchType, pathMatchType: redirects.pathMatchType,
matchPath: redirects.matchPath, matchPath: redirects.matchPath,
rewritePath: redirects.rewritePath, rewritePath: redirects.rewritePath,
+4 -4
View File
@@ -11,7 +11,7 @@ import { OpenAPITags, registry } from "@server/openApi";
import { and, eq, ne } from "drizzle-orm"; import { and, eq, ne } from "drizzle-orm";
import { import {
redirectNiceIdSchema, redirectNiceIdSchema,
redirectDestinationDomainSchema, redirectDestinationHostSchema,
redirectMatchPathSchema, redirectMatchPathSchema,
redirectPathMatchTypeSchema, redirectPathMatchTypeSchema,
redirectRewritePathSchema, redirectRewritePathSchema,
@@ -37,7 +37,7 @@ const bodySchema = z.strictObject({
resourceId: z.number().int().positive().optional().nullable(), resourceId: z.number().int().positive().optional().nullable(),
domainId: z.string().nonempty().optional().nullable(), domainId: z.string().nonempty().optional().nullable(),
subdomain: z.string().nonempty().optional().nullable(), subdomain: z.string().nonempty().optional().nullable(),
destinationDomain: redirectDestinationDomainSchema.optional(), destinationHost: redirectDestinationHostSchema.optional(),
pathMatchType: redirectPathMatchTypeSchema.optional(), pathMatchType: redirectPathMatchTypeSchema.optional(),
matchPath: redirectMatchPathSchema.optional().nullable(), matchPath: redirectMatchPathSchema.optional().nullable(),
rewritePath: redirectRewritePathSchema.optional().nullable(), rewritePath: redirectRewritePathSchema.optional().nullable(),
@@ -253,8 +253,8 @@ export async function updateRedirect(
if (body.subdomain !== undefined) { if (body.subdomain !== undefined) {
updateData.subdomain = body.subdomain; updateData.subdomain = body.subdomain;
} }
if (body.destinationDomain !== undefined) { if (body.destinationHost !== undefined) {
updateData.destinationDomain = body.destinationDomain; updateData.destinationHost = body.destinationHost;
} }
if (body.pathMatchType !== undefined) { if (body.pathMatchType !== undefined) {
updateData.pathMatchType = body.pathMatchType; updateData.pathMatchType = body.pathMatchType;
+12 -3
View File
@@ -49,11 +49,20 @@ export const redirectRewritePathSchema = z.string().nonempty();
// Same range as target priorities; 100 means "let the system order it". // Same range as target priorities; 100 means "let the system order it".
export const redirectPrioritySchema = z.int().min(1).max(1000); export const redirectPrioritySchema = z.int().min(1).max(1000);
export const redirectDestinationDomainSchema = z /**
* A destination is `scheme://host[:port]` with no path, query or fragment;
* the request path (after any rewrite) is appended to it by badger.
*/
export function isValidDestinationHost(value: string): boolean {
const match = /^https?:\/\/([^/:?#]+)(:\d{1,5})?$/.exec(value);
return match !== null && isValidDomain(match[1]);
}
export const redirectDestinationHostSchema = z
.string() .string()
.nonempty() .nonempty()
.refine(isValidDomain, { .refine(isValidDestinationHost, {
message: "Invalid domain" message: "Invalid destination, expected scheme://host such as https://example.com"
}); });
/** /**
+1 -1
View File
@@ -49,7 +49,7 @@ export default async function RedirectIndexPage(props: RedirectIndexPageProps) {
niceId: redirect.niceId, niceId: redirect.niceId,
name: redirect.name, name: redirect.name,
subdomain: redirect.subdomain, subdomain: redirect.subdomain,
destinationDomain: redirect.destinationDomain, destinationHost: redirect.destinationHost,
pathMatchType: redirect.pathMatchType, pathMatchType: redirect.pathMatchType,
matchPath: redirect.matchPath, matchPath: redirect.matchPath,
rewritePath: redirect.rewritePath, rewritePath: redirect.rewritePath,
+25 -20
View File
@@ -40,8 +40,10 @@ import {
import { useEnvContext } from "@app/hooks/useEnvContext"; import { useEnvContext } from "@app/hooks/useEnvContext";
import { toast } from "@app/hooks/useToast"; import { toast } from "@app/hooks/useToast";
import { createApiClient, formatAxiosError } from "@app/lib/api"; import { createApiClient, formatAxiosError } from "@app/lib/api";
import { isValidDomain } from "@server/lib/validators"; import {
import { isValidRegex } from "@server/routers/redirect/validation"; isValidDestinationHost,
isValidRegex
} from "@server/routers/redirect/validation";
import { build } from "@server/build"; import { build } from "@server/build";
import { cn } from "@app/lib/cn"; import { cn } from "@app/lib/cn";
import { CaretSortIcon } from "@radix-ui/react-icons"; import { CaretSortIcon } from "@radix-ui/react-icons";
@@ -119,14 +121,14 @@ export default function RedirectForm({
domainId: z.string().nullable(), domainId: z.string().nullable(),
subdomain: z.string().nullable(), subdomain: z.string().nullable(),
resourceId: z.number().int().positive().nullable(), resourceId: z.number().int().positive().nullable(),
destinationDomain: z destinationHost: z
.string() .string()
.trim() .trim()
.min(1, { .min(1, {
message: t("redirectDestinationDomainRequired") message: t("redirectDestinationHostRequired")
}) })
.refine(isValidDomain, { .refine(isValidDestinationHost, {
message: t("redirectDestinationDomainInvalid") message: t("redirectDestinationHostInvalid")
}), }),
pathMatchType: z.enum(["exact", "prefix", "regex"]), pathMatchType: z.enum(["exact", "prefix", "regex"]),
matchPath: z.string().trim().nullable(), matchPath: z.string().trim().nullable(),
@@ -196,7 +198,7 @@ export default function RedirectForm({
domainId: redirect?.domainId ?? null, domainId: redirect?.domainId ?? null,
subdomain: redirect?.subdomain ?? null, subdomain: redirect?.subdomain ?? null,
resourceId: redirect?.resourceId ?? null, resourceId: redirect?.resourceId ?? null,
destinationDomain: redirect?.destinationDomain ?? "", destinationHost: redirect?.destinationHost ?? "",
pathMatchType: redirect?.pathMatchType ?? DEFAULT_PATH_MATCH_TYPE, pathMatchType: redirect?.pathMatchType ?? DEFAULT_PATH_MATCH_TYPE,
matchPath: redirect?.matchPath ?? null, matchPath: redirect?.matchPath ?? null,
rewritePath: redirect?.rewritePath ?? null, rewritePath: redirect?.rewritePath ?? null,
@@ -209,29 +211,32 @@ export default function RedirectForm({
}); });
const attachTo = form.watch("attachTo"); const attachTo = form.watch("attachTo");
const ssl = form.watch("ssl");
const sourceFullDomain = const sourceFullDomain =
attachTo === "domain" attachTo === "domain"
? domainFullDomain ? domainFullDomain
: (selectedResource?.fullDomain ?? null); : (selectedResource?.fullDomain ?? null);
// Resource-attached redirects inherit the resource's ssl setting
const sourceSsl =
attachTo === "domain" ? ssl : (selectedResource?.ssl ?? true);
const sourceHost = sourceFullDomain
? `${sourceSsl ? "https" : "http"}://${sourceFullDomain}`
: null;
// Mirror is UI-only state; on edit, infer it from whether the saved // Mirror is UI-only state; on edit, infer it from whether the saved
// destination already equals the source host. // destination already equals the source host.
const [sameDomainAsSource, setSameDomainAsSource] = useState( const [sameDomainAsSource, setSameDomainAsSource] = useState(
Boolean( Boolean(redirect && sourceHost && redirect.destinationHost === sourceHost)
redirect &&
sourceFullDomain &&
redirect.destinationDomain === sourceFullDomain
)
); );
useEffect(() => { useEffect(() => {
if (sameDomainAsSource && sourceFullDomain) { if (sameDomainAsSource && sourceHost) {
form.setValue("destinationDomain", sourceFullDomain, { form.setValue("destinationHost", sourceHost, {
shouldValidate: true shouldValidate: true
}); });
} }
}, [sameDomainAsSource, sourceFullDomain, form]); }, [sameDomainAsSource, sourceHost, form]);
const pathMatchType = form.watch("pathMatchType"); const pathMatchType = form.watch("pathMatchType");
const rewritePath = form.watch("rewritePath"); const rewritePath = form.watch("rewritePath");
const rewritePathType = form.watch("rewritePathType"); const rewritePathType = form.watch("rewritePathType");
@@ -253,7 +258,7 @@ export default function RedirectForm({
values.attachTo === "domain" ? values.subdomain || null : null, values.attachTo === "domain" ? values.subdomain || null : null,
resourceId: resourceId:
values.attachTo === "resource" ? values.resourceId : null, values.attachTo === "resource" ? values.resourceId : null,
destinationDomain: values.destinationDomain.trim(), destinationHost: values.destinationHost.trim(),
pathMatchType: values.pathMatchType, pathMatchType: values.pathMatchType,
matchPath: values.matchPath?.trim() || null, matchPath: values.matchPath?.trim() || null,
rewritePath: values.rewritePath?.trim() || null, rewritePath: values.rewritePath?.trim() || null,
@@ -674,18 +679,18 @@ export default function RedirectForm({
<SettingsFormCell span="full"> <SettingsFormCell span="full">
<FormField <FormField
control={form.control} control={form.control}
name="destinationDomain" name="destinationHost"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel> <FormLabel>
{t( {t(
"redirectDestinationDomain" "redirectDestinationHost"
)} )}
</FormLabel> </FormLabel>
<FormControl> <FormControl>
<Input <Input
autoComplete="off" autoComplete="off"
placeholder="example.com" placeholder="https://example.com"
readOnly={ readOnly={
sameDomainAsSource sameDomainAsSource
} }
@@ -694,7 +699,7 @@ export default function RedirectForm({
</FormControl> </FormControl>
<FormDescription> <FormDescription>
{t( {t(
"redirectDestinationDomainDescription" "redirectDestinationHostDescription"
)} )}
</FormDescription> </FormDescription>
<FormMessage /> <FormMessage />
+3 -3
View File
@@ -50,7 +50,7 @@ export type RedirectRow = {
niceId: string; niceId: string;
name: string; name: string;
subdomain: string | null; subdomain: string | null;
destinationDomain: string; destinationHost: string;
pathMatchType: "exact" | "prefix" | "regex"; pathMatchType: "exact" | "prefix" | "regex";
matchPath: string | null; matchPath: string | null;
rewritePath: string | null; rewritePath: string | null;
@@ -301,7 +301,7 @@ export default function RedirectsTable({
}, },
{ {
id: "destination", id: "destination",
accessorKey: "destinationDomain", accessorKey: "destinationHost",
friendlyName: t("redirectDestination"), friendlyName: t("redirectDestination"),
header: () => ( header: () => (
<span className="p-3">{t("redirectDestination")}</span> <span className="p-3">{t("redirectDestination")}</span>
@@ -310,7 +310,7 @@ export default function RedirectsTable({
const redirect = row.original; const redirect = row.original;
return ( return (
<code className="text-sm truncate"> <code className="text-sm truncate">
{redirect.destinationDomain} {redirect.destinationHost}
{redirect.rewritePath && ( {redirect.rewritePath && (
<span className="text-muted-foreground"> <span className="text-muted-foreground">
{redirect.rewritePathType === "prefix" {redirect.rewritePathType === "prefix"