mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-02 04:12:47 +02:00
Merge pull request #590 from CREDO23/feat/add-jotai-tanstack-rbac
[Feat] RBAC | Add jotai & tanstack
This commit is contained in:
commit
3c3527d498
11 changed files with 591 additions and 210 deletions
|
|
@ -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,
|
||||
});
|
||||
|
|
|
|||
13
surfsense_web/atoms/permissions/permissions-query.atoms.ts
Normal file
13
surfsense_web/atoms/permissions/permissions-query.atoms.ts
Normal 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();
|
||||
},
|
||||
};
|
||||
});
|
||||
67
surfsense_web/atoms/roles/roles-mutation.atoms.ts
Normal file
67
surfsense_web/atoms/roles/roles-mutation.atoms.ts
Normal 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");
|
||||
},
|
||||
};
|
||||
});
|
||||
111
surfsense_web/contracts/types/invites.types.ts
Normal file
111
surfsense_web/contracts/types/invites.types.ts
Normal 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>;
|
||||
87
surfsense_web/contracts/types/members.types.ts
Normal file
87
surfsense_web/contracts/types/members.types.ts
Normal 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>;
|
||||
17
surfsense_web/contracts/types/permissions.types.ts
Normal file
17
surfsense_web/contracts/types/permissions.types.ts
Normal 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>;
|
||||
86
surfsense_web/contracts/types/roles.types.ts
Normal file
86
surfsense_web/contracts/types/roles.types.ts
Normal 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>;
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
10
surfsense_web/lib/apis/permissions-api.service.ts
Normal file
10
surfsense_web/lib/apis/permissions-api.service.ts
Normal 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();
|
||||
109
surfsense_web/lib/apis/roles-api.service.ts
Normal file
109
surfsense_web/lib/apis/roles-api.service.ts
Normal 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();
|
||||
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue