add add/remove user/roles to siteResources/resources to integration api

This commit is contained in:
miloschwartz
2025-11-06 21:25:20 -08:00
parent bdb564823d
commit 76d54b2d0f
20 changed files with 1370 additions and 65 deletions

View File

@@ -11,6 +11,7 @@ import {
ApiKeyOrg, ApiKeyOrg,
RemoteExitNode, RemoteExitNode,
Session, Session,
SiteResource,
User, User,
UserOrg UserOrg
} from "@server/db"; } from "@server/db";
@@ -77,6 +78,7 @@ declare global {
userOrgId?: string; userOrgId?: string;
userOrgIds?: string[]; userOrgIds?: string[];
remoteExitNode?: RemoteExitNode; remoteExitNode?: RemoteExitNode;
siteResource?: SiteResource;
} }
} }
} }

View File

@@ -780,10 +780,6 @@ async function syncRoleResources(
.where(eq(roleResources.resourceId, resourceId)); .where(eq(roleResources.resourceId, resourceId));
for (const roleName of ssoRoles) { for (const roleName of ssoRoles) {
if (roleName === "Admin") {
continue; // never add admin access
}
const [role] = await trx const [role] = await trx
.select() .select()
.from(roles) .from(roles)
@@ -794,6 +790,10 @@ async function syncRoleResources(
throw new Error(`Role not found: ${roleName} in org ${orgId}`); throw new Error(`Role not found: ${roleName} in org ${orgId}`);
} }
if (role.isAdmin) {
continue; // never add admin access
}
const existingRoleResource = existingRoleResources.find( const existingRoleResource = existingRoleResources.find(
(rr) => rr.roleId === role.roleId (rr) => rr.roleId === role.roleId
); );

View File

@@ -4,7 +4,7 @@ import { getHostMeta } from "./hostMeta";
import logger from "@server/logger"; import logger from "@server/logger";
import { apiKeys, db, roles } from "@server/db"; import { apiKeys, db, roles } from "@server/db";
import { sites, users, orgs, resources, clients, idp } from "@server/db"; import { sites, users, orgs, resources, clients, idp } from "@server/db";
import { eq, count, notInArray } from "drizzle-orm"; import { eq, count, notInArray, and } from "drizzle-orm";
import { APP_VERSION } from "./consts"; import { APP_VERSION } from "./consts";
import crypto from "crypto"; import crypto from "crypto";
import { UserType } from "@server/types/UserTypes"; import { UserType } from "@server/types/UserTypes";
@@ -113,7 +113,12 @@ class TelemetryClient {
const [customRoles] = await db const [customRoles] = await db
.select({ count: count() }) .select({ count: count() })
.from(roles) .from(roles)
.where(notInArray(roles.name, ["Admin", "Member"])); .where(
and(
eq(roles.isAdmin, false),
notInArray(roles.name, ["Member"])
)
);
const adminUsers = await db const adminUsers = await db
.select({ email: users.email }) .select({ email: users.email })

View File

@@ -11,7 +11,9 @@ export async function verifyApiKeySetResourceUsers(
next: NextFunction next: NextFunction
) { ) {
const apiKey = req.apiKey; const apiKey = req.apiKey;
const userIds = req.body.userIds; const singleUserId = req.params.userId || req.body.userId || req.query.userId;
const { userIds } = req.body;
const allUserIds = userIds || (singleUserId ? [singleUserId] : []);
if (!apiKey) { if (!apiKey) {
return next( return next(
@@ -33,11 +35,7 @@ export async function verifyApiKeySetResourceUsers(
); );
} }
if (!userIds) { if (allUserIds.length === 0) {
return next(createHttpError(HttpCode.BAD_REQUEST, "Invalid user IDs"));
}
if (userIds.length === 0) {
return next(); return next();
} }
@@ -48,12 +46,12 @@ export async function verifyApiKeySetResourceUsers(
.from(userOrgs) .from(userOrgs)
.where( .where(
and( and(
inArray(userOrgs.userId, userIds), inArray(userOrgs.userId, allUserIds),
eq(userOrgs.orgId, orgId) eq(userOrgs.orgId, orgId)
) )
); );
if (userOrgsData.length !== userIds.length) { if (userOrgsData.length !== allUserIds.length) {
return next( return next(
createHttpError( createHttpError(
HttpCode.FORBIDDEN, HttpCode.FORBIDDEN,

View File

@@ -13,8 +13,6 @@ export async function verifyApiKeySiteResourceAccess(
try { try {
const apiKey = req.apiKey; const apiKey = req.apiKey;
const siteResourceId = parseInt(req.params.siteResourceId); const siteResourceId = parseInt(req.params.siteResourceId);
const siteId = parseInt(req.params.siteId);
const orgId = req.params.orgId;
if (!apiKey) { if (!apiKey) {
return next( return next(
@@ -22,11 +20,11 @@ export async function verifyApiKeySiteResourceAccess(
); );
} }
if (!siteResourceId || !siteId || !orgId) { if (!siteResourceId) {
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
"Missing required parameters" "Missing siteResourceId parameter"
) )
); );
} }
@@ -41,9 +39,7 @@ export async function verifyApiKeySiteResourceAccess(
.select() .select()
.from(siteResources) .from(siteResources)
.where(and( .where(and(
eq(siteResources.siteResourceId, siteResourceId), eq(siteResources.siteResourceId, siteResourceId)
eq(siteResources.siteId, siteId),
eq(siteResources.orgId, orgId)
)) ))
.limit(1); .limit(1);
@@ -64,11 +60,11 @@ export async function verifyApiKeySiteResourceAccess(
.where( .where(
and( and(
eq(apiKeyOrg.apiKeyId, apiKey.apiKeyId), eq(apiKeyOrg.apiKeyId, apiKey.apiKeyId),
eq(apiKeyOrg.orgId, orgId) eq(apiKeyOrg.orgId, siteResource.orgId)
) )
) )
.limit(1); .limit(1);
if (apiKeyOrgRes.length === 0) { if (apiKeyOrgRes.length === 0) {
return next( return next(
createHttpError( createHttpError(
@@ -77,12 +73,11 @@ export async function verifyApiKeySiteResourceAccess(
) )
); );
} }
req.apiKeyOrg = apiKeyOrgRes[0]; req.apiKeyOrg = apiKeyOrgRes[0];
} }
// Attach the siteResource to the request for use in the next middleware/route // Attach the siteResource to the request for use in the next middleware/route
// @ts-ignore - Extending Request type
req.siteResource = siteResource; req.siteResource = siteResource;
return next(); return next();

View File

@@ -95,7 +95,6 @@ export async function verifySiteResourceAccess(
req.userOrgId = siteResource.orgId; req.userOrgId = siteResource.orgId;
// Attach the siteResource to the request for use in the next middleware/route // Attach the siteResource to the request for use in the next middleware/route
// @ts-ignore - Extending Request type
req.siteResource = siteResource; req.siteResource = siteResource;
const roleResourceAccess = await db const roleResourceAccess = await db

View File

@@ -229,6 +229,42 @@ authenticated.post(
siteResource.setSiteResourceUsers siteResource.setSiteResourceUsers
); );
authenticated.post(
"/site-resource/:siteResourceId/roles/add",
verifyApiKeySiteResourceAccess,
verifyApiKeyRoleAccess,
verifyApiKeyHasAction(ActionsEnum.setResourceRoles),
logActionAudit(ActionsEnum.setResourceRoles),
siteResource.addRoleToSiteResource
);
authenticated.post(
"/site-resource/:siteResourceId/roles/remove",
verifyApiKeySiteResourceAccess,
verifyApiKeyRoleAccess,
verifyApiKeyHasAction(ActionsEnum.setResourceRoles),
logActionAudit(ActionsEnum.setResourceRoles),
siteResource.removeRoleFromSiteResource
);
authenticated.post(
"/site-resource/:siteResourceId/users/add",
verifyApiKeySiteResourceAccess,
verifyApiKeySetResourceUsers,
verifyApiKeyHasAction(ActionsEnum.setResourceUsers),
logActionAudit(ActionsEnum.setResourceUsers),
siteResource.addUserToSiteResource
);
authenticated.post(
"/site-resource/:siteResourceId/users/remove",
verifyApiKeySiteResourceAccess,
verifyApiKeySetResourceUsers,
verifyApiKeyHasAction(ActionsEnum.setResourceUsers),
logActionAudit(ActionsEnum.setResourceUsers),
siteResource.removeUserFromSiteResource
);
authenticated.put( authenticated.put(
"/org/:orgId/resource", "/org/:orgId/resource",
verifyApiKeyOrgAccess, verifyApiKeyOrgAccess,
@@ -444,6 +480,42 @@ authenticated.post(
resource.setResourceUsers resource.setResourceUsers
); );
authenticated.post(
"/resource/:resourceId/roles/add",
verifyApiKeyResourceAccess,
verifyApiKeyRoleAccess,
verifyApiKeyHasAction(ActionsEnum.setResourceRoles),
logActionAudit(ActionsEnum.setResourceRoles),
resource.addRoleToResource
);
authenticated.post(
"/resource/:resourceId/roles/remove",
verifyApiKeyResourceAccess,
verifyApiKeyRoleAccess,
verifyApiKeyHasAction(ActionsEnum.setResourceRoles),
logActionAudit(ActionsEnum.setResourceRoles),
resource.removeRoleFromResource
);
authenticated.post(
"/resource/:resourceId/users/add",
verifyApiKeyResourceAccess,
verifyApiKeySetResourceUsers,
verifyApiKeyHasAction(ActionsEnum.setResourceUsers),
logActionAudit(ActionsEnum.setResourceUsers),
resource.addUserToResource
);
authenticated.post(
"/resource/:resourceId/users/remove",
verifyApiKeyResourceAccess,
verifyApiKeySetResourceUsers,
verifyApiKeyHasAction(ActionsEnum.setResourceUsers),
logActionAudit(ActionsEnum.setResourceUsers),
resource.removeUserFromResource
);
authenticated.post( authenticated.post(
`/resource/:resourceId/password`, `/resource/:resourceId/password`,
verifyApiKeyResourceAccess, verifyApiKeyResourceAccess,

View File

@@ -132,7 +132,7 @@ export async function getOrgOverview(
numSites, numSites,
numUsers, numUsers,
numResources, numResources,
isAdmin: role.name === "Admin", isAdmin: role.isAdmin || false,
isOwner: req.userOrg?.isOwner || false isOwner: req.userOrg?.isOwner || false
}, },
success: true, success: true,

View File

@@ -0,0 +1,161 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { db, resources } from "@server/db";
import { roleResources, roles } from "@server/db";
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 { eq, and } from "drizzle-orm";
import { OpenAPITags, registry } from "@server/openApi";
const addRoleToResourceBodySchema = z
.object({
roleId: z.number().int().positive()
})
.strict();
const addRoleToResourceParamsSchema = z
.object({
resourceId: z
.string()
.transform(Number)
.pipe(z.number().int().positive())
})
.strict();
registry.registerPath({
method: "post",
path: "/resource/{resourceId}/roles/add",
description: "Add a single role to a resource.",
tags: [OpenAPITags.Resource, OpenAPITags.Role],
request: {
params: addRoleToResourceParamsSchema,
body: {
content: {
"application/json": {
schema: addRoleToResourceBodySchema
}
}
}
},
responses: {}
});
export async function addRoleToResource(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const parsedBody = addRoleToResourceBodySchema.safeParse(req.body);
if (!parsedBody.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedBody.error).toString()
)
);
}
const { roleId } = parsedBody.data;
const parsedParams = addRoleToResourceParamsSchema.safeParse(
req.params
);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString()
)
);
}
const { resourceId } = parsedParams.data;
// get the resource
const [resource] = await db
.select()
.from(resources)
.where(eq(resources.resourceId, resourceId))
.limit(1);
if (!resource) {
return next(
createHttpError(HttpCode.NOT_FOUND, "Resource not found")
);
}
// verify the role exists and belongs to the same org
const [role] = await db
.select()
.from(roles)
.where(
and(
eq(roles.roleId, roleId),
eq(roles.orgId, resource.orgId)
)
)
.limit(1);
if (!role) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
"Role not found or does not belong to the same organization"
)
);
}
// Check if the role is an admin role
if (role.isAdmin) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Admin role cannot be assigned to resources"
)
);
}
// Check if role already exists in resource
const existingEntry = await db
.select()
.from(roleResources)
.where(
and(
eq(roleResources.resourceId, resourceId),
eq(roleResources.roleId, roleId)
)
);
if (existingEntry.length > 0) {
return next(
createHttpError(
HttpCode.CONFLICT,
"Role already assigned to resource"
)
);
}
await db.insert(roleResources).values({
roleId,
resourceId
});
return response(res, {
data: {},
success: true,
error: false,
message: "Role added to resource successfully",
status: HttpCode.CREATED
});
} catch (error) {
logger.error(error);
return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
);
}
}

View File

@@ -0,0 +1,130 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { db, resources } from "@server/db";
import { userResources } from "@server/db";
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 { eq, and } from "drizzle-orm";
import { OpenAPITags, registry } from "@server/openApi";
const addUserToResourceBodySchema = z
.object({
userId: z.string()
})
.strict();
const addUserToResourceParamsSchema = z
.object({
resourceId: z
.string()
.transform(Number)
.pipe(z.number().int().positive())
})
.strict();
registry.registerPath({
method: "post",
path: "/resource/{resourceId}/users/add",
description: "Add a single user to a resource.",
tags: [OpenAPITags.Resource, OpenAPITags.User],
request: {
params: addUserToResourceParamsSchema,
body: {
content: {
"application/json": {
schema: addUserToResourceBodySchema
}
}
}
},
responses: {}
});
export async function addUserToResource(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const parsedBody = addUserToResourceBodySchema.safeParse(req.body);
if (!parsedBody.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedBody.error).toString()
)
);
}
const { userId } = parsedBody.data;
const parsedParams = addUserToResourceParamsSchema.safeParse(
req.params
);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString()
)
);
}
const { resourceId } = parsedParams.data;
// get the resource
const [resource] = await db
.select()
.from(resources)
.where(eq(resources.resourceId, resourceId))
.limit(1);
if (!resource) {
return next(
createHttpError(HttpCode.NOT_FOUND, "Resource not found")
);
}
// Check if user already exists in resource
const existingEntry = await db
.select()
.from(userResources)
.where(
and(
eq(userResources.resourceId, resourceId),
eq(userResources.userId, userId)
)
);
if (existingEntry.length > 0) {
return next(
createHttpError(
HttpCode.CONFLICT,
"User already assigned to resource"
)
);
}
await db.insert(userResources).values({
userId,
resourceId
});
return response(res, {
data: {},
success: true,
error: false,
message: "User added to resource successfully",
status: HttpCode.CREATED
});
} catch (error) {
logger.error(error);
return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
);
}
}

