use fullDomain from resources in get traefik config

This commit is contained in:
Milo Schwartz 2024-10-26 19:57:47 -04:00
parent d7c6903a8e
commit aa4806fb7f
5 changed files with 143 additions and 136 deletions

View file

@ -47,9 +47,7 @@ app.prepare().then(() => {
} }
const prefix = `/api/v1`; const prefix = `/api/v1`;
if (dev) { externalServer.use(logIncomingMiddleware);
externalServer.use(logIncomingMiddleware);
}
externalServer.use(prefix, unauthenticated); externalServer.use(prefix, unauthenticated);
externalServer.use(prefix, authenticated); externalServer.use(prefix, authenticated);

View file

@ -6,7 +6,6 @@ import response from "@server/utils/response";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import { sql, inArray } from "drizzle-orm"; import { sql, inArray } from "drizzle-orm";
import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
import logger from "@server/logger"; import logger from "@server/logger";
const listOrgsSchema = z.object({ const listOrgsSchema = z.object({
@ -32,7 +31,7 @@ export type ListOrgsResponse = {
export async function listOrgs( export async function listOrgs(
req: Request, req: Request,
res: Response, res: Response,
next: NextFunction, next: NextFunction
): Promise<any> { ): Promise<any> {
try { try {
const parsedQuery = listOrgsSchema.safeParse(req.query); const parsedQuery = listOrgsSchema.safeParse(req.query);
@ -40,13 +39,15 @@ export async function listOrgs(
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
parsedQuery.error.errors.map((e) => e.message).join(", "), parsedQuery.error.errors.map((e) => e.message).join(", ")
), )
); );
} }
const { limit, offset } = parsedQuery.data; const { limit, offset } = parsedQuery.data;
logger.debug("here0")
// Use the userOrgs passed from the middleware // Use the userOrgs passed from the middleware
const userOrgIds = req.userOrgIds; const userOrgIds = req.userOrgIds;
@ -65,6 +66,7 @@ export async function listOrgs(
message: "No organizations found for the user", message: "No organizations found for the user",
status: HttpCode.OK, status: HttpCode.OK,
}); });
logger.debug("here1")
} }
const organizations = await db const organizations = await db
@ -80,6 +82,8 @@ export async function listOrgs(
.where(inArray(orgs.orgId, userOrgIds)); .where(inArray(orgs.orgId, userOrgIds));
const totalCount = totalCountResult[0].count; const totalCount = totalCountResult[0].count;
logger.debug("here2")
return response<ListOrgsResponse>(res, { return response<ListOrgsResponse>(res, {
data: { data: {
orgs: organizations, orgs: organizations,
@ -99,8 +103,8 @@ export async function listOrgs(
return next( return next(
createHttpError( createHttpError(
HttpCode.INTERNAL_SERVER_ERROR, HttpCode.INTERNAL_SERVER_ERROR,
"An error occurred...", "An error occurred..."
), )
); );
} }
} }

View file

@ -2,100 +2,97 @@ import { Request, Response } from "express";
import db from "@server/db"; import db from "@server/db";
import * as schema from "@server/db/schema"; import * as schema from "@server/db/schema";
import { DynamicTraefikConfig } from "./configSchema"; import { DynamicTraefikConfig } from "./configSchema";
import { and, like, eq, isNotNull } from "drizzle-orm"; import { and, eq, isNotNull } from "drizzle-orm";
import logger from "@server/logger"; import logger from "@server/logger";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import config from "@server/config"; import config from "@server/config";
export async function traefikConfigProvider(_: Request, res: Response) { export async function traefikConfigProvider(
_: Request,
res: Response
): Promise<any> {
try { try {
const targets = await getAllTargets(); const all = await db
const traefikConfig = buildTraefikConfig(targets); .select()
res.status(HttpCode.OK).json(traefikConfig); .from(schema.targets)
.innerJoin(
schema.resources,
eq(schema.targets.resourceId, schema.resources.resourceId)
)
.where(
and(
eq(schema.targets.enabled, true),
isNotNull(schema.resources.fullDomain)
)
);
if (!all.length) {
return { http: {} } as DynamicTraefikConfig;
}
const middlewareName = "badger";
const baseDomain = new URL(config.app.base_url).hostname;
const tls = {
certResolver: config.traefik.cert_resolver,
...(config.traefik.prefer_wildcard_cert
? { domains: [baseDomain, `*.${baseDomain}`] }
: {}),
};
const http: any = {
routers: {},
services: {},
middlewares: {
[middlewareName]: {
plugin: {
[middlewareName]: {
apiBaseUrl: new URL(
"/api/v1",
`http://${config.server.internal_hostname}:${config.server.internal_port}`
).href,
appBaseUrl: config.app.base_url,
},
},
},
},
};
for (const item of all) {
const target = item.targets;
const resource = item.resources;
const routerName = `${target.targetId}-router`;
const serviceName = `${target.targetId}-service`;
http.routers![routerName] = {
entryPoints: [
target.ssl
? config.traefik.https_entrypoint
: config.traefik.http_entrypoint,
],
middlewares: [middlewareName],
service: serviceName,
rule: `Host(\`${resource.fullDomain}\`)`,
...(target.ssl ? { tls } : {}),
};
http.services![serviceName] = {
loadBalancer: {
servers: [
{
url: `${target.method}://${target.ip}:${target.port}`,
},
],
},
};
}
return res.status(HttpCode.OK).json({ http });
} catch (e) { } catch (e) {
logger.error(`Failed to build traefik config: ${e}`); logger.error(`Failed to build traefik config: ${e}`);
res.status(HttpCode.INTERNAL_SERVER_ERROR).json({ return res.status(HttpCode.INTERNAL_SERVER_ERROR).json({
error: "Failed to build traefik config", error: "Failed to build traefik config",
}); });
} }
} }
export function buildTraefikConfig(
targets: schema.Target[]
): DynamicTraefikConfig {
if (!targets.length) {
return { http: {} } as DynamicTraefikConfig;
}
const middlewareName = "badger";
const baseDomain = new URL(config.app.base_url).hostname;
const tls = {
certResolver: config.traefik.cert_resolver,
...(config.traefik.prefer_wildcard_cert
? { domains: [baseDomain, `*.${baseDomain}`] }
: {}),
};
const http: any = {
routers: {},
services: {},
middlewares: {
[middlewareName]: {
plugin: {
[middlewareName]: {
apiBaseUrl: new URL(
"/api/v1",
`http://${config.server.internal_hostname}:${config.server.internal_port}`
).href,
appBaseUrl: config.app.base_url,
},
},
},
},
};
for (const target of targets) {
const routerName = `${target.targetId}-router`;
const serviceName = `${target.targetId}-service`;
http.routers![routerName] = {
entryPoints: [
target.ssl
? config.traefik.https_entrypoint
: config.traefik.http_entrypoint,
],
middlewares: [middlewareName],
service: serviceName,
rule: `Host(\`${target.resourceId}\`)`, // assuming resourceId is a valid full hostname
...(target.ssl ? { tls } : {}),
};
http.services![serviceName] = {
loadBalancer: {
servers: [
{ url: `${target.method}://${target.ip}:${target.port}` },
],
},
};
}
return { http } as DynamicTraefikConfig;
}
export async function getAllTargets(): Promise<schema.Target[]> {
const all = await db
.select()
.from(schema.targets)
.innerJoin(
schema.resources,
eq(schema.targets.resourceId, schema.resources.resourceId)
)
.where(
and(
eq(schema.targets.enabled, true),
isNotNull(schema.resources.fullDomain)
)
);
return all.map((row) => row.targets);
}

View file

@ -24,24 +24,24 @@ export default async function RootLayout({
}>) { }>) {
const user = await verifySession(); const user = await verifySession();
console.log("layout.tsx user", user);
let orgs: ListOrgsResponse["orgs"] = []; let orgs: ListOrgsResponse["orgs"] = [];
if (user) { // if (user) {
try { // try {
const res = await internal.get<AxiosResponse<ListOrgsResponse>>( // const res = await internal.get<AxiosResponse<ListOrgsResponse>>(
`/orgs`, // `/orgs`,
await authCookieHeader(), // await authCookieHeader()
); // );
if (res && res.data.data.orgs) { // if (res && res.data.data.orgs) {
orgs = res.data.data.orgs; // orgs = res.data.data.orgs;
} // }
// } catch {}
if (!orgs.length) {
redirect(`/setup`); // if (!orgs.length) {
} // redirect(`/setup`);
} catch (e) { // }
console.error("Error fetching orgs", e); // }
}
}
return ( return (
<html suppressHydrationWarning> <html suppressHydrationWarning>

View file

@ -8,26 +8,30 @@ import { ArrowUpLeft, ArrowUpRight } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
export default async function Page() { export default async function Page(props: {
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
}) {
const params = await props.searchParams;
const user = await verifySession(); const user = await verifySession();
console.log("page.tsx user", user);
if (!user) { if (!user) {
redirect("/auth/login"); // redirect("/auth/login");
return; return;
} }
let orgs: ListOrgsResponse["orgs"] = []; // let orgs: ListOrgsResponse["orgs"] = [];
try { // try {
const res = await internal.get<AxiosResponse<ListOrgsResponse>>( // const res = await internal.get<AxiosResponse<ListOrgsResponse>>(
`/orgs`, // `/orgs`,
await authCookieHeader(), // await authCookieHeader()
); // );
if (res && res.data.data.orgs) { // if (res && res.data.data.orgs) {
orgs = res.data.data.orgs; // orgs = res.data.data.orgs;
} // }
} catch (e) { // } catch (e) {}
console.error("Error fetching orgs", e);
}
return ( return (
<> <>
@ -35,16 +39,20 @@ export default async function Page() {
<p>Logged in as {user.email}</p> <p>Logged in as {user.email}</p>
</LandingProvider> </LandingProvider>
<div className="mt-4"> {/* <div className="mt-4">
{orgs.map((org) => ( {orgs.map((org) => (
<Link key={org.orgId} href={`/${org.orgId}`} className="text-primary underline"> <Link
<div className="flex items-center"> key={org.orgId}
{org.name} href={`/${org.orgId}`}
<ArrowUpRight className="w-4 h-4"/> className="text-primary underline"
</div> >
</Link> <div className="flex items-center">
))} {org.name}
</div> <ArrowUpRight className="w-4 h-4" />
</div>
</Link>
))}
</div> */}
</> </>
); );
} }