mirror of
https://github.com/fosrl/pangolin.git
synced 2026-03-05 18:26:40 +00:00
add users and roles to site resources
This commit is contained in:
@@ -211,6 +211,24 @@ export const siteResources = pgTable("siteResources", {
|
|||||||
enabled: boolean("enabled").notNull().default(true)
|
enabled: boolean("enabled").notNull().default(true)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const roleSiteResources = pgTable("roleSiteResources", {
|
||||||
|
roleId: integer("roleId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => roles.roleId, { onDelete: "cascade" }),
|
||||||
|
siteResourceId: integer("siteResourceId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => siteResources.siteResourceId, { onDelete: "cascade" })
|
||||||
|
});
|
||||||
|
|
||||||
|
export const userSiteResources = pgTable("userSiteResources", {
|
||||||
|
userId: varchar("userId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => users.userId, { onDelete: "cascade" }),
|
||||||
|
siteResourceId: integer("siteResourceId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => siteResources.siteResourceId, { onDelete: "cascade" })
|
||||||
|
});
|
||||||
|
|
||||||
export const users = pgTable("user", {
|
export const users = pgTable("user", {
|
||||||
userId: varchar("id").primaryKey(),
|
userId: varchar("id").primaryKey(),
|
||||||
email: varchar("email"),
|
email: varchar("email"),
|
||||||
|
|||||||
@@ -232,6 +232,24 @@ export const siteResources = sqliteTable("siteResources", {
|
|||||||
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true)
|
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const roleSiteResources = sqliteTable("roleSiteResources", {
|
||||||
|
roleId: integer("roleId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => roles.roleId, { onDelete: "cascade" }),
|
||||||
|
siteResourceId: integer("siteResourceId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => siteResources.siteResourceId, { onDelete: "cascade" })
|
||||||
|
});
|
||||||
|
|
||||||
|
export const userSiteResources = sqliteTable("userSiteResources", {
|
||||||
|
userId: text("userId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => users.userId, { onDelete: "cascade" }),
|
||||||
|
siteResourceId: integer("siteResourceId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => siteResources.siteResourceId, { onDelete: "cascade" })
|
||||||
|
});
|
||||||
|
|
||||||
export const users = sqliteTable("user", {
|
export const users = sqliteTable("user", {
|
||||||
userId: text("id").primaryKey(),
|
userId: text("id").primaryKey(),
|
||||||
email: text("email"),
|
email: text("email"),
|
||||||
@@ -356,7 +374,8 @@ export const olms = sqliteTable("olms", {
|
|||||||
// we will switch this depending on the current org it wants to connect to
|
// we will switch this depending on the current org it wants to connect to
|
||||||
onDelete: "set null"
|
onDelete: "set null"
|
||||||
}),
|
}),
|
||||||
userId: text("userId").references(() => users.userId, { // optionally tied to a user and in this case delete when the user deletes
|
userId: text("userId").references(() => users.userId, {
|
||||||
|
// optionally tied to a user and in this case delete when the user deletes
|
||||||
onDelete: "cascade"
|
onDelete: "cascade"
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
@@ -822,7 +841,9 @@ export const deviceWebAuthCodes = sqliteTable("deviceWebAuthCodes", {
|
|||||||
expiresAt: integer("expiresAt").notNull(),
|
expiresAt: integer("expiresAt").notNull(),
|
||||||
createdAt: integer("createdAt").notNull(),
|
createdAt: integer("createdAt").notNull(),
|
||||||
verified: integer("verified", { mode: "boolean" }).notNull().default(false),
|
verified: integer("verified", { mode: "boolean" }).notNull().default(false),
|
||||||
userId: text("userId").references(() => users.userId, { onDelete: "cascade" })
|
userId: text("userId").references(() => users.userId, {
|
||||||
|
onDelete: "cascade"
|
||||||
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
export type Org = InferSelectModel<typeof orgs>;
|
export type Org = InferSelectModel<typeof orgs>;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { db } from "@server/db";
|
import { db, roleSiteResources, userOrgs, userSiteResources } from "@server/db";
|
||||||
import { siteResources } from "@server/db";
|
import { siteResources } from "@server/db";
|
||||||
import { eq, and } from "drizzle-orm";
|
import { eq, and } from "drizzle-orm";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
@@ -12,44 +12,128 @@ export async function verifySiteResourceAccess(
|
|||||||
next: NextFunction
|
next: NextFunction
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
try {
|
try {
|
||||||
const siteResourceId = parseInt(req.params.siteResourceId);
|
const userId = req.user!.userId;
|
||||||
const siteId = parseInt(req.params.siteId);
|
const siteResourceId =
|
||||||
const orgId = req.params.orgId;
|
req.params.siteResourceId ||
|
||||||
|
req.body.siteResourceId ||
|
||||||
|
req.query.siteResourceId;
|
||||||
|
|
||||||
if (!siteResourceId || !siteId || !orgId) {
|
if (!userId) {
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.UNAUTHORIZED, "User not authenticated")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!siteResourceId) {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
HttpCode.BAD_REQUEST,
|
HttpCode.BAD_REQUEST,
|
||||||
"Missing required parameters"
|
"Site resource ID is required"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const siteResourceIdNum = parseInt(siteResourceId as string, 10);
|
||||||
|
if (isNaN(siteResourceIdNum)) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
"Invalid site resource ID"
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the site resource exists and belongs to the specified site and org
|
|
||||||
const [siteResource] = await db
|
const [siteResource] = await db
|
||||||
.select()
|
.select()
|
||||||
.from(siteResources)
|
.from(siteResources)
|
||||||
.where(and(
|
.where(eq(siteResources.siteResourceId, siteResourceIdNum))
|
||||||
eq(siteResources.siteResourceId, siteResourceId),
|
|
||||||
eq(siteResources.siteId, siteId),
|
|
||||||
eq(siteResources.orgId, orgId)
|
|
||||||
))
|
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
if (!siteResource) {
|
if (!siteResource) {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
HttpCode.NOT_FOUND,
|
HttpCode.NOT_FOUND,
|
||||||
"Site resource not found"
|
`Site resource with ID ${siteResourceIdNum} not found`
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!siteResource.orgId) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.INTERNAL_SERVER_ERROR,
|
||||||
|
`Site resource with ID ${siteResourceIdNum} does not have an organization ID`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!req.userOrg) {
|
||||||
|
const userOrgRole = await db
|
||||||
|
.select()
|
||||||
|
.from(userOrgs)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(userOrgs.userId, userId),
|
||||||
|
eq(userOrgs.orgId, siteResource.orgId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
req.userOrg = userOrgRole[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!req.userOrg) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.FORBIDDEN,
|
||||||
|
"User does not have access to this organization"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const userOrgRoleId = req.userOrg.roleId;
|
||||||
|
req.userOrgRoleId = userOrgRoleId;
|
||||||
|
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
|
// @ts-ignore - Extending Request type
|
||||||
req.siteResource = siteResource;
|
req.siteResource = siteResource;
|
||||||
|
|
||||||
next();
|
const roleResourceAccess = await db
|
||||||
|
.select()
|
||||||
|
.from(roleSiteResources)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(roleSiteResources.siteResourceId, siteResourceIdNum),
|
||||||
|
eq(roleSiteResources.roleId, userOrgRoleId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (roleResourceAccess.length > 0) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
const userResourceAccess = await db
|
||||||
|
.select()
|
||||||
|
.from(userSiteResources)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(userSiteResources.userId, userId),
|
||||||
|
eq(userSiteResources.siteResourceId, siteResourceIdNum)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (userResourceAccess.length > 0) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.FORBIDDEN,
|
||||||
|
"User does not have access to this resource"
|
||||||
|
)
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Error verifying site resource access:", error);
|
logger.error("Error verifying site resource access:", error);
|
||||||
return next(
|
return next(
|
||||||
|
|||||||
@@ -285,6 +285,38 @@ authenticated.delete(
|
|||||||
siteResource.deleteSiteResource,
|
siteResource.deleteSiteResource,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
authenticated.get(
|
||||||
|
"/site-resource/:siteResourceId/roles",
|
||||||
|
verifySiteResourceAccess,
|
||||||
|
verifyUserHasAction(ActionsEnum.listResourceRoles),
|
||||||
|
siteResource.listSiteResourceRoles
|
||||||
|
);
|
||||||
|
|
||||||
|
authenticated.get(
|
||||||
|
"/site-resource/:siteResourceId/users",
|
||||||
|
verifySiteResourceAccess,
|
||||||
|
verifyUserHasAction(ActionsEnum.listResourceUsers),
|
||||||
|
siteResource.listSiteResourceUsers
|
||||||
|
);
|
||||||
|
|
||||||
|
authenticated.post(
|
||||||
|
"/site-resource/:siteResourceId/roles",
|
||||||
|
verifySiteResourceAccess,
|
||||||
|
verifyRoleAccess,
|
||||||
|
verifyUserHasAction(ActionsEnum.setResourceRoles),
|
||||||
|
logActionAudit(ActionsEnum.setResourceRoles),
|
||||||
|
siteResource.setSiteResourceRoles,
|
||||||
|
);
|
||||||
|
|
||||||
|
authenticated.post(
|
||||||
|
"/site-resource/:siteResourceId/users",
|
||||||
|
verifySiteResourceAccess,
|
||||||
|
verifySetResourceUsers,
|
||||||
|
verifyUserHasAction(ActionsEnum.setResourceUsers),
|
||||||
|
logActionAudit(ActionsEnum.setResourceUsers),
|
||||||
|
siteResource.setSiteResourceUsers,
|
||||||
|
);
|
||||||
|
|
||||||
authenticated.put(
|
authenticated.put(
|
||||||
"/org/:orgId/resource",
|
"/org/:orgId/resource",
|
||||||
verifyOrgAccess,
|
verifyOrgAccess,
|
||||||
|
|||||||
@@ -198,6 +198,38 @@ authenticated.delete(
|
|||||||
siteResource.deleteSiteResource
|
siteResource.deleteSiteResource
|
||||||
);
|
);
|
||||||
|
|
||||||
|
authenticated.get(
|
||||||
|
"/site-resource/:siteResourceId/roles",
|
||||||
|
verifyApiKeySiteResourceAccess,
|
||||||
|
verifyApiKeyHasAction(ActionsEnum.listResourceRoles),
|
||||||
|
siteResource.listSiteResourceRoles
|
||||||
|
);
|
||||||
|
|
||||||
|
authenticated.get(
|
||||||
|
"/site-resource/:siteResourceId/users",
|
||||||
|
verifyApiKeySiteResourceAccess,
|
||||||
|
verifyApiKeyHasAction(ActionsEnum.listResourceUsers),
|
||||||
|
siteResource.listSiteResourceUsers
|
||||||
|
);
|
||||||
|
|
||||||
|
authenticated.post(
|
||||||
|
"/site-resource/:siteResourceId/roles",
|
||||||
|
verifyApiKeySiteResourceAccess,
|
||||||
|
verifyApiKeyRoleAccess,
|
||||||
|
verifyApiKeyHasAction(ActionsEnum.setResourceRoles),
|
||||||
|
logActionAudit(ActionsEnum.setResourceRoles),
|
||||||
|
siteResource.setSiteResourceRoles
|
||||||
|
);
|
||||||
|
|
||||||
|
authenticated.post(
|
||||||
|
"/site-resource/:siteResourceId/users",
|
||||||
|
verifyApiKeySiteResourceAccess,
|
||||||
|
verifyApiKeySetResourceUsers,
|
||||||
|
verifyApiKeyHasAction(ActionsEnum.setResourceUsers),
|
||||||
|
logActionAudit(ActionsEnum.setResourceUsers),
|
||||||
|
siteResource.setSiteResourceUsers
|
||||||
|
);
|
||||||
|
|
||||||
authenticated.put(
|
authenticated.put(
|
||||||
"/org/:orgId/resource",
|
"/org/:orgId/resource",
|
||||||
verifyApiKeyOrgAccess,
|
verifyApiKeyOrgAccess,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { db, newts } from "@server/db";
|
import { db, newts, roleResources, roles, roleSiteResources } from "@server/db";
|
||||||
import { siteResources, sites, orgs, SiteResource } from "@server/db";
|
import { siteResources, sites, orgs, SiteResource } from "@server/db";
|
||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
@@ -140,6 +140,23 @@ export async function createSiteResource(
|
|||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
|
const adminRole = await db
|
||||||
|
.select()
|
||||||
|
.from(roles)
|
||||||
|
.where(and(eq(roles.isAdmin, true), eq(roles.orgId, orgId)))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (adminRole.length === 0) {
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.NOT_FOUND, `Admin role not found`)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.insert(roleSiteResources).values({
|
||||||
|
roleId: adminRole[0].roleId,
|
||||||
|
siteResourceId: newSiteResource.siteResourceId
|
||||||
|
});
|
||||||
|
|
||||||
const [newt] = await db
|
const [newt] = await db
|
||||||
.select()
|
.select()
|
||||||
.from(newts)
|
.from(newts)
|
||||||
@@ -150,7 +167,13 @@ export async function createSiteResource(
|
|||||||
return next(createHttpError(HttpCode.NOT_FOUND, "Newt not found"));
|
return next(createHttpError(HttpCode.NOT_FOUND, "Newt not found"));
|
||||||
}
|
}
|
||||||
|
|
||||||
await addTargets(newt.newtId, destinationIp, destinationPort, protocol, proxyPort);
|
await addTargets(
|
||||||
|
newt.newtId,
|
||||||
|
destinationIp,
|
||||||
|
destinationPort,
|
||||||
|
protocol,
|
||||||
|
proxyPort
|
||||||
|
);
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
`Created site resource ${newSiteResource.siteResourceId} for site ${siteId}`
|
`Created site resource ${newSiteResource.siteResourceId} for site ${siteId}`
|
||||||
|
|||||||
@@ -4,3 +4,7 @@ export * from "./getSiteResource";
|
|||||||
export * from "./updateSiteResource";
|
export * from "./updateSiteResource";
|
||||||
export * from "./listSiteResources";
|
export * from "./listSiteResources";
|
||||||
export * from "./listAllSiteResourcesByOrg";
|
export * from "./listAllSiteResourcesByOrg";
|
||||||
|
export * from "./listSiteResourceRoles";
|
||||||
|
export * from "./listSiteResourceUsers";
|
||||||
|
export * from "./setSiteResourceRoles";
|
||||||
|
export * from "./setSiteResourceUsers";
|
||||||
|
|||||||
86
server/routers/siteResource/listSiteResourceRoles.ts
Normal file
86
server/routers/siteResource/listSiteResourceRoles.ts
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import { Request, Response, NextFunction } from "express";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { db } from "@server/db";
|
||||||
|
import { roleSiteResources, roles } from "@server/db";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
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 { OpenAPITags, registry } from "@server/openApi";
|
||||||
|
|
||||||
|
const listSiteResourceRolesSchema = z
|
||||||
|
.object({
|
||||||
|
siteResourceId: z
|
||||||
|
.string()
|
||||||
|
.transform(Number)
|
||||||
|
.pipe(z.number().int().positive())
|
||||||
|
})
|
||||||
|
.strict();
|
||||||
|
|
||||||
|
async function query(siteResourceId: number) {
|
||||||
|
return await db
|
||||||
|
.select({
|
||||||
|
roleId: roles.roleId,
|
||||||
|
name: roles.name,
|
||||||
|
description: roles.description,
|
||||||
|
isAdmin: roles.isAdmin
|
||||||
|
})
|
||||||
|
.from(roleSiteResources)
|
||||||
|
.innerJoin(roles, eq(roleSiteResources.roleId, roles.roleId))
|
||||||
|
.where(eq(roleSiteResources.siteResourceId, siteResourceId));
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ListSiteResourceRolesResponse = {
|
||||||
|
roles: NonNullable<Awaited<ReturnType<typeof query>>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
registry.registerPath({
|
||||||
|
method: "get",
|
||||||
|
path: "/site-resource/{siteResourceId}/roles",
|
||||||
|
description: "List all roles for a site resource.",
|
||||||
|
tags: [OpenAPITags.Resource, OpenAPITags.Role],
|
||||||
|
request: {
|
||||||
|
params: listSiteResourceRolesSchema
|
||||||
|
},
|
||||||
|
responses: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function listSiteResourceRoles(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
): Promise<any> {
|
||||||
|
try {
|
||||||
|
const parsedParams = listSiteResourceRolesSchema.safeParse(req.params);
|
||||||
|
if (!parsedParams.success) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
fromError(parsedParams.error).toString()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { siteResourceId } = parsedParams.data;
|
||||||
|
|
||||||
|
const siteResourceRolesList = await query(siteResourceId);
|
||||||
|
|
||||||
|
return response<ListSiteResourceRolesResponse>(res, {
|
||||||
|
data: {
|
||||||
|
roles: siteResourceRolesList
|
||||||
|
},
|
||||||
|
success: true,
|
||||||
|
error: false,
|
||||||
|
message: "Site resource roles retrieved successfully",
|
||||||
|
status: HttpCode.OK
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
89
server/routers/siteResource/listSiteResourceUsers.ts
Normal file
89
server/routers/siteResource/listSiteResourceUsers.ts
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import { Request, Response, NextFunction } from "express";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { db } from "@server/db";
|
||||||
|
import { idp, userSiteResources, users } from "@server/db";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
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 { OpenAPITags, registry } from "@server/openApi";
|
||||||
|
|
||||||
|
const listSiteResourceUsersSchema = z
|
||||||
|
.object({
|
||||||
|
siteResourceId: z
|
||||||
|
.string()
|
||||||
|
.transform(Number)
|
||||||
|
.pipe(z.number().int().positive())
|
||||||
|
})
|
||||||
|
.strict();
|
||||||
|
|
||||||
|
async function queryUsers(siteResourceId: number) {
|
||||||
|
return await db
|
||||||
|
.select({
|
||||||
|
userId: userSiteResources.userId,
|
||||||
|
username: users.username,
|
||||||
|
type: users.type,
|
||||||
|
idpName: idp.name,
|
||||||
|
idpId: users.idpId,
|
||||||
|
email: users.email
|
||||||
|
})
|
||||||
|
.from(userSiteResources)
|
||||||
|
.innerJoin(users, eq(userSiteResources.userId, users.userId))
|
||||||
|
.leftJoin(idp, eq(users.idpId, idp.idpId))
|
||||||
|
.where(eq(userSiteResources.siteResourceId, siteResourceId));
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ListSiteResourceUsersResponse = {
|
||||||
|
users: NonNullable<Awaited<ReturnType<typeof queryUsers>>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
registry.registerPath({
|
||||||
|
method: "get",
|
||||||
|
path: "/site-resource/{siteResourceId}/users",
|
||||||
|
description: "List all users for a site resource.",
|
||||||
|
tags: [OpenAPITags.Resource, OpenAPITags.User],
|
||||||
|
request: {
|
||||||
|
params: listSiteResourceUsersSchema
|
||||||
|
},
|
||||||
|
responses: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function listSiteResourceUsers(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
): Promise<any> {
|
||||||
|
try {
|
||||||
|
const parsedParams = listSiteResourceUsersSchema.safeParse(req.params);
|
||||||
|
if (!parsedParams.success) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
fromError(parsedParams.error).toString()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { siteResourceId } = parsedParams.data;
|
||||||
|
|
||||||
|
const siteResourceUsersList = await queryUsers(siteResourceId);
|
||||||
|
|
||||||
|
return response<ListSiteResourceUsersResponse>(res, {
|
||||||
|
data: {
|
||||||
|
users: siteResourceUsersList
|
||||||
|
},
|
||||||
|
success: true,
|
||||||
|
error: false,
|
||||||
|
message: "Site resource users retrieved successfully",
|
||||||
|
status: HttpCode.OK
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
155
server/routers/siteResource/setSiteResourceRoles.ts
Normal file
155
server/routers/siteResource/setSiteResourceRoles.ts
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
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, ne } from "drizzle-orm";
|
||||||
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
|
|
||||||
|
const setSiteResourceRolesBodySchema = z
|
||||||
|
.object({
|
||||||
|
roleIds: z.array(z.number().int().positive())
|
||||||
|
})
|
||||||
|
.strict();
|
||||||
|
|
||||||
|
const setSiteResourceRolesParamsSchema = z
|
||||||
|
.object({
|
||||||
|
siteResourceId: z
|
||||||
|
.string()
|
||||||
|
.transform(Number)
|
||||||
|
.pipe(z.number().int().positive())
|
||||||
|
})
|
||||||
|
.strict();
|
||||||
|
|
||||||
|
registry.registerPath({
|
||||||
|
method: "post",
|
||||||
|
path: "/site-resource/{siteResourceId}/roles",
|
||||||
|
description:
|
||||||
|
"Set roles for a site resource. This will replace all existing roles.",
|
||||||
|
tags: [OpenAPITags.Resource, OpenAPITags.Role],
|
||||||
|
request: {
|
||||||
|
params: setSiteResourceRolesParamsSchema,
|
||||||
|
body: {
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: setSiteResourceRolesBodySchema
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
responses: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function setSiteResourceRoles(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
): Promise<any> {
|
||||||
|
try {
|
||||||
|
const parsedBody = setSiteResourceRolesBodySchema.safeParse(req.body);
|
||||||
|
if (!parsedBody.success) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
fromError(parsedBody.error).toString()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { roleIds } = parsedBody.data;
|
||||||
|
|
||||||
|
const parsedParams = setSiteResourceRolesParamsSchema.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.INTERNAL_SERVER_ERROR,
|
||||||
|
"Site resource not found"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// get this org's admin role
|
||||||
|
const adminRole = await db
|
||||||
|
.select()
|
||||||
|
.from(roles)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(roles.name, "Admin"),
|
||||||
|
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)) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
"Admin role cannot be assigned to site resources"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.transaction(async (trx) => {
|
||||||
|
await trx.delete(roleSiteResources).where(
|
||||||
|
and(
|
||||||
|
eq(roleSiteResources.siteResourceId, siteResourceId),
|
||||||
|
ne(roleSiteResources.roleId, adminRole[0].roleId) // delete all but the admin role
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
roleIds.map((roleId) =>
|
||||||
|
trx
|
||||||
|
.insert(roleSiteResources)
|
||||||
|
.values({ roleId, siteResourceId })
|
||||||
|
.returning()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return response(res, {
|
||||||
|
data: {},
|
||||||
|
success: true,
|
||||||
|
error: false,
|
||||||
|
message: "Roles set for site resource successfully",
|
||||||
|
status: HttpCode.CREATED
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
106
server/routers/siteResource/setSiteResourceUsers.ts
Normal file
106
server/routers/siteResource/setSiteResourceUsers.ts
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
import { Request, Response, NextFunction } from "express";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { db } 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 } from "drizzle-orm";
|
||||||
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
|
|
||||||
|
const setSiteResourceUsersBodySchema = z
|
||||||
|
.object({
|
||||||
|
userIds: z.array(z.string())
|
||||||
|
})
|
||||||
|
.strict();
|
||||||
|
|
||||||
|
const setSiteResourceUsersParamsSchema = z
|
||||||
|
.object({
|
||||||
|
siteResourceId: z
|
||||||
|
.string()
|
||||||
|
.transform(Number)
|
||||||
|
.pipe(z.number().int().positive())
|
||||||
|
})
|
||||||
|
.strict();
|
||||||
|
|
||||||
|
registry.registerPath({
|
||||||
|
method: "post",
|
||||||
|
path: "/site-resource/{siteResourceId}/users",
|
||||||
|
description:
|
||||||
|
"Set users for a site resource. This will replace all existing users.",
|
||||||
|
tags: [OpenAPITags.Resource, OpenAPITags.User],
|
||||||
|
request: {
|
||||||
|
params: setSiteResourceUsersParamsSchema,
|
||||||
|
body: {
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: setSiteResourceUsersBodySchema
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
responses: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function setSiteResourceUsers(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
): Promise<any> {
|
||||||
|
try {
|
||||||
|
const parsedBody = setSiteResourceUsersBodySchema.safeParse(req.body);
|
||||||
|
if (!parsedBody.success) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
fromError(parsedBody.error).toString()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { userIds } = parsedBody.data;
|
||||||
|
|
||||||
|
const parsedParams = setSiteResourceUsersParamsSchema.safeParse(req.params);
|
||||||
|
if (!parsedParams.success) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
fromError(parsedParams.error).toString()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { siteResourceId } = parsedParams.data;
|
||||||
|
|
||||||
|
await db.transaction(async (trx) => {
|
||||||
|
await trx
|
||||||
|
.delete(userSiteResources)
|
||||||
|
.where(eq(userSiteResources.siteResourceId, siteResourceId));
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
userIds.map((userId) =>
|
||||||
|
trx
|
||||||
|
.insert(userSiteResources)
|
||||||
|
.values({ userId, siteResourceId })
|
||||||
|
.returning()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return response(res, {
|
||||||
|
data: {},
|
||||||
|
success: true,
|
||||||
|
error: false,
|
||||||
|
message: "Users set for site resource successfully",
|
||||||
|
status: HttpCode.CREATED
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -51,7 +51,13 @@ import { useTranslations } from "next-intl";
|
|||||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
import { ListSitesResponse } from "@server/routers/site";
|
import { ListSitesResponse } from "@server/routers/site";
|
||||||
|
import { ListRolesResponse } from "@server/routers/role";
|
||||||
|
import { ListUsersResponse } from "@server/routers/user";
|
||||||
import { cn } from "@app/lib/cn";
|
import { cn } from "@app/lib/cn";
|
||||||
|
import { Tag, TagInput } from "@app/components/tags/tag-input";
|
||||||
|
import { Separator } from "@app/components/ui/separator";
|
||||||
|
import { AxiosResponse } from "axios";
|
||||||
|
import { UserType } from "@server/types/UserTypes";
|
||||||
|
|
||||||
type Site = ListSitesResponse["sites"][0];
|
type Site = ListSitesResponse["sites"][0];
|
||||||
|
|
||||||
@@ -93,11 +99,28 @@ export default function CreateInternalResourceDialog({
|
|||||||
.int()
|
.int()
|
||||||
.positive()
|
.positive()
|
||||||
.min(1, t("createInternalResourceDialogDestinationPortMin"))
|
.min(1, t("createInternalResourceDialogDestinationPortMin"))
|
||||||
.max(65535, t("createInternalResourceDialogDestinationPortMax"))
|
.max(65535, t("createInternalResourceDialogDestinationPortMax")),
|
||||||
|
roles: z.array(
|
||||||
|
z.object({
|
||||||
|
id: z.string(),
|
||||||
|
text: z.string()
|
||||||
|
})
|
||||||
|
).optional(),
|
||||||
|
users: z.array(
|
||||||
|
z.object({
|
||||||
|
id: z.string(),
|
||||||
|
text: z.string()
|
||||||
|
})
|
||||||
|
).optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
type FormData = z.infer<typeof formSchema>;
|
type FormData = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
|
const [allRoles, setAllRoles] = useState<{ id: string; text: string }[]>([]);
|
||||||
|
const [allUsers, setAllUsers] = useState<{ id: string; text: string }[]>([]);
|
||||||
|
const [activeRolesTagIndex, setActiveRolesTagIndex] = useState<number | null>(null);
|
||||||
|
const [activeUsersTagIndex, setActiveUsersTagIndex] = useState<number | null>(null);
|
||||||
|
|
||||||
const availableSites = sites.filter(
|
const availableSites = sites.filter(
|
||||||
(site) => site.type === "newt" && site.subnet
|
(site) => site.type === "newt" && site.subnet
|
||||||
);
|
);
|
||||||
@@ -110,7 +133,9 @@ export default function CreateInternalResourceDialog({
|
|||||||
protocol: "tcp",
|
protocol: "tcp",
|
||||||
proxyPort: undefined,
|
proxyPort: undefined,
|
||||||
destinationIp: "",
|
destinationIp: "",
|
||||||
destinationPort: undefined
|
destinationPort: undefined,
|
||||||
|
roles: [],
|
||||||
|
users: []
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -122,22 +147,75 @@ export default function CreateInternalResourceDialog({
|
|||||||
protocol: "tcp",
|
protocol: "tcp",
|
||||||
proxyPort: undefined,
|
proxyPort: undefined,
|
||||||
destinationIp: "",
|
destinationIp: "",
|
||||||
destinationPort: undefined
|
destinationPort: undefined,
|
||||||
|
roles: [],
|
||||||
|
users: []
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [open]);
|
}, [open]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchRolesAndUsers = async () => {
|
||||||
|
try {
|
||||||
|
const [rolesResponse, usersResponse] = await Promise.all([
|
||||||
|
api.get<AxiosResponse<ListRolesResponse>>(`/org/${orgId}/roles`),
|
||||||
|
api.get<AxiosResponse<ListUsersResponse>>(`/org/${orgId}/users`)
|
||||||
|
]);
|
||||||
|
|
||||||
|
setAllRoles(
|
||||||
|
rolesResponse.data.data.roles
|
||||||
|
.map((role) => ({
|
||||||
|
id: role.roleId.toString(),
|
||||||
|
text: role.name
|
||||||
|
}))
|
||||||
|
.filter((role) => role.text !== "Admin")
|
||||||
|
);
|
||||||
|
|
||||||
|
setAllUsers(
|
||||||
|
usersResponse.data.data.users.map((user) => ({
|
||||||
|
id: user.id.toString(),
|
||||||
|
text: `${user.email || user.username}${user.type !== UserType.Internal ? ` (${user.idpName})` : ""}`
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching roles and users:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (open) {
|
||||||
|
fetchRolesAndUsers();
|
||||||
|
}
|
||||||
|
}, [open, orgId]);
|
||||||
|
|
||||||
const handleSubmit = async (data: FormData) => {
|
const handleSubmit = async (data: FormData) => {
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
try {
|
try {
|
||||||
await api.put(`/org/${orgId}/site/${data.siteId}/resource`, {
|
const response = await api.put<AxiosResponse<any>>(
|
||||||
name: data.name,
|
`/org/${orgId}/site/${data.siteId}/resource`,
|
||||||
protocol: data.protocol,
|
{
|
||||||
proxyPort: data.proxyPort,
|
name: data.name,
|
||||||
destinationIp: data.destinationIp,
|
protocol: data.protocol,
|
||||||
destinationPort: data.destinationPort,
|
proxyPort: data.proxyPort,
|
||||||
enabled: true
|
destinationIp: data.destinationIp,
|
||||||
});
|
destinationPort: data.destinationPort,
|
||||||
|
enabled: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const siteResourceId = response.data.data.siteResourceId;
|
||||||
|
|
||||||
|
// Set roles and users if provided
|
||||||
|
if (data.roles && data.roles.length > 0) {
|
||||||
|
await api.post(`/site-resource/${siteResourceId}/roles`, {
|
||||||
|
roleIds: data.roles.map((r) => parseInt(r.id))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.users && data.users.length > 0) {
|
||||||
|
await api.post(`/site-resource/${siteResourceId}/users`, {
|
||||||
|
userIds: data.users.map((u) => u.id)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: t("createInternalResourceDialogSuccess"),
|
title: t("createInternalResourceDialogSuccess"),
|
||||||
@@ -396,6 +474,81 @@ export default function CreateInternalResourceDialog({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Access Control Section */}
|
||||||
|
<Separator />
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold mb-4">
|
||||||
|
{t("resourceUsersRoles")}
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="roles"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex flex-col items-start">
|
||||||
|
<FormLabel>{t("roles")}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<TagInput
|
||||||
|
{...field}
|
||||||
|
activeTagIndex={activeRolesTagIndex}
|
||||||
|
setActiveTagIndex={setActiveRolesTagIndex}
|
||||||
|
placeholder={t("accessRoleSelect2")}
|
||||||
|
size="sm"
|
||||||
|
tags={form.getValues().roles || []}
|
||||||
|
setTags={(newRoles) => {
|
||||||
|
form.setValue(
|
||||||
|
"roles",
|
||||||
|
newRoles as [Tag, ...Tag[]]
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
enableAutocomplete={true}
|
||||||
|
autocompleteOptions={allRoles}
|
||||||
|
allowDuplicates={false}
|
||||||
|
restrictTagsToAutocompleteOptions={true}
|
||||||
|
sortTags={true}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
<FormDescription>
|
||||||
|
{t("resourceRoleDescription")}
|
||||||
|
</FormDescription>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="users"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex flex-col items-start">
|
||||||
|
<FormLabel>{t("users")}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<TagInput
|
||||||
|
{...field}
|
||||||
|
activeTagIndex={activeUsersTagIndex}
|
||||||
|
setActiveTagIndex={setActiveUsersTagIndex}
|
||||||
|
placeholder={t("accessUserSelect")}
|
||||||
|
tags={form.getValues().users || []}
|
||||||
|
size="sm"
|
||||||
|
setTags={(newUsers) => {
|
||||||
|
form.setValue(
|
||||||
|
"users",
|
||||||
|
newUsers as [Tag, ...Tag[]]
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
enableAutocomplete={true}
|
||||||
|
autocompleteOptions={allUsers}
|
||||||
|
allowDuplicates={false}
|
||||||
|
restrictTagsToAutocompleteOptions={true}
|
||||||
|
sortTags={true}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</CredenzaBody>
|
</CredenzaBody>
|
||||||
|
|||||||
@@ -37,6 +37,13 @@ import { useTranslations } from "next-intl";
|
|||||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
import { Separator } from "@app/components/ui/separator";
|
import { Separator } from "@app/components/ui/separator";
|
||||||
|
import { ListRolesResponse } from "@server/routers/role";
|
||||||
|
import { ListUsersResponse } from "@server/routers/user";
|
||||||
|
import { ListSiteResourceRolesResponse } from "@server/routers/siteResource/listSiteResourceRoles";
|
||||||
|
import { ListSiteResourceUsersResponse } from "@server/routers/siteResource/listSiteResourceUsers";
|
||||||
|
import { Tag, TagInput } from "@app/components/tags/tag-input";
|
||||||
|
import { AxiosResponse } from "axios";
|
||||||
|
import { UserType } from "@server/types/UserTypes";
|
||||||
|
|
||||||
type InternalResourceData = {
|
type InternalResourceData = {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -74,11 +81,29 @@ export default function EditInternalResourceDialog({
|
|||||||
protocol: z.enum(["tcp", "udp"]),
|
protocol: z.enum(["tcp", "udp"]),
|
||||||
proxyPort: z.number().int().positive().min(1, t("editInternalResourceDialogProxyPortMin")).max(65535, t("editInternalResourceDialogProxyPortMax")),
|
proxyPort: z.number().int().positive().min(1, t("editInternalResourceDialogProxyPortMin")).max(65535, t("editInternalResourceDialogProxyPortMax")),
|
||||||
destinationIp: z.string(),
|
destinationIp: z.string(),
|
||||||
destinationPort: z.number().int().positive().min(1, t("editInternalResourceDialogDestinationPortMin")).max(65535, t("editInternalResourceDialogDestinationPortMax"))
|
destinationPort: z.number().int().positive().min(1, t("editInternalResourceDialogDestinationPortMin")).max(65535, t("editInternalResourceDialogDestinationPortMax")),
|
||||||
|
roles: z.array(
|
||||||
|
z.object({
|
||||||
|
id: z.string(),
|
||||||
|
text: z.string()
|
||||||
|
})
|
||||||
|
).optional(),
|
||||||
|
users: z.array(
|
||||||
|
z.object({
|
||||||
|
id: z.string(),
|
||||||
|
text: z.string()
|
||||||
|
})
|
||||||
|
).optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
type FormData = z.infer<typeof formSchema>;
|
type FormData = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
|
const [allRoles, setAllRoles] = useState<{ id: string; text: string }[]>([]);
|
||||||
|
const [allUsers, setAllUsers] = useState<{ id: string; text: string }[]>([]);
|
||||||
|
const [activeRolesTagIndex, setActiveRolesTagIndex] = useState<number | null>(null);
|
||||||
|
const [activeUsersTagIndex, setActiveUsersTagIndex] = useState<number | null>(null);
|
||||||
|
const [loadingRolesUsers, setLoadingRolesUsers] = useState(false);
|
||||||
|
|
||||||
const form = useForm<FormData>({
|
const form = useForm<FormData>({
|
||||||
resolver: zodResolver(formSchema),
|
resolver: zodResolver(formSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -86,10 +111,71 @@ export default function EditInternalResourceDialog({
|
|||||||
protocol: resource.protocol as "tcp" | "udp",
|
protocol: resource.protocol as "tcp" | "udp",
|
||||||
proxyPort: resource.proxyPort || undefined,
|
proxyPort: resource.proxyPort || undefined,
|
||||||
destinationIp: resource.destinationIp || "",
|
destinationIp: resource.destinationIp || "",
|
||||||
destinationPort: resource.destinationPort || undefined
|
destinationPort: resource.destinationPort || undefined,
|
||||||
|
roles: [],
|
||||||
|
users: []
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const fetchRolesAndUsers = async () => {
|
||||||
|
setLoadingRolesUsers(true);
|
||||||
|
try {
|
||||||
|
const [
|
||||||
|
rolesResponse,
|
||||||
|
resourceRolesResponse,
|
||||||
|
usersResponse,
|
||||||
|
resourceUsersResponse
|
||||||
|
] = await Promise.all([
|
||||||
|
api.get<AxiosResponse<ListRolesResponse>>(`/org/${orgId}/roles`),
|
||||||
|
api.get<AxiosResponse<ListSiteResourceRolesResponse>>(
|
||||||
|
`/site-resource/${resource.id}/roles`
|
||||||
|
),
|
||||||
|
api.get<AxiosResponse<ListUsersResponse>>(`/org/${orgId}/users`),
|
||||||
|
api.get<AxiosResponse<ListSiteResourceUsersResponse>>(
|
||||||
|
`/site-resource/${resource.id}/users`
|
||||||
|
)
|
||||||
|
]);
|
||||||
|
|
||||||
|
setAllRoles(
|
||||||
|
rolesResponse.data.data.roles
|
||||||
|
.map((role) => ({
|
||||||
|
id: role.roleId.toString(),
|
||||||
|
text: role.name
|
||||||
|
}))
|
||||||
|
.filter((role) => role.text !== "Admin")
|
||||||
|
);
|
||||||
|
|
||||||
|
form.setValue(
|
||||||
|
"roles",
|
||||||
|
resourceRolesResponse.data.data.roles
|
||||||
|
.map((i) => ({
|
||||||
|
id: i.roleId.toString(),
|
||||||
|
text: i.name
|
||||||
|
}))
|
||||||
|
.filter((role) => role.text !== "Admin")
|
||||||
|
);
|
||||||
|
|
||||||
|
setAllUsers(
|
||||||
|
usersResponse.data.data.users.map((user) => ({
|
||||||
|
id: user.id.toString(),
|
||||||
|
text: `${user.email || user.username}${user.type !== UserType.Internal ? ` (${user.idpName})` : ""}`
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
form.setValue(
|
||||||
|
"users",
|
||||||
|
resourceUsersResponse.data.data.users.map((i) => ({
|
||||||
|
id: i.userId.toString(),
|
||||||
|
text: `${i.email || i.username}${i.type !== UserType.Internal ? ` (${i.idpName})` : ""}`
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching roles and users:", error);
|
||||||
|
} finally {
|
||||||
|
setLoadingRolesUsers(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open) {
|
if (open) {
|
||||||
form.reset({
|
form.reset({
|
||||||
@@ -97,10 +183,14 @@ export default function EditInternalResourceDialog({
|
|||||||
protocol: resource.protocol as "tcp" | "udp",
|
protocol: resource.protocol as "tcp" | "udp",
|
||||||
proxyPort: resource.proxyPort || undefined,
|
proxyPort: resource.proxyPort || undefined,
|
||||||
destinationIp: resource.destinationIp || "",
|
destinationIp: resource.destinationIp || "",
|
||||||
destinationPort: resource.destinationPort || undefined
|
destinationPort: resource.destinationPort || undefined,
|
||||||
|
roles: [],
|
||||||
|
users: []
|
||||||
});
|
});
|
||||||
|
fetchRolesAndUsers();
|
||||||
}
|
}
|
||||||
}, [open, resource, form]);
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [open, resource]);
|
||||||
|
|
||||||
const handleSubmit = async (data: FormData) => {
|
const handleSubmit = async (data: FormData) => {
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
@@ -114,6 +204,16 @@ export default function EditInternalResourceDialog({
|
|||||||
destinationPort: data.destinationPort
|
destinationPort: data.destinationPort
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Update roles and users
|
||||||
|
await Promise.all([
|
||||||
|
api.post(`/site-resource/${resource.id}/roles`, {
|
||||||
|
roleIds: (data.roles || []).map((r) => parseInt(r.id))
|
||||||
|
}),
|
||||||
|
api.post(`/site-resource/${resource.id}/users`, {
|
||||||
|
userIds: (data.users || []).map((u) => u.id)
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: t("editInternalResourceDialogSuccess"),
|
title: t("editInternalResourceDialogSuccess"),
|
||||||
description: t("editInternalResourceDialogInternalResourceUpdatedSuccessfully"),
|
description: t("editInternalResourceDialogInternalResourceUpdatedSuccessfully"),
|
||||||
@@ -250,6 +350,87 @@ export default function EditInternalResourceDialog({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Access Control Section */}
|
||||||
|
<Separator />
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold mb-4">
|
||||||
|
{t("resourceUsersRoles")}
|
||||||
|
</h3>
|
||||||
|
{loadingRolesUsers ? (
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
{t("loading")}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="roles"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex flex-col items-start">
|
||||||
|
<FormLabel>{t("roles")}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<TagInput
|
||||||
|
{...field}
|
||||||
|
activeTagIndex={activeRolesTagIndex}
|
||||||
|
setActiveTagIndex={setActiveRolesTagIndex}
|
||||||
|
placeholder={t("accessRoleSelect2")}
|
||||||
|
size="sm"
|
||||||
|
tags={form.getValues().roles || []}
|
||||||
|
setTags={(newRoles) => {
|
||||||
|
form.setValue(
|
||||||
|
"roles",
|
||||||
|
newRoles as [Tag, ...Tag[]]
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
enableAutocomplete={true}
|
||||||
|
autocompleteOptions={allRoles}
|
||||||
|
allowDuplicates={false}
|
||||||
|
restrictTagsToAutocompleteOptions={true}
|
||||||
|
sortTags={true}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
<FormDescription>
|
||||||
|
{t("resourceRoleDescription")}
|
||||||
|
</FormDescription>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="users"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex flex-col items-start">
|
||||||
|
<FormLabel>{t("users")}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<TagInput
|
||||||
|
{...field}
|
||||||
|
activeTagIndex={activeUsersTagIndex}
|
||||||
|
setActiveTagIndex={setActiveUsersTagIndex}
|
||||||
|
placeholder={t("accessUserSelect")}
|
||||||
|
tags={form.getValues().users || []}
|
||||||
|
size="sm"
|
||||||
|
setTags={(newUsers) => {
|
||||||
|
form.setValue(
|
||||||
|
"users",
|
||||||
|
newUsers as [Tag, ...Tag[]]
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
enableAutocomplete={true}
|
||||||
|
autocompleteOptions={allUsers}
|
||||||
|
allowDuplicates={false}
|
||||||
|
restrictTagsToAutocompleteOptions={true}
|
||||||
|
sortTags={true}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</CredenzaBody>
|
</CredenzaBody>
|
||||||
|
|||||||
Reference in New Issue
Block a user