This commit is contained in:
2026-02-17 21:53:14 -06:00
parent 6d951e426d
commit 987a1c8a6a
35 changed files with 1539 additions and 39 deletions
+6
View File
@@ -70,4 +70,10 @@ Purpose: make AI coding agents immediately productive in this repository by desc
- **When editing generated or infra files**: if you need to change `generated/prisma/*` (rare), explain why in the PR and show commands used to regenerate. - **When editing generated or infra files**: if you need to change `generated/prisma/*` (rare), explain why in the PR and show commands used to regenerate.
- **Documentation sync (IMPORTANT)**: Whenever you add, remove, or modify API routes or permission nodes, you **must** update all three of the following files to keep them in sync:
1. `src/types/PermissionNodes.ts` — the single source of truth for all permission node definitions, categories, descriptions, and `usedIn` references.
2. `PERMISSIONS.md` — human-readable documentation of all permission nodes; must strictly reflect the data in `PermissionNodes.ts`.
3. `API_ROUTES.md` — comprehensive documentation of all API routes, including method, path, auth requirements, permissions, request/response examples.
Always verify that new routes have their required permissions listed in `PermissionNodes.ts`, that `PERMISSIONS.md` tables match the TS file exactly, and that `API_ROUTES.md` includes full documentation for every mounted route. Run through all three files at the end of any route or permission change to catch discrepancies.
If anything here is unclear or you'd like more examples (e.g., a walk-through editing a controller + manager + test run), tell me which area to expand and I'll iterate. If anything here is unclear or you'd like more examples (e.g., a walk-through editing a controller + manager + test run), tell me which area to expand and I'll iterate.
+563 -5
View File
@@ -153,6 +153,52 @@ Update the currently authenticated user's information.
--- ---
### Check User Permissions
**POST** `/user/@me/check-permission`
Check if the currently authenticated user has specific permissions. Accepts an array of permission nodes and returns the status for each.
**Authentication Required:** Yes
**Required Scopes:** `user.read`
**Request Body:**
```json
{
"permissions": ["user.read", "company.create", "credential.write"]
}
```
**Response:**
```json
{
"status": 200,
"message": "Permission check completed.",
"data": {
"results": [
{
"permission": "user.read",
"hasPermission": true
},
{
"permission": "company.create",
"hasPermission": false
},
{
"permission": "credential.write",
"hasPermission": true
}
]
},
"successful": true
}
```
---
## Company Routes ## Company Routes
### Get All Companies ### Get All Companies
@@ -203,17 +249,24 @@ Fetch a paginated list of all companies with optional search functionality.
**GET** `/company/companies/:identifier` **GET** `/company/companies/:identifier`
Fetch a single company by its ID. Fetch a single company by its ID. Automatically fetches fresh data from ConnectWise and returns it along with internal company data.
**Authentication Required:** Yes **Authentication Required:** Yes
**Required Permissions:** `company.fetch` **Required Permissions:**
- `company.fetch` (base permission)
- `company.fetch.address` (required when `includeAddress=true`)
**URL Parameters:** **URL Parameters:**
- `identifier` - Company ID - `identifier` - Company ID (internal database ID)
**Response:** **Query Parameters:**
- `includeAddress` (optional) - Set to "true" to include full address information. Requires `company.fetch.address` permission. (default: false)
**Response (without includeAddress):**
```json ```json
{ {
@@ -223,7 +276,34 @@ Fetch a single company by its ID.
"id": "ckx...", "id": "ckx...",
"name": "Acme Corp", "name": "Acme Corp",
"cw_CompanyId": 12345, "cw_CompanyId": 12345,
"cw_Identifier": "AcmeCorp" "cw_Identifier": "AcmeCorp",
"cw_Data": {}
},
"successful": true
}
```
**Response (with includeAddress=true):**
```json
{
"status": 200,
"message": "Company Fetched Successfully!",
"data": {
"id": "ckx...",
"name": "Acme Corp",
"cw_CompanyId": 12345,
"cw_Identifier": "AcmeCorp",
"cw_Data": {
"address": {
"line1": "123 Main St",
"line2": null,
"city": "Springfield",
"state": "IL",
"zip": "62701",
"country": "United States"
}
}
}, },
"successful": true "successful": true
} }
@@ -861,6 +941,484 @@ Fetch all credentials that use a specific credential type.
--- ---
## Role Routes
### Create Role
**POST** `/role`
Create a new role with a title, moniker, and optional permissions.
**Authentication Required:** Yes
**Required Permissions:** `role.create`
**Request Body:**
```json
{
"title": "System Administrator",
"moniker": "system_admin",
"permissions": [
"user.read",
"user.write",
"company.fetch",
"credential.create"
]
}
```
**Response:**
```json
{
"status": 201,
"message": "Role Created Successfully!",
"data": {
"id": "ckx...",
"title": "System Administrator",
"moniker": "system_admin",
"permissions": [
"user.read",
"user.write",
"company.fetch",
"credential.create"
],
"createdAt": "2026-02-17T00:00:00.000Z",
"updatedAt": "2026-02-17T00:00:00.000Z"
},
"successful": true
}
```
---
### Get Role by ID or Moniker
**GET** `/role/:identifier`
Fetch a single role by its ID or moniker.
**Authentication Required:** Yes
**Required Permissions:** `role.read`
**URL Parameters:**
- `identifier` - Role ID or moniker
**Response:**
```json
{
"status": 200,
"message": "Role Fetched Successfully!",
"data": {
"id": "ckx...",
"title": "System Administrator",
"moniker": "system_admin",
"permissions": [
"user.read",
"user.write",
"company.fetch",
"credential.create"
],
"createdAt": "2026-02-17T00:00:00.000Z",
"updatedAt": "2026-02-17T00:00:00.000Z"
},
"successful": true
}
```
---
### Get All Roles
**GET** `/role`
Fetch all roles in the system.
**Authentication Required:** Yes
**Required Permissions:** `role.read`, `role.list`
**Response:**
```json
{
"status": 200,
"message": "Roles Fetched Successfully!",
"data": [
{
"id": "ckx...",
"title": "System Administrator",
"moniker": "system_admin",
"permissions": ["user.read", "user.write"],
"createdAt": "2026-02-17T00:00:00.000Z",
"updatedAt": "2026-02-17T00:00:00.000Z"
},
{
"id": "cky...",
"title": "Viewer",
"moniker": "viewer",
"permissions": ["user.read", "company.fetch"],
"createdAt": "2026-02-17T00:00:00.000Z",
"updatedAt": "2026-02-17T00:00:00.000Z"
}
],
"successful": true
}
```
---
### Update Role
**PATCH** `/role/:identifier`
Update a role's title, moniker, or permissions.
**Authentication Required:** Yes
**Required Permissions:** `role.modify`
**URL Parameters:**
- `identifier` - Role ID or moniker
**Request Body:**
```json
{
"title": "Super Administrator",
"moniker": "super_admin",
"permissions": ["*"]
}
```
**Response:**
```json
{
"status": 200,
"message": "Role Updated Successfully!",
"data": {
"id": "ckx...",
"title": "Super Administrator",
"moniker": "super_admin",
"permissions": ["*"],
"createdAt": "2026-02-17T00:00:00.000Z",
"updatedAt": "2026-02-17T12:00:00.000Z"
},
"successful": true
}
```
---
### Delete Role
**DELETE** `/role/:identifier`
Delete a role. This will remove the role from all users that have it assigned.
**Authentication Required:** Yes
**Required Permissions:** `role.delete`
**URL Parameters:**
- `identifier` - Role ID or moniker
**Response:**
```json
{
"status": 200,
"message": "Role Deleted Successfully!",
"data": {
"id": "ckx...",
"title": "System Administrator",
"moniker": "system_admin",
"createdAt": "2026-02-17T00:00:00.000Z",
"updatedAt": "2026-02-17T12:00:00.000Z"
},
"successful": true
}
```
---
### Add Permissions to Role
**POST** `/role/:identifier/permissions`
Add one or more permissions to an existing role. The new permissions will be merged with existing permissions.
**Authentication Required:** Yes
**Required Permissions:** `role.modify`
**URL Parameters:**
- `identifier` - Role ID or moniker
**Request Body:**
```json
{
"permissions": ["credential.update", "credential.delete"]
}
```
**Response:**
```json
{
"status": 200,
"message": "Permissions Added Successfully!",
"data": {
"id": "ckx...",
"title": "System Administrator",
"moniker": "system_admin",
"permissions": [
"user.read",
"user.write",
"company.fetch",
"credential.create",
"credential.update",
"credential.delete"
],
"createdAt": "2026-02-17T00:00:00.000Z",
"updatedAt": "2026-02-17T12:30:00.000Z"
},
"successful": true
}
```
---
### Remove Permissions from Role
**DELETE** `/role/:identifier/permissions`
Remove one or more permissions from an existing role.
**Authentication Required:** Yes
**Required Permissions:** `role.modify`
**URL Parameters:**
- `identifier` - Role ID or moniker
**Request Body:**
```json
{
"permissions": ["credential.delete"]
}
```
**Response:**
```json
{
"status": 200,
"message": "Permissions Removed Successfully!",
"data": {
"id": "ckx...",
"title": "System Administrator",
"moniker": "system_admin",
"permissions": [
"user.read",
"user.write",
"company.fetch",
"credential.create",
"credential.update"
],
"createdAt": "2026-02-17T00:00:00.000Z",
"updatedAt": "2026-02-17T12:45:00.000Z"
},
"successful": true
}
```
---
### Get Users with Role
**GET** `/role/:identifier/users`
Fetch all users that have been assigned a specific role.
**Authentication Required:** Yes
**Required Permissions:** `role.read`, `user.read`
**URL Parameters:**
- `identifier` - Role ID or moniker
**Response:**
```json
{
"status": 200,
"message": "Users Fetched Successfully!",
"data": [
{
"id": "cku...",
"name": "John Doe",
"login": "john.doe",
"roles": ["ckx..."],
"createdAt": "2026-01-15T00:00:00.000Z",
"updatedAt": "2026-02-10T00:00:00.000Z"
},
{
"id": "ckv...",
"name": "Jane Smith",
"login": "jane.smith",
"roles": ["ckx...", "cky..."],
"createdAt": "2026-01-20T00:00:00.000Z",
"updatedAt": "2026-02-12T00:00:00.000Z"
}
],
"successful": true
}
```
---
## Permission Routes
### Get All Permission Nodes (Categorized)
**GET** `/permissions`
Fetch all permission nodes organized by category. Returns the full permission node definition object with categories as keys.
**Authentication Required:** Yes
**Required Permissions:** `role.read`
**Response:**
```json
{
"status": 200,
"message": "Permission Nodes Fetched Successfully!",
"data": {
"global": {
"name": "Global Permissions",
"description": "Global wildcard permissions that grant access to all resources",
"permissions": [
{
"node": "*",
"description": "Full access to all resources and actions (administrator role)",
"usedIn": []
}
]
},
"company": { "..." },
"credential": { "..." },
"...additional categories": { "..." }
},
"successful": true
}
```
---
### Get All Permission Nodes (Flat)
**GET** `/permissions/nodes`
Fetch a flat array of all permission nodes across all categories.
**Authentication Required:** Yes
**Required Permissions:** `role.read`
**Response:**
```json
{
"status": 200,
"message": "All Permission Nodes Fetched Successfully!",
"data": [
{
"node": "*",
"description": "Full access to all resources and actions (administrator role)",
"usedIn": []
},
{
"node": "company.fetch",
"description": "Fetch a single company",
"usedIn": ["src/api/companies/[id]/fetch.ts"]
},
"...additional nodes"
],
"successful": true
}
```
---
### Get Permission Nodes by Category
**GET** `/permissions/:category`
Fetch all permission nodes for a specific category.
**Authentication Required:** Yes
**Required Permissions:** `role.read`
**Path Parameters:**
- `category` - The category key (e.g., `global`, `company`, `credential`, `credentialType`, `role`, `user`, `permission`, `uiNavigation`, `adminUI`)
**Response (Success):**
```json
{
"status": 200,
"message": "Permission Category Fetched Successfully!",
"data": {
"name": "Company Permissions",
"description": "Permissions for accessing and managing company resources",
"permissions": [
{
"node": "company.fetch",
"description": "Fetch a single company",
"usedIn": ["src/api/companies/[id]/fetch.ts"]
},
{
"node": "company.fetch.address",
"description": "View company address information",
"usedIn": ["src/api/companies/[id]/fetch.ts"],
"dependencies": ["company.fetch"]
}
]
},
"successful": true
}
```
**Response (Not Found):**
```json
{
"status": 404,
"message": "Permission category \"invalidCategory\" not found",
"error": "NotFound",
"successful": false
}
```
---
## Utility Routes ## Utility Routes
### Teapot ### Teapot
+148
View File
@@ -0,0 +1,148 @@
# Permission Nodes
This document lists all known permission nodes in the ttscm-api application, categorized by resource type.
## Permission System Overview
The permission system uses a dot-notation format: `resource.action[.modifier]`
### Special Tokens
The permission validator supports special tokens for flexible permission management:
- **Asterisk (\*)**: Matches the token and all following tokens (e.g., `credential.*` grants all credential permissions)
- **Question Mark (?)**: Matches only the specific token (single character wildcard)
- **Inclusive List ([a,b,c])**: Matches only the tokens in the list
- **Exclusive List (<a,b,c>)**: Matches all tokens except those in the list
### Global Permissions
- `*` - Full access to all resources and actions (typically assigned to administrator role)
## Permission Nodes by Resource
### Company Permissions
| Permission Node | Description | Used In |
| ------------------------------ | ------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
| `company.fetch` | Fetch a single company | [src/api/companies/[id]/fetch.ts](src/api/companies/[id]/fetch.ts) |
| `company.fetch.address` | View company address information (requires `company.fetch` as well) | [src/api/companies/[id]/fetch.ts](src/api/companies/[id]/fetch.ts) |
| `company.fetch.many` | Fetch multiple companies | [src/api/companies/fetchAll.ts](src/api/companies/fetchAll.ts) |
| `company.fetch.configurations` | Fetch company configurations (requires `company.fetch` as well) | [src/api/companies/[id]/configurations.ts](src/api/companies/[id]/configurations.ts) |
### Credential Permissions
| Permission Node | Description | Used In |
| ------------------------------- | ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- |
| `credential.create` | Create a new credential | [src/api/credentials/create.ts](src/api/credentials/create.ts) |
| `credential.fetch` | Fetch a single credential | [src/api/credentials/fetch.ts](src/api/credentials/fetch.ts) |
| `credential.fetch.many` | Fetch multiple credentials | [src/api/credentials/fetchByCompany.ts](src/api/credentials/fetchByCompany.ts) |
| `credential.update` | Update a credential | [src/api/credentials/update.ts](src/api/credentials/update.ts) |
| `credential.delete` | Delete a credential | [src/api/credentials/delete.ts](src/api/credentials/delete.ts) |
| `credential.fields.fetch` | Fetch credential fields (requires `credential.fetch` as well) | [src/api/credentials/fetchFields.ts](src/api/credentials/fetchFields.ts) |
| `credential.fields.update` | Update credential fields (requires `credential.update` as well) | [src/api/credentials/updateFields.ts](src/api/credentials/updateFields.ts) |
| `credential.secure_values.read` | Read secure values of a credential (requires `credential.fetch` as well) | [src/api/credentials/readSecureValues.ts](src/api/credentials/readSecureValues.ts) |
### Credential Type Permissions
| Permission Node | Description | Used In |
| ---------------------------- | ------------------------------- | ---------------------------------------------------------------------------- |
| `credential_type.create` | Create a new credential type | [src/api/credential-types/create.ts](src/api/credential-types/create.ts) |
| `credential_type.fetch` | Fetch a single credential type | [src/api/credential-types/fetch.ts](src/api/credential-types/fetch.ts) |
| `credential_type.fetch.many` | Fetch multiple credential types | [src/api/credential-types/fetchAll.ts](src/api/credential-types/fetchAll.ts) |
| `credential_type.update` | Update a credential type | [src/api/credential-types/update.ts](src/api/credential-types/update.ts) |
| `credential_type.delete` | Delete a credential type | [src/api/credential-types/delete.ts](src/api/credential-types/delete.ts) |
### Role Permissions
| Permission Node | Description | Used In | Dependencies |
| --------------- | ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------ |
| `role.create` | Create a new role | [src/api/roles/create.ts](src/api/roles/create.ts) | |
| `role.read` | Fetch a single role or view role information | [src/api/roles/fetch.ts](src/api/roles/fetch.ts) | |
| `role.list` | Fetch all roles | [src/api/roles/fetchAll.ts](src/api/roles/fetchAll.ts) | `role.read` |
| `role.modify` | Update role properties or manage role permissions | [src/api/roles/update.ts](src/api/roles/update.ts), [src/api/roles/addPermissions.ts](src/api/roles/addPermissions.ts), [src/api/roles/removePermissions.ts](src/api/roles/removePermissions.ts) | |
| `role.delete` | Delete a role | [src/api/roles/delete.ts](src/api/roles/delete.ts) | |
| `user.read` | View users assigned to a role | [src/api/roles/getUsers.ts](src/api/roles/getUsers.ts) | `role.read` |
### User Permissions
| Permission Node | Description | Used In |
| --------------- | ----------------------- | -------------------------------------------------------- |
| `user.read` | Read user information | [src/api/user/@me/fetch.ts](src/api/user/@me/fetch.ts) |
| `user.write` | Update user information | [src/api/user/@me/update.ts](src/api/user/@me/update.ts) |
### Permission Routes
Permissions required for accessing the permission node definitions API.
| Permission Node | Description | Used In |
| --------------- | ------------------------------------------------ | -------------------------------------------------------------------------------- |
| `role.read` | Fetch all permission nodes organized by category | [src/api/permissions/fetchAll.ts](src/api/permissions/fetchAll.ts) |
| `role.read` | Fetch a flat list of all permission nodes | [src/api/permissions/fetchNodes.ts](src/api/permissions/fetchNodes.ts) |
| `role.read` | Fetch permission nodes by category | [src/api/permissions/fetchByCategory.ts](src/api/permissions/fetchByCategory.ts) |
### UI Navigation Permissions
Permissions for controlling navigation visibility on the frontend.
| Permission Node | Description | Usage Pattern |
| ---------------------- | -------------------------------------------------------------------- | ------------------------------------------------- |
| `ui.navigation.*.view` | View specific navigation sections (e.g., `ui.navigation.admin.view`) | Control which navigation menu items are displayed |
### Admin UI Permissions
Admin-specific UI permissions that control visibility and data loading for admin sub-tabs.
| Permission Node | Description | Usage Pattern |
| ----------------------------- | ------------------------------------------------ | ------------------------------------------ |
| `admin.users.view` | Show the Users tab and load user data | Show/hide users tab, allow user list fetch |
| `admin.roles.view` | Show the Roles tab and load role data | Show/hide roles tab, allow role list fetch |
| `admin.credential-types.view` | Show the Credential Types tab and load type data | Show/hide types tab, allow type list fetch |
#### Notes on UI Permissions
- **Client-side validation is not secure**: Always enforce permissions on the API level. UI permissions only control visibility and user experience.
- **Combine with API permissions**: A user with an admin UI permission should also have the corresponding API permission (e.g., `role.list`) to actually load data.
- **Use wildcards for flexibility**: Grant `ui.navigation.*.view` to allow all navigation sections.
## Permission Issuers
Permissions can be issued by different sources:
- `roles` - Permissions granted through role assignment
- `user` - Permissions granted directly to a user
- `api_key` - Permissions associated with an API key
## Permission Validation
The authorization middleware ([src/api/middleware/authorization.ts](src/api/middleware/authorization.ts)) enforces permissions by:
1. Extracting the authorization header (Bearer token or API Key)
2. Validating the session/token
3. Checking if the user has all required permissions for the route
4. Throwing an `InsufficentPermission` error (403) if any required permission is missing
## Usage Example
```typescript
// Require single permission
authMiddleware({ permissions: ["credential.fetch"] })
// Require multiple permissions (all must be satisfied)
authMiddleware({
permissions: ["credential.fetch", "credential.secure_values.read"]
})
// Administrator role with wildcard permission
{
moniker: "administrator",
permissions: ["*"] // Grants all permissions
}
```
## Notes
- Multiple permissions in a single route require **all** permissions to be satisfied (AND logic)
- The `*` wildcard permission grants access to everything in the application
- Permissions are signed using JWT with a private key for integrity
- Permission validation supports pattern matching for flexible permission management
+21
View File
@@ -0,0 +1,21 @@
meta {
name: Check User Permission
type: http
seq: 3
}
post {
url: {{baseUrl}}/v1/user/@me/check-permission
body: json
auth: none
}
headers {
Content-Type: application/json
}
body:json {
{
"permissions": ["user.read", "company.create", "credential.write"]
}
}
+1 -1
View File
@@ -1,5 +1,5 @@
{ {
"name": "ttscm-api", "name": "tts-optima-api",
"homepage": "https://totaltech.net", "homepage": "https://totaltech.net",
"author": { "author": {
"name": "Jackson Roberts", "name": "Jackson Roberts",
+15 -1
View File
@@ -4,6 +4,7 @@ import { companies } from "../../../managers/companies";
import { apiResponse } from "../../../modules/api-utils/apiResponse"; import { apiResponse } from "../../../modules/api-utils/apiResponse";
import { ContentfulStatusCode } from "hono/utils/http-status"; import { ContentfulStatusCode } from "hono/utils/http-status";
import { authMiddleware } from "../../middleware/authorization"; import { authMiddleware } from "../../middleware/authorization";
import GenericError from "../../../Errors/GenericError";
/* /v1/company/companies/[id] */ /* /v1/company/companies/[id] */
export default createRoute( export default createRoute(
@@ -12,10 +13,23 @@ export default createRoute(
async (c) => { async (c) => {
const company = await companies.fetch(c.req.param("identifier")); const company = await companies.fetch(c.req.param("identifier"));
const includeAddress = c.req.query("includeAddress") === "true";
// Check for address-specific permission if includeAddress is requested
if (includeAddress) {
const user = c.get("user");
if (!user || !(await user.hasPermission("company.fetch.address"))) {
throw new GenericError({
name: "InsufficientPermission",
message: "You do not have permission to view company addresses.",
status: 403,
});
}
}
const response = apiResponse.successful( const response = apiResponse.successful(
"Company Fetched Successfully!", "Company Fetched Successfully!",
company, company.toJson({ includeAddress }),
); );
return c.json(response, response.status as ContentfulStatusCode); return c.json(response, response.status as ContentfulStatusCode);
}, },
+25
View File
@@ -0,0 +1,25 @@
import { Hono } from "hono/tiny";
import { createRoute } from "../../modules/api-utils/createRoute";
import { companies } from "../../managers/companies";
import { apiResponse } from "../../modules/api-utils/apiResponse";
import { ContentfulStatusCode } from "hono/utils/http-status";
import { authMiddleware } from "../middleware/authorization";
/* /v1/company/count */
export default createRoute(
"get",
["/count"],
async (c) => {
const count = await companies.count();
const response = apiResponse.successful(
"Company count fetched successfully!",
{
count,
},
);
return c.json(response, response.status as ContentfulStatusCode);
},
authMiddleware({ permissions: ["company.fetch.many"] }),
);
+2 -1
View File
@@ -1,5 +1,6 @@
import { default as fetchAll } from "./fetchAll"; import { default as fetchAll } from "./fetchAll";
import { default as fetch } from "./[id]/fetch"; import { default as fetch } from "./[id]/fetch";
import { default as configurations } from "./[id]/configurations"; import { default as configurations } from "./[id]/configurations";
import { default as count } from "./count";
export { configurations, fetch, fetchAll }; export { configurations, count, fetch, fetchAll };
+20
View File
@@ -0,0 +1,20 @@
import { createRoute } from "../../modules/api-utils/createRoute";
import { apiResponse } from "../../modules/api-utils/apiResponse";
import { ContentfulStatusCode } from "hono/utils/http-status";
import { authMiddleware } from "../middleware/authorization";
import { PERMISSION_NODES } from "../../types/PermissionNodes";
/* /v1/permissions */
export default createRoute(
"get",
["/"],
async (c) => {
const response = apiResponse.successful(
"Permission Nodes Fetched Successfully!",
PERMISSION_NODES,
);
return c.json(response, response.status as ContentfulStatusCode);
},
authMiddleware({ permissions: ["role.read"] }),
);
+34
View File
@@ -0,0 +1,34 @@
import { createRoute } from "../../modules/api-utils/createRoute";
import { apiResponse } from "../../modules/api-utils/apiResponse";
import { ContentfulStatusCode } from "hono/utils/http-status";
import { authMiddleware } from "../middleware/authorization";
import { PERMISSION_NODES } from "../../types/PermissionNodes";
import GenericError from "../../Errors/GenericError";
/* /v1/permissions/:category */
export default createRoute(
"get",
["/:category"],
async (c) => {
const categoryKey = c.req.param(
"category",
) as keyof typeof PERMISSION_NODES;
if (!(categoryKey in PERMISSION_NODES)) {
throw new GenericError({
name: "NotFound",
message: `Permission category "${categoryKey}" not found`,
status: 404,
cause: `Valid categories: ${Object.keys(PERMISSION_NODES).join(", ")}`,
});
}
const response = apiResponse.successful(
"Permission Category Fetched Successfully!",
PERMISSION_NODES[categoryKey],
);
return c.json(response, response.status as ContentfulStatusCode);
},
authMiddleware({ permissions: ["role.read"] }),
);
+22
View File
@@ -0,0 +1,22 @@
import { createRoute } from "../../modules/api-utils/createRoute";
import { apiResponse } from "../../modules/api-utils/apiResponse";
import { ContentfulStatusCode } from "hono/utils/http-status";
import { authMiddleware } from "../middleware/authorization";
import { getAllPermissionNodes } from "../../types/PermissionNodes";
/* /v1/permissions/nodes */
export default createRoute(
"get",
["/nodes"],
async (c) => {
const allNodes = getAllPermissionNodes();
const response = apiResponse.successful(
"All Permission Nodes Fetched Successfully!",
allNodes,
);
return c.json(response, response.status as ContentfulStatusCode);
},
authMiddleware({ permissions: ["role.read"] }),
);
+5
View File
@@ -0,0 +1,5 @@
import { default as fetchAll } from "./fetchAll";
import { default as fetchByCategory } from "./fetchByCategory";
import { default as fetchNodes } from "./fetchNodes";
export { fetchAll, fetchByCategory, fetchNodes };
+36
View File
@@ -0,0 +1,36 @@
import { Hono } from "hono/tiny";
import { createRoute } from "../../modules/api-utils/createRoute";
import { roles } from "../../managers/roles";
import { apiResponse } from "../../modules/api-utils/apiResponse";
import { ContentfulStatusCode } from "hono/utils/http-status";
import { authMiddleware } from "../middleware/authorization";
import { z } from "zod";
/* POST /v1/role/:identifier/permissions */
export default createRoute(
"post",
["/:identifier/permissions"],
async (c) => {
const identifier = c.req.param("identifier");
const body = await c.req.json();
const schema = z.object({
permissions: z
.array(z.string().min(1, "Permission node cannot be empty"))
.min(1, "At least one permission is required"),
});
const data = schema.parse(body);
const role = await roles.fetch(identifier);
await role.addPermissions(...data.permissions);
const response = apiResponse.successful(
"Permissions Added Successfully!",
role.toJson({ viewPermissions: true }),
);
return c.json(response, response.status as ContentfulStatusCode);
},
authMiddleware({ permissions: ["role.update"] }),
);
+36
View File
@@ -0,0 +1,36 @@
import { Hono } from "hono/tiny";
import { createRoute } from "../../modules/api-utils/createRoute";
import { roles } from "../../managers/roles";
import { apiResponse } from "../../modules/api-utils/apiResponse";
import { ContentfulStatusCode } from "hono/utils/http-status";
import { authMiddleware } from "../middleware/authorization";
import { z } from "zod";
/* POST /v1/role */
export default createRoute(
"post",
["/"],
async (c) => {
const body = await c.req.json();
const schema = z.object({
title: z.string().min(1, "Title is required"),
moniker: z.string().min(1, "Moniker is required"),
permissions: z
.array(z.string().min(1, "Permission node cannot be empty"))
.optional(),
});
const data = schema.parse(body);
const role = await roles.create(data);
const response = apiResponse.created(
"Role Created Successfully!",
role.toJson({ viewPermissions: true }),
);
return c.json(response, response.status as ContentfulStatusCode);
},
authMiddleware({ permissions: ["role.create"] }),
);
+26
View File
@@ -0,0 +1,26 @@
import { Hono } from "hono/tiny";
import { createRoute } from "../../modules/api-utils/createRoute";
import { roles } from "../../managers/roles";
import { apiResponse } from "../../modules/api-utils/apiResponse";
import { ContentfulStatusCode } from "hono/utils/http-status";
import { authMiddleware } from "../middleware/authorization";
/* DELETE /v1/role/:identifier */
export default createRoute(
"delete",
["/:identifier"],
async (c) => {
const identifier = c.req.param("identifier");
const role = await roles.fetch(identifier);
await role.delete();
const response = apiResponse.successful(
"Role Deleted Successfully!",
role.toJson(),
);
return c.json(response, response.status as ContentfulStatusCode);
},
authMiddleware({ permissions: ["role.delete"] }),
);
+25
View File
@@ -0,0 +1,25 @@
import { Hono } from "hono/tiny";
import { createRoute } from "../../modules/api-utils/createRoute";
import { roles } from "../../managers/roles";
import { apiResponse } from "../../modules/api-utils/apiResponse";
import { ContentfulStatusCode } from "hono/utils/http-status";
import { authMiddleware } from "../middleware/authorization";
/* GET /v1/role/:identifier */
export default createRoute(
"get",
["/:identifier"],
async (c) => {
const identifier = c.req.param("identifier");
const role = await roles.fetch(identifier);
const response = apiResponse.successful(
"Role Fetched Successfully!",
role.toJson({ viewPermissions: true }),
);
return c.json(response, response.status as ContentfulStatusCode);
},
authMiddleware({ permissions: ["role.read"] }),
);
+27
View File
@@ -0,0 +1,27 @@
import { Hono } from "hono/tiny";
import { createRoute } from "../../modules/api-utils/createRoute";
import { roles } from "../../managers/roles";
import { apiResponse } from "../../modules/api-utils/apiResponse";
import { ContentfulStatusCode } from "hono/utils/http-status";
import { authMiddleware } from "../middleware/authorization";
/* GET /v1/role */
export default createRoute(
"get",
["/"],
async (c) => {
const allRoles = await roles.fetchAllRoles();
const rolesArray = allRoles.map((role) =>
role.toJson({ viewPermissions: true }),
);
const response = apiResponse.successful(
"Roles Fetched Successfully!",
rolesArray,
);
return c.json(response, response.status as ContentfulStatusCode);
},
authMiddleware({ permissions: ["role.read", "role.list"] }),
);
+28
View File
@@ -0,0 +1,28 @@
import { Hono } from "hono/tiny";
import { createRoute } from "../../modules/api-utils/createRoute";
import { roles } from "../../managers/roles";
import { apiResponse } from "../../modules/api-utils/apiResponse";
import { ContentfulStatusCode } from "hono/utils/http-status";
import { authMiddleware } from "../middleware/authorization";
/* GET /v1/role/:identifier/users */
export default createRoute(
"get",
["/:identifier/users"],
async (c) => {
const identifier = c.req.param("identifier");
const role = await roles.fetch(identifier);
const users = role.getUsers();
const usersArray = users.map((user) => user.toJson());
const response = apiResponse.successful(
"Users Fetched Successfully!",
usersArray,
);
return c.json(response, response.status as ContentfulStatusCode);
},
authMiddleware({ permissions: ["role.read", "user.read"] }),
);
+8
View File
@@ -0,0 +1,8 @@
export { default as create } from "./create";
export { default as fetch } from "./fetch";
export { default as fetchAll } from "./fetchAll";
export { default as update } from "./update";
export { default as deleteRole } from "./delete";
export { default as addPermissions } from "./addPermissions";
export { default as removePermissions } from "./removePermissions";
export { default as getUsers } from "./getUsers";
+36
View File
@@ -0,0 +1,36 @@
import { Hono } from "hono/tiny";
import { createRoute } from "../../modules/api-utils/createRoute";
import { roles } from "../../managers/roles";
import { apiResponse } from "../../modules/api-utils/apiResponse";
import { ContentfulStatusCode } from "hono/utils/http-status";
import { authMiddleware } from "../middleware/authorization";
import { z } from "zod";
/* DELETE /v1/role/:identifier/permissions */
export default createRoute(
"delete",
["/:identifier/permissions"],
async (c) => {
const identifier = c.req.param("identifier");
const body = await c.req.json();
const schema = z.object({
permissions: z
.array(z.string().min(1, "Permission node cannot be empty"))
.min(1, "At least one permission is required"),
});
const data = schema.parse(body);
const role = await roles.fetch(identifier);
await role.removePermissions(...data.permissions);
const response = apiResponse.successful(
"Permissions Removed Successfully!",
role.toJson({ viewPermissions: true }),
);
return c.json(response, response.status as ContentfulStatusCode);
},
authMiddleware({ permissions: ["role.update"] }),
);
+41
View File
@@ -0,0 +1,41 @@
import { Hono } from "hono/tiny";
import { createRoute } from "../../modules/api-utils/createRoute";
import { roles } from "../../managers/roles";
import { apiResponse } from "../../modules/api-utils/apiResponse";
import { ContentfulStatusCode } from "hono/utils/http-status";
import { authMiddleware } from "../middleware/authorization";
import { z } from "zod";
/* PATCH /v1/role/:identifier */
export default createRoute(
"patch",
["/:identifier"],
async (c) => {
const identifier = c.req.param("identifier");
const body = await c.req.json();
const schema = z
.object({
title: z.string().min(1, "Title cannot be empty"),
moniker: z.string().min(1, "Moniker cannot be empty"),
permissions: z.array(
z.string().min(1, "Permission node cannot be empty"),
),
})
.partial()
.strict();
const data = schema.parse(body);
const role = await roles.fetch(identifier);
await role.update(data);
const response = apiResponse.successful(
"Role Updated Successfully!",
role.toJson({ viewPermissions: true }),
);
return c.json(response, response.status as ContentfulStatusCode);
},
authMiddleware({ permissions: ["role.update"] }),
);
+7
View File
@@ -0,0 +1,7 @@
import { Hono } from "hono";
import * as permissionRoutes from "../permissions";
const permissionRouter = new Hono();
Object.values(permissionRoutes).map((r) => permissionRouter.route("/", r));
export default permissionRouter;
+7
View File
@@ -0,0 +1,7 @@
import { Hono } from "hono";
import * as roleRoutes from "../roles";
const roleRouter = new Hono();
Object.values(roleRoutes).map((r) => roleRouter.route("/", r));
export default roleRouter;
+2
View File
@@ -52,6 +52,8 @@ v1.route("/user", require("./routers/user").default);
v1.route("/company", require("./routers/companyRouter").default); v1.route("/company", require("./routers/companyRouter").default);
v1.route("/credential", require("./routers/credentialRouter").default); v1.route("/credential", require("./routers/credentialRouter").default);
v1.route("/credential-type", require("./routers/credentialTypeRouter").default); v1.route("/credential-type", require("./routers/credentialTypeRouter").default);
v1.route("/role", require("./routers/roleRouter").default);
v1.route("/permissions", require("./routers/permissionRouter").default);
app.route("/v1", v1); app.route("/v1", v1);
export default app; export default app;
+37
View File
@@ -0,0 +1,37 @@
import { ContentfulStatusCode } from "hono/utils/http-status";
import { z } from "zod";
import { apiResponse } from "../../../modules/api-utils/apiResponse";
import { createRoute } from "../../../modules/api-utils/createRoute";
import { authMiddleware } from "../../middleware/authorization";
const checkPermissionSchema = z.object({
permissions: z
.array(z.string().min(1, "Permission node cannot be empty"))
.min(1, "At least one permission is required"),
});
// /v1/user/@me/check-permission
export default createRoute(
"post",
["/@me/check-permission"],
async (c) => {
const user = c.get("user");
const body = await c.req.json();
const { permissions } = checkPermissionSchema.parse(body);
const results = await Promise.all(
permissions.map(async (permission) => ({
permission,
hasPermission: await user.hasPermission(permission),
})),
);
const response = apiResponse.successful("Permission check completed.", {
results,
});
return c.json(response, response.status as ContentfulStatusCode);
},
authMiddleware({ scopes: ["user.read"] }),
);
+1
View File
@@ -1,2 +1,3 @@
export { default as fetch } from "./fetch"; export { default as fetch } from "./fetch";
export { default as update } from "./update"; export { default as update } from "./update";
export { default as checkPermission } from "./checkPermission";
+15 -3
View File
@@ -1,6 +1,6 @@
import { Company } from "../../generated/prisma/client"; import { Company } from "../../generated/prisma/client";
import { fetchCwCompanyById } from "../modules/cw-utils/fetchCompany"; import { fetchCwCompanyById } from "../modules/cw-utils/fetchCompany";
import { fetchCompanyConfigurations } from "../modules/cw-utils/fetchCompanyConfigurations"; import { fetchCompanyConfigurations } from "../modules/cw-utils/configurations/fetchCompanyConfigurations";
import { updateCwInternalCompany } from "../modules/cw-utils/updateCompany"; import { updateCwInternalCompany } from "../modules/cw-utils/updateCompany";
import { Company as CWCompany } from "../types/ConnectWiseTypes"; import { Company as CWCompany } from "../types/ConnectWiseTypes";
@@ -16,12 +16,14 @@ export class CompanyController {
public name: string; public name: string;
public readonly cw_Identifier: string; public readonly cw_Identifier: string;
public readonly cw_CompanyId: number; public readonly cw_CompanyId: number;
public readonly cw_Data?: CWCompany;
constructor(companyData: Company) { constructor(companyData: Company, cwData?: CWCompany) {
this.id = companyData.id; this.id = companyData.id;
this.name = companyData.name; this.name = companyData.name;
this.cw_Identifier = companyData.cw_Identifier; this.cw_Identifier = companyData.cw_Identifier;
this.cw_CompanyId = companyData.cw_CompanyId; this.cw_CompanyId = companyData.cw_CompanyId;
this.cw_Data = cwData;
} }
/** /**
@@ -65,12 +67,22 @@ export class CompanyController {
return data; return data;
} }
public toJson() { public toJson(opts?: { includeAddress: boolean }) {
return { return {
id: this.id, id: this.id,
name: this.name, name: this.name,
cw_Identifier: this.cw_Identifier, cw_Identifier: this.cw_Identifier,
cw_CompanyId: this.cw_CompanyId, cw_CompanyId: this.cw_CompanyId,
cw_Data: {
address: {
line1: this.cw_Data?.addressLine1,
line2: this.cw_Data?.addressLine2 ?? null,
city: this.cw_Data?.city,
state: this.cw_Data?.state,
zip: this.cw_Data?.zip,
country: this.cw_Data?.country.name,
},
},
}; };
} }
} }
+1 -1
View File
@@ -265,7 +265,7 @@ export class RoleController {
where: { moniker: data.moniker }, where: { moniker: data.moniker },
}); });
if (checkMoniker) if (checkMoniker && checkMoniker.moniker !== this.moniker)
throw new RoleError( throw new RoleError(
"Moniker is already taken.", "Moniker is already taken.",
"Another role with this moniker already exists in the databse.", "Another role with this moniker already exists in the databse.",
+6 -2
View File
@@ -1,5 +1,6 @@
import { prisma } from "../constants"; import { connectWiseApi, prisma } from "../constants";
import { CompanyController } from "../controllers/CompanyController"; import { CompanyController } from "../controllers/CompanyController";
import { Company } from "../types/ConnectWiseTypes";
export const companies = { export const companies = {
async fetch(identifier: string | number): Promise<CompanyController> { async fetch(identifier: string | number): Promise<CompanyController> {
@@ -11,7 +12,10 @@ export const companies = {
if (!search) throw new Error("Unknown company."); if (!search) throw new Error("Unknown company.");
return new CompanyController(search); const freshCwData = await connectWiseApi.get(
`/company/companies/${search.cw_CompanyId}`,
);
return new CompanyController(search, freshCwData.data);
}, },
async count() { async count() {
@@ -1,10 +1,10 @@
import { connectWiseApi } from "../../constants"; import { connectWiseApi } from "../../../constants";
import { ConfigurationResponse } from "../../types/ConnectWiseTypes"; import { ConfigurationResponse } from "../../../types/ConnectWiseTypes";
import { import {
processConfigurationResponse, processConfigurationResponse,
ProcessedConfiguration, ProcessedConfiguration,
} from "./processConfigurationResponse"; } from "./processConfigurationResponse";
import GenericError from "../../Errors/GenericError"; import GenericError from "../../../Errors/GenericError";
export const fetchCompanyConfigurations = async ( export const fetchCompanyConfigurations = async (
cwCompanyId: number, cwCompanyId: number,
@@ -0,0 +1,29 @@
import { ConfigurationResponse } from "../../../types/ConnectWiseTypes";
export type ProcessedConfiguration = ReturnType<
typeof processConfigurationResponse
>;
export const processConfigurationResponse = (c: ConfigurationResponse) => {
return c.map((item) => ({
id: item.id,
name: item.name,
active: item.activeFlag,
serialNumber: item.serialNumber,
type: item.type,
notes: item.notes,
status: {
id: item.status.id,
name: item.status.name,
},
questions: !item.questions
? null
: item.questions.map((q) => ({
id: q.questionId,
question: q.question,
answer: q.answer,
fieldType: q.fieldType,
})),
info: item._info,
}));
};
@@ -1,22 +0,0 @@
import { ConfigurationResponse } from "../../types/ConnectWiseTypes";
export type ProcessedConfiguration = ReturnType<
typeof processConfigurationResponse
>;
export const processConfigurationResponse = (c: ConfigurationResponse) => {
return c.map((item) => ({
id: item.id,
name: item.name,
active: item.activeFlag,
serialNumber: item.serialNumber,
type: item.type,
questions: item.questions.map((q) => ({
id: q.questionId,
question: q.question,
answer: q.answer,
fieldType: q.fieldType,
})),
info: item._info,
}));
};
+2
View File
@@ -4,9 +4,11 @@ export interface Company {
name: string; name: string;
status: CompanyStatus; status: CompanyStatus;
addressLine1: string; addressLine1: string;
addressLine2?: string;
city: string; city: string;
state: string; state: string;
zip: string; zip: string;
country: BasicEntity;
phoneNumber: string; phoneNumber: string;
faxNumber: string; faxNumber: string;
website: string; website: string;
+304
View File
@@ -0,0 +1,304 @@
/**
* Permission Nodes - Centralized definition of all permission nodes
* organized by category with descriptions and usage information.
*
* Format: resource.action[.modifier]
* Special tokens: * (matches all), ? (single char wildcard), [a,b,c] (inclusive), <a,b,c> (exclusive)
*/
export interface PermissionNode {
/** The permission node identifier (e.g., "company.fetch") */
node: string;
/** Description of what this permission allows */
description: string;
/** File paths where this permission is used */
usedIn: string[];
/** Dependencies - other permissions that must be granted alongside this one */
dependencies?: string[];
}
export interface PermissionCategory {
/** Category name */
name: string;
/** Category description */
description: string;
/** Permission nodes in this category */
permissions: PermissionNode[];
}
export const PERMISSION_NODES = {
global: {
name: "Global Permissions",
description:
"Global wildcard permissions that grant access to all resources",
permissions: [
{
node: "*",
description:
"Full access to all resources and actions (administrator role)",
usedIn: [],
},
],
},
company: {
name: "Company Permissions",
description: "Permissions for accessing and managing company resources",
permissions: [
{
node: "company.fetch",
description: "Fetch a single company",
usedIn: ["src/api/companies/[id]/fetch.ts"],
},
{
node: "company.fetch.address",
description: "View company address information",
usedIn: ["src/api/companies/[id]/fetch.ts"],
dependencies: ["company.fetch"],
},
{
node: "company.fetch.many",
description: "Fetch multiple companies",
usedIn: ["src/api/companies/fetchAll.ts"],
},
{
node: "company.fetch.configurations",
description: "Fetch company configurations",
usedIn: ["src/api/companies/[id]/configurations.ts"],
dependencies: ["company.fetch"],
},
],
},
credential: {
name: "Credential Permissions",
description: "Permissions for managing credentials and their fields",
permissions: [
{
node: "credential.create",
description: "Create a new credential",
usedIn: ["src/api/credentials/create.ts"],
},
{
node: "credential.fetch",
description: "Fetch a single credential",
usedIn: ["src/api/credentials/fetch.ts"],
},
{
node: "credential.fetch.many",
description: "Fetch multiple credentials",
usedIn: ["src/api/credentials/fetchByCompany.ts"],
},
{
node: "credential.update",
description: "Update a credential",
usedIn: ["src/api/credentials/update.ts"],
},
{
node: "credential.delete",
description: "Delete a credential",
usedIn: ["src/api/credentials/delete.ts"],
},
{
node: "credential.fields.fetch",
description: "Fetch credential fields",
usedIn: ["src/api/credentials/fetchFields.ts"],
dependencies: ["credential.fetch"],
},
{
node: "credential.fields.update",
description: "Update credential fields",
usedIn: ["src/api/credentials/updateFields.ts"],
dependencies: ["credential.update"],
},
{
node: "credential.secure_values.read",
description: "Read secure values of a credential",
usedIn: ["src/api/credentials/readSecureValues.ts"],
dependencies: ["credential.fetch"],
},
],
},
credentialType: {
name: "Credential Type Permissions",
description: "Permissions for managing credential types and definitions",
permissions: [
{
node: "credential_type.create",
description: "Create a new credential type",
usedIn: ["src/api/credential-types/create.ts"],
},
{
node: "credential_type.fetch",
description: "Fetch a single credential type",
usedIn: ["src/api/credential-types/fetch.ts"],
},
{
node: "credential_type.fetch.many",
description: "Fetch multiple credential types",
usedIn: ["src/api/credential-types/fetchAll.ts"],
},
{
node: "credential_type.update",
description: "Update a credential type",
usedIn: ["src/api/credential-types/update.ts"],
},
{
node: "credential_type.delete",
description: "Delete a credential type",
usedIn: ["src/api/credential-types/delete.ts"],
},
],
},
permission: {
name: "Permission Permissions",
description: "Permissions for viewing permission node definitions",
permissions: [
{
node: "role.read",
description: "Fetch all permission nodes organized by category",
usedIn: ["src/api/permissions/fetchAll.ts"],
},
{
node: "role.read",
description: "Fetch a flat list of all permission nodes",
usedIn: ["src/api/permissions/fetchNodes.ts"],
},
{
node: "role.read",
description: "Fetch permission nodes by category",
usedIn: ["src/api/permissions/fetchByCategory.ts"],
},
],
},
role: {
name: "Role Permissions",
description: "Permissions for managing roles and role assignments",
permissions: [
{
node: "role.create",
description: "Create a new role",
usedIn: ["src/api/roles/create.ts"],
},
{
node: "role.read",
description: "Fetch a single role or view role information",
usedIn: ["src/api/roles/fetch.ts"],
},
{
node: "role.list",
description: "Fetch all roles",
usedIn: ["src/api/roles/fetchAll.ts"],
dependencies: ["role.read"],
},
{
node: "role.update",
description: "Update role properties or manage role permissions",
usedIn: [
"src/api/roles/update.ts",
"src/api/roles/addPermissions.ts",
"src/api/roles/removePermissions.ts",
],
},
{
node: "role.delete",
description: "Delete a role",
usedIn: ["src/api/roles/delete.ts"],
},
{
node: "user.read",
description: "View users assigned to a role",
usedIn: ["src/api/roles/getUsers.ts"],
dependencies: ["role.read"],
},
],
},
user: {
name: "User Permissions",
description: "Permissions for user profile and information management",
permissions: [
{
node: "user.read",
description: "Read user information",
usedIn: ["src/api/user/@me/fetch.ts"],
},
{
node: "user.write",
description: "Update user information",
usedIn: ["src/api/user/@me/update.ts"],
},
],
},
uiNavigation: {
name: "UI Navigation Permissions",
description: "Permissions for controlling navigation visibility",
permissions: [
{
node: "ui.navigation.*.view",
description:
"View specific navigation sections (e.g., ui.navigation.admin.view)",
usedIn: [],
},
],
},
adminUI: {
name: "Admin UI Permissions",
description:
"Admin-specific UI permissions that control visibility of admin sub-tabs",
permissions: [
{
node: "admin.users.view",
description: "Show the Users tab and load user data",
usedIn: [],
},
{
node: "admin.roles.view",
description: "Show the Roles tab and load role data",
usedIn: [],
},
{
node: "admin.credential-types.view",
description: "Show the Credential Types tab and load type data",
usedIn: [],
},
],
},
} as const satisfies Record<string, PermissionCategory>;
/**
* Utility function to get all permission nodes flattened into a single array
*/
export function getAllPermissionNodes(): PermissionNode[] {
return Object.values(PERMISSION_NODES).flatMap(
(category) => category.permissions as PermissionNode[],
);
}
/**
* Utility function to get all permission node strings
*/
export function getAllPermissionStrings(): string[] {
return getAllPermissionNodes().map((p) => p.node);
}
/**
* Utility function to get a specific permission node by its identifier
*/
export function getPermissionNode(nodeId: string): PermissionNode | undefined {
return getAllPermissionNodes().find((p) => p.node === nodeId);
}
/**
* Utility function to get all permissions in a specific category
*/
export function getPermissionsByCategory(
categoryKey: keyof typeof PERMISSION_NODES,
): PermissionNode[] {
return PERMISSION_NODES[categoryKey].permissions;
}