View File

@@ -25,3 +25,7 @@ export * from "./getUserResources";
export * from "./setResourceHeaderAuth"; export * from "./setResourceHeaderAuth";
export * from "./addEmailToResourceWhitelist"; export * from "./addEmailToResourceWhitelist";
export * from "./removeEmailFromResourceWhitelist"; export * from "./removeEmailFromResourceWhitelist";
export * from "./addRoleToResource";
export * from "./removeRoleFromResource";
export * from "./addUserToResource";
export * from "./removeUserFromResource";

View File

@@ -0,0 +1,166 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { db, resources } from "@server/db";
import { roleResources, roles } from "@server/db";
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 { eq, and } from "drizzle-orm";
import { OpenAPITags, registry } from "@server/openApi";
const removeRoleFromResourceBodySchema = z
.object({
roleId: z.number().int().positive()
})
.strict();
const removeRoleFromResourceParamsSchema = z
.object({
resourceId: z
.string()
.transform(Number)
.pipe(z.number().int().positive())
})
.strict();
registry.registerPath({
method: "post",
path: "/resource/{resourceId}/roles/remove",
description: "Remove a single role from a resource.",
tags: [OpenAPITags.Resource, OpenAPITags.Role],
request: {
params: removeRoleFromResourceParamsSchema,
body: {
content: {
"application/json": {
schema: removeRoleFromResourceBodySchema
}
}
}
},
responses: {}
});
export async function removeRoleFromResource(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const parsedBody = removeRoleFromResourceBodySchema.safeParse(
req.body
);
if (!parsedBody.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedBody.error).toString()
)
);
}
const { roleId } = parsedBody.data;
const parsedParams = removeRoleFromResourceParamsSchema.safeParse(
req.params
);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString()
)
);
}
const { resourceId } = parsedParams.data;
// get the resource
const [resource] = await db
.select()
.from(resources)
.where(eq(resources.resourceId, resourceId))
.limit(1);
if (!resource) {
return next(
createHttpError(HttpCode.NOT_FOUND, "Resource not found")
);
}
// Check if the role is an admin role
const [roleToCheck] = await db
.select()
.from(roles)
.where(
and(
eq(roles.roleId, roleId),
eq(roles.orgId, resource.orgId)
)
)
.limit(1);
if (!roleToCheck) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
"Role not found or does not belong to the same organization"
)
);
}
if (roleToCheck.isAdmin) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Admin role cannot be removed from resources"
)
);
}
// Check if role exists in resource
const existingEntry = await db
.select()
.from(roleResources)
.where(
and(
eq(roleResources.resourceId, resourceId),
eq(roleResources.roleId, roleId)
)
);
if (existingEntry.length === 0) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
"Role not found in resource"
)
);
}
await db
.delete(roleResources)
.where(
and(
eq(roleResources.resourceId, resourceId),
eq(roleResources.roleId, roleId)
)
);
return response(res, {
data: {},
success: true,
error: false,
message: "Role removed from resource successfully",
status: HttpCode.OK
});
} catch (error) {
logger.error(error);
return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
);
}
}

