add users and roles to site resources

This commit is contained in:
miloschwartz
2025-11-05 12:24:50 -08:00
parent c73f8c88f7
commit e51b6b545e
13 changed files with 1017 additions and 33 deletions

View File

@@ -51,7 +51,13 @@ import { useTranslations } from "next-intl";
import { createApiClient, formatAxiosError } from "@app/lib/api";
import { useEnvContext } from "@app/hooks/useEnvContext";
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 { 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];
@@ -93,11 +99,28 @@ export default function CreateInternalResourceDialog({
.int()
.positive()
.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>;
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(
(site) => site.type === "newt" && site.subnet
);
@@ -110,7 +133,9 @@ export default function CreateInternalResourceDialog({
protocol: "tcp",
proxyPort: undefined,
destinationIp: "",
destinationPort: undefined
destinationPort: undefined,
roles: [],
users: []
}
});
@@ -122,22 +147,75 @@ export default function CreateInternalResourceDialog({
protocol: "tcp",
proxyPort: undefined,
destinationIp: "",
destinationPort: undefined
destinationPort: undefined,
roles: [],
users: []
});
}
}, [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) => {
setIsSubmitting(true);
try {
await api.put(`/org/${orgId}/site/${data.siteId}/resource`, {
name: data.name,
protocol: data.protocol,
proxyPort: data.proxyPort,
destinationIp: data.destinationIp,
destinationPort: data.destinationPort,
enabled: true
});
const response = await api.put<AxiosResponse<any>>(
`/org/${orgId}/site/${data.siteId}/resource`,
{
name: data.name,
protocol: data.protocol,
proxyPort: data.proxyPort,
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({
title: t("createInternalResourceDialogSuccess"),
@@ -396,6 +474,81 @@ export default function CreateInternalResourceDialog({
</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>
</CredenzaBody>

View File

@@ -37,6 +37,13 @@ import { useTranslations } from "next-intl";
import { createApiClient, formatAxiosError } from "@app/lib/api";
import { useEnvContext } from "@app/hooks/useEnvContext";
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 = {
id: number;
@@ -74,11 +81,29 @@ export default function EditInternalResourceDialog({
protocol: z.enum(["tcp", "udp"]),
proxyPort: z.number().int().positive().min(1, t("editInternalResourceDialogProxyPortMin")).max(65535, t("editInternalResourceDialogProxyPortMax")),
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>;
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>({
resolver: zodResolver(formSchema),
defaultValues: {
@@ -86,10 +111,71 @@ export default function EditInternalResourceDialog({
protocol: resource.protocol as "tcp" | "udp",
proxyPort: resource.proxyPort || undefined,
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(() => {
if (open) {
form.reset({
@@ -97,10 +183,14 @@ export default function EditInternalResourceDialog({
protocol: resource.protocol as "tcp" | "udp",
proxyPort: resource.proxyPort || undefined,
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) => {
setIsSubmitting(true);
@@ -114,6 +204,16 @@ export default function EditInternalResourceDialog({
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({
title: t("editInternalResourceDialogSuccess"),
description: t("editInternalResourceDialogInternalResourceUpdatedSuccessfully"),
@@ -250,6 +350,87 @@ export default function EditInternalResourceDialog({
</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>
</CredenzaBody>