diff --git a/ui/actions/invitations/invitation.ts b/ui/actions/invitations/invitation.ts index c9871dcde0..d56c928c6b 100644 --- a/ui/actions/invitations/invitation.ts +++ b/ui/actions/invitations/invitation.ts @@ -104,9 +104,33 @@ export const updateInvite = async (formData: FormData) => { const invitationId = formData.get("invitationId"); const invitationEmail = formData.get("invitationEmail"); const expiresAt = formData.get("expires_at"); + const role = formData.get("role"); const url = new URL(`${keyServer}/tenants/invitations/${invitationId}`); + const body = JSON.stringify({ + data: { + type: "invitations", + id: invitationId, + attributes: { + email: invitationEmail, + ...(expiresAt && { expires_at: expiresAt }), + }, + relationships: { + roles: { + data: role + ? [ + { + id: role, + type: "role", + }, + ] + : [], + }, + }, + }, + }); + try { const response = await fetch(url.toString(), { method: "PATCH", @@ -115,22 +139,12 @@ export const updateInvite = async (formData: FormData) => { Accept: "application/vnd.api+json", Authorization: `Bearer ${session?.accessToken}`, }, - body: JSON.stringify({ - data: { - type: "invitations", - id: invitationId, - attributes: { - email: invitationEmail, - ...(expiresAt && { expires_at: expiresAt }), - }, - }, - }), + body, }); const data = await response.json(); revalidatePath("/invitations"); return parseStringify(data); } catch (error) { - // eslint-disable-next-line no-console console.error("Error updating invitation:", error); return { error: getErrorMessage(error), diff --git a/ui/app/(prowler)/invitations/page.tsx b/ui/app/(prowler)/invitations/page.tsx index b18cbbcba3..5dd4753916 100644 --- a/ui/app/(prowler)/invitations/page.tsx +++ b/ui/app/(prowler)/invitations/page.tsx @@ -1,7 +1,8 @@ import { Spacer } from "@nextui-org/react"; -import { Suspense } from "react"; +import React, { Suspense } from "react"; import { getInvitations } from "@/actions/invitations/invitation"; +import { getRoles } from "@/actions/roles"; import { FilterControls } from "@/components/filters"; import { filterInvitations } from "@/components/filters/data-filters"; import { SendInvitationButton } from "@/components/invitations"; @@ -11,7 +12,7 @@ import { } from "@/components/invitations/table"; import { Header } from "@/components/ui"; import { DataTable, DataTableFilterCustom } from "@/components/ui/table"; -import { SearchParamsProps } from "@/types"; +import { InvitationProps, Role, SearchParamsProps } from "@/types"; export default async function Invitations({ searchParams, @@ -54,12 +55,45 @@ const SSRDataTable = async ({ // Extract query from filters const query = (filters["filter[search]"] as string) || ""; + // Fetch invitations and roles const invitationsData = await getInvitations({ query, page, sort, filters }); + const rolesData = await getRoles({}); + + // Create a dictionary for roles by invitation ID + const roleDict = rolesData?.data?.reduce( + (acc: Record, role: Role) => { + role.relationships.invitations.data.forEach((invitation: any) => { + acc[invitation.id] = role; + }); + return acc; + }, + {}, + ); + + // Expand invitations with role information + const expandedInvitations = invitationsData?.data?.map( + (invitation: InvitationProps) => { + const role = roleDict[invitation.id]; + return { + ...invitation, + relationships: { + ...invitation.relationships, + role, + }, + }; + }, + ); + + // Create the expanded response + const expandedResponse = { + ...invitationsData, + data: expandedInvitations, + }; return ( ); diff --git a/ui/components/invitations/forms/edit-form.tsx b/ui/components/invitations/forms/edit-form.tsx index 17924cea97..810a358463 100644 --- a/ui/components/invitations/forms/edit-form.tsx +++ b/ui/components/invitations/forms/edit-form.tsx @@ -1,8 +1,7 @@ -"use client"; - import { zodResolver } from "@hookform/resolvers/zod"; +import { Select, SelectItem } from "@nextui-org/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 { updateInvite } from "@/actions/invitations/invitation"; @@ -15,10 +14,14 @@ import { editInviteFormSchema } from "@/types"; export const EditForm = ({ invitationId, invitationEmail, + roles = [], + defaultRole = "", setIsOpen, }: { invitationId: string; invitationEmail?: string; + roles: Array<{ id: string; name: string }>; + defaultRole?: string; setIsOpen: Dispatch>; }) => { const formSchema = editInviteFormSchema; @@ -27,7 +30,8 @@ export const EditForm = ({ resolver: zodResolver(formSchema), defaultValues: { invitationId, - invitationEmail: invitationEmail, + invitationEmail: invitationEmail || "", + role: defaultRole, }, }); @@ -37,7 +41,6 @@ export const EditForm = ({ const onSubmitClient = async (values: z.infer) => { const formData = new FormData(); - console.log(values); Object.entries(values).forEach( ([key, value]) => value !== undefined && formData.append(key, value), @@ -83,6 +86,33 @@ export const EditForm = ({ isInvalid={!!form.formState.errors.invitationEmail} /> +
+ ( + + )} + /> + {form.formState.errors.role && ( +

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

+ )} +
diff --git a/ui/components/invitations/table/column-invitations.tsx b/ui/components/invitations/table/column-invitations.tsx index 9e30eb8024..0fb823cffa 100644 --- a/ui/components/invitations/table/column-invitations.tsx +++ b/ui/components/invitations/table/column-invitations.tsx @@ -31,6 +31,15 @@ export const ColumnsInvitation: ColumnDef[] = [ return

{state}

; }, }, + { + accessorKey: "role", + header: () =>
Role
, + cell: ({ row }) => { + const roleName = + row.original.relationships?.role?.attributes?.name || "No Role"; + return

{roleName}

; + }, + }, { accessorKey: "inserted_at", header: ({ column }) => ( @@ -60,7 +69,6 @@ export const ColumnsInvitation: ColumnDef[] = [ return ; }, }, - { accessorKey: "actions", header: () =>
Actions
, diff --git a/ui/types/components.ts b/ui/types/components.ts index 561e6b2d86..3fc727dd64 100644 --- a/ui/types/components.ts +++ b/ui/types/components.ts @@ -222,6 +222,72 @@ export interface InvitationProps { id: string; }; }; + role?: { + data: { + type: "roles"; + id: string; + }; + attributes?: { + name: string; + manage_users?: boolean; + manage_account?: boolean; + manage_billing?: boolean; + manage_providers?: boolean; + manage_integrations?: boolean; + manage_scans?: boolean; + permission_state?: "unlimited" | "limited" | "none"; + }; + }; + }; + links: { + self: string; + }; +} + +export interface Role { + type: "role"; + id: string; + attributes: { + name: string; + manage_users: boolean; + manage_account: boolean; + manage_billing: boolean; + manage_providers: boolean; + manage_integrations: boolean; + manage_scans: boolean; + unlimited_visibility: boolean; + permission_state: "unlimited" | "limited" | "none"; + inserted_at: string; + updated_at: string; + }; + relationships: { + provider_groups: { + meta: { + count: number; + }; + data: { + type: string; + id: string; + }[]; + }; + users: { + meta: { + count: number; + }; + data: { + type: string; + id: string; + }[]; + }; + invitations: { + meta: { + count: number; + }; + data: { + type: string; + id: string; + }[]; + }; }; links: { self: string; @@ -235,55 +301,7 @@ export interface RolesProps { next: string | null; prev: string | null; }; - data: { - type: "role"; - id: string; - attributes: { - name: string; - manage_users: boolean; - manage_account: boolean; - manage_billing: boolean; - manage_providers: boolean; - manage_integrations: boolean; - manage_scans: boolean; - unlimited_visibility: boolean; - permission_state: "unlimited" | "limited" | "none"; - inserted_at: string; - updated_at: string; - }; - relationships: { - provider_groups: { - meta: { - count: number; - }; - data: { - type: string; - id: string; - }[]; - }; - users: { - meta: { - count: number; - }; - data: { - type: string; - id: string; - }[]; - }; - invitations: { - meta: { - count: number; - }; - data: { - type: string; - id: string; - }[]; - }; - }; - links: { - self: string; - }; - }[]; + data: Role[]; meta: { pagination: { page: number; @@ -293,6 +311,7 @@ export interface RolesProps { version: string; }; } + export interface UserProfileProps { data: { type: "users"; diff --git a/ui/types/formSchemas.ts b/ui/types/formSchemas.ts index 92f4cc0f99..b8ce65ed73 100644 --- a/ui/types/formSchemas.ts +++ b/ui/types/formSchemas.ts @@ -180,6 +180,7 @@ export const editInviteFormSchema = z.object({ invitationId: z.string().uuid(), invitationEmail: z.string().email(), expires_at: z.string().optional(), + role: z.string().optional(), }); export const editUserFormSchema = () =>