View File

@@ -0,0 +1,136 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { db, resources } from "@server/db";
import { userResources } from "@server/db";
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 { eq, and } from "drizzle-orm";
import { OpenAPITags, registry } from "@server/openApi";
const removeUserFromResourceBodySchema = z
.object({
userId: z.string()
})
.strict();
const removeUserFromResourceParamsSchema = z
.object({
resourceId: z
.string()
.transform(Number)
.pipe(z.number().int().positive())
})
.strict();
registry.registerPath({
method: "post",
path: "/resource/{resourceId}/users/remove",
description: "Remove a single user from a resource.",
tags: [OpenAPITags.Resource, OpenAPITags.User],
request: {
params: removeUserFromResourceParamsSchema,
body: {
content: {
"application/json": {
schema: removeUserFromResourceBodySchema
}
}
}
},
responses: {}
});
export async function removeUserFromResource(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const parsedBody = removeUserFromResourceBodySchema.safeParse(
req.body
);
if (!parsedBody.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedBody.error).toString()
)
);
}
const { userId } = parsedBody.data;
const parsedParams = removeUserFromResourceParamsSchema.safeParse(
req.params
);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString()
)
);
}
const { resourceId } = parsedParams.data;
// get the resource
const [resource] = await db
.select()
.from(resources)
.where(eq(resources.resourceId, resourceId))
.limit(1);
if (!resource) {
return next(
createHttpError(HttpCode.NOT_FOUND, "Resource not found")
);
}
// Check if user exists in resource
const existingEntry = await db
.select()
.from(userResources)
.where(
and(
eq(userResources.resourceId, resourceId),
eq(userResources.userId, userId)
)
);
if (existingEntry.length === 0) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
"User not found in resource"
)
);
}
await db
.delete(userResources)
.where(
and(
eq(userResources.resourceId, resourceId),
eq(userResources.userId, userId)
)
);
return response(res, {
data: {},
success: true,
error: false,
message: "User removed from resource successfully",
status: HttpCode.OK
});
} catch (error) {
logger.error(error);
return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
);
}
}

