From 37f2b2745118032af80589fe41cab227fa3831ad Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Mon, 15 Dec 2025 13:04:57 +0000 Subject: [PATCH 01/42] feat: add Role type for RBAC --- surfsense_web/contracts/types/rbac.types.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 surfsense_web/contracts/types/rbac.types.ts diff --git a/surfsense_web/contracts/types/rbac.types.ts b/surfsense_web/contracts/types/rbac.types.ts new file mode 100644 index 000000000..f90656a49 --- /dev/null +++ b/surfsense_web/contracts/types/rbac.types.ts @@ -0,0 +1,14 @@ +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(), +}); + +export type Role = z.infer; \ No newline at end of file From 8a9f3e1c18f1e4c5b84d1d23532238eca3fd2c0b Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Mon, 15 Dec 2025 13:10:43 +0000 Subject: [PATCH 02/42] feat: add Membership type for RBAC --- surfsense_web/contracts/types/rbac.types.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/surfsense_web/contracts/types/rbac.types.ts b/surfsense_web/contracts/types/rbac.types.ts index f90656a49..f624bd2da 100644 --- a/surfsense_web/contracts/types/rbac.types.ts +++ b/surfsense_web/contracts/types/rbac.types.ts @@ -11,4 +11,18 @@ export const role = z.object({ created_at: z.string(), }); -export type Role = z.infer; \ No newline at end of file +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(), +}); + +export type Role = z.infer; +export type Membership = z.infer; From 34f0e4514cc974f46cddcbf1eed8976e6e2ab370 Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Mon, 15 Dec 2025 13:16:11 +0000 Subject: [PATCH 03/42] feat: add Invite type for RBAC --- surfsense_web/contracts/types/rbac.types.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/surfsense_web/contracts/types/rbac.types.ts b/surfsense_web/contracts/types/rbac.types.ts index f624bd2da..aa664fa21 100644 --- a/surfsense_web/contracts/types/rbac.types.ts +++ b/surfsense_web/contracts/types/rbac.types.ts @@ -24,5 +24,21 @@ export const membership = z.object({ user_is_active: z.boolean().nullable().optional(), }); +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(), +}); + export type Role = z.infer; export type Membership = z.infer; +export type Invite = z.infer; From 18917519e94bf0317cbab402b6c587d5a16a37b0 Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Mon, 15 Dec 2025 13:20:47 +0000 Subject: [PATCH 04/42] feat: add PermissionInfo type for RBAC --- surfsense_web/contracts/types/rbac.types.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/surfsense_web/contracts/types/rbac.types.ts b/surfsense_web/contracts/types/rbac.types.ts index aa664fa21..72a9dde4c 100644 --- a/surfsense_web/contracts/types/rbac.types.ts +++ b/surfsense_web/contracts/types/rbac.types.ts @@ -39,6 +39,13 @@ export const invite = z.object({ role: role.nullable().optional(), }); +export const permissionInfo = z.object({ + value: z.string(), + name: z.string(), + category: z.string(), +}); + export type Role = z.infer; export type Membership = z.infer; export type Invite = z.infer; +export type PermissionInfo = z.infer; From d5af72bed620e7d24655dd3b8f0fbb210644f4ad Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Mon, 15 Dec 2025 13:34:04 +0000 Subject: [PATCH 05/42] feat: add GetPermissionsResponse schema for RBAC --- surfsense_web/contracts/types/rbac.types.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/surfsense_web/contracts/types/rbac.types.ts b/surfsense_web/contracts/types/rbac.types.ts index 72a9dde4c..1cf3b70d3 100644 --- a/surfsense_web/contracts/types/rbac.types.ts +++ b/surfsense_web/contracts/types/rbac.types.ts @@ -45,7 +45,15 @@ export const permissionInfo = z.object({ category: z.string(), }); +/** + * Get permissions + */ +export const getPermissionsResponse = z.object({ + permissions: z.array(permissionInfo), +}); + export type Role = z.infer; export type Membership = z.infer; export type Invite = z.infer; export type PermissionInfo = z.infer; +export type GetPermissionsResponse = z.infer; From f11215fcef531666ca479af359bb24014bb9c266 Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Mon, 15 Dec 2025 13:38:43 +0000 Subject: [PATCH 06/42] feat: add CreateRole request/response schemas for RBAC --- surfsense_web/contracts/types/rbac.types.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/surfsense_web/contracts/types/rbac.types.ts b/surfsense_web/contracts/types/rbac.types.ts index 1cf3b70d3..04c3874f8 100644 --- a/surfsense_web/contracts/types/rbac.types.ts +++ b/surfsense_web/contracts/types/rbac.types.ts @@ -52,8 +52,25 @@ export const getPermissionsResponse = z.object({ permissions: z.array(permissionInfo), }); +/** + * 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; + export type Role = z.infer; export type Membership = z.infer; export type Invite = z.infer; export type PermissionInfo = z.infer; export type GetPermissionsResponse = z.infer; +export type CreateRoleRequest = z.infer; +export type CreateRoleResponse = z.infer; From 4919b4717cac3ed3bf2931dc5fc1743b724182f6 Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Mon, 15 Dec 2025 13:44:31 +0000 Subject: [PATCH 07/42] feat: add GetRoles request/response schemas for RBAC --- surfsense_web/contracts/types/rbac.types.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/surfsense_web/contracts/types/rbac.types.ts b/surfsense_web/contracts/types/rbac.types.ts index 04c3874f8..b404b52c4 100644 --- a/surfsense_web/contracts/types/rbac.types.ts +++ b/surfsense_web/contracts/types/rbac.types.ts @@ -67,6 +67,15 @@ export const createRoleRequest = z.object({ export const createRoleResponse = role; +/** + * Get roles + */ +export const getRolesRequest = z.object({ + search_space_id: z.number(), +}); + +export const getRolesResponse = z.array(role); + export type Role = z.infer; export type Membership = z.infer; export type Invite = z.infer; @@ -74,3 +83,5 @@ export type PermissionInfo = z.infer; export type GetPermissionsResponse = z.infer; export type CreateRoleRequest = z.infer; export type CreateRoleResponse = z.infer; +export type GetRolesRequest = z.infer; +export type GetRolesResponse = z.infer; From f02fc9f4aafad07d44b8b9c56ca1221a6c1e7ed5 Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Mon, 15 Dec 2025 13:50:17 +0000 Subject: [PATCH 08/42] feat: add getRoleById request/response schemas --- surfsense_web/contracts/types/rbac.types.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/surfsense_web/contracts/types/rbac.types.ts b/surfsense_web/contracts/types/rbac.types.ts index b404b52c4..82a45cdba 100644 --- a/surfsense_web/contracts/types/rbac.types.ts +++ b/surfsense_web/contracts/types/rbac.types.ts @@ -76,6 +76,16 @@ export const getRolesRequest = z.object({ 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; + export type Role = z.infer; export type Membership = z.infer; export type Invite = z.infer; @@ -85,3 +95,5 @@ export type CreateRoleRequest = z.infer; export type CreateRoleResponse = z.infer; export type GetRolesRequest = z.infer; export type GetRolesResponse = z.infer; +export type GetRoleByIdRequest = z.infer; +export type GetRoleByIdResponse = z.infer; From 6eaf37589d33e3933cb48612f52286ce82760289 Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Mon, 15 Dec 2025 13:55:18 +0000 Subject: [PATCH 09/42] feat: add updateRole request/response schemas --- surfsense_web/contracts/types/rbac.types.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/surfsense_web/contracts/types/rbac.types.ts b/surfsense_web/contracts/types/rbac.types.ts index 82a45cdba..3ab294ec6 100644 --- a/surfsense_web/contracts/types/rbac.types.ts +++ b/surfsense_web/contracts/types/rbac.types.ts @@ -86,6 +86,22 @@ export const getRoleByIdRequest = z.object({ 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; + export type Role = z.infer; export type Membership = z.infer; export type Invite = z.infer; @@ -97,3 +113,5 @@ export type GetRolesRequest = z.infer; export type GetRolesResponse = z.infer; export type GetRoleByIdRequest = z.infer; export type GetRoleByIdResponse = z.infer; +export type UpdateRoleRequest = z.infer; +export type UpdateRoleResponse = z.infer; From c51612867de7d1ce8e280960aebf787204069ba9 Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Mon, 15 Dec 2025 13:59:47 +0000 Subject: [PATCH 10/42] feat: add deleteRole request/response schemas --- surfsense_web/contracts/types/rbac.types.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/surfsense_web/contracts/types/rbac.types.ts b/surfsense_web/contracts/types/rbac.types.ts index 3ab294ec6..15f4430c7 100644 --- a/surfsense_web/contracts/types/rbac.types.ts +++ b/surfsense_web/contracts/types/rbac.types.ts @@ -102,6 +102,18 @@ export const updateRoleRequest = z.object({ 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; export type Membership = z.infer; export type Invite = z.infer; @@ -115,3 +127,5 @@ export type GetRoleByIdRequest = z.infer; export type GetRoleByIdResponse = z.infer; export type UpdateRoleRequest = z.infer; export type UpdateRoleResponse = z.infer; +export type DeleteRoleRequest = z.infer; +export type DeleteRoleResponse = z.infer; From 289cbc627cd362426218ccbf66d5eb7af0d65923 Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Mon, 15 Dec 2025 14:03:33 +0000 Subject: [PATCH 11/42] feat: add getMembers request/response schemas --- surfsense_web/contracts/types/rbac.types.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/surfsense_web/contracts/types/rbac.types.ts b/surfsense_web/contracts/types/rbac.types.ts index 15f4430c7..5e69852eb 100644 --- a/surfsense_web/contracts/types/rbac.types.ts +++ b/surfsense_web/contracts/types/rbac.types.ts @@ -114,6 +114,15 @@ export const deleteRoleResponse = z.object({ message: z.string(), }); +/** + * Get members + */ +export const getMembersRequest = z.object({ + search_space_id: z.number(), +}); + +export const getMembersResponse = z.array(membership); + export type Role = z.infer; export type Membership = z.infer; export type Invite = z.infer; @@ -129,3 +138,5 @@ export type UpdateRoleRequest = z.infer; export type UpdateRoleResponse = z.infer; export type DeleteRoleRequest = z.infer; export type DeleteRoleResponse = z.infer; +export type GetMembersRequest = z.infer; +export type GetMembersResponse = z.infer; From 39c64103ab69a3b77fe8679152a24a4e4f12228a Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Mon, 15 Dec 2025 14:07:53 +0000 Subject: [PATCH 12/42] feat: add updateMembership request/response schemas --- surfsense_web/contracts/types/rbac.types.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/surfsense_web/contracts/types/rbac.types.ts b/surfsense_web/contracts/types/rbac.types.ts index 5e69852eb..653a12e61 100644 --- a/surfsense_web/contracts/types/rbac.types.ts +++ b/surfsense_web/contracts/types/rbac.types.ts @@ -123,6 +123,19 @@ export const getMembersRequest = z.object({ 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; + export type Role = z.infer; export type Membership = z.infer; export type Invite = z.infer; @@ -140,3 +153,5 @@ export type DeleteRoleRequest = z.infer; export type DeleteRoleResponse = z.infer; export type GetMembersRequest = z.infer; export type GetMembersResponse = z.infer; +export type UpdateMembershipRequest = z.infer; +export type UpdateMembershipResponse = z.infer; From 9ec5b324fdd4ccb81e97f5366b033a6bac7e5d3e Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Mon, 15 Dec 2025 14:13:14 +0000 Subject: [PATCH 13/42] feat: add deleteMembership request/response schemas --- surfsense_web/contracts/types/rbac.types.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/surfsense_web/contracts/types/rbac.types.ts b/surfsense_web/contracts/types/rbac.types.ts index 653a12e61..8d7b0cf39 100644 --- a/surfsense_web/contracts/types/rbac.types.ts +++ b/surfsense_web/contracts/types/rbac.types.ts @@ -136,6 +136,18 @@ export const updateMembershipRequest = z.object({ 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(), +}); + export type Role = z.infer; export type Membership = z.infer; export type Invite = z.infer; @@ -155,3 +167,5 @@ export type GetMembersRequest = z.infer; export type GetMembersResponse = z.infer; export type UpdateMembershipRequest = z.infer; export type UpdateMembershipResponse = z.infer; +export type DeleteMembershipRequest = z.infer; +export type DeleteMembershipResponse = z.infer; From 86b8cd6eff96b4505b4503c82fa5600c254f8597 Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Mon, 15 Dec 2025 19:22:11 +0000 Subject: [PATCH 14/42] feat: add leaveSearchSpace request/response schemas --- surfsense_web/contracts/types/rbac.types.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/surfsense_web/contracts/types/rbac.types.ts b/surfsense_web/contracts/types/rbac.types.ts index 8d7b0cf39..2d2516337 100644 --- a/surfsense_web/contracts/types/rbac.types.ts +++ b/surfsense_web/contracts/types/rbac.types.ts @@ -148,6 +148,17 @@ 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(), +}); + export type Role = z.infer; export type Membership = z.infer; export type Invite = z.infer; @@ -169,3 +180,5 @@ export type UpdateMembershipRequest = z.infer; export type UpdateMembershipResponse = z.infer; export type DeleteMembershipRequest = z.infer; export type DeleteMembershipResponse = z.infer; +export type LeaveSearchSpaceRequest = z.infer; +export type LeaveSearchSpaceResponse = z.infer; From bec21a72070df338e2eba170f368ee3244284f98 Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Mon, 15 Dec 2025 19:28:59 +0000 Subject: [PATCH 15/42] feat: add createInvite request/response schemas --- surfsense_web/contracts/types/rbac.types.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/surfsense_web/contracts/types/rbac.types.ts b/surfsense_web/contracts/types/rbac.types.ts index 2d2516337..306676195 100644 --- a/surfsense_web/contracts/types/rbac.types.ts +++ b/surfsense_web/contracts/types/rbac.types.ts @@ -159,6 +159,21 @@ export const leaveSearchSpaceResponse = z.object({ message: z.string(), }); +/** + * 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; + export type Role = z.infer; export type Membership = z.infer; export type Invite = z.infer; @@ -182,3 +197,5 @@ export type DeleteMembershipRequest = z.infer; export type DeleteMembershipResponse = z.infer; export type LeaveSearchSpaceRequest = z.infer; export type LeaveSearchSpaceResponse = z.infer; +export type CreateInviteRequest = z.infer; +export type CreateInviteResponse = z.infer; From a74e69fdc5127661d31bec6fd57d1a9b54c48f05 Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Mon, 15 Dec 2025 19:33:40 +0000 Subject: [PATCH 16/42] feat: add getInvites request/response schemas --- surfsense_web/contracts/types/rbac.types.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/surfsense_web/contracts/types/rbac.types.ts b/surfsense_web/contracts/types/rbac.types.ts index 306676195..f00d9fa52 100644 --- a/surfsense_web/contracts/types/rbac.types.ts +++ b/surfsense_web/contracts/types/rbac.types.ts @@ -174,6 +174,15 @@ export const createInviteRequest = z.object({ export const createInviteResponse = invite; +/** + * Get invites + */ +export const getInvitesRequest = z.object({ + search_space_id: z.number(), +}); + +export const getInvitesResponse = z.array(invite); + export type Role = z.infer; export type Membership = z.infer; export type Invite = z.infer; @@ -199,3 +208,5 @@ export type LeaveSearchSpaceRequest = z.infer; export type LeaveSearchSpaceResponse = z.infer; export type CreateInviteRequest = z.infer; export type CreateInviteResponse = z.infer; +export type GetInvitesRequest = z.infer; +export type GetInvitesResponse = z.infer; From 134c70f87fda31d778dabcd1af0c5ca15f393ffa Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Mon, 15 Dec 2025 19:36:54 +0000 Subject: [PATCH 17/42] feat: add updateInvite request/response schemas --- surfsense_web/contracts/types/rbac.types.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/surfsense_web/contracts/types/rbac.types.ts b/surfsense_web/contracts/types/rbac.types.ts index f00d9fa52..9cfe6dcdb 100644 --- a/surfsense_web/contracts/types/rbac.types.ts +++ b/surfsense_web/contracts/types/rbac.types.ts @@ -183,6 +183,23 @@ export const getInvitesRequest = z.object({ 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; + export type Role = z.infer; export type Membership = z.infer; export type Invite = z.infer; @@ -210,3 +227,5 @@ export type CreateInviteRequest = z.infer; export type CreateInviteResponse = z.infer; export type GetInvitesRequest = z.infer; export type GetInvitesResponse = z.infer; +export type UpdateInviteRequest = z.infer; +export type UpdateInviteResponse = z.infer; From e5f5e46312a4e29393b74d8ac45a70fad202916c Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Mon, 15 Dec 2025 19:39:39 +0000 Subject: [PATCH 18/42] feat: add deleteInvite request/response schemas --- surfsense_web/contracts/types/rbac.types.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/surfsense_web/contracts/types/rbac.types.ts b/surfsense_web/contracts/types/rbac.types.ts index 9cfe6dcdb..5bc78ab01 100644 --- a/surfsense_web/contracts/types/rbac.types.ts +++ b/surfsense_web/contracts/types/rbac.types.ts @@ -200,6 +200,18 @@ export const updateInviteRequest = z.object({ 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(), +}); + export type Role = z.infer; export type Membership = z.infer; export type Invite = z.infer; @@ -229,3 +241,5 @@ export type GetInvitesRequest = z.infer; export type GetInvitesResponse = z.infer; export type UpdateInviteRequest = z.infer; export type UpdateInviteResponse = z.infer; +export type DeleteInviteRequest = z.infer; +export type DeleteInviteResponse = z.infer; From 1b41b2ff2310284ca338bf7e553955885603352c Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Mon, 15 Dec 2025 19:43:07 +0000 Subject: [PATCH 19/42] feat: add getInviteInfo request/response schemas --- surfsense_web/contracts/types/rbac.types.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/surfsense_web/contracts/types/rbac.types.ts b/surfsense_web/contracts/types/rbac.types.ts index 5bc78ab01..33ee5682f 100644 --- a/surfsense_web/contracts/types/rbac.types.ts +++ b/surfsense_web/contracts/types/rbac.types.ts @@ -212,6 +212,21 @@ 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(), +}); + export type Role = z.infer; export type Membership = z.infer; export type Invite = z.infer; @@ -243,3 +258,5 @@ export type UpdateInviteRequest = z.infer; export type UpdateInviteResponse = z.infer; export type DeleteInviteRequest = z.infer; export type DeleteInviteResponse = z.infer; +export type GetInviteInfoRequest = z.infer; +export type GetInviteInfoResponse = z.infer; From fdfba4bb4a4faf2ca3909e76d399b870e36524ac Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Mon, 15 Dec 2025 19:47:25 +0000 Subject: [PATCH 20/42] feat: add acceptInvite request/response schemas --- surfsense_web/contracts/types/rbac.types.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/surfsense_web/contracts/types/rbac.types.ts b/surfsense_web/contracts/types/rbac.types.ts index 33ee5682f..be3b2df3c 100644 --- a/surfsense_web/contracts/types/rbac.types.ts +++ b/surfsense_web/contracts/types/rbac.types.ts @@ -227,6 +227,18 @@ export const getInviteInfoResponse = z.object({ 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 Role = z.infer; export type Membership = z.infer; export type Invite = z.infer; @@ -260,3 +272,5 @@ export type DeleteInviteRequest = z.infer; export type DeleteInviteResponse = z.infer; export type GetInviteInfoRequest = z.infer; export type GetInviteInfoResponse = z.infer; +export type AcceptInviteRequest = z.infer; +export type AcceptInviteResponse = z.infer; From 86cac96fe07a215ba2a9f967cfa84231604fae29 Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Mon, 15 Dec 2025 19:51:01 +0000 Subject: [PATCH 21/42] feat: add getMyAccess request/response schemas --- surfsense_web/contracts/types/rbac.types.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/surfsense_web/contracts/types/rbac.types.ts b/surfsense_web/contracts/types/rbac.types.ts index be3b2df3c..724b9fd3e 100644 --- a/surfsense_web/contracts/types/rbac.types.ts +++ b/surfsense_web/contracts/types/rbac.types.ts @@ -239,6 +239,21 @@ export const acceptInviteResponse = z.object({ search_space_id: z.number(), }); +/** + * 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 Role = z.infer; export type Membership = z.infer; export type Invite = z.infer; @@ -274,3 +289,5 @@ export type GetInviteInfoRequest = z.infer; export type GetInviteInfoResponse = z.infer; export type AcceptInviteRequest = z.infer; export type AcceptInviteResponse = z.infer; +export type GetMyAccessRequest = z.infer; +export type GetMyAccessResponse = z.infer; From ec9ed9d2154225db7f54149d338ff27ede1024f9 Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Mon, 15 Dec 2025 20:19:43 +0000 Subject: [PATCH 22/42] feat: create permissions.types.ts with single responsibility --- .../contracts/types/permissions.types.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 surfsense_web/contracts/types/permissions.types.ts diff --git a/surfsense_web/contracts/types/permissions.types.ts b/surfsense_web/contracts/types/permissions.types.ts new file mode 100644 index 000000000..3f75192a3 --- /dev/null +++ b/surfsense_web/contracts/types/permissions.types.ts @@ -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; +export type GetPermissionsResponse = z.infer; From 5f156cfa85d7c36b5d962740a6517ebf700b4a6d Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Mon, 15 Dec 2025 20:23:36 +0000 Subject: [PATCH 23/42] feat: create roles.types.ts with all role-related schemas --- surfsense_web/contracts/types/roles.types.ts | 86 ++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 surfsense_web/contracts/types/roles.types.ts diff --git a/surfsense_web/contracts/types/roles.types.ts b/surfsense_web/contracts/types/roles.types.ts new file mode 100644 index 000000000..31ad0e970 --- /dev/null +++ b/surfsense_web/contracts/types/roles.types.ts @@ -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; +export type CreateRoleRequest = z.infer; +export type CreateRoleResponse = z.infer; +export type GetRolesRequest = z.infer; +export type GetRolesResponse = z.infer; +export type GetRoleByIdRequest = z.infer; +export type GetRoleByIdResponse = z.infer; +export type UpdateRoleRequest = z.infer; +export type UpdateRoleResponse = z.infer; +export type DeleteRoleRequest = z.infer; +export type DeleteRoleResponse = z.infer; From 226ebf2ddf64405e9cdae1ee127464934727df58 Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Mon, 15 Dec 2025 20:26:34 +0000 Subject: [PATCH 24/42] feat: create members.types.ts with membership and access schemas --- .../contracts/types/members.types.ts | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 surfsense_web/contracts/types/members.types.ts diff --git a/surfsense_web/contracts/types/members.types.ts b/surfsense_web/contracts/types/members.types.ts new file mode 100644 index 000000000..a6d6333ac --- /dev/null +++ b/surfsense_web/contracts/types/members.types.ts @@ -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; +export type GetMembersRequest = z.infer; +export type GetMembersResponse = z.infer; +export type UpdateMembershipRequest = z.infer; +export type UpdateMembershipResponse = z.infer; +export type DeleteMembershipRequest = z.infer; +export type DeleteMembershipResponse = z.infer; +export type LeaveSearchSpaceRequest = z.infer; +export type LeaveSearchSpaceResponse = z.infer; +export type GetMyAccessRequest = z.infer; +export type GetMyAccessResponse = z.infer; From ead71eb0260f8a47228de42df7da2eb52e61fea9 Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Mon, 15 Dec 2025 20:29:04 +0000 Subject: [PATCH 25/42] feat: create invites.types.ts with all invite-related schemas --- .../contracts/types/invites.types.ts | 111 ++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 surfsense_web/contracts/types/invites.types.ts diff --git a/surfsense_web/contracts/types/invites.types.ts b/surfsense_web/contracts/types/invites.types.ts new file mode 100644 index 000000000..2a9460e53 --- /dev/null +++ b/surfsense_web/contracts/types/invites.types.ts @@ -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; +export type CreateInviteRequest = z.infer; +export type CreateInviteResponse = z.infer; +export type GetInvitesRequest = z.infer; +export type GetInvitesResponse = z.infer; +export type UpdateInviteRequest = z.infer; +export type UpdateInviteResponse = z.infer; +export type DeleteInviteRequest = z.infer; +export type DeleteInviteResponse = z.infer; +export type GetInviteInfoRequest = z.infer; +export type GetInviteInfoResponse = z.infer; +export type AcceptInviteRequest = z.infer; +export type AcceptInviteResponse = z.infer; From ccdcec4dd4356a09006e30929708c8e1c8277c88 Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Mon, 15 Dec 2025 20:32:41 +0000 Subject: [PATCH 26/42] feat: delete old rbac.types.ts after splitting into domain-specific files --- surfsense_web/contracts/types/rbac.types.ts | 293 -------------------- 1 file changed, 293 deletions(-) delete mode 100644 surfsense_web/contracts/types/rbac.types.ts diff --git a/surfsense_web/contracts/types/rbac.types.ts b/surfsense_web/contracts/types/rbac.types.ts deleted file mode 100644 index 724b9fd3e..000000000 --- a/surfsense_web/contracts/types/rbac.types.ts +++ /dev/null @@ -1,293 +0,0 @@ -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(), -}); - -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(), -}); - -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(), -}); - -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), -}); - -/** - * 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(), -}); - -/** - * 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(), -}); - -/** - * 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(), -}); - -/** - * 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 Role = z.infer; -export type Membership = z.infer; -export type Invite = z.infer; -export type PermissionInfo = z.infer; -export type GetPermissionsResponse = z.infer; -export type CreateRoleRequest = z.infer; -export type CreateRoleResponse = z.infer; -export type GetRolesRequest = z.infer; -export type GetRolesResponse = z.infer; -export type GetRoleByIdRequest = z.infer; -export type GetRoleByIdResponse = z.infer; -export type UpdateRoleRequest = z.infer; -export type UpdateRoleResponse = z.infer; -export type DeleteRoleRequest = z.infer; -export type DeleteRoleResponse = z.infer; -export type GetMembersRequest = z.infer; -export type GetMembersResponse = z.infer; -export type UpdateMembershipRequest = z.infer; -export type UpdateMembershipResponse = z.infer; -export type DeleteMembershipRequest = z.infer; -export type DeleteMembershipResponse = z.infer; -export type LeaveSearchSpaceRequest = z.infer; -export type LeaveSearchSpaceResponse = z.infer; -export type CreateInviteRequest = z.infer; -export type CreateInviteResponse = z.infer; -export type GetInvitesRequest = z.infer; -export type GetInvitesResponse = z.infer; -export type UpdateInviteRequest = z.infer; -export type UpdateInviteResponse = z.infer; -export type DeleteInviteRequest = z.infer; -export type DeleteInviteResponse = z.infer; -export type GetInviteInfoRequest = z.infer; -export type GetInviteInfoResponse = z.infer; -export type AcceptInviteRequest = z.infer; -export type AcceptInviteResponse = z.infer; -export type GetMyAccessRequest = z.infer; -export type GetMyAccessResponse = z.infer; From 4affdf70a6cd44424fd1ca40902fa69ec8d38758 Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Mon, 15 Dec 2025 20:54:02 +0000 Subject: [PATCH 27/42] fix: correct roles-api.service.ts to use parsedRequest.data pattern --- surfsense_web/lib/apis/roles-api.service.ts | 42 +++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 surfsense_web/lib/apis/roles-api.service.ts diff --git a/surfsense_web/lib/apis/roles-api.service.ts b/surfsense_web/lib/apis/roles-api.service.ts new file mode 100644 index 000000000..12de889b1 --- /dev/null +++ b/surfsense_web/lib/apis/roles-api.service.ts @@ -0,0 +1,42 @@ +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/searchspaces/${parsedRequest.data.search_space_id}/roles`, + createRoleResponse, + { + body: parsedRequest.data.data, + }, + ); + }; +} + +export const rolesApiService = new RolesApiService(); From b20c526951775ddc00d97749006645e45b2f3e45 Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Tue, 16 Dec 2025 05:48:18 +0000 Subject: [PATCH 28/42] feat: complete roles-api.service.ts with all CRUD methods --- surfsense_web/lib/apis/roles-api.service.ts | 67 +++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/surfsense_web/lib/apis/roles-api.service.ts b/surfsense_web/lib/apis/roles-api.service.ts index 12de889b1..a1e7fea88 100644 --- a/surfsense_web/lib/apis/roles-api.service.ts +++ b/surfsense_web/lib/apis/roles-api.service.ts @@ -37,6 +37,73 @@ class RolesApiService { }, ); }; + + 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/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/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/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/searchspaces/${parsedRequest.data.search_space_id}/roles/${parsedRequest.data.role_id}`, + deleteRoleResponse, + ); + }; } export const rolesApiService = new RolesApiService(); From e086dd51fa56b32f0b70ba338958f84dcd28cd30 Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Tue, 16 Dec 2025 05:52:52 +0000 Subject: [PATCH 29/42] feat: add permissions-api.service.ts with getPermissions method --- surfsense_web/lib/apis/permissions-api.service.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 surfsense_web/lib/apis/permissions-api.service.ts diff --git a/surfsense_web/lib/apis/permissions-api.service.ts b/surfsense_web/lib/apis/permissions-api.service.ts new file mode 100644 index 000000000..225ed892f --- /dev/null +++ b/surfsense_web/lib/apis/permissions-api.service.ts @@ -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/permissions`, getPermissionsResponse); + }; +} + +export const permissionsApiService = new PermissionsApiService(); From c732c5deee848f76a651a72f1dece64e4962f53a Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Tue, 16 Dec 2025 06:06:36 +0000 Subject: [PATCH 30/42] feat: add cache keys and permissions query atom for RBAC --- .../atoms/permissions/permissions-query.atoms.ts | 13 +++++++++++++ surfsense_web/lib/query-client/cache-keys.ts | 3 ++- 2 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 surfsense_web/atoms/permissions/permissions-query.atoms.ts diff --git a/surfsense_web/atoms/permissions/permissions-query.atoms.ts b/surfsense_web/atoms/permissions/permissions-query.atoms.ts new file mode 100644 index 000000000..335ddd77d --- /dev/null +++ b/surfsense_web/atoms/permissions/permissions-query.atoms.ts @@ -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(); + }, + }; +}); diff --git a/surfsense_web/lib/query-client/cache-keys.ts b/surfsense_web/lib/query-client/cache-keys.ts index eb2c4972a..28e9396c9 100644 --- a/surfsense_web/lib/query-client/cache-keys.ts +++ b/surfsense_web/lib/query-client/cache-keys.ts @@ -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,4 @@ export const cacheKeys = { user: { current: () => ["user", "me"] as const, }, -}; \ No newline at end of file +}; From 51216f0d042b47d60c511417b2c9b2b3ab3cbf0c Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Tue, 16 Dec 2025 06:16:22 +0000 Subject: [PATCH 31/42] feat: add roles mutation atoms with improved type safety --- .../atoms/roles/roles-mutation.atoms.ts | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 surfsense_web/atoms/roles/roles-mutation.atoms.ts diff --git a/surfsense_web/atoms/roles/roles-mutation.atoms.ts b/surfsense_web/atoms/roles/roles-mutation.atoms.ts new file mode 100644 index 000000000..47ece8b68 --- /dev/null +++ b/surfsense_web/atoms/roles/roles-mutation.atoms.ts @@ -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"); + }, + }; +}); From f69b49e4c0bc04800fc85a1e76e7f40347cee09b Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Tue, 16 Dec 2025 06:37:29 +0000 Subject: [PATCH 32/42] fix: add type assertions for Motion animation properties in team page --- surfsense_web/app/dashboard/[search_space_id]/team/page.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/surfsense_web/app/dashboard/[search_space_id]/team/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/team/page.tsx index dd3f25218..b73fda65d 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/team/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/team/page.tsx @@ -116,7 +116,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 +132,7 @@ const cardVariants = { visible: { opacity: 1, scale: 1, - transition: { type: "spring", stiffness: 300, damping: 30 }, + transition: { type: "spring" as const, stiffness: 300, damping: 30 }, }, }; @@ -882,7 +882,7 @@ function InvitesTab({ size="sm" className="gap-2" onClick={() => copyInviteLink(invite)} - disabled={isInactive} + disabled={Boolean(isInactive)} > {copiedId === invite.id ? ( <> From 635be2b4e6db071dd827bfcc432d1528b96f786e Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Tue, 16 Dec 2025 07:02:52 +0000 Subject: [PATCH 33/42] refactor: migrate team page to use React Query for permissions and roles fetching --- .../dashboard/[search_space_id]/team/page.tsx | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/surfsense_web/app/dashboard/[search_space_id]/team/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/team/page.tsx index b73fda65d..f25c5a576 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/team/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/team/page.tsx @@ -1,5 +1,6 @@ "use client"; +import { useQuery } from "@tanstack/react-query"; import { type ColumnDef, type ColumnFiltersState, @@ -44,6 +45,8 @@ import { motion } from "motion/react"; import { useParams, useRouter } from "next/navigation"; import { useCallback, useMemo, useState } from "react"; import { toast } from "sonner"; +import { rolesApiService } from "@/lib/apis/roles-api.service"; +import { cacheKeys } from "@/lib/query-client/cache-keys"; import { AlertDialog, AlertDialogAction, @@ -151,13 +154,20 @@ export default function TeamManagementPage() { removeMember, } = useMembers(searchSpaceId); const { - roles, - loading: rolesLoading, - fetchRoles, - createRole, + createRole, updateRole, deleteRole, } = useRoles(searchSpaceId); + + const { + 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, From 7d89fea31d34a0893c5ea08a2dc654617713ed5b Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Tue, 16 Dec 2025 07:25:15 +0000 Subject: [PATCH 34/42] refactor: migrate createRole to use mutation atom with proper types in team page --- .../dashboard/[search_space_id]/team/page.tsx | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/surfsense_web/app/dashboard/[search_space_id]/team/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/team/page.tsx index f25c5a576..08d1efab4 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/team/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/team/page.tsx @@ -45,6 +45,9 @@ import { motion } from "motion/react"; import { useParams, useRouter } from "next/navigation"; import { useCallback, useMemo, useState } from "react"; import { toast } from "sonner"; +import { createRoleMutationAtom } from "@/atoms/roles/roles-mutation.atoms"; +import { useAtomValue } from "jotai"; +import type { CreateRoleRequest } from "@/contracts/types/roles.types"; import { rolesApiService } from "@/lib/apis/roles-api.service"; import { cacheKeys } from "@/lib/query-client/cache-keys"; import { @@ -107,7 +110,6 @@ import { type InviteCreate, type Member, type Role, - type RoleCreate, useInvites, useMembers, usePermissions, @@ -154,11 +156,23 @@ export default function TeamManagementPage() { removeMember, } = useMembers(searchSpaceId); const { - createRole, updateRole, deleteRole, } = useRoles(searchSpaceId); + const { mutateAsync: createRole } = useAtomValue(createRoleMutationAtom); + + const handleCreateRole = useCallback( + async (roleData: CreateRoleRequest['data']): Promise => { + const request: CreateRoleRequest = { + search_space_id: searchSpaceId, + data: roleData, + }; + return await createRole(request); + }, + [createRole, searchSpaceId] + ); + const { data: roles = [], isLoading: rolesLoading, @@ -339,7 +353,7 @@ export default function TeamManagementPage() { {activeTab === "roles" && hasPermission("roles:create") && ( )} @@ -1168,7 +1182,7 @@ function CreateRoleDialog({ onCreateRole, }: { groupedPermissions: Record; - onCreateRole: (data: RoleCreate) => Promise; + onCreateRole: (data: CreateRoleRequest['data']) => Promise; }) { const [open, setOpen] = useState(false); const [creating, setCreating] = useState(false); @@ -1187,7 +1201,7 @@ function CreateRoleDialog({ try { await onCreateRole({ name: name.trim(), - description: description.trim() || undefined, + description: description.trim() || null, permissions: selectedPermissions, is_default: isDefault, }); From cbe6a0525392f315d57670a6a5141ee244df9378 Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Tue, 16 Dec 2025 07:29:12 +0000 Subject: [PATCH 35/42] refactor: migrate updateRole to use mutation atom in team page --- .../dashboard/[search_space_id]/team/page.tsx | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/surfsense_web/app/dashboard/[search_space_id]/team/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/team/page.tsx index 08d1efab4..77a9f24eb 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/team/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/team/page.tsx @@ -45,9 +45,9 @@ import { motion } from "motion/react"; import { useParams, useRouter } from "next/navigation"; import { useCallback, useMemo, useState } from "react"; import { toast } from "sonner"; -import { createRoleMutationAtom } from "@/atoms/roles/roles-mutation.atoms"; +import { createRoleMutationAtom, updateRoleMutationAtom } from "@/atoms/roles/roles-mutation.atoms"; import { useAtomValue } from "jotai"; -import type { CreateRoleRequest } from "@/contracts/types/roles.types"; +import type { CreateRoleRequest, UpdateRoleRequest } from "@/contracts/types/roles.types"; import { rolesApiService } from "@/lib/apis/roles-api.service"; import { cacheKeys } from "@/lib/query-client/cache-keys"; import { @@ -156,11 +156,23 @@ export default function TeamManagementPage() { removeMember, } = useMembers(searchSpaceId); const { - updateRole, deleteRole, } = useRoles(searchSpaceId); const { mutateAsync: createRole } = useAtomValue(createRoleMutationAtom); + const { mutateAsync: updateRole } = useAtomValue(updateRoleMutationAtom); + + const handleUpdateRole = useCallback( + async (roleId: number, data: { permissions?: string[] }): Promise => { + const request: UpdateRoleRequest = { + search_space_id: searchSpaceId, + role_id: roleId, + data: data, + }; + return await updateRole(request); + }, + [updateRole, searchSpaceId] + ); const handleCreateRole = useCallback( async (roleData: CreateRoleRequest['data']): Promise => { @@ -375,7 +387,7 @@ export default function TeamManagementPage() { roles={roles} groupedPermissions={groupedPermissions} loading={rolesLoading} - onUpdateRole={updateRole} + onUpdateRole={handleUpdateRole} onDeleteRole={deleteRole} canUpdate={hasPermission("roles:update")} canDelete={hasPermission("roles:delete")} From b4cdf2dcc1a8cedf736dc63cccec955fc9d95940 Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Tue, 16 Dec 2025 07:33:34 +0000 Subject: [PATCH 36/42] refactor: migrate deleteRole to use mutation atom and remove useRoles dependency in team page --- .../dashboard/[search_space_id]/team/page.tsx | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/surfsense_web/app/dashboard/[search_space_id]/team/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/team/page.tsx index 77a9f24eb..96849a198 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/team/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/team/page.tsx @@ -45,9 +45,9 @@ import { motion } from "motion/react"; import { useParams, useRouter } from "next/navigation"; import { useCallback, useMemo, useState } from "react"; import { toast } from "sonner"; -import { createRoleMutationAtom, updateRoleMutationAtom } from "@/atoms/roles/roles-mutation.atoms"; +import { createRoleMutationAtom, updateRoleMutationAtom, deleteRoleMutationAtom } from "@/atoms/roles/roles-mutation.atoms"; import { useAtomValue } from "jotai"; -import type { CreateRoleRequest, UpdateRoleRequest } from "@/contracts/types/roles.types"; +import type { CreateRoleRequest, UpdateRoleRequest, DeleteRoleRequest } from "@/contracts/types/roles.types"; import { rolesApiService } from "@/lib/apis/roles-api.service"; import { cacheKeys } from "@/lib/query-client/cache-keys"; import { @@ -113,7 +113,6 @@ import { useInvites, useMembers, usePermissions, - useRoles, useUserAccess, } from "@/hooks/use-rbac"; import { cn } from "@/lib/utils"; @@ -155,12 +154,10 @@ export default function TeamManagementPage() { updateMemberRole, removeMember, } = useMembers(searchSpaceId); - const { - deleteRole, - } = useRoles(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 => { @@ -174,6 +171,18 @@ export default function TeamManagementPage() { [updateRole, searchSpaceId] ); + const handleDeleteRole = useCallback( + async (roleId: number): Promise => { + 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 => { const request: CreateRoleRequest = { @@ -388,7 +397,7 @@ export default function TeamManagementPage() { groupedPermissions={groupedPermissions} loading={rolesLoading} onUpdateRole={handleUpdateRole} - onDeleteRole={deleteRole} + onDeleteRole={handleDeleteRole} canUpdate={hasPermission("roles:update")} canDelete={hasPermission("roles:delete")} /> From 55d204e05bb31640bbd1ca1f06e9554077dc3569 Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Tue, 16 Dec 2025 07:41:16 +0000 Subject: [PATCH 37/42] fix: complete usePermissions migration to permissionsAtom in team page --- .../dashboard/[search_space_id]/team/page.tsx | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/surfsense_web/app/dashboard/[search_space_id]/team/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/team/page.tsx index 96849a198..276ca3878 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/team/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/team/page.tsx @@ -48,6 +48,7 @@ import { toast } from "sonner"; import { createRoleMutationAtom, updateRoleMutationAtom, deleteRoleMutationAtom } from "@/atoms/roles/roles-mutation.atoms"; import { useAtomValue } from "jotai"; import type { CreateRoleRequest, UpdateRoleRequest, DeleteRoleRequest } 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 { @@ -112,7 +113,6 @@ import { type Role, useInvites, useMembers, - usePermissions, useUserAccess, } from "@/hooks/use-rbac"; import { cn } from "@/lib/utils"; @@ -210,7 +210,20 @@ 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 grouped: Record = {}; + permissions.forEach((permission) => { + const category = permission.permission_name.split(":")[0]; + if (!grouped[category]) { + grouped[category] = []; + } + grouped[category].push(permission); + }); + return grouped; + }, [permissions]); const canManageMembers = hasPermission("members:view"); const canManageRoles = hasPermission("roles:read"); From 5b85b1d0907f39687c525449d572f66f48cd39b2 Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Tue, 16 Dec 2025 07:45:44 +0000 Subject: [PATCH 38/42] fix: use correct groupedPermissions logic matching original implementation --- .../app/dashboard/[search_space_id]/team/page.tsx | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/surfsense_web/app/dashboard/[search_space_id]/team/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/team/page.tsx index 276ca3878..630e0849e 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/team/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/team/page.tsx @@ -214,15 +214,14 @@ export default function TeamManagementPage() { const { data: permissionsData, isLoading: permissionsLoading } = useAtomValue(permissionsAtom); const permissions = permissionsData?.permissions || []; const groupedPermissions = useMemo(() => { - const grouped: Record = {}; - permissions.forEach((permission) => { - const category = permission.permission_name.split(":")[0]; - if (!grouped[category]) { - grouped[category] = []; + const groups: Record = {}; + for (const perm of permissions) { + if (!groups[perm.category]) { + groups[perm.category] = []; } - grouped[category].push(permission); - }); - return grouped; + groups[perm.category].push(perm); + } + return groups; }, [permissions]); const canManageMembers = hasPermission("members:view"); From fcfa621a7414eacd0802a34b4b74e9ab4088c9ed Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Tue, 16 Dec 2025 07:48:00 +0000 Subject: [PATCH 39/42] refactor: remove unused useRoles and usePermissions hooks after migration --- surfsense_web/hooks/use-rbac.ts | 188 -------------------------------- 1 file changed, 188 deletions(-) diff --git a/surfsense_web/hooks/use-rbac.ts b/surfsense_web/hooks/use-rbac.ts index ee3450746..fa619407a 100644 --- a/surfsense_web/hooks/use-rbac.ts +++ b/surfsense_web/hooks/use-rbac.ts @@ -218,137 +218,6 @@ export function useMembers(searchSpaceId: number) { // ============ Roles Hook ============ -export function useRoles(searchSpaceId: number) { - const [roles, setRoles] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(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([]); const [loading, setLoading] = useState(true); @@ -480,63 +349,6 @@ export function useInvites(searchSpaceId: number) { // ============ Permissions Hook ============ -export function usePermissions() { - const [permissions, setPermissions] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(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 = {}; - 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(null); const [loading, setLoading] = useState(true); From 0397f716f883b0db753c63c2bbe11ce4e1a640c2 Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Tue, 16 Dec 2025 08:32:57 +0000 Subject: [PATCH 40/42] chore: add TODO for edit role dialog implementation --- .../app/dashboard/[search_space_id]/team/page.tsx | 7 ++++++- surfsense_web/lib/apis/permissions-api.service.ts | 2 +- surfsense_web/lib/apis/roles-api.service.ts | 10 +++++----- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/surfsense_web/app/dashboard/[search_space_id]/team/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/team/page.tsx index 630e0849e..a5e1b45df 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/team/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/team/page.tsx @@ -720,7 +720,12 @@ function RolesTab({ {canUpdate && ( - + { + // TODO: Implement edit role dialog/modal + console.log("Edit role not yet implemented", role); + }} + > Edit Role diff --git a/surfsense_web/lib/apis/permissions-api.service.ts b/surfsense_web/lib/apis/permissions-api.service.ts index 225ed892f..d161879b9 100644 --- a/surfsense_web/lib/apis/permissions-api.service.ts +++ b/surfsense_web/lib/apis/permissions-api.service.ts @@ -3,7 +3,7 @@ import { baseApiService } from "./base-api.service"; class PermissionsApiService { getPermissions = async () => { - return baseApiService.get(`/api/permissions`, getPermissionsResponse); + return baseApiService.get(`/api/v1/permissions`, getPermissionsResponse); }; } diff --git a/surfsense_web/lib/apis/roles-api.service.ts b/surfsense_web/lib/apis/roles-api.service.ts index a1e7fea88..92083293a 100644 --- a/surfsense_web/lib/apis/roles-api.service.ts +++ b/surfsense_web/lib/apis/roles-api.service.ts @@ -30,7 +30,7 @@ class RolesApiService { } return baseApiService.post( - `/api/searchspaces/${parsedRequest.data.search_space_id}/roles`, + `/api/v1/searchspaces/${parsedRequest.data.search_space_id}/roles`, createRoleResponse, { body: parsedRequest.data.data, @@ -49,7 +49,7 @@ class RolesApiService { } return baseApiService.get( - `/api/searchspaces/${parsedRequest.data.search_space_id}/roles`, + `/api/v1/searchspaces/${parsedRequest.data.search_space_id}/roles`, getRolesResponse, ); }; @@ -65,7 +65,7 @@ class RolesApiService { } return baseApiService.get( - `/api/searchspaces/${parsedRequest.data.search_space_id}/roles/${parsedRequest.data.role_id}`, + `/api/v1/searchspaces/${parsedRequest.data.search_space_id}/roles/${parsedRequest.data.role_id}`, getRoleByIdResponse, ); }; @@ -81,7 +81,7 @@ class RolesApiService { } return baseApiService.put( - `/api/searchspaces/${parsedRequest.data.search_space_id}/roles/${parsedRequest.data.role_id}`, + `/api/v1/searchspaces/${parsedRequest.data.search_space_id}/roles/${parsedRequest.data.role_id}`, updateRoleResponse, { body: parsedRequest.data.data, @@ -100,7 +100,7 @@ class RolesApiService { } return baseApiService.delete( - `/api/searchspaces/${parsedRequest.data.search_space_id}/roles/${parsedRequest.data.role_id}`, + `/api/v1/searchspaces/${parsedRequest.data.search_space_id}/roles/${parsedRequest.data.role_id}`, deleteRoleResponse, ); }; From 78035a6e90c1053575687a56e5eda4893ee73ed3 Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Tue, 16 Dec 2025 08:44:53 +0000 Subject: [PATCH 41/42] fix: update team page implementation --- surfsense_web/app/dashboard/[search_space_id]/team/page.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/surfsense_web/app/dashboard/[search_space_id]/team/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/team/page.tsx index a5e1b45df..63826b407 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/team/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/team/page.tsx @@ -47,7 +47,7 @@ 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 } from "@/contracts/types/roles.types"; +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"; @@ -110,7 +110,6 @@ import { type Invite, type InviteCreate, type Member, - type Role, useInvites, useMembers, useUserAccess, From 66a467436049166248ddbe1a380f532bdbf4cda7 Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Tue, 16 Dec 2025 09:16:41 +0000 Subject: [PATCH 42/42] fix: restore RBAC cache keys after rebase --- surfsense_web/lib/query-client/cache-keys.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/surfsense_web/lib/query-client/cache-keys.ts b/surfsense_web/lib/query-client/cache-keys.ts index 28e9396c9..6ac7c6a6e 100644 --- a/surfsense_web/lib/query-client/cache-keys.ts +++ b/surfsense_web/lib/query-client/cache-keys.ts @@ -45,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, + }, };