mirror of
https://github.com/fosrl/pangolin.git
synced 2025-12-14 20:16:38 +00:00
add add/remove user/roles to siteResources/resources to integration api
This commit is contained in:
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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 })
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
161
server/routers/resource/addRoleToResource.ts
Normal file
161
server/routers/resource/addRoleToResource.ts
Normal 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")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
130
server/routers/resource/addUserToResource.ts
Normal file
130
server/routers/resource/addUserToResource.ts
Normal 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")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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";
|
||||||
|
|||||||
166
server/routers/resource/removeRoleFromResource.ts
Normal file
166
server/routers/resource/removeRoleFromResource.ts
Normal 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")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
136
server/routers/resource/removeUserFromResource.ts
Normal file
136
server/routers/resource/removeUserFromResource.ts
Normal 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")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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) =>
|
||||||
|
|||||||
166
server/routers/siteResource/addRoleToSiteResource.ts
Normal file
166
server/routers/siteResource/addRoleToSiteResource.ts
Normal 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")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
135
server/routers/siteResource/addUserToSiteResource.ts
Normal file
135
server/routers/siteResource/addUserToSiteResource.ts
Normal 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")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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";
|
||||||
|
|||||||
171
server/routers/siteResource/removeRoleFromSiteResource.ts
Normal file
171
server/routers/siteResource/removeRoleFromSiteResource.ts
Normal 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")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
141
server/routers/siteResource/removeUserFromSiteResource.ts
Normal file
141
server/routers/siteResource/removeUserFromSiteResource.ts
Normal 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")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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) =>
|
||||||
|
|||||||
Reference in New Issue
Block a user