Merge pull request #590 from CREDO23/feat/add-jotai-tanstack-rbac

[Feat] RBAC | Add jotai & tanstack
This commit is contained in:
Rohan Verma 2025-12-16 22:45:46 -08:00 committed by GitHub
commit 3c3527d498
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 591 additions and 210 deletions

View file

@ -1,5 +1,6 @@
"use client";
import { useQuery } from "@tanstack/react-query";
import {
type ColumnDef,
type ColumnFiltersState,
@ -44,6 +45,12 @@ import { motion } from "motion/react";
import { useParams, useRouter } from "next/navigation";
import { useCallback, useMemo, useState } from "react";
import { toast } from "sonner";
import { createRoleMutationAtom, updateRoleMutationAtom, deleteRoleMutationAtom } from "@/atoms/roles/roles-mutation.atoms";
import { useAtomValue } from "jotai";
import type { CreateRoleRequest, UpdateRoleRequest, DeleteRoleRequest, Role } from "@/contracts/types/roles.types";
import { permissionsAtom } from "@/atoms/permissions/permissions-query.atoms";
import { rolesApiService } from "@/lib/apis/roles-api.service";
import { cacheKeys } from "@/lib/query-client/cache-keys";
import {
AlertDialog,
AlertDialogAction,
@ -103,12 +110,8 @@ import {
type Invite,
type InviteCreate,
type Member,
type Role,
type RoleCreate,
useInvites,
useMembers,
usePermissions,
useRoles,
useUserAccess,
} from "@/hooks/use-rbac";
import { cn } from "@/lib/utils";
@ -116,7 +119,7 @@ import { cn } from "@/lib/utils";
// Animation variants
const fadeInUp = {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0, transition: { duration: 0.4, ease: "easeOut" } },
visible: { opacity: 1, y: 0, transition: { duration: 0.4, ease: "easeOut" as const} },
};
const staggerContainer = {
@ -132,7 +135,7 @@ const cardVariants = {
visible: {
opacity: 1,
scale: 1,
transition: { type: "spring", stiffness: 300, damping: 30 },
transition: { type: "spring" as const, stiffness: 300, damping: 30 },
},
};
@ -150,14 +153,55 @@ export default function TeamManagementPage() {
updateMemberRole,
removeMember,
} = useMembers(searchSpaceId);
const { mutateAsync: createRole } = useAtomValue(createRoleMutationAtom);
const { mutateAsync: updateRole } = useAtomValue(updateRoleMutationAtom);
const { mutateAsync: deleteRole } = useAtomValue(deleteRoleMutationAtom);
const handleUpdateRole = useCallback(
async (roleId: number, data: { permissions?: string[] }): Promise<Role> => {
const request: UpdateRoleRequest = {
search_space_id: searchSpaceId,
role_id: roleId,
data: data,
};
return await updateRole(request);
},
[updateRole, searchSpaceId]
);
const handleDeleteRole = useCallback(
async (roleId: number): Promise<boolean> => {
const request: DeleteRoleRequest = {
search_space_id: searchSpaceId,
role_id: roleId,
};
await deleteRole(request);
return true;
},
[deleteRole, searchSpaceId]
);
const handleCreateRole = useCallback(
async (roleData: CreateRoleRequest['data']): Promise<Role> => {
const request: CreateRoleRequest = {
search_space_id: searchSpaceId,
data: roleData,
};
return await createRole(request);
},
[createRole, searchSpaceId]
);
const {
roles,
loading: rolesLoading,
fetchRoles,
createRole,
updateRole,
deleteRole,
} = useRoles(searchSpaceId);
data: roles = [],
isLoading: rolesLoading,
refetch: fetchRoles,
} = useQuery({
queryKey: cacheKeys.roles.all(searchSpaceId.toString()),
queryFn: () => rolesApiService.getRoles({ search_space_id: searchSpaceId }),
enabled: !!searchSpaceId,
});
const {
invites,
loading: invitesLoading,
@ -165,7 +209,19 @@ export default function TeamManagementPage() {
createInvite,
revokeInvite,
} = useInvites(searchSpaceId);
const { groupedPermissions, loading: permissionsLoading } = usePermissions();
const { data: permissionsData, isLoading: permissionsLoading } = useAtomValue(permissionsAtom);
const permissions = permissionsData?.permissions || [];
const groupedPermissions = useMemo(() => {
const groups: Record<string, typeof permissions> = {};
for (const perm of permissions) {
if (!groups[perm.category]) {
groups[perm.category] = [];
}
groups[perm.category].push(perm);
}
return groups;
}, [permissions]);
const canManageMembers = hasPermission("members:view");
const canManageRoles = hasPermission("roles:read");
@ -329,7 +385,7 @@ export default function TeamManagementPage() {
{activeTab === "roles" && hasPermission("roles:create") && (
<CreateRoleDialog
groupedPermissions={groupedPermissions}
onCreateRole={createRole}
onCreateRole={handleCreateRole}
/>
)}
</div>
@ -351,8 +407,8 @@ export default function TeamManagementPage() {
roles={roles}
groupedPermissions={groupedPermissions}
loading={rolesLoading}
onUpdateRole={updateRole}
onDeleteRole={deleteRole}
onUpdateRole={handleUpdateRole}
onDeleteRole={handleDeleteRole}
canUpdate={hasPermission("roles:update")}
canDelete={hasPermission("roles:delete")}
/>
@ -663,7 +719,12 @@ function RolesTab({
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{canUpdate && (
<DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
// TODO: Implement edit role dialog/modal
console.log("Edit role not yet implemented", role);
}}
>
<Edit2 className="h-4 w-4 mr-2" />
Edit Role
</DropdownMenuItem>
@ -882,7 +943,7 @@ function InvitesTab({
size="sm"
className="gap-2"
onClick={() => copyInviteLink(invite)}
disabled={isInactive}
disabled={Boolean(isInactive)}
>
{copiedId === invite.id ? (
<>
@ -1158,7 +1219,7 @@ function CreateRoleDialog({
onCreateRole,
}: {
groupedPermissions: Record<string, { value: string; name: string; category: string }[]>;
onCreateRole: (data: RoleCreate) => Promise<Role>;
onCreateRole: (data: CreateRoleRequest['data']) => Promise<Role>;
}) {
const [open, setOpen] = useState(false);
const [creating, setCreating] = useState(false);
@ -1177,7 +1238,7 @@ function CreateRoleDialog({
try {
await onCreateRole({
name: name.trim(),
description: description.trim() || undefined,
description: description.trim() || null,
permissions: selectedPermissions,
is_default: isDefault,
});

View file

@ -0,0 +1,13 @@
import { atomWithQuery } from "jotai-tanstack-query";
import { permissionsApiService } from "@/lib/apis/permissions-api.service";
import { cacheKeys } from "@/lib/query-client/cache-keys";
export const permissionsAtom = atomWithQuery(() => {
return {
queryKey: cacheKeys.permissions.all(),
staleTime: 10 * 60 * 1000, // 10 minutes
queryFn: async () => {
return permissionsApiService.getPermissions();
},
};
});

View file

@ -0,0 +1,67 @@
import { atomWithMutation } from "jotai-tanstack-query";
import { toast } from "sonner";
import type {
CreateRoleRequest,
CreateRoleResponse,
UpdateRoleRequest,
UpdateRoleResponse,
DeleteRoleRequest,
DeleteRoleResponse,
} from "@/contracts/types/roles.types";
import { rolesApiService } from "@/lib/apis/roles-api.service";
import { cacheKeys } from "@/lib/query-client/cache-keys";
import { queryClient } from "@/lib/query-client/client";
export const createRoleMutationAtom = atomWithMutation(() => {
return {
mutationFn: async (request: CreateRoleRequest) => {
return rolesApiService.createRole(request);
},
onSuccess: (_: CreateRoleResponse, request: CreateRoleRequest) => {
toast.success("Role created successfully");
queryClient.invalidateQueries({
queryKey: cacheKeys.roles.all(request.search_space_id.toString()),
});
},
onError: () => {
toast.error("Failed to create role");
},
};
});
export const updateRoleMutationAtom = atomWithMutation(() => {
return {
mutationFn: async (request: UpdateRoleRequest) => {
return rolesApiService.updateRole(request);
},
onSuccess: (_: UpdateRoleResponse, request: UpdateRoleRequest) => {
toast.success("Role updated successfully");
queryClient.invalidateQueries({
queryKey: cacheKeys.roles.all(request.search_space_id.toString()),
});
queryClient.invalidateQueries({
queryKey: cacheKeys.roles.byId(request.search_space_id.toString(), request.role_id.toString()),
});
},
onError: () => {
toast.error("Failed to update role");
},
};
});
export const deleteRoleMutationAtom = atomWithMutation(() => {
return {
mutationFn: async (request: DeleteRoleRequest) => {
return rolesApiService.deleteRole(request);
},
onSuccess: (_: DeleteRoleResponse, request: DeleteRoleRequest) => {
toast.success("Role deleted successfully");
queryClient.invalidateQueries({
queryKey: cacheKeys.roles.all(request.search_space_id.toString()),
});
},
onError: () => {
toast.error("Failed to delete role");
},
};
});

View file

@ -0,0 +1,111 @@
import { z } from "zod";
import { role } from "./roles.types";
export const invite = z.object({
id: z.number(),
name: z.string().max(100).nullable().optional(),
invite_code: z.string(),
search_space_id: z.number(),
created_by_id: z.string().nullable(),
role_id: z.number().nullable(),
expires_at: z.string().nullable(),
max_uses: z.number().nullable(),
uses_count: z.number(),
is_active: z.boolean(),
created_at: z.string(),
role: role.nullable().optional(),
});
/**
* Create invite
*/
export const createInviteRequest = z.object({
search_space_id: z.number(),
data: z.object({
name: z.string().max(100).optional(),
role_id: z.number().nullable().optional(),
expires_at: z.string().nullable().optional(),
max_uses: z.number().nullable().optional(),
}),
});
export const createInviteResponse = invite;
/**
* Get invites
*/
export const getInvitesRequest = z.object({
search_space_id: z.number(),
});
export const getInvitesResponse = z.array(invite);
/**
* Update invite
*/
export const updateInviteRequest = z.object({
search_space_id: z.number(),
invite_id: z.number(),
data: z.object({
name: z.string().max(100).optional(),
role_id: z.number().nullable().optional(),
expires_at: z.string().nullable().optional(),
max_uses: z.number().nullable().optional(),
is_active: z.boolean().optional(),
}),
});
export const updateInviteResponse = invite;
/**
* Delete invite
*/
export const deleteInviteRequest = z.object({
search_space_id: z.number(),
invite_id: z.number(),
});
export const deleteInviteResponse = z.object({
message: z.string(),
});
/**
* Get invite info by code
*/
export const getInviteInfoRequest = z.object({
invite_code: z.string(),
});
export const getInviteInfoResponse = z.object({
invite_code: z.string(),
search_space_name: z.string(),
role_name: z.string().nullable(),
expires_at: z.string().nullable(),
is_valid: z.boolean(),
});
/**
* Accept invite
*/
export const acceptInviteRequest = z.object({
invite_code: z.string(),
});
export const acceptInviteResponse = z.object({
message: z.string(),
search_space_id: z.number(),
});
export type Invite = z.infer<typeof invite>;
export type CreateInviteRequest = z.infer<typeof createInviteRequest>;
export type CreateInviteResponse = z.infer<typeof createInviteResponse>;
export type GetInvitesRequest = z.infer<typeof getInvitesRequest>;
export type GetInvitesResponse = z.infer<typeof getInvitesResponse>;
export type UpdateInviteRequest = z.infer<typeof updateInviteRequest>;
export type UpdateInviteResponse = z.infer<typeof updateInviteResponse>;
export type DeleteInviteRequest = z.infer<typeof deleteInviteRequest>;
export type DeleteInviteResponse = z.infer<typeof deleteInviteResponse>;
export type GetInviteInfoRequest = z.infer<typeof getInviteInfoRequest>;
export type GetInviteInfoResponse = z.infer<typeof getInviteInfoResponse>;
export type AcceptInviteRequest = z.infer<typeof acceptInviteRequest>;
export type AcceptInviteResponse = z.infer<typeof acceptInviteResponse>;

View file

@ -0,0 +1,87 @@
import { z } from "zod";
import { role } from "./roles.types";
export const membership = z.object({
id: z.number(),
user_id: z.string(),
search_space_id: z.number(),
role_id: z.number().nullable(),
is_owner: z.boolean(),
joined_at: z.string(),
created_at: z.string(),
role: role.nullable().optional(),
user_email: z.string().nullable().optional(),
user_is_active: z.boolean().nullable().optional(),
});
/**
* Get members
*/
export const getMembersRequest = z.object({
search_space_id: z.number(),
});
export const getMembersResponse = z.array(membership);
/**
* Update membership
*/
export const updateMembershipRequest = z.object({
search_space_id: z.number(),
membership_id: z.number(),
data: z.object({
role_id: z.number(),
}),
});
export const updateMembershipResponse = membership;
/**
* Delete membership
*/
export const deleteMembershipRequest = z.object({
search_space_id: z.number(),
membership_id: z.number(),
});
export const deleteMembershipResponse = z.object({
message: z.string(),
});
/**
* Leave search space
*/
export const leaveSearchSpaceRequest = z.object({
search_space_id: z.number(),
});
export const leaveSearchSpaceResponse = z.object({
message: z.string(),
});
/**
* Get my access
*/
export const getMyAccessRequest = z.object({
search_space_id: z.number(),
});
export const getMyAccessResponse = z.object({
user_id: z.string(),
search_space_id: z.number(),
is_owner: z.boolean(),
permissions: z.array(z.string()),
role_name: z.string().nullable(),
});
export type Membership = z.infer<typeof membership>;
export type GetMembersRequest = z.infer<typeof getMembersRequest>;
export type GetMembersResponse = z.infer<typeof getMembersResponse>;
export type UpdateMembershipRequest = z.infer<typeof updateMembershipRequest>;
export type UpdateMembershipResponse = z.infer<typeof updateMembershipResponse>;
export type DeleteMembershipRequest = z.infer<typeof deleteMembershipRequest>;
export type DeleteMembershipResponse = z.infer<typeof deleteMembershipResponse>;
export type LeaveSearchSpaceRequest = z.infer<typeof leaveSearchSpaceRequest>;
export type LeaveSearchSpaceResponse = z.infer<typeof leaveSearchSpaceResponse>;
export type GetMyAccessRequest = z.infer<typeof getMyAccessRequest>;
export type GetMyAccessResponse = z.infer<typeof getMyAccessResponse>;

View file

@ -0,0 +1,17 @@
import { z } from "zod";
export const permissionInfo = z.object({
value: z.string(),
name: z.string(),
category: z.string(),
});
/**
* Get permissions
*/
export const getPermissionsResponse = z.object({
permissions: z.array(permissionInfo),
});
export type PermissionInfo = z.infer<typeof permissionInfo>;
export type GetPermissionsResponse = z.infer<typeof getPermissionsResponse>;

View file

@ -0,0 +1,86 @@
import { z } from "zod";
export const role = z.object({
id: z.number(),
name: z.string().min(1).max(100),
description: z.string().max(500).nullable(),
permissions: z.array(z.string()),
is_default: z.boolean(),
is_system_role: z.boolean(),
search_space_id: z.number(),
created_at: z.string(),
});
/**
* Create role
*/
export const createRoleRequest = z.object({
search_space_id: z.number(),
data: role.pick({
name: true,
description: true,
permissions: true,
is_default: true,
}),
});
export const createRoleResponse = role;
/**
* Get roles
*/
export const getRolesRequest = z.object({
search_space_id: z.number(),
});
export const getRolesResponse = z.array(role);
/**
* Get role by ID
*/
export const getRoleByIdRequest = z.object({
search_space_id: z.number(),
role_id: z.number(),
});
export const getRoleByIdResponse = role;
/**
* Update role
*/
export const updateRoleRequest = z.object({
search_space_id: z.number(),
role_id: z.number(),
data: role.pick({
name: true,
description: true,
permissions: true,
is_default: true,
}).partial(),
});
export const updateRoleResponse = role;
/**
* Delete role
*/
export const deleteRoleRequest = z.object({
search_space_id: z.number(),
role_id: z.number(),
});
export const deleteRoleResponse = z.object({
message: z.string(),
});
export type Role = z.infer<typeof role>;
export type CreateRoleRequest = z.infer<typeof createRoleRequest>;
export type CreateRoleResponse = z.infer<typeof createRoleResponse>;
export type GetRolesRequest = z.infer<typeof getRolesRequest>;
export type GetRolesResponse = z.infer<typeof getRolesResponse>;
export type GetRoleByIdRequest = z.infer<typeof getRoleByIdRequest>;
export type GetRoleByIdResponse = z.infer<typeof getRoleByIdResponse>;
export type UpdateRoleRequest = z.infer<typeof updateRoleRequest>;
export type UpdateRoleResponse = z.infer<typeof updateRoleResponse>;
export type DeleteRoleRequest = z.infer<typeof deleteRoleRequest>;
export type DeleteRoleResponse = z.infer<typeof deleteRoleResponse>;

View file

@ -218,137 +218,6 @@ export function useMembers(searchSpaceId: number) {
// ============ Roles Hook ============
export function useRoles(searchSpaceId: number) {
const [roles, setRoles] = useState<Role[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const fetchRoles = useCallback(async () => {
if (!searchSpaceId) return;
try {
setLoading(true);
const response = await authenticatedFetch(
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/searchspaces/${searchSpaceId}/roles`,
{ method: "GET" }
);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.detail || "Failed to fetch roles");
}
const data = await response.json();
setRoles(data);
setError(null);
return data;
} catch (err: any) {
setError(err.message || "Failed to fetch roles");
console.error("Error fetching roles:", err);
} finally {
setLoading(false);
}
}, [searchSpaceId]);
useEffect(() => {
fetchRoles();
}, [fetchRoles]);
const createRole = useCallback(
async (roleData: RoleCreate) => {
try {
const response = await authenticatedFetch(
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/searchspaces/${searchSpaceId}/roles`,
{
headers: { "Content-Type": "application/json" },
method: "POST",
body: JSON.stringify(roleData),
}
);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.detail || "Failed to create role");
}
const newRole = await response.json();
setRoles((prev) => [...prev, newRole]);
toast.success("Role created successfully");
return newRole;
} catch (err: any) {
toast.error(err.message || "Failed to create role");
throw err;
}
},
[searchSpaceId]
);
const updateRole = useCallback(
async (roleId: number, roleData: RoleUpdate) => {
try {
const response = await authenticatedFetch(
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/searchspaces/${searchSpaceId}/roles/${roleId}`,
{
headers: { "Content-Type": "application/json" },
method: "PUT",
body: JSON.stringify(roleData),
}
);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.detail || "Failed to update role");
}
const updatedRole = await response.json();
setRoles((prev) => prev.map((r) => (r.id === roleId ? updatedRole : r)));
toast.success("Role updated successfully");
return updatedRole;
} catch (err: any) {
toast.error(err.message || "Failed to update role");
throw err;
}
},
[searchSpaceId]
);
const deleteRole = useCallback(
async (roleId: number) => {
try {
const response = await authenticatedFetch(
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/searchspaces/${searchSpaceId}/roles/${roleId}`,
{ method: "DELETE" }
);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.detail || "Failed to delete role");
}
setRoles((prev) => prev.filter((r) => r.id !== roleId));
toast.success("Role deleted successfully");
return true;
} catch (err: any) {
toast.error(err.message || "Failed to delete role");
return false;
}
},
[searchSpaceId]
);
return {
roles,
loading,
error,
fetchRoles,
createRole,
updateRole,
deleteRole,
};
}
// ============ Invites Hook ============
export function useInvites(searchSpaceId: number) {
const [invites, setInvites] = useState<Invite[]>([]);
const [loading, setLoading] = useState(true);
@ -480,63 +349,6 @@ export function useInvites(searchSpaceId: number) {
// ============ Permissions Hook ============
export function usePermissions() {
const [permissions, setPermissions] = useState<PermissionInfo[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const fetchPermissions = useCallback(async () => {
try {
setLoading(true);
const response = await authenticatedFetch(
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/permissions`,
{ method: "GET" }
);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.detail || "Failed to fetch permissions");
}
const data = await response.json();
setPermissions(data.permissions);
setError(null);
return data.permissions;
} catch (err: any) {
setError(err.message || "Failed to fetch permissions");
console.error("Error fetching permissions:", err);
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
fetchPermissions();
}, [fetchPermissions]);
// Group permissions by category
const groupedPermissions = useMemo(() => {
const groups: Record<string, PermissionInfo[]> = {};
for (const perm of permissions) {
if (!groups[perm.category]) {
groups[perm.category] = [];
}
groups[perm.category].push(perm);
}
return groups;
}, [permissions]);
return {
permissions,
groupedPermissions,
loading,
error,
fetchPermissions,
};
}
// ============ User Access Hook ============
export function useUserAccess(searchSpaceId: number) {
const [access, setAccess] = useState<UserAccess | null>(null);
const [loading, setLoading] = useState(true);

View file

@ -0,0 +1,10 @@
import { getPermissionsResponse } from "@/contracts/types/permissions.types";
import { baseApiService } from "./base-api.service";
class PermissionsApiService {
getPermissions = async () => {
return baseApiService.get(`/api/v1/permissions`, getPermissionsResponse);
};
}
export const permissionsApiService = new PermissionsApiService();

View file

@ -0,0 +1,109 @@
import {
type CreateRoleRequest,
createRoleRequest,
createRoleResponse,
type DeleteRoleRequest,
deleteRoleRequest,
deleteRoleResponse,
type GetRoleByIdRequest,
getRoleByIdRequest,
getRoleByIdResponse,
type GetRolesRequest,
getRolesRequest,
getRolesResponse,
type UpdateRoleRequest,
updateRoleRequest,
updateRoleResponse,
} from "@/contracts/types/roles.types";
import { ValidationError } from "../error";
import { baseApiService } from "./base-api.service";
class RolesApiService {
createRole = async (request: CreateRoleRequest) => {
const parsedRequest = createRoleRequest.safeParse(request);
if (!parsedRequest.success) {
console.error("Invalid request:", parsedRequest.error);
const errorMessage = parsedRequest.error.errors.map((err) => err.message).join(", ");
throw new ValidationError(`Invalid request: ${errorMessage}`);
}
return baseApiService.post(
`/api/v1/searchspaces/${parsedRequest.data.search_space_id}/roles`,
createRoleResponse,
{
body: parsedRequest.data.data,
},
);
};
getRoles = async (request: GetRolesRequest) => {
const parsedRequest = getRolesRequest.safeParse(request);
if (!parsedRequest.success) {
console.error("Invalid request:", parsedRequest.error);
const errorMessage = parsedRequest.error.errors.map((err) => err.message).join(", ");
throw new ValidationError(`Invalid request: ${errorMessage}`);
}
return baseApiService.get(
`/api/v1/searchspaces/${parsedRequest.data.search_space_id}/roles`,
getRolesResponse,
);
};
getRoleById = async (request: GetRoleByIdRequest) => {
const parsedRequest = getRoleByIdRequest.safeParse(request);
if (!parsedRequest.success) {
console.error("Invalid request:", parsedRequest.error);
const errorMessage = parsedRequest.error.errors.map((err) => err.message).join(", ");
throw new ValidationError(`Invalid request: ${errorMessage}`);
}
return baseApiService.get(
`/api/v1/searchspaces/${parsedRequest.data.search_space_id}/roles/${parsedRequest.data.role_id}`,
getRoleByIdResponse,
);
};
updateRole = async (request: UpdateRoleRequest) => {
const parsedRequest = updateRoleRequest.safeParse(request);
if (!parsedRequest.success) {
console.error("Invalid request:", parsedRequest.error);
const errorMessage = parsedRequest.error.errors.map((err) => err.message).join(", ");
throw new ValidationError(`Invalid request: ${errorMessage}`);
}
return baseApiService.put(
`/api/v1/searchspaces/${parsedRequest.data.search_space_id}/roles/${parsedRequest.data.role_id}`,
updateRoleResponse,
{
body: parsedRequest.data.data,
},
);
};
deleteRole = async (request: DeleteRoleRequest) => {
const parsedRequest = deleteRoleRequest.safeParse(request);
if (!parsedRequest.success) {
console.error("Invalid request:", parsedRequest.error);
const errorMessage = parsedRequest.error.errors.map((err) => err.message).join(", ");
throw new ValidationError(`Invalid request: ${errorMessage}`);
}
return baseApiService.delete(
`/api/v1/searchspaces/${parsedRequest.data.search_space_id}/roles/${parsedRequest.data.role_id}`,
deleteRoleResponse,
);
};
}
export const rolesApiService = new RolesApiService();

View file

@ -3,6 +3,7 @@ import type { GetDocumentsRequest } from "@/contracts/types/document.types";
import type { GetLLMConfigsRequest } from "@/contracts/types/llm-config.types";
import type { GetPodcastsRequest } from "@/contracts/types/podcast.types";
import type { GetSearchSpacesRequest } from "@/contracts/types/search-space.types";
import type { GetRolesRequest } from "@/contracts/types/roles.types";
export const cacheKeys = {
chats: {
@ -44,4 +45,11 @@ export const cacheKeys = {
user: {
current: () => ["user", "me"] as const,
},
};
roles: {
all: (searchSpaceId: string) => ["roles", searchSpaceId] as const,
byId: (searchSpaceId: string, roleId: string) => ["roles", searchSpaceId, roleId] as const,
},
permissions: {
all: () => ["permissions"] as const,
},
};