mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-15 09:26:40 +00:00
add users and roles to site resources
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user