add site targets, client resources, and auto login

This commit is contained in:
miloschwartz
2025-08-14 18:24:21 -07:00
parent 67ba225003
commit 5c04b1e14a
80 changed files with 5651 additions and 2385 deletions

View File

@@ -15,7 +15,6 @@ import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import { eq, and } from "drizzle-orm";
import stoi from "@server/lib/stoi";
import { fromError } from "zod-validation-error";
import logger from "@server/logger";
import { subdomainSchema } from "@server/lib/schemas";
@@ -25,7 +24,6 @@ import { build } from "@server/build";
const createResourceParamsSchema = z
.object({
siteId: z.string().transform(stoi).pipe(z.number().int().positive()),
orgId: z.string()
})
.strict();
@@ -34,7 +32,6 @@ const createHttpResourceSchema = z
.object({
name: z.string().min(1).max(255),
subdomain: z.string().nullable().optional(),
siteId: z.number(),
http: z.boolean(),
protocol: z.enum(["tcp", "udp"]),
domainId: z.string()
@@ -53,11 +50,10 @@ const createHttpResourceSchema = z
const createRawResourceSchema = z
.object({
name: z.string().min(1).max(255),
siteId: z.number(),
http: z.boolean(),
protocol: z.enum(["tcp", "udp"]),
proxyPort: z.number().int().min(1).max(65535),
enableProxy: z.boolean().default(true)
// enableProxy: z.boolean().default(true) // always true now
})
.strict()
.refine(
@@ -78,7 +74,7 @@ export type CreateResourceResponse = Resource;
registry.registerPath({
method: "put",
path: "/org/{orgId}/site/{siteId}/resource",
path: "/org/{orgId}/resource",
description: "Create a resource.",
tags: [OpenAPITags.Org, OpenAPITags.Resource],
request: {
@@ -111,7 +107,7 @@ export async function createResource(
);
}
const { siteId, orgId } = parsedParams.data;
const { orgId } = parsedParams.data;
if (req.user && !req.userOrgRoleId) {
return next(
@@ -146,7 +142,7 @@ export async function createResource(
if (http) {
return await createHttpResource(
{ req, res, next },
{ siteId, orgId }
{ orgId }
);
} else {
if (
@@ -162,7 +158,7 @@ export async function createResource(
}
return await createRawResource(
{ req, res, next },
{ siteId, orgId }
{ orgId }
);
}
} catch (error) {
@@ -180,12 +176,11 @@ async function createHttpResource(
next: NextFunction;
},
meta: {
siteId: number;
orgId: string;
}
) {
const { req, res, next } = route;
const { siteId, orgId } = meta;
const { orgId } = meta;
const parsedBody = createHttpResourceSchema.safeParse(req.body);
if (!parsedBody.success) {
@@ -292,7 +287,6 @@ async function createHttpResource(
const newResource = await trx
.insert(resources)
.values({
siteId,
fullDomain,
domainId,
orgId,
@@ -357,12 +351,11 @@ async function createRawResource(
next: NextFunction;
},
meta: {
siteId: number;
orgId: string;
}
) {
const { req, res, next } = route;
const { siteId, orgId } = meta;
const { orgId } = meta;
const parsedBody = createRawResourceSchema.safeParse(req.body);
if (!parsedBody.success) {
@@ -374,7 +367,7 @@ async function createRawResource(
);
}
const { name, http, protocol, proxyPort, enableProxy } = parsedBody.data;
const { name, http, protocol, proxyPort } = parsedBody.data;
// if http is false check to see if there is already a resource with the same port and protocol
const existingResource = await db
@@ -402,13 +395,12 @@ async function createRawResource(
const newResource = await trx
.insert(resources)
.values({
siteId,
orgId,
name,
http,
protocol,
proxyPort,
enableProxy
// enableProxy
})
.returning();

View File

@@ -71,44 +71,44 @@ export async function deleteResource(
);
}
const [site] = await db
.select()
.from(sites)
.where(eq(sites.siteId, deletedResource.siteId!))
.limit(1);
if (!site) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`Site with ID ${deletedResource.siteId} not found`
)
);
}
if (site.pubKey) {
if (site.type == "wireguard") {
await addPeer(site.exitNodeId!, {
publicKey: site.pubKey,
allowedIps: await getAllowedIps(site.siteId)
});
} else if (site.type == "newt") {
// get the newt on the site by querying the newt table for siteId
const [newt] = await db
.select()
.from(newts)
.where(eq(newts.siteId, site.siteId))
.limit(1);
removeTargets(
newt.newtId,
targetsToBeRemoved,
deletedResource.protocol,
deletedResource.proxyPort
);
}
}
// const [site] = await db
// .select()
// .from(sites)
// .where(eq(sites.siteId, deletedResource.siteId!))
// .limit(1);
//
// if (!site) {
// return next(
// createHttpError(
// HttpCode.NOT_FOUND,
// `Site with ID ${deletedResource.siteId} not found`
// )
// );
// }
//
// if (site.pubKey) {
// if (site.type == "wireguard") {
// await addPeer(site.exitNodeId!, {
// publicKey: site.pubKey,
// allowedIps: await getAllowedIps(site.siteId)
// });
// } else if (site.type == "newt") {
// // get the newt on the site by querying the newt table for siteId
// const [newt] = await db
// .select()
// .from(newts)
// .where(eq(newts.siteId, site.siteId))
// .limit(1);
//
// removeTargets(
// newt.newtId,
// targetsToBeRemoved,
// deletedResource.protocol,
// deletedResource.proxyPort
// );
// }
// }
//
return response(res, {
data: null,
success: true,

View File

@@ -19,9 +19,7 @@ const getResourceSchema = z
})
.strict();
export type GetResourceResponse = Resource & {
siteName: string;
};
export type GetResourceResponse = Resource;
registry.registerPath({
method: "get",
@@ -56,11 +54,9 @@ export async function getResource(
.select()
.from(resources)
.where(eq(resources.resourceId, resourceId))
.leftJoin(sites, eq(sites.siteId, resources.siteId))
.limit(1);
const resource = resp.resources;
const site = resp.sites;
const resource = resp;
if (!resource) {
return next(
@@ -73,8 +69,7 @@ export async function getResource(
return response(res, {
data: {
...resource,
siteName: site?.name
...resource
},
success: true,
error: false,

View File

@@ -31,6 +31,7 @@ export type GetResourceAuthInfoResponse = {
blockAccess: boolean;
url: string;
whitelist: boolean;
skipToIdpId: number | null;
};
export async function getResourceAuthInfo(
@@ -86,7 +87,8 @@ export async function getResourceAuthInfo(
sso: resource.sso,
blockAccess: resource.blockAccess,
url,
whitelist: resource.emailWhitelistEnabled
whitelist: resource.emailWhitelistEnabled,
skipToIdpId: resource.skipToIdpId
},
success: true,
error: false,

View File

@@ -1,16 +1,14 @@
import { Request, Response, NextFunction } from "express";
import { db } from "@server/db";
import { and, eq, or, inArray } from "drizzle-orm";
import {
resources,
userResources,
roleResources,
userOrgs,
roles,
import {
resources,
userResources,
roleResources,
userOrgs,
resourcePassword,
resourcePincode,
resourceWhitelist,
sites
resourceWhitelist
} from "@server/db";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
@@ -37,12 +35,7 @@ export async function getUserResources(
roleId: userOrgs.roleId
})
.from(userOrgs)
.where(
and(
eq(userOrgs.userId, userId),
eq(userOrgs.orgId, orgId)
)
)
.where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId)))
.limit(1);
if (userOrgResult.length === 0) {
@@ -71,8 +64,8 @@ export async function getUserResources(
// Combine all accessible resource IDs
const accessibleResourceIds = [
...directResources.map(r => r.resourceId),
...roleResourceResults.map(r => r.resourceId)
...directResources.map((r) => r.resourceId),
...roleResourceResults.map((r) => r.resourceId)
];
if (accessibleResourceIds.length === 0) {
@@ -95,11 +88,9 @@ export async function getUserResources(
enabled: resources.enabled,
sso: resources.sso,
protocol: resources.protocol,
emailWhitelistEnabled: resources.emailWhitelistEnabled,
siteName: sites.name
emailWhitelistEnabled: resources.emailWhitelistEnabled
})
.from(resources)
.leftJoin(sites, eq(sites.siteId, resources.siteId))
.where(
and(
inArray(resources.resourceId, accessibleResourceIds),
@@ -111,28 +102,61 @@ export async function getUserResources(
// Check for password, pincode, and whitelist protection for each resource
const resourcesWithAuth = await Promise.all(
resourcesData.map(async (resource) => {
const [passwordCheck, pincodeCheck, whitelistCheck] = await Promise.all([
db.select().from(resourcePassword).where(eq(resourcePassword.resourceId, resource.resourceId)).limit(1),
db.select().from(resourcePincode).where(eq(resourcePincode.resourceId, resource.resourceId)).limit(1),
db.select().from(resourceWhitelist).where(eq(resourceWhitelist.resourceId, resource.resourceId)).limit(1)
]);
const [passwordCheck, pincodeCheck, whitelistCheck] =
await Promise.all([
db
.select()
.from(resourcePassword)
.where(
eq(
resourcePassword.resourceId,
resource.resourceId
)
)
.limit(1),
db
.select()
.from(resourcePincode)
.where(
eq(
resourcePincode.resourceId,
resource.resourceId
)
)
.limit(1),
db
.select()
.from(resourceWhitelist)
.where(
eq(
resourceWhitelist.resourceId,
resource.resourceId
)
)
.limit(1)
]);
const hasPassword = passwordCheck.length > 0;
const hasPincode = pincodeCheck.length > 0;
const hasWhitelist = whitelistCheck.length > 0 || resource.emailWhitelistEnabled;
const hasWhitelist =
whitelistCheck.length > 0 || resource.emailWhitelistEnabled;
return {
resourceId: resource.resourceId,
name: resource.name,
domain: `${resource.ssl ? "https://" : "http://"}${resource.fullDomain}`,
enabled: resource.enabled,
protected: !!(resource.sso || hasPassword || hasPincode || hasWhitelist),
protected: !!(
resource.sso ||
hasPassword ||
hasPincode ||
hasWhitelist
),
protocol: resource.protocol,
sso: resource.sso,
password: hasPassword,
pincode: hasPincode,
whitelist: hasWhitelist,
siteName: resource.siteName
whitelist: hasWhitelist
};
})
);
@@ -144,11 +168,13 @@ export async function getUserResources(
message: "User resources retrieved successfully",
status: HttpCode.OK
});
} catch (error) {
console.error("Error fetching user resources:", error);
return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "Internal server error")
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Internal server error"
)
);
}
}
@@ -165,4 +191,4 @@ export type GetUserResourcesResponse = {
protocol: string;
}>;
};
};
};

View File

@@ -16,10 +16,9 @@ export * from "./setResourceWhitelist";
export * from "./getResourceWhitelist";
export * from "./authWithWhitelist";
export * from "./authWithAccessToken";
export * from "./transferResource";
export * from "./getExchangeToken";
export * from "./createResourceRule";
export * from "./deleteResourceRule";
export * from "./listResourceRules";
export * from "./updateResourceRule";
export * from "./getUserResources";
export * from "./getUserResources";

View File

@@ -3,7 +3,6 @@ import { z } from "zod";
import { db } from "@server/db";
import {
resources,
sites,
userResources,
roleResources,
resourcePassword,
@@ -20,17 +19,9 @@ import { OpenAPITags, registry } from "@server/openApi";
const listResourcesParamsSchema = z
.object({
siteId: z
.string()
.optional()
.transform(stoi)
.pipe(z.number().int().positive().optional()),
orgId: z.string().optional()
orgId: z.string()
})
.strict()
.refine((data) => !!data.siteId !== !!data.orgId, {
message: "Either siteId or orgId must be provided, but not both"
});
.strict();
const listResourcesSchema = z.object({
limit: z
@@ -48,82 +39,38 @@ const listResourcesSchema = z.object({
.pipe(z.number().int().nonnegative())
});
function queryResources(
accessibleResourceIds: number[],
siteId?: number,
orgId?: string
) {
if (siteId) {
return db
.select({
resourceId: resources.resourceId,
name: resources.name,
fullDomain: resources.fullDomain,
ssl: resources.ssl,
siteName: sites.name,
siteId: sites.niceId,
passwordId: resourcePassword.passwordId,
pincodeId: resourcePincode.pincodeId,
sso: resources.sso,
whitelist: resources.emailWhitelistEnabled,
http: resources.http,
protocol: resources.protocol,
proxyPort: resources.proxyPort,
enabled: resources.enabled,
domainId: resources.domainId
})
.from(resources)
.leftJoin(sites, eq(resources.siteId, sites.siteId))
.leftJoin(
resourcePassword,
eq(resourcePassword.resourceId, resources.resourceId)
function queryResources(accessibleResourceIds: number[], orgId: string) {
return db
.select({
resourceId: resources.resourceId,
name: resources.name,
ssl: resources.ssl,
fullDomain: resources.fullDomain,
passwordId: resourcePassword.passwordId,
sso: resources.sso,
pincodeId: resourcePincode.pincodeId,
whitelist: resources.emailWhitelistEnabled,
http: resources.http,
protocol: resources.protocol,
proxyPort: resources.proxyPort,
enabled: resources.enabled,
domainId: resources.domainId
})
.from(resources)
.leftJoin(
resourcePassword,
eq(resourcePassword.resourceId, resources.resourceId)
)
.leftJoin(
resourcePincode,
eq(resourcePincode.resourceId, resources.resourceId)
)
.where(
and(
inArray(resources.resourceId, accessibleResourceIds),
eq(resources.orgId, orgId)
)
.leftJoin(
resourcePincode,
eq(resourcePincode.resourceId, resources.resourceId)
)
.where(
and(
inArray(resources.resourceId, accessibleResourceIds),
eq(resources.siteId, siteId)
)
);
} else if (orgId) {
return db
.select({
resourceId: resources.resourceId,
name: resources.name,
ssl: resources.ssl,
fullDomain: resources.fullDomain,
siteName: sites.name,
siteId: sites.niceId,
passwordId: resourcePassword.passwordId,
sso: resources.sso,
pincodeId: resourcePincode.pincodeId,
whitelist: resources.emailWhitelistEnabled,
http: resources.http,
protocol: resources.protocol,
proxyPort: resources.proxyPort,
enabled: resources.enabled,
domainId: resources.domainId
})
.from(resources)
.leftJoin(sites, eq(resources.siteId, sites.siteId))
.leftJoin(
resourcePassword,
eq(resourcePassword.resourceId, resources.resourceId)
)
.leftJoin(
resourcePincode,
eq(resourcePincode.resourceId, resources.resourceId)
)
.where(
and(
inArray(resources.resourceId, accessibleResourceIds),
eq(resources.orgId, orgId)
)
);
}
);
}
export type ListResourcesResponse = {
@@ -131,20 +78,6 @@ export type ListResourcesResponse = {
pagination: { total: number; limit: number; offset: number };
};
registry.registerPath({
method: "get",
path: "/site/{siteId}/resources",
description: "List resources for a site.",
tags: [OpenAPITags.Site, OpenAPITags.Resource],
request: {
params: z.object({
siteId: z.number()
}),
query: listResourcesSchema
},
responses: {}
});
registry.registerPath({
method: "get",
path: "/org/{orgId}/resources",
@@ -185,9 +118,11 @@ export async function listResources(
)
);
}
const { siteId } = parsedParams.data;
const orgId = parsedParams.data.orgId || req.userOrg?.orgId || req.apiKeyOrg?.orgId;
const orgId =
parsedParams.data.orgId ||
req.userOrg?.orgId ||
req.apiKeyOrg?.orgId;
if (!orgId) {
return next(
@@ -207,24 +142,27 @@ export async function listResources(
let accessibleResources;
if (req.user) {
accessibleResources = await db
.select({
resourceId: sql<number>`COALESCE(${userResources.resourceId}, ${roleResources.resourceId})`
})
.from(userResources)
.fullJoin(
roleResources,
eq(userResources.resourceId, roleResources.resourceId)
)
.where(
or(
eq(userResources.userId, req.user!.userId),
eq(roleResources.roleId, req.userOrgRoleId!)
.select({
resourceId: sql<number>`COALESCE(${userResources.resourceId}, ${roleResources.resourceId})`
})
.from(userResources)
.fullJoin(
roleResources,
eq(userResources.resourceId, roleResources.resourceId)
)
);
.where(
or(
eq(userResources.userId, req.user!.userId),
eq(roleResources.roleId, req.userOrgRoleId!)
)
);
} else {
accessibleResources = await db.select({
resourceId: resources.resourceId
}).from(resources).where(eq(resources.orgId, orgId));
accessibleResources = await db
.select({
resourceId: resources.resourceId
})
.from(resources)
.where(eq(resources.orgId, orgId));
}
const accessibleResourceIds = accessibleResources.map(
@@ -236,7 +174,7 @@ export async function listResources(
.from(resources)
.where(inArray(resources.resourceId, accessibleResourceIds));
const baseQuery = queryResources(accessibleResourceIds, siteId, orgId);
const baseQuery = queryResources(accessibleResourceIds, orgId);
const resourcesList = await baseQuery!.limit(limit).offset(offset);
const totalCountResult = await countQuery;

View File

@@ -1,214 +0,0 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { db } from "@server/db";
import { newts, resources, sites, targets } from "@server/db";
import { eq } from "drizzle-orm";
import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import logger from "@server/logger";
import { fromError } from "zod-validation-error";
import { addPeer } from "../gerbil/peers";
import { addTargets, removeTargets } from "../newt/targets";
import { getAllowedIps } from "../target/helpers";
import { OpenAPITags, registry } from "@server/openApi";
const transferResourceParamsSchema = z
.object({
resourceId: z
.string()
.transform(Number)
.pipe(z.number().int().positive())
})
.strict();
const transferResourceBodySchema = z
.object({
siteId: z.number().int().positive()
})
.strict();
registry.registerPath({
method: "post",
path: "/resource/{resourceId}/transfer",
description:
"Transfer a resource to a different site. This will also transfer the targets associated with the resource.",
tags: [OpenAPITags.Resource],
request: {
params: transferResourceParamsSchema,
body: {
content: {
"application/json": {
schema: transferResourceBodySchema
}
}
}
},
responses: {}
});
export async function transferResource(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const parsedParams = transferResourceParamsSchema.safeParse(req.params);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString()
)
);
}
const parsedBody = transferResourceBodySchema.safeParse(req.body);
if (!parsedBody.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedBody.error).toString()
)
);
}
const { resourceId } = parsedParams.data;
const { siteId } = parsedBody.data;
const [oldResource] = await db
.select()
.from(resources)
.where(eq(resources.resourceId, resourceId))
.limit(1);
if (!oldResource) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`Resource with ID ${resourceId} not found`
)
);
}
if (oldResource.siteId === siteId) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
`Resource is already assigned to this site`
)
);
}
const [newSite] = await db
.select()
.from(sites)
.where(eq(sites.siteId, siteId))
.limit(1);
if (!newSite) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`Site with ID ${siteId} not found`
)
);
}
const [oldSite] = await db
.select()
.from(sites)
.where(eq(sites.siteId, oldResource.siteId))
.limit(1);
if (!oldSite) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`Site with ID ${oldResource.siteId} not found`
)
);
}
const [updatedResource] = await db
.update(resources)
.set({ siteId })
.where(eq(resources.resourceId, resourceId))
.returning();
if (!updatedResource) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`Resource with ID ${resourceId} not found`
)
);
}
const resourceTargets = await db
.select()
.from(targets)
.where(eq(targets.resourceId, resourceId));
if (resourceTargets.length > 0) {
////// REMOVE THE TARGETS FROM THE OLD SITE //////
if (oldSite.pubKey) {
if (oldSite.type == "wireguard") {
await addPeer(oldSite.exitNodeId!, {
publicKey: oldSite.pubKey,
allowedIps: await getAllowedIps(oldSite.siteId)
});
} else if (oldSite.type == "newt") {
const [newt] = await db
.select()
.from(newts)
.where(eq(newts.siteId, oldSite.siteId))
.limit(1);
removeTargets(
newt.newtId,
resourceTargets,
updatedResource.protocol,
updatedResource.proxyPort
);
}
}
////// ADD THE TARGETS TO THE NEW SITE //////
if (newSite.pubKey) {
if (newSite.type == "wireguard") {
await addPeer(newSite.exitNodeId!, {
publicKey: newSite.pubKey,
allowedIps: await getAllowedIps(newSite.siteId)
});
} else if (newSite.type == "newt") {
const [newt] = await db
.select()
.from(newts)
.where(eq(newts.siteId, newSite.siteId))
.limit(1);
addTargets(
newt.newtId,
resourceTargets,
updatedResource.protocol,
updatedResource.proxyPort
);
}
}
}
return response(res, {
data: updatedResource,
success: true,
error: false,
message: "Resource transferred successfully",
status: HttpCode.OK
});
} catch (error) {
logger.error(error);
return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
);
}
}

View File

@@ -20,7 +20,6 @@ import { tlsNameSchema } from "@server/lib/schemas";
import { subdomainSchema } from "@server/lib/schemas";
import { registry } from "@server/openApi";
import { OpenAPITags } from "@server/openApi";
import { build } from "@server/build";
const updateResourceParamsSchema = z
.object({
@@ -44,7 +43,8 @@ const updateHttpResourceBodySchema = z
enabled: z.boolean().optional(),
stickySession: z.boolean().optional(),
tlsServerName: z.string().nullable().optional(),
setHostHeader: z.string().nullable().optional()
setHostHeader: z.string().nullable().optional(),
skipToIdpId: z.number().int().positive().nullable().optional()
})
.strict()
.refine((data) => Object.keys(data).length > 0, {
@@ -91,8 +91,8 @@ const updateRawResourceBodySchema = z
name: z.string().min(1).max(255).optional(),
proxyPort: z.number().int().min(1).max(65535).optional(),
stickySession: z.boolean().optional(),
enabled: z.boolean().optional(),
enableProxy: z.boolean().optional()
enabled: z.boolean().optional()
// enableProxy: z.boolean().optional() // always true now
})
.strict()
.refine((data) => Object.keys(data).length > 0, {