sync config managed domains to db

This commit is contained in:
miloschwartz 2025-02-15 22:41:39 -05:00
parent abcc99a830
commit e41c0c9d7c
3 changed files with 106 additions and 39 deletions

View file

@ -2,21 +2,23 @@ import { InferSelectModel } from "drizzle-orm";
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core"; import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
export const domains = sqliteTable("domains", { export const domains = sqliteTable("domains", {
domainId: integer("domainId").primaryKey({ autoIncrement: true }), domainId: text("domainId").primaryKey(),
baseDomain: text("domain").notNull().unique() baseDomain: text("baseDomain").notNull().unique(),
configManaged: integer("configManaged", { mode: "boolean" })
.notNull()
.default(false)
}); });
export const orgs = sqliteTable("orgs", { export const orgs = sqliteTable("orgs", {
orgId: text("orgId").primaryKey(), orgId: text("orgId").primaryKey(),
name: text("name").notNull(), name: text("name").notNull()
domain: text("domain").notNull()
}); });
export const orgDomains = sqliteTable("orgDomains", { export const orgDomains = sqliteTable("orgDomains", {
orgId: text("orgId") orgId: text("orgId")
.notNull() .notNull()
.references(() => orgs.orgId, { onDelete: "cascade" }), .references(() => orgs.orgId, { onDelete: "cascade" }),
domainId: integer("domainId") domainId: text("domainId")
.notNull() .notNull()
.references(() => domains.domainId, { onDelete: "cascade" }) .references(() => domains.domainId, { onDelete: "cascade" })
}); });
@ -57,7 +59,7 @@ export const resources = sqliteTable("resources", {
name: text("name").notNull(), name: text("name").notNull(),
subdomain: text("subdomain"), subdomain: text("subdomain"),
fullDomain: text("fullDomain"), fullDomain: text("fullDomain"),
domainId: integer("domainId").references(() => domains.domainId, { domainId: text("domainId").references(() => domains.domainId, {
onDelete: "set null" onDelete: "set null"
}), }),
ssl: integer("ssl", { mode: "boolean" }).notNull().default(false), ssl: integer("ssl", { mode: "boolean" }).notNull().default(false),

View file

@ -38,23 +38,12 @@ const configSchema = z.object({
save_logs: z.boolean(), save_logs: z.boolean(),
log_failed_attempts: z.boolean().optional() log_failed_attempts: z.boolean().optional()
}), }),
domains: z domains: z.record(
.array( z.string(),
z.object({ z.object({
base_domain: hostnameSchema.transform((url) => base_domain: hostnameSchema.transform((url) => url.toLowerCase())
url.toLowerCase() })
) ),
})
)
.refine(
(data) => {
const baseDomains = data.map((d) => d.base_domain);
return new Set(baseDomains).size === baseDomains.length;
},
{
message: "Base domains must be unique"
}
),
server: z.object({ server: z.object({
external_port: portSchema external_port: portSchema
.optional() .optional()

View file

@ -1,40 +1,116 @@
import { db } from "@server/db"; import { db } from "@server/db";
import { exitNodes, orgs, resources } from "../db/schema"; import { domains, exitNodes, orgDomains, orgs, resources } from "../db/schema";
import config from "@server/lib/config"; import config from "@server/lib/config";
import { eq, ne } from "drizzle-orm"; import { eq, ne } from "drizzle-orm";
import logger from "@server/logger"; import logger from "@server/logger";
export async function copyInConfig() { export async function copyInConfig() {
const domain = config.getBaseDomain();
const endpoint = config.getRawConfig().gerbil.base_endpoint; const endpoint = config.getRawConfig().gerbil.base_endpoint;
const listenPort = config.getRawConfig().gerbil.start_port; const listenPort = config.getRawConfig().gerbil.start_port;
// update the domain on all of the orgs where the domain is not equal to the new domain
// TODO: eventually each org could have a unique domain that we do not want to overwrite, so this will be unnecessary
await db.update(orgs).set({ domain }).where(ne(orgs.domain, domain));
// TODO: eventually each exit node could have a different endpoint
await db.update(exitNodes).set({ endpoint }).where(ne(exitNodes.endpoint, endpoint));
// TODO: eventually each exit node could have a different port
await db.update(exitNodes).set({ listenPort }).where(ne(exitNodes.listenPort, listenPort));
// update all resources fullDomain to use the new domain
await db.transaction(async (trx) => { await db.transaction(async (trx) => {
const allResources = await trx.select().from(resources); const rawDomains = config.getRawConfig().domains;
const configDomains = Object.entries(rawDomains).map(
([key, value]) => ({
domainId: key,
baseDomain: value.base_domain.toLowerCase()
})
);
const existingDomains = await trx
.select()
.from(domains)
.where(eq(domains.configManaged, true));
const existingDomainKeys = new Set(
existingDomains.map((d) => d.domainId)
);
const configDomainKeys = new Set(configDomains.map((d) => d.domainId));
for (const existingDomain of existingDomains) {
if (!configDomainKeys.has(existingDomain.domainId)) {
await trx
.delete(domains)
.where(eq(domains.domainId, existingDomain.domainId))
.execute();
}
}
for (const { domainId, baseDomain } of configDomains) {
if (existingDomainKeys.has(domainId)) {
await trx
.update(domains)
.set({ baseDomain })
.where(eq(domains.domainId, domainId))
.execute();
} else {
await trx
.insert(domains)
.values({ domainId, baseDomain, configManaged: true })
.execute();
}
}
const allResources = await trx
.select()
.from(resources)
.leftJoin(domains, eq(domains.domainId, resources.domainId));
for (const { resources: resource, domains: domain } of allResources) {
if (!resource || !domain) {
continue;
}
if (!domain.configManaged) {
continue;
}
for (const resource of allResources) {
let fullDomain = ""; let fullDomain = "";
if (resource.isBaseDomain) { if (resource.isBaseDomain) {
fullDomain = domain; fullDomain = domain.baseDomain;
} else { } else {
fullDomain = `${resource.subdomain}.${domain}`; fullDomain = `${resource.subdomain}.${domain}`;
} }
await trx await trx
.update(resources) .update(resources)
.set({ fullDomain }) .set({ fullDomain })
.where(eq(resources.resourceId, resource.resourceId)); .where(eq(resources.resourceId, resource.resourceId));
} }
const allOrgs = await trx.select().from(orgs);
const existingOrgDomains = await trx.select().from(orgDomains);
const existingOrgDomainSet = new Set(
existingOrgDomains.map((od) => `${od.orgId}-${od.domainId}`)
);
const newOrgDomains = [];
for (const org of allOrgs) {
for (const domain of configDomains) {
const key = `${org.orgId}-${domain.domainId}`;
if (!existingOrgDomainSet.has(key)) {
newOrgDomains.push({
orgId: org.orgId,
domainId: domain.domainId
});
}
}
}
if (newOrgDomains.length > 0) {
await trx.insert(orgDomains).values(newOrgDomains).execute();
}
}); });
logger.info(`Updated orgs with new domain (${domain})`); // TODO: eventually each exit node could have a different endpoint
await db
.update(exitNodes)
.set({ endpoint })
.where(ne(exitNodes.endpoint, endpoint));
// TODO: eventually each exit node could have a different port
await db
.update(exitNodes)
.set({ listenPort })
.where(ne(exitNodes.listenPort, listenPort));
} }