View File

@@ -7,7 +7,7 @@ import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import logger from "@server/logger"; import logger from "@server/logger";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import { eq, and, ne } from "drizzle-orm"; import { eq, and, ne, inArray } from "drizzle-orm";
import { OpenAPITags, registry } from "@server/openApi"; import { OpenAPITags, registry } from "@server/openApi";
const setResourceRolesBodySchema = z const setResourceRolesBodySchema = z
@@ -90,28 +90,20 @@ export async function setResourceRoles(
); );
} }
// get this org's admin role // Check if any of the roleIds are admin roles
const adminRole = await db const rolesToCheck = await db
.select() .select()
.from(roles) .from(roles)
.where( .where(
and( and(
eq(roles.name, "Admin"), inArray(roles.roleId, roleIds),
eq(roles.orgId, resource.orgId) eq(roles.orgId, resource.orgId)
) )
)
.limit(1);
if (!adminRole.length) {
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Admin role not found"
)
); );
}
if (roleIds.includes(adminRole[0].roleId)) { const hasAdminRole = rolesToCheck.some((role) => role.isAdmin);
if (hasAdminRole) {
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
@@ -120,13 +112,31 @@ export async function setResourceRoles(
); );
} }
await db.transaction(async (trx) => { // Get all admin role IDs for this org to exclude from deletion
await trx.delete(roleResources).where( const adminRoles = await db
.select()
.from(roles)
.where(
and( and(
eq(roleResources.resourceId, resourceId), eq(roles.isAdmin, true),
ne(roleResources.roleId, adminRole[0].roleId) // delete all but the admin role eq(roles.orgId, resource.orgId)
) )
); );
const adminRoleIds = adminRoles.map((role) => role.roleId);
await db.transaction(async (trx) => {
if (adminRoleIds.length > 0) {
await trx.delete(roleResources).where(
and(
eq(roleResources.resourceId, resourceId),
ne(roleResources.roleId, adminRoleIds[0]) // delete all but the admin role
)
);
} else {
await trx.delete(roleResources).where(
eq(roleResources.resourceId, resourceId)
);
}
const newRoleResources = await Promise.all( const newRoleResources = await Promise.all(
roleIds.map((roleId) => roleIds.map((roleId) =>

View File

@@ -0,0 +1,166 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { db, siteResources } from "@server/db";
import { roleSiteResources, roles } from "@server/db";
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 { eq, and } from "drizzle-orm";
import { OpenAPITags, registry } from "@server/openApi";
import { rebuildSiteClientAssociations } from "@server/lib/rebuildSiteClientAssociations";
const addRoleToSiteResourceBodySchema = z
.object({
roleId: z.number().int().positive()
})
.strict();
const addRoleToSiteResourceParamsSchema = z
.object({
siteResourceId: z
.string()
.transform(Number)
.pipe(z.number().int().positive())
})
.strict();
registry.registerPath({
method: "post",
path: "/site-resource/{siteResourceId}/roles/add",
description: "Add a single role to a site resource.",
tags: [OpenAPITags.Resource, OpenAPITags.Role],
request: {
params: addRoleToSiteResourceParamsSchema,
body: {
content: {
"application/json": {
schema: addRoleToSiteResourceBodySchema
}
}
}
},
responses: {}
});
export async function addRoleToSiteResource(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const parsedBody = addRoleToSiteResourceBodySchema.safeParse(req.body);
if (!parsedBody.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedBody.error).toString()
)
);
}
const { roleId } = parsedBody.data;
const parsedParams = addRoleToSiteResourceParamsSchema.safeParse(
req.params
);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString()
)
);
}
const { siteResourceId } = parsedParams.data;
// get the site resource
const [siteResource] = await db
.select()
.from(siteResources)
.where(eq(siteResources.siteResourceId, siteResourceId))
.limit(1);
if (!siteResource) {
return next(
createHttpError(HttpCode.NOT_FOUND, "Site resource not found")
);
}
// verify the role exists and belongs to the same org
const [role] = await db
.select()
.from(roles)
.where(
and(
eq(roles.roleId, roleId),
eq(roles.orgId, siteResource.orgId)
)
)
.limit(1);
if (!role) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
"Role not found or does not belong to the same organization"
)
);
}
// Check if the role is an admin role
if (role.isAdmin) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Admin role cannot be assigned to site resources"
)
);
}
// Check if role already exists in site resource
const existingEntry = await db
.select()
.from(roleSiteResources)
.where(
and(
eq(roleSiteResources.siteResourceId, siteResourceId),
eq(roleSiteResources.roleId, roleId)
)
);
if (existingEntry.length > 0) {
return next(
createHttpError(
HttpCode.CONFLICT,
"Role already assigned to site resource"
)
);
}
await db.transaction(async (trx) => {
await trx.insert(roleSiteResources).values({
roleId,
siteResourceId
});
await rebuildSiteClientAssociations(siteResource, trx);
});
return response(res, {
data: {},
success: true,
error: false,
message: "Role added to site resource successfully",
status: HttpCode.CREATED
});
} catch (error) {
logger.error(error);
return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
);
}
}

