diff --git a/ui/actions/users/users.ts b/ui/actions/users/users.ts index 5f8cfa4b25..7ed4e581a1 100644 --- a/ui/actions/users/users.ts +++ b/ui/actions/users/users.ts @@ -14,10 +14,10 @@ export const getUsers = async ({ }) => { const session = await auth(); - if (isNaN(Number(page)) || page < 1) redirect("/users"); + if (isNaN(Number(page)) || page < 1) redirect("/users?include=roles"); const keyServer = process.env.API_BASE_URL; - const url = new URL(`${keyServer}/users`); + const url = new URL(`${keyServer}/users?include=roles`); if (page) url.searchParams.append("page[number]", page.toString()); if (query) url.searchParams.append("filter[search]", query); @@ -94,6 +94,58 @@ export const updateUser = async (formData: FormData) => { revalidatePath("/users"); return parseStringify(data); } catch (error) { + // eslint-disable-next-line no-console + console.error(error); + return { + error: getErrorMessage(error), + }; + } +}; + +export const updateUserRole = async (formData: FormData) => { + const session = await auth(); + const keyServer = process.env.API_BASE_URL; + + const userId = formData.get("userId") as string; + const roleId = formData.get("roleId") as string; + + // Validate required fields + if (!userId || !roleId) { + return { error: "userId and roleId are required" }; + } + + const url = new URL(`${keyServer}/users/${userId}/relationships/roles`); + + const requestBody = { + data: [ + { + type: "role", + id: roleId, + }, + ], + }; + + try { + const response = await fetch(url.toString(), { + method: "PATCH", + headers: { + "Content-Type": "application/vnd.api+json", + Accept: "application/vnd.api+json", + Authorization: `Bearer ${session?.accessToken}`, + }, + body: JSON.stringify(requestBody), + }); + + const data = await response.json(); + + if (!response.ok) { + return { error: data.errors || "An error occurred" }; + } + + revalidatePath("/users"); // Update the path as needed + return parseStringify(data); + } catch (error) { + // eslint-disable-next-line no-console console.error(error); return { error: getErrorMessage(error), diff --git a/ui/app/(prowler)/invitations/page.tsx b/ui/app/(prowler)/invitations/page.tsx index e0762ffb05..e9cc2a8cb0 100644 --- a/ui/app/(prowler)/invitations/page.tsx +++ b/ui/app/(prowler)/invitations/page.tsx @@ -60,7 +60,7 @@ const SSRDataTable = async ({ const rolesData = await getRoles({}); // Create a dictionary for roles by invitation ID - const roleDict = rolesData?.data?.reduce( + const roleDict = (rolesData?.data || []).reduce( (acc: Record, role: Role) => { role.relationships.invitations.data.forEach((invitation: any) => { acc[invitation.id] = role; @@ -73,7 +73,7 @@ const SSRDataTable = async ({ // Generate the array of roles with all the roles available const roles = Array.from( new Map( - rolesData.data.map((role: any) => [ + (rolesData?.data || []).map((role: Role) => [ role.id, { id: role.id, name: role.attributes?.name || "Unnamed Role" }, ]), diff --git a/ui/app/(prowler)/users/page.tsx b/ui/app/(prowler)/users/page.tsx index 364651ca22..704ce0c1a4 100644 --- a/ui/app/(prowler)/users/page.tsx +++ b/ui/app/(prowler)/users/page.tsx @@ -1,6 +1,7 @@ import { Spacer } from "@nextui-org/react"; import { Suspense } from "react"; +import { getRoles } from "@/actions/roles"; import { getUsers } from "@/actions/users/users"; import { FilterControls } from "@/components/filters"; import { filterUsers } from "@/components/filters/data-filters"; @@ -8,7 +9,7 @@ import { Header } from "@/components/ui"; import { DataTable, DataTableFilterCustom } from "@/components/ui/table"; import { AddUserButton } from "@/components/users"; import { ColumnsUser, SkeletonTableUser } from "@/components/users/table"; -import { SearchParamsProps } from "@/types"; +import { Role, SearchParamsProps, UserProps } from "@/types"; export default async function Users({ searchParams, @@ -52,11 +53,49 @@ const SSRDataTable = async ({ const query = (filters["filter[search]"] as string) || ""; const usersData = await getUsers({ query, page, sort, filters }); + const rolesData = await getRoles({}); + + // Create a dictionary for roles by user ID + const roleDict = (usersData?.included || []).reduce( + (acc: Record, item: Role) => { + if (item.type === "role") { + acc[item.id] = item.attributes; + } + return acc; + }, + {} as Record, + ); + + // Generate the array of roles with all the roles available + const roles = Array.from( + new Map( + (rolesData?.data || []).map((role: Role) => [ + role.id, + { id: role.id, name: role.attributes?.name || "Unnamed Role" }, + ]), + ).values(), + ); + + // Expand the users with their roles + const expandedUsers = (usersData?.data || []).map((user: UserProps) => { + // Check if the user has a role + const roleId = user?.relationships?.roles?.data?.[0]?.id; + const role = roleDict?.[roleId] || null; + + return { + ...user, + attributes: { + ...(user?.attributes || {}), + role, + }, + roles, + }; + }); return ( ); diff --git a/ui/components/invitations/forms/edit-form.tsx b/ui/components/invitations/forms/edit-form.tsx index 4782222dd0..c2313905a8 100644 --- a/ui/components/invitations/forms/edit-form.tsx +++ b/ui/components/invitations/forms/edit-form.tsx @@ -40,7 +40,6 @@ export const EditForm = ({ const isLoading = form.formState.isSubmitting; const onSubmitClient = async (values: z.infer) => { - console.log(values, " from edit form"); const formData = new FormData(); Object.entries(values).forEach( diff --git a/ui/components/scans/table/scans/column-get-scans.tsx b/ui/components/scans/table/scans/column-get-scans.tsx index 0d08684936..396b192bd4 100644 --- a/ui/components/scans/table/scans/column-get-scans.tsx +++ b/ui/components/scans/table/scans/column-get-scans.tsx @@ -127,7 +127,7 @@ export const ColumnGetScans: ColumnDef[] = [ header: ({ column }) => ( ), diff --git a/ui/components/ui/table/data-table-column-header.tsx b/ui/components/ui/table/data-table-column-header.tsx index aaa1299b3d..8e7b4ebe91 100644 --- a/ui/components/ui/table/data-table-column-header.tsx +++ b/ui/components/ui/table/data-table-column-header.tsx @@ -80,20 +80,10 @@ export const DataTableColumnHeader = ({ return ( ); diff --git a/ui/components/users/forms/edit-form.tsx b/ui/components/users/forms/edit-form.tsx index 8f6788ac8f..6c5a8c2d77 100644 --- a/ui/components/users/forms/edit-form.tsx +++ b/ui/components/users/forms/edit-form.tsx @@ -1,11 +1,13 @@ "use client"; import { zodResolver } from "@hookform/resolvers/zod"; +import { Select, SelectItem } from "@nextui-org/react"; +import { ShieldIcon, UserIcon } from "lucide-react"; import { Dispatch, SetStateAction } from "react"; -import { useForm } from "react-hook-form"; +import { Controller, useForm } from "react-hook-form"; import * as z from "zod"; -import { updateUser } from "@/actions/users/users"; +import { updateUser, updateUserRole } from "@/actions/users/users"; import { SaveIcon } from "@/components/icons"; import { useToast } from "@/components/ui"; import { CustomButton, CustomInput } from "@/components/ui/custom"; @@ -17,12 +19,16 @@ export const EditForm = ({ userName, userEmail, userCompanyName, + roles = [], + currentRole = "", setIsOpen, }: { userId: string; userName?: string; userEmail?: string; userCompanyName?: string; + roles: Array<{ id: string; name: string }>; + currentRole?: string; setIsOpen: Dispatch>; }) => { const formSchema = editUserFormSchema(); @@ -34,6 +40,7 @@ export const EditForm = ({ name: userName, email: userEmail, company_name: userCompanyName, + role: roles.find((role) => role.name === currentRole)?.id || "", }, }); @@ -44,7 +51,7 @@ export const EditForm = ({ const onSubmitClient = async (values: z.infer) => { const formData = new FormData(); - // Check if the value is not undefined before appending to FormData + // Update basic user data if (values.name !== undefined) { formData.append("name", values.name); } @@ -58,6 +65,26 @@ export const EditForm = ({ // Always include userId formData.append("userId", userId); + // Handle role updates separately + if (values.role !== roles.find((role) => role.name === currentRole)?.id) { + const roleFormData = new FormData(); + roleFormData.append("userId", userId); + roleFormData.append("roleId", values.role || ""); + + const roleUpdateResponse = await updateUserRole(roleFormData); + + if (roleUpdateResponse?.errors && roleUpdateResponse.errors.length > 0) { + const error = roleUpdateResponse.errors[0]; + toast({ + variant: "destructive", + title: "Role Update Failed", + description: `${error.detail}`, + }); + return; + } + } + + // Update other user attributes const data = await updateUser(formData); if (data?.errors && data.errors.length > 0) { @@ -84,22 +111,49 @@ export const EditForm = ({ onSubmit={form.handleSubmit(onSubmitClient)} className="flex flex-col space-y-4" > -
- Current name: {userName} +
+
+ + Name: + {userName} +
+
+ + Role: + + {currentRole} + +
-
- +
+
+ +
+
+ +
+
+
- ( + + )} /> + + {form.formState.errors.role && ( +

+ {form.formState.errors.role.message} +

+ )}
diff --git a/ui/components/users/table/column-users.tsx b/ui/components/users/table/column-users.tsx index 084982a659..9e2ab5a794 100644 --- a/ui/components/users/table/column-users.tsx +++ b/ui/components/users/table/column-users.tsx @@ -33,6 +33,14 @@ export const ColumnsUser: ColumnDef[] = [ return

{email}

; }, }, + { + accessorKey: "role", + header: () =>
Role
, + cell: ({ row }) => { + const { role } = getUserData(row); + return

{role?.name || "No Role"}

; + }, + }, { accessorKey: "company_name", header: ({ column }) => ( @@ -47,7 +55,6 @@ export const ColumnsUser: ColumnDef[] = [ return

{company_name}

; }, }, - { accessorKey: "date_joined", header: ({ column }) => ( @@ -68,7 +75,8 @@ export const ColumnsUser: ColumnDef[] = [ header: () =>
Actions
, id: "actions", cell: ({ row }) => { - return ; + const roles = row.original.roles; + return ; }, }, ]; diff --git a/ui/components/users/table/data-table-row-actions.tsx b/ui/components/users/table/data-table-row-actions.tsx index 24cba6006a..a053f0188c 100644 --- a/ui/components/users/table/data-table-row-actions.tsx +++ b/ui/components/users/table/data-table-row-actions.tsx @@ -21,34 +21,39 @@ import { CustomAlertModal } from "@/components/ui/custom"; import { DeleteForm, EditForm } from "../forms"; -interface DataTableRowActionsProps { - row: Row; +interface DataTableRowActionsProps { + row: Row; + roles?: { id: string; name: string }[]; } const iconClasses = "text-2xl text-default-500 pointer-events-none flex-shrink-0"; -export function DataTableRowActions({ +export function DataTableRowActions({ row, -}: DataTableRowActionsProps) { + roles, +}: DataTableRowActionsProps) { const [isEditOpen, setIsEditOpen] = useState(false); const [isDeleteOpen, setIsDeleteOpen] = useState(false); const userId = (row.original as { id: string }).id; const userName = (row.original as any).attributes?.name; const userEmail = (row.original as any).attributes?.email; const userCompanyName = (row.original as any).attributes?.company_name; + const userRole = (row.original as any).attributes?.role?.name; + return ( <> diff --git a/ui/types/components.ts b/ui/types/components.ts index 1b237d55b6..b1700b997f 100644 --- a/ui/types/components.ts +++ b/ui/types/components.ts @@ -325,6 +325,9 @@ export interface UserProfileProps { email: string; company_name: string; date_joined: string; + role: { + name: string; + }; }; relationships: { memberships: { @@ -351,6 +354,9 @@ export interface UserProps { email: string; company_name: string; date_joined: string; + role: { + name: string; + }; }; relationships: { memberships: { @@ -362,7 +368,20 @@ export interface UserProps { id: string; }>; }; + roles: { + meta: { + count: number; + }; + data: Array<{ + type: "role"; + id: string; + }>; + }; }; + roles: { + id: string; + name: string; + }[]; } export interface ProviderProps { diff --git a/ui/types/formSchemas.ts b/ui/types/formSchemas.ts index b8ce65ed73..18a62c22d4 100644 --- a/ui/types/formSchemas.ts +++ b/ui/types/formSchemas.ts @@ -200,4 +200,5 @@ export const editUserFormSchema = () => .optional(), company_name: z.string().optional(), userId: z.string(), + role: z.string().optional(), });