This commit is contained in:
Owen Schwartz
2024-11-15 21:54:47 -05:00
102 changed files with 4021 additions and 2307 deletions

View File

@@ -35,8 +35,7 @@ export enum ActionsEnum {
listUsers = "listUsers",
listSiteRoles = "listSiteRoles",
listResourceRoles = "listResourceRoles",
addRoleSite = "addRoleSite",
addRoleResource = "addRoleResource",
setResourceRoles = "setResourceRoles",
removeRoleResource = "removeRoleResource",
removeRoleSite = "removeRoleSite",
// addRoleAction = "addRoleAction",

View File

@@ -25,7 +25,6 @@ export const sites = sqliteTable("sites", {
export const resources = sqliteTable("resources", {
resourceId: integer("resourceId").primaryKey({ autoIncrement: true }),
fullDomain: text("fullDomain", { length: 2048 }),
siteId: integer("siteId").references(() => sites.siteId, {
onDelete: "cascade",
}),
@@ -33,7 +32,7 @@ export const resources = sqliteTable("resources", {
onDelete: "cascade",
}),
name: text("name").notNull(),
subdomain: text("subdomain"),
subdomain: text("subdomain").notNull(),
ssl: integer("ssl", { mode: "boolean" }).notNull().default(false),
});

View File

@@ -1,10 +1,16 @@
import { Request, Response, NextFunction } from "express";
import { db } from "@server/db";
import { roles, userOrgs } from "@server/db/schema";
import { and, eq } from "drizzle-orm";
import { and, eq, inArray } from "drizzle-orm";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
import logger from "@server/logger";
import { z } from "zod";
import { fromError } from "zod-validation-error";
const verifyRoleAccessSchema = z.object({
roleIds: z.array(z.number().int().positive()).optional(),
});
export async function verifyRoleAccess(
req: Request,
@@ -12,7 +18,7 @@ export async function verifyRoleAccess(
next: NextFunction
) {
const userId = req.user?.userId;
const roleId = parseInt(
const singleRoleId = parseInt(
req.params.roleId || req.body.roleId || req.query.roleId
);
@@ -22,61 +28,61 @@ export async function verifyRoleAccess(
);
}
if (isNaN(roleId)) {
return next(createHttpError(HttpCode.BAD_REQUEST, "Invalid role ID"));
const parsedBody = verifyRoleAccessSchema.safeParse(req.body);
if (!parsedBody.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedBody.error).toString()
)
);
}
const { roleIds } = parsedBody.data;
const allRoleIds = roleIds || (isNaN(singleRoleId) ? [] : [singleRoleId]);
if (allRoleIds.length === 0) {
return next();
}
try {
const role = await db
const rolesData = await db
.select()
.from(roles)
.where(eq(roles.roleId, roleId))
.limit(1);
.where(inArray(roles.roleId, allRoleIds));
if (role.length === 0) {
if (rolesData.length !== allRoleIds.length) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`Role with ID ${roleId} not found`
"One or more roles not found"
)
);
}
if (!req.userOrg) {
// Check user access to each role's organization
for (const role of rolesData) {
const userOrgRole = await db
.select()
.from(userOrgs)
.where(
and(
eq(userOrgs.userId, userId),
eq(userOrgs.orgId, role[0].orgId!)
eq(userOrgs.orgId, role.orgId!)
)
)
.limit(1);
req.userOrg = userOrgRole[0];
}
if (!req.userOrg) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"User does not have access to this organization"
)
);
if (userOrgRole.length === 0) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
`User does not have access to organization for role ID ${role.roleId}`
)
);
}
}
if (req.userOrg.orgId !== role[0].orgId) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"Role does not belong to the organization"
)
);
}
req.userOrgRoleId = req.userOrg.roleId;
req.userOrgId = req.userOrg.orgId;
return next();
} catch (error) {
logger.error("Error verifying role access:", error);

View File

@@ -10,6 +10,7 @@ import {
import { and, eq, or } from "drizzle-orm";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
import logger from "@server/logger";
export async function verifySiteAccess(
req: Request,
@@ -28,6 +29,7 @@ export async function verifySiteAccess(
}
if (isNaN(siteId)) {
logger.debug(JSON.stringify(req.body));
return next(createHttpError(HttpCode.BAD_REQUEST, "Invalid site ID"));
}

View File

@@ -38,7 +38,7 @@ export async function verifyUserAccess(
req.userOrg = res[0];
}
if (req.userOrg) {
if (!req.userOrg) {
return next(
createHttpError(
HttpCode.FORBIDDEN,

View File

@@ -20,6 +20,7 @@ import {
verifyTargetAccess,
verifyRoleAccess,
verifyUserAccess,
verifyUserInRole,
} from "./auth";
import { verifyUserHasAction } from "./auth/verifyUserHasAction";
import { ActionsEnum } from "@server/auth/actions";
@@ -135,12 +136,13 @@ authenticated.post(
); // maybe make this /invite/create instead
authenticated.post("/invite/accept", user.acceptInvite);
// authenticated.get(
// "/resource/:resourceId/roles",
// verifyResourceAccess,
// verifyUserHasAction(ActionsEnum.listResourceRoles),
// resource.listResourceRoles
// );
authenticated.get(
"/resource/:resourceId/roles",
verifyResourceAccess,
verifyUserHasAction(ActionsEnum.listResourceRoles),
resource.listResourceRoles
);
authenticated.get(
"/resource/:resourceId",
verifyResourceAccess,
@@ -251,20 +253,15 @@ authenticated.post(
// verifyUserHasAction(ActionsEnum.listRoleSites),
// role.listRoleSites
// );
// authenticated.put(
// "/role/:roleId/resource",
// verifyRoleAccess,
// verifyUserInRole,
// verifyUserHasAction(ActionsEnum.addRoleResource),
// role.addRoleResource
// );
// authenticated.delete(
// "/role/:roleId/resource",
// verifyRoleAccess,
// verifyUserInRole,
// verifyUserHasAction(ActionsEnum.removeRoleResource),
// role.removeRoleResource
// );
authenticated.post(
"/resource/:resourceId/roles",
verifyResourceAccess,
verifyRoleAccess,
verifyUserHasAction(ActionsEnum.setResourceRoles),
role.addRoleResource
);
// authenticated.get(
// "/role/:roleId/resources",
// verifyRoleAccess,
@@ -370,7 +367,7 @@ authRouter.use(
authRouter.put("/signup", auth.signup);
authRouter.post("/login", auth.login);
authRouter.post("/logout", auth.logout);
authRouter.post('/newt/get-token', getToken);
authRouter.post("/newt/get-token", getToken);
authRouter.post("/2fa/enable", verifySessionUserMiddleware, auth.verifyTotp);
authRouter.post(

View File

@@ -12,11 +12,13 @@ import config from "@server/config";
import { fromError } from "zod-validation-error";
import { defaultRoleAllowedActions } from "../role";
const createOrgSchema = z.object({
orgId: z.string(),
name: z.string().min(1).max(255),
// domain: z.string().min(1).max(255).optional(),
});
const createOrgSchema = z
.object({
orgId: z.string(),
name: z.string().min(1).max(255),
// domain: z.string().min(1).max(255).optional(),
})
.strict();
const MAX_ORGS = 5;

View File

@@ -18,6 +18,7 @@ const updateOrgBodySchema = z
name: z.string().min(1).max(255).optional(),
domain: z.string().min(1).max(255).optional(),
})
.strict()
.refine((data) => Object.keys(data).length > 0, {
message: "At least one field must be provided for update",
});

View File

@@ -15,6 +15,7 @@ import createHttpError from "http-errors";
import { eq, and } from "drizzle-orm";
import stoi from "@server/utils/stoi";
import { fromError } from "zod-validation-error";
import { subdomainSchema } from "@server/schemas/subdomainSchema";
const createResourceParamsSchema = z.object({
siteId: z
@@ -25,10 +26,12 @@ const createResourceParamsSchema = z.object({
orgId: z.string(),
});
const createResourceSchema = z.object({
name: z.string().min(1).max(255),
subdomain: z.string().min(1).max(255).optional(),
});
const createResourceSchema = z
.object({
name: z.string().min(1).max(255),
subdomain: subdomainSchema,
})
.strict();
export type CreateResourceResponse = Resource;
@@ -85,12 +88,9 @@ export async function createResource(
);
}
const fullDomain = `${subdomain}.${org[0].domain}`;
const newResource = await db
.insert(resources)
.values({
fullDomain,
siteId,
orgId,
name,

View File

@@ -1,7 +1,7 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { db } from "@server/db";
import { resources } from "@server/db/schema";
import { Resource, resources } from "@server/db/schema";
import { eq } from "drizzle-orm";
import response from "@server/utils/response";
import HttpCode from "@server/types/HttpCode";
@@ -12,12 +12,7 @@ const getResourceSchema = z.object({
resourceId: z.string().transform(Number).pipe(z.number().int().positive()),
});
export type GetResourceResponse = {
resourceId: number;
siteId: number;
orgId: string;
name: string;
};
export type GetResourceResponse = Resource;
export async function getResource(
req: Request,
@@ -53,12 +48,7 @@ export async function getResource(
}
return response(res, {
data: {
resourceId: resource[0].resourceId,
siteId: resource[0].siteId,
orgId: resource[0].orgId,
name: resource[0].name,
},
data: resource[0],
success: true,
error: false,
message: "Resource retrieved successfully",

View File

@@ -13,6 +13,23 @@ const listResourceRolesSchema = z.object({
resourceId: z.string().transform(Number).pipe(z.number().int().positive()),
});
async function query(resourceId: number) {
return await db
.select({
roleId: roles.roleId,
name: roles.name,
description: roles.description,
isAdmin: roles.isAdmin,
})
.from(roleResources)
.innerJoin(roles, eq(roleResources.roleId, roles.roleId))
.where(eq(roleResources.resourceId, resourceId));
}
export type ListResourceRolesResponse = {
roles: NonNullable<Awaited<ReturnType<typeof query>>>;
};
export async function listResourceRoles(
req: Request,
res: Response,
@@ -31,19 +48,12 @@ export async function listResourceRoles(
const { resourceId } = parsedParams.data;
const resourceRolesList = await db
.select({
roleId: roles.roleId,
name: roles.name,
description: roles.description,
isAdmin: roles.isAdmin,
})
.from(roleResources)
.innerJoin(roles, eq(roleResources.roleId, roles.roleId))
.where(eq(roleResources.resourceId, resourceId));
const resourceRolesList = await query(resourceId);
return response(res, {
data: resourceRolesList,
return response<ListResourceRolesResponse>(res, {
data: {
roles: resourceRolesList,
},
success: true,
error: false,
message: "Resource roles retrieved successfully",

View File

@@ -0,0 +1,111 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { db } from "@server/db";
import { roleResources, roles } from "@server/db/schema";
import response from "@server/utils/response";
import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import logger from "@server/logger";
import { fromError } from "zod-validation-error";
import { eq, and, ne } from "drizzle-orm";
const setResourceRolesBodySchema = z.object({
roleIds: z.array(z.number().int().positive()),
});
const setResourceRolesParamsSchema = z.object({
resourceId: z.string().transform(Number).pipe(z.number().int().positive()),
});
export async function addRoleResource(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const parsedBody = setResourceRolesBodySchema.safeParse(req.body);
if (!parsedBody.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedBody.error).toString()
)
);
}
const { roleIds } = parsedBody.data;
const parsedParams = setResourceRolesParamsSchema.safeParse(req.params);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString()
)
);
}
const { resourceId } = parsedParams.data;
// get this org's admin role
const adminRole = await db
.select()
.from(roles)
.where(
and(
eq(roles.name, "Admin"),
eq(roles.orgId, req.userOrg!.orgId)
)
)
.limit(1);
if (!adminRole.length) {
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Admin role not found"
)
);
}
if (roleIds.includes(adminRole[0].roleId)) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Admin role cannot be assigned to resources"
)
);
}
await db.transaction(async (trx) => {
await trx.delete(roleResources).where(
and(
eq(roleResources.resourceId, resourceId),
ne(roleResources.roleId, adminRole[0].roleId) // delete all but the admin role
)
);
const newRoleResources = await Promise.all(
roleIds.map((roleId) =>
trx
.insert(roleResources)
.values({ roleId, resourceId })
.returning()
)
);
return response(res, {
data: {},
success: true,
error: false,
message: "Roles set for resource successfully",
status: HttpCode.CREATED,
});
});
} catch (error) {
logger.error(error);
return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
);
}
}

View File

@@ -1,13 +1,14 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { db } from "@server/db";
import { resources } from "@server/db/schema";
import { resources, sites } from "@server/db/schema";
import { eq } from "drizzle-orm";
import response from "@server/utils/response";
import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import logger from "@server/logger";
import { fromError } from "zod-validation-error";
import { subdomainSchema } from "@server/schemas/subdomainSchema";
const updateResourceParamsSchema = z.object({
resourceId: z.string().transform(Number).pipe(z.number().int().positive()),
@@ -16,8 +17,11 @@ const updateResourceParamsSchema = z.object({
const updateResourceBodySchema = z
.object({
name: z.string().min(1).max(255).optional(),
subdomain: z.string().min(1).max(255).optional(),
subdomain: subdomainSchema.optional(),
ssl: z.boolean().optional(),
// siteId: z.number(),
})
.strict()
.refine((data) => Object.keys(data).length > 0, {
message: "At least one field must be provided for update",
});

View File

@@ -1,70 +0,0 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { db } from "@server/db";
import { roleResources } from "@server/db/schema";
import response from "@server/utils/response";
import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import logger from "@server/logger";
import { fromError } from "zod-validation-error";
const addRoleResourceParamsSchema = z.object({
roleId: z.string().transform(Number).pipe(z.number().int().positive()),
});
const addRoleResourceSchema = z.object({
resourceId: z.string().transform(Number).pipe(z.number().int().positive()),
});
export async function addRoleResource(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const parsedBody = addRoleResourceSchema.safeParse(req.body);
if (!parsedBody.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedBody.error).toString()
)
);
}
const { resourceId } = parsedBody.data;
const parsedParams = addRoleResourceParamsSchema.safeParse(req.params);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString()
)
);
}
const { roleId } = parsedParams.data;
const newRoleResource = await db
.insert(roleResources)
.values({
roleId,
resourceId,
})
.returning();
return response(res, {
data: newRoleResource[0],
success: true,
error: false,
message: "Resource added to role successfully",
status: HttpCode.CREATED,
});
} catch (error) {
logger.error(error);
return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
);
}
}

View File

@@ -14,10 +14,12 @@ const createRoleParamsSchema = z.object({
orgId: z.string(),
});
const createRoleSchema = z.object({
name: z.string().min(1).max(255),
description: z.string().optional(),
});
const createRoleSchema = z
.object({
name: z.string().min(1).max(255),
description: z.string().optional(),
})
.strict();
export const defaultRoleAllowedActions: ActionsEnum[] = [
ActionsEnum.getOrg,

View File

@@ -1,7 +1,7 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { db } from "@server/db";
import { roles } from "@server/db/schema";
import { roles, userOrgs } from "@server/db/schema";
import { eq } from "drizzle-orm";
import response from "@server/utils/response";
import HttpCode from "@server/types/HttpCode";
@@ -13,6 +13,10 @@ const deleteRoleSchema = z.object({
roleId: z.string().transform(Number).pipe(z.number().int().positive()),
});
const deelteRoleBodySchema = z.object({
roleId: z.string().transform(Number).pipe(z.number().int().positive()),
});
export async function deleteRole(
req: Request,
res: Response,
@@ -29,7 +33,27 @@ export async function deleteRole(
);
}
const parsedBody = deelteRoleBodySchema.safeParse(req.body);
if (!parsedBody.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedBody.error).toString()
)
);
}
const { roleId } = parsedParams.data;
const { roleId: newRoleId } = parsedBody.data;
if (roleId === newRoleId) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
`Cannot delete a role and assign the same role`
)
);
}
const role = await db
.select()
@@ -55,20 +79,30 @@ export async function deleteRole(
);
}
const deletedRole = await db
.delete(roles)
.where(eq(roles.roleId, roleId))
.returning();
const newRole = await db
.select()
.from(roles)
.where(eq(roles.roleId, newRoleId))
.limit(1);
if (deletedRole.length === 0) {
if (newRole.length === 0) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`Role with ID ${roleId} not found`
`Role with ID ${newRoleId} not found`
)
);
}
// move all users from the userOrgs table with roleId to newRoleId
await db
.update(userOrgs)
.set({ roleId: newRoleId })
.where(eq(userOrgs.roleId, roleId));
// delete the old role
await db.delete(roles).where(eq(roles.roleId, roleId));
return response(res, {
data: null,
success: true,

View File

@@ -1,5 +1,5 @@
export * from "./addRoleAction";
export * from "./addRoleResource";
export * from "../resource/setResourceRoles";
export * from "./addRoleSite";
export * from "./createRole";
export * from "./deleteRole";

View File

@@ -18,6 +18,7 @@ const updateRoleBodySchema = z
name: z.string().min(1).max(255).optional(),
description: z.string().optional(),
})
.strict()
.refine((data) => Object.keys(data).length > 0, {
message: "At least one field must be provided for update",
});