View File

@@ -0,0 +1,135 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { db, siteResources } from "@server/db";
import { userSiteResources } from "@server/db";
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 { eq, and } from "drizzle-orm";
import { OpenAPITags, registry } from "@server/openApi";
import { rebuildSiteClientAssociations } from "@server/lib/rebuildSiteClientAssociations";
const addUserToSiteResourceBodySchema = z
.object({
userId: z.string()
})
.strict();
const addUserToSiteResourceParamsSchema = z
.object({
siteResourceId: z
.string()
.transform(Number)
.pipe(z.number().int().positive())
})
.strict();
registry.registerPath({
method: "post",
path: "/site-resource/{siteResourceId}/users/add",
description: "Add a single user to a site resource.",
tags: [OpenAPITags.Resource, OpenAPITags.User],
request: {
params: addUserToSiteResourceParamsSchema,
body: {
content: {
"application/json": {
schema: addUserToSiteResourceBodySchema
}
}
}
},
responses: {}
});
export async function addUserToSiteResource(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const parsedBody = addUserToSiteResourceBodySchema.safeParse(req.body);
if (!parsedBody.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedBody.error).toString()
)
);
}
const { userId } = parsedBody.data;
const parsedParams = addUserToSiteResourceParamsSchema.safeParse(
req.params
);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString()
)
);
}
const { siteResourceId } = parsedParams.data;
// get the site resource
const [siteResource] = await db
.select()
.from(siteResources)
.where(eq(siteResources.siteResourceId, siteResourceId))
.limit(1);
if (!siteResource) {
return next(
createHttpError(HttpCode.NOT_FOUND, "Site resource not found")
);
}
// Check if user already exists in site resource
const existingEntry = await db
.select()
.from(userSiteResources)
.where(
and(
eq(userSiteResources.siteResourceId, siteResourceId),
eq(userSiteResources.userId, userId)
)
);
if (existingEntry.length > 0) {
return next(
createHttpError(
HttpCode.CONFLICT,
"User already assigned to site resource"
)
);
}
await db.transaction(async (trx) => {
await trx.insert(userSiteResources).values({
userId,
siteResourceId
});
await rebuildSiteClientAssociations(siteResource, trx);
});
return response(res, {
data: {},
success: true,
error: false,
message: "User added to site resource successfully",
status: HttpCode.CREATED
});
} catch (error) {
logger.error(error);
return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
);
}
}

