From 987a1c8a6ab3e4266d94d8d003ad4027a8bd6c20 Mon Sep 17 00:00:00 2001 From: Jackson Roberts Date: Tue, 17 Feb 2026 21:53:14 -0600 Subject: [PATCH] roles --- .github/copilot-instructions.md | 6 + API_ROUTES.md | 568 +++++++++++++++++- PERMISSIONS.md | 148 +++++ bruno/ttscm/Check User Permission.bru | 21 + package.json | 2 +- src/api/companies/[id]/fetch.ts | 16 +- src/api/companies/count.ts | 25 + src/api/companies/index.ts | 3 +- src/api/permissions/fetchAll.ts | 20 + src/api/permissions/fetchByCategory.ts | 34 ++ src/api/permissions/fetchNodes.ts | 22 + src/api/permissions/index.ts | 5 + src/api/roles/addPermissions.ts | 36 ++ src/api/roles/create.ts | 36 ++ src/api/roles/delete.ts | 26 + src/api/roles/fetch.ts | 25 + src/api/roles/fetchAll.ts | 27 + src/api/roles/getUsers.ts | 28 + src/api/roles/index.ts | 8 + src/api/roles/removePermissions.ts | 36 ++ src/api/roles/update.ts | 41 ++ src/api/routers/permissionRouter.ts | 7 + src/api/routers/roleRouter.ts | 7 + src/api/server.ts | 2 + src/api/user/@me/checkPermission.ts | 37 ++ src/api/user/@me/index.ts | 1 + src/controllers/CompanyController.ts | 18 +- src/controllers/RoleController.ts | 2 +- src/managers/companies.ts | 8 +- .../fetchCompanyConfigurations.ts | 6 +- .../fetchSingleConfiguration.ts | 0 .../processConfigurationResponse.ts | 29 + .../cw-utils/processConfigurationResponse.ts | 22 - src/types/ConnectWiseTypes.ts | 2 + src/types/PermissionNodes.ts | 304 ++++++++++ 35 files changed, 1539 insertions(+), 39 deletions(-) create mode 100644 PERMISSIONS.md create mode 100644 bruno/ttscm/Check User Permission.bru create mode 100644 src/api/companies/count.ts create mode 100644 src/api/permissions/fetchAll.ts create mode 100644 src/api/permissions/fetchByCategory.ts create mode 100644 src/api/permissions/fetchNodes.ts create mode 100644 src/api/permissions/index.ts create mode 100644 src/api/roles/addPermissions.ts create mode 100644 src/api/roles/create.ts create mode 100644 src/api/roles/delete.ts create mode 100644 src/api/roles/fetch.ts create mode 100644 src/api/roles/fetchAll.ts create mode 100644 src/api/roles/getUsers.ts create mode 100644 src/api/roles/index.ts create mode 100644 src/api/roles/removePermissions.ts create mode 100644 src/api/roles/update.ts create mode 100644 src/api/routers/permissionRouter.ts create mode 100644 src/api/routers/roleRouter.ts create mode 100644 src/api/user/@me/checkPermission.ts rename src/modules/cw-utils/{ => configurations}/fetchCompanyConfigurations.ts (82%) create mode 100644 src/modules/cw-utils/configurations/fetchSingleConfiguration.ts create mode 100644 src/modules/cw-utils/configurations/processConfigurationResponse.ts delete mode 100644 src/modules/cw-utils/processConfigurationResponse.ts create mode 100644 src/types/PermissionNodes.ts diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 451e0d3..c46371c 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -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. +- **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. diff --git a/API_ROUTES.md b/API_ROUTES.md index d3878fd..91f734e 100644 --- a/API_ROUTES.md +++ b/API_ROUTES.md @@ -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 ### Get All Companies @@ -203,17 +249,24 @@ Fetch a paginated list of all companies with optional search functionality. **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 -**Required Permissions:** `company.fetch` +**Required Permissions:** + +- `company.fetch` (base permission) +- `company.fetch.address` (required when `includeAddress=true`) **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 { @@ -223,7 +276,34 @@ Fetch a single company by its ID. "id": "ckx...", "name": "Acme Corp", "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 } @@ -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 ### Teapot diff --git a/PERMISSIONS.md b/PERMISSIONS.md new file mode 100644 index 0000000..f8b11e7 --- /dev/null +++ b/PERMISSIONS.md @@ -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 ()**: 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 diff --git a/bruno/ttscm/Check User Permission.bru b/bruno/ttscm/Check User Permission.bru new file mode 100644 index 0000000..a3823a9 --- /dev/null +++ b/bruno/ttscm/Check User Permission.bru @@ -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"] + } +} diff --git a/package.json b/package.json index dc13047..49c234d 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "ttscm-api", + "name": "tts-optima-api", "homepage": "https://totaltech.net", "author": { "name": "Jackson Roberts", diff --git a/src/api/companies/[id]/fetch.ts b/src/api/companies/[id]/fetch.ts index d7dfad4..168f789 100644 --- a/src/api/companies/[id]/fetch.ts +++ b/src/api/companies/[id]/fetch.ts @@ -4,6 +4,7 @@ import { companies } from "../../../managers/companies"; import { apiResponse } from "../../../modules/api-utils/apiResponse"; import { ContentfulStatusCode } from "hono/utils/http-status"; import { authMiddleware } from "../../middleware/authorization"; +import GenericError from "../../../Errors/GenericError"; /* /v1/company/companies/[id] */ export default createRoute( @@ -12,10 +13,23 @@ export default createRoute( async (c) => { 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( "Company Fetched Successfully!", - company, + company.toJson({ includeAddress }), ); return c.json(response, response.status as ContentfulStatusCode); }, diff --git a/src/api/companies/count.ts b/src/api/companies/count.ts new file mode 100644 index 0000000..1402e74 --- /dev/null +++ b/src/api/companies/count.ts @@ -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"] }), +); diff --git a/src/api/companies/index.ts b/src/api/companies/index.ts index 0569b09..4719b03 100644 --- a/src/api/companies/index.ts +++ b/src/api/companies/index.ts @@ -1,5 +1,6 @@ import { default as fetchAll } from "./fetchAll"; import { default as fetch } from "./[id]/fetch"; import { default as configurations } from "./[id]/configurations"; +import { default as count } from "./count"; -export { configurations, fetch, fetchAll }; +export { configurations, count, fetch, fetchAll }; diff --git a/src/api/permissions/fetchAll.ts b/src/api/permissions/fetchAll.ts new file mode 100644 index 0000000..7cfd1aa --- /dev/null +++ b/src/api/permissions/fetchAll.ts @@ -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"] }), +); diff --git a/src/api/permissions/fetchByCategory.ts b/src/api/permissions/fetchByCategory.ts new file mode 100644 index 0000000..301b9e9 --- /dev/null +++ b/src/api/permissions/fetchByCategory.ts @@ -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"] }), +); diff --git a/src/api/permissions/fetchNodes.ts b/src/api/permissions/fetchNodes.ts new file mode 100644 index 0000000..797eca1 --- /dev/null +++ b/src/api/permissions/fetchNodes.ts @@ -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"] }), +); diff --git a/src/api/permissions/index.ts b/src/api/permissions/index.ts new file mode 100644 index 0000000..18fc897 --- /dev/null +++ b/src/api/permissions/index.ts @@ -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 }; diff --git a/src/api/roles/addPermissions.ts b/src/api/roles/addPermissions.ts new file mode 100644 index 0000000..0b37a7c --- /dev/null +++ b/src/api/roles/addPermissions.ts @@ -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"] }), +); diff --git a/src/api/roles/create.ts b/src/api/roles/create.ts new file mode 100644 index 0000000..f39ec0f --- /dev/null +++ b/src/api/roles/create.ts @@ -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"] }), +); diff --git a/src/api/roles/delete.ts b/src/api/roles/delete.ts new file mode 100644 index 0000000..6db0aeb --- /dev/null +++ b/src/api/roles/delete.ts @@ -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"] }), +); diff --git a/src/api/roles/fetch.ts b/src/api/roles/fetch.ts new file mode 100644 index 0000000..a02bb26 --- /dev/null +++ b/src/api/roles/fetch.ts @@ -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"] }), +); diff --git a/src/api/roles/fetchAll.ts b/src/api/roles/fetchAll.ts new file mode 100644 index 0000000..93c6b4a --- /dev/null +++ b/src/api/roles/fetchAll.ts @@ -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"] }), +); diff --git a/src/api/roles/getUsers.ts b/src/api/roles/getUsers.ts new file mode 100644 index 0000000..b6d1119 --- /dev/null +++ b/src/api/roles/getUsers.ts @@ -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"] }), +); diff --git a/src/api/roles/index.ts b/src/api/roles/index.ts new file mode 100644 index 0000000..cb4da97 --- /dev/null +++ b/src/api/roles/index.ts @@ -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"; diff --git a/src/api/roles/removePermissions.ts b/src/api/roles/removePermissions.ts new file mode 100644 index 0000000..50e0f87 --- /dev/null +++ b/src/api/roles/removePermissions.ts @@ -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"] }), +); diff --git a/src/api/roles/update.ts b/src/api/roles/update.ts new file mode 100644 index 0000000..50b0154 --- /dev/null +++ b/src/api/roles/update.ts @@ -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"] }), +); diff --git a/src/api/routers/permissionRouter.ts b/src/api/routers/permissionRouter.ts new file mode 100644 index 0000000..a2dfcbb --- /dev/null +++ b/src/api/routers/permissionRouter.ts @@ -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; diff --git a/src/api/routers/roleRouter.ts b/src/api/routers/roleRouter.ts new file mode 100644 index 0000000..c5f8443 --- /dev/null +++ b/src/api/routers/roleRouter.ts @@ -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; diff --git a/src/api/server.ts b/src/api/server.ts index 57aa83e..8b24476 100644 --- a/src/api/server.ts +++ b/src/api/server.ts @@ -52,6 +52,8 @@ v1.route("/user", require("./routers/user").default); v1.route("/company", require("./routers/companyRouter").default); v1.route("/credential", require("./routers/credentialRouter").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); export default app; diff --git a/src/api/user/@me/checkPermission.ts b/src/api/user/@me/checkPermission.ts new file mode 100644 index 0000000..f67f0d0 --- /dev/null +++ b/src/api/user/@me/checkPermission.ts @@ -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"] }), +); diff --git a/src/api/user/@me/index.ts b/src/api/user/@me/index.ts index 492e227..22674fa 100644 --- a/src/api/user/@me/index.ts +++ b/src/api/user/@me/index.ts @@ -1,2 +1,3 @@ export { default as fetch } from "./fetch"; export { default as update } from "./update"; +export { default as checkPermission } from "./checkPermission"; diff --git a/src/controllers/CompanyController.ts b/src/controllers/CompanyController.ts index adabd6c..5117672 100644 --- a/src/controllers/CompanyController.ts +++ b/src/controllers/CompanyController.ts @@ -1,6 +1,6 @@ import { Company } from "../../generated/prisma/client"; 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 { Company as CWCompany } from "../types/ConnectWiseTypes"; @@ -16,12 +16,14 @@ export class CompanyController { public name: string; public readonly cw_Identifier: string; public readonly cw_CompanyId: number; + public readonly cw_Data?: CWCompany; - constructor(companyData: Company) { + constructor(companyData: Company, cwData?: CWCompany) { this.id = companyData.id; this.name = companyData.name; this.cw_Identifier = companyData.cw_Identifier; this.cw_CompanyId = companyData.cw_CompanyId; + this.cw_Data = cwData; } /** @@ -65,12 +67,22 @@ export class CompanyController { return data; } - public toJson() { + public toJson(opts?: { includeAddress: boolean }) { return { id: this.id, name: this.name, cw_Identifier: this.cw_Identifier, 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, + }, + }, }; } } diff --git a/src/controllers/RoleController.ts b/src/controllers/RoleController.ts index ec96a24..cdfd9cd 100644 --- a/src/controllers/RoleController.ts +++ b/src/controllers/RoleController.ts @@ -265,7 +265,7 @@ export class RoleController { where: { moniker: data.moniker }, }); - if (checkMoniker) + if (checkMoniker && checkMoniker.moniker !== this.moniker) throw new RoleError( "Moniker is already taken.", "Another role with this moniker already exists in the databse.", diff --git a/src/managers/companies.ts b/src/managers/companies.ts index 75dc747..39c0a9f 100644 --- a/src/managers/companies.ts +++ b/src/managers/companies.ts @@ -1,5 +1,6 @@ -import { prisma } from "../constants"; +import { connectWiseApi, prisma } from "../constants"; import { CompanyController } from "../controllers/CompanyController"; +import { Company } from "../types/ConnectWiseTypes"; export const companies = { async fetch(identifier: string | number): Promise { @@ -11,7 +12,10 @@ export const companies = { 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() { diff --git a/src/modules/cw-utils/fetchCompanyConfigurations.ts b/src/modules/cw-utils/configurations/fetchCompanyConfigurations.ts similarity index 82% rename from src/modules/cw-utils/fetchCompanyConfigurations.ts rename to src/modules/cw-utils/configurations/fetchCompanyConfigurations.ts index 3924f63..cde609a 100644 --- a/src/modules/cw-utils/fetchCompanyConfigurations.ts +++ b/src/modules/cw-utils/configurations/fetchCompanyConfigurations.ts @@ -1,10 +1,10 @@ -import { connectWiseApi } from "../../constants"; -import { ConfigurationResponse } from "../../types/ConnectWiseTypes"; +import { connectWiseApi } from "../../../constants"; +import { ConfigurationResponse } from "../../../types/ConnectWiseTypes"; import { processConfigurationResponse, ProcessedConfiguration, } from "./processConfigurationResponse"; -import GenericError from "../../Errors/GenericError"; +import GenericError from "../../../Errors/GenericError"; export const fetchCompanyConfigurations = async ( cwCompanyId: number, diff --git a/src/modules/cw-utils/configurations/fetchSingleConfiguration.ts b/src/modules/cw-utils/configurations/fetchSingleConfiguration.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/modules/cw-utils/configurations/processConfigurationResponse.ts b/src/modules/cw-utils/configurations/processConfigurationResponse.ts new file mode 100644 index 0000000..1358a69 --- /dev/null +++ b/src/modules/cw-utils/configurations/processConfigurationResponse.ts @@ -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, + })); +}; diff --git a/src/modules/cw-utils/processConfigurationResponse.ts b/src/modules/cw-utils/processConfigurationResponse.ts deleted file mode 100644 index 117dbc9..0000000 --- a/src/modules/cw-utils/processConfigurationResponse.ts +++ /dev/null @@ -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, - })); -}; diff --git a/src/types/ConnectWiseTypes.ts b/src/types/ConnectWiseTypes.ts index b4f1dfc..2b5e7ae 100644 --- a/src/types/ConnectWiseTypes.ts +++ b/src/types/ConnectWiseTypes.ts @@ -4,9 +4,11 @@ export interface Company { name: string; status: CompanyStatus; addressLine1: string; + addressLine2?: string; city: string; state: string; zip: string; + country: BasicEntity; phoneNumber: string; faxNumber: string; website: string; diff --git a/src/types/PermissionNodes.ts b/src/types/PermissionNodes.ts new file mode 100644 index 0000000..2aedfbd --- /dev/null +++ b/src/types/PermissionNodes.ts @@ -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), (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; + +/** + * 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; +}