View File

@@ -15,13 +15,15 @@ const createSiteParamsSchema = z.object({
orgId: z.string(),
});
const createSiteSchema = z.object({
name: z.string().min(1).max(255),
exitNodeId: z.number().int().positive(),
subdomain: z.string().min(1).max(255).optional(),
pubKey: z.string().optional(),
subnet: z.string(),
});
const createSiteSchema = z
.object({
name: z.string().min(1).max(255),
exitNodeId: z.number().int().positive(),
subdomain: z.string().min(1).max(255).optional(),
pubKey: z.string().optional(),
subnet: z.string(),
})
.strict();
export type CreateSiteResponse = {
name: string;
@@ -83,10 +85,7 @@ export async function createSite(
};
}
const [newSite] = await db
.insert(sites)
.values(payload)
.returning();
const [newSite] = await db.insert(sites).values(payload).returning();
const adminRole = await db
.select()

View File

@@ -76,7 +76,7 @@ export async function listSites(
return next(
createHttpError(
HttpCode.BAD_REQUEST,
parsedParams.error.errors.map((e) => e.message).join(", ")
fromError(parsedParams.error)
)
);
}

View File

@@ -76,6 +76,7 @@ export async function pickSiteDefaults(
status: HttpCode.OK,
});
} catch (error) {
throw error;
logger.error(error);
return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")

View File

@@ -23,6 +23,7 @@ const updateSiteBodySchema = z
megabytesIn: z.number().int().nonnegative().optional(),
megabytesOut: z.number().int().nonnegative().optional(),
})
.strict()
.refine((data) => Object.keys(data).length > 0, {
message: "At least one field must be provided for update",
});

