diff --git a/server/routers/org/createOrg.ts b/server/routers/org/createOrg.ts index 3c25c0c3..a6072a30 100644 --- a/server/routers/org/createOrg.ts +++ b/server/routers/org/createOrg.ts @@ -2,7 +2,15 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db } from "@server/db"; import { eq } from "drizzle-orm"; -import { Org, orgs, roleActions, roles, userOrgs } from "@server/db/schema"; +import { + domains, + Org, + orgDomains, + orgs, + roleActions, + roles, + userOrgs +} from "@server/db/schema"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; @@ -16,7 +24,6 @@ const createOrgSchema = z .object({ orgId: z.string(), name: z.string().min(1).max(255) - // domain: z.string().min(1).max(255).optional(), }) .strict(); @@ -82,14 +89,13 @@ export async function createOrg( let org: Org | null = null; await db.transaction(async (trx) => { - const domain = config.getBaseDomain(); + const allDomains = await trx.select().from(domains); const newOrg = await trx .insert(orgs) .values({ orgId, - name, - domain + name }) .returning(); @@ -109,6 +115,13 @@ export async function createOrg( return; } + await trx.insert(orgDomains).values( + allDomains.map((domain) => ({ + orgId: newOrg[0].orgId, + domainId: domain.domainId + })) + ); + await trx.insert(userOrgs).values({ userId: req.user!.userId, orgId: newOrg[0].orgId, diff --git a/server/routers/resource/createResource.ts b/server/routers/resource/createResource.ts index 39b07a57..2cf8052e 100644 --- a/server/routers/resource/createResource.ts +++ b/server/routers/resource/createResource.ts @@ -1,8 +1,9 @@ -import { SqliteError } from "better-sqlite3"; import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db } from "@server/db"; import { + domains, + orgDomains, orgs, Resource, resources, @@ -27,69 +28,17 @@ const createResourceParamsSchema = z }) .strict(); -const createResourceSchema = z +const createHttpResourceSchema = z .object({ - subdomain: z.string().optional(), name: z.string().min(1).max(255), + subdomain: subdomainSchema.optional(), + isBaseDomain: z.boolean().optional(), siteId: z.number(), http: z.boolean(), protocol: z.string(), - proxyPort: z.number().optional(), - isBaseDomain: z.boolean().optional() + domainId: z.string() }) - .refine( - (data) => { - if (!data.http) { - return z - .number() - .int() - .min(1) - .max(65535) - .safeParse(data.proxyPort).success; - } - return true; - }, - { - message: "Invalid port number", - path: ["proxyPort"] - } - ) - .refine( - (data) => { - if (data.http && !data.isBaseDomain) { - return subdomainSchema.safeParse(data.subdomain).success; - } - return true; - }, - { - message: "Invalid subdomain", - path: ["subdomain"] - } - ) - .refine( - (data) => { - if (!config.getRawConfig().flags?.allow_raw_resources) { - if (data.proxyPort !== undefined) { - return false; - } - } - return true; - }, - { - message: "Proxy port cannot be set" - } - ) - // .refine( - // (data) => { - // if (data.proxyPort === 443 || data.proxyPort === 80) { - // return false; - // } - // return true; - // }, - // { - // message: "Port 80 and 443 are reserved for http and https resources" - // } - // ) + .strict() .refine( (data) => { if (!config.getRawConfig().flags?.allow_base_domain_resources) { @@ -104,6 +53,29 @@ const createResourceSchema = z } ); +const createRawResourceSchema = z + .object({ + name: z.string().min(1).max(255), + siteId: z.number(), + http: z.boolean(), + protocol: z.string(), + proxyPort: z.number().int().min(1).max(65535) + }) + .strict() + .refine( + (data) => { + if (!config.getRawConfig().flags?.allow_raw_resources) { + if (data.proxyPort !== undefined) { + return false; + } + } + return true; + }, + { + message: "Proxy port cannot be set" + } + ); + export type CreateResourceResponse = Resource; export async function createResource( @@ -112,18 +84,6 @@ export async function createResource( next: NextFunction ): Promise { try { - const parsedBody = createResourceSchema.safeParse(req.body); - if (!parsedBody.success) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - fromError(parsedBody.error).toString() - ) - ); - } - - let { name, subdomain, protocol, proxyPort, http, isBaseDomain } = parsedBody.data; - // Validate request params const parsedParams = createResourceParamsSchema.safeParse(req.params); if (!parsedParams.success) { @@ -159,99 +119,25 @@ export async function createResource( ); } - let fullDomain = ""; - if (isBaseDomain) { - fullDomain = org[0].domain; - } else { - fullDomain = `${subdomain}.${org[0].domain}`; + if (!req.body?.http) { + return next( + createHttpError(HttpCode.BAD_REQUEST, "http field is required") + ); } - // if http is false check to see if there is already a resource with the same port and protocol - if (!http) { - const existingResource = await db - .select() - .from(resources) - .where( - and( - eq(resources.protocol, protocol), - eq(resources.proxyPort, proxyPort!) - ) - ); + const { http } = req.body; - if (existingResource.length > 0) { - return next( - createHttpError( - HttpCode.CONFLICT, - "Resource with that protocol and port already exists" - ) - ); - } + if (http) { + return await createHttpResource( + { req, res, next }, + { siteId, orgId } + ); } else { - // make sure the full domain is unique - const existingResource = await db - .select() - .from(resources) - .where(eq(resources.fullDomain, fullDomain)); - - if (existingResource.length > 0) { - return next( - createHttpError( - HttpCode.CONFLICT, - "Resource with that domain already exists" - ) - ); - } + return await createRawResource( + { req, res, next }, + { siteId, orgId } + ); } - - await db.transaction(async (trx) => { - const newResource = await trx - .insert(resources) - .values({ - siteId, - fullDomain: http ? fullDomain : null, - orgId, - name, - subdomain, - http, - protocol, - proxyPort, - ssl: true, - isBaseDomain - }) - .returning(); - - const adminRole = await db - .select() - .from(roles) - .where(and(eq(roles.isAdmin, true), eq(roles.orgId, orgId))) - .limit(1); - - if (adminRole.length === 0) { - return next( - createHttpError(HttpCode.NOT_FOUND, `Admin role not found`) - ); - } - - await trx.insert(roleResources).values({ - roleId: adminRole[0].roleId, - resourceId: newResource[0].resourceId - }); - - if (req.userOrgRoleId != adminRole[0].roleId) { - // make sure the user can access the resource - await trx.insert(userResources).values({ - userId: req.user?.userId!, - resourceId: newResource[0].resourceId - }); - } - response(res, { - data: newResource[0], - success: true, - error: false, - message: "Resource created successfully", - status: HttpCode.CREATED - }); - }); } catch (error) { logger.error(error); return next( @@ -259,3 +145,242 @@ export async function createResource( ); } } + +async function createHttpResource( + route: { + req: Request; + res: Response; + next: NextFunction; + }, + meta: { + siteId: number; + orgId: string; + } +) { + const { req, res, next } = route; + const { siteId, orgId } = meta; + + const parsedBody = createHttpResourceSchema.safeParse(req.body); + if (!parsedBody.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedBody.error).toString() + ) + ); + } + + const { name, subdomain, isBaseDomain, http, protocol, domainId } = + parsedBody.data; + + const [orgDomain] = await db + .select() + .from(orgDomains) + .where( + and(eq(orgDomains.orgId, orgId), eq(orgDomains.domainId, domainId)) + ) + .leftJoin(domains, eq(orgDomains.domainId, domains.domainId)); + + if (!orgDomain || !orgDomain.domains) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + `Domain with ID ${parsedBody.data.domainId} not found` + ) + ); + } + + const domain = orgDomain.domains; + + let fullDomain = ""; + if (isBaseDomain) { + fullDomain = domain.baseDomain; + } else { + fullDomain = `${subdomain}.${domain.baseDomain}`; + } + + // make sure the full domain is unique + const existingResource = await db + .select() + .from(resources) + .where(eq(resources.fullDomain, fullDomain)); + + if (existingResource.length > 0) { + return next( + createHttpError( + HttpCode.CONFLICT, + "Resource with that domain already exists" + ) + ); + } + + let resource: Resource | undefined; + + await db.transaction(async (trx) => { + const newResource = await trx + .insert(resources) + .values({ + siteId, + fullDomain: http ? fullDomain : null, + orgId, + name, + subdomain, + http, + protocol, + ssl: true, + isBaseDomain + }) + .returning(); + + const adminRole = await db + .select() + .from(roles) + .where(and(eq(roles.isAdmin, true), eq(roles.orgId, orgId))) + .limit(1); + + if (adminRole.length === 0) { + return next( + createHttpError(HttpCode.NOT_FOUND, `Admin role not found`) + ); + } + + await trx.insert(roleResources).values({ + roleId: adminRole[0].roleId, + resourceId: newResource[0].resourceId + }); + + if (req.userOrgRoleId != adminRole[0].roleId) { + // make sure the user can access the resource + await trx.insert(userResources).values({ + userId: req.user?.userId!, + resourceId: newResource[0].resourceId + }); + } + + resource = newResource[0]; + }); + + if (!resource) { + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "Failed to create resource" + ) + ); + } + + return response(res, { + data: resource, + success: true, + error: false, + message: "Http resource created successfully", + status: HttpCode.CREATED + }); +} + +async function createRawResource( + route: { + req: Request; + res: Response; + next: NextFunction; + }, + meta: { + siteId: number; + orgId: string; + } +) { + const { req, res, next } = route; + const { siteId, orgId } = meta; + + const parsedBody = createRawResourceSchema.safeParse(req.body); + if (!parsedBody.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedBody.error).toString() + ) + ); + } + + 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 + .select() + .from(resources) + .where( + and( + eq(resources.protocol, protocol), + eq(resources.proxyPort, proxyPort!) + ) + ); + + if (existingResource.length > 0) { + return next( + createHttpError( + HttpCode.CONFLICT, + "Resource with that protocol and port already exists" + ) + ); + } + + let resource: Resource | undefined; + + await db.transaction(async (trx) => { + const newResource = await trx + .insert(resources) + .values({ + siteId, + orgId, + name, + http, + protocol, + proxyPort + }) + .returning(); + + const adminRole = await db + .select() + .from(roles) + .where(and(eq(roles.isAdmin, true), eq(roles.orgId, orgId))) + .limit(1); + + if (adminRole.length === 0) { + return next( + createHttpError(HttpCode.NOT_FOUND, `Admin role not found`) + ); + } + + await trx.insert(roleResources).values({ + roleId: adminRole[0].roleId, + resourceId: newResource[0].resourceId + }); + + if (req.userOrgRoleId != adminRole[0].roleId) { + // make sure the user can access the resource + await trx.insert(userResources).values({ + userId: req.user?.userId!, + resourceId: newResource[0].resourceId + }); + } + + resource = newResource[0]; + }); + + if (!resource) { + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "Failed to create resource" + ) + ); + } + + return response(res, { + data: resource, + success: true, + error: false, + message: "Non-http resource created successfully", + status: HttpCode.CREATED + }); +} diff --git a/server/routers/resource/updateResource.ts b/server/routers/resource/updateResource.ts index e464b4c5..8d737541 100644 --- a/server/routers/resource/updateResource.ts +++ b/server/routers/resource/updateResource.ts @@ -1,8 +1,15 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db } from "@server/db"; -import { orgs, resources, sites } from "@server/db/schema"; -import { eq, or, and } from "drizzle-orm"; +import { + domains, + Org, + orgDomains, + orgs, + Resource, + resources +} from "@server/db/schema"; +import { eq, and } from "drizzle-orm"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; @@ -20,17 +27,40 @@ const updateResourceParamsSchema = z }) .strict(); -const updateResourceBodySchema = z +const updateHttpResourceBodySchema = z .object({ name: z.string().min(1).max(255).optional(), subdomain: subdomainSchema.optional(), ssl: z.boolean().optional(), sso: z.boolean().optional(), blockAccess: z.boolean().optional(), - proxyPort: z.number().int().min(1).max(65535).optional(), emailWhitelistEnabled: z.boolean().optional(), isBaseDomain: z.boolean().optional(), applyRules: z.boolean().optional(), + domainId: z.string().optional() + }) + .strict() + .refine((data) => Object.keys(data).length > 0, { + message: "At least one field must be provided for update" + }) + .refine( + (data) => { + if (!config.getRawConfig().flags?.allow_base_domain_resources) { + if (data.isBaseDomain) { + return false; + } + } + return true; + }, + { + message: "Base domain resources are not allowed" + } + ); + +const updateRawResourceBodySchema = z + .object({ + name: z.string().min(1).max(255).optional(), + proxyPort: z.number().int().min(1).max(65535).optional() }) .strict() .refine((data) => Object.keys(data).length > 0, { @@ -46,30 +76,6 @@ const updateResourceBodySchema = z return true; }, { message: "Cannot update proxyPort" } - ) - // .refine( - // (data) => { - // if (data.proxyPort === 443 || data.proxyPort === 80) { - // return false; - // } - // return true; - // }, - // { - // message: "Port 80 and 443 are reserved for http and https resources" - // } - // ) - .refine( - (data) => { - if (!config.getRawConfig().flags?.allow_base_domain_resources) { - if (data.isBaseDomain) { - return false; - } - } - return true; - }, - { - message: "Base domain resources are not allowed" - } ); export async function updateResource( @@ -88,18 +94,7 @@ export async function updateResource( ); } - const parsedBody = updateResourceBodySchema.safeParse(req.body); - if (!parsedBody.success) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - fromError(parsedBody.error).toString() - ) - ); - } - const { resourceId } = parsedParams.data; - const updateData = parsedBody.data; const [result] = await db .select() @@ -119,117 +114,33 @@ export async function updateResource( ); } - if (updateData.subdomain) { - if (!resource.http) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - "Cannot update subdomain for non-http resource" - ) - ); - } - - const valid = subdomainSchema.safeParse( - updateData.subdomain - ).success; - if (!valid) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - "Invalid subdomain provided" - ) - ); - } - } - - if (updateData.proxyPort) { - const proxyPort = updateData.proxyPort; - const existingResource = await db - .select() - .from(resources) - .where( - and( - eq(resources.protocol, resource.protocol), - eq(resources.proxyPort, proxyPort!) - ) - ); - - if ( - existingResource.length > 0 && - existingResource[0].resourceId !== resourceId - ) { - return next( - createHttpError( - HttpCode.CONFLICT, - "Resource with that protocol and port already exists" - ) - ); - } - } - - if (!org?.domain) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - "Resource does not have a domain" - ) + if (resource.http) { + // HANDLE UPDATING HTTP RESOURCES + return await updateHttpResource( + { + req, + res, + next + }, + { + resource, + org + } + ); + } else { + // HANDLE UPDATING RAW TCP/UDP RESOURCES + return await updateRawResource( + { + req, + res, + next + }, + { + resource, + org + } ); } - - let fullDomain: string | undefined; - if (updateData.isBaseDomain) { - fullDomain = org.domain; - } else if (updateData.subdomain) { - fullDomain = `${updateData.subdomain}.${org.domain}`; - } - - const updatePayload = { - ...updateData, - ...(fullDomain && { fullDomain }) - }; - - if ( - fullDomain && - (updatePayload.subdomain !== undefined || - updatePayload.isBaseDomain !== undefined) - ) { - const [existingDomain] = await db - .select() - .from(resources) - .where(eq(resources.fullDomain, fullDomain)); - - if (existingDomain && existingDomain.resourceId !== resourceId) { - return next( - createHttpError( - HttpCode.CONFLICT, - "Resource with that domain already exists" - ) - ); - } - } - - const updatedResource = await db - .update(resources) - .set(updatePayload) - .where(eq(resources.resourceId, resourceId)) - .returning(); - - if (updatedResource.length === 0) { - return next( - createHttpError( - HttpCode.NOT_FOUND, - `Resource with ID ${resourceId} not found` - ) - ); - } - - return response(res, { - data: updatedResource[0], - success: true, - error: false, - message: "Resource updated successfully", - status: HttpCode.OK - }); } catch (error) { logger.error(error); return next( @@ -237,3 +148,186 @@ export async function updateResource( ); } } + +async function updateHttpResource( + route: { + req: Request; + res: Response; + next: NextFunction; + }, + meta: { + resource: Resource; + org: Org; + } +) { + const { next, req, res } = route; + const { resource, org } = meta; + + const parsedBody = updateHttpResourceBodySchema.safeParse(req.body); + if (!parsedBody.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedBody.error).toString() + ) + ); + } + + const updateData = parsedBody.data; + + if (updateData.domainId) { + const [existingDomain] = await db + .select() + .from(orgDomains) + .where( + and( + eq(orgDomains.orgId, org.orgId), + eq(orgDomains.domainId, updateData.domainId) + ) + ) + .leftJoin(domains, eq(orgDomains.domainId, domains.domainId)); + + if (!existingDomain) { + return next( + createHttpError(HttpCode.NOT_FOUND, `Domain not found`) + ); + } + } + + const domainId = updateData.domainId || resource.domainId!; + const subdomain = updateData.subdomain || resource.subdomain; + + const [domain] = await db + .select() + .from(domains) + .where(eq(domains.domainId, domainId)); + + let fullDomain: string | null = null; + if (updateData.isBaseDomain) { + fullDomain = domain.baseDomain; + } else if (subdomain && domain) { + fullDomain = `${subdomain}.${domain}`; + } + + if (fullDomain) { + const [existingDomain] = await db + .select() + .from(resources) + .where(eq(resources.fullDomain, fullDomain)); + + if ( + existingDomain && + existingDomain.resourceId !== resource.resourceId + ) { + return next( + createHttpError( + HttpCode.CONFLICT, + "Resource with that domain already exists" + ) + ); + } + } + + const updatePayload = { + ...updateData, + fullDomain + }; + + const updatedResource = await db + .update(resources) + .set(updatePayload) + .where(eq(resources.resourceId, resource.resourceId)) + .returning(); + + if (updatedResource.length === 0) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + `Resource with ID ${resource.resourceId} not found` + ) + ); + } + + return response(res, { + data: updatedResource[0], + success: true, + error: false, + message: "HTTP resource updated successfully", + status: HttpCode.OK + }); +} + +async function updateRawResource( + route: { + req: Request; + res: Response; + next: NextFunction; + }, + meta: { + resource: Resource; + org: Org; + } +) { + const { next, req, res } = route; + const { resource } = meta; + + const parsedBody = updateRawResourceBodySchema.safeParse(req.body); + if (!parsedBody.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedBody.error).toString() + ) + ); + } + + const updateData = parsedBody.data; + + if (updateData.proxyPort) { + const proxyPort = updateData.proxyPort; + const existingResource = await db + .select() + .from(resources) + .where( + and( + eq(resources.protocol, resource.protocol), + eq(resources.proxyPort, proxyPort!) + ) + ); + + if ( + existingResource.length > 0 && + existingResource[0].resourceId !== resource.resourceId + ) { + return next( + createHttpError( + HttpCode.CONFLICT, + "Resource with that protocol and port already exists" + ) + ); + } + } + + const updatedResource = await db + .update(resources) + .set(updateData) + .where(eq(resources.resourceId, resource.resourceId)) + .returning(); + + if (updatedResource.length === 0) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + `Resource with ID ${resource.resourceId} not found` + ) + ); + } + + return response(res, { + data: updatedResource[0], + success: true, + error: false, + message: "Non-http Resource updated successfully", + status: HttpCode.OK + }); +}