This commit is contained in:
Fred KISSIE
2026-09-10 20:08:47 +02:00
parent 30ca3e7e97
commit 9e85b39fd5
14 changed files with 1147 additions and 61 deletions
+13 -2
View File
@@ -242,8 +242,19 @@ export const redirects = pgTable("redirects", {
}),
niceId: text("niceId").notNull(),
name: varchar("name").notNull(),
sourcePath: varchar("sourcePath").notNull(),
destinationUrl: varchar("destinationUrl"),
subdomain: varchar("subdomain"),
destinationDomain: varchar("destinationDomain").notNull(),
pathMatchType: varchar("pathMatchType")
.$type<"exact" | "prefix" | "regex">()
.notNull()
.default("regex"), // exact, prefix, regex
matchPath: varchar("matchPath").notNull().default("*"),
rewritePath: varchar("rewritePath"), // if set, rewrites the path to this value,
// else, the original path will be kept
rewritePathType: varchar("rewritePathType").$type<
"exact" | "prefix" | "regex" | "stripPrefix"
>(), // exact, prefix, regex, stripPrefix
permanent: boolean("permanent").notNull().default(false),
enabled: boolean("enabled").notNull().default(true)
});
+12 -2
View File
@@ -258,8 +258,18 @@ export const redirects = sqliteTable("redirects", {
}),
niceId: text("niceId").notNull(),
name: text("name").notNull(),
sourcePath: text("sourcePath").notNull(),
destinationUrl: text("destinationUrl"),
destinationDomain: text("destinationDomain").notNull(),
pathMatchType: text("pathMatchType")
.$type<"exact" | "prefix" | "regex">()
.notNull()
.default("regex"), // exact, prefix, regex
matchPath: text("matchPath").notNull().default("*"),
rewritePath: text("rewritePath"), // if set, rewrites the path to this value,
// else, the original path will be kept
rewritePathType: text("rewritePathType").$type<
"exact" | "prefix" | "regex" | "stripPrefix"
>(), // exact, prefix, regex, stripPrefix
permanent: integer("permanent", { mode: "boolean" })
.notNull()
.default(false),
+34 -8
View File
@@ -9,7 +9,12 @@ import logger from "@server/logger";
import { fromError } from "zod-validation-error";
import { OpenAPITags, registry } from "@server/openApi";
import { and, eq } from "drizzle-orm";
import { redirectSourcePathSchema } from "@server/routers/redirect/validation";
import {
redirectMatchPathSchema,
redirectPathMatchTypeSchema,
redirectRewritePathSchema,
redirectRewritePathTypeSchema
} from "@server/routers/redirect/validation";
import { getUniqueRedirectName } from "@server/db/names";
export type CreateRedirectResponse = {
@@ -24,11 +29,26 @@ const bodySchema = z.strictObject({
name: z.string().nonempty(),
resourceId: z.number().int().positive().optional().nullable(),
domainId: z.string().nonempty().optional().nullable(),
sourcePath: redirectSourcePathSchema,
destinationUrl: z.url().optional().nullable(),
destinationDomain: z.string().nonempty(),
pathMatchType: redirectPathMatchTypeSchema.optional(),
matchPath: redirectMatchPathSchema,
rewritePath: redirectRewritePathSchema.optional().nullable(),
rewritePathType: redirectRewritePathTypeSchema.optional().nullable(),
permanent: z.boolean().optional(),
enabled: z.boolean().optional()
});
}).refine(
(data) =>
// stripPrefix removes the matched prefix and needs no replacement
// value; every other rewrite type is meaningless without one.
!data.rewritePathType ||
data.rewritePathType === "stripPrefix" ||
Boolean(data.rewritePath),
{
message:
"rewritePath is required unless rewritePathType is stripPrefix",
path: ["rewritePath"]
}
);
registry.registerPath({
method: "put",
@@ -83,8 +103,11 @@ export async function createRedirect(
name,
resourceId,
domainId,
sourcePath,
destinationUrl,
destinationDomain,
pathMatchType,
matchPath,
rewritePath,
rewritePathType,
permanent,
enabled
} = parsedBody.data;
@@ -147,8 +170,11 @@ export async function createRedirect(
niceId,
resourceId: resourceId ?? null,
domainId: domainId ?? null,
sourcePath,
destinationUrl: destinationUrl ?? null,
destinationDomain,
pathMatchType: pathMatchType ?? "regex",
matchPath,
rewritePath: rewritePath ?? null,
rewritePathType: rewritePathType ?? null,
permanent: permanent ?? false,
enabled: enabled ?? true
})
+50 -5
View File
@@ -1,7 +1,6 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { redirects, db } from "@server/db";
import type { Redirect } from "@server/db";
import { domains, redirects, resources, db } from "@server/db";
import response from "@server/lib/response";
import stoi from "@server/lib/stoi";
import HttpCode from "@server/types/HttpCode";
@@ -12,7 +11,49 @@ import { OpenAPITags, registry } from "@server/openApi";
import { and, eq } from "drizzle-orm";
export type GetRedirectResponse = {
redirect: Redirect;
redirect: {
redirectId: number;
orgId: string;
niceId: string;
name: string;
destinationDomain: string;
pathMatchType: "exact" | "prefix" | "regex";
matchPath: string;
rewritePath: string | null;
rewritePathType: "exact" | "prefix" | "regex" | "stripPrefix" | null;
permanent: boolean;
enabled: boolean;
resourceId: number | null;
resourceName: string | null;
resourceNiceId: string | null;
resourceFullDomain: string | null;
resourceSsl: boolean | null;
resourceWildcard: boolean | null;
domainId: string | null;
baseDomain: string | null;
};
};
const redirectColumns = {
redirectId: redirects.redirectId,
orgId: redirects.orgId,
niceId: redirects.niceId,
name: redirects.name,
destinationDomain: redirects.destinationDomain,
pathMatchType: redirects.pathMatchType,
matchPath: redirects.matchPath,
rewritePath: redirects.rewritePath,
rewritePathType: redirects.rewritePathType,
permanent: redirects.permanent,
enabled: redirects.enabled,
resourceId: redirects.resourceId,
resourceName: resources.name,
resourceNiceId: resources.niceId,
resourceFullDomain: resources.fullDomain,
resourceSsl: resources.ssl,
resourceWildcard: resources.wildcard,
domainId: redirects.domainId,
baseDomain: domains.baseDomain
};
const paramsSchema = z.strictObject({
@@ -29,8 +70,10 @@ const paramsSchema = z.strictObject({
async function query(orgId: string, redirectId?: number, niceId?: string) {
if (redirectId) {
const [res] = await db
.select()
.select(redirectColumns)
.from(redirects)
.leftJoin(resources, eq(resources.resourceId, redirects.resourceId))
.leftJoin(domains, eq(domains.domainId, redirects.domainId))
.where(
and(
eq(redirects.redirectId, redirectId),
@@ -41,8 +84,10 @@ async function query(orgId: string, redirectId?: number, niceId?: string) {
return res;
} else if (niceId) {
const [res] = await db
.select()
.select(redirectColumns)
.from(redirects)
.leftJoin(resources, eq(resources.resourceId, redirects.resourceId))
.leftJoin(domains, eq(domains.domainId, redirects.domainId))
.where(
and(eq(redirects.niceId, niceId), eq(redirects.orgId, orgId))
)
+12 -6
View File
@@ -16,8 +16,11 @@ export type ListRedirectsResponse = PaginatedResponse<{
orgId: string;
niceId: string;
name: string;
sourcePath: string;
destinationUrl: string | null;
destinationDomain: string;
pathMatchType: "exact" | "prefix" | "regex";
matchPath: string;
rewritePath: string | null;
rewritePathType: "exact" | "prefix" | "regex" | "stripPrefix" | null;
permanent: boolean;
enabled: boolean;
resourceId: number | null;
@@ -122,8 +125,8 @@ export async function listRedirects(
conditions.push(
or(
like(sql`LOWER(${redirects.name})`, term),
like(sql`LOWER(${redirects.sourcePath})`, term),
like(sql`LOWER(${redirects.destinationUrl})`, term)
like(sql`LOWER(${redirects.matchPath})`, term),
like(sql`LOWER(${redirects.destinationDomain})`, term)
)!
);
}
@@ -134,8 +137,11 @@ export async function listRedirects(
orgId: redirects.orgId,
niceId: redirects.niceId,
name: redirects.name,
sourcePath: redirects.sourcePath,
destinationUrl: redirects.destinationUrl,
destinationDomain: redirects.destinationDomain,
pathMatchType: redirects.pathMatchType,
matchPath: redirects.matchPath,
rewritePath: redirects.rewritePath,
rewritePathType: redirects.rewritePathType,
permanent: redirects.permanent,
enabled: redirects.enabled,
resourceId: redirects.resourceId,
+22 -7
View File
@@ -11,7 +11,10 @@ import { OpenAPITags, registry } from "@server/openApi";
import { and, eq, ne } from "drizzle-orm";
import {
redirectNiceIdSchema,
redirectSourcePathSchema
redirectMatchPathSchema,
redirectPathMatchTypeSchema,
redirectRewritePathSchema,
redirectRewritePathTypeSchema
} from "@server/routers/redirect/validation";
export type UpdateRedirectResponse = {
@@ -28,8 +31,11 @@ const bodySchema = z.strictObject({
niceId: redirectNiceIdSchema.optional(),
resourceId: z.number().int().positive().optional().nullable(),
domainId: z.string().nonempty().optional().nullable(),
sourcePath: redirectSourcePathSchema.optional(),
destinationUrl: z.url().optional().nullable(),
destinationDomain: z.string().nonempty().optional(),
pathMatchType: redirectPathMatchTypeSchema.optional(),
matchPath: redirectMatchPathSchema.optional(),
rewritePath: redirectRewritePathSchema.optional().nullable(),
rewritePathType: redirectRewritePathTypeSchema.optional().nullable(),
permanent: z.boolean().optional(),
enabled: z.boolean().optional()
});
@@ -190,11 +196,20 @@ export async function updateRedirect(
if (body.domainId !== undefined) {
updateData.domainId = body.domainId;
}
if (body.sourcePath !== undefined) {
updateData.sourcePath = body.sourcePath;
if (body.destinationDomain !== undefined) {
updateData.destinationDomain = body.destinationDomain;
}
if (body.destinationUrl !== undefined) {
updateData.destinationUrl = body.destinationUrl;
if (body.pathMatchType !== undefined) {
updateData.pathMatchType = body.pathMatchType;
}
if (body.matchPath !== undefined) {
updateData.matchPath = body.matchPath;
}
if (body.rewritePath !== undefined) {
updateData.rewritePath = body.rewritePath;
}
if (body.rewritePathType !== undefined) {
updateData.rewritePathType = body.rewritePathType;
}
if (body.permanent !== undefined) {
updateData.permanent = body.permanent;
+12 -5
View File
@@ -9,8 +9,15 @@ export const redirectNiceIdSchema = z
"niceId can only contain letters, numbers, and dashes"
);
export const redirectSourcePathSchema = z
.string()
.nonempty()
.regex(/^\//, "sourcePath must start with a /")
.default("/*");
export const redirectPathMatchTypeSchema = z.enum(["exact", "prefix", "regex"]);
export const redirectRewritePathTypeSchema = z.enum([
"exact",
"prefix",
"regex",
"stripPrefix"
]);
export const redirectMatchPathSchema = z.string().nonempty().default("*");
export const redirectRewritePathSchema = z.string().nonempty();