View File

@@ -8,3 +8,7 @@ export * from "./listSiteResourceRoles";
export * from "./listSiteResourceUsers"; export * from "./listSiteResourceUsers";
export * from "./setSiteResourceRoles"; export * from "./setSiteResourceRoles";
export * from "./setSiteResourceUsers"; export * from "./setSiteResourceUsers";
export * from "./addRoleToSiteResource";
export * from "./removeRoleFromSiteResource";
export * from "./addUserToSiteResource";
export * from "./removeUserFromSiteResource";

View File

@@ -0,0 +1,171 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { db, siteResources } from "@server/db";
import { roleSiteResources, roles } from "@server/db";
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 { eq, and } from "drizzle-orm";
import { OpenAPITags, registry } from "@server/openApi";
import { rebuildSiteClientAssociations } from "@server/lib/rebuildSiteClientAssociations";
const removeRoleFromSiteResourceBodySchema = z
.object({
roleId: z.number().int().positive()
})
.strict();
const removeRoleFromSiteResourceParamsSchema = z
.object({
siteResourceId: z
.string()
.transform(Number)
.pipe(z.number().int().positive())
})
.strict();
registry.registerPath({
method: "post",
path: "/site-resource/{siteResourceId}/roles/remove",
description: "Remove a single role from a site resource.",
tags: [OpenAPITags.Resource, OpenAPITags.Role],
request: {
params: removeRoleFromSiteResourceParamsSchema,
body: {
content: {
"application/json": {
schema: removeRoleFromSiteResourceBodySchema
}
}
}
},
responses: {}
});
export async function removeRoleFromSiteResource(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const parsedBody = removeRoleFromSiteResourceBodySchema.safeParse(
req.body
);
if (!parsedBody.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedBody.error).toString()
)
);
}
const { roleId } = parsedBody.data;
const parsedParams = removeRoleFromSiteResourceParamsSchema.safeParse(
req.params
);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString()
)
);
}
const { siteResourceId } = parsedParams.data;
// get the site resource
const [siteResource] = await db
.select()
.from(siteResources)
.where(eq(siteResources.siteResourceId, siteResourceId))
.limit(1);
if (!siteResource) {
return next(
createHttpError(HttpCode.NOT_FOUND, "Site resource not found")
);
}
// Check if the role is an admin role
const [roleToCheck] = await db
.select()
.from(roles)
.where(
and(
eq(roles.roleId, roleId),
eq(roles.orgId, siteResource.orgId)
)
)
.limit(1);
if (!roleToCheck) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
"Role not found or does not belong to the same organization"
)
);
}
if (roleToCheck.isAdmin) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Admin role cannot be removed from site resources"
)
);
}
// Check if role exists in site resource
const existingEntry = await db
.select()
.from(roleSiteResources)
.where(
and(
eq(roleSiteResources.siteResourceId, siteResourceId),
eq(roleSiteResources.roleId, roleId)
)
);
if (existingEntry.length === 0) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
"Role not found in site resource"
)
);
}
await db.transaction(async (trx) => {
await trx
.delete(roleSiteResources)
.where(
and(
eq(roleSiteResources.siteResourceId, siteResourceId),
eq(roleSiteResources.roleId, roleId)
)
);
await rebuildSiteClientAssociations(siteResource, trx);
});
return response(res, {
data: {},
success: true,
error: false,
message: "Role removed from site resource successfully",
status: HttpCode.OK
});
} catch (error) {
logger.error(error);
return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
);
}
}

