Skip to content
Draft
28 changes: 24 additions & 4 deletions server/db/pg/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import {
integer,
bigint,
real,
text
text,
timestamp
} from "drizzle-orm/pg-core";
import { InferSelectModel } from "drizzle-orm";

Expand Down Expand Up @@ -64,6 +65,24 @@ export const sites = pgTable("sites", {
remoteSubnets: text("remoteSubnets") // comma-separated list of subnets that this site can access
});

export const resourceHostnames = pgTable("resourceHostnames", {
hostnameId: serial("hostnameId").primaryKey(),
resourceId: integer("resourceId")
.notNull()
.references(() => resources.resourceId, { onDelete: "cascade" }),
domainId: varchar("domainId")
.notNull()
.references(() => domains.domainId, { onDelete: "cascade" }),
subdomain: varchar("subdomain"),
fullDomain: varchar("fullDomain").notNull(),
baseDomain: varchar("baseDomain").notNull(),
primary: boolean("primary").notNull().default(false),
createdAt: timestamp("createdAt")
.notNull()
.defaultNow(),
});


export const resources = pgTable("resources", {
resourceId: serial("resourceId").primaryKey(),
orgId: varchar("orgId")
Expand All @@ -73,11 +92,11 @@ export const resources = pgTable("resources", {
.notNull(),
niceId: text("niceId").notNull(),
name: varchar("name").notNull(),
subdomain: varchar("subdomain"),
fullDomain: varchar("fullDomain"),
subdomain: varchar("subdomain"), // Keep for backward compatibility
fullDomain: varchar("fullDomain"), // Keep for backward compatibility
domainId: varchar("domainId").references(() => domains.domainId, {
onDelete: "set null"
}),
}), // Keep for backward compatibility
ssl: boolean("ssl").notNull().default(false),
blockAccess: boolean("blockAccess").notNull().default(false),
sso: boolean("sso").notNull().default(true),
Expand All @@ -97,6 +116,7 @@ export const resources = pgTable("resources", {
onDelete: "cascade"
}),
headers: text("headers"), // comma-separated list of headers to add to the request
hostMode: varchar("hostMode").default("multi") // "multi" or "redirect"
});