View File

@@ -1,7 +1,7 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { db } from "@server/db";
import { resources, sites, targets } from "@server/db/schema";
import { resources, sites, Target, targets } from "@server/db/schema";
import response from "@server/utils/response";
import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
@@ -15,13 +15,17 @@ const createTargetParamsSchema = z.object({
resourceId: z.string().transform(Number).pipe(z.number().int().positive()),
});
const createTargetSchema = z.object({
ip: z.string().ip(),
method: z.string().min(1).max(10),
port: z.number().int().min(1).max(65535),
protocol: z.string().optional(),
enabled: z.boolean().default(true),
});
const createTargetSchema = z
.object({
ip: z.string().ip(),
method: z.string().min(1).max(10),
port: z.number().int().min(1).max(65535),
protocol: z.string().optional(),
enabled: z.boolean().default(true),
})
.strict();
export type CreateTargetResponse = Target;
export async function createTarget(
req: Request,
@@ -102,6 +106,7 @@ export async function createTarget(
.insert(targets)
.values({
resourceId,
protocol: "tcp", // hard code for now
...targetData,
})
.returning();
@@ -126,7 +131,7 @@ export async function createTarget(
allowedIps: targetIps.flat(),
});
return response(res, {
return response<CreateTargetResponse>(res, {
data: newTarget[0],
success: true,
error: false,

View File

@@ -15,12 +15,12 @@ const updateTargetParamsSchema = z.object({
const updateTargetBodySchema = z
.object({
// ip: z.string().ip().optional(), // for now we cant update the ip; you will have to delete
ip: z.string().ip().optional(), // for now we cant update the ip; you will have to delete
method: z.string().min(1).max(10).optional(),
port: z.number().int().min(1).max(65535).optional(),
protocol: z.string().optional(),
enabled: z.boolean().optional(),
})
.strict()
.refine((data) => Object.keys(data).length > 0, {
message: "At least one field must be provided for update",
});

View File

@@ -18,10 +18,15 @@ export async function traefikConfigProvider(
schema.resources,
eq(schema.targets.resourceId, schema.resources.resourceId)
)
.innerJoin(
schema.orgs,
eq(schema.resources.orgId, schema.orgs.orgId)
)
.where(
and(
eq(schema.targets.enabled, true),
isNotNull(schema.resources.fullDomain)
isNotNull(schema.resources.subdomain),
isNotNull(schema.orgs.domain)
)
);
@@ -60,15 +65,22 @@ export async function traefikConfigProvider(
for (const item of all) {
const target = item.targets;
const resource = item.resources;
const org = item.orgs;
const routerName = `${target.targetId}-router`;
const serviceName = `${target.targetId}-service`;
if (!resource.fullDomain) {
if (!resource || !resource.subdomain) {
continue;
}
const domainParts = resource.fullDomain.split(".");
if (!org || !org.domain) {
continue;
}
const fullDomain = `${resource.subdomain}.${org.domain}`;
const domainParts = fullDomain.split(".");
let wildCard;
if (domainParts.length <= 2) {
wildCard = `*.${domainParts.join(".")}`;
@@ -97,7 +109,7 @@ export async function traefikConfigProvider(
],
middlewares: resource.ssl ? [badgerMiddlewareName] : [],
service: serviceName,
rule: `Host(\`${resource.fullDomain}\`)`,
rule: `Host(\`${fullDomain}\`)`,
...(resource.ssl ? { tls } : {}),
};
@@ -107,7 +119,7 @@ export async function traefikConfigProvider(
entryPoints: [config.traefik.http_entrypoint],
middlewares: [redirectMiddlewareName],
service: serviceName,
rule: `Host(\`${resource.fullDomain}\`)`,
rule: `Host(\`${fullDomain}\`)`,
};
}

View File

@@ -89,7 +89,7 @@ export async function acceptInvite(
);
}
if (existingUser[0].email !== existingInvite[0].email) {
if (req.user && req.user.email !== existingInvite[0].email) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,

View File

@@ -8,10 +8,11 @@ import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import logger from "@server/logger";
import { fromError } from "zod-validation-error";
import stoi from "@server/utils/stoi";
const addUserRoleParamsSchema = z.object({
userId: z.string(),
roleId: z.number().int().positive(),
roleId: z.string().transform(stoi).pipe(z.number()),
});
export type AddUserRoleResponse = z.infer<typeof addUserRoleParamsSchema>;
@@ -22,17 +23,17 @@ export async function addUserRole(
next: NextFunction
): Promise<any> {
try {
const parsedBody = addUserRoleParamsSchema.safeParse(req.body);
if (!parsedBody.success) {
const parsedParams = addUserRoleParamsSchema.safeParse(req.params);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedBody.error).toString()
fromError(parsedParams.error).toString()
)
);
}
const { userId, roleId } = parsedBody.data;
const { userId, roleId } = parsedParams.data;
if (!req.userOrg) {
return next(

View File

@@ -0,0 +1,9 @@
import { z } from "zod";
export const subdomainSchema = z
.string()
.regex(
/^(?!:\/\/)([a-zA-Z0-9-_]+\.)*[a-zA-Z0-9-_]+$/,
"Invalid subdomain format"
)
.min(1, "Subdomain must be at least 1 character long");