View File

@@ -0,0 +1,141 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { db, siteResources } from "@server/db";
import { userSiteResources } from "@server/db";
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 { eq, and } from "drizzle-orm";
import { OpenAPITags, registry } from "@server/openApi";
import { rebuildSiteClientAssociations } from "@server/lib/rebuildSiteClientAssociations";
const removeUserFromSiteResourceBodySchema = z
.object({
userId: z.string()
})
.strict();
const removeUserFromSiteResourceParamsSchema = z
.object({
siteResourceId: z
.string()
.transform(Number)
.pipe(z.number().int().positive())
})
.strict();
registry.registerPath({
method: "post",
path: "/site-resource/{siteResourceId}/users/remove",
description: "Remove a single user from a site resource.",
tags: [OpenAPITags.Resource, OpenAPITags.User],
request: {
params: removeUserFromSiteResourceParamsSchema,
body: {
content: {
"application/json": {
schema: removeUserFromSiteResourceBodySchema
}
}
}
},
responses: {}
});
export async function removeUserFromSiteResource(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const parsedBody = removeUserFromSiteResourceBodySchema.safeParse(
req.body
);
if (!parsedBody.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedBody.error).toString()
)
);
}
const { userId } = parsedBody.data;
const parsedParams = removeUserFromSiteResourceParamsSchema.safeParse(
req.params
);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString()
)
);
}
const { siteResourceId } = parsedParams.data;
// get the site resource
const [siteResource] = await db
.select()
.from(siteResources)
.where(eq(siteResources.siteResourceId, siteResourceId))
.limit(1);
if (!siteResource) {
return next(
createHttpError(HttpCode.NOT_FOUND, "Site resource not found")
);
}
// Check if user exists in site resource
const existingEntry = await db
.select()
.from(userSiteResources)
.where(
and(
eq(userSiteResources.siteResourceId, siteResourceId),
eq(userSiteResources.userId, userId)
)
);
if (existingEntry.length === 0) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
"User not found in site resource"
)
);
}
await db.transaction(async (trx) => {
await trx
.delete(userSiteResources)
.where(
and(
eq(userSiteResources.siteResourceId, siteResourceId),
eq(userSiteResources.userId, userId)
)
);
await rebuildSiteClientAssociations(siteResource, trx);
});
return response(res, {
data: {},
success: true,
error: false,
message: "User removed from site resource successfully",
status: HttpCode.OK
});
} catch (error) {
logger.error(error);
return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
);
}
}