export const targets = pgTable("targets", {
Expand Down
26 changes: 23 additions & 3 deletions server/db/sqlite/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,22 @@ export const sites = sqliteTable("sites", {
remoteSubnets: text("remoteSubnets") // comma-separated list of subnets that this site can access
});

export const resourceHostnames = sqliteTable("resourceHostnames", {
hostnameId: integer("hostnameId").primaryKey({ autoIncrement: true }),
resourceId: integer("resourceId")
.notNull()
.references(() => resources.resourceId, { onDelete: "cascade" }),
domainId: text("domainId")
.notNull()
.references(() => domains.domainId, { onDelete: "cascade" }),
subdomain: text("subdomain"),
fullDomain: text("fullDomain").notNull(),
baseDomain: text("baseDomain").notNull(),
primary: integer("primary", { mode: "boolean" }).notNull().default(false),
createdAt: text("createdAt").notNull().default(new Date().toISOString()),
});

// Update resources table to add hostMode
export const resources = sqliteTable("resources", {
resourceId: integer("resourceId").primaryKey({ autoIncrement: true }),
orgId: text("orgId")
Expand All @@ -79,11 +95,11 @@ export const resources = sqliteTable("resources", {
.notNull(),
niceId: text("niceId").notNull(),
name: text("name").notNull(),
subdomain: text("subdomain"),
fullDomain: text("fullDomain"),
subdomain: text("subdomain"), // Keep for backward compatibility
fullDomain: text("fullDomain"), // Keep for backward compatibility
domainId: text("domainId").references(() => domains.domainId, {
onDelete: "set null"
}),
}), // Keep for backward compatibility
ssl: integer("ssl", { mode: "boolean" }).notNull().default(false),
blockAccess: integer("blockAccess", { mode: "boolean" })
.notNull()
Expand All @@ -109,8 +125,11 @@ export const resources = sqliteTable("resources", {
onDelete: "cascade"
}),
headers: text("headers"), // comma-separated list of headers to add to the request
// New field for host mode
hostMode: text("hostMode").default("multi"), // "multi" or "redirect"
});


export const targets = sqliteTable("targets", {
targetId: integer("targetId").primaryKey({ autoIncrement: true }),
resourceId: integer("resourceId")
Expand Down Expand Up @@ -731,3 +750,4 @@ export type SiteResource = InferSelectModel<typeof siteResources>;
export type OrgDomains = InferSelectModel<typeof orgDomains>;
export type SetupToken = InferSelectModel<typeof setupTokens>;
export type HostMeta = InferSelectModel<typeof hostMeta>;
export type ResourceHostname = InferSelectModel<typeof resourceHostnames>;
66 changes: 43 additions & 23 deletions server/lib/domainUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,25 @@ import { eq, and } from "drizzle-orm";
import { subdomainSchema } from "@server/lib/schemas";
import { fromError } from "zod-validation-error";

export type DomainValidationResult = {
success: true;
type ValidatedHostname = {
domainId: string;
subdomain?: string | null;
fullDomain: string;
subdomain: string | null;
} | {
success: false;
error: string;
baseDomain: string;
primary: boolean;
};

export type DomainValidationResult =
| {
success: true;
data: ValidatedHostname;
}
| {
success: false;
error: string;
data?: ValidatedHostname;
};

/**
* Validates a domain and constructs the full domain based on domain type and subdomain.
*
Expand All @@ -22,69 +32,74 @@ export type DomainValidationResult = {
* @returns DomainValidationResult with success status and either fullDomain/subdomain or error message
*/
export async function validateAndConstructDomain(
domainId: string,
orgId: string,
subdomain?: string | null
hostname: {
domainId: string;
subdomain?: string | null;
baseDomain?: string;
fullDomain?: string;
primary: boolean;
},
orgId: string
): Promise<DomainValidationResult> {
try {
// Query domain with organization access check
const [domainRes] = await db
.select()
.from(domains)
.where(eq(domains.domainId, domainId))
.where(eq(domains.domainId, hostname.domainId))
.leftJoin(
orgDomains,
and(eq(orgDomains.orgId, orgId), eq(orgDomains.domainId, domainId))
and(eq(orgDomains.orgId, orgId), eq(orgDomains.domainId, hostname.domainId))
);

// Check if domain exists
if (!domainRes || !domainRes.domains) {
return {
success: false,
error: `Domain with ID ${domainId} not found`
error: `Domain with ID ${hostname.domainId} not found`
};
}

// Check if organization has access to domain
if (domainRes.orgDomains && domainRes.orgDomains.orgId !== orgId) {
return {
success: false,
error: `Organization does not have access to domain with ID ${domainId}`
error: `Organization does not have access to domain with ID ${hostname.domainId}`
};
}

// Check if domain is verified
if (!domainRes.domains.verified) {
return {
success: false,
error: `Domain with ID ${domainId} is not verified`
error: `Domain with ID ${hostname.domainId} is not verified`
};
}

// Construct full domain based on domain type
let fullDomain = "";
let finalSubdomain = subdomain;
let finalSubdomain = hostname.subdomain;

if (domainRes.domains.type === "ns") {
if (subdomain) {
fullDomain = `${subdomain}.${domainRes.domains.baseDomain}`;
if (hostname.subdomain) {
fullDomain = `${hostname.subdomain}.${domainRes.domains.baseDomain}`;
} else {
fullDomain = domainRes.domains.baseDomain;
}
} else if (domainRes.domains.type === "cname") {
fullDomain = domainRes.domains.baseDomain;
finalSubdomain = null; // CNAME domains don't use subdomains
} else if (domainRes.domains.type === "wildcard") {
if (subdomain !== undefined && subdomain !== null) {
if (hostname.subdomain !== undefined && hostname.subdomain !== null) {
// Validate subdomain format for wildcard domains
const parsedSubdomain = subdomainSchema.safeParse(subdomain);
const parsedSubdomain = subdomainSchema.safeParse(hostname.subdomain);
if (!parsedSubdomain.success) {
return {
success: false,
error: fromError(parsedSubdomain.error).toString()
};
}
fullDomain = `${subdomain}.${domainRes.domains.baseDomain}`;
fullDomain = `${hostname.subdomain}.${domainRes.domains.baseDomain}`;
} else {
fullDomain = domainRes.domains.baseDomain;
}
Expand All @@ -100,13 +115,18 @@ export async function validateAndConstructDomain(

return {
success: true,
fullDomain,
subdomain: finalSubdomain ?? null
data: {
domainId: hostname.domainId,
subdomain: finalSubdomain || null,
fullDomain: fullDomain,
baseDomain: domainRes.domains.baseDomain,
primary: hostname.primary
}
};
} catch (error) {
return {
success: false,
error: `An error occurred while validating domain: ${error instanceof Error ? error.message : 'Unknown error'}`
};
}
}
}
Loading