123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261 |
- import { SqliteError } from "better-sqlite3";
- import { Request, Response, NextFunction } from "express";
- import { z } from "zod";
- import { db } from "@server/db";
- import {
- orgs,
- Resource,
- resources,
- roleResources,
- roles,
- userResources
- } from "@server/db/schema";
- 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";
- import config from "@server/lib/config";
- const createResourceParamsSchema = z
- .object({
- siteId: z.string().transform(stoi).pipe(z.number().int().positive()),
- orgId: z.string()
- })
- .strict();
- const createResourceSchema = z
- .object({
- subdomain: z.string().optional(),
- name: z.string().min(1).max(255),
- siteId: z.number(),
- http: z.boolean(),
- protocol: z.string(),
- proxyPort: z.number().optional(),
- isBaseDomain: z.boolean().optional()
- })
- .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"
- // }
- // )
- .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 type CreateResourceResponse = Resource;
- export async function createResource(
- req: Request,
- res: Response,
- next: NextFunction
- ): Promise<any> {
- 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) {
- return next(
- createHttpError(
- HttpCode.BAD_REQUEST,
- fromError(parsedParams.error).toString()
- )
- );
- }
- const { siteId, orgId } = parsedParams.data;
- if (!req.userOrgRoleId) {
- return next(
- createHttpError(HttpCode.FORBIDDEN, "User does not have a role")
- );
- }
- // get the org
- const org = await db
- .select()
- .from(orgs)
- .where(eq(orgs.orgId, orgId))
- .limit(1);
- if (org.length === 0) {
- return next(
- createHttpError(
- HttpCode.NOT_FOUND,
- `Organization with ID ${orgId} not found`
- )
- );
- }
- let fullDomain = "";
- if (isBaseDomain) {
- fullDomain = org[0].domain;
- } else {
- fullDomain = `${subdomain}.${org[0].domain}`;
- }
- // 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!)
- )
- );
- if (existingResource.length > 0) {
- return next(
- createHttpError(
- HttpCode.CONFLICT,
- "Resource with that protocol and port already exists"
- )
- );
- }
- } 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"
- )
- );
- }
- }
- 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<CreateResourceResponse>(res, {
- data: newResource[0],
- success: true,
- error: false,
- message: "Resource created successfully",
- status: HttpCode.CREATED
- });
- });
- } catch (error) {
- logger.error(error);
- return next(
- createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
- );
- }
- }
|