View File

@@ -7,7 +7,7 @@ import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import logger from "@server/logger"; import logger from "@server/logger";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import { eq, and, ne } from "drizzle-orm"; import { eq, and, ne, inArray } from "drizzle-orm";
import { OpenAPITags, registry } from "@server/openApi"; import { OpenAPITags, registry } from "@server/openApi";
import { rebuildSiteClientAssociations } from "@server/lib/rebuildSiteClientAssociations"; import { rebuildSiteClientAssociations } from "@server/lib/rebuildSiteClientAssociations";
@@ -93,28 +93,20 @@ export async function setSiteResourceRoles(
); );
} }
// get this org's admin role // Check if any of the roleIds are admin roles
const adminRole = await db const rolesToCheck = await db
.select() .select()
.from(roles) .from(roles)
.where( .where(
and( and(
eq(roles.name, "Admin"), inArray(roles.roleId, roleIds),
eq(roles.orgId, siteResource.orgId) eq(roles.orgId, siteResource.orgId)
) )
)
.limit(1);
if (!adminRole.length) {
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Admin role not found"
)
); );
}
if (roleIds.includes(adminRole[0].roleId)) { const hasAdminRole = rolesToCheck.some((role) => role.isAdmin);
if (hasAdminRole) {
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
@@ -123,13 +115,31 @@ export async function setSiteResourceRoles(
); );
} }
await db.transaction(async (trx) => { // Get all admin role IDs for this org to exclude from deletion
await trx.delete(roleSiteResources).where( const adminRoles = await db
.select()
.from(roles)
.where(
and( and(
eq(roleSiteResources.siteResourceId, siteResourceId), eq(roles.isAdmin, true),
ne(roleSiteResources.roleId, adminRole[0].roleId) // delete all but the admin role eq(roles.orgId, siteResource.orgId)
) )
); );
const adminRoleIds = adminRoles.map((role) => role.roleId);
await db.transaction(async (trx) => {
if (adminRoleIds.length > 0) {
await trx.delete(roleSiteResources).where(
and(
eq(roleSiteResources.siteResourceId, siteResourceId),
ne(roleSiteResources.roleId, adminRoleIds[0]) // delete all but the admin role
)
);
} else {
await trx.delete(roleSiteResources).where(
eq(roleSiteResources.siteResourceId, siteResourceId)
);
}
await Promise.all( await Promise.all(
roleIds.map((roleId) => roleIds.map((roleId) =>