diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index c46371c..745f1fc 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -76,4 +76,14 @@ Purpose: make AI coding agents immediately productive in this repository by desc 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. +- **Field-level permission gating (processObjectValuePerms)**: Some routes use `processObjectValuePerms` from `src/modules/permission-utils/processObjectPermissions.ts` to filter response objects on a per-field basis. When this pattern is used, every key of the response object becomes a permission node in the form `.` (e.g., `unifi.site.wifi.read.passphrase`). Only fields whose corresponding permission the user holds are included in the response. + + **When documenting a route that uses field-level gating, you must:** + 1. Note in `API_ROUTES.md` that the route uses field-level gating, explain the behaviour, and list every `.` permission node in a collapsible table. + 2. Add a `unifi.site.wifi.read`-style parent permission node in `PermissionNodes.ts` with a `fieldLevelPermissions` array listing every `.` node. + 3. Add matching rows/notes to `PERMISSIONS.md` including the full list of field-level nodes. + + **Current routes using field-level gating:** + - `GET /v1/unifi/site/:id/wifi` — scope `unifi.site.wifi.read`, gates every field on the `WlanConf` object. + 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 195eee1..8b70882 100644 --- a/API_ROUTES.md +++ b/API_ROUTES.md @@ -524,6 +524,7 @@ Fetch a single company by its ID. Automatically fetches fresh data from ConnectW - `company.fetch` (base permission) - `company.fetch.address` (required when `includeAddress=true`) +- `company.fetch.contacts` (required when `includeAllContacts=true`) **URL Parameters:** @@ -532,8 +533,10 @@ Fetch a single company by its ID. Automatically fetches fresh data from ConnectW **Query Parameters:** - `includeAddress` (optional) - Set to "true" to include full address information. Requires `company.fetch.address` permission. (default: false) +- `includePrimaryContact` (optional) - Set to "true" to include the company's default contact from ConnectWise. (default: false) +- `includeAllContacts` (optional) - Set to "true" to include all contacts for the company from ConnectWise. Requires `company.fetch.contacts` permission. (default: false) -**Response (without includeAddress):** +**Response (without optional query params):** ```json { @@ -576,6 +579,71 @@ Fetch a single company by its ID. Automatically fetches fresh data from ConnectW } ``` +**Response (with includePrimaryContact=true):** + +```json +{ + "status": 200, + "message": "Company Fetched Successfully!", + "data": { + "id": "ckx...", + "name": "Acme Corp", + "cw_CompanyId": 12345, + "cw_Identifier": "AcmeCorp", + "cw_Data": { + "primaryContact": { + "firstName": "John", + "lastName": "Doe", + "cwId": 456, + "inactive": false, + "title": "IT Manager", + "phone": "555-0123", + "email": "john.doe@acmecorp.com" + } + } + }, + "successful": true +} +``` + +**Response (with includeAllContacts=true):** + +```json +{ + "status": 200, + "message": "Company Fetched Successfully!", + "data": { + "id": "ckx...", + "name": "Acme Corp", + "cw_CompanyId": 12345, + "cw_Identifier": "AcmeCorp", + "cw_Data": { + "allContacts": [ + { + "firstName": "John", + "lastName": "Doe", + "cwId": 456, + "inactive": false, + "title": "IT Manager", + "phone": "555-0123", + "email": "john.doe@acmecorp.com" + }, + { + "firstName": "Jane", + "lastName": "Smith", + "cwId": 789, + "inactive": false, + "title": "CTO", + "phone": "555-0456", + "email": "jane.smith@acmecorp.com" + } + ] + } + }, + "successful": true +} +``` + --- ### Get Company Configurations @@ -607,6 +675,42 @@ Fetch configurations for a specific company from ConnectWise. --- +### Get Company UniFi Sites + +**GET** `/company/companies/:identifier/unifi/sites` + +Fetch all UniFi sites linked to a specific company. + +**Authentication Required:** Yes + +**Required Permissions:** `unifi.access`, `company.fetch` + +**URL Parameters:** + +- `identifier` - Company ID + +**Response:** + +```json +{ + "status": 200, + "message": "Company UniFi Sites Fetched Successfully!", + "data": [ + { + "id": "ckx...", + "name": "Main Office", + "siteId": "abc123", + "companyId": "ckx...", + "createdAt": "2025-01-01T00:00:00.000Z", + "updatedAt": "2025-01-01T00:00:00.000Z" + } + ], + "successful": true +} +``` + +--- + ## Credential Routes ### Get Value Types @@ -629,7 +733,8 @@ Returns all available field value types for credential type fields. "ip_address", "generic_secret", "bitlocker_key", - "password" + "password", + "multi_credential" ], "successful": true } @@ -784,10 +889,32 @@ Create a new credential with validated and encrypted fields. "fieldId": "secretAccessKey", "value": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" } - ] + ], + "subCredentials": { + "tunnels": [ + { + "name": "Tunnel 1", + "fields": [ + { "fieldId": "server", "value": "vpn1.example.com" }, + { "fieldId": "port", "value": "443" } + ] + } + ] + } } ``` +| Field | Type | Required | Description | +| ---------------- | -------- | -------- | ------------------------------------------------------------------------------------------------------------------- | +| `name` | `string` | Yes | Display name for the credential | +| `notes` | `string` | No | Optional notes | +| `typeId` | `string` | Yes | The credential type ID | +| `companyId` | `string` | Yes | The company ID this credential belongs to | +| `fields` | `array` | Yes | Array of field values (`{ fieldId, value }`) | +| `subCredentials` | `object` | No | Keyed by multi-credential field ID. Each value is an array of `{ name, fields }` objects for inline sub-credentials | + +```` + **Response:** ```json @@ -822,7 +949,7 @@ Create a new credential with validated and encrypted fields. }, "successful": true } -``` +```` --- @@ -1076,7 +1203,7 @@ Decrypt and return a single secure field value for a credential. **DELETE** `/credential/credentials/:id` -Delete a credential and all associated secure values. +Delete a credential and all associated secure values. Sub-credentials are cascade-deleted automatically. **Authentication Required:** Yes @@ -1099,6 +1226,149 @@ Delete a credential and all associated secure values. --- +### Get Sub-Credentials + +**GET** `/credential/credentials/:id/sub-credentials` + +Fetch all sub-credentials that belong to a specific parent credential. + +**Authentication Required:** Yes + +**Required Permissions:** `credential.fetch`, `credential.sub_credentials.fetch` + +**URL Parameters:** + +- `id` - Parent Credential ID + +**Response:** + +```json +{ + "status": 200, + "message": "Sub-Credentials Fetched Successfully!", + "data": [ + { + "id": "ckx1...", + "name": "Tunnel 1", + "notes": null, + "typeId": "cky...", + "companyId": "ckz...", + "subCredentialOfId": "ckx...", + "fields": [ + { "id": "server", "value": "vpn1.example.com", "secure": false }, + { "id": "port", "value": "443", "secure": false } + ], + "type": { "..." }, + "company": { "..." }, + "createdAt": "2026-02-20T00:00:00.000Z", + "updatedAt": "2026-02-20T00:00:00.000Z" + } + ], + "successful": true +} +``` + +--- + +### Add Sub-Credential + +**POST** `/credential/credentials/:id/sub-credentials` + +Create a new sub-credential under an existing parent credential for a specific multi-credential field. + +**Authentication Required:** Yes + +**Required Permissions:** `credential.fetch`, `credential.sub_credentials.create` + +**URL Parameters:** + +- `id` - Parent Credential ID + +**Request Body:** + +```json +{ + "fieldId": "tunnels", + "name": "Tunnel 2", + "fields": [ + { "fieldId": "server", "value": "vpn2.example.com" }, + { "fieldId": "port", "value": "1194" } + ] +} +``` + +| Field | Type | Required | Description | +| --------- | -------- | -------- | --------------------------------------------------------------- | +| `fieldId` | `string` | Yes | The multi-credential field ID on the parent credential type | +| `name` | `string` | Yes | Display name for the sub-credential | +| `fields` | `array` | Yes | Array of field values matching the multi-credential's subFields | + +**Response:** + +```json +{ + "status": 201, + "message": "Sub-Credential Created Successfully!", + "data": { + "id": "ckx2...", + "name": "Tunnel 2", + "typeId": "cky...", + "companyId": "ckz...", + "subCredentialOfId": "ckx...", + "fields": [ + { "id": "server", "value": "vpn2.example.com", "secure": false }, + { "id": "port", "value": "1194", "secure": false } + ], + "type": { "..." }, + "company": { "..." }, + "createdAt": "2026-02-20T00:00:00.000Z", + "updatedAt": "2026-02-20T00:00:00.000Z" + }, + "successful": true +} +``` + +--- + +### Remove Sub-Credential + +**DELETE** `/credential/credentials/:id/sub-credentials/:subId` + +Delete a sub-credential and remove its reference from the parent credential's multi-credential field. + +**Authentication Required:** Yes + +**Required Permissions:** `credential.fetch`, `credential.sub_credentials.delete` + +**URL Parameters:** + +- `id` - Parent Credential ID +- `subId` - Sub-Credential ID to remove + +**Response:** + +```json +{ + "status": 200, + "message": "Sub-Credential Removed Successfully!", + "data": null, + "successful": true +} +``` + +**Error Response (404):** + +```json +{ + "status": 404, + "message": "Sub-credential not found", + "error": "SubCredentialNotFound", + "successful": false +} +``` + +--- + ## Credential Type Routes ### Get Credential Type by ID or Name @@ -1228,6 +1498,59 @@ Create a new credential type with field definitions. } ``` +**Multi-Credential Example:** + +Fields with `valueType: "multi_credential"` support an optional `subFields` array that defines the field structure for each sub-credential entry. Sub-fields use the same schema and can be nested recursively. + +```json +{ + "name": "VPN Config", + "permissionScope": "vpn.credentials", + "icon": "https://example.com/vpn.ico", + "fields": [ + { + "id": "hostname", + "name": "Hostname", + "required": true, + "secure": false, + "valueType": "plain_text" + }, + { + "id": "tunnels", + "name": "Tunnels", + "required": false, + "secure": false, + "valueType": "multi_credential", + "subFields": [ + { + "id": "server", + "name": "Server", + "required": true, + "secure": false, + "valueType": "plain_text" + }, + { + "id": "psk", + "name": "Pre-Shared Key", + "required": true, + "secure": true, + "valueType": "password" + } + ] + } + ] +} +``` + +| Field | Type | Required | Description | +| -------------------- | -------- | -------- | ---------------------------------------------------------------------------------------------- | +| `fields[].id` | `string` | Yes | Unique identifier for the field | +| `fields[].name` | `string` | Yes | Display name for the field | +| `fields[].required` | `bool` | Yes | Whether the field is required when creating a credential | +| `fields[].secure` | `bool` | Yes | Whether the field value should be encrypted at rest | +| `fields[].valueType` | `string` | Yes | One of the supported value types (see `GET /credential/valuetypes`) | +| `fields[].subFields` | `array` | No | Only for `multi_credential` fields. Defines the field structure for each nested sub-credential | + **Response:** ```json @@ -1293,6 +1616,8 @@ Update a credential type's properties or field definitions. } ``` +> **Note:** Fields with `valueType: "multi_credential"` support an optional `subFields` array — see the [Create Credential Type](#create-credential-type) section for the full schema and example. + **Response:** ```json @@ -1879,6 +2204,1029 @@ A fun Easter egg endpoint that returns HTTP 418 (I'm a teapot). --- +## UniFi Routes + +All UniFi routes require the `unifi.access` permission in addition to their route-specific permission. This acts as a gate for the entire UniFi API. + +### Get All UniFi Sites + +**GET** `/unifi/sites` + +Fetch all UniFi site records from the database. + +**Authentication Required:** Yes + +**Required Permissions:** `unifi.access`, `unifi.sites.fetch.many` + +**Response:** + +```json +{ + "status": 200, + "message": "UniFi Sites Fetched Successfully!", + "data": [ + { + "id": "ckx...", + "name": "Total Tech - Murray Office", + "siteId": "km9b1v8i", + "companyId": "ckx...", + "company": { + "id": "ckx...", + "name": "Acme Corp" + }, + "createdAt": "2025-01-01T00:00:00.000Z", + "updatedAt": "2025-01-01T00:00:00.000Z" + } + ], + "successful": true +} +``` + +--- + +### Sync UniFi Sites + +**POST** `/unifi/sites/sync` + +Synchronize sites from the UniFi controller into the database. Creates new records for sites not yet tracked and updates names for existing ones. + +**Authentication Required:** Yes + +**Required Permissions:** `unifi.access`, `unifi.sites.sync` + +**Response:** + +```json +{ + "status": 200, + "message": "UniFi Sites Synced Successfully!", + "data": [ + { + "id": "ckx...", + "name": "Total Tech - Murray Office", + "siteId": "km9b1v8i", + "companyId": null, + "createdAt": "2025-01-01T00:00:00.000Z", + "updatedAt": "2025-01-01T00:00:00.000Z" + } + ], + "successful": true +} +``` + +--- + +### Create UniFi Site + +**POST** `/unifi/sites/create` + +Create a new site on the UniFi controller and track it in the database. + +**Authentication Required:** Yes + +**Required Permissions:** `unifi.access`, `unifi.sites.create` + +**Request Body:** + +```json +{ + "description": "New Office Site" +} +``` + +| Field | Type | Required | Description | +| ------------- | ------ | -------- | ---------------------------------------------- | +| `description` | string | Yes | Human-readable name / description for the site | + +**Response:** + +```json +{ + "status": 200, + "message": "UniFi Site Created Successfully!", + "data": { + "id": "ckx...", + "name": "New Office Site", + "siteId": "abc123", + "companyId": null, + "createdAt": "2025-01-01T00:00:00.000Z", + "updatedAt": "2025-01-01T00:00:00.000Z" + }, + "successful": true +} +``` + +--- + +### Get UniFi Site + +**GET** `/unifi/site/:id` + +Fetch a single UniFi site record from the database by its internal ID. + +**Authentication Required:** Yes + +**Required Permissions:** `unifi.access`, `unifi.sites.fetch` + +**URL Parameters:** + +- `id` - Internal UniFi site ID (database ID) + +**Response:** + +```json +{ + "status": 200, + "message": "UniFi Site Fetched Successfully!", + "data": { + "id": "ckx...", + "name": "Total Tech - Murray Office", + "siteId": "km9b1v8i", + "companyId": "ckx...", + "createdAt": "2025-01-01T00:00:00.000Z", + "updatedAt": "2025-01-01T00:00:00.000Z" + }, + "successful": true +} +``` + +--- + +### Get Site Overview + +**GET** `/unifi/site/:id/overview` + +Fetch live site overview data from the UniFi controller, including health status, system info, and site information. + +**Authentication Required:** Yes + +**Required Permissions:** `unifi.access`, `unifi.site.overview` + +**URL Parameters:** + +- `id` - Internal UniFi site ID (database ID) + +**Response:** + +```json +{ + "status": 200, + "message": "UniFi Site Overview Fetched Successfully!", + "data": { + "health": [ + { + "subsystem": "wan", + "status": "ok", + "numAdopted": 1, + "numGateway": 1 + } + ], + "sysInfo": { + "timezone": "America/Denver", + "hostname": "UniFi-Controller", + "version": "8.x.x" + }, + "siteInfo": { + "description": "Total Tech - Murray Office", + "name": "km9b1v8i" + } + }, + "successful": true +} +``` + +--- + +### Get Site Devices + +**GET** `/unifi/site/:id/devices` + +Fetch live device list from the UniFi controller for a specific site. + +**Authentication Required:** Yes + +**Required Permissions:** `unifi.access`, `unifi.site.devices` + +**URL Parameters:** + +- `id` - Internal UniFi site ID (database ID) + +**Response:** + +```json +{ + "status": 200, + "message": "UniFi Devices Fetched Successfully!", + "data": [ + { + "id": "abc123...", + "mac": "00:11:22:33:44:55", + "model": "U6-Pro", + "name": "Office AP", + "type": "uap", + "state": "connected", + "ip": "192.168.1.10", + "version": "6.x.x", + "uptime": 123456, + "radios": [], + "uplink": {} + } + ], + "successful": true +} +``` + +--- + +### Get Site WiFi Networks + +**GET** `/unifi/site/:id/wifi` + +Fetch live WiFi network (WLAN) configurations from the UniFi controller for a specific site. + +**Authentication Required:** Yes + +**Required Permissions:** `unifi.access`, `unifi.site.wifi` + +**Field-Level Gating:** `unifi.site.wifi.read.` + +This route uses `processObjectValuePerms` to filter each WLAN object on a per-field basis. Only fields whose corresponding `unifi.site.wifi.read.` permission the user holds are included in the response. For example, a user with `unifi.site.wifi.read.name` and `unifi.site.wifi.read.enabled` but _without_ `unifi.site.wifi.read.passphrase` will receive objects containing only `name` and `enabled`. Use `unifi.site.wifi.read.*` to grant access to all fields. + +
+All available field-level permission nodes + +| Permission Node | Field | +| -------------------------------------------------- | ----------------------------- | +| `unifi.site.wifi.read.id` | `id` | +| `unifi.site.wifi.read.name` | `name` | +| `unifi.site.wifi.read.siteId` | `siteId` | +| `unifi.site.wifi.read.enabled` | `enabled` | +| `unifi.site.wifi.read.security` | `security` | +| `unifi.site.wifi.read.wpaMode` | `wpaMode` | +| `unifi.site.wifi.read.wpaEnc` | `wpaEnc` | +| `unifi.site.wifi.read.wpa3Support` | `wpa3Support` | +| `unifi.site.wifi.read.wpa3Transition` | `wpa3Transition` | +| `unifi.site.wifi.read.wpa3FastRoaming` | `wpa3FastRoaming` | +| `unifi.site.wifi.read.wpa3Enhanced192` | `wpa3Enhanced192` | +| `unifi.site.wifi.read.passphrase` | `passphrase` | +| `unifi.site.wifi.read.passphraseAutogenerated` | `passphraseAutogenerated` | +| `unifi.site.wifi.read.hideSSID` | `hideSSID` | +| `unifi.site.wifi.read.isGuest` | `isGuest` | +| `unifi.site.wifi.read.band` | `band` | +| `unifi.site.wifi.read.bands` | `bands` | +| `unifi.site.wifi.read.networkconfId` | `networkconfId` | +| `unifi.site.wifi.read.usergroupId` | `usergroupId` | +| `unifi.site.wifi.read.apGroupIds` | `apGroupIds` | +| `unifi.site.wifi.read.apGroupMode` | `apGroupMode` | +| `unifi.site.wifi.read.pmfMode` | `pmfMode` | +| `unifi.site.wifi.read.groupRekey` | `groupRekey` | +| `unifi.site.wifi.read.dtimMode` | `dtimMode` | +| `unifi.site.wifi.read.dtimNg` | `dtimNg` | +| `unifi.site.wifi.read.dtimNa` | `dtimNa` | +| `unifi.site.wifi.read.dtim6e` | `dtim6e` | +| `unifi.site.wifi.read.l2Isolation` | `l2Isolation` | +| `unifi.site.wifi.read.fastRoamingEnabled` | `fastRoamingEnabled` | +| `unifi.site.wifi.read.bssTransition` | `bssTransition` | +| `unifi.site.wifi.read.uapsdEnabled` | `uapsdEnabled` | +| `unifi.site.wifi.read.iappEnabled` | `iappEnabled` | +| `unifi.site.wifi.read.proxyArp` | `proxyArp` | +| `unifi.site.wifi.read.mcastenhanceEnabled` | `mcastenhanceEnabled` | +| `unifi.site.wifi.read.macFilterEnabled` | `macFilterEnabled` | +| `unifi.site.wifi.read.macFilterPolicy` | `macFilterPolicy` | +| `unifi.site.wifi.read.macFilterList` | `macFilterList` | +| `unifi.site.wifi.read.radiusDasEnabled` | `radiusDasEnabled` | +| `unifi.site.wifi.read.radiusMacAuthEnabled` | `radiusMacAuthEnabled` | +| `unifi.site.wifi.read.radiusMacaclFormat` | `radiusMacaclFormat` | +| `unifi.site.wifi.read.minrateSettingPreference` | `minrateSettingPreference` | +| `unifi.site.wifi.read.minrateNgEnabled` | `minrateNgEnabled` | +| `unifi.site.wifi.read.minrateNgDataRateKbps` | `minrateNgDataRateKbps` | +| `unifi.site.wifi.read.minrateNgAdvertisingRates` | `minrateNgAdvertisingRates` | +| `unifi.site.wifi.read.minrateNaEnabled` | `minrateNaEnabled` | +| `unifi.site.wifi.read.minrateNaDataRateKbps` | `minrateNaDataRateKbps` | +| `unifi.site.wifi.read.minrateNaAdvertisingRates` | `minrateNaAdvertisingRates` | +| `unifi.site.wifi.read.settingPreference` | `settingPreference` | +| `unifi.site.wifi.read.no2ghzOui` | `no2ghzOui` | +| `unifi.site.wifi.read.privatePreSharedKeysEnabled` | `privatePreSharedKeysEnabled` | +| `unifi.site.wifi.read.privatePreSharedKeys` | `privatePreSharedKeys` | +| `unifi.site.wifi.read.saeGroups` | `saeGroups` | +| `unifi.site.wifi.read.saePsk` | `saePsk` | +| `unifi.site.wifi.read.schedule` | `schedule` | +| `unifi.site.wifi.read.scheduleWithDuration` | `scheduleWithDuration` | +| `unifi.site.wifi.read.bcFilterList` | `bcFilterList` | +| `unifi.site.wifi.read.externalId` | `externalId` | + +
+ +**URL Parameters:** + +- `id` - Internal UniFi site ID (database ID) + +**Response:** + +```json +{ + "status": 200, + "message": "UniFi WiFi Networks Fetched Successfully!", + "data": [ + { + "id": "66eb36e54bb53f0ae3fb82bd", + "name": "TTAD", + "siteId": "61ae856a8eae567c3905c54f", + "enabled": true, + "security": "wpapsk", + "wpaMode": "wpa2", + "wpaEnc": "ccmp", + "wpa3Support": false, + "wpa3Transition": false, + "wpa3FastRoaming": false, + "wpa3Enhanced192": false, + "passphrase": "S3cur3Alarm!", + "passphraseAutogenerated": false, + "hideSSID": false, + "isGuest": false, + "band": "2g", + "bands": ["2g"], + "networkconfId": "66eb368d4bb53f0ae3fb7e0d", + "usergroupId": "61ae856a8eae567c3905c55c", + "apGroupIds": ["66eb36e44bb53f0ae3fb82bc"], + "apGroupMode": "devices", + "pmfMode": "disabled", + "groupRekey": 0, + "dtimMode": "default", + "dtimNg": 1, + "dtimNa": 3, + "dtim6e": 3, + "l2Isolation": false, + "fastRoamingEnabled": false, + "bssTransition": true, + "uapsdEnabled": false, + "iappEnabled": true, + "proxyArp": false, + "mcastenhanceEnabled": false, + "macFilterEnabled": false, + "macFilterPolicy": "allow", + "macFilterList": [], + "radiusDasEnabled": false, + "radiusMacAuthEnabled": false, + "radiusMacaclFormat": "none_lower", + "minrateSettingPreference": "auto", + "minrateNgEnabled": true, + "minrateNgDataRateKbps": 1000, + "minrateNgAdvertisingRates": false, + "minrateNaEnabled": false, + "minrateNaDataRateKbps": 6000, + "minrateNaAdvertisingRates": false, + "settingPreference": "manual", + "no2ghzOui": true, + "privatePreSharedKeysEnabled": false, + "privatePreSharedKeys": [], + "saeGroups": [], + "saePsk": [], + "schedule": [], + "scheduleWithDuration": [], + "bcFilterList": [], + "externalId": "ba0f579c-6b21-4dfc-8d03-5a1a09243328" + } + ], + "successful": true +} +``` + +--- + +### Update WiFi Network + +**PATCH** `/unifi/site/:id/wifi/:wlanId` + +Update a WiFi network configuration on the UniFi controller. + +**Authentication Required:** Yes + +**Required Permissions:** `unifi.access`, `unifi.site.wifi`, `unifi.site.wifi.update` + +**URL Parameters:** + +- `id` - Internal UniFi site ID (database ID) +- `wlanId` - UniFi WLAN ID + +**Request Body:** + +```json +{ + "name": "NewSSIDName", + "x_passphrase": "NewPassword123", + "enabled": true, + "security": "wpapsk", + "wpa_mode": "wpa2", + "hide_ssid": false, + "is_guest": false, + "band": "both" +} +``` + +All fields are optional. Valid values: + +- `security`: `"wpapsk"`, `"wpaeap"`, `"open"` +- `wpa_mode`: `"wpa2"`, `"wpa3"`, `"wpa2wpa3"` +- `band`: `"both"`, `"2g"`, `"5g"` + +**Response:** + +```json +{ + "status": 200, + "message": "UniFi WiFi Network Updated Successfully!", + "data": { + "id": "66eb36e54bb53f0ae3fb82bd", + "name": "NewSSIDName", + "siteId": "61ae856a8eae567c3905c54f", + "enabled": true, + "security": "wpapsk", + "wpaMode": "wpa2", + "wpaEnc": "ccmp", + "wpa3Support": false, + "wpa3Transition": false, + "wpa3FastRoaming": false, + "wpa3Enhanced192": false, + "passphrase": "NewPassword123", + "passphraseAutogenerated": false, + "hideSSID": false, + "isGuest": false, + "band": "both", + "bands": ["2g", "5g"], + "networkconfId": "66eb368d4bb53f0ae3fb7e0d", + "usergroupId": "61ae856a8eae567c3905c55c", + "apGroupIds": ["66eb36e44bb53f0ae3fb82bc"], + "apGroupMode": "devices", + "pmfMode": "disabled", + "groupRekey": 0, + "dtimMode": "default", + "dtimNg": 1, + "dtimNa": 3, + "dtim6e": 3, + "l2Isolation": false, + "fastRoamingEnabled": false, + "bssTransition": true, + "uapsdEnabled": false, + "iappEnabled": true, + "proxyArp": false, + "mcastenhanceEnabled": false, + "macFilterEnabled": false, + "macFilterPolicy": "allow", + "macFilterList": [], + "radiusDasEnabled": false, + "radiusMacAuthEnabled": false, + "radiusMacaclFormat": "none_lower", + "minrateSettingPreference": "auto", + "minrateNgEnabled": true, + "minrateNgDataRateKbps": 1000, + "minrateNgAdvertisingRates": false, + "minrateNaEnabled": false, + "minrateNaDataRateKbps": 6000, + "minrateNaAdvertisingRates": false, + "settingPreference": "manual", + "no2ghzOui": true, + "privatePreSharedKeysEnabled": false, + "privatePreSharedKeys": [], + "saeGroups": [], + "saePsk": [], + "schedule": [], + "scheduleWithDuration": [], + "bcFilterList": [], + "externalId": "ba0f579c-6b21-4dfc-8d03-5a1a09243328" + }, + "successful": true +} +``` + +--- + +### Get Site Networks + +**GET** `/unifi/site/:id/networks` + +Fetch live network configurations from the UniFi controller for a specific site. + +**Authentication Required:** Yes + +**Required Permissions:** `unifi.access`, `unifi.site.networks` + +**URL Parameters:** + +- `id` - Internal UniFi site ID (database ID) + +**Response:** + +```json +{ + "status": 200, + "message": "UniFi Networks Fetched Successfully!", + "data": [ + { + "id": "abc123...", + "name": "LAN", + "purpose": "corporate", + "subnet": "192.168.1.0/24", + "vlanId": null, + "dhcpEnabled": true, + "dhcpStart": "192.168.1.100", + "dhcpStop": "192.168.1.254", + "domainName": "localdomain", + "isNat": true, + "enabled": true + } + ], + "successful": true +} +``` + +--- + +### Get WLAN Groups + +**GET** `/unifi/site/:id/wlan-groups` + +Fetch WLAN groups (AP groups) from the UniFi controller for a specific site. WLAN groups define which WLANs are broadcast on which access points. + +**Authentication Required:** Yes + +**Required Permissions:** `unifi.access`, `unifi.site.wlan-groups` + +**URL Parameters:** + +- `id` - Internal UniFi site ID (database ID) + +**Response:** + +```json +{ + "status": 200, + "message": "UniFi WLAN Groups Fetched Successfully!", + "data": [ + { + "id": "61ae856a8eae567c3905c55d", + "name": "Default", + "siteId": "61ae856a8eae567c3905c550", + "noDelete": true, + "noEdit": false, + "hidden": false + } + ], + "successful": true +} +``` + +--- + +### Create WLAN Group + +**POST** `/unifi/site/:id/wlan-groups` + +Create a new WLAN group (AP broadcasting group) on the UniFi controller. WLAN groups control which WLANs are broadcast on which access points. + +**Authentication Required:** Yes + +**Required Permissions:** `unifi.access`, `unifi.site.wlan-groups`, `unifi.site.wlan-groups.create` + +**URL Parameters:** + +- `id` - Internal UniFi site ID (database ID) + +**Request Body:** + +```json +{ + "name": "Lobby APs" +} +``` + +| Field | Type | Required | Description | +| ------ | ------ | -------- | ---------------------- | +| `name` | string | Yes | Name of the WLAN group | + +**Response (201):** + +```json +{ + "status": 201, + "message": "UniFi WLAN Group Created Successfully!", + "data": { + "id": "abc123...", + "name": "Lobby APs", + "siteId": "61ae856a8eae567c3905c550", + "noDelete": false, + "noEdit": false, + "hidden": false + }, + "successful": true +} +``` + +--- + +### Get AP Groups + +**GET** `/unifi/site/:id/ap-groups` + +Fetch AP groups from the UniFi controller for a specific site. AP groups define collections of access points — individual WLANs can target specific AP groups to control which APs broadcast a given SSID. + +**Authentication Required:** Yes + +**Required Permissions:** `unifi.access`, `unifi.site.ap-groups` + +**URL Parameters:** + +- `id` - Internal UniFi site ID (database ID) + +**Response:** + +```json +{ + "status": 200, + "message": "UniFi AP Groups Fetched Successfully!", + "data": [ + { + "id": "61ae856a8eae567c3905c565", + "name": "All APs", + "deviceMacs": [ + "18:e8:29:59:25:bc", + "78:45:58:29:fa:87", + "68:d7:9a:73:9a:28" + ], + "noDelete": true + }, + { + "id": "63c5be017e957d08189ed997", + "name": "301Andrus", + "deviceMacs": ["78:45:58:29:fa:87", "18:e8:29:59:25:bc"], + "noDelete": false + } + ], + "successful": true +} +``` + +--- + +### Get Access Points + +**GET** `/unifi/site/:id/access-points` + +Fetch access points (UAPs only) from the UniFi controller for a specific site. This filters the full device list to only return wireless access points. + +**Authentication Required:** Yes + +**Required Permissions:** `unifi.access`, `unifi.site.access-points` + +**URL Parameters:** + +- `id` - Internal UniFi site ID (database ID) + +**Response:** + +```json +{ + "status": 200, + "message": "UniFi Access Points Fetched Successfully!", + "data": [ + { + "id": "abc123...", + "mac": "00:11:22:33:44:55", + "model": "U6-Pro", + "type": "uap", + "name": "Office AP", + "state": 1, + "adopted": true, + "ip": "192.168.1.10", + "version": "7.1.68" + } + ], + "successful": true +} +``` + +--- + +### Get WiFi Limits + +**GET** `/unifi/site/:id/wifi-limits` + +Check the WiFi SSID limits per access point per radio band. UniFi access points support a maximum of 8 SSIDs per radio. This endpoint shows how many SSIDs are active on each radio and how many more can be added. + +**Authentication Required:** Yes + +**Required Permissions:** `unifi.access`, `unifi.site.wifi-limits` + +**URL Parameters:** + +- `id` - Internal UniFi site ID (database ID) + +**Response:** + +```json +{ + "status": 200, + "message": "UniFi WiFi Limits Fetched Successfully!", + "data": [ + { + "apId": "abc123...", + "apName": "Office AP", + "mac": "00:11:22:33:44:55", + "model": "U6-Pro", + "radios": [ + { + "radio": "ng", + "band": "2g", + "activeWlans": 3, + "limit": 8, + "remaining": 5, + "wlanNames": ["Corporate", "Guest", "IoT"] + }, + { + "radio": "na", + "band": "5g", + "activeWlans": 3, + "limit": 8, + "remaining": 5, + "wlanNames": ["Corporate", "Guest", "IoT"] + } + ] + } + ], + "successful": true +} +``` + +--- + +### Get Speed Profiles + +**GET** `/unifi/site/:id/speed-profiles` + +Fetch speed limit profiles (user groups) from the UniFi controller for a specific site. Speed profiles define bandwidth limits that can be applied to WiFi networks. + +**Authentication Required:** Yes + +**Required Permissions:** `unifi.access`, `unifi.site.speed-profiles` + +**URL Parameters:** + +- `id` - Internal UniFi site ID (database ID) + +**Response:** + +```json +{ + "status": 200, + "message": "UniFi Speed Profiles Fetched Successfully!", + "data": [ + { + "id": "61ae856a8eae567c3905c55e", + "name": "Default", + "siteId": "61ae856a8eae567c3905c550", + "noDelete": true, + "downloadLimitKbps": -1, + "uploadLimitKbps": -1 + }, + { + "id": "abc123...", + "name": "Guest 10Mbps", + "siteId": "61ae856a8eae567c3905c550", + "noDelete": false, + "downloadLimitKbps": 10000, + "uploadLimitKbps": 5000 + } + ], + "successful": true +} +``` + +> **Note:** A value of `-1` for `downloadLimitKbps` or `uploadLimitKbps` means unlimited. + +--- + +### Create Speed Profile + +**POST** `/unifi/site/:id/speed-profiles` + +Create a new speed limit profile (user group) on the UniFi controller. + +**Authentication Required:** Yes + +**Required Permissions:** `unifi.access`, `unifi.site.speed-profiles`, `unifi.site.speed-profiles.create` + +**URL Parameters:** + +- `id` - Internal UniFi site ID (database ID) + +**Request Body:** + +```json +{ + "name": "Guest 10Mbps", + "downloadLimitKbps": 10000, + "uploadLimitKbps": 5000 +} +``` + +| Field | Type | Required | Description | +| ------------------- | ------ | -------- | ----------------------------------------------- | +| `name` | string | Yes | Name of the speed profile | +| `downloadLimitKbps` | number | No | Download limit in Kbps (-1 or omit = unlimited) | +| `uploadLimitKbps` | number | No | Upload limit in Kbps (-1 or omit = unlimited) | + +**Response (201):** + +```json +{ + "status": 201, + "message": "UniFi Speed Profile Created Successfully!", + "data": { + "id": "abc123...", + "name": "Guest 10Mbps", + "siteId": "61ae856a8eae567c3905c550", + "noDelete": false, + "downloadLimitKbps": 10000, + "uploadLimitKbps": 5000 + }, + "successful": true +} +``` + +--- + +### Get Private PSKs + +**GET** `/unifi/site/:id/wifi/:wlanId/ppsk` + +Fetch private pre-shared keys (PPSKs) for a specific WiFi network. PPSKs allow different devices to connect to the same SSID with unique passwords. + +**Authentication Required:** Yes + +**Required Permissions:** `unifi.access`, `unifi.site.wifi`, `unifi.site.wifi.ppsk` + +**URL Parameters:** + +- `id` - Internal UniFi site ID (database ID) +- `wlanId` - UniFi WLAN configuration ID + +**Response:** + +```json +{ + "status": 200, + "message": "UniFi Private PSKs Fetched Successfully!", + "data": [ + { + "key": "mySecurePassword123", + "name": "John's Laptop", + "mac": null, + "vlanId": null + }, + { + "key": "anotherPassword456", + "name": "IoT Device", + "mac": "AA:BB:CC:DD:EE:FF", + "vlanId": 100 + } + ], + "successful": true +} +``` + +--- + +### Create Private PSK + +**POST** `/unifi/site/:id/wifi/:wlanId/ppsk` + +Create a private pre-shared key on a specific WiFi network. This adds a new PPSK to the WLAN's list and enables PPSK mode if not already enabled. + +**Authentication Required:** Yes + +**Required Permissions:** `unifi.access`, `unifi.site.wifi`, `unifi.site.wifi.ppsk`, `unifi.site.wifi.ppsk.create` + +**URL Parameters:** + +- `id` - Internal UniFi site ID (database ID) +- `wlanId` - UniFi WLAN configuration ID + +**Request Body:** + +```json +{ + "key": "mySecurePassword123", + "name": "John's Laptop", + "mac": "AA:BB:CC:DD:EE:FF", + "vlanId": 100 +} +``` + +| Field | Type | Required | Description | +| -------- | ------ | -------- | ------------------------------------------------- | +| `key` | string | Yes | The pre-shared key (min 8 characters) | +| `name` | string | Yes | Descriptive name for this PSK | +| `mac` | string | No | MAC address to lock this PSK to a specific device | +| `vlanId` | number | No | VLAN ID to assign to clients using this PSK | + +**Response (201):** + +```json +{ + "status": 201, + "message": "UniFi Private PSK Created Successfully!", + "data": [ + { + "key": "mySecurePassword123", + "name": "John's Laptop", + "mac": "AA:BB:CC:DD:EE:FF", + "vlanId": 100 + } + ], + "successful": true +} +``` + +> **Note:** The response returns the full updated list of all PPSKs on the WLAN after creation. + +--- + +### Link Site to Company + +**POST** `/unifi/site/:id/link` + +Link a UniFi site to a company. + +**Authentication Required:** Yes + +**Required Permissions:** `unifi.access`, `unifi.sites.link` + +**URL Parameters:** + +- `id` - Internal UniFi site ID (database ID) + +**Request Body:** + +```json +{ + "companyId": "ckx..." +} +``` + +**Response:** + +```json +{ + "status": 200, + "message": "UniFi Site Linked to Company Successfully!", + "data": { + "id": "ckx...", + "name": "Total Tech - Murray Office", + "siteId": "km9b1v8i", + "companyId": "ckx...", + "createdAt": "2025-01-01T00:00:00.000Z", + "updatedAt": "2025-01-01T00:00:00.000Z" + }, + "successful": true +} +``` + +--- + +### Unlink Site from Company + +**POST** `/unifi/site/:id/unlink` + +Unlink a UniFi site from its associated company. + +**Authentication Required:** Yes + +**Required Permissions:** `unifi.access`, `unifi.sites.link` + +**URL Parameters:** + +- `id` - Internal UniFi site ID (database ID) + +**Response:** + +```json +{ + "status": 200, + "message": "UniFi Site Unlinked from Company Successfully!", + "data": { + "id": "ckx...", + "name": "Total Tech - Murray Office", + "siteId": "km9b1v8i", + "companyId": null, + "createdAt": "2025-01-01T00:00:00.000Z", + "updatedAt": "2025-01-01T00:00:00.000Z" + }, + "successful": true +} +``` + +--- + ## Error Responses All endpoints may return error responses in the following format: diff --git a/PERMISSIONS.md b/PERMISSIONS.md index d5c20a7..3a07668 100644 --- a/PERMISSIONS.md +++ b/PERMISSIONS.md @@ -27,21 +27,25 @@ The permission validator supports special tokens for flexible permission managem | ------------------------------ | ------------------------------------------------------------------- | ------------------------------------------------------------------------------------ | | `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.contacts` | View all company contacts (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), [src/api/credentials/readSecureValue.ts](src/api/credentials/readSecureValue.ts) | +| 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), [src/api/credentials/readSecureValue.ts](src/api/credentials/readSecureValue.ts) | +| `credential.sub_credentials.fetch` | Fetch sub-credentials of a parent credential (requires `credential.fetch` as well) | [src/api/credentials/fetchSubCredentials.ts](src/api/credentials/fetchSubCredentials.ts) | +| `credential.sub_credentials.create` | Create a sub-credential on a parent credential (requires `credential.fetch` as well) | [src/api/credentials/addSubCredential.ts](src/api/credentials/addSubCredential.ts) | +| `credential.sub_credentials.delete` | Remove a sub-credential from a parent credential (requires `credential.fetch` as well) | [src/api/credentials/removeSubCredential.ts](src/api/credentials/removeSubCredential.ts) | ### Credential Type Permissions @@ -111,6 +115,43 @@ Admin-specific UI permissions that control visibility and data loading for admin - **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. +### UniFi Permissions + +Permissions for accessing and managing UniFi network infrastructure. The `unifi.access` permission is a gate permission required for **all** UniFi routes. + +| Permission Node | Description | Used In | Dependencies | +| ------------------------------ | --------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------- | +| `unifi.access` | Gate permission for the entire UniFi API — required for all | [src/api/unifi/sites/fetchAll.ts](src/api/unifi/sites/fetchAll.ts), [src/api/unifi/sites/sync.ts](src/api/unifi/sites/sync.ts), [src/api/unifi/site/fetch.ts](src/api/unifi/site/fetch.ts), [src/api/unifi/site/overview.ts](src/api/unifi/site/overview.ts), [src/api/unifi/site/devices.ts](src/api/unifi/site/devices.ts), [src/api/unifi/site/wifi/fetchAll.ts](src/api/unifi/site/wifi/fetchAll.ts), [src/api/unifi/site/wifi/update.ts](src/api/unifi/site/wifi/update.ts), [src/api/unifi/site/networks.ts](src/api/unifi/site/networks.ts), [src/api/unifi/site/link.ts](src/api/unifi/site/link.ts), [src/api/unifi/site/unlink.ts](src/api/unifi/site/unlink.ts), [src/api/companies/[id]/unifiSites.ts](src/api/companies/[id]/unifiSites.ts), [src/api/unifi/sites/create.ts](src/api/unifi/sites/create.ts) | | +| `unifi.sites.create` | Create a new site on the UniFi controller | [src/api/unifi/sites/create.ts](src/api/unifi/sites/create.ts) | `unifi.access` | +| `unifi.sites.fetch` | Fetch a single UniFi site | [src/api/unifi/site/fetch.ts](src/api/unifi/site/fetch.ts) | `unifi.access` | +| `unifi.sites.fetch.many` | Fetch all UniFi sites | [src/api/unifi/sites/fetchAll.ts](src/api/unifi/sites/fetchAll.ts) | `unifi.access` | +| `unifi.sites.sync` | Sync sites from the UniFi controller into the database | [src/api/unifi/sites/sync.ts](src/api/unifi/sites/sync.ts) | `unifi.access` | +| `unifi.sites.link` | Link or unlink a UniFi site to/from a company | [src/api/unifi/site/link.ts](src/api/unifi/site/link.ts), [src/api/unifi/site/unlink.ts](src/api/unifi/site/unlink.ts) | `unifi.access` | +| `unifi.site.overview` | View live site overview from the UniFi controller | [src/api/unifi/site/overview.ts](src/api/unifi/site/overview.ts) | `unifi.access` | +| `unifi.site.devices` | View live device list from the UniFi controller | [src/api/unifi/site/devices.ts](src/api/unifi/site/devices.ts) | `unifi.access` | +| `unifi.site.wifi` | View WiFi networks (WLANs) from the UniFi controller | [src/api/unifi/site/wifi/fetchAll.ts](src/api/unifi/site/wifi/fetchAll.ts) | `unifi.access` | +| `unifi.site.wifi.read` | Field-level gate for WiFi response data (see note below) | [src/api/unifi/site/wifi/fetchAll.ts](src/api/unifi/site/wifi/fetchAll.ts) | `unifi.access`, `unifi.site.wifi` | +| `unifi.site.wifi.read.` | Read a specific field from WiFi response (e.g. `unifi.site.wifi.read.passphrase`) | [src/api/unifi/site/wifi/fetchAll.ts](src/api/unifi/site/wifi/fetchAll.ts) | `unifi.access`, `unifi.site.wifi`, `unifi.site.wifi.read` | +| `unifi.site.wifi.update` | Update a WiFi network on the UniFi controller | [src/api/unifi/site/wifi/update.ts](src/api/unifi/site/wifi/update.ts) | `unifi.access`, `unifi.site.wifi` | + +#### Field-Level Permission Gating (`unifi.site.wifi.read`) + +The WiFi fetch route uses `processObjectValuePerms` to filter each WLAN object on a per-field basis. For every key on the `WlanConf` response object, the system checks `unifi.site.wifi.read.`. Only fields the user has permission for are included in the response. Use `unifi.site.wifi.read.*` to grant access to all fields. + +**Available field-level nodes:** + +`unifi.site.wifi.read.id`, `unifi.site.wifi.read.name`, `unifi.site.wifi.read.siteId`, `unifi.site.wifi.read.enabled`, `unifi.site.wifi.read.security`, `unifi.site.wifi.read.wpaMode`, `unifi.site.wifi.read.wpaEnc`, `unifi.site.wifi.read.wpa3Support`, `unifi.site.wifi.read.wpa3Transition`, `unifi.site.wifi.read.wpa3FastRoaming`, `unifi.site.wifi.read.wpa3Enhanced192`, `unifi.site.wifi.read.passphrase`, `unifi.site.wifi.read.passphraseAutogenerated`, `unifi.site.wifi.read.hideSSID`, `unifi.site.wifi.read.isGuest`, `unifi.site.wifi.read.band`, `unifi.site.wifi.read.bands`, `unifi.site.wifi.read.networkconfId`, `unifi.site.wifi.read.usergroupId`, `unifi.site.wifi.read.apGroupIds`, `unifi.site.wifi.read.apGroupMode`, `unifi.site.wifi.read.pmfMode`, `unifi.site.wifi.read.groupRekey`, `unifi.site.wifi.read.dtimMode`, `unifi.site.wifi.read.dtimNg`, `unifi.site.wifi.read.dtimNa`, `unifi.site.wifi.read.dtim6e`, `unifi.site.wifi.read.l2Isolation`, `unifi.site.wifi.read.fastRoamingEnabled`, `unifi.site.wifi.read.bssTransition`, `unifi.site.wifi.read.uapsdEnabled`, `unifi.site.wifi.read.iappEnabled`, `unifi.site.wifi.read.proxyArp`, `unifi.site.wifi.read.mcastenhanceEnabled`, `unifi.site.wifi.read.macFilterEnabled`, `unifi.site.wifi.read.macFilterPolicy`, `unifi.site.wifi.read.macFilterList`, `unifi.site.wifi.read.radiusDasEnabled`, `unifi.site.wifi.read.radiusMacAuthEnabled`, `unifi.site.wifi.read.radiusMacaclFormat`, `unifi.site.wifi.read.minrateSettingPreference`, `unifi.site.wifi.read.minrateNgEnabled`, `unifi.site.wifi.read.minrateNgDataRateKbps`, `unifi.site.wifi.read.minrateNgAdvertisingRates`, `unifi.site.wifi.read.minrateNaEnabled`, `unifi.site.wifi.read.minrateNaDataRateKbps`, `unifi.site.wifi.read.minrateNaAdvertisingRates`, `unifi.site.wifi.read.settingPreference`, `unifi.site.wifi.read.no2ghzOui`, `unifi.site.wifi.read.privatePreSharedKeysEnabled`, `unifi.site.wifi.read.privatePreSharedKeys`, `unifi.site.wifi.read.saeGroups`, `unifi.site.wifi.read.saePsk`, `unifi.site.wifi.read.schedule`, `unifi.site.wifi.read.scheduleWithDuration`, `unifi.site.wifi.read.bcFilterList`, `unifi.site.wifi.read.externalId` +| `unifi.site.networks` | View network configurations from the UniFi controller | [src/api/unifi/site/networks.ts](src/api/unifi/site/networks.ts) | `unifi.access` | +| `unifi.site.wlan-groups` | View WLAN groups (AP broadcasting groups) from the UniFi controller for a site | [src/api/unifi/site/wlanGroups.ts](src/api/unifi/site/wlanGroups.ts), [src/api/unifi/site/wlanGroupsCreate.ts](src/api/unifi/site/wlanGroupsCreate.ts) | `unifi.access` | +| `unifi.site.wlan-groups.create` | Create a new WLAN group (AP broadcasting group) on the UniFi controller | [src/api/unifi/site/wlanGroupsCreate.ts](src/api/unifi/site/wlanGroupsCreate.ts) | `unifi.access`, `unifi.site.wlan-groups` | +| `unifi.site.access-points` | View access points (UAPs only) from the UniFi controller for a site | [src/api/unifi/site/accessPoints.ts](src/api/unifi/site/accessPoints.ts) | `unifi.access` | +| `unifi.site.ap-groups` | View AP groups — shows which APs are grouped together for SSID broadcasting | [src/api/unifi/site/apGroups.ts](src/api/unifi/site/apGroups.ts) | `unifi.access` | +| `unifi.site.wifi-limits` | View WiFi SSID limits per AP per radio band | [src/api/unifi/site/wifiLimits.ts](src/api/unifi/site/wifiLimits.ts) | `unifi.access` | +| `unifi.site.speed-profiles` | View speed limit profiles (user groups) from the UniFi controller | [src/api/unifi/site/speedProfilesFetchAll.ts](src/api/unifi/site/speedProfilesFetchAll.ts), [src/api/unifi/site/speedProfilesCreate.ts](src/api/unifi/site/speedProfilesCreate.ts) | `unifi.access` | +| `unifi.site.speed-profiles.create` | Create a new speed limit profile (user group) on the UniFi controller | [src/api/unifi/site/speedProfilesCreate.ts](src/api/unifi/site/speedProfilesCreate.ts) | `unifi.access`, `unifi.site.speed-profiles` | +| `unifi.site.wifi.ppsk` | View private pre-shared keys (PPSKs) for a specific WiFi network | [src/api/unifi/site/wifi/ppskFetchAll.ts](src/api/unifi/site/wifi/ppskFetchAll.ts), [src/api/unifi/site/wifi/ppskCreate.ts](src/api/unifi/site/wifi/ppskCreate.ts) | `unifi.access`, `unifi.site.wifi` | +| `unifi.site.wifi.ppsk.create` | Create a private pre-shared key on a specific WiFi network | [src/api/unifi/site/wifi/ppskCreate.ts](src/api/unifi/site/wifi/ppskCreate.ts) | `unifi.access`, `unifi.site.wifi`, `unifi.site.wifi.ppsk` | + ## Permission Issuers Permissions can be issued by different sources: diff --git a/generated/prisma/browser.ts b/generated/prisma/browser.ts index 8fbda0f..c456817 100644 --- a/generated/prisma/browser.ts +++ b/generated/prisma/browser.ts @@ -32,6 +32,11 @@ export type User = Prisma.UserModel * */ export type Role = Prisma.RoleModel +/** + * Model UnifiSite + * + */ +export type UnifiSite = Prisma.UnifiSiteModel /** * Model Company * diff --git a/generated/prisma/client.ts b/generated/prisma/client.ts index 76ad72c..495947d 100644 --- a/generated/prisma/client.ts +++ b/generated/prisma/client.ts @@ -54,6 +54,11 @@ export type User = Prisma.UserModel * */ export type Role = Prisma.RoleModel +/** + * Model UnifiSite + * + */ +export type UnifiSite = Prisma.UnifiSiteModel /** * Model Company * diff --git a/generated/prisma/internal/class.ts b/generated/prisma/internal/class.ts index f500f20..0b83420 100644 --- a/generated/prisma/internal/class.ts +++ b/generated/prisma/internal/class.ts @@ -20,7 +20,7 @@ const config: runtime.GetPrismaClientConfig = { "clientVersion": "7.3.0", "engineVersion": "9d6ad21cbbceab97458517b147a6a09ff43aa735", "activeProvider": "postgresql", - "inlineSchema": "// This is your Prisma schema file,\n// learn more about it in the docs: https://pris.ly/d/prisma-schema\n\n// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?\n// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init\n\ngenerator client {\n provider = \"prisma-client\"\n output = \"../generated/prisma\"\n}\n\ndatasource db {\n provider = \"postgresql\"\n}\n\nmodel Session {\n id String @id @default(uuid())\n sessionKey String @unique @default(cuid())\n userId String\n expires DateTime\n refreshTokenGenerated Boolean @default(false)\n refreshedAt DateTime?\n invalidatedAt DateTime?\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n}\n\nmodel User {\n id String @id @default(cuid())\n roles Role[]\n permissions String?\n login String @unique\n name String?\n email String @unique\n emailVerified DateTime?\n image String?\n\n userId String @unique\n token String?\n\n sessions Session[]\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n\nmodel Role {\n id String @id @default(uuid())\n title String\n moniker String @unique // e.g. admin, super_admin, moderator\n\n permissions String\n users User[]\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n\nmodel Company {\n id String @id @default(cuid())\n name String\n\n cw_CompanyId Int @unique\n cw_Identifier String @unique\n\n credentials Credential[]\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n\nmodel CredentialType {\n id String @id @default(cuid())\n name String @unique\n\n permissionScope String\n icon String?\n fields Json\n\n credentials Credential[]\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n\nmodel SecureValue {\n id String @id @default(cuid())\n name String\n\n content String // Encrypted content\n hash String // Hash of the original content for integrity verification and Search\n\n credentialId String\n credential Credential @relation(fields: [credentialId], references: [id], onDelete: Cascade)\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n\nmodel Credential {\n id String @id @default(cuid())\n name String\n notes String?\n\n typeId String\n type CredentialType @relation(fields: [typeId], references: [id], onDelete: Cascade)\n\n fields Json\n\n companyId String\n company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)\n\n securevalues SecureValue[]\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n", + "inlineSchema": "// This is your Prisma schema file,\n// learn more about it in the docs: https://pris.ly/d/prisma-schema\n\n// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?\n// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init\n\ngenerator client {\n provider = \"prisma-client\"\n output = \"../generated/prisma\"\n}\n\ndatasource db {\n provider = \"postgresql\"\n}\n\nmodel Session {\n id String @id @default(uuid())\n sessionKey String @unique @default(cuid())\n userId String\n expires DateTime\n refreshTokenGenerated Boolean @default(false)\n refreshedAt DateTime?\n invalidatedAt DateTime?\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n}\n\nmodel User {\n id String @id @default(cuid())\n roles Role[]\n permissions String?\n login String @unique\n name String?\n email String @unique\n emailVerified DateTime?\n image String?\n\n userId String @unique\n token String?\n\n sessions Session[]\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n\nmodel Role {\n id String @id @default(uuid())\n title String\n moniker String @unique // e.g. admin, super_admin, moderator\n\n permissions String\n users User[]\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n\nmodel UnifiSite {\n id String @id @default(cuid())\n name String\n\n siteId String @unique\n\n companyId String?\n company Company? @relation(fields: [companyId], references: [id])\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n\nmodel Company {\n id String @id @default(cuid())\n name String\n\n cw_CompanyId Int @unique\n cw_Identifier String @unique\n\n credentials Credential[]\n unifiSites UnifiSite[]\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n\nmodel CredentialType {\n id String @id @default(cuid())\n name String @unique\n\n permissionScope String\n icon String?\n fields Json\n\n credentials Credential[]\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n\nmodel SecureValue {\n id String @id @default(cuid())\n name String\n\n content String // Encrypted content\n hash String // Hash of the original content for integrity verification and Search\n\n credentialId String\n credential Credential @relation(fields: [credentialId], references: [id], onDelete: Cascade)\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n\nmodel Credential {\n id String @id @default(cuid())\n name String\n notes String?\n subCredentialOfId String?\n subCredentialOf Credential? @relation(\"SubCredentials\", fields: [subCredentialOfId], references: [id], onDelete: Cascade)\n subCredentials Credential[] @relation(\"SubCredentials\")\n\n typeId String\n type CredentialType @relation(fields: [typeId], references: [id], onDelete: Cascade)\n\n fields Json\n\n companyId String\n company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)\n\n securevalues SecureValue[]\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n", "runtimeDataModel": { "models": {}, "enums": {}, @@ -28,7 +28,7 @@ const config: runtime.GetPrismaClientConfig = { } } -config.runtimeDataModel = JSON.parse("{\"models\":{\"Session\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"sessionKey\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"expires\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"refreshTokenGenerated\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"refreshedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"invalidatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"SessionToUser\"}],\"dbName\":null},\"User\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"roles\",\"kind\":\"object\",\"type\":\"Role\",\"relationName\":\"RoleToUser\"},{\"name\":\"permissions\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"login\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"email\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"emailVerified\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"image\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"token\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"sessions\",\"kind\":\"object\",\"type\":\"Session\",\"relationName\":\"SessionToUser\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null},\"Role\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"title\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"moniker\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"permissions\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"users\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"RoleToUser\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null},\"Company\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"cw_CompanyId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"cw_Identifier\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"credentials\",\"kind\":\"object\",\"type\":\"Credential\",\"relationName\":\"CompanyToCredential\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null},\"CredentialType\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"permissionScope\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"icon\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"fields\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"credentials\",\"kind\":\"object\",\"type\":\"Credential\",\"relationName\":\"CredentialToCredentialType\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null},\"SecureValue\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"content\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"hash\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"credentialId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"credential\",\"kind\":\"object\",\"type\":\"Credential\",\"relationName\":\"CredentialToSecureValue\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null},\"Credential\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"notes\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"typeId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"type\",\"kind\":\"object\",\"type\":\"CredentialType\",\"relationName\":\"CredentialToCredentialType\"},{\"name\":\"fields\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"companyId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"company\",\"kind\":\"object\",\"type\":\"Company\",\"relationName\":\"CompanyToCredential\"},{\"name\":\"securevalues\",\"kind\":\"object\",\"type\":\"SecureValue\",\"relationName\":\"CredentialToSecureValue\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null}},\"enums\":{},\"types\":{}}") +config.runtimeDataModel = JSON.parse("{\"models\":{\"Session\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"sessionKey\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"expires\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"refreshTokenGenerated\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"refreshedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"invalidatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"SessionToUser\"}],\"dbName\":null},\"User\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"roles\",\"kind\":\"object\",\"type\":\"Role\",\"relationName\":\"RoleToUser\"},{\"name\":\"permissions\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"login\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"email\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"emailVerified\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"image\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"token\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"sessions\",\"kind\":\"object\",\"type\":\"Session\",\"relationName\":\"SessionToUser\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null},\"Role\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"title\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"moniker\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"permissions\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"users\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"RoleToUser\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null},\"UnifiSite\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"siteId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"companyId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"company\",\"kind\":\"object\",\"type\":\"Company\",\"relationName\":\"CompanyToUnifiSite\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null},\"Company\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"cw_CompanyId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"cw_Identifier\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"credentials\",\"kind\":\"object\",\"type\":\"Credential\",\"relationName\":\"CompanyToCredential\"},{\"name\":\"unifiSites\",\"kind\":\"object\",\"type\":\"UnifiSite\",\"relationName\":\"CompanyToUnifiSite\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null},\"CredentialType\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"permissionScope\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"icon\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"fields\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"credentials\",\"kind\":\"object\",\"type\":\"Credential\",\"relationName\":\"CredentialToCredentialType\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null},\"SecureValue\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"content\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"hash\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"credentialId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"credential\",\"kind\":\"object\",\"type\":\"Credential\",\"relationName\":\"CredentialToSecureValue\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null},\"Credential\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"notes\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"subCredentialOfId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"subCredentialOf\",\"kind\":\"object\",\"type\":\"Credential\",\"relationName\":\"SubCredentials\"},{\"name\":\"subCredentials\",\"kind\":\"object\",\"type\":\"Credential\",\"relationName\":\"SubCredentials\"},{\"name\":\"typeId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"type\",\"kind\":\"object\",\"type\":\"CredentialType\",\"relationName\":\"CredentialToCredentialType\"},{\"name\":\"fields\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"companyId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"company\",\"kind\":\"object\",\"type\":\"Company\",\"relationName\":\"CompanyToCredential\"},{\"name\":\"securevalues\",\"kind\":\"object\",\"type\":\"SecureValue\",\"relationName\":\"CredentialToSecureValue\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null}},\"enums\":{},\"types\":{}}") async function decodeBase64AsWasm(wasmBase64: string): Promise { const { Buffer } = await import('node:buffer') @@ -206,6 +206,16 @@ export interface PrismaClient< */ get role(): Prisma.RoleDelegate; + /** + * `prisma.unifiSite`: Exposes CRUD operations for the **UnifiSite** model. + * Example usage: + * ```ts + * // Fetch zero or more UnifiSites + * const unifiSites = await prisma.unifiSite.findMany() + * ``` + */ + get unifiSite(): Prisma.UnifiSiteDelegate; + /** * `prisma.company`: Exposes CRUD operations for the **Company** model. * Example usage: diff --git a/generated/prisma/internal/prismaNamespace.ts b/generated/prisma/internal/prismaNamespace.ts index 1ffb4b2..e67bbf4 100644 --- a/generated/prisma/internal/prismaNamespace.ts +++ b/generated/prisma/internal/prismaNamespace.ts @@ -387,6 +387,7 @@ export const ModelName = { Session: 'Session', User: 'User', Role: 'Role', + UnifiSite: 'UnifiSite', Company: 'Company', CredentialType: 'CredentialType', SecureValue: 'SecureValue', @@ -406,7 +407,7 @@ export type TypeMap + fields: Prisma.UnifiSiteFieldRefs + operations: { + findUnique: { + args: Prisma.UnifiSiteFindUniqueArgs + result: runtime.Types.Utils.PayloadToResult | null + } + findUniqueOrThrow: { + args: Prisma.UnifiSiteFindUniqueOrThrowArgs + result: runtime.Types.Utils.PayloadToResult + } + findFirst: { + args: Prisma.UnifiSiteFindFirstArgs + result: runtime.Types.Utils.PayloadToResult | null + } + findFirstOrThrow: { + args: Prisma.UnifiSiteFindFirstOrThrowArgs + result: runtime.Types.Utils.PayloadToResult + } + findMany: { + args: Prisma.UnifiSiteFindManyArgs + result: runtime.Types.Utils.PayloadToResult[] + } + create: { + args: Prisma.UnifiSiteCreateArgs + result: runtime.Types.Utils.PayloadToResult + } + createMany: { + args: Prisma.UnifiSiteCreateManyArgs + result: BatchPayload + } + createManyAndReturn: { + args: Prisma.UnifiSiteCreateManyAndReturnArgs + result: runtime.Types.Utils.PayloadToResult[] + } + delete: { + args: Prisma.UnifiSiteDeleteArgs + result: runtime.Types.Utils.PayloadToResult + } + update: { + args: Prisma.UnifiSiteUpdateArgs + result: runtime.Types.Utils.PayloadToResult + } + deleteMany: { + args: Prisma.UnifiSiteDeleteManyArgs + result: BatchPayload + } + updateMany: { + args: Prisma.UnifiSiteUpdateManyArgs + result: BatchPayload + } + updateManyAndReturn: { + args: Prisma.UnifiSiteUpdateManyAndReturnArgs + result: runtime.Types.Utils.PayloadToResult[] + } + upsert: { + args: Prisma.UnifiSiteUpsertArgs + result: runtime.Types.Utils.PayloadToResult + } + aggregate: { + args: Prisma.UnifiSiteAggregateArgs + result: runtime.Types.Utils.Optional + } + groupBy: { + args: Prisma.UnifiSiteGroupByArgs + result: runtime.Types.Utils.Optional[] + } + count: { + args: Prisma.UnifiSiteCountArgs + result: runtime.Types.Utils.Optional | number + } + } + } Company: { payload: Prisma.$CompanyPayload fields: Prisma.CompanyFieldRefs @@ -1009,6 +1084,18 @@ export const RoleScalarFieldEnum = { export type RoleScalarFieldEnum = (typeof RoleScalarFieldEnum)[keyof typeof RoleScalarFieldEnum] +export const UnifiSiteScalarFieldEnum = { + id: 'id', + name: 'name', + siteId: 'siteId', + companyId: 'companyId', + createdAt: 'createdAt', + updatedAt: 'updatedAt' +} as const + +export type UnifiSiteScalarFieldEnum = (typeof UnifiSiteScalarFieldEnum)[keyof typeof UnifiSiteScalarFieldEnum] + + export const CompanyScalarFieldEnum = { id: 'id', name: 'name', @@ -1051,6 +1138,7 @@ export const CredentialScalarFieldEnum = { id: 'id', name: 'name', notes: 'notes', + subCredentialOfId: 'subCredentialOfId', typeId: 'typeId', fields: 'fields', companyId: 'companyId', @@ -1281,6 +1369,7 @@ export type GlobalOmitConfig = { session?: Prisma.SessionOmit user?: Prisma.UserOmit role?: Prisma.RoleOmit + unifiSite?: Prisma.UnifiSiteOmit company?: Prisma.CompanyOmit credentialType?: Prisma.CredentialTypeOmit secureValue?: Prisma.SecureValueOmit diff --git a/generated/prisma/internal/prismaNamespaceBrowser.ts b/generated/prisma/internal/prismaNamespaceBrowser.ts index 7a6cfbf..2951e43 100644 --- a/generated/prisma/internal/prismaNamespaceBrowser.ts +++ b/generated/prisma/internal/prismaNamespaceBrowser.ts @@ -54,6 +54,7 @@ export const ModelName = { Session: 'Session', User: 'User', Role: 'Role', + UnifiSite: 'UnifiSite', Company: 'Company', CredentialType: 'CredentialType', SecureValue: 'SecureValue', @@ -118,6 +119,18 @@ export const RoleScalarFieldEnum = { export type RoleScalarFieldEnum = (typeof RoleScalarFieldEnum)[keyof typeof RoleScalarFieldEnum] +export const UnifiSiteScalarFieldEnum = { + id: 'id', + name: 'name', + siteId: 'siteId', + companyId: 'companyId', + createdAt: 'createdAt', + updatedAt: 'updatedAt' +} as const + +export type UnifiSiteScalarFieldEnum = (typeof UnifiSiteScalarFieldEnum)[keyof typeof UnifiSiteScalarFieldEnum] + + export const CompanyScalarFieldEnum = { id: 'id', name: 'name', @@ -160,6 +173,7 @@ export const CredentialScalarFieldEnum = { id: 'id', name: 'name', notes: 'notes', + subCredentialOfId: 'subCredentialOfId', typeId: 'typeId', fields: 'fields', companyId: 'companyId', diff --git a/generated/prisma/models.ts b/generated/prisma/models.ts index b33ceb9..da02256 100644 --- a/generated/prisma/models.ts +++ b/generated/prisma/models.ts @@ -11,6 +11,7 @@ export type * from './models/Session.ts' export type * from './models/User.ts' export type * from './models/Role.ts' +export type * from './models/UnifiSite.ts' export type * from './models/Company.ts' export type * from './models/CredentialType.ts' export type * from './models/SecureValue.ts' diff --git a/generated/prisma/models/Company.ts b/generated/prisma/models/Company.ts index 170087b..8f2bb11 100644 --- a/generated/prisma/models/Company.ts +++ b/generated/prisma/models/Company.ts @@ -225,6 +225,7 @@ export type CompanyWhereInput = { createdAt?: Prisma.DateTimeFilter<"Company"> | Date | string updatedAt?: Prisma.DateTimeFilter<"Company"> | Date | string credentials?: Prisma.CredentialListRelationFilter + unifiSites?: Prisma.UnifiSiteListRelationFilter } export type CompanyOrderByWithRelationInput = { @@ -235,6 +236,7 @@ export type CompanyOrderByWithRelationInput = { createdAt?: Prisma.SortOrder updatedAt?: Prisma.SortOrder credentials?: Prisma.CredentialOrderByRelationAggregateInput + unifiSites?: Prisma.UnifiSiteOrderByRelationAggregateInput } export type CompanyWhereUniqueInput = Prisma.AtLeast<{ @@ -248,6 +250,7 @@ export type CompanyWhereUniqueInput = Prisma.AtLeast<{ createdAt?: Prisma.DateTimeFilter<"Company"> | Date | string updatedAt?: Prisma.DateTimeFilter<"Company"> | Date | string credentials?: Prisma.CredentialListRelationFilter + unifiSites?: Prisma.UnifiSiteListRelationFilter }, "id" | "cw_CompanyId" | "cw_Identifier"> export type CompanyOrderByWithAggregationInput = { @@ -284,6 +287,7 @@ export type CompanyCreateInput = { createdAt?: Date | string updatedAt?: Date | string credentials?: Prisma.CredentialCreateNestedManyWithoutCompanyInput + unifiSites?: Prisma.UnifiSiteCreateNestedManyWithoutCompanyInput } export type CompanyUncheckedCreateInput = { @@ -294,6 +298,7 @@ export type CompanyUncheckedCreateInput = { createdAt?: Date | string updatedAt?: Date | string credentials?: Prisma.CredentialUncheckedCreateNestedManyWithoutCompanyInput + unifiSites?: Prisma.UnifiSiteUncheckedCreateNestedManyWithoutCompanyInput } export type CompanyUpdateInput = { @@ -304,6 +309,7 @@ export type CompanyUpdateInput = { createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string credentials?: Prisma.CredentialUpdateManyWithoutCompanyNestedInput + unifiSites?: Prisma.UnifiSiteUpdateManyWithoutCompanyNestedInput } export type CompanyUncheckedUpdateInput = { @@ -314,6 +320,7 @@ export type CompanyUncheckedUpdateInput = { createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string credentials?: Prisma.CredentialUncheckedUpdateManyWithoutCompanyNestedInput + unifiSites?: Prisma.UnifiSiteUncheckedUpdateManyWithoutCompanyNestedInput } export type CompanyCreateManyInput = { @@ -343,6 +350,11 @@ export type CompanyUncheckedUpdateManyInput = { updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string } +export type CompanyNullableScalarRelationFilter = { + is?: Prisma.CompanyWhereInput | null + isNot?: Prisma.CompanyWhereInput | null +} + export type CompanyCountOrderByAggregateInput = { id?: Prisma.SortOrder name?: Prisma.SortOrder @@ -383,6 +395,22 @@ export type CompanyScalarRelationFilter = { isNot?: Prisma.CompanyWhereInput } +export type CompanyCreateNestedOneWithoutUnifiSitesInput = { + create?: Prisma.XOR + connectOrCreate?: Prisma.CompanyCreateOrConnectWithoutUnifiSitesInput + connect?: Prisma.CompanyWhereUniqueInput +} + +export type CompanyUpdateOneWithoutUnifiSitesNestedInput = { + create?: Prisma.XOR + connectOrCreate?: Prisma.CompanyCreateOrConnectWithoutUnifiSitesInput + upsert?: Prisma.CompanyUpsertWithoutUnifiSitesInput + disconnect?: Prisma.CompanyWhereInput | boolean + delete?: Prisma.CompanyWhereInput | boolean + connect?: Prisma.CompanyWhereUniqueInput + update?: Prisma.XOR, Prisma.CompanyUncheckedUpdateWithoutUnifiSitesInput> +} + export type IntFieldUpdateOperationsInput = { set?: number increment?: number @@ -405,6 +433,62 @@ export type CompanyUpdateOneRequiredWithoutCredentialsNestedInput = { update?: Prisma.XOR, Prisma.CompanyUncheckedUpdateWithoutCredentialsInput> } +export type CompanyCreateWithoutUnifiSitesInput = { + id?: string + name: string + cw_CompanyId: number + cw_Identifier: string + createdAt?: Date | string + updatedAt?: Date | string + credentials?: Prisma.CredentialCreateNestedManyWithoutCompanyInput +} + +export type CompanyUncheckedCreateWithoutUnifiSitesInput = { + id?: string + name: string + cw_CompanyId: number + cw_Identifier: string + createdAt?: Date | string + updatedAt?: Date | string + credentials?: Prisma.CredentialUncheckedCreateNestedManyWithoutCompanyInput +} + +export type CompanyCreateOrConnectWithoutUnifiSitesInput = { + where: Prisma.CompanyWhereUniqueInput + create: Prisma.XOR +} + +export type CompanyUpsertWithoutUnifiSitesInput = { + update: Prisma.XOR + create: Prisma.XOR + where?: Prisma.CompanyWhereInput +} + +export type CompanyUpdateToOneWithWhereWithoutUnifiSitesInput = { + where?: Prisma.CompanyWhereInput + data: Prisma.XOR +} + +export type CompanyUpdateWithoutUnifiSitesInput = { + id?: Prisma.StringFieldUpdateOperationsInput | string + name?: Prisma.StringFieldUpdateOperationsInput | string + cw_CompanyId?: Prisma.IntFieldUpdateOperationsInput | number + cw_Identifier?: Prisma.StringFieldUpdateOperationsInput | string + createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string + updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string + credentials?: Prisma.CredentialUpdateManyWithoutCompanyNestedInput +} + +export type CompanyUncheckedUpdateWithoutUnifiSitesInput = { + id?: Prisma.StringFieldUpdateOperationsInput | string + name?: Prisma.StringFieldUpdateOperationsInput | string + cw_CompanyId?: Prisma.IntFieldUpdateOperationsInput | number + cw_Identifier?: Prisma.StringFieldUpdateOperationsInput | string + createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string + updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string + credentials?: Prisma.CredentialUncheckedUpdateManyWithoutCompanyNestedInput +} + export type CompanyCreateWithoutCredentialsInput = { id?: string name: string @@ -412,6 +496,7 @@ export type CompanyCreateWithoutCredentialsInput = { cw_Identifier: string createdAt?: Date | string updatedAt?: Date | string + unifiSites?: Prisma.UnifiSiteCreateNestedManyWithoutCompanyInput } export type CompanyUncheckedCreateWithoutCredentialsInput = { @@ -421,6 +506,7 @@ export type CompanyUncheckedCreateWithoutCredentialsInput = { cw_Identifier: string createdAt?: Date | string updatedAt?: Date | string + unifiSites?: Prisma.UnifiSiteUncheckedCreateNestedManyWithoutCompanyInput } export type CompanyCreateOrConnectWithoutCredentialsInput = { @@ -446,6 +532,7 @@ export type CompanyUpdateWithoutCredentialsInput = { cw_Identifier?: Prisma.StringFieldUpdateOperationsInput | string createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string + unifiSites?: Prisma.UnifiSiteUpdateManyWithoutCompanyNestedInput } export type CompanyUncheckedUpdateWithoutCredentialsInput = { @@ -455,6 +542,7 @@ export type CompanyUncheckedUpdateWithoutCredentialsInput = { cw_Identifier?: Prisma.StringFieldUpdateOperationsInput | string createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string + unifiSites?: Prisma.UnifiSiteUncheckedUpdateManyWithoutCompanyNestedInput } @@ -464,10 +552,12 @@ export type CompanyUncheckedUpdateWithoutCredentialsInput = { export type CompanyCountOutputType = { credentials: number + unifiSites: number } export type CompanyCountOutputTypeSelect = { credentials?: boolean | CompanyCountOutputTypeCountCredentialsArgs + unifiSites?: boolean | CompanyCountOutputTypeCountUnifiSitesArgs } /** @@ -487,6 +577,13 @@ export type CompanyCountOutputTypeCountCredentialsArgs = { + where?: Prisma.UnifiSiteWhereInput +} + export type CompanySelect = runtime.Types.Extensions.GetSelect<{ id?: boolean @@ -496,6 +593,7 @@ export type CompanySelect + unifiSites?: boolean | Prisma.Company$unifiSitesArgs _count?: boolean | Prisma.CompanyCountOutputTypeDefaultArgs }, ExtArgs["result"]["company"]> @@ -529,6 +627,7 @@ export type CompanySelectScalar = { export type CompanyOmit = runtime.Types.Extensions.GetOmit<"id" | "name" | "cw_CompanyId" | "cw_Identifier" | "createdAt" | "updatedAt", ExtArgs["result"]["company"]> export type CompanyInclude = { credentials?: boolean | Prisma.Company$credentialsArgs + unifiSites?: boolean | Prisma.Company$unifiSitesArgs _count?: boolean | Prisma.CompanyCountOutputTypeDefaultArgs } export type CompanyIncludeCreateManyAndReturn = {} @@ -538,6 +637,7 @@ export type $CompanyPayload[] + unifiSites: Prisma.$UnifiSitePayload[] } scalars: runtime.Types.Extensions.GetPayloadResult<{ id: string @@ -941,6 +1041,7 @@ readonly fields: CompanyFieldRefs; export interface Prisma__CompanyClient extends Prisma.PrismaPromise { readonly [Symbol.toStringTag]: "PrismaPromise" credentials = {}>(args?: Prisma.Subset>): Prisma.PrismaPromise, T, "findMany", GlobalOmitOptions> | Null> + unifiSites = {}>(args?: Prisma.Subset>): Prisma.PrismaPromise, T, "findMany", GlobalOmitOptions> | Null> /** * Attaches callbacks for the resolution and/or rejection of the Promise. * @param onfulfilled The callback to execute when the Promise is resolved. @@ -1387,6 +1488,30 @@ export type Company$credentialsArgs = { + /** + * Select specific fields to fetch from the UnifiSite + */ + select?: Prisma.UnifiSiteSelect | null + /** + * Omit specific fields from the UnifiSite + */ + omit?: Prisma.UnifiSiteOmit | null + /** + * Choose, which related nodes to fetch as well + */ + include?: Prisma.UnifiSiteInclude | null + where?: Prisma.UnifiSiteWhereInput + orderBy?: Prisma.UnifiSiteOrderByWithRelationInput | Prisma.UnifiSiteOrderByWithRelationInput[] + cursor?: Prisma.UnifiSiteWhereUniqueInput + take?: number + skip?: number + distinct?: Prisma.UnifiSiteScalarFieldEnum | Prisma.UnifiSiteScalarFieldEnum[] +} + /** * Company without action */ diff --git a/generated/prisma/models/Credential.ts b/generated/prisma/models/Credential.ts index d42ea43..aa992c2 100644 --- a/generated/prisma/models/Credential.ts +++ b/generated/prisma/models/Credential.ts @@ -28,6 +28,7 @@ export type CredentialMinAggregateOutputType = { id: string | null name: string | null notes: string | null + subCredentialOfId: string | null typeId: string | null companyId: string | null createdAt: Date | null @@ -38,6 +39,7 @@ export type CredentialMaxAggregateOutputType = { id: string | null name: string | null notes: string | null + subCredentialOfId: string | null typeId: string | null companyId: string | null createdAt: Date | null @@ -48,6 +50,7 @@ export type CredentialCountAggregateOutputType = { id: number name: number notes: number + subCredentialOfId: number typeId: number fields: number companyId: number @@ -61,6 +64,7 @@ export type CredentialMinAggregateInputType = { id?: true name?: true notes?: true + subCredentialOfId?: true typeId?: true companyId?: true createdAt?: true @@ -71,6 +75,7 @@ export type CredentialMaxAggregateInputType = { id?: true name?: true notes?: true + subCredentialOfId?: true typeId?: true companyId?: true createdAt?: true @@ -81,6 +86,7 @@ export type CredentialCountAggregateInputType = { id?: true name?: true notes?: true + subCredentialOfId?: true typeId?: true fields?: true companyId?: true @@ -165,6 +171,7 @@ export type CredentialGroupByOutputType = { id: string name: string notes: string | null + subCredentialOfId: string | null typeId: string fields: runtime.JsonValue companyId: string @@ -197,11 +204,14 @@ export type CredentialWhereInput = { id?: Prisma.StringFilter<"Credential"> | string name?: Prisma.StringFilter<"Credential"> | string notes?: Prisma.StringNullableFilter<"Credential"> | string | null + subCredentialOfId?: Prisma.StringNullableFilter<"Credential"> | string | null typeId?: Prisma.StringFilter<"Credential"> | string fields?: Prisma.JsonFilter<"Credential"> companyId?: Prisma.StringFilter<"Credential"> | string createdAt?: Prisma.DateTimeFilter<"Credential"> | Date | string updatedAt?: Prisma.DateTimeFilter<"Credential"> | Date | string + subCredentialOf?: Prisma.XOR | null + subCredentials?: Prisma.CredentialListRelationFilter type?: Prisma.XOR company?: Prisma.XOR securevalues?: Prisma.SecureValueListRelationFilter @@ -211,11 +221,14 @@ export type CredentialOrderByWithRelationInput = { id?: Prisma.SortOrder name?: Prisma.SortOrder notes?: Prisma.SortOrderInput | Prisma.SortOrder + subCredentialOfId?: Prisma.SortOrderInput | Prisma.SortOrder typeId?: Prisma.SortOrder fields?: Prisma.SortOrder companyId?: Prisma.SortOrder createdAt?: Prisma.SortOrder updatedAt?: Prisma.SortOrder + subCredentialOf?: Prisma.CredentialOrderByWithRelationInput + subCredentials?: Prisma.CredentialOrderByRelationAggregateInput type?: Prisma.CredentialTypeOrderByWithRelationInput company?: Prisma.CompanyOrderByWithRelationInput securevalues?: Prisma.SecureValueOrderByRelationAggregateInput @@ -228,11 +241,14 @@ export type CredentialWhereUniqueInput = Prisma.AtLeast<{ NOT?: Prisma.CredentialWhereInput | Prisma.CredentialWhereInput[] name?: Prisma.StringFilter<"Credential"> | string notes?: Prisma.StringNullableFilter<"Credential"> | string | null + subCredentialOfId?: Prisma.StringNullableFilter<"Credential"> | string | null typeId?: Prisma.StringFilter<"Credential"> | string fields?: Prisma.JsonFilter<"Credential"> companyId?: Prisma.StringFilter<"Credential"> | string createdAt?: Prisma.DateTimeFilter<"Credential"> | Date | string updatedAt?: Prisma.DateTimeFilter<"Credential"> | Date | string + subCredentialOf?: Prisma.XOR | null + subCredentials?: Prisma.CredentialListRelationFilter type?: Prisma.XOR company?: Prisma.XOR securevalues?: Prisma.SecureValueListRelationFilter @@ -242,6 +258,7 @@ export type CredentialOrderByWithAggregationInput = { id?: Prisma.SortOrder name?: Prisma.SortOrder notes?: Prisma.SortOrderInput | Prisma.SortOrder + subCredentialOfId?: Prisma.SortOrderInput | Prisma.SortOrder typeId?: Prisma.SortOrder fields?: Prisma.SortOrder companyId?: Prisma.SortOrder @@ -259,6 +276,7 @@ export type CredentialScalarWhereWithAggregatesInput = { id?: Prisma.StringWithAggregatesFilter<"Credential"> | string name?: Prisma.StringWithAggregatesFilter<"Credential"> | string notes?: Prisma.StringNullableWithAggregatesFilter<"Credential"> | string | null + subCredentialOfId?: Prisma.StringNullableWithAggregatesFilter<"Credential"> | string | null typeId?: Prisma.StringWithAggregatesFilter<"Credential"> | string fields?: Prisma.JsonWithAggregatesFilter<"Credential"> companyId?: Prisma.StringWithAggregatesFilter<"Credential"> | string @@ -273,6 +291,8 @@ export type CredentialCreateInput = { fields: Prisma.JsonNullValueInput | runtime.InputJsonValue createdAt?: Date | string updatedAt?: Date | string + subCredentialOf?: Prisma.CredentialCreateNestedOneWithoutSubCredentialsInput + subCredentials?: Prisma.CredentialCreateNestedManyWithoutSubCredentialOfInput type: Prisma.CredentialTypeCreateNestedOneWithoutCredentialsInput company: Prisma.CompanyCreateNestedOneWithoutCredentialsInput securevalues?: Prisma.SecureValueCreateNestedManyWithoutCredentialInput @@ -282,11 +302,13 @@ export type CredentialUncheckedCreateInput = { id?: string name: string notes?: string | null + subCredentialOfId?: string | null typeId: string fields: Prisma.JsonNullValueInput | runtime.InputJsonValue companyId: string createdAt?: Date | string updatedAt?: Date | string + subCredentials?: Prisma.CredentialUncheckedCreateNestedManyWithoutSubCredentialOfInput securevalues?: Prisma.SecureValueUncheckedCreateNestedManyWithoutCredentialInput } @@ -297,6 +319,8 @@ export type CredentialUpdateInput = { fields?: Prisma.JsonNullValueInput | runtime.InputJsonValue createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string + subCredentialOf?: Prisma.CredentialUpdateOneWithoutSubCredentialsNestedInput + subCredentials?: Prisma.CredentialUpdateManyWithoutSubCredentialOfNestedInput type?: Prisma.CredentialTypeUpdateOneRequiredWithoutCredentialsNestedInput company?: Prisma.CompanyUpdateOneRequiredWithoutCredentialsNestedInput securevalues?: Prisma.SecureValueUpdateManyWithoutCredentialNestedInput @@ -306,11 +330,13 @@ export type CredentialUncheckedUpdateInput = { id?: Prisma.StringFieldUpdateOperationsInput | string name?: Prisma.StringFieldUpdateOperationsInput | string notes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + subCredentialOfId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null typeId?: Prisma.StringFieldUpdateOperationsInput | string fields?: Prisma.JsonNullValueInput | runtime.InputJsonValue companyId?: Prisma.StringFieldUpdateOperationsInput | string createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string + subCredentials?: Prisma.CredentialUncheckedUpdateManyWithoutSubCredentialOfNestedInput securevalues?: Prisma.SecureValueUncheckedUpdateManyWithoutCredentialNestedInput } @@ -318,6 +344,7 @@ export type CredentialCreateManyInput = { id?: string name: string notes?: string | null + subCredentialOfId?: string | null typeId: string fields: Prisma.JsonNullValueInput | runtime.InputJsonValue companyId: string @@ -338,6 +365,7 @@ export type CredentialUncheckedUpdateManyInput = { id?: Prisma.StringFieldUpdateOperationsInput | string name?: Prisma.StringFieldUpdateOperationsInput | string notes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + subCredentialOfId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null typeId?: Prisma.StringFieldUpdateOperationsInput | string fields?: Prisma.JsonNullValueInput | runtime.InputJsonValue companyId?: Prisma.StringFieldUpdateOperationsInput | string @@ -360,10 +388,16 @@ export type CredentialScalarRelationFilter = { isNot?: Prisma.CredentialWhereInput } +export type CredentialNullableScalarRelationFilter = { + is?: Prisma.CredentialWhereInput | null + isNot?: Prisma.CredentialWhereInput | null +} + export type CredentialCountOrderByAggregateInput = { id?: Prisma.SortOrder name?: Prisma.SortOrder notes?: Prisma.SortOrder + subCredentialOfId?: Prisma.SortOrder typeId?: Prisma.SortOrder fields?: Prisma.SortOrder companyId?: Prisma.SortOrder @@ -375,6 +409,7 @@ export type CredentialMaxOrderByAggregateInput = { id?: Prisma.SortOrder name?: Prisma.SortOrder notes?: Prisma.SortOrder + subCredentialOfId?: Prisma.SortOrder typeId?: Prisma.SortOrder companyId?: Prisma.SortOrder createdAt?: Prisma.SortOrder @@ -385,6 +420,7 @@ export type CredentialMinOrderByAggregateInput = { id?: Prisma.SortOrder name?: Prisma.SortOrder notes?: Prisma.SortOrder + subCredentialOfId?: Prisma.SortOrder typeId?: Prisma.SortOrder companyId?: Prisma.SortOrder createdAt?: Prisma.SortOrder @@ -489,6 +525,64 @@ export type CredentialUpdateOneRequiredWithoutSecurevaluesNestedInput = { update?: Prisma.XOR, Prisma.CredentialUncheckedUpdateWithoutSecurevaluesInput> } +export type CredentialCreateNestedOneWithoutSubCredentialsInput = { + create?: Prisma.XOR + connectOrCreate?: Prisma.CredentialCreateOrConnectWithoutSubCredentialsInput + connect?: Prisma.CredentialWhereUniqueInput +} + +export type CredentialCreateNestedManyWithoutSubCredentialOfInput = { + create?: Prisma.XOR | Prisma.CredentialCreateWithoutSubCredentialOfInput[] | Prisma.CredentialUncheckedCreateWithoutSubCredentialOfInput[] + connectOrCreate?: Prisma.CredentialCreateOrConnectWithoutSubCredentialOfInput | Prisma.CredentialCreateOrConnectWithoutSubCredentialOfInput[] + createMany?: Prisma.CredentialCreateManySubCredentialOfInputEnvelope + connect?: Prisma.CredentialWhereUniqueInput | Prisma.CredentialWhereUniqueInput[] +} + +export type CredentialUncheckedCreateNestedManyWithoutSubCredentialOfInput = { + create?: Prisma.XOR | Prisma.CredentialCreateWithoutSubCredentialOfInput[] | Prisma.CredentialUncheckedCreateWithoutSubCredentialOfInput[] + connectOrCreate?: Prisma.CredentialCreateOrConnectWithoutSubCredentialOfInput | Prisma.CredentialCreateOrConnectWithoutSubCredentialOfInput[] + createMany?: Prisma.CredentialCreateManySubCredentialOfInputEnvelope + connect?: Prisma.CredentialWhereUniqueInput | Prisma.CredentialWhereUniqueInput[] +} + +export type CredentialUpdateOneWithoutSubCredentialsNestedInput = { + create?: Prisma.XOR + connectOrCreate?: Prisma.CredentialCreateOrConnectWithoutSubCredentialsInput + upsert?: Prisma.CredentialUpsertWithoutSubCredentialsInput + disconnect?: Prisma.CredentialWhereInput | boolean + delete?: Prisma.CredentialWhereInput | boolean + connect?: Prisma.CredentialWhereUniqueInput + update?: Prisma.XOR, Prisma.CredentialUncheckedUpdateWithoutSubCredentialsInput> +} + +export type CredentialUpdateManyWithoutSubCredentialOfNestedInput = { + create?: Prisma.XOR | Prisma.CredentialCreateWithoutSubCredentialOfInput[] | Prisma.CredentialUncheckedCreateWithoutSubCredentialOfInput[] + connectOrCreate?: Prisma.CredentialCreateOrConnectWithoutSubCredentialOfInput | Prisma.CredentialCreateOrConnectWithoutSubCredentialOfInput[] + upsert?: Prisma.CredentialUpsertWithWhereUniqueWithoutSubCredentialOfInput | Prisma.CredentialUpsertWithWhereUniqueWithoutSubCredentialOfInput[] + createMany?: Prisma.CredentialCreateManySubCredentialOfInputEnvelope + set?: Prisma.CredentialWhereUniqueInput | Prisma.CredentialWhereUniqueInput[] + disconnect?: Prisma.CredentialWhereUniqueInput | Prisma.CredentialWhereUniqueInput[] + delete?: Prisma.CredentialWhereUniqueInput | Prisma.CredentialWhereUniqueInput[] + connect?: Prisma.CredentialWhereUniqueInput | Prisma.CredentialWhereUniqueInput[] + update?: Prisma.CredentialUpdateWithWhereUniqueWithoutSubCredentialOfInput | Prisma.CredentialUpdateWithWhereUniqueWithoutSubCredentialOfInput[] + updateMany?: Prisma.CredentialUpdateManyWithWhereWithoutSubCredentialOfInput | Prisma.CredentialUpdateManyWithWhereWithoutSubCredentialOfInput[] + deleteMany?: Prisma.CredentialScalarWhereInput | Prisma.CredentialScalarWhereInput[] +} + +export type CredentialUncheckedUpdateManyWithoutSubCredentialOfNestedInput = { + create?: Prisma.XOR | Prisma.CredentialCreateWithoutSubCredentialOfInput[] | Prisma.CredentialUncheckedCreateWithoutSubCredentialOfInput[] + connectOrCreate?: Prisma.CredentialCreateOrConnectWithoutSubCredentialOfInput | Prisma.CredentialCreateOrConnectWithoutSubCredentialOfInput[] + upsert?: Prisma.CredentialUpsertWithWhereUniqueWithoutSubCredentialOfInput | Prisma.CredentialUpsertWithWhereUniqueWithoutSubCredentialOfInput[] + createMany?: Prisma.CredentialCreateManySubCredentialOfInputEnvelope + set?: Prisma.CredentialWhereUniqueInput | Prisma.CredentialWhereUniqueInput[] + disconnect?: Prisma.CredentialWhereUniqueInput | Prisma.CredentialWhereUniqueInput[] + delete?: Prisma.CredentialWhereUniqueInput | Prisma.CredentialWhereUniqueInput[] + connect?: Prisma.CredentialWhereUniqueInput | Prisma.CredentialWhereUniqueInput[] + update?: Prisma.CredentialUpdateWithWhereUniqueWithoutSubCredentialOfInput | Prisma.CredentialUpdateWithWhereUniqueWithoutSubCredentialOfInput[] + updateMany?: Prisma.CredentialUpdateManyWithWhereWithoutSubCredentialOfInput | Prisma.CredentialUpdateManyWithWhereWithoutSubCredentialOfInput[] + deleteMany?: Prisma.CredentialScalarWhereInput | Prisma.CredentialScalarWhereInput[] +} + export type CredentialCreateWithoutCompanyInput = { id?: string name: string @@ -496,6 +590,8 @@ export type CredentialCreateWithoutCompanyInput = { fields: Prisma.JsonNullValueInput | runtime.InputJsonValue createdAt?: Date | string updatedAt?: Date | string + subCredentialOf?: Prisma.CredentialCreateNestedOneWithoutSubCredentialsInput + subCredentials?: Prisma.CredentialCreateNestedManyWithoutSubCredentialOfInput type: Prisma.CredentialTypeCreateNestedOneWithoutCredentialsInput securevalues?: Prisma.SecureValueCreateNestedManyWithoutCredentialInput } @@ -504,10 +600,12 @@ export type CredentialUncheckedCreateWithoutCompanyInput = { id?: string name: string notes?: string | null + subCredentialOfId?: string | null typeId: string fields: Prisma.JsonNullValueInput | runtime.InputJsonValue createdAt?: Date | string updatedAt?: Date | string + subCredentials?: Prisma.CredentialUncheckedCreateNestedManyWithoutSubCredentialOfInput securevalues?: Prisma.SecureValueUncheckedCreateNestedManyWithoutCredentialInput } @@ -544,6 +642,7 @@ export type CredentialScalarWhereInput = { id?: Prisma.StringFilter<"Credential"> | string name?: Prisma.StringFilter<"Credential"> | string notes?: Prisma.StringNullableFilter<"Credential"> | string | null + subCredentialOfId?: Prisma.StringNullableFilter<"Credential"> | string | null typeId?: Prisma.StringFilter<"Credential"> | string fields?: Prisma.JsonFilter<"Credential"> companyId?: Prisma.StringFilter<"Credential"> | string @@ -558,6 +657,8 @@ export type CredentialCreateWithoutTypeInput = { fields: Prisma.JsonNullValueInput | runtime.InputJsonValue createdAt?: Date | string updatedAt?: Date | string + subCredentialOf?: Prisma.CredentialCreateNestedOneWithoutSubCredentialsInput + subCredentials?: Prisma.CredentialCreateNestedManyWithoutSubCredentialOfInput company: Prisma.CompanyCreateNestedOneWithoutCredentialsInput securevalues?: Prisma.SecureValueCreateNestedManyWithoutCredentialInput } @@ -566,10 +667,12 @@ export type CredentialUncheckedCreateWithoutTypeInput = { id?: string name: string notes?: string | null + subCredentialOfId?: string | null fields: Prisma.JsonNullValueInput | runtime.InputJsonValue companyId: string createdAt?: Date | string updatedAt?: Date | string + subCredentials?: Prisma.CredentialUncheckedCreateNestedManyWithoutSubCredentialOfInput securevalues?: Prisma.SecureValueUncheckedCreateNestedManyWithoutCredentialInput } @@ -606,6 +709,8 @@ export type CredentialCreateWithoutSecurevaluesInput = { fields: Prisma.JsonNullValueInput | runtime.InputJsonValue createdAt?: Date | string updatedAt?: Date | string + subCredentialOf?: Prisma.CredentialCreateNestedOneWithoutSubCredentialsInput + subCredentials?: Prisma.CredentialCreateNestedManyWithoutSubCredentialOfInput type: Prisma.CredentialTypeCreateNestedOneWithoutCredentialsInput company: Prisma.CompanyCreateNestedOneWithoutCredentialsInput } @@ -614,11 +719,13 @@ export type CredentialUncheckedCreateWithoutSecurevaluesInput = { id?: string name: string notes?: string | null + subCredentialOfId?: string | null typeId: string fields: Prisma.JsonNullValueInput | runtime.InputJsonValue companyId: string createdAt?: Date | string updatedAt?: Date | string + subCredentials?: Prisma.CredentialUncheckedCreateNestedManyWithoutSubCredentialOfInput } export type CredentialCreateOrConnectWithoutSecurevaluesInput = { @@ -644,6 +751,8 @@ export type CredentialUpdateWithoutSecurevaluesInput = { fields?: Prisma.JsonNullValueInput | runtime.InputJsonValue createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string + subCredentialOf?: Prisma.CredentialUpdateOneWithoutSubCredentialsNestedInput + subCredentials?: Prisma.CredentialUpdateManyWithoutSubCredentialOfNestedInput type?: Prisma.CredentialTypeUpdateOneRequiredWithoutCredentialsNestedInput company?: Prisma.CompanyUpdateOneRequiredWithoutCredentialsNestedInput } @@ -652,17 +761,140 @@ export type CredentialUncheckedUpdateWithoutSecurevaluesInput = { id?: Prisma.StringFieldUpdateOperationsInput | string name?: Prisma.StringFieldUpdateOperationsInput | string notes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + subCredentialOfId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null typeId?: Prisma.StringFieldUpdateOperationsInput | string fields?: Prisma.JsonNullValueInput | runtime.InputJsonValue companyId?: Prisma.StringFieldUpdateOperationsInput | string createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string + subCredentials?: Prisma.CredentialUncheckedUpdateManyWithoutSubCredentialOfNestedInput +} + +export type CredentialCreateWithoutSubCredentialsInput = { + id?: string + name: string + notes?: string | null + fields: Prisma.JsonNullValueInput | runtime.InputJsonValue + createdAt?: Date | string + updatedAt?: Date | string + subCredentialOf?: Prisma.CredentialCreateNestedOneWithoutSubCredentialsInput + type: Prisma.CredentialTypeCreateNestedOneWithoutCredentialsInput + company: Prisma.CompanyCreateNestedOneWithoutCredentialsInput + securevalues?: Prisma.SecureValueCreateNestedManyWithoutCredentialInput +} + +export type CredentialUncheckedCreateWithoutSubCredentialsInput = { + id?: string + name: string + notes?: string | null + subCredentialOfId?: string | null + typeId: string + fields: Prisma.JsonNullValueInput | runtime.InputJsonValue + companyId: string + createdAt?: Date | string + updatedAt?: Date | string + securevalues?: Prisma.SecureValueUncheckedCreateNestedManyWithoutCredentialInput +} + +export type CredentialCreateOrConnectWithoutSubCredentialsInput = { + where: Prisma.CredentialWhereUniqueInput + create: Prisma.XOR +} + +export type CredentialCreateWithoutSubCredentialOfInput = { + id?: string + name: string + notes?: string | null + fields: Prisma.JsonNullValueInput | runtime.InputJsonValue + createdAt?: Date | string + updatedAt?: Date | string + subCredentials?: Prisma.CredentialCreateNestedManyWithoutSubCredentialOfInput + type: Prisma.CredentialTypeCreateNestedOneWithoutCredentialsInput + company: Prisma.CompanyCreateNestedOneWithoutCredentialsInput + securevalues?: Prisma.SecureValueCreateNestedManyWithoutCredentialInput +} + +export type CredentialUncheckedCreateWithoutSubCredentialOfInput = { + id?: string + name: string + notes?: string | null + typeId: string + fields: Prisma.JsonNullValueInput | runtime.InputJsonValue + companyId: string + createdAt?: Date | string + updatedAt?: Date | string + subCredentials?: Prisma.CredentialUncheckedCreateNestedManyWithoutSubCredentialOfInput + securevalues?: Prisma.SecureValueUncheckedCreateNestedManyWithoutCredentialInput +} + +export type CredentialCreateOrConnectWithoutSubCredentialOfInput = { + where: Prisma.CredentialWhereUniqueInput + create: Prisma.XOR +} + +export type CredentialCreateManySubCredentialOfInputEnvelope = { + data: Prisma.CredentialCreateManySubCredentialOfInput | Prisma.CredentialCreateManySubCredentialOfInput[] + skipDuplicates?: boolean +} + +export type CredentialUpsertWithoutSubCredentialsInput = { + update: Prisma.XOR + create: Prisma.XOR + where?: Prisma.CredentialWhereInput +} + +export type CredentialUpdateToOneWithWhereWithoutSubCredentialsInput = { + where?: Prisma.CredentialWhereInput + data: Prisma.XOR +} + +export type CredentialUpdateWithoutSubCredentialsInput = { + id?: Prisma.StringFieldUpdateOperationsInput | string + name?: Prisma.StringFieldUpdateOperationsInput | string + notes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + fields?: Prisma.JsonNullValueInput | runtime.InputJsonValue + createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string + updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string + subCredentialOf?: Prisma.CredentialUpdateOneWithoutSubCredentialsNestedInput + type?: Prisma.CredentialTypeUpdateOneRequiredWithoutCredentialsNestedInput + company?: Prisma.CompanyUpdateOneRequiredWithoutCredentialsNestedInput + securevalues?: Prisma.SecureValueUpdateManyWithoutCredentialNestedInput +} + +export type CredentialUncheckedUpdateWithoutSubCredentialsInput = { + id?: Prisma.StringFieldUpdateOperationsInput | string + name?: Prisma.StringFieldUpdateOperationsInput | string + notes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + subCredentialOfId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + typeId?: Prisma.StringFieldUpdateOperationsInput | string + fields?: Prisma.JsonNullValueInput | runtime.InputJsonValue + companyId?: Prisma.StringFieldUpdateOperationsInput | string + createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string + updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string + securevalues?: Prisma.SecureValueUncheckedUpdateManyWithoutCredentialNestedInput +} + +export type CredentialUpsertWithWhereUniqueWithoutSubCredentialOfInput = { + where: Prisma.CredentialWhereUniqueInput + update: Prisma.XOR + create: Prisma.XOR +} + +export type CredentialUpdateWithWhereUniqueWithoutSubCredentialOfInput = { + where: Prisma.CredentialWhereUniqueInput + data: Prisma.XOR +} + +export type CredentialUpdateManyWithWhereWithoutSubCredentialOfInput = { + where: Prisma.CredentialScalarWhereInput + data: Prisma.XOR } export type CredentialCreateManyCompanyInput = { id?: string name: string notes?: string | null + subCredentialOfId?: string | null typeId: string fields: Prisma.JsonNullValueInput | runtime.InputJsonValue createdAt?: Date | string @@ -676,6 +908,8 @@ export type CredentialUpdateWithoutCompanyInput = { fields?: Prisma.JsonNullValueInput | runtime.InputJsonValue createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string + subCredentialOf?: Prisma.CredentialUpdateOneWithoutSubCredentialsNestedInput + subCredentials?: Prisma.CredentialUpdateManyWithoutSubCredentialOfNestedInput type?: Prisma.CredentialTypeUpdateOneRequiredWithoutCredentialsNestedInput securevalues?: Prisma.SecureValueUpdateManyWithoutCredentialNestedInput } @@ -684,10 +918,12 @@ export type CredentialUncheckedUpdateWithoutCompanyInput = { id?: Prisma.StringFieldUpdateOperationsInput | string name?: Prisma.StringFieldUpdateOperationsInput | string notes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + subCredentialOfId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null typeId?: Prisma.StringFieldUpdateOperationsInput | string fields?: Prisma.JsonNullValueInput | runtime.InputJsonValue createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string + subCredentials?: Prisma.CredentialUncheckedUpdateManyWithoutSubCredentialOfNestedInput securevalues?: Prisma.SecureValueUncheckedUpdateManyWithoutCredentialNestedInput } @@ -695,6 +931,7 @@ export type CredentialUncheckedUpdateManyWithoutCompanyInput = { id?: Prisma.StringFieldUpdateOperationsInput | string name?: Prisma.StringFieldUpdateOperationsInput | string notes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + subCredentialOfId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null typeId?: Prisma.StringFieldUpdateOperationsInput | string fields?: Prisma.JsonNullValueInput | runtime.InputJsonValue createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string @@ -705,6 +942,7 @@ export type CredentialCreateManyTypeInput = { id?: string name: string notes?: string | null + subCredentialOfId?: string | null fields: Prisma.JsonNullValueInput | runtime.InputJsonValue companyId: string createdAt?: Date | string @@ -718,6 +956,8 @@ export type CredentialUpdateWithoutTypeInput = { fields?: Prisma.JsonNullValueInput | runtime.InputJsonValue createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string + subCredentialOf?: Prisma.CredentialUpdateOneWithoutSubCredentialsNestedInput + subCredentials?: Prisma.CredentialUpdateManyWithoutSubCredentialOfNestedInput company?: Prisma.CompanyUpdateOneRequiredWithoutCredentialsNestedInput securevalues?: Prisma.SecureValueUpdateManyWithoutCredentialNestedInput } @@ -726,10 +966,12 @@ export type CredentialUncheckedUpdateWithoutTypeInput = { id?: Prisma.StringFieldUpdateOperationsInput | string name?: Prisma.StringFieldUpdateOperationsInput | string notes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + subCredentialOfId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null fields?: Prisma.JsonNullValueInput | runtime.InputJsonValue companyId?: Prisma.StringFieldUpdateOperationsInput | string createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string + subCredentials?: Prisma.CredentialUncheckedUpdateManyWithoutSubCredentialOfNestedInput securevalues?: Prisma.SecureValueUncheckedUpdateManyWithoutCredentialNestedInput } @@ -737,6 +979,55 @@ export type CredentialUncheckedUpdateManyWithoutTypeInput = { id?: Prisma.StringFieldUpdateOperationsInput | string name?: Prisma.StringFieldUpdateOperationsInput | string notes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + subCredentialOfId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + fields?: Prisma.JsonNullValueInput | runtime.InputJsonValue + companyId?: Prisma.StringFieldUpdateOperationsInput | string + createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string + updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string +} + +export type CredentialCreateManySubCredentialOfInput = { + id?: string + name: string + notes?: string | null + typeId: string + fields: Prisma.JsonNullValueInput | runtime.InputJsonValue + companyId: string + createdAt?: Date | string + updatedAt?: Date | string +} + +export type CredentialUpdateWithoutSubCredentialOfInput = { + id?: Prisma.StringFieldUpdateOperationsInput | string + name?: Prisma.StringFieldUpdateOperationsInput | string + notes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + fields?: Prisma.JsonNullValueInput | runtime.InputJsonValue + createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string + updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string + subCredentials?: Prisma.CredentialUpdateManyWithoutSubCredentialOfNestedInput + type?: Prisma.CredentialTypeUpdateOneRequiredWithoutCredentialsNestedInput + company?: Prisma.CompanyUpdateOneRequiredWithoutCredentialsNestedInput + securevalues?: Prisma.SecureValueUpdateManyWithoutCredentialNestedInput +} + +export type CredentialUncheckedUpdateWithoutSubCredentialOfInput = { + id?: Prisma.StringFieldUpdateOperationsInput | string + name?: Prisma.StringFieldUpdateOperationsInput | string + notes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + typeId?: Prisma.StringFieldUpdateOperationsInput | string + fields?: Prisma.JsonNullValueInput | runtime.InputJsonValue + companyId?: Prisma.StringFieldUpdateOperationsInput | string + createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string + updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string + subCredentials?: Prisma.CredentialUncheckedUpdateManyWithoutSubCredentialOfNestedInput + securevalues?: Prisma.SecureValueUncheckedUpdateManyWithoutCredentialNestedInput +} + +export type CredentialUncheckedUpdateManyWithoutSubCredentialOfInput = { + id?: Prisma.StringFieldUpdateOperationsInput | string + name?: Prisma.StringFieldUpdateOperationsInput | string + notes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + typeId?: Prisma.StringFieldUpdateOperationsInput | string fields?: Prisma.JsonNullValueInput | runtime.InputJsonValue companyId?: Prisma.StringFieldUpdateOperationsInput | string createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string @@ -749,10 +1040,12 @@ export type CredentialUncheckedUpdateManyWithoutTypeInput = { */ export type CredentialCountOutputType = { + subCredentials: number securevalues: number } export type CredentialCountOutputTypeSelect = { + subCredentials?: boolean | CredentialCountOutputTypeCountSubCredentialsArgs securevalues?: boolean | CredentialCountOutputTypeCountSecurevaluesArgs } @@ -766,6 +1059,13 @@ export type CredentialCountOutputTypeDefaultArgs | null } +/** + * CredentialCountOutputType without action + */ +export type CredentialCountOutputTypeCountSubCredentialsArgs = { + where?: Prisma.CredentialWhereInput +} + /** * CredentialCountOutputType without action */ @@ -778,11 +1078,14 @@ export type CredentialSelect + subCredentials?: boolean | Prisma.Credential$subCredentialsArgs type?: boolean | Prisma.CredentialTypeDefaultArgs company?: boolean | Prisma.CompanyDefaultArgs securevalues?: boolean | Prisma.Credential$securevaluesArgs @@ -793,11 +1096,13 @@ export type CredentialSelectCreateManyAndReturn type?: boolean | Prisma.CredentialTypeDefaultArgs company?: boolean | Prisma.CompanyDefaultArgs }, ExtArgs["result"]["credential"]> @@ -806,11 +1111,13 @@ export type CredentialSelectUpdateManyAndReturn type?: boolean | Prisma.CredentialTypeDefaultArgs company?: boolean | Prisma.CompanyDefaultArgs }, ExtArgs["result"]["credential"]> @@ -819,6 +1126,7 @@ export type CredentialSelectScalar = { id?: boolean name?: boolean notes?: boolean + subCredentialOfId?: boolean typeId?: boolean fields?: boolean companyId?: boolean @@ -826,18 +1134,22 @@ export type CredentialSelectScalar = { updatedAt?: boolean } -export type CredentialOmit = runtime.Types.Extensions.GetOmit<"id" | "name" | "notes" | "typeId" | "fields" | "companyId" | "createdAt" | "updatedAt", ExtArgs["result"]["credential"]> +export type CredentialOmit = runtime.Types.Extensions.GetOmit<"id" | "name" | "notes" | "subCredentialOfId" | "typeId" | "fields" | "companyId" | "createdAt" | "updatedAt", ExtArgs["result"]["credential"]> export type CredentialInclude = { + subCredentialOf?: boolean | Prisma.Credential$subCredentialOfArgs + subCredentials?: boolean | Prisma.Credential$subCredentialsArgs type?: boolean | Prisma.CredentialTypeDefaultArgs company?: boolean | Prisma.CompanyDefaultArgs securevalues?: boolean | Prisma.Credential$securevaluesArgs _count?: boolean | Prisma.CredentialCountOutputTypeDefaultArgs } export type CredentialIncludeCreateManyAndReturn = { + subCredentialOf?: boolean | Prisma.Credential$subCredentialOfArgs type?: boolean | Prisma.CredentialTypeDefaultArgs company?: boolean | Prisma.CompanyDefaultArgs } export type CredentialIncludeUpdateManyAndReturn = { + subCredentialOf?: boolean | Prisma.Credential$subCredentialOfArgs type?: boolean | Prisma.CredentialTypeDefaultArgs company?: boolean | Prisma.CompanyDefaultArgs } @@ -845,6 +1157,8 @@ export type CredentialIncludeUpdateManyAndReturn = { name: "Credential" objects: { + subCredentialOf: Prisma.$CredentialPayload | null + subCredentials: Prisma.$CredentialPayload[] type: Prisma.$CredentialTypePayload company: Prisma.$CompanyPayload securevalues: Prisma.$SecureValuePayload[] @@ -853,6 +1167,7 @@ export type $CredentialPayload extends Prisma.PrismaPromise { readonly [Symbol.toStringTag]: "PrismaPromise" + subCredentialOf = {}>(args?: Prisma.Subset>): Prisma.Prisma__CredentialClient, T, "findUniqueOrThrow", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions> + subCredentials = {}>(args?: Prisma.Subset>): Prisma.PrismaPromise, T, "findMany", GlobalOmitOptions> | Null> type = {}>(args?: Prisma.Subset>): Prisma.Prisma__CredentialTypeClient, T, "findUniqueOrThrow", GlobalOmitOptions> | Null, Null, ExtArgs, GlobalOmitOptions> company = {}>(args?: Prisma.Subset>): Prisma.Prisma__CompanyClient, T, "findUniqueOrThrow", GlobalOmitOptions> | Null, Null, ExtArgs, GlobalOmitOptions> securevalues = {}>(args?: Prisma.Subset>): Prisma.PrismaPromise, T, "findMany", GlobalOmitOptions> | Null> @@ -1287,6 +1604,7 @@ export interface CredentialFieldRefs { readonly id: Prisma.FieldRef<"Credential", 'String'> readonly name: Prisma.FieldRef<"Credential", 'String'> readonly notes: Prisma.FieldRef<"Credential", 'String'> + readonly subCredentialOfId: Prisma.FieldRef<"Credential", 'String'> readonly typeId: Prisma.FieldRef<"Credential", 'String'> readonly fields: Prisma.FieldRef<"Credential", 'Json'> readonly companyId: Prisma.FieldRef<"Credential", 'String'> @@ -1687,6 +2005,49 @@ export type CredentialDeleteManyArgs = { + /** + * Select specific fields to fetch from the Credential + */ + select?: Prisma.CredentialSelect | null + /** + * Omit specific fields from the Credential + */ + omit?: Prisma.CredentialOmit | null + /** + * Choose, which related nodes to fetch as well + */ + include?: Prisma.CredentialInclude | null + where?: Prisma.CredentialWhereInput +} + +/** + * Credential.subCredentials + */ +export type Credential$subCredentialsArgs = { + /** + * Select specific fields to fetch from the Credential + */ + select?: Prisma.CredentialSelect | null + /** + * Omit specific fields from the Credential + */ + omit?: Prisma.CredentialOmit | null + /** + * Choose, which related nodes to fetch as well + */ + include?: Prisma.CredentialInclude | null + where?: Prisma.CredentialWhereInput + orderBy?: Prisma.CredentialOrderByWithRelationInput | Prisma.CredentialOrderByWithRelationInput[] + cursor?: Prisma.CredentialWhereUniqueInput + take?: number + skip?: number + distinct?: Prisma.CredentialScalarFieldEnum | Prisma.CredentialScalarFieldEnum[] +} + /** * Credential.securevalues */ diff --git a/generated/prisma/models/UnifiSite.ts b/generated/prisma/models/UnifiSite.ts new file mode 100644 index 0000000..29e71f1 --- /dev/null +++ b/generated/prisma/models/UnifiSite.ts @@ -0,0 +1,1394 @@ + +/* !!! This is code generated by Prisma. Do not edit directly. !!! */ +/* eslint-disable */ +// biome-ignore-all lint: generated file +// @ts-nocheck +/* + * This file exports the `UnifiSite` model and its related types. + * + * 🟢 You can import this file directly. + */ +import type * as runtime from "@prisma/client/runtime/client" +import type * as $Enums from "../enums.ts" +import type * as Prisma from "../internal/prismaNamespace.ts" + +/** + * Model UnifiSite + * + */ +export type UnifiSiteModel = runtime.Types.Result.DefaultSelection + +export type AggregateUnifiSite = { + _count: UnifiSiteCountAggregateOutputType | null + _min: UnifiSiteMinAggregateOutputType | null + _max: UnifiSiteMaxAggregateOutputType | null +} + +export type UnifiSiteMinAggregateOutputType = { + id: string | null + name: string | null + siteId: string | null + companyId: string | null + createdAt: Date | null + updatedAt: Date | null +} + +export type UnifiSiteMaxAggregateOutputType = { + id: string | null + name: string | null + siteId: string | null + companyId: string | null + createdAt: Date | null + updatedAt: Date | null +} + +export type UnifiSiteCountAggregateOutputType = { + id: number + name: number + siteId: number + companyId: number + createdAt: number + updatedAt: number + _all: number +} + + +export type UnifiSiteMinAggregateInputType = { + id?: true + name?: true + siteId?: true + companyId?: true + createdAt?: true + updatedAt?: true +} + +export type UnifiSiteMaxAggregateInputType = { + id?: true + name?: true + siteId?: true + companyId?: true + createdAt?: true + updatedAt?: true +} + +export type UnifiSiteCountAggregateInputType = { + id?: true + name?: true + siteId?: true + companyId?: true + createdAt?: true + updatedAt?: true + _all?: true +} + +export type UnifiSiteAggregateArgs = { + /** + * Filter which UnifiSite to aggregate. + */ + where?: Prisma.UnifiSiteWhereInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/sorting Sorting Docs} + * + * Determine the order of UnifiSites to fetch. + */ + orderBy?: Prisma.UnifiSiteOrderByWithRelationInput | Prisma.UnifiSiteOrderByWithRelationInput[] + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination#cursor-based-pagination Cursor Docs} + * + * Sets the start position + */ + cursor?: Prisma.UnifiSiteWhereUniqueInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Take `±n` UnifiSites from the position of the cursor. + */ + take?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Skip the first `n` UnifiSites. + */ + skip?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/aggregations Aggregation Docs} + * + * Count returned UnifiSites + **/ + _count?: true | UnifiSiteCountAggregateInputType + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/aggregations Aggregation Docs} + * + * Select which fields to find the minimum value + **/ + _min?: UnifiSiteMinAggregateInputType + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/aggregations Aggregation Docs} + * + * Select which fields to find the maximum value + **/ + _max?: UnifiSiteMaxAggregateInputType +} + +export type GetUnifiSiteAggregateType = { + [P in keyof T & keyof AggregateUnifiSite]: P extends '_count' | 'count' + ? T[P] extends true + ? number + : Prisma.GetScalarType + : Prisma.GetScalarType +} + + + + +export type UnifiSiteGroupByArgs = { + where?: Prisma.UnifiSiteWhereInput + orderBy?: Prisma.UnifiSiteOrderByWithAggregationInput | Prisma.UnifiSiteOrderByWithAggregationInput[] + by: Prisma.UnifiSiteScalarFieldEnum[] | Prisma.UnifiSiteScalarFieldEnum + having?: Prisma.UnifiSiteScalarWhereWithAggregatesInput + take?: number + skip?: number + _count?: UnifiSiteCountAggregateInputType | true + _min?: UnifiSiteMinAggregateInputType + _max?: UnifiSiteMaxAggregateInputType +} + +export type UnifiSiteGroupByOutputType = { + id: string + name: string + siteId: string + companyId: string | null + createdAt: Date + updatedAt: Date + _count: UnifiSiteCountAggregateOutputType | null + _min: UnifiSiteMinAggregateOutputType | null + _max: UnifiSiteMaxAggregateOutputType | null +} + +type GetUnifiSiteGroupByPayload = Prisma.PrismaPromise< + Array< + Prisma.PickEnumerable & + { + [P in ((keyof T) & (keyof UnifiSiteGroupByOutputType))]: P extends '_count' + ? T[P] extends boolean + ? number + : Prisma.GetScalarType + : Prisma.GetScalarType + } + > + > + + + +export type UnifiSiteWhereInput = { + AND?: Prisma.UnifiSiteWhereInput | Prisma.UnifiSiteWhereInput[] + OR?: Prisma.UnifiSiteWhereInput[] + NOT?: Prisma.UnifiSiteWhereInput | Prisma.UnifiSiteWhereInput[] + id?: Prisma.StringFilter<"UnifiSite"> | string + name?: Prisma.StringFilter<"UnifiSite"> | string + siteId?: Prisma.StringFilter<"UnifiSite"> | string + companyId?: Prisma.StringNullableFilter<"UnifiSite"> | string | null + createdAt?: Prisma.DateTimeFilter<"UnifiSite"> | Date | string + updatedAt?: Prisma.DateTimeFilter<"UnifiSite"> | Date | string + company?: Prisma.XOR | null +} + +export type UnifiSiteOrderByWithRelationInput = { + id?: Prisma.SortOrder + name?: Prisma.SortOrder + siteId?: Prisma.SortOrder + companyId?: Prisma.SortOrderInput | Prisma.SortOrder + createdAt?: Prisma.SortOrder + updatedAt?: Prisma.SortOrder + company?: Prisma.CompanyOrderByWithRelationInput +} + +export type UnifiSiteWhereUniqueInput = Prisma.AtLeast<{ + id?: string + siteId?: string + AND?: Prisma.UnifiSiteWhereInput | Prisma.UnifiSiteWhereInput[] + OR?: Prisma.UnifiSiteWhereInput[] + NOT?: Prisma.UnifiSiteWhereInput | Prisma.UnifiSiteWhereInput[] + name?: Prisma.StringFilter<"UnifiSite"> | string + companyId?: Prisma.StringNullableFilter<"UnifiSite"> | string | null + createdAt?: Prisma.DateTimeFilter<"UnifiSite"> | Date | string + updatedAt?: Prisma.DateTimeFilter<"UnifiSite"> | Date | string + company?: Prisma.XOR | null +}, "id" | "siteId"> + +export type UnifiSiteOrderByWithAggregationInput = { + id?: Prisma.SortOrder + name?: Prisma.SortOrder + siteId?: Prisma.SortOrder + companyId?: Prisma.SortOrderInput | Prisma.SortOrder + createdAt?: Prisma.SortOrder + updatedAt?: Prisma.SortOrder + _count?: Prisma.UnifiSiteCountOrderByAggregateInput + _max?: Prisma.UnifiSiteMaxOrderByAggregateInput + _min?: Prisma.UnifiSiteMinOrderByAggregateInput +} + +export type UnifiSiteScalarWhereWithAggregatesInput = { + AND?: Prisma.UnifiSiteScalarWhereWithAggregatesInput | Prisma.UnifiSiteScalarWhereWithAggregatesInput[] + OR?: Prisma.UnifiSiteScalarWhereWithAggregatesInput[] + NOT?: Prisma.UnifiSiteScalarWhereWithAggregatesInput | Prisma.UnifiSiteScalarWhereWithAggregatesInput[] + id?: Prisma.StringWithAggregatesFilter<"UnifiSite"> | string + name?: Prisma.StringWithAggregatesFilter<"UnifiSite"> | string + siteId?: Prisma.StringWithAggregatesFilter<"UnifiSite"> | string + companyId?: Prisma.StringNullableWithAggregatesFilter<"UnifiSite"> | string | null + createdAt?: Prisma.DateTimeWithAggregatesFilter<"UnifiSite"> | Date | string + updatedAt?: Prisma.DateTimeWithAggregatesFilter<"UnifiSite"> | Date | string +} + +export type UnifiSiteCreateInput = { + id?: string + name: string + siteId: string + createdAt?: Date | string + updatedAt?: Date | string + company?: Prisma.CompanyCreateNestedOneWithoutUnifiSitesInput +} + +export type UnifiSiteUncheckedCreateInput = { + id?: string + name: string + siteId: string + companyId?: string | null + createdAt?: Date | string + updatedAt?: Date | string +} + +export type UnifiSiteUpdateInput = { + id?: Prisma.StringFieldUpdateOperationsInput | string + name?: Prisma.StringFieldUpdateOperationsInput | string + siteId?: Prisma.StringFieldUpdateOperationsInput | string + createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string + updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string + company?: Prisma.CompanyUpdateOneWithoutUnifiSitesNestedInput +} + +export type UnifiSiteUncheckedUpdateInput = { + id?: Prisma.StringFieldUpdateOperationsInput | string + name?: Prisma.StringFieldUpdateOperationsInput | string + siteId?: Prisma.StringFieldUpdateOperationsInput | string + companyId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string + updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string +} + +export type UnifiSiteCreateManyInput = { + id?: string + name: string + siteId: string + companyId?: string | null + createdAt?: Date | string + updatedAt?: Date | string +} + +export type UnifiSiteUpdateManyMutationInput = { + id?: Prisma.StringFieldUpdateOperationsInput | string + name?: Prisma.StringFieldUpdateOperationsInput | string + siteId?: Prisma.StringFieldUpdateOperationsInput | string + createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string + updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string +} + +export type UnifiSiteUncheckedUpdateManyInput = { + id?: Prisma.StringFieldUpdateOperationsInput | string + name?: Prisma.StringFieldUpdateOperationsInput | string + siteId?: Prisma.StringFieldUpdateOperationsInput | string + companyId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string + updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string +} + +export type UnifiSiteCountOrderByAggregateInput = { + id?: Prisma.SortOrder + name?: Prisma.SortOrder + siteId?: Prisma.SortOrder + companyId?: Prisma.SortOrder + createdAt?: Prisma.SortOrder + updatedAt?: Prisma.SortOrder +} + +export type UnifiSiteMaxOrderByAggregateInput = { + id?: Prisma.SortOrder + name?: Prisma.SortOrder + siteId?: Prisma.SortOrder + companyId?: Prisma.SortOrder + createdAt?: Prisma.SortOrder + updatedAt?: Prisma.SortOrder +} + +export type UnifiSiteMinOrderByAggregateInput = { + id?: Prisma.SortOrder + name?: Prisma.SortOrder + siteId?: Prisma.SortOrder + companyId?: Prisma.SortOrder + createdAt?: Prisma.SortOrder + updatedAt?: Prisma.SortOrder +} + +export type UnifiSiteListRelationFilter = { + every?: Prisma.UnifiSiteWhereInput + some?: Prisma.UnifiSiteWhereInput + none?: Prisma.UnifiSiteWhereInput +} + +export type UnifiSiteOrderByRelationAggregateInput = { + _count?: Prisma.SortOrder +} + +export type UnifiSiteCreateNestedManyWithoutCompanyInput = { + create?: Prisma.XOR | Prisma.UnifiSiteCreateWithoutCompanyInput[] | Prisma.UnifiSiteUncheckedCreateWithoutCompanyInput[] + connectOrCreate?: Prisma.UnifiSiteCreateOrConnectWithoutCompanyInput | Prisma.UnifiSiteCreateOrConnectWithoutCompanyInput[] + createMany?: Prisma.UnifiSiteCreateManyCompanyInputEnvelope + connect?: Prisma.UnifiSiteWhereUniqueInput | Prisma.UnifiSiteWhereUniqueInput[] +} + +export type UnifiSiteUncheckedCreateNestedManyWithoutCompanyInput = { + create?: Prisma.XOR | Prisma.UnifiSiteCreateWithoutCompanyInput[] | Prisma.UnifiSiteUncheckedCreateWithoutCompanyInput[] + connectOrCreate?: Prisma.UnifiSiteCreateOrConnectWithoutCompanyInput | Prisma.UnifiSiteCreateOrConnectWithoutCompanyInput[] + createMany?: Prisma.UnifiSiteCreateManyCompanyInputEnvelope + connect?: Prisma.UnifiSiteWhereUniqueInput | Prisma.UnifiSiteWhereUniqueInput[] +} + +export type UnifiSiteUpdateManyWithoutCompanyNestedInput = { + create?: Prisma.XOR | Prisma.UnifiSiteCreateWithoutCompanyInput[] | Prisma.UnifiSiteUncheckedCreateWithoutCompanyInput[] + connectOrCreate?: Prisma.UnifiSiteCreateOrConnectWithoutCompanyInput | Prisma.UnifiSiteCreateOrConnectWithoutCompanyInput[] + upsert?: Prisma.UnifiSiteUpsertWithWhereUniqueWithoutCompanyInput | Prisma.UnifiSiteUpsertWithWhereUniqueWithoutCompanyInput[] + createMany?: Prisma.UnifiSiteCreateManyCompanyInputEnvelope + set?: Prisma.UnifiSiteWhereUniqueInput | Prisma.UnifiSiteWhereUniqueInput[] + disconnect?: Prisma.UnifiSiteWhereUniqueInput | Prisma.UnifiSiteWhereUniqueInput[] + delete?: Prisma.UnifiSiteWhereUniqueInput | Prisma.UnifiSiteWhereUniqueInput[] + connect?: Prisma.UnifiSiteWhereUniqueInput | Prisma.UnifiSiteWhereUniqueInput[] + update?: Prisma.UnifiSiteUpdateWithWhereUniqueWithoutCompanyInput | Prisma.UnifiSiteUpdateWithWhereUniqueWithoutCompanyInput[] + updateMany?: Prisma.UnifiSiteUpdateManyWithWhereWithoutCompanyInput | Prisma.UnifiSiteUpdateManyWithWhereWithoutCompanyInput[] + deleteMany?: Prisma.UnifiSiteScalarWhereInput | Prisma.UnifiSiteScalarWhereInput[] +} + +export type UnifiSiteUncheckedUpdateManyWithoutCompanyNestedInput = { + create?: Prisma.XOR | Prisma.UnifiSiteCreateWithoutCompanyInput[] | Prisma.UnifiSiteUncheckedCreateWithoutCompanyInput[] + connectOrCreate?: Prisma.UnifiSiteCreateOrConnectWithoutCompanyInput | Prisma.UnifiSiteCreateOrConnectWithoutCompanyInput[] + upsert?: Prisma.UnifiSiteUpsertWithWhereUniqueWithoutCompanyInput | Prisma.UnifiSiteUpsertWithWhereUniqueWithoutCompanyInput[] + createMany?: Prisma.UnifiSiteCreateManyCompanyInputEnvelope + set?: Prisma.UnifiSiteWhereUniqueInput | Prisma.UnifiSiteWhereUniqueInput[] + disconnect?: Prisma.UnifiSiteWhereUniqueInput | Prisma.UnifiSiteWhereUniqueInput[] + delete?: Prisma.UnifiSiteWhereUniqueInput | Prisma.UnifiSiteWhereUniqueInput[] + connect?: Prisma.UnifiSiteWhereUniqueInput | Prisma.UnifiSiteWhereUniqueInput[] + update?: Prisma.UnifiSiteUpdateWithWhereUniqueWithoutCompanyInput | Prisma.UnifiSiteUpdateWithWhereUniqueWithoutCompanyInput[] + updateMany?: Prisma.UnifiSiteUpdateManyWithWhereWithoutCompanyInput | Prisma.UnifiSiteUpdateManyWithWhereWithoutCompanyInput[] + deleteMany?: Prisma.UnifiSiteScalarWhereInput | Prisma.UnifiSiteScalarWhereInput[] +} + +export type UnifiSiteCreateWithoutCompanyInput = { + id?: string + name: string + siteId: string + createdAt?: Date | string + updatedAt?: Date | string +} + +export type UnifiSiteUncheckedCreateWithoutCompanyInput = { + id?: string + name: string + siteId: string + createdAt?: Date | string + updatedAt?: Date | string +} + +export type UnifiSiteCreateOrConnectWithoutCompanyInput = { + where: Prisma.UnifiSiteWhereUniqueInput + create: Prisma.XOR +} + +export type UnifiSiteCreateManyCompanyInputEnvelope = { + data: Prisma.UnifiSiteCreateManyCompanyInput | Prisma.UnifiSiteCreateManyCompanyInput[] + skipDuplicates?: boolean +} + +export type UnifiSiteUpsertWithWhereUniqueWithoutCompanyInput = { + where: Prisma.UnifiSiteWhereUniqueInput + update: Prisma.XOR + create: Prisma.XOR +} + +export type UnifiSiteUpdateWithWhereUniqueWithoutCompanyInput = { + where: Prisma.UnifiSiteWhereUniqueInput + data: Prisma.XOR +} + +export type UnifiSiteUpdateManyWithWhereWithoutCompanyInput = { + where: Prisma.UnifiSiteScalarWhereInput + data: Prisma.XOR +} + +export type UnifiSiteScalarWhereInput = { + AND?: Prisma.UnifiSiteScalarWhereInput | Prisma.UnifiSiteScalarWhereInput[] + OR?: Prisma.UnifiSiteScalarWhereInput[] + NOT?: Prisma.UnifiSiteScalarWhereInput | Prisma.UnifiSiteScalarWhereInput[] + id?: Prisma.StringFilter<"UnifiSite"> | string + name?: Prisma.StringFilter<"UnifiSite"> | string + siteId?: Prisma.StringFilter<"UnifiSite"> | string + companyId?: Prisma.StringNullableFilter<"UnifiSite"> | string | null + createdAt?: Prisma.DateTimeFilter<"UnifiSite"> | Date | string + updatedAt?: Prisma.DateTimeFilter<"UnifiSite"> | Date | string +} + +export type UnifiSiteCreateManyCompanyInput = { + id?: string + name: string + siteId: string + createdAt?: Date | string + updatedAt?: Date | string +} + +export type UnifiSiteUpdateWithoutCompanyInput = { + id?: Prisma.StringFieldUpdateOperationsInput | string + name?: Prisma.StringFieldUpdateOperationsInput | string + siteId?: Prisma.StringFieldUpdateOperationsInput | string + createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string + updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string +} + +export type UnifiSiteUncheckedUpdateWithoutCompanyInput = { + id?: Prisma.StringFieldUpdateOperationsInput | string + name?: Prisma.StringFieldUpdateOperationsInput | string + siteId?: Prisma.StringFieldUpdateOperationsInput | string + createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string + updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string +} + +export type UnifiSiteUncheckedUpdateManyWithoutCompanyInput = { + id?: Prisma.StringFieldUpdateOperationsInput | string + name?: Prisma.StringFieldUpdateOperationsInput | string + siteId?: Prisma.StringFieldUpdateOperationsInput | string + createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string + updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string +} + + + +export type UnifiSiteSelect = runtime.Types.Extensions.GetSelect<{ + id?: boolean + name?: boolean + siteId?: boolean + companyId?: boolean + createdAt?: boolean + updatedAt?: boolean + company?: boolean | Prisma.UnifiSite$companyArgs +}, ExtArgs["result"]["unifiSite"]> + +export type UnifiSiteSelectCreateManyAndReturn = runtime.Types.Extensions.GetSelect<{ + id?: boolean + name?: boolean + siteId?: boolean + companyId?: boolean + createdAt?: boolean + updatedAt?: boolean + company?: boolean | Prisma.UnifiSite$companyArgs +}, ExtArgs["result"]["unifiSite"]> + +export type UnifiSiteSelectUpdateManyAndReturn = runtime.Types.Extensions.GetSelect<{ + id?: boolean + name?: boolean + siteId?: boolean + companyId?: boolean + createdAt?: boolean + updatedAt?: boolean + company?: boolean | Prisma.UnifiSite$companyArgs +}, ExtArgs["result"]["unifiSite"]> + +export type UnifiSiteSelectScalar = { + id?: boolean + name?: boolean + siteId?: boolean + companyId?: boolean + createdAt?: boolean + updatedAt?: boolean +} + +export type UnifiSiteOmit = runtime.Types.Extensions.GetOmit<"id" | "name" | "siteId" | "companyId" | "createdAt" | "updatedAt", ExtArgs["result"]["unifiSite"]> +export type UnifiSiteInclude = { + company?: boolean | Prisma.UnifiSite$companyArgs +} +export type UnifiSiteIncludeCreateManyAndReturn = { + company?: boolean | Prisma.UnifiSite$companyArgs +} +export type UnifiSiteIncludeUpdateManyAndReturn = { + company?: boolean | Prisma.UnifiSite$companyArgs +} + +export type $UnifiSitePayload = { + name: "UnifiSite" + objects: { + company: Prisma.$CompanyPayload | null + } + scalars: runtime.Types.Extensions.GetPayloadResult<{ + id: string + name: string + siteId: string + companyId: string | null + createdAt: Date + updatedAt: Date + }, ExtArgs["result"]["unifiSite"]> + composites: {} +} + +export type UnifiSiteGetPayload = runtime.Types.Result.GetResult + +export type UnifiSiteCountArgs = + Omit & { + select?: UnifiSiteCountAggregateInputType | true + } + +export interface UnifiSiteDelegate { + [K: symbol]: { types: Prisma.TypeMap['model']['UnifiSite'], meta: { name: 'UnifiSite' } } + /** + * Find zero or one UnifiSite that matches the filter. + * @param {UnifiSiteFindUniqueArgs} args - Arguments to find a UnifiSite + * @example + * // Get one UnifiSite + * const unifiSite = await prisma.unifiSite.findUnique({ + * where: { + * // ... provide filter here + * } + * }) + */ + findUnique(args: Prisma.SelectSubset>): Prisma.Prisma__UnifiSiteClient, T, "findUnique", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions> + + /** + * Find one UnifiSite that matches the filter or throw an error with `error.code='P2025'` + * if no matches were found. + * @param {UnifiSiteFindUniqueOrThrowArgs} args - Arguments to find a UnifiSite + * @example + * // Get one UnifiSite + * const unifiSite = await prisma.unifiSite.findUniqueOrThrow({ + * where: { + * // ... provide filter here + * } + * }) + */ + findUniqueOrThrow(args: Prisma.SelectSubset>): Prisma.Prisma__UnifiSiteClient, T, "findUniqueOrThrow", GlobalOmitOptions>, never, ExtArgs, GlobalOmitOptions> + + /** + * Find the first UnifiSite that matches the filter. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {UnifiSiteFindFirstArgs} args - Arguments to find a UnifiSite + * @example + * // Get one UnifiSite + * const unifiSite = await prisma.unifiSite.findFirst({ + * where: { + * // ... provide filter here + * } + * }) + */ + findFirst(args?: Prisma.SelectSubset>): Prisma.Prisma__UnifiSiteClient, T, "findFirst", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions> + + /** + * Find the first UnifiSite that matches the filter or + * throw `PrismaKnownClientError` with `P2025` code if no matches were found. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {UnifiSiteFindFirstOrThrowArgs} args - Arguments to find a UnifiSite + * @example + * // Get one UnifiSite + * const unifiSite = await prisma.unifiSite.findFirstOrThrow({ + * where: { + * // ... provide filter here + * } + * }) + */ + findFirstOrThrow(args?: Prisma.SelectSubset>): Prisma.Prisma__UnifiSiteClient, T, "findFirstOrThrow", GlobalOmitOptions>, never, ExtArgs, GlobalOmitOptions> + + /** + * Find zero or more UnifiSites that matches the filter. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {UnifiSiteFindManyArgs} args - Arguments to filter and select certain fields only. + * @example + * // Get all UnifiSites + * const unifiSites = await prisma.unifiSite.findMany() + * + * // Get first 10 UnifiSites + * const unifiSites = await prisma.unifiSite.findMany({ take: 10 }) + * + * // Only select the `id` + * const unifiSiteWithIdOnly = await prisma.unifiSite.findMany({ select: { id: true } }) + * + */ + findMany(args?: Prisma.SelectSubset>): Prisma.PrismaPromise, T, "findMany", GlobalOmitOptions>> + + /** + * Create a UnifiSite. + * @param {UnifiSiteCreateArgs} args - Arguments to create a UnifiSite. + * @example + * // Create one UnifiSite + * const UnifiSite = await prisma.unifiSite.create({ + * data: { + * // ... data to create a UnifiSite + * } + * }) + * + */ + create(args: Prisma.SelectSubset>): Prisma.Prisma__UnifiSiteClient, T, "create", GlobalOmitOptions>, never, ExtArgs, GlobalOmitOptions> + + /** + * Create many UnifiSites. + * @param {UnifiSiteCreateManyArgs} args - Arguments to create many UnifiSites. + * @example + * // Create many UnifiSites + * const unifiSite = await prisma.unifiSite.createMany({ + * data: [ + * // ... provide data here + * ] + * }) + * + */ + createMany(args?: Prisma.SelectSubset>): Prisma.PrismaPromise + + /** + * Create many UnifiSites and returns the data saved in the database. + * @param {UnifiSiteCreateManyAndReturnArgs} args - Arguments to create many UnifiSites. + * @example + * // Create many UnifiSites + * const unifiSite = await prisma.unifiSite.createManyAndReturn({ + * data: [ + * // ... provide data here + * ] + * }) + * + * // Create many UnifiSites and only return the `id` + * const unifiSiteWithIdOnly = await prisma.unifiSite.createManyAndReturn({ + * select: { id: true }, + * data: [ + * // ... provide data here + * ] + * }) + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * + */ + createManyAndReturn(args?: Prisma.SelectSubset>): Prisma.PrismaPromise, T, "createManyAndReturn", GlobalOmitOptions>> + + /** + * Delete a UnifiSite. + * @param {UnifiSiteDeleteArgs} args - Arguments to delete one UnifiSite. + * @example + * // Delete one UnifiSite + * const UnifiSite = await prisma.unifiSite.delete({ + * where: { + * // ... filter to delete one UnifiSite + * } + * }) + * + */ + delete(args: Prisma.SelectSubset>): Prisma.Prisma__UnifiSiteClient, T, "delete", GlobalOmitOptions>, never, ExtArgs, GlobalOmitOptions> + + /** + * Update one UnifiSite. + * @param {UnifiSiteUpdateArgs} args - Arguments to update one UnifiSite. + * @example + * // Update one UnifiSite + * const unifiSite = await prisma.unifiSite.update({ + * where: { + * // ... provide filter here + * }, + * data: { + * // ... provide data here + * } + * }) + * + */ + update(args: Prisma.SelectSubset>): Prisma.Prisma__UnifiSiteClient, T, "update", GlobalOmitOptions>, never, ExtArgs, GlobalOmitOptions> + + /** + * Delete zero or more UnifiSites. + * @param {UnifiSiteDeleteManyArgs} args - Arguments to filter UnifiSites to delete. + * @example + * // Delete a few UnifiSites + * const { count } = await prisma.unifiSite.deleteMany({ + * where: { + * // ... provide filter here + * } + * }) + * + */ + deleteMany(args?: Prisma.SelectSubset>): Prisma.PrismaPromise + + /** + * Update zero or more UnifiSites. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {UnifiSiteUpdateManyArgs} args - Arguments to update one or more rows. + * @example + * // Update many UnifiSites + * const unifiSite = await prisma.unifiSite.updateMany({ + * where: { + * // ... provide filter here + * }, + * data: { + * // ... provide data here + * } + * }) + * + */ + updateMany(args: Prisma.SelectSubset>): Prisma.PrismaPromise + + /** + * Update zero or more UnifiSites and returns the data updated in the database. + * @param {UnifiSiteUpdateManyAndReturnArgs} args - Arguments to update many UnifiSites. + * @example + * // Update many UnifiSites + * const unifiSite = await prisma.unifiSite.updateManyAndReturn({ + * where: { + * // ... provide filter here + * }, + * data: [ + * // ... provide data here + * ] + * }) + * + * // Update zero or more UnifiSites and only return the `id` + * const unifiSiteWithIdOnly = await prisma.unifiSite.updateManyAndReturn({ + * select: { id: true }, + * where: { + * // ... provide filter here + * }, + * data: [ + * // ... provide data here + * ] + * }) + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * + */ + updateManyAndReturn(args: Prisma.SelectSubset>): Prisma.PrismaPromise, T, "updateManyAndReturn", GlobalOmitOptions>> + + /** + * Create or update one UnifiSite. + * @param {UnifiSiteUpsertArgs} args - Arguments to update or create a UnifiSite. + * @example + * // Update or create a UnifiSite + * const unifiSite = await prisma.unifiSite.upsert({ + * create: { + * // ... data to create a UnifiSite + * }, + * update: { + * // ... in case it already exists, update + * }, + * where: { + * // ... the filter for the UnifiSite we want to update + * } + * }) + */ + upsert(args: Prisma.SelectSubset>): Prisma.Prisma__UnifiSiteClient, T, "upsert", GlobalOmitOptions>, never, ExtArgs, GlobalOmitOptions> + + + /** + * Count the number of UnifiSites. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {UnifiSiteCountArgs} args - Arguments to filter UnifiSites to count. + * @example + * // Count the number of UnifiSites + * const count = await prisma.unifiSite.count({ + * where: { + * // ... the filter for the UnifiSites we want to count + * } + * }) + **/ + count( + args?: Prisma.Subset, + ): Prisma.PrismaPromise< + T extends runtime.Types.Utils.Record<'select', any> + ? T['select'] extends true + ? number + : Prisma.GetScalarType + : number + > + + /** + * Allows you to perform aggregations operations on a UnifiSite. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {UnifiSiteAggregateArgs} args - Select which aggregations you would like to apply and on what fields. + * @example + * // Ordered by age ascending + * // Where email contains prisma.io + * // Limited to the 10 users + * const aggregations = await prisma.user.aggregate({ + * _avg: { + * age: true, + * }, + * where: { + * email: { + * contains: "prisma.io", + * }, + * }, + * orderBy: { + * age: "asc", + * }, + * take: 10, + * }) + **/ + aggregate(args: Prisma.Subset): Prisma.PrismaPromise> + + /** + * Group by UnifiSite. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {UnifiSiteGroupByArgs} args - Group by arguments. + * @example + * // Group by city, order by createdAt, get count + * const result = await prisma.user.groupBy({ + * by: ['city', 'createdAt'], + * orderBy: { + * createdAt: true + * }, + * _count: { + * _all: true + * }, + * }) + * + **/ + groupBy< + T extends UnifiSiteGroupByArgs, + HasSelectOrTake extends Prisma.Or< + Prisma.Extends<'skip', Prisma.Keys>, + Prisma.Extends<'take', Prisma.Keys> + >, + OrderByArg extends Prisma.True extends HasSelectOrTake + ? { orderBy: UnifiSiteGroupByArgs['orderBy'] } + : { orderBy?: UnifiSiteGroupByArgs['orderBy'] }, + OrderFields extends Prisma.ExcludeUnderscoreKeys>>, + ByFields extends Prisma.MaybeTupleToUnion, + ByValid extends Prisma.Has, + HavingFields extends Prisma.GetHavingFields, + HavingValid extends Prisma.Has, + ByEmpty extends T['by'] extends never[] ? Prisma.True : Prisma.False, + InputErrors extends ByEmpty extends Prisma.True + ? `Error: "by" must not be empty.` + : HavingValid extends Prisma.False + ? { + [P in HavingFields]: P extends ByFields + ? never + : P extends string + ? `Error: Field "${P}" used in "having" needs to be provided in "by".` + : [ + Error, + 'Field ', + P, + ` in "having" needs to be provided in "by"`, + ] + }[HavingFields] + : 'take' extends Prisma.Keys + ? 'orderBy' extends Prisma.Keys + ? ByValid extends Prisma.True + ? {} + : { + [P in OrderFields]: P extends ByFields + ? never + : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` + }[OrderFields] + : 'Error: If you provide "take", you also need to provide "orderBy"' + : 'skip' extends Prisma.Keys + ? 'orderBy' extends Prisma.Keys + ? ByValid extends Prisma.True + ? {} + : { + [P in OrderFields]: P extends ByFields + ? never + : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` + }[OrderFields] + : 'Error: If you provide "skip", you also need to provide "orderBy"' + : ByValid extends Prisma.True + ? {} + : { + [P in OrderFields]: P extends ByFields + ? never + : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` + }[OrderFields] + >(args: Prisma.SubsetIntersection & InputErrors): {} extends InputErrors ? GetUnifiSiteGroupByPayload : Prisma.PrismaPromise +/** + * Fields of the UnifiSite model + */ +readonly fields: UnifiSiteFieldRefs; +} + +/** + * The delegate class that acts as a "Promise-like" for UnifiSite. + * Why is this prefixed with `Prisma__`? + * Because we want to prevent naming conflicts as mentioned in + * https://github.com/prisma/prisma-client-js/issues/707 + */ +export interface Prisma__UnifiSiteClient extends Prisma.PrismaPromise { + readonly [Symbol.toStringTag]: "PrismaPromise" + company = {}>(args?: Prisma.Subset>): Prisma.Prisma__CompanyClient, T, "findUniqueOrThrow", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions> + /** + * Attaches callbacks for the resolution and/or rejection of the Promise. + * @param onfulfilled The callback to execute when the Promise is resolved. + * @param onrejected The callback to execute when the Promise is rejected. + * @returns A Promise for the completion of which ever callback is executed. + */ + then(onfulfilled?: ((value: T) => TResult1 | PromiseLike) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike) | undefined | null): runtime.Types.Utils.JsPromise + /** + * Attaches a callback for only the rejection of the Promise. + * @param onrejected The callback to execute when the Promise is rejected. + * @returns A Promise for the completion of the callback. + */ + catch(onrejected?: ((reason: any) => TResult | PromiseLike) | undefined | null): runtime.Types.Utils.JsPromise + /** + * Attaches a callback that is invoked when the Promise is settled (fulfilled or rejected). The + * resolved value cannot be modified from the callback. + * @param onfinally The callback to execute when the Promise is settled (fulfilled or rejected). + * @returns A Promise for the completion of the callback. + */ + finally(onfinally?: (() => void) | undefined | null): runtime.Types.Utils.JsPromise +} + + + + +/** + * Fields of the UnifiSite model + */ +export interface UnifiSiteFieldRefs { + readonly id: Prisma.FieldRef<"UnifiSite", 'String'> + readonly name: Prisma.FieldRef<"UnifiSite", 'String'> + readonly siteId: Prisma.FieldRef<"UnifiSite", 'String'> + readonly companyId: Prisma.FieldRef<"UnifiSite", 'String'> + readonly createdAt: Prisma.FieldRef<"UnifiSite", 'DateTime'> + readonly updatedAt: Prisma.FieldRef<"UnifiSite", 'DateTime'> +} + + +// Custom InputTypes +/** + * UnifiSite findUnique + */ +export type UnifiSiteFindUniqueArgs = { + /** + * Select specific fields to fetch from the UnifiSite + */ + select?: Prisma.UnifiSiteSelect | null + /** + * Omit specific fields from the UnifiSite + */ + omit?: Prisma.UnifiSiteOmit | null + /** + * Choose, which related nodes to fetch as well + */ + include?: Prisma.UnifiSiteInclude | null + /** + * Filter, which UnifiSite to fetch. + */ + where: Prisma.UnifiSiteWhereUniqueInput +} + +/** + * UnifiSite findUniqueOrThrow + */ +export type UnifiSiteFindUniqueOrThrowArgs = { + /** + * Select specific fields to fetch from the UnifiSite + */ + select?: Prisma.UnifiSiteSelect | null + /** + * Omit specific fields from the UnifiSite + */ + omit?: Prisma.UnifiSiteOmit | null + /** + * Choose, which related nodes to fetch as well + */ + include?: Prisma.UnifiSiteInclude | null + /** + * Filter, which UnifiSite to fetch. + */ + where: Prisma.UnifiSiteWhereUniqueInput +} + +/** + * UnifiSite findFirst + */ +export type UnifiSiteFindFirstArgs = { + /** + * Select specific fields to fetch from the UnifiSite + */ + select?: Prisma.UnifiSiteSelect | null + /** + * Omit specific fields from the UnifiSite + */ + omit?: Prisma.UnifiSiteOmit | null + /** + * Choose, which related nodes to fetch as well + */ + include?: Prisma.UnifiSiteInclude | null + /** + * Filter, which UnifiSite to fetch. + */ + where?: Prisma.UnifiSiteWhereInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/sorting Sorting Docs} + * + * Determine the order of UnifiSites to fetch. + */ + orderBy?: Prisma.UnifiSiteOrderByWithRelationInput | Prisma.UnifiSiteOrderByWithRelationInput[] + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination#cursor-based-pagination Cursor Docs} + * + * Sets the position for searching for UnifiSites. + */ + cursor?: Prisma.UnifiSiteWhereUniqueInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Take `±n` UnifiSites from the position of the cursor. + */ + take?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Skip the first `n` UnifiSites. + */ + skip?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/distinct Distinct Docs} + * + * Filter by unique combinations of UnifiSites. + */ + distinct?: Prisma.UnifiSiteScalarFieldEnum | Prisma.UnifiSiteScalarFieldEnum[] +} + +/** + * UnifiSite findFirstOrThrow + */ +export type UnifiSiteFindFirstOrThrowArgs = { + /** + * Select specific fields to fetch from the UnifiSite + */ + select?: Prisma.UnifiSiteSelect | null + /** + * Omit specific fields from the UnifiSite + */ + omit?: Prisma.UnifiSiteOmit | null + /** + * Choose, which related nodes to fetch as well + */ + include?: Prisma.UnifiSiteInclude | null + /** + * Filter, which UnifiSite to fetch. + */ + where?: Prisma.UnifiSiteWhereInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/sorting Sorting Docs} + * + * Determine the order of UnifiSites to fetch. + */ + orderBy?: Prisma.UnifiSiteOrderByWithRelationInput | Prisma.UnifiSiteOrderByWithRelationInput[] + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination#cursor-based-pagination Cursor Docs} + * + * Sets the position for searching for UnifiSites. + */ + cursor?: Prisma.UnifiSiteWhereUniqueInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Take `±n` UnifiSites from the position of the cursor. + */ + take?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Skip the first `n` UnifiSites. + */ + skip?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/distinct Distinct Docs} + * + * Filter by unique combinations of UnifiSites. + */ + distinct?: Prisma.UnifiSiteScalarFieldEnum | Prisma.UnifiSiteScalarFieldEnum[] +} + +/** + * UnifiSite findMany + */ +export type UnifiSiteFindManyArgs = { + /** + * Select specific fields to fetch from the UnifiSite + */ + select?: Prisma.UnifiSiteSelect | null + /** + * Omit specific fields from the UnifiSite + */ + omit?: Prisma.UnifiSiteOmit | null + /** + * Choose, which related nodes to fetch as well + */ + include?: Prisma.UnifiSiteInclude | null + /** + * Filter, which UnifiSites to fetch. + */ + where?: Prisma.UnifiSiteWhereInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/sorting Sorting Docs} + * + * Determine the order of UnifiSites to fetch. + */ + orderBy?: Prisma.UnifiSiteOrderByWithRelationInput | Prisma.UnifiSiteOrderByWithRelationInput[] + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination#cursor-based-pagination Cursor Docs} + * + * Sets the position for listing UnifiSites. + */ + cursor?: Prisma.UnifiSiteWhereUniqueInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Take `±n` UnifiSites from the position of the cursor. + */ + take?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Skip the first `n` UnifiSites. + */ + skip?: number + distinct?: Prisma.UnifiSiteScalarFieldEnum | Prisma.UnifiSiteScalarFieldEnum[] +} + +/** + * UnifiSite create + */ +export type UnifiSiteCreateArgs = { + /** + * Select specific fields to fetch from the UnifiSite + */ + select?: Prisma.UnifiSiteSelect | null + /** + * Omit specific fields from the UnifiSite + */ + omit?: Prisma.UnifiSiteOmit | null + /** + * Choose, which related nodes to fetch as well + */ + include?: Prisma.UnifiSiteInclude | null + /** + * The data needed to create a UnifiSite. + */ + data: Prisma.XOR +} + +/** + * UnifiSite createMany + */ +export type UnifiSiteCreateManyArgs = { + /** + * The data used to create many UnifiSites. + */ + data: Prisma.UnifiSiteCreateManyInput | Prisma.UnifiSiteCreateManyInput[] + skipDuplicates?: boolean +} + +/** + * UnifiSite createManyAndReturn + */ +export type UnifiSiteCreateManyAndReturnArgs = { + /** + * Select specific fields to fetch from the UnifiSite + */ + select?: Prisma.UnifiSiteSelectCreateManyAndReturn | null + /** + * Omit specific fields from the UnifiSite + */ + omit?: Prisma.UnifiSiteOmit | null + /** + * The data used to create many UnifiSites. + */ + data: Prisma.UnifiSiteCreateManyInput | Prisma.UnifiSiteCreateManyInput[] + skipDuplicates?: boolean + /** + * Choose, which related nodes to fetch as well + */ + include?: Prisma.UnifiSiteIncludeCreateManyAndReturn | null +} + +/** + * UnifiSite update + */ +export type UnifiSiteUpdateArgs = { + /** + * Select specific fields to fetch from the UnifiSite + */ + select?: Prisma.UnifiSiteSelect | null + /** + * Omit specific fields from the UnifiSite + */ + omit?: Prisma.UnifiSiteOmit | null + /** + * Choose, which related nodes to fetch as well + */ + include?: Prisma.UnifiSiteInclude | null + /** + * The data needed to update a UnifiSite. + */ + data: Prisma.XOR + /** + * Choose, which UnifiSite to update. + */ + where: Prisma.UnifiSiteWhereUniqueInput +} + +/** + * UnifiSite updateMany + */ +export type UnifiSiteUpdateManyArgs = { + /** + * The data used to update UnifiSites. + */ + data: Prisma.XOR + /** + * Filter which UnifiSites to update + */ + where?: Prisma.UnifiSiteWhereInput + /** + * Limit how many UnifiSites to update. + */ + limit?: number +} + +/** + * UnifiSite updateManyAndReturn + */ +export type UnifiSiteUpdateManyAndReturnArgs = { + /** + * Select specific fields to fetch from the UnifiSite + */ + select?: Prisma.UnifiSiteSelectUpdateManyAndReturn | null + /** + * Omit specific fields from the UnifiSite + */ + omit?: Prisma.UnifiSiteOmit | null + /** + * The data used to update UnifiSites. + */ + data: Prisma.XOR + /** + * Filter which UnifiSites to update + */ + where?: Prisma.UnifiSiteWhereInput + /** + * Limit how many UnifiSites to update. + */ + limit?: number + /** + * Choose, which related nodes to fetch as well + */ + include?: Prisma.UnifiSiteIncludeUpdateManyAndReturn | null +} + +/** + * UnifiSite upsert + */ +export type UnifiSiteUpsertArgs = { + /** + * Select specific fields to fetch from the UnifiSite + */ + select?: Prisma.UnifiSiteSelect | null + /** + * Omit specific fields from the UnifiSite + */ + omit?: Prisma.UnifiSiteOmit | null + /** + * Choose, which related nodes to fetch as well + */ + include?: Prisma.UnifiSiteInclude | null + /** + * The filter to search for the UnifiSite to update in case it exists. + */ + where: Prisma.UnifiSiteWhereUniqueInput + /** + * In case the UnifiSite found by the `where` argument doesn't exist, create a new UnifiSite with this data. + */ + create: Prisma.XOR + /** + * In case the UnifiSite was found with the provided `where` argument, update it with this data. + */ + update: Prisma.XOR +} + +/** + * UnifiSite delete + */ +export type UnifiSiteDeleteArgs = { + /** + * Select specific fields to fetch from the UnifiSite + */ + select?: Prisma.UnifiSiteSelect | null + /** + * Omit specific fields from the UnifiSite + */ + omit?: Prisma.UnifiSiteOmit | null + /** + * Choose, which related nodes to fetch as well + */ + include?: Prisma.UnifiSiteInclude | null + /** + * Filter which UnifiSite to delete. + */ + where: Prisma.UnifiSiteWhereUniqueInput +} + +/** + * UnifiSite deleteMany + */ +export type UnifiSiteDeleteManyArgs = { + /** + * Filter which UnifiSites to delete + */ + where?: Prisma.UnifiSiteWhereInput + /** + * Limit how many UnifiSites to delete. + */ + limit?: number +} + +/** + * UnifiSite.company + */ +export type UnifiSite$companyArgs = { + /** + * Select specific fields to fetch from the Company + */ + select?: Prisma.CompanySelect | null + /** + * Omit specific fields from the Company + */ + omit?: Prisma.CompanyOmit | null + /** + * Choose, which related nodes to fetch as well + */ + include?: Prisma.CompanyInclude | null + where?: Prisma.CompanyWhereInput +} + +/** + * UnifiSite without action + */ +export type UnifiSiteDefaultArgs = { + /** + * Select specific fields to fetch from the UnifiSite + */ + select?: Prisma.UnifiSiteSelect | null + /** + * Omit specific fields from the UnifiSite + */ + omit?: Prisma.UnifiSiteOmit | null + /** + * Choose, which related nodes to fetch as well + */ + include?: Prisma.UnifiSiteInclude | null +} diff --git a/prisma/migrations/20260220182446_add_sub_credentials_self_relation/migration.sql b/prisma/migrations/20260220182446_add_sub_credentials_self_relation/migration.sql new file mode 100644 index 0000000..ff48543 --- /dev/null +++ b/prisma/migrations/20260220182446_add_sub_credentials_self_relation/migration.sql @@ -0,0 +1,73 @@ +-- CreateTable +CREATE TABLE "Company" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "cw_CompanyId" INTEGER NOT NULL, + "cw_Identifier" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Company_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "CredentialType" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "permissionScope" TEXT NOT NULL, + "icon" TEXT, + "fields" JSONB NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "CredentialType_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "SecureValue" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "content" TEXT NOT NULL, + "hash" TEXT NOT NULL, + "credentialId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "SecureValue_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Credential" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "notes" TEXT, + "subCredentialOfId" TEXT, + "typeId" TEXT NOT NULL, + "fields" JSONB NOT NULL, + "companyId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Credential_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "Company_cw_CompanyId_key" ON "Company"("cw_CompanyId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Company_cw_Identifier_key" ON "Company"("cw_Identifier"); + +-- CreateIndex +CREATE UNIQUE INDEX "CredentialType_name_key" ON "CredentialType"("name"); + +-- AddForeignKey +ALTER TABLE "SecureValue" ADD CONSTRAINT "SecureValue_credentialId_fkey" FOREIGN KEY ("credentialId") REFERENCES "Credential"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Credential" ADD CONSTRAINT "Credential_subCredentialOfId_fkey" FOREIGN KEY ("subCredentialOfId") REFERENCES "Credential"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Credential" ADD CONSTRAINT "Credential_typeId_fkey" FOREIGN KEY ("typeId") REFERENCES "CredentialType"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Credential" ADD CONSTRAINT "Credential_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "Company"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 9e524a4..844d555 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -55,6 +55,19 @@ model Role { updatedAt DateTime @updatedAt } +model UnifiSite { + id String @id @default(cuid()) + name String + + siteId String @unique + + companyId String? + company Company? @relation(fields: [companyId], references: [id]) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + model Company { id String @id @default(cuid()) name String @@ -63,6 +76,7 @@ model Company { cw_Identifier String @unique credentials Credential[] + unifiSites UnifiSite[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@ -97,9 +111,12 @@ model SecureValue { } model Credential { - id String @id @default(cuid()) - name String - notes String? + id String @id @default(cuid()) + name String + notes String? + subCredentialOfId String? + subCredentialOf Credential? @relation("SubCredentials", fields: [subCredentialOfId], references: [id], onDelete: Cascade) + subCredentials Credential[] @relation("SubCredentials") typeId String type CredentialType @relation(fields: [typeId], references: [id], onDelete: Cascade) diff --git a/src/api/companies/[id]/fetch.ts b/src/api/companies/[id]/fetch.ts index 168f789..0da65b5 100644 --- a/src/api/companies/[id]/fetch.ts +++ b/src/api/companies/[id]/fetch.ts @@ -14,6 +14,9 @@ export default createRoute( async (c) => { const company = await companies.fetch(c.req.param("identifier")); const includeAddress = c.req.query("includeAddress") === "true"; + const includePrimaryContact = + c.req.query("includePrimaryContact") === "true"; + const includeAllContacts = c.req.query("includeAllContacts") === "true"; // Check for address-specific permission if includeAddress is requested if (includeAddress) { @@ -27,9 +30,25 @@ export default createRoute( } } + // Check for contacts permission if includeAllContacts is requested + if (includeAllContacts) { + const user = c.get("user"); + if (!user || !(await user.hasPermission("company.fetch.contacts"))) { + throw new GenericError({ + name: "InsufficientPermission", + message: "You do not have permission to view company contacts.", + status: 403, + }); + } + } + const response = apiResponse.successful( "Company Fetched Successfully!", - company.toJson({ includeAddress }), + company.toJson({ + includeAddress, + includePrimaryContact, + includeAllContacts, + }), ); return c.json(response, response.status as ContentfulStatusCode); }, diff --git a/src/api/companies/[id]/unifiSites.ts b/src/api/companies/[id]/unifiSites.ts new file mode 100644 index 0000000..335e1a7 --- /dev/null +++ b/src/api/companies/[id]/unifiSites.ts @@ -0,0 +1,24 @@ +import { createRoute } from "../../../modules/api-utils/createRoute"; +import { unifiSites } from "../../../managers/unifiSites"; +import { companies } from "../../../managers/companies"; +import { apiResponse } from "../../../modules/api-utils/apiResponse"; +import { ContentfulStatusCode } from "hono/utils/http-status"; +import { authMiddleware } from "../../middleware/authorization"; + +/* GET /v1/company/companies/:identifier/unifi/sites */ +export default createRoute( + "get", + ["/companies/:identifier/unifi/sites"], + async (c) => { + const company = await companies.fetch(c.req.param("identifier")); + const sites = await unifiSites.fetchByCompany(company.id); + const response = apiResponse.successful( + "Company UniFi Sites Fetched Successfully!", + sites, + ); + return c.json(response, response.status as ContentfulStatusCode); + }, + authMiddleware({ + permissions: ["unifi.access", "company.fetch"], + }), +); diff --git a/src/api/companies/index.ts b/src/api/companies/index.ts index 4719b03..e2e1461 100644 --- a/src/api/companies/index.ts +++ b/src/api/companies/index.ts @@ -1,6 +1,7 @@ import { default as fetchAll } from "./fetchAll"; import { default as fetch } from "./[id]/fetch"; import { default as configurations } from "./[id]/configurations"; +import { default as unifiSites } from "./[id]/unifiSites"; import { default as count } from "./count"; -export { configurations, count, fetch, fetchAll }; +export { configurations, count, fetch, fetchAll, unifiSites }; diff --git a/src/api/credential-types/create.ts b/src/api/credential-types/create.ts index 203a709..46db160 100644 --- a/src/api/credential-types/create.ts +++ b/src/api/credential-types/create.ts @@ -15,19 +15,22 @@ export default createRoute( async (c) => { const body = await c.req.json(); + const fieldSchema: z.ZodType = z.lazy(() => + z.object({ + id: z.string(), + name: z.string(), + required: z.boolean(), + secure: z.boolean(), + valueType: z.enum(Object.values(ValueType)), + subFields: z.array(fieldSchema).optional(), + }), + ); + const schema = z.object({ name: z.string().min(1, "Name is required"), permissionScope: z.string().min(1, "Permission scope is required"), icon: z.string().optional(), - fields: z.array( - z.object({ - id: z.string(), - name: z.string(), - required: z.boolean(), - secure: z.boolean(), - valueType: z.enum(Object.values(ValueType)), - }), - ), + fields: z.array(fieldSchema), }); const data = schema.parse(body); diff --git a/src/api/credential-types/update.ts b/src/api/credential-types/update.ts index d785658..545fb55 100644 --- a/src/api/credential-types/update.ts +++ b/src/api/credential-types/update.ts @@ -16,21 +16,22 @@ export default createRoute( const body = await c.req.json(); const credentialType = await credentialTypes.fetch(c.req.param("id")); + const fieldSchema: z.ZodType = z.lazy(() => + z.object({ + id: z.string(), + name: z.string(), + required: z.boolean(), + secure: z.boolean(), + valueType: z.enum(Object.values(ValueType)), + subFields: z.array(fieldSchema).optional(), + }), + ); + const schema = z.object({ name: z.string().optional(), permissionScope: z.string().optional(), icon: z.string().optional(), - fields: z - .array( - z.object({ - id: z.string(), - name: z.string(), - required: z.boolean(), - secure: z.boolean(), - valueType: z.enum(Object.values(ValueType)), - }), - ) - .optional(), + fields: z.array(fieldSchema).optional(), }); const data = schema.parse(body); diff --git a/src/api/credentials/addSubCredential.ts b/src/api/credentials/addSubCredential.ts new file mode 100644 index 0000000..92edd97 --- /dev/null +++ b/src/api/credentials/addSubCredential.ts @@ -0,0 +1,48 @@ +import { createRoute } from "../../modules/api-utils/createRoute"; +import { credentials } from "../../managers/credentials"; +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/credential/credentials/:id/sub-credentials */ +export default createRoute( + "post", + ["/credentials/:id/sub-credentials"], + + async (c) => { + const parentId = c.req.param("id"); + const body = await c.req.json(); + + const schema = z.object({ + fieldId: z.string().min(1, "Field ID is required"), + name: z.string().min(1, "Name is required"), + fields: z.array( + z.object({ + fieldId: z.string(), + value: z.string(), + }), + ), + }); + + const data = schema.parse(body); + + const subCredential = await credentials.addSubCredential( + parentId, + data.fieldId, + { + name: data.name, + fields: data.fields, + }, + ); + + const response = apiResponse.created( + "Sub-Credential Created Successfully!", + subCredential.toJson(), + ); + return c.json(response, response.status as ContentfulStatusCode); + }, + authMiddleware({ + permissions: ["credential.fetch", "credential.sub_credentials.create"], + }), +); diff --git a/src/api/credentials/create.ts b/src/api/credentials/create.ts index 7115947..e19ae9d 100644 --- a/src/api/credentials/create.ts +++ b/src/api/credentials/create.ts @@ -25,6 +25,22 @@ export default createRoute( value: z.string(), }), ), + subCredentials: z + .record( + z.string(), + z.array( + z.object({ + name: z.string().min(1, "Sub-credential name is required"), + fields: z.array( + z.object({ + fieldId: z.string(), + value: z.string(), + }), + ), + }), + ), + ) + .optional(), }); const data = schema.parse(body); diff --git a/src/api/credentials/fetchSubCredentials.ts b/src/api/credentials/fetchSubCredentials.ts new file mode 100644 index 0000000..31268ba --- /dev/null +++ b/src/api/credentials/fetchSubCredentials.ts @@ -0,0 +1,29 @@ +import { createRoute } from "../../modules/api-utils/createRoute"; +import { credentials } from "../../managers/credentials"; +import { apiResponse } from "../../modules/api-utils/apiResponse"; +import { ContentfulStatusCode } from "hono/utils/http-status"; +import { authMiddleware } from "../middleware/authorization"; + +/* GET /v1/credential/credentials/:id/sub-credentials */ +export default createRoute( + "get", + ["/credentials/:id/sub-credentials"], + + async (c) => { + const parentId = c.req.param("id"); + + // Verify the parent credential exists + await credentials.fetch(parentId); + + const subCredentials = await credentials.fetchSubCredentials(parentId); + + const response = apiResponse.successful( + "Sub-Credentials Fetched Successfully!", + subCredentials.map((sc) => sc.toJson()), + ); + return c.json(response, response.status as ContentfulStatusCode); + }, + authMiddleware({ + permissions: ["credential.fetch", "credential.sub_credentials.fetch"], + }), +); diff --git a/src/api/credentials/index.ts b/src/api/credentials/index.ts index c506113..f2235da 100644 --- a/src/api/credentials/index.ts +++ b/src/api/credentials/index.ts @@ -8,6 +8,9 @@ import { default as readSecureValues } from "./readSecureValues"; import { default as readSecureValue } from "./readSecureValue"; import { default as deleteCredential } from "./delete"; import { default as valueTypes } from "./valueTypes"; +import { default as fetchSubCredentials } from "./fetchSubCredentials"; +import { default as addSubCredential } from "./addSubCredential"; +import { default as removeSubCredential } from "./removeSubCredential"; export { valueTypes, @@ -20,4 +23,7 @@ export { readSecureValues, readSecureValue, deleteCredential as delete, + fetchSubCredentials, + addSubCredential, + removeSubCredential, }; diff --git a/src/api/credentials/removeSubCredential.ts b/src/api/credentials/removeSubCredential.ts new file mode 100644 index 0000000..15dd12a --- /dev/null +++ b/src/api/credentials/removeSubCredential.ts @@ -0,0 +1,27 @@ +import { createRoute } from "../../modules/api-utils/createRoute"; +import { credentials } from "../../managers/credentials"; +import { apiResponse } from "../../modules/api-utils/apiResponse"; +import { ContentfulStatusCode } from "hono/utils/http-status"; +import { authMiddleware } from "../middleware/authorization"; + +/* DELETE /v1/credential/credentials/:id/sub-credentials/:subId */ +export default createRoute( + "delete", + ["/credentials/:id/sub-credentials/:subId"], + + async (c) => { + const parentId = c.req.param("id"); + const subId = c.req.param("subId"); + + await credentials.removeSubCredential(parentId, subId); + + const response = apiResponse.successful( + "Sub-Credential Removed Successfully!", + null, + ); + return c.json(response, response.status as ContentfulStatusCode); + }, + authMiddleware({ + permissions: ["credential.fetch", "credential.sub_credentials.delete"], + }), +); diff --git a/src/api/routers/unifiRouter.ts b/src/api/routers/unifiRouter.ts new file mode 100644 index 0000000..6294205 --- /dev/null +++ b/src/api/routers/unifiRouter.ts @@ -0,0 +1,7 @@ +import { Hono } from "hono"; +import * as unifiRoutes from "../unifi"; + +const unifiRouter = new Hono(); +Object.values(unifiRoutes).map((r) => unifiRouter.route("/", r)); + +export default unifiRouter; diff --git a/src/api/server.ts b/src/api/server.ts index 8b24476..c686e58 100644 --- a/src/api/server.ts +++ b/src/api/server.ts @@ -54,6 +54,7 @@ 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); +v1.route("/unifi", require("./routers/unifiRouter").default); app.route("/v1", v1); export default app; diff --git a/src/api/unifi/index.ts b/src/api/unifi/index.ts new file mode 100644 index 0000000..0964930 --- /dev/null +++ b/src/api/unifi/index.ts @@ -0,0 +1,43 @@ +import { default as fetchAllSites } from "./sites/fetchAll"; +import { default as syncSites } from "./sites/sync"; +import { default as createSite } from "./sites/create"; +import { default as fetchSite } from "./site/fetch"; +import { default as siteOverview } from "./site/overview"; +import { default as siteDevices } from "./site/devices"; +import { default as siteNetworks } from "./site/networks"; +import { default as siteWifiFetchAll } from "./site/wifi/fetchAll"; +import { default as siteWifiUpdate } from "./site/wifi/update"; +import { default as siteWifiPpskFetchAll } from "./site/wifi/ppskFetchAll"; +import { default as siteWifiPpskCreate } from "./site/wifi/ppskCreate"; +import { default as siteLink } from "./site/link"; +import { default as siteUnlink } from "./site/unlink"; +import { default as siteWlanGroups } from "./site/wlanGroups"; +import { default as siteWlanGroupsCreate } from "./site/wlanGroupsCreate"; +import { default as siteAccessPoints } from "./site/accessPoints"; +import { default as siteApGroups } from "./site/apGroups"; +import { default as siteWifiLimits } from "./site/wifiLimits"; +import { default as siteSpeedProfilesFetchAll } from "./site/speedProfilesFetchAll"; +import { default as siteSpeedProfilesCreate } from "./site/speedProfilesCreate"; + +export { + fetchAllSites, + syncSites, + createSite, + fetchSite, + siteOverview, + siteDevices, + siteNetworks, + siteWifiFetchAll, + siteWifiUpdate, + siteWifiPpskFetchAll, + siteWifiPpskCreate, + siteLink, + siteUnlink, + siteWlanGroups, + siteWlanGroupsCreate, + siteAccessPoints, + siteApGroups, + siteWifiLimits, + siteSpeedProfilesFetchAll, + siteSpeedProfilesCreate, +}; diff --git a/src/api/unifi/site/accessPoints.ts b/src/api/unifi/site/accessPoints.ts new file mode 100644 index 0000000..bcbf35f --- /dev/null +++ b/src/api/unifi/site/accessPoints.ts @@ -0,0 +1,23 @@ +import { createRoute } from "../../../modules/api-utils/createRoute"; +import { unifiSites } from "../../../managers/unifiSites"; +import { apiResponse } from "../../../modules/api-utils/apiResponse"; +import { ContentfulStatusCode } from "hono/utils/http-status"; +import { authMiddleware } from "../../middleware/authorization"; + +/* GET /v1/unifi/site/:id/access-points */ +export default createRoute( + "get", + ["/site/:id/access-points"], + async (c) => { + const site = await unifiSites.fetch(c.req.param("id")); + const accessPoints = await unifiSites.getAccessPoints(site.siteId); + const response = apiResponse.successful( + "UniFi Access Points Fetched Successfully!", + accessPoints, + ); + return c.json(response, response.status as ContentfulStatusCode); + }, + authMiddleware({ + permissions: ["unifi.access", "unifi.site.access-points"], + }), +); diff --git a/src/api/unifi/site/apGroups.ts b/src/api/unifi/site/apGroups.ts new file mode 100644 index 0000000..b8efeb8 --- /dev/null +++ b/src/api/unifi/site/apGroups.ts @@ -0,0 +1,23 @@ +import { createRoute } from "../../../modules/api-utils/createRoute"; +import { unifiSites } from "../../../managers/unifiSites"; +import { apiResponse } from "../../../modules/api-utils/apiResponse"; +import { ContentfulStatusCode } from "hono/utils/http-status"; +import { authMiddleware } from "../../middleware/authorization"; + +/* GET /v1/unifi/site/:id/ap-groups */ +export default createRoute( + "get", + ["/site/:id/ap-groups"], + async (c) => { + const site = await unifiSites.fetch(c.req.param("id")); + const apGroups = await unifiSites.getApGroups(site.siteId); + const response = apiResponse.successful( + "UniFi AP Groups Fetched Successfully!", + apGroups, + ); + return c.json(response, response.status as ContentfulStatusCode); + }, + authMiddleware({ + permissions: ["unifi.access", "unifi.site.ap-groups"], + }), +); diff --git a/src/api/unifi/site/devices.ts b/src/api/unifi/site/devices.ts new file mode 100644 index 0000000..90e217a --- /dev/null +++ b/src/api/unifi/site/devices.ts @@ -0,0 +1,21 @@ +import { createRoute } from "../../../modules/api-utils/createRoute"; +import { unifiSites } from "../../../managers/unifiSites"; +import { apiResponse } from "../../../modules/api-utils/apiResponse"; +import { ContentfulStatusCode } from "hono/utils/http-status"; +import { authMiddleware } from "../../middleware/authorization"; + +/* GET /v1/unifi/site/:id/devices */ +export default createRoute( + "get", + ["/site/:id/devices"], + async (c) => { + const site = await unifiSites.fetch(c.req.param("id")); + const devices = await unifiSites.getDevices(site.siteId); + const response = apiResponse.successful( + "UniFi Devices Fetched Successfully!", + devices, + ); + return c.json(response, response.status as ContentfulStatusCode); + }, + authMiddleware({ permissions: ["unifi.access", "unifi.site.devices"] }), +); diff --git a/src/api/unifi/site/fetch.ts b/src/api/unifi/site/fetch.ts new file mode 100644 index 0000000..4e920ed --- /dev/null +++ b/src/api/unifi/site/fetch.ts @@ -0,0 +1,20 @@ +import { createRoute } from "../../../modules/api-utils/createRoute"; +import { unifiSites } from "../../../managers/unifiSites"; +import { apiResponse } from "../../../modules/api-utils/apiResponse"; +import { ContentfulStatusCode } from "hono/utils/http-status"; +import { authMiddleware } from "../../middleware/authorization"; + +/* GET /v1/unifi/site/:id */ +export default createRoute( + "get", + ["/site/:id"], + async (c) => { + const site = await unifiSites.fetch(c.req.param("id")); + const response = apiResponse.successful( + "UniFi Site Fetched Successfully!", + site, + ); + return c.json(response, response.status as ContentfulStatusCode); + }, + authMiddleware({ permissions: ["unifi.access", "unifi.sites.fetch"] }), +); diff --git a/src/api/unifi/site/link.ts b/src/api/unifi/site/link.ts new file mode 100644 index 0000000..b2e9107 --- /dev/null +++ b/src/api/unifi/site/link.ts @@ -0,0 +1,26 @@ +import { createRoute } from "../../../modules/api-utils/createRoute"; +import { unifiSites } from "../../../managers/unifiSites"; +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/unifi/site/:id/link */ +export default createRoute( + "post", + ["/site/:id/link"], + async (c) => { + const siteId = c.req.param("id"); + const body = await c.req.json(); + const schema = z.object({ companyId: z.string() }).strict(); + const { companyId } = schema.parse(body); + + const site = await unifiSites.linkToCompany(siteId, companyId); + const response = apiResponse.successful( + "UniFi Site Linked to Company Successfully!", + site, + ); + return c.json(response, response.status as ContentfulStatusCode); + }, + authMiddleware({ permissions: ["unifi.access", "unifi.sites.link"] }), +); diff --git a/src/api/unifi/site/networks.ts b/src/api/unifi/site/networks.ts new file mode 100644 index 0000000..c25025e --- /dev/null +++ b/src/api/unifi/site/networks.ts @@ -0,0 +1,21 @@ +import { createRoute } from "../../../modules/api-utils/createRoute"; +import { unifiSites } from "../../../managers/unifiSites"; +import { apiResponse } from "../../../modules/api-utils/apiResponse"; +import { ContentfulStatusCode } from "hono/utils/http-status"; +import { authMiddleware } from "../../middleware/authorization"; + +/* GET /v1/unifi/site/:id/networks */ +export default createRoute( + "get", + ["/site/:id/networks"], + async (c) => { + const site = await unifiSites.fetch(c.req.param("id")); + const networks = await unifiSites.getNetworks(site.siteId); + const response = apiResponse.successful( + "UniFi Networks Fetched Successfully!", + networks, + ); + return c.json(response, response.status as ContentfulStatusCode); + }, + authMiddleware({ permissions: ["unifi.access", "unifi.site.networks"] }), +); diff --git a/src/api/unifi/site/overview.ts b/src/api/unifi/site/overview.ts new file mode 100644 index 0000000..2664be7 --- /dev/null +++ b/src/api/unifi/site/overview.ts @@ -0,0 +1,21 @@ +import { createRoute } from "../../../modules/api-utils/createRoute"; +import { unifiSites } from "../../../managers/unifiSites"; +import { apiResponse } from "../../../modules/api-utils/apiResponse"; +import { ContentfulStatusCode } from "hono/utils/http-status"; +import { authMiddleware } from "../../middleware/authorization"; + +/* GET /v1/unifi/site/:id/overview */ +export default createRoute( + "get", + ["/site/:id/overview"], + async (c) => { + const site = await unifiSites.fetch(c.req.param("id")); + const overview = await unifiSites.getSiteOverview(site.siteId); + const response = apiResponse.successful( + "UniFi Site Overview Fetched Successfully!", + overview, + ); + return c.json(response, response.status as ContentfulStatusCode); + }, + authMiddleware({ permissions: ["unifi.access", "unifi.site.overview"] }), +); diff --git a/src/api/unifi/site/speedProfilesCreate.ts b/src/api/unifi/site/speedProfilesCreate.ts new file mode 100644 index 0000000..f79bcac --- /dev/null +++ b/src/api/unifi/site/speedProfilesCreate.ts @@ -0,0 +1,40 @@ +import { createRoute } from "../../../modules/api-utils/createRoute"; +import { unifiSites } from "../../../managers/unifiSites"; +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/unifi/site/:id/speed-profiles */ +export default createRoute( + "post", + ["/site/:id/speed-profiles"], + async (c) => { + const site = await unifiSites.fetch(c.req.param("id")); + + const body = await c.req.json(); + const schema = z + .object({ + name: z.string(), + downloadLimitKbps: z.number().optional(), + uploadLimitKbps: z.number().optional(), + }) + .strict(); + + const parsed = schema.parse(body); + const profile = await unifiSites.createUserGroup(site.siteId, parsed); + + const response = apiResponse.created( + "UniFi Speed Profile Created Successfully!", + profile, + ); + return c.json(response, response.status as ContentfulStatusCode); + }, + authMiddleware({ + permissions: [ + "unifi.access", + "unifi.site.speed-profiles", + "unifi.site.speed-profiles.create", + ], + }), +); diff --git a/src/api/unifi/site/speedProfilesFetchAll.ts b/src/api/unifi/site/speedProfilesFetchAll.ts new file mode 100644 index 0000000..2d3d2bb --- /dev/null +++ b/src/api/unifi/site/speedProfilesFetchAll.ts @@ -0,0 +1,23 @@ +import { createRoute } from "../../../modules/api-utils/createRoute"; +import { unifiSites } from "../../../managers/unifiSites"; +import { apiResponse } from "../../../modules/api-utils/apiResponse"; +import { ContentfulStatusCode } from "hono/utils/http-status"; +import { authMiddleware } from "../../middleware/authorization"; + +/* GET /v1/unifi/site/:id/speed-profiles */ +export default createRoute( + "get", + ["/site/:id/speed-profiles"], + async (c) => { + const site = await unifiSites.fetch(c.req.param("id")); + const profiles = await unifiSites.getUserGroups(site.siteId); + const response = apiResponse.successful( + "UniFi Speed Profiles Fetched Successfully!", + profiles, + ); + return c.json(response, response.status as ContentfulStatusCode); + }, + authMiddleware({ + permissions: ["unifi.access", "unifi.site.speed-profiles"], + }), +); diff --git a/src/api/unifi/site/unlink.ts b/src/api/unifi/site/unlink.ts new file mode 100644 index 0000000..897708f --- /dev/null +++ b/src/api/unifi/site/unlink.ts @@ -0,0 +1,21 @@ +import { createRoute } from "../../../modules/api-utils/createRoute"; +import { unifiSites } from "../../../managers/unifiSites"; +import { apiResponse } from "../../../modules/api-utils/apiResponse"; +import { ContentfulStatusCode } from "hono/utils/http-status"; +import { authMiddleware } from "../../middleware/authorization"; + +/* POST /v1/unifi/site/:id/unlink */ +export default createRoute( + "post", + ["/site/:id/unlink"], + async (c) => { + const siteId = c.req.param("id"); + const site = await unifiSites.unlinkFromCompany(siteId); + const response = apiResponse.successful( + "UniFi Site Unlinked from Company Successfully!", + site, + ); + return c.json(response, response.status as ContentfulStatusCode); + }, + authMiddleware({ permissions: ["unifi.access", "unifi.sites.link"] }), +); diff --git a/src/api/unifi/site/wifi/fetchAll.ts b/src/api/unifi/site/wifi/fetchAll.ts new file mode 100644 index 0000000..ce5132f --- /dev/null +++ b/src/api/unifi/site/wifi/fetchAll.ts @@ -0,0 +1,31 @@ +import { createRoute } from "../../../../modules/api-utils/createRoute"; +import { unifiSites } from "../../../../managers/unifiSites"; +import { apiResponse } from "../../../../modules/api-utils/apiResponse"; +import { ContentfulStatusCode } from "hono/utils/http-status"; +import { authMiddleware } from "../../../middleware/authorization"; +import { processObjectValuePerms } from "../../../../modules/permission-utils/processObjectPermissions"; + +/* GET /v1/unifi/site/:id/wifi */ +export default createRoute( + "get", + ["/site/:id/wifi"], + async (c) => { + const site = await unifiSites.fetch(c.req.param("id")); + const wlans = await unifiSites.getWlanConf(site.siteId); + + const processWlans = await Promise.all( + wlans.map((wlan) => + processObjectValuePerms(wlan, "unifi.site.wifi.read", c.get("user")), + ), + ); + + console.log(processWlans); + + const response = apiResponse.successful( + "UniFi WiFi Networks Fetched Successfully!", + processWlans, + ); + return c.json(response, response.status as ContentfulStatusCode); + }, + authMiddleware({ permissions: ["unifi.access", "unifi.site.wifi"] }), +); diff --git a/src/api/unifi/site/wifi/ppskCreate.ts b/src/api/unifi/site/wifi/ppskCreate.ts new file mode 100644 index 0000000..05f20bc --- /dev/null +++ b/src/api/unifi/site/wifi/ppskCreate.ts @@ -0,0 +1,47 @@ +import { createRoute } from "../../../../modules/api-utils/createRoute"; +import { unifiSites } from "../../../../managers/unifiSites"; +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/unifi/site/:id/wifi/:wlanId/ppsk */ +export default createRoute( + "post", + ["/site/:id/wifi/:wlanId/ppsk"], + async (c) => { + const site = await unifiSites.fetch(c.req.param("id")); + const wlanId = c.req.param("wlanId"); + + const body = await c.req.json(); + const schema = z + .object({ + key: z.string().min(8, "PSK must be at least 8 characters"), + name: z.string().min(1, "Name is required"), + mac: z.string().optional(), + vlanId: z.number().optional(), + }) + .strict(); + + const parsed = schema.parse(body); + const ppsks = await unifiSites.createPrivatePSK( + site.siteId, + wlanId, + parsed, + ); + + const response = apiResponse.created( + "UniFi Private PSK Created Successfully!", + ppsks, + ); + return c.json(response, response.status as ContentfulStatusCode); + }, + authMiddleware({ + permissions: [ + "unifi.access", + "unifi.site.wifi", + "unifi.site.wifi.ppsk", + "unifi.site.wifi.ppsk.create", + ], + }), +); diff --git a/src/api/unifi/site/wifi/ppskFetchAll.ts b/src/api/unifi/site/wifi/ppskFetchAll.ts new file mode 100644 index 0000000..eea5005 --- /dev/null +++ b/src/api/unifi/site/wifi/ppskFetchAll.ts @@ -0,0 +1,24 @@ +import { createRoute } from "../../../../modules/api-utils/createRoute"; +import { unifiSites } from "../../../../managers/unifiSites"; +import { apiResponse } from "../../../../modules/api-utils/apiResponse"; +import { ContentfulStatusCode } from "hono/utils/http-status"; +import { authMiddleware } from "../../../middleware/authorization"; + +/* GET /v1/unifi/site/:id/wifi/:wlanId/ppsk */ +export default createRoute( + "get", + ["/site/:id/wifi/:wlanId/ppsk"], + async (c) => { + const site = await unifiSites.fetch(c.req.param("id")); + const wlanId = c.req.param("wlanId"); + const ppsks = await unifiSites.getPrivatePSKs(site.siteId, wlanId); + const response = apiResponse.successful( + "UniFi Private PSKs Fetched Successfully!", + ppsks, + ); + return c.json(response, response.status as ContentfulStatusCode); + }, + authMiddleware({ + permissions: ["unifi.access", "unifi.site.wifi", "unifi.site.wifi.ppsk"], + }), +); diff --git a/src/api/unifi/site/wifi/update.ts b/src/api/unifi/site/wifi/update.ts new file mode 100644 index 0000000..759f678 --- /dev/null +++ b/src/api/unifi/site/wifi/update.ts @@ -0,0 +1,117 @@ +import { createRoute } from "../../../../modules/api-utils/createRoute"; +import { unifiSites } from "../../../../managers/unifiSites"; +import { apiResponse } from "../../../../modules/api-utils/apiResponse"; +import { ContentfulStatusCode } from "hono/utils/http-status"; +import { authMiddleware } from "../../../middleware/authorization"; +import { toWlanConfUpdate } from "../../../../modules/unifi-api/unifiTypes"; +import { z } from "zod"; + +/* PATCH /v1/unifi/site/:id/wifi/:wlanId */ +export default createRoute( + "patch", + ["/site/:id/wifi/:wlanId"], + async (c) => { + const site = await unifiSites.fetch(c.req.param("id")); + const wlanId = c.req.param("wlanId"); + + const body = await c.req.json(); + const schema = z + .object({ + name: z.string().optional(), + passphrase: z.string().optional(), + enabled: z.boolean().optional(), + security: z.enum(["wpapsk", "wpaeap", "open", "osen"]).optional(), + wpaMode: z.enum(["wpa2", "wpa3", "wpa2wpa3"]).optional(), + wpaEnc: z.enum(["ccmp", "gcmp", "ccmp-gcmp"]).optional(), + hideSSID: z.boolean().optional(), + macFilterEnabled: z.boolean().optional(), + macFilterPolicy: z.enum(["allow", "deny"]).optional(), + isGuest: z.boolean().optional(), + l2Isolation: z.boolean().optional(), + fastRoamingEnabled: z.boolean().optional(), + bssTransition: z.boolean().optional(), + uapsdEnabled: z.boolean().optional(), + groupRekey: z.number().optional(), + dtimMode: z.enum(["default", "custom"]).optional(), + dtimNg: z.number().optional(), + dtimNa: z.number().optional(), + minrateNgEnabled: z.boolean().optional(), + minrateNaEnabled: z.boolean().optional(), + radiusDasEnabled: z.boolean().optional(), + radiusMacAuthEnabled: z.boolean().optional(), + pmfMode: z.enum(["disabled", "optional", "required"]).optional(), + band: z.enum(["both", "2g", "5g"]).optional(), + usergroupId: z.string().optional(), + proxyArp: z.boolean().optional(), + mcastenhanceEnabled: z.boolean().optional(), + macFilterList: z.array(z.string()).optional(), + no2ghzOui: z.boolean().optional(), + apGroupIds: z.array(z.string()).optional(), + apGroupMode: z.enum(["all", "groups", "devices"]).optional(), + deviceMacs: z.array(z.string()).optional(), + }) + .strict(); + + const parsed = schema.parse(body); + + // If deviceMacs is provided, manage the devices_ap_group: + // - If already in devices mode with a for_wlanconf group, update that group + // - Otherwise create a new for_wlanconf group and switch to devices mode + if (parsed.deviceMacs && parsed.deviceMacs.length > 0) { + const wlans = await unifiSites.getWlanConf(site.siteId); + const currentWlan = wlans.find((w) => w.id === wlanId); + + let existingGroupId: string | undefined; + if ( + currentWlan?.apGroupMode === "devices" && + currentWlan.apGroupIds.length > 0 + ) { + // Check if the current group is a for_wlanconf group we can update + const apGroups = await unifiSites.getApGroups(site.siteId); + const currentGroup = apGroups.find( + (g) => g.id === currentWlan.apGroupIds[0] && g.forWlanconf, + ); + if (currentGroup) existingGroupId = currentGroup.id; + } + + if (existingGroupId) { + // Update the existing for_wlanconf group's device list + await unifiSites.updateApGroup( + site.siteId, + existingGroupId, + parsed.deviceMacs, + ); + parsed.apGroupMode = "devices"; + parsed.apGroupIds = [existingGroupId]; + } else { + // Create a new for_wlanconf group + const apGroup = await unifiSites.createApGroup( + site.siteId, + "devices_ap_group", + parsed.deviceMacs, + true, // for_wlanconf must be true for devices mode + ); + parsed.apGroupMode = "devices"; + parsed.apGroupIds = [apGroup.id]; + } + } + + // Remove deviceMacs before converting — it's not a UniFi field + const { deviceMacs: _, ...updateInput } = parsed; + const updates = toWlanConfUpdate(updateInput); + + const result = await unifiSites.updateWlanConf( + site.siteId, + wlanId, + updates, + ); + const response = apiResponse.successful( + "UniFi WiFi Network Updated Successfully!", + result, + ); + return c.json(response, response.status as ContentfulStatusCode); + }, + authMiddleware({ + permissions: ["unifi.access", "unifi.site.wifi", "unifi.site.wifi.update"], + }), +); diff --git a/src/api/unifi/site/wifiLimits.ts b/src/api/unifi/site/wifiLimits.ts new file mode 100644 index 0000000..50f1c64 --- /dev/null +++ b/src/api/unifi/site/wifiLimits.ts @@ -0,0 +1,23 @@ +import { createRoute } from "../../../modules/api-utils/createRoute"; +import { unifiSites } from "../../../managers/unifiSites"; +import { apiResponse } from "../../../modules/api-utils/apiResponse"; +import { ContentfulStatusCode } from "hono/utils/http-status"; +import { authMiddleware } from "../../middleware/authorization"; + +/* GET /v1/unifi/site/:id/wifi-limits */ +export default createRoute( + "get", + ["/site/:id/wifi-limits"], + async (c) => { + const site = await unifiSites.fetch(c.req.param("id")); + const limits = await unifiSites.getWifiLimits(site.siteId); + const response = apiResponse.successful( + "UniFi WiFi Limits Fetched Successfully!", + limits, + ); + return c.json(response, response.status as ContentfulStatusCode); + }, + authMiddleware({ + permissions: ["unifi.access", "unifi.site.wifi-limits"], + }), +); diff --git a/src/api/unifi/site/wlanGroups.ts b/src/api/unifi/site/wlanGroups.ts new file mode 100644 index 0000000..af5254d --- /dev/null +++ b/src/api/unifi/site/wlanGroups.ts @@ -0,0 +1,23 @@ +import { createRoute } from "../../../modules/api-utils/createRoute"; +import { unifiSites } from "../../../managers/unifiSites"; +import { apiResponse } from "../../../modules/api-utils/apiResponse"; +import { ContentfulStatusCode } from "hono/utils/http-status"; +import { authMiddleware } from "../../middleware/authorization"; + +/* GET /v1/unifi/site/:id/wlan-groups */ +export default createRoute( + "get", + ["/site/:id/wlan-groups"], + async (c) => { + const site = await unifiSites.fetch(c.req.param("id")); + const wlanGroups = await unifiSites.getWlanGroups(site.siteId); + const response = apiResponse.successful( + "UniFi WLAN Groups Fetched Successfully!", + wlanGroups, + ); + return c.json(response, response.status as ContentfulStatusCode); + }, + authMiddleware({ + permissions: ["unifi.access", "unifi.site.wlan-groups"], + }), +); diff --git a/src/api/unifi/site/wlanGroupsCreate.ts b/src/api/unifi/site/wlanGroupsCreate.ts new file mode 100644 index 0000000..10ffeb6 --- /dev/null +++ b/src/api/unifi/site/wlanGroupsCreate.ts @@ -0,0 +1,38 @@ +import { createRoute } from "../../../modules/api-utils/createRoute"; +import { unifiSites } from "../../../managers/unifiSites"; +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/unifi/site/:id/wlan-groups */ +export default createRoute( + "post", + ["/site/:id/wlan-groups"], + async (c) => { + const site = await unifiSites.fetch(c.req.param("id")); + + const body = await c.req.json(); + const schema = z + .object({ + name: z.string().min(1, "Name is required"), + }) + .strict(); + + const parsed = schema.parse(body); + const group = await unifiSites.createWlanGroup(site.siteId, parsed); + + const response = apiResponse.created( + "UniFi WLAN Group Created Successfully!", + group, + ); + return c.json(response, response.status as ContentfulStatusCode); + }, + authMiddleware({ + permissions: [ + "unifi.access", + "unifi.site.wlan-groups", + "unifi.site.wlan-groups.create", + ], + }), +); diff --git a/src/api/unifi/sites/create.ts b/src/api/unifi/sites/create.ts new file mode 100644 index 0000000..d5a09ca --- /dev/null +++ b/src/api/unifi/sites/create.ts @@ -0,0 +1,25 @@ +import { createRoute } from "../../../modules/api-utils/createRoute"; +import { unifiSites } from "../../../managers/unifiSites"; +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/unifi/sites/create */ +export default createRoute( + "post", + ["/sites/create"], + async (c) => { + const body = await c.req.json(); + const schema = z.object({ description: z.string().min(1) }).strict(); + const { description } = schema.parse(body); + + const site = await unifiSites.createSite(description); + const response = apiResponse.successful( + "UniFi Site Created Successfully!", + site, + ); + return c.json(response, response.status as ContentfulStatusCode); + }, + authMiddleware({ permissions: ["unifi.access", "unifi.sites.create"] }), +); diff --git a/src/api/unifi/sites/fetchAll.ts b/src/api/unifi/sites/fetchAll.ts new file mode 100644 index 0000000..de36729 --- /dev/null +++ b/src/api/unifi/sites/fetchAll.ts @@ -0,0 +1,20 @@ +import { createRoute } from "../../../modules/api-utils/createRoute"; +import { unifiSites } from "../../../managers/unifiSites"; +import { apiResponse } from "../../../modules/api-utils/apiResponse"; +import { ContentfulStatusCode } from "hono/utils/http-status"; +import { authMiddleware } from "../../middleware/authorization"; + +/* GET /v1/unifi/sites */ +export default createRoute( + "get", + ["/sites"], + async (c) => { + const sites = await unifiSites.fetchAll(); + const response = apiResponse.successful( + "UniFi Sites Fetched Successfully!", + sites, + ); + return c.json(response, response.status as ContentfulStatusCode); + }, + authMiddleware({ permissions: ["unifi.access", "unifi.sites.fetch.many"] }), +); diff --git a/src/api/unifi/sites/sync.ts b/src/api/unifi/sites/sync.ts new file mode 100644 index 0000000..2f641cb --- /dev/null +++ b/src/api/unifi/sites/sync.ts @@ -0,0 +1,20 @@ +import { createRoute } from "../../../modules/api-utils/createRoute"; +import { unifiSites } from "../../../managers/unifiSites"; +import { apiResponse } from "../../../modules/api-utils/apiResponse"; +import { ContentfulStatusCode } from "hono/utils/http-status"; +import { authMiddleware } from "../../middleware/authorization"; + +/* POST /v1/unifi/sites/sync */ +export default createRoute( + "post", + ["/sites/sync"], + async (c) => { + const sites = await unifiSites.syncSites(); + const response = apiResponse.successful( + "UniFi Sites Synced Successfully!", + sites, + ); + return c.json(response, response.status as ContentfulStatusCode); + }, + authMiddleware({ permissions: ["unifi.access", "unifi.sites.sync"] }), +); diff --git a/src/constants.ts b/src/constants.ts index 5133001..a519e9f 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -5,6 +5,7 @@ import * as msal from "@azure/msal-node"; import { Server } from "socket.io"; import { Server as Engine } from "@socket.io/bun-engine"; import axios from "axios"; +import { UnifiClient } from "./modules/unifi-api/UnifiClient"; const connectionString = `${process.env.DATABASE_URL}`; const adapter = new PrismaPg({ connectionString }); @@ -67,3 +68,13 @@ const connectWiseApi = axios.create({ }); export { connectWiseApi }; + +// Unifi API Constants + +export const unifiControllerBaseUrl = + process.env.UNIFI_CONTROLLER_BASE_URL || "https://unifi.example.com"; +export const unifiSite = process.env.UNIFI_SITE || "default"; +export const unifiUsername = process.env.UNIFI_USERNAME || "admin"; +export const unifiPassword = process.env.UNIFI_PASSWORD || ""; + +export const unifi = new UnifiClient(unifiControllerBaseUrl); diff --git a/src/controllers/CompanyController.ts b/src/controllers/CompanyController.ts index 4eb6890..d1cd2ba 100644 --- a/src/controllers/CompanyController.ts +++ b/src/controllers/CompanyController.ts @@ -2,7 +2,7 @@ import { Company } from "../../generated/prisma/client"; import { fetchCwCompanyById } from "../modules/cw-utils/fetchCompany"; import { fetchCompanyConfigurations } from "../modules/cw-utils/configurations/fetchCompanyConfigurations"; import { updateCwInternalCompany } from "../modules/cw-utils/updateCompany"; -import { Company as CWCompany } from "../types/ConnectWiseTypes"; +import { Company as CWCompany, Contact } from "../types/ConnectWiseTypes"; /** * Company Controller @@ -16,9 +16,13 @@ export class CompanyController { public name: string; public readonly cw_Identifier: string; public readonly cw_CompanyId: number; - public readonly cw_Data?: CWCompany; + public readonly cw_Data?: { + company: CWCompany; + defaultContact: Contact; + allContacts: Contact[]; + }; - constructor(companyData: Company, cwData?: CWCompany) { + constructor(companyData: Company, cwData?: typeof this.cw_Data) { this.id = companyData.id; this.name = companyData.name; this.cw_Identifier = companyData.cw_Identifier; @@ -67,23 +71,66 @@ export class CompanyController { return data; } - public toJson(opts?: { includeAddress: boolean }) { + public toJson(opts?: { + includeAddress: boolean; + includePrimaryContact: boolean; + includeAllContacts?: 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 - ? this.cw_Data.country.name - : "United States", - }, + address: !opts?.includeAddress + ? undefined + : { + line1: this.cw_Data?.company.addressLine1, + line2: this.cw_Data?.company.addressLine2 ?? null, + city: this.cw_Data?.company.city, + state: this.cw_Data?.company.state, + zip: this.cw_Data?.company.zip, + country: this.cw_Data?.company.country + ? this.cw_Data.company.country.name + : "United States", + }, + primaryContact: !opts?.includePrimaryContact + ? undefined + : { + firstName: this.cw_Data?.defaultContact.firstName, + lastName: this.cw_Data?.defaultContact.lastName, + cwId: this.cw_Data?.defaultContact.id, + inactive: this.cw_Data?.defaultContact.inactiveFlag, + title: this.cw_Data?.defaultContact.title, + phone: this.cw_Data?.defaultContact.defaultPhoneNbr, + email: (() => { + if (!this.cw_Data?.defaultContact.communicationItems) + return null; + return ( + this.cw_Data?.defaultContact.communicationItems.find( + (v) => v.type.name === "Email", + )?.value ?? null + ); + })(), + }, + allContacts: !opts?.includeAllContacts + ? undefined + : this.cw_Data?.allContacts.map((contact) => ({ + firstName: contact.firstName, + lastName: contact.lastName, + cwId: contact.id, + inactive: contact.inactiveFlag, + title: contact.title, + phone: contact.defaultPhoneNbr, + email: (() => { + if (!contact.communicationItems) return null; + return ( + contact.communicationItems.find( + (v) => v.type.name === "Email", + )?.value ?? null + ); + })(), + })), }, }; } diff --git a/src/controllers/CredentialController.ts b/src/controllers/CredentialController.ts index 5cc03fe..2552117 100644 --- a/src/controllers/CredentialController.ts +++ b/src/controllers/CredentialController.ts @@ -28,11 +28,13 @@ export class CredentialController { public notes: string | null; public readonly typeId: string; public readonly companyId: string; + public readonly subCredentialOfId: string | null; public fields: any; private _type: CredentialType; private _company: Company; private _secureValues: SecureValue[]; + private _subCredentials: CredentialController[]; public readonly createdAt: Date; public updatedAt: Date; @@ -42,6 +44,11 @@ export class CredentialController { type: CredentialType; company: Company; securevalues: SecureValue[]; + subCredentials?: (Credential & { + type: CredentialType; + company: Company; + securevalues: SecureValue[]; + })[]; }, ) { this.id = credentialData.id; @@ -49,13 +56,69 @@ export class CredentialController { this.notes = credentialData.notes; this.typeId = credentialData.typeId; this.companyId = credentialData.companyId; + this.subCredentialOfId = credentialData.subCredentialOfId; this._type = credentialData.type; this._company = credentialData.company; this._secureValues = credentialData.securevalues; - this.fields = (() => { - let fields = credentialData.fields as Record; + this._subCredentials = (credentialData.subCredentials ?? []).map( + (sc) => new CredentialController(sc), + ); + this.fields = this._buildFields(credentialData); + this.createdAt = credentialData.createdAt; + this.updatedAt = credentialData.updatedAt; + } - return (this._type.fields! as any).map((f: any) => ({ + /** + * Build Fields + * + * Maps raw credential data into a structured fields array. + * - Regular credentials: maps through the type's field definitions. + * - Multi-credential fields: returns sub-credential references and subField definitions. + * - Sub-credentials: returns raw field data (validated against subFields, not the type's top-level fields). + */ + private _buildFields(credentialData: Credential) { + const raw = credentialData.fields as Record; + const typeFields = this._type.fields as any as CredentialTypeField[]; + + // Sub-credentials: their fields don't match the type's top-level definitions, + // so we return a simple id/value list built from raw JSON + secure values. + if (credentialData.subCredentialOfId) { + const result: any[] = []; + + // Collect field IDs that have secure values + const secureFieldIds = new Set(this._secureValues.map((sv) => sv.name)); + + // Non-secure fields from JSON + Object.entries(raw).forEach(([fieldId, value]) => { + if (!secureFieldIds.has(fieldId)) { + result.push({ id: fieldId, value, secure: false }); + } + }); + + // Secure value references + this._secureValues.forEach((sv) => { + result.push({ id: sv.name, value: `secure-${sv.id}`, secure: true }); + }); + + return result; + } + + // Regular (parent) credential: map through type field definitions + return typeFields.map((f: any) => { + if (f.valueType === ValueType.MULTI_CREDENTIAL) { + const subCredIds: string[] = raw[f.id] ?? []; + return { + id: f.id, + name: f.name, + secure: false, + required: f.required, + valueType: f.valueType, + subFields: f.subFields ?? [], + subCredentialIds: subCredIds, + }; + } + + return { id: f.id, name: f.name, secure: f.secure, @@ -63,13 +126,9 @@ export class CredentialController { valueType: f.valueType as ValueType, value: f.secure ? `secure-${this._secureValues.find((sv) => sv.name === f.id)?.id}` - : fields[f.id], - })); - - return fields; - })(); - this.createdAt = credentialData.createdAt; - this.updatedAt = credentialData.updatedAt; + : raw[f.id], + }; + }); } /** @@ -151,6 +210,19 @@ export class CredentialController { fieldsObject[field.fieldId] = field.value; }); + // Preserve multi-credential field values (sub-credential ID arrays) + const currentFields = (await prisma.credential.findFirst({ + where: { id: this.id }, + select: { fields: true }, + }))!.fields as Record; + + const typeFields = this._type.fields as any as CredentialTypeField[]; + typeFields.forEach((f) => { + if (f.valueType === ValueType.MULTI_CREDENTIAL && currentFields[f.id]) { + fieldsObject[f.id] = currentFields[f.id]; + } + }); + // Update the credential with non-secure fields const updatedCredential = await prisma.credential.update({ where: { id: this.id }, @@ -313,13 +385,14 @@ export class CredentialController { * @param opts - Options to change the output * @returns - An object that is JSON friendly */ - toJson(opts?: { includeSecureValues?: boolean }) { + toJson(opts?: { includeSecureValues?: boolean }): Record { return { id: this.id, name: this.name, notes: this.notes, typeId: this.typeId, companyId: this.companyId, + subCredentialOfId: this.subCredentialOfId ?? undefined, fields: this.fields, type: { id: this._type.id, @@ -331,6 +404,10 @@ export class CredentialController { id: this._company.id, name: this._company.name, }, + subCredentials: + this._subCredentials.length > 0 + ? this._subCredentials.map((sc) => sc.toJson(opts)) + : undefined, secureFieldIds: opts?.includeSecureValues ? this._secureValues.map((sv) => sv.name) : undefined, diff --git a/src/controllers/UnifiSiteController.ts b/src/controllers/UnifiSiteController.ts new file mode 100644 index 0000000..a3aa901 --- /dev/null +++ b/src/controllers/UnifiSiteController.ts @@ -0,0 +1,29 @@ +import { UnifiSite } from "../../generated/prisma/client"; + +/** + * UniFi Site Controller + * + * Handles formatting and presentation of UniFi site data. + */ +export class UnifiSiteController { + public readonly id: string; + public readonly name: string; + public readonly siteId: string; + public readonly companyId: string | null; + + constructor(site: UnifiSite) { + this.id = site.id; + this.name = site.name; + this.siteId = site.siteId; + this.companyId = site.companyId; + } + + public toJson() { + return { + id: this.id, + name: this.name, + siteId: this.siteId, + companyId: this.companyId, + }; + } +} diff --git a/src/index.ts b/src/index.ts index ed19a55..06489bd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,7 @@ import { refresh } from "./api/auth"; import app from "./api/server"; import { engine, PORT } from "./constants"; +import { unifiSites } from "./managers/unifiSites"; import { refreshCompanies } from "./modules/cw-utils/refreshCompanies"; import { events, setupEventDebugger } from "./modules/globalEvents"; @@ -13,6 +14,11 @@ setInterval(() => { return refreshCompanies(); }, 60 * 1000); +await unifiSites.syncSites(); +setInterval(() => { + return unifiSites.syncSites(); +}, 60 * 1000); + Bun.serve({ port: PORT, websocket: engine.handler().websocket, diff --git a/src/managers/companies.ts b/src/managers/companies.ts index 39c0a9f..56032b4 100644 --- a/src/managers/companies.ts +++ b/src/managers/companies.ts @@ -12,10 +12,21 @@ export const companies = { if (!search) throw new Error("Unknown company."); - const freshCwData = await connectWiseApi.get( + const freshCwData: { data: Company } = await connectWiseApi.get( `/company/companies/${search.cw_CompanyId}`, ); - return new CompanyController(search, freshCwData.data); + const defaultContactData = await connectWiseApi.get( + (freshCwData.data as Company).defaultContact._info.contact_href, + ); + const allContactsData = await connectWiseApi.get( + `${freshCwData.data._info.contacts_href}&pageSize=1000`, + ); + + return new CompanyController(search, { + company: freshCwData.data, + defaultContact: defaultContactData.data, + allContacts: allContactsData.data, + }); }, async count() { diff --git a/src/managers/credentialTypes.ts b/src/managers/credentialTypes.ts index 4e9aa7e..14f0ae0 100644 --- a/src/managers/credentialTypes.ts +++ b/src/managers/credentialTypes.ts @@ -79,8 +79,6 @@ export const credentialTypes = { }); } - console.log(data.fields); - const credentialType = await prisma.credentialType.create({ data: { name: data.name, diff --git a/src/managers/credentials.ts b/src/managers/credentials.ts index 4c32902..05cbabd 100644 --- a/src/managers/credentials.ts +++ b/src/managers/credentials.ts @@ -4,10 +4,28 @@ import { fieldValidator } from "../modules/credentials/fieldValidator"; import { CredentialField, CredentialTypeField, + ValueType, } from "../modules/credentials/credentialTypeDefs"; import { generateSecureValue } from "../modules/credentials/generateSecureValue"; import GenericError from "../Errors/GenericError"; +/** + * Standard include clause used by every credential query. + * Includes the credential type, company, secure values, and one level of sub-credentials. + */ +const credentialInclude = { + type: true, + company: true, + securevalues: true, + subCredentials: { + include: { + type: true, + company: true, + securevalues: true, + }, + }, +} as const; + export const credentials = { /** * Fetch Credential @@ -20,11 +38,7 @@ export const credentials = { async fetch(id: string): Promise { const credential = await prisma.credential.findFirst({ where: { id }, - include: { - type: true, - company: true, - securevalues: true, - }, + include: credentialInclude, }); if (!credential) { @@ -42,19 +56,17 @@ export const credentials = { /** * Fetch Credentials by Company * - * Fetch all credentials associated with a specific company. + * Fetch all top-level credentials associated with a specific company. + * Sub-credentials are excluded from the top-level list and instead + * included nested under their parent credential. * * @param companyId - The company ID to fetch credentials for * @returns {Promise} - Array of credential controllers */ async fetchByCompany(companyId: string): Promise { const credentialsList = await prisma.credential.findMany({ - where: { companyId }, - include: { - type: true, - company: true, - securevalues: true, - }, + where: { companyId, subCredentialOfId: null }, + include: credentialInclude, }); return credentialsList.map((cred) => new CredentialController(cred)); @@ -68,6 +80,10 @@ export const credentials = { * the credential type, encrypting secure fields, and inserting everything * into the database atomically. * + * When the credential type contains multi-credential fields, pass + * `subCredentials` keyed by the multi-credential field ID. Each entry + * is an array of sub-credential objects with their own name and fields. + * * @param data - The credential data to create * @returns {Promise} - The created credential controller */ @@ -80,6 +96,10 @@ export const credentials = { fieldId: string; value: string; }[]; + subCredentials?: Record< + string, + { name: string; fields: { fieldId: string; value: string }[] }[] + >; }): Promise { // Fetch the credential type to get acceptable fields const credentialType = await prisma.credentialType.findFirst({ @@ -95,22 +115,31 @@ export const credentials = { }); } - // Validate the fields against acceptable fields - const acceptableFields = ( - credentialType.fields! as any as CredentialTypeField[] - ).map((f: { id: string; name: string; secure: boolean }) => ({ + const typeFields = credentialType.fields! as any as CredentialTypeField[]; + + // Validate the fields against acceptable fields (exclude multi-credential fields + // from value validation since they don't carry a direct value). + const acceptableFields = typeFields.map((f) => ({ id: f.id, name: f.name, secure: f.secure, + required: f.required, + valueType: f.valueType, + subFields: f.subFields, })) as CredentialTypeField[]; + const validatedFields = await fieldValidator( data.fields as any as CredentialField[], acceptableFields, ); - // Separate secure and non-secure fields - const secureFields = validatedFields.filter((f) => f.secure); - const nonSecureFields = validatedFields.filter((f) => !f.secure); + // Separate secure, non-secure, and multi-credential fields + const secureFields = validatedFields.filter( + (f) => f.secure && !f.isMultiCredential, + ); + const nonSecureFields = validatedFields.filter( + (f) => !f.secure && !f.isMultiCredential, + ); // Build fields object for non-secure fields const fieldsObject: Record = {}; @@ -118,6 +147,13 @@ export const credentials = { fieldsObject[field.fieldId] = field.value; }); + // Initialise multi-credential field slots with empty arrays + typeFields + .filter((f) => f.valueType === ValueType.MULTI_CREDENTIAL) + .forEach((f) => { + fieldsObject[f.id] = []; + }); + // Encrypt secure field values const secureValueData = secureFields.map((field) => { const { encrypted, hash } = generateSecureValue(field.value); @@ -128,7 +164,7 @@ export const credentials = { }; }); - // Create credential and all secure values in a transaction + // Create the parent credential first const credential = await prisma.credential.create({ data: { name: data.name, @@ -140,20 +176,246 @@ export const credentials = { create: secureValueData, }, }, - include: { - type: true, - company: true, - securevalues: true, - }, }); - return new CredentialController(credential); + // Create inline sub-credentials when provided + if (data.subCredentials) { + for (const [fieldId, subCredDataList] of Object.entries( + data.subCredentials, + )) { + const fieldDef = typeFields.find((f) => f.id === fieldId); + + if (!fieldDef || fieldDef.valueType !== ValueType.MULTI_CREDENTIAL) { + throw new GenericError({ + message: `Field '${fieldId}' is not a multi-credential field`, + name: "InvalidMultiCredentialField", + cause: `Cannot create sub-credentials for field '${fieldId}' because it is not a multi-credential field.`, + status: 400, + }); + } + + const subFieldDefs = (fieldDef.subFields ?? + []) as CredentialTypeField[]; + const subCredIds: string[] = []; + + for (const subCredData of subCredDataList) { + const validatedSubFields = await fieldValidator( + subCredData.fields as any as CredentialField[], + subFieldDefs, + ); + + const subSecure = validatedSubFields.filter((f) => f.secure); + const subNonSecure = validatedSubFields.filter((f) => !f.secure); + + const subFieldsObject: Record = {}; + subNonSecure.forEach((f) => { + subFieldsObject[f.fieldId] = f.value; + }); + + const subSecureValueData = subSecure.map((f) => { + const { encrypted, hash } = generateSecureValue(f.value); + return { name: f.fieldId, content: encrypted, hash }; + }); + + const subCred = await prisma.credential.create({ + data: { + name: subCredData.name, + typeId: data.typeId, + companyId: data.companyId, + subCredentialOfId: credential.id, + fields: subFieldsObject, + securevalues: { create: subSecureValueData }, + }, + }); + + subCredIds.push(subCred.id); + } + + fieldsObject[fieldId] = subCredIds; + } + + // Persist the sub-credential ID arrays on the parent + await prisma.credential.update({ + where: { id: credential.id }, + data: { fields: fieldsObject }, + }); + } + + // Re-fetch with full includes + const completeCredential = await prisma.credential.findFirst({ + where: { id: credential.id }, + include: credentialInclude, + }); + + return new CredentialController(completeCredential!); + }, + + /** + * Add Sub-Credential + * + * Create a new sub-credential under an existing parent credential + * for a specific multi-credential field. + * + * @param parentId - The parent credential ID + * @param fieldId - The multi-credential field this sub-credential belongs to + * @param data - The sub-credential data (name and fields) + * @returns {Promise} - The created sub-credential controller + */ + async addSubCredential( + parentId: string, + fieldId: string, + data: { + name: string; + fields: { fieldId: string; value: string }[]; + }, + ): Promise { + const parent = await prisma.credential.findFirst({ + where: { id: parentId }, + include: { type: true }, + }); + + if (!parent) { + throw new GenericError({ + message: "Parent credential not found", + name: "CredentialNotFound", + cause: `No credential exists with ID '${parentId}'`, + status: 404, + }); + } + + const typeFields = parent.type.fields as any as CredentialTypeField[]; + const fieldDef = typeFields.find((f) => f.id === fieldId); + + if (!fieldDef || fieldDef.valueType !== ValueType.MULTI_CREDENTIAL) { + throw new GenericError({ + message: `Field '${fieldId}' is not a multi-credential field`, + name: "InvalidMultiCredentialField", + cause: `Cannot create sub-credentials for field '${fieldId}' because it is not a multi-credential field.`, + status: 400, + }); + } + + const subFieldDefs = (fieldDef.subFields ?? []) as CredentialTypeField[]; + const validatedFields = await fieldValidator( + data.fields as any as CredentialField[], + subFieldDefs, + ); + + const secureFields = validatedFields.filter((f) => f.secure); + const nonSecureFields = validatedFields.filter((f) => !f.secure); + + const subFieldsObject: Record = {}; + nonSecureFields.forEach((f) => { + subFieldsObject[f.fieldId] = f.value; + }); + + const secureValueData = secureFields.map((f) => { + const { encrypted, hash } = generateSecureValue(f.value); + return { name: f.fieldId, content: encrypted, hash }; + }); + + const subCredential = await prisma.credential.create({ + data: { + name: data.name, + typeId: parent.typeId, + companyId: parent.companyId, + subCredentialOfId: parentId, + fields: subFieldsObject, + securevalues: { create: secureValueData }, + }, + include: credentialInclude, + }); + + // Update parent's fields JSON to include the new sub-credential ID + const parentFields = parent.fields as Record; + const subCredIds: string[] = parentFields[fieldId] ?? []; + subCredIds.push(subCredential.id); + parentFields[fieldId] = subCredIds; + + await prisma.credential.update({ + where: { id: parentId }, + data: { fields: parentFields }, + }); + + return new CredentialController(subCredential); + }, + + /** + * Remove Sub-Credential + * + * Delete a sub-credential and remove its reference from the parent credential's + * multi-credential field. + * + * @param parentId - The parent credential ID + * @param subCredentialId - The sub-credential ID to remove + * @returns {Promise} + */ + async removeSubCredential( + parentId: string, + subCredentialId: string, + ): Promise { + const subCredential = await prisma.credential.findFirst({ + where: { id: subCredentialId, subCredentialOfId: parentId }, + }); + + if (!subCredential) { + throw new GenericError({ + message: "Sub-credential not found", + name: "SubCredentialNotFound", + cause: `No sub-credential with ID '${subCredentialId}' exists under credential '${parentId}'`, + status: 404, + }); + } + + // Delete the sub-credential (cascade removes its secure values) + await prisma.credential.delete({ + where: { id: subCredentialId }, + }); + + // Remove the sub-credential ID from the parent's fields JSON + const parent = await prisma.credential.findFirst({ + where: { id: parentId }, + }); + + if (parent) { + const parentFields = parent.fields as Record; + for (const key of Object.keys(parentFields)) { + if (Array.isArray(parentFields[key])) { + parentFields[key] = parentFields[key].filter( + (id: string) => id !== subCredentialId, + ); + } + } + + await prisma.credential.update({ + where: { id: parentId }, + data: { fields: parentFields }, + }); + } + }, + + /** + * Fetch Sub-Credentials + * + * Fetch all sub-credentials that belong to a specific parent credential. + * + * @param parentId - The parent credential ID + * @returns {Promise} - Array of sub-credential controllers + */ + async fetchSubCredentials(parentId: string): Promise { + const subCredentials = await prisma.credential.findMany({ + where: { subCredentialOfId: parentId }, + include: credentialInclude, + }); + + return subCredentials.map((sc) => new CredentialController(sc)); }, /** * Delete Credential * * Delete a credential by its ID. + * Sub-credentials are cascade-deleted automatically by the database. * * @param id - The credential ID to delete * @returns {Promise} diff --git a/src/managers/unifiSites.ts b/src/managers/unifiSites.ts new file mode 100644 index 0000000..7d283d5 --- /dev/null +++ b/src/managers/unifiSites.ts @@ -0,0 +1,293 @@ +import { prisma, unifi, unifiUsername, unifiPassword } from "../constants"; +import GenericError from "../Errors/GenericError"; +import { UnifiSite } from "../../generated/prisma/client"; + +let loggedIn = false; + +async function ensureLoggedIn(): Promise { + if (loggedIn) return; + if (!unifiPassword) + throw new GenericError({ + name: "UnifiConfigError", + message: "UniFi controller credentials are not configured.", + status: 503, + }); + await unifi.login(unifiUsername, unifiPassword); + loggedIn = true; +} + +export const unifiSites = { + /** + * Fetch a UniFi site record from the database by its internal ID. + */ + async fetch(id: string): Promise { + const site = await prisma.unifiSite.findFirst({ + where: { id }, + }); + + if (!site) + throw new GenericError({ + name: "UnifiSiteNotFound", + message: `UniFi site with id '${id}' was not found.`, + status: 404, + }); + + return site; + }, + + /** + * Fetch all UniFi site records from the database. + */ + async fetchAll(): Promise { + return prisma.unifiSite.findMany({ + include: { company: true }, + }); + }, + + /** + * Fetch all UniFi site records linked to a specific company. + */ + async fetchByCompany(companyId: string): Promise { + return prisma.unifiSite.findMany({ + where: { companyId }, + }); + }, + + /** + * Link a UniFi site to a company. + */ + async linkToCompany(siteId: string, companyId: string): Promise { + const site = await prisma.unifiSite.findFirst({ where: { id: siteId } }); + if (!site) + throw new GenericError({ + name: "UnifiSiteNotFound", + message: `UniFi site '${siteId}' was not found.`, + status: 404, + }); + + const company = await prisma.company.findFirst({ + where: { id: companyId }, + }); + if (!company) + throw new GenericError({ + name: "CompanyNotFound", + message: `Company '${companyId}' was not found.`, + status: 404, + }); + + return prisma.unifiSite.update({ + where: { id: siteId }, + data: { companyId }, + }); + }, + + /** + * Unlink a UniFi site from its company. + */ + async unlinkFromCompany(siteId: string): Promise { + return prisma.unifiSite.update({ + where: { id: siteId }, + data: { companyId: null }, + }); + }, + + /** + * Sync all sites from the UniFi controller into the database. + * Creates new records for sites not yet tracked, updates names for existing ones. + */ + async syncSites(): Promise { + await ensureLoggedIn(); + + // Fetch all sites from the controller + const allSites = await unifi.getAllSites(); + + const results: UnifiSite[] = []; + + for (const site of allSites) { + const existing = await prisma.unifiSite.findFirst({ + where: { siteId: site.name }, + }); + + if (existing) { + const updated = await prisma.unifiSite.update({ + where: { id: existing.id }, + data: { name: site.description }, + }); + results.push(updated); + } else { + const created = await prisma.unifiSite.create({ + data: { + name: site.description, + siteId: site.name, + }, + }); + results.push(created); + } + } + + return results; + }, + + /** + * Get live site overview from the UniFi controller. + */ + async getSiteOverview(siteId: string) { + await ensureLoggedIn(); + return unifi.getSiteOverview(siteId); + }, + + /** + * Get live devices from the UniFi controller for a site. + */ + async getDevices(siteId: string) { + await ensureLoggedIn(); + return unifi.getDevices(siteId); + }, + + /** + * Get live WiFi networks (WLANs) from the UniFi controller for a site. + */ + async getWlanConf(siteId: string) { + await ensureLoggedIn(); + return unifi.getWlanConf(siteId); + }, + + /** + * Update a WiFi network on the UniFi controller. + */ + async updateWlanConf( + siteId: string, + wlanId: string, + updates: Parameters[2], + ) { + await ensureLoggedIn(); + return unifi.updateWlanConf(siteId, wlanId, updates); + }, + + /** + * Get live network configurations from the UniFi controller for a site. + */ + async getNetworks(siteId: string) { + await ensureLoggedIn(); + return unifi.getNetworks(siteId); + }, + + /** + * Create a new site on the UniFi controller and track it in the database. + */ + async createSite(description: string): Promise { + await ensureLoggedIn(); + + const created = await unifi.createSite(description); + + return prisma.unifiSite.create({ + data: { + name: created.description, + siteId: created.name, + }, + }); + }, + + /** + * Get WLAN groups from the UniFi controller for a site. + */ + async getWlanGroups(siteId: string) { + await ensureLoggedIn(); + return unifi.getWlanGroups(siteId); + }, + + /** + * Create a new WLAN group (AP broadcasting group) on the UniFi controller. + */ + async createWlanGroup( + siteId: string, + input: Parameters[1], + ) { + await ensureLoggedIn(); + return unifi.createWlanGroup(siteId, input); + }, + + /** + * Get user groups (speed profiles) from the UniFi controller for a site. + */ + async getUserGroups(siteId: string) { + await ensureLoggedIn(); + return unifi.getUserGroups(siteId); + }, + + /** + * Create a new user group (speed profile) on the UniFi controller. + */ + async createUserGroup( + siteId: string, + input: Parameters[1], + ) { + await ensureLoggedIn(); + return unifi.createUserGroup(siteId, input); + }, + + /** + * Get AP groups from the UniFi controller for a site. + */ + async getApGroups(siteId: string) { + await ensureLoggedIn(); + return unifi.getApGroups(siteId); + }, + + /** + * Create a new AP group on the UniFi controller. + */ + async createApGroup( + siteId: string, + name: string, + deviceMacs: string[], + forWlanconf: boolean = false, + ) { + await ensureLoggedIn(); + return unifi.createApGroup(siteId, name, deviceMacs, forWlanconf); + }, + + /** + * Update an existing AP group's device MACs on the UniFi controller. + */ + async updateApGroup(siteId: string, groupId: string, deviceMacs: string[]) { + await ensureLoggedIn(); + return unifi.updateApGroup(siteId, groupId, deviceMacs); + }, + + /** + * Get access points only from the UniFi controller for a site. + */ + async getAccessPoints(siteId: string) { + await ensureLoggedIn(); + return unifi.getAccessPoints(siteId); + }, + + /** + * Get WiFi SSID limits per AP per radio band. + */ + async getWifiLimits(siteId: string) { + await ensureLoggedIn(); + return unifi.getWifiLimits(siteId); + }, + + /** + * Get private pre-shared keys for a specific WLAN. + */ + async getPrivatePSKs(siteId: string, wlanId: string) { + await ensureLoggedIn(); + return unifi.getPrivatePSKs(siteId, wlanId); + }, + + /** + * Create a private pre-shared key on a specific WLAN. + */ + async createPrivatePSK( + siteId: string, + wlanId: string, + psk: Parameters[2], + ) { + await ensureLoggedIn(); + return unifi.createPrivatePSK(siteId, wlanId, psk); + }, +}; diff --git a/src/modules/credentials/credentialTypeDefs.ts b/src/modules/credentials/credentialTypeDefs.ts index 4a7561e..3e66910 100644 --- a/src/modules/credentials/credentialTypeDefs.ts +++ b/src/modules/credentials/credentialTypeDefs.ts @@ -5,12 +5,14 @@ export enum ValueType { GENERIC_SECRET = "generic_secret", BITLOCKER_KEY = "bitlocker_key", PASSWORD = "password", + MULTI_CREDENTIAL = "multi_credential", } export interface CredentialTypeField { id: string; // I.e. "clientId", "clientSecret", etc. name: string; // I.e. "Client ID", "Client Secret", etc. required: boolean; + subFields?: CredentialTypeField[]; // For multi-credential fields, defines the sub-fields that are required secure: boolean; // Whether this field should be stored encrypted in the database valueType: ValueType; // For future extensibility, currently all fields are strings } @@ -18,4 +20,5 @@ export interface CredentialTypeField { export interface CredentialField { fieldId: string; // I.e. "clientId", "clientSecret", etc. value: string; // Encrypted value stored in the database + subCredentials?: string[]; // For multi-credential fields, the IDs of the sub-credentials that are associated with this field } diff --git a/src/modules/credentials/fieldValidator.ts b/src/modules/credentials/fieldValidator.ts index f910544..0200c6f 100644 --- a/src/modules/credentials/fieldValidator.ts +++ b/src/modules/credentials/fieldValidator.ts @@ -1,7 +1,19 @@ import { Collection } from "@discordjs/collection"; -import { CredentialField, CredentialTypeField } from "./credentialTypeDefs"; +import { + CredentialField, + CredentialTypeField, + ValueType, +} from "./credentialTypeDefs"; import GenericError from "../../Errors/GenericError"; +export interface ValidatedField { + fieldId: string; + value: string; + secure: boolean; + isMultiCredential?: boolean; + subCredentials?: string[]; +} + /** * Field Validator * @@ -11,19 +23,16 @@ import GenericError from "../../Errors/GenericError"; * If all the credentials pass, it will return a processed version of the submitted fields including fields that need to be * stored securely (encrypted) and fields that do not. * + * Multi-credential fields are handled specially — they don't carry a direct value but instead + * reference sub-credential IDs. + * * @param fields - The fields in object form that are being submitted. * @param acceptableFields - The acceptable field to be compared against. */ export const fieldValidator = async ( fields: CredentialField[], acceptableFields: CredentialTypeField[], -): Promise< - { - fieldId: string; - value: string; - secure: boolean; - }[] -> => { +): Promise => { const afCollection = new Collection(acceptableFields.map((f) => [f.id, f])); await Promise.all( @@ -45,6 +54,18 @@ export const fieldValidator = async ( return fields.map((field) => { const matchingField = afCollection.get(field.fieldId)!; + // Multi-credential fields don't carry a direct value; + // they reference sub-credential IDs instead. + if (matchingField.valueType === ValueType.MULTI_CREDENTIAL) { + return { + fieldId: field.fieldId, + value: "", + secure: false, + isMultiCredential: true, + subCredentials: field.subCredentials ?? [], + }; + } + return { fieldId: field.fieldId, value: field.value, diff --git a/src/modules/permission-utils/processObjectPermissions.ts b/src/modules/permission-utils/processObjectPermissions.ts new file mode 100644 index 0000000..62d192f --- /dev/null +++ b/src/modules/permission-utils/processObjectPermissions.ts @@ -0,0 +1,31 @@ +import UserController from "../../controllers/UserController"; + +export const processObjectValuePerms = async ( + obj: T, + scope: string, // e.g. "unifi.wifi.read" + user: UserController, +): Promise> => { + let result: Partial = {}; + + for (const key in obj) { + if (await user.hasPermission(`${scope}.${key}`)) { + result[key] = obj[key]; + } + } + + return result; +}; + +export const processObjectPermMap = async >( + obj: T, + scope: string, + user: UserController, +): Promise> => { + const result = {} as Record; + + for (const key in obj) { + result[key] = await user.hasPermission(`${scope}.${key}`); + } + + return result; +}; diff --git a/src/modules/unifi-api/UnifiClient.ts b/src/modules/unifi-api/UnifiClient.ts new file mode 100644 index 0000000..207a272 --- /dev/null +++ b/src/modules/unifi-api/UnifiClient.ts @@ -0,0 +1,952 @@ +import axios, { AxiosInstance } from "axios"; +import https from "https"; +import { + ApGroup, + ApRadioWifiUsage, + ApWifiLimits, + CreateSiteOptions, + Device, + DeviceRadio, + DeviceState, + DeviceUplink, + Network, + PrivatePSK, + PrivatePSKCreateInput, + SiteListItem, + SiteOverview, + SubsystemHealth, + SysInfo, + UserGroup, + UserGroupCreateInput, + WlanConf, + WlanConfRaw, + WlanConfUpdate, + WlanGroup, + WlanGroupCreateInput, +} from "./unifiTypes"; + +export class UnifiClient { + private client: AxiosInstance; + + constructor(baseURL: string) { + this.client = axios.create({ + baseURL, + validateStatus: (s) => s >= 200 && s < 400, + httpsAgent: new https.Agent({ rejectUnauthorized: false }), + }); + } + + private persistSession(res: { headers: Record }): void { + // Cookies + const raw = res.headers["set-cookie"]; + if (raw) { + const cookies = (Array.isArray(raw) ? raw : [raw]) as string[]; + const cookieString = cookies.map((c) => c.split(";")[0]).join("; "); + this.client.defaults.headers.common["Cookie"] = cookieString; + } + // CSRF token (UniFi OS) + const csrf = res.headers["x-csrf-token"]; + if (typeof csrf === "string") { + this.client.defaults.headers.common["X-CSRF-Token"] = csrf; + } + } + + async login(username: string, password: string): Promise { + const body = { username, password }; + + try { + // UniFi OS + const res = await this.client.post("/api/auth/login", body); + console.log("Login OK (UniFi OS)", res.status); + this.persistSession(res); + } catch (e) { + // Legacy controller + console.log("UniFi OS login failed, trying legacy..."); + const res = await this.client.post("/api/login", body); + console.log("Login OK (legacy)", res.status); + this.persistSession(res); + } + } + + private async fetchWlanConfRaw(site: string): Promise { + const paths = [ + `/proxy/network/api/s/${site}/rest/wlanconf`, + `/api/s/${site}/rest/wlanconf`, + ]; + + for (const path of paths) { + try { + const res = await this.client.get(path); + const data = (res.data?.data ?? res.data) as WlanConfRaw[]; + console.log(`Fetched wlan from ${path}`); + return data; + } catch (e) { + console.log( + `Failed ${path}:`, + axios.isAxiosError(e) ? e.response?.status : e, + ); + } + } + + throw new Error("Could not fetch WLAN config from any known path"); + } + + private static parseWlanConf(w: any): WlanConf { + return { + id: w._id, + name: (w.name || w.ssid || "").toString(), + siteId: w.site_id ?? "", + enabled: w.enabled ?? true, + security: w.security ?? "open", + wpaMode: w.wpa_mode ?? "", + wpaEnc: w.wpa_enc ?? "", + wpa3Support: w.wpa3_support ?? false, + wpa3Transition: w.wpa3_transition ?? false, + wpa3FastRoaming: w.wpa3_fast_roaming ?? false, + wpa3Enhanced192: w.wpa3_enhanced_192 ?? false, + passphrase: typeof w.x_passphrase === "string" ? w.x_passphrase : null, + passphraseAutogenerated: w.passphrase_autogenerated ?? false, + hideSSID: w.hide_ssid ?? false, + isGuest: w.is_guest ?? false, + band: w.wlan_band ?? "both", + bands: w.wlan_bands ?? [], + networkconfId: w.networkconf_id ?? "", + usergroupId: w.usergroup_id ?? "", + apGroupIds: w.ap_group_ids ?? [], + apGroupMode: w.ap_group_mode ?? "devices", + pmfMode: w.pmf_mode ?? "disabled", + groupRekey: w.group_rekey ?? 0, + dtimMode: w.dtim_mode ?? "default", + dtimNg: w.dtim_ng ?? 1, + dtimNa: w.dtim_na ?? 3, + dtim6e: w.dtim_6e ?? 3, + l2Isolation: w.l2_isolation ?? false, + fastRoamingEnabled: w.fast_roaming_enabled ?? false, + bssTransition: w.bss_transition ?? false, + uapsdEnabled: w.uapsd_enabled ?? false, + iappEnabled: w.iapp_enabled ?? false, + proxyArp: w.proxy_arp ?? false, + mcastenhanceEnabled: w.mcastenhance_enabled ?? false, + macFilterEnabled: w.mac_filter_enabled ?? false, + macFilterPolicy: w.mac_filter_policy ?? "allow", + macFilterList: w.mac_filter_list ?? [], + radiusDasEnabled: w.radius_das_enabled ?? false, + radiusMacAuthEnabled: w.radius_mac_auth_enabled ?? false, + radiusMacaclFormat: w.radius_macacl_format ?? "none_lower", + minrateSettingPreference: w.minrate_setting_preference ?? "auto", + minrateNgEnabled: w.minrate_ng_enabled ?? false, + minrateNgDataRateKbps: w.minrate_ng_data_rate_kbps ?? 1000, + minrateNgAdvertisingRates: w.minrate_ng_advertising_rates ?? false, + minrateNaEnabled: w.minrate_na_enabled ?? false, + minrateNaDataRateKbps: w.minrate_na_data_rate_kbps ?? 6000, + minrateNaAdvertisingRates: w.minrate_na_advertising_rates ?? false, + settingPreference: w.setting_preference ?? "auto", + no2ghzOui: w.no2ghz_oui ?? false, + privatePreSharedKeysEnabled: w.private_preshared_keys_enabled ?? false, + privatePreSharedKeys: w.private_preshared_keys ?? [], + saeGroups: w.sae_groups ?? [], + saePsk: w.sae_psk ?? [], + schedule: w.schedule ?? [], + scheduleWithDuration: w.schedule_with_duration ?? [], + bcFilterList: w.bc_filter_list ?? [], + externalId: w.external_id ?? null, + }; + } + + async getWlanConf(site: string): Promise { + const raw = await this.fetchWlanConfRaw(site); + return raw.map(UnifiClient.parseWlanConf); + } + + async updateWlanConf( + site: string, + wlanId: string, + updates: WlanConfUpdate, + ): Promise { + const paths = [ + `/proxy/network/api/s/${site}/rest/wlanconf/${wlanId}`, + `/api/s/${site}/rest/wlanconf/${wlanId}`, + ]; + + // Fetch current WLAN to check if a RADIUS profile is configured. + // The controller rejects RADIUS fields when no profile is set. + const currentWlans = await this.getWlanConf(site); + const currentWlan = currentWlans.find((w) => w.id === wlanId); + const hasRadius = + currentWlan?.security === "wpaeap" || updates.security === "wpaeap"; + + if (!hasRadius) { + delete updates.radius_das_enabled; + delete updates.radius_mac_auth_enabled; + } + + for (const path of paths) { + try { + const res = await this.client.put(path, updates); + const raw = (res.data?.data?.[0] ?? res.data) as any; + return UnifiClient.parseWlanConf(raw); + } catch (e) { + if (!axios.isAxiosError(e)) throw e; + // Try next path on 404/401, throw on other errors + if ( + e.response && + e.response.status !== 404 && + e.response.status !== 401 + ) { + throw new Error( + `Failed to update WLAN ${wlanId}: ${e.response.status} ${JSON.stringify(e.response.data)}`, + ); + } + } + } + + throw new Error("Could not update WLAN config from any known path"); + } + + async getAllSites(): Promise { + const paths = ["/proxy/network/api/self/sites", "/api/self/sites"]; + + for (const path of paths) { + try { + const res = await this.client.get(path); + const raw = (res.data?.data ?? res.data) as any[]; + return raw.map( + (s: any): SiteListItem => ({ + id: s._id, + name: s.name, + description: s.desc ?? "", + deviceCount: s.device_count ?? 0, + role: s.role ?? "", + }), + ); + } catch (e) { + if (!axios.isAxiosError(e)) throw e; + if ( + e.response && + e.response.status !== 404 && + e.response.status !== 401 + ) { + throw e; + } + } + } + + throw new Error("Could not fetch sites from any known path"); + } + + async getSiteOverview(site: string): Promise { + const prefixes = ["/proxy/network", ""]; + + for (const prefix of prefixes) { + try { + const [healthRes, sysInfoRes, sitesRes] = await Promise.all([ + this.client.get(`${prefix}/api/s/${site}/stat/health`), + this.client.get(`${prefix}/api/s/${site}/stat/sysinfo`), + this.client.get(`${prefix}/api/self/sites`), + ]); + + const healthRaw = (healthRes.data?.data ?? healthRes.data) as any[]; + const sysInfoRaw = (sysInfoRes.data?.data?.[0] ?? + sysInfoRes.data) as any; + const sitesRaw = (sitesRes.data?.data ?? sitesRes.data) as any[]; + + const siteRaw = sitesRaw.find((s: any) => s.name === site); + if (!siteRaw) throw new Error(`Site "${site}" not found in sites list`); + + const health: SubsystemHealth[] = healthRaw.map((h: any) => ({ + subsystem: h.subsystem, + status: h.status, + numUser: h.num_user, + numGuest: h.num_guest, + numIot: h.num_iot, + txBytesR: h["tx_bytes-r"], + rxBytesR: h["rx_bytes-r"], + numAp: h.num_ap, + numSw: h.num_sw, + numGw: h.num_gw, + numAdopted: h.num_adopted, + numDisconnected: h.num_disconnected, + numPending: h.num_pending, + numDisabled: h.num_disabled, + })); + + const sysInfo: SysInfo = { + name: sysInfoRaw.name, + hostname: sysInfoRaw.hostname, + version: sysInfoRaw.version, + build: sysInfoRaw.build, + timezone: sysInfoRaw.timezone, + uptime: sysInfoRaw.uptime, + ipAddresses: sysInfoRaw.ip_addrs ?? [], + updateAvailable: sysInfoRaw.update_available ?? false, + isCloudConsole: sysInfoRaw.is_cloud_console ?? false, + dataRetentionDays: sysInfoRaw.data_retention_days ?? 0, + informPort: sysInfoRaw.inform_port, + httpsPort: sysInfoRaw.https_port, + unsupportedDeviceCount: sysInfoRaw.unsupported_device_count ?? 0, + }; + + return { + site: { + id: siteRaw._id, + name: siteRaw.name, + description: siteRaw.desc ?? "", + deviceCount: siteRaw.device_count ?? 0, + role: siteRaw.role ?? "", + }, + health, + sysInfo, + }; + } catch (e) { + if (!axios.isAxiosError(e)) throw e; + if ( + e.response && + e.response.status !== 404 && + e.response.status !== 401 + ) { + throw e; + } + } + } + + throw new Error("Could not fetch site overview from any known path"); + } + + private static parseDeviceState(state: number): DeviceState { + const map: Record = { + 0: "disconnected", + 1: "connected", + 2: "pending", + 4: "adopting", + 5: "adopting", + }; + return map[state] ?? "unknown"; + } + + async getDevices(site: string): Promise { + const paths = [ + `/proxy/network/api/s/${site}/stat/device`, + `/api/s/${site}/stat/device`, + ]; + + for (const path of paths) { + try { + const res = await this.client.get(path); + const raw = (res.data?.data ?? res.data) as any[]; + + return raw.map((d: any): Device => { + const uplink: DeviceUplink | null = d.uplink + ? { + type: d.uplink.type, + mac: d.uplink.uplink_mac, + ip: d.uplink.uplink_remote_ip, + uplinkRemotePort: d.uplink.uplink_remote_port, + speed: d.uplink.speed, + fullDuplex: d.uplink.full_duplex, + } + : null; + + const radios: DeviceRadio[] = (d.radio_table ?? []).map( + (r: any, i: number) => { + const stats = d.radio_table_stats?.[i] ?? {}; + return { + name: r.name ?? r.radio, + radio: r.radio, + channel: r.channel, + txPower: r.tx_power, + txPowerMode: r.tx_power_mode, + minRssiEnabled: r.min_rssi_enabled ?? false, + numSta: stats.num_sta ?? 0, + satisfaction: stats.satisfaction ?? null, + }; + }, + ); + + return { + id: d._id, + mac: d.mac, + ip: d.ip ?? "", + name: d.name ?? d.mac, + model: d.model, + shortname: d.shortname ?? d.model, + type: d.type, + version: d.version ?? "", + serial: d.serial ?? "", + state: UnifiClient.parseDeviceState(d.state), + adopted: d.adopted ?? false, + uptime: d.uptime ?? 0, + lastSeen: d.last_seen ?? 0, + upgradable: d.upgradable ?? false, + satisfaction: d.satisfaction ?? null, + numClients: d.num_sta ?? 0, + numUserClients: d["user-num_sta"] ?? 0, + numGuestClients: d["guest-num_sta"] ?? 0, + txBytes: d.tx_bytes ?? 0, + rxBytes: d.rx_bytes ?? 0, + uplink, + radios, + modelInLts: d.model_in_lts ?? false, + modelInEol: d.model_in_eol ?? false, + }; + }); + } catch (e) { + if (!axios.isAxiosError(e)) throw e; + if ( + e.response && + e.response.status !== 404 && + e.response.status !== 401 + ) { + throw e; + } + } + } + + throw new Error("Could not fetch devices from any known path"); + } + + async getNetworks(site: string): Promise { + const paths = [ + `/proxy/network/api/s/${site}/rest/networkconf`, + `/api/s/${site}/rest/networkconf`, + ]; + + for (const path of paths) { + try { + const res = await this.client.get(path); + const raw = (res.data?.data ?? res.data) as any[]; + + return raw.map( + (n: any): Network => ({ + id: n._id, + name: n.name ?? "", + purpose: n.purpose ?? "corporate", + enabled: n.enabled ?? true, + ipSubnet: n.ip_subnet ?? null, + vlan: n.vlan != null ? Number(n.vlan) : null, + vlanEnabled: n.vlan_enabled ?? false, + isNat: n.is_nat ?? false, + domainName: n.domain_name ?? null, + networkGroup: n.networkgroup ?? null, + dhcpdEnabled: n.dhcpd_enabled ?? false, + dhcpdStart: n.dhcpd_start ?? null, + dhcpdStop: n.dhcpd_stop ?? null, + dhcpdLeasetime: n.dhcpd_leasetime ?? null, + dhcpRelayEnabled: n.dhcp_relay_enabled ?? false, + dhcpGuardEnabled: n.dhcpguard_enabled ?? false, + igmpSnooping: n.igmp_snooping ?? false, + ipv6Enabled: n.ipv6_enabled ?? false, + ipv6InterfaceType: n.ipv6_interface_type ?? null, + internetAccessEnabled: n.internet_access_enabled ?? null, + }), + ); + } catch (e) { + if (!axios.isAxiosError(e)) throw e; + if ( + e.response && + e.response.status !== 404 && + e.response.status !== 401 + ) { + throw e; + } + } + } + + throw new Error("Could not fetch networks from any known path"); + } + + async createSite(description: string): Promise { + const paths = [ + "/proxy/network/api/s/default/cmd/sitemgr", + "/api/s/default/cmd/sitemgr", + ]; + + const body = { cmd: "add-site", desc: description }; + + for (const path of paths) { + try { + const res = await this.client.post(path, body); + const raw = (res.data?.data?.[0] ?? res.data) as any; + return { + id: raw._id, + name: raw.name, + description: raw.desc ?? description, + deviceCount: raw.device_count ?? 0, + role: raw.role ?? "", + }; + } catch (e) { + if (!axios.isAxiosError(e)) throw e; + if ( + e.response && + e.response.status !== 404 && + e.response.status !== 401 + ) { + throw new Error( + `Failed to create site: ${e.response.status} ${JSON.stringify(e.response.data)}`, + ); + } + } + } + + throw new Error("Could not create site from any known path"); + } + + // --- WLAN Groups --- + + async getWlanGroups(site: string): Promise { + const paths = [ + `/proxy/network/api/s/${site}/rest/wlangroup`, + `/api/s/${site}/rest/wlangroup`, + ]; + + for (const path of paths) { + try { + const res = await this.client.get(path); + const raw = (res.data?.data ?? res.data) as any[]; + return raw.map( + (g: any): WlanGroup => ({ + id: g._id, + name: g.name ?? "", + siteId: g.site_id ?? "", + noDelete: g.attr_no_delete ?? false, + noEdit: g.attr_no_edit ?? false, + hidden: g.attr_hidden ?? false, + }), + ); + } catch (e) { + if (!axios.isAxiosError(e)) throw e; + if ( + e.response && + e.response.status !== 404 && + e.response.status !== 401 + ) + throw e; + } + } + + throw new Error("Could not fetch WLAN groups from any known path"); + } + + async createWlanGroup( + site: string, + input: WlanGroupCreateInput, + ): Promise { + const paths = [ + `/proxy/network/api/s/${site}/rest/wlangroup`, + `/api/s/${site}/rest/wlangroup`, + ]; + + const body: Record = { name: input.name }; + + for (const path of paths) { + try { + const res = await this.client.post(path, body); + const raw = (res.data?.data?.[0] ?? res.data) as any; + return { + id: raw._id, + name: raw.name ?? input.name, + siteId: raw.site_id ?? "", + noDelete: raw.attr_no_delete ?? false, + noEdit: raw.attr_no_edit ?? false, + hidden: raw.attr_hidden ?? false, + }; + } catch (e) { + if (!axios.isAxiosError(e)) throw e; + if ( + e.response && + e.response.status !== 404 && + e.response.status !== 401 + ) { + throw new Error( + `Failed to create WLAN group: ${e.response.status} ${JSON.stringify(e.response.data)}`, + ); + } + } + } + + throw new Error("Could not create WLAN group from any known path"); + } + + // --- User Groups (Speed Profiles) --- + + async getUserGroups(site: string): Promise { + const paths = [ + `/proxy/network/api/s/${site}/rest/usergroup`, + `/api/s/${site}/rest/usergroup`, + ]; + + for (const path of paths) { + try { + const res = await this.client.get(path); + const raw = (res.data?.data ?? res.data) as any[]; + return raw.map( + (g: any): UserGroup => ({ + id: g._id, + name: g.name ?? "", + siteId: g.site_id ?? "", + noDelete: g.attr_no_delete ?? false, + downloadLimitKbps: g.qos_rate_max_down ?? -1, + uploadLimitKbps: g.qos_rate_max_up ?? -1, + }), + ); + } catch (e) { + if (!axios.isAxiosError(e)) throw e; + if ( + e.response && + e.response.status !== 404 && + e.response.status !== 401 + ) + throw e; + } + } + + throw new Error("Could not fetch user groups from any known path"); + } + + async createUserGroup( + site: string, + input: UserGroupCreateInput, + ): Promise { + const paths = [ + `/proxy/network/api/s/${site}/rest/usergroup`, + `/api/s/${site}/rest/usergroup`, + ]; + + const body: Record = { name: input.name }; + if (input.downloadLimitKbps !== undefined) + body.qos_rate_max_down = input.downloadLimitKbps; + if (input.uploadLimitKbps !== undefined) + body.qos_rate_max_up = input.uploadLimitKbps; + + for (const path of paths) { + try { + const res = await this.client.post(path, body); + const raw = (res.data?.data?.[0] ?? res.data) as any; + return { + id: raw._id, + name: raw.name ?? input.name, + siteId: raw.site_id ?? "", + noDelete: raw.attr_no_delete ?? false, + downloadLimitKbps: raw.qos_rate_max_down ?? -1, + uploadLimitKbps: raw.qos_rate_max_up ?? -1, + }; + } catch (e) { + if (!axios.isAxiosError(e)) throw e; + if ( + e.response && + e.response.status !== 404 && + e.response.status !== 401 + ) { + throw new Error( + `Failed to create user group: ${e.response.status} ${JSON.stringify(e.response.data)}`, + ); + } + } + } + + throw new Error("Could not create user group from any known path"); + } + + // --- AP Groups --- + + async getApGroups(site: string): Promise { + const paths = [ + `/proxy/network/v2/api/site/${site}/apgroups`, + `/v2/api/site/${site}/apgroups`, + ]; + + for (const path of paths) { + try { + const res = await this.client.get(path); + const raw = (res.data?.data ?? res.data) as any[]; + return raw.map( + (g: any): ApGroup => ({ + id: g._id, + name: g.name ?? "", + deviceMacs: g.device_macs ?? [], + noDelete: g.attr_no_delete ?? false, + forWlanconf: g.for_wlanconf ?? false, + }), + ); + } catch (e) { + if (!axios.isAxiosError(e)) throw e; + if ( + e.response && + e.response.status !== 404 && + e.response.status !== 401 + ) + throw e; + } + } + + throw new Error("Could not fetch AP groups from any known path"); + } + + async createApGroup( + site: string, + name: string, + deviceMacs: string[], + forWlanconf: boolean = false, + ): Promise { + const paths = [ + `/proxy/network/v2/api/site/${site}/apgroups`, + `/v2/api/site/${site}/apgroups`, + ]; + + const body = { + name, + device_macs: deviceMacs, + for_wlanconf: forWlanconf, + }; + + for (const path of paths) { + try { + const res = await this.client.post(path, body); + const raw = (res.data?.data?.[0] ?? res.data) as any; + return { + id: raw._id, + name: raw.name ?? name, + deviceMacs: raw.device_macs ?? deviceMacs, + noDelete: raw.attr_no_delete ?? false, + forWlanconf: raw.for_wlanconf ?? forWlanconf, + }; + } catch (e) { + if (!axios.isAxiosError(e)) throw e; + if ( + e.response && + e.response.status !== 404 && + e.response.status !== 401 + ) { + throw new Error( + `Failed to create AP group: ${e.response.status} ${JSON.stringify(e.response.data)}`, + ); + } + } + } + + throw new Error("Could not create AP group from any known path"); + } + + async updateApGroup( + site: string, + groupId: string, + deviceMacs: string[], + ): Promise { + const paths = [ + `/proxy/network/v2/api/site/${site}/apgroups/${groupId}`, + `/v2/api/site/${site}/apgroups/${groupId}`, + ]; + + const body = { + name: "devices_ap_group", + device_macs: deviceMacs, + for_wlanconf: true, + }; + + for (const path of paths) { + try { + const res = await this.client.put(path, body); + const raw = (res.data?.data?.[0] ?? res.data) as any; + return { + id: raw._id ?? groupId, + name: raw.name ?? "devices_ap_group", + deviceMacs: raw.device_macs ?? deviceMacs, + noDelete: raw.attr_no_delete ?? false, + forWlanconf: raw.for_wlanconf ?? true, + }; + } catch (e) { + if (!axios.isAxiosError(e)) throw e; + if ( + e.response && + e.response.status !== 404 && + e.response.status !== 401 + ) { + throw new Error( + `Failed to update AP group: ${e.response.status} ${JSON.stringify(e.response.data)}`, + ); + } + } + } + + throw new Error("Could not update AP group from any known path"); + } + + // --- Access Points --- + + async getAccessPoints(site: string): Promise { + const devices = await this.getDevices(site); + return devices.filter((d) => d.type === "uap"); + } + + // --- WiFi Limits --- + + async getWifiLimits(site: string): Promise { + const SSID_LIMIT_PER_RADIO = 8; + + const paths = [ + `/proxy/network/api/s/${site}/stat/device`, + `/api/s/${site}/stat/device`, + ]; + + for (const path of paths) { + try { + const res = await this.client.get(path); + const raw = (res.data?.data ?? res.data) as any[]; + const aps = raw.filter((d: any) => d.type === "uap"); + + return aps.map((ap: any): ApWifiLimits => { + const vapTable: any[] = ap.vap_table ?? []; + const radioMap = new Map }>(); + + for (const vap of vapTable) { + if (!vap.up || !vap.radio) continue; + if (!radioMap.has(vap.radio)) { + radioMap.set(vap.radio, { wlanNames: new Set() }); + } + if (vap.essid) { + radioMap.get(vap.radio)!.wlanNames.add(vap.essid); + } + } + + const radioBandMap: Record = { + ng: "2g", + na: "5g", + "6e": "6e", + }; + + const radios: ApRadioWifiUsage[] = Array.from(radioMap.entries()).map( + ([radio, data]): ApRadioWifiUsage => ({ + radio, + band: radioBandMap[radio] ?? radio, + activeWlans: data.wlanNames.size, + limit: SSID_LIMIT_PER_RADIO, + remaining: Math.max( + 0, + SSID_LIMIT_PER_RADIO - data.wlanNames.size, + ), + wlanNames: Array.from(data.wlanNames), + }), + ); + + return { + apId: ap._id, + apName: ap.name ?? ap.mac, + mac: ap.mac, + model: ap.model ?? "", + radios, + }; + }); + } catch (e) { + if (!axios.isAxiosError(e)) throw e; + if ( + e.response && + e.response.status !== 404 && + e.response.status !== 401 + ) + throw e; + } + } + + throw new Error("Could not fetch WiFi limits from any known path"); + } + + // --- Private Pre-Shared Keys --- + + private static parsePPSKs(raw: any[]): PrivatePSK[] { + return raw.map( + (p: any): PrivatePSK => ({ + key: p.key ?? "", + name: p.name ?? "", + mac: p.mac ?? null, + vlanId: p.vlan_id ?? null, + }), + ); + } + + async getPrivatePSKs(site: string, wlanId: string): Promise { + const paths = [ + `/proxy/network/api/s/${site}/rest/wlanconf/${wlanId}`, + `/api/s/${site}/rest/wlanconf/${wlanId}`, + ]; + + for (const path of paths) { + try { + const res = await this.client.get(path); + const raw = (res.data?.data?.[0] ?? res.data) as any; + return UnifiClient.parsePPSKs(raw.private_preshared_keys ?? []); + } catch (e) { + if (!axios.isAxiosError(e)) throw e; + if ( + e.response && + e.response.status !== 404 && + e.response.status !== 401 + ) + throw e; + } + } + + throw new Error("Could not fetch PPSKs from any known path"); + } + + async createPrivatePSK( + site: string, + wlanId: string, + psk: PrivatePSKCreateInput, + ): Promise { + const paths = [ + `/proxy/network/api/s/${site}/rest/wlanconf/${wlanId}`, + `/api/s/${site}/rest/wlanconf/${wlanId}`, + ]; + + // Fetch current PPSKs + let currentPpsks: any[] = []; + for (const path of paths) { + try { + const res = await this.client.get(path); + const raw = (res.data?.data?.[0] ?? res.data) as any; + currentPpsks = (raw.private_preshared_keys ?? []) as any[]; + break; + } catch (e) { + if (!axios.isAxiosError(e)) throw e; + if ( + e.response && + e.response.status !== 404 && + e.response.status !== 401 + ) + throw e; + } + } + + const newPsk: Record = { + key: psk.key, + name: psk.name, + }; + if (psk.mac) newPsk.mac = psk.mac; + if (psk.vlanId !== undefined) newPsk.vlan_id = psk.vlanId; + currentPpsks.push(newPsk); + + // Update WLAN with new PPSKs + for (const path of paths) { + try { + const res = await this.client.put(path, { + private_preshared_keys: currentPpsks, + private_preshared_keys_enabled: true, + }); + const raw = (res.data?.data?.[0] ?? res.data) as any; + return UnifiClient.parsePPSKs(raw.private_preshared_keys ?? []); + } catch (e) { + if (!axios.isAxiosError(e)) throw e; + if ( + e.response && + e.response.status !== 404 && + e.response.status !== 401 + ) { + throw new Error( + `Failed to create PPSK: ${e.response.status} ${JSON.stringify(e.response.data)}`, + ); + } + } + } + + throw new Error("Could not create PPSK from any known path"); + } +} diff --git a/src/modules/unifi-api/unifiTypes.ts b/src/modules/unifi-api/unifiTypes.ts new file mode 100644 index 0000000..b512611 --- /dev/null +++ b/src/modules/unifi-api/unifiTypes.ts @@ -0,0 +1,434 @@ +export interface WlanConfRaw { + _id: string; + name?: string; + ssid?: string; + x_passphrase?: string; + [key: string]: unknown; +} + +export interface WlanConf { + id: string; + name: string; + siteId: string; + enabled: boolean; + security: string; + wpaMode: string; + wpaEnc: string; + wpa3Support: boolean; + wpa3Transition: boolean; + wpa3FastRoaming: boolean; + wpa3Enhanced192: boolean; + passphrase: string | null; + passphraseAutogenerated: boolean; + hideSSID: boolean; + isGuest: boolean; + band: string; + bands: string[]; + networkconfId: string; + usergroupId: string; + apGroupIds: string[]; + apGroupMode: string; + pmfMode: string; + groupRekey: number; + dtimMode: string; + dtimNg: number; + dtimNa: number; + dtim6e: number; + l2Isolation: boolean; + fastRoamingEnabled: boolean; + bssTransition: boolean; + uapsdEnabled: boolean; + iappEnabled: boolean; + proxyArp: boolean; + mcastenhanceEnabled: boolean; + macFilterEnabled: boolean; + macFilterPolicy: string; + macFilterList: string[]; + radiusDasEnabled: boolean; + radiusMacAuthEnabled: boolean; + radiusMacaclFormat: string; + minrateSettingPreference: string; + minrateNgEnabled: boolean; + minrateNgDataRateKbps: number; + minrateNgAdvertisingRates: boolean; + minrateNaEnabled: boolean; + minrateNaDataRateKbps: number; + minrateNaAdvertisingRates: boolean; + settingPreference: string; + no2ghzOui: boolean; + privatePreSharedKeysEnabled: boolean; + privatePreSharedKeys: unknown[]; + saeGroups: unknown[]; + saePsk: unknown[]; + schedule: unknown[]; + scheduleWithDuration: unknown[]; + bcFilterList: unknown[]; + externalId: string | null; +} + +export interface WlanConfUpdate { + name?: string; + x_passphrase?: string; + enabled?: boolean; + security?: "wpapsk" | "wpaeap" | "open" | "osen"; + wpa_mode?: "wpa2" | "wpa3" | "wpa2wpa3"; + wpa_enc?: "ccmp" | "gcmp" | "ccmp-gcmp"; + hide_ssid?: boolean; + mac_filter_enabled?: boolean; + mac_filter_policy?: "allow" | "deny"; + is_guest?: boolean; + l2_isolation?: boolean; + fast_roaming_enabled?: boolean; + bss_transition?: boolean; + uapsd_enabled?: boolean; + group_rekey?: number; + dtim_mode?: "default" | "custom"; + dtim_ng?: number; + dtim_na?: number; + minrate_ng_enabled?: boolean; + minrate_na_enabled?: boolean; + radius_das_enabled?: boolean; + radius_mac_auth_enabled?: boolean; + pmf_mode?: "disabled" | "optional" | "required"; + wlan_band?: "both" | "2g" | "5g"; + usergroup_id?: string; + proxy_arp?: boolean; + mcastenhance_enabled?: boolean; + mac_filter_list?: string[]; + no2ghz_oui?: boolean; + ap_group_ids?: string[]; + ap_group_mode?: string; +} + +/** + * CamelCase update input matching the WlanConf return shape. + * Accepted by the API and converted to WlanConfUpdate (snake_case) before + * being sent to the UniFi controller. + */ +export interface WlanConfUpdateInput { + name?: string; + passphrase?: string; + enabled?: boolean; + security?: "wpapsk" | "wpaeap" | "open" | "osen"; + wpaMode?: "wpa2" | "wpa3" | "wpa2wpa3"; + wpaEnc?: "ccmp" | "gcmp" | "ccmp-gcmp"; + hideSSID?: boolean; + macFilterEnabled?: boolean; + macFilterPolicy?: "allow" | "deny"; + isGuest?: boolean; + l2Isolation?: boolean; + fastRoamingEnabled?: boolean; + bssTransition?: boolean; + uapsdEnabled?: boolean; + groupRekey?: number; + dtimMode?: "default" | "custom"; + dtimNg?: number; + dtimNa?: number; + minrateNgEnabled?: boolean; + minrateNaEnabled?: boolean; + radiusDasEnabled?: boolean; + radiusMacAuthEnabled?: boolean; + pmfMode?: "disabled" | "optional" | "required"; + band?: "both" | "2g" | "5g"; + usergroupId?: string; + proxyArp?: boolean; + mcastenhanceEnabled?: boolean; + macFilterList?: string[]; + no2ghzOui?: boolean; + apGroupIds?: string[]; + apGroupMode?: string; +} + +/** + * Converts a camelCase WlanConfUpdateInput to the snake_case WlanConfUpdate + * expected by the UniFi controller API. + */ +export function toWlanConfUpdate(input: WlanConfUpdateInput): WlanConfUpdate { + const result: WlanConfUpdate = {}; + + if (input.name !== undefined) result.name = input.name; + if (input.passphrase !== undefined) result.x_passphrase = input.passphrase; + if (input.enabled !== undefined) result.enabled = input.enabled; + if (input.security !== undefined) result.security = input.security; + if (input.wpaMode !== undefined) result.wpa_mode = input.wpaMode; + if (input.wpaEnc !== undefined) result.wpa_enc = input.wpaEnc; + if (input.hideSSID !== undefined) result.hide_ssid = input.hideSSID; + if (input.macFilterEnabled !== undefined) + result.mac_filter_enabled = input.macFilterEnabled; + if (input.macFilterPolicy !== undefined) + result.mac_filter_policy = input.macFilterPolicy; + if (input.isGuest !== undefined) result.is_guest = input.isGuest; + if (input.l2Isolation !== undefined) result.l2_isolation = input.l2Isolation; + if (input.fastRoamingEnabled !== undefined) + result.fast_roaming_enabled = input.fastRoamingEnabled; + if (input.bssTransition !== undefined) + result.bss_transition = input.bssTransition; + if (input.uapsdEnabled !== undefined) + result.uapsd_enabled = input.uapsdEnabled; + if (input.groupRekey !== undefined) result.group_rekey = input.groupRekey; + if (input.dtimMode !== undefined) result.dtim_mode = input.dtimMode; + if (input.dtimNg !== undefined) result.dtim_ng = input.dtimNg; + if (input.dtimNa !== undefined) result.dtim_na = input.dtimNa; + if (input.minrateNgEnabled !== undefined) + result.minrate_ng_enabled = input.minrateNgEnabled; + if (input.minrateNaEnabled !== undefined) + result.minrate_na_enabled = input.minrateNaEnabled; + if (input.radiusDasEnabled !== undefined) + result.radius_das_enabled = input.radiusDasEnabled; + if (input.radiusMacAuthEnabled !== undefined) + result.radius_mac_auth_enabled = input.radiusMacAuthEnabled; + if (input.pmfMode !== undefined) result.pmf_mode = input.pmfMode; + if (input.band !== undefined) result.wlan_band = input.band; + if (input.usergroupId !== undefined) result.usergroup_id = input.usergroupId; + if (input.proxyArp !== undefined) result.proxy_arp = input.proxyArp; + if (input.mcastenhanceEnabled !== undefined) + result.mcastenhance_enabled = input.mcastenhanceEnabled; + if (input.macFilterList !== undefined) + result.mac_filter_list = input.macFilterList; + if (input.no2ghzOui !== undefined) result.no2ghz_oui = input.no2ghzOui; + if (input.apGroupIds !== undefined) result.ap_group_ids = input.apGroupIds; + if (input.apGroupMode !== undefined) result.ap_group_mode = input.apGroupMode; + + return result; +} + +// --- Site overview types --- + +export interface SubsystemHealth { + subsystem: "wlan" | "wan" | "www" | "lan" | "vpn"; + status: "ok" | "warn" | "error" | "unknown"; + numUser?: number; + numGuest?: number; + numIot?: number; + txBytesR?: number; + rxBytesR?: number; + // WLAN-specific + numAp?: number; + // LAN-specific + numSw?: number; + // WAN-specific + numGw?: number; + // Shared device counts + numAdopted?: number; + numDisconnected?: number; + numPending?: number; + numDisabled?: number; +} + +export interface SysInfo { + name: string; + hostname: string; + version: string; + build: string; + timezone: string; + uptime: number; + ipAddresses: string[]; + updateAvailable: boolean; + isCloudConsole: boolean; + dataRetentionDays: number; + informPort: number; + httpsPort: number; + unsupportedDeviceCount: number; +} + +export interface SiteInfo { + id: string; + name: string; + description: string; + deviceCount: number; + role: string; +} + +export interface SiteOverview { + site: SiteInfo; + health: SubsystemHealth[]; + sysInfo: SysInfo; +} + +// --- Device types --- + +export type DeviceType = "uap" | "usw" | "ugw" | "uxg" | "ubb" | "udm"; +export type DeviceState = + | "connected" + | "disconnected" + | "pending" + | "adopting" + | "unknown"; + +export interface DeviceUplink { + type?: string; + mac?: string; + ip?: string; + uplinkRemotePort?: number; + speed?: number; + fullDuplex?: boolean; +} + +export interface DeviceRadio { + name: string; + radio: string; + channel: number; + txPower: number; + txPowerMode: string; + minRssiEnabled: boolean; + numSta: number; + satisfaction: number | null; +} + +export interface Device { + id: string; + mac: string; + ip: string; + name: string; + model: string; + shortname: string; + type: DeviceType; + version: string; + serial: string; + state: DeviceState; + adopted: boolean; + uptime: number; + lastSeen: number; + upgradable: boolean; + satisfaction: number | null; + numClients: number; + numUserClients: number; + numGuestClients: number; + txBytes: number; + rxBytes: number; + uplink: DeviceUplink | null; + radios: DeviceRadio[]; + modelInLts: boolean; + modelInEol: boolean; +} + +// --- Network types --- + +export type NetworkPurpose = + | "corporate" + | "vlan-only" + | "wan" + | "vpn-client" + | "remote-user-vpn" + | "site-vpn"; + +export interface Network { + id: string; + name: string; + purpose: NetworkPurpose; + enabled: boolean; + ipSubnet: string | null; + vlan: number | null; + vlanEnabled: boolean; + isNat: boolean; + domainName: string | null; + networkGroup: string | null; + dhcpdEnabled: boolean; + dhcpdStart: string | null; + dhcpdStop: string | null; + dhcpdLeasetime: number | null; + dhcpRelayEnabled: boolean; + dhcpGuardEnabled: boolean; + igmpSnooping: boolean; + ipv6Enabled: boolean; + ipv6InterfaceType: string | null; + internetAccessEnabled: boolean | null; +} + +// --- Site create types --- + +export interface CreateSiteOptions { + /** Human-readable description / display name for the site */ + description: string; +} + +// --- Site list types --- + +export interface SiteListItem { + id: string; + name: string; + description: string; + deviceCount: number; + role: string; +} + +// --- WLAN Group types --- + +export interface WlanGroup { + id: string; + name: string; + siteId: string; + noDelete: boolean; + noEdit: boolean; + hidden: boolean; +} + +export interface WlanGroupCreateInput { + name: string; +} + +// --- AP Group types --- + +export interface ApGroup { + id: string; + name: string; + deviceMacs: string[]; + noDelete: boolean; + forWlanconf: boolean; +} + +// --- User Group (Speed Profile) types --- + +export interface UserGroup { + id: string; + name: string; + siteId: string; + noDelete: boolean; + /** Download rate limit in Kbps. -1 means unlimited. */ + downloadLimitKbps: number; + /** Upload rate limit in Kbps. -1 means unlimited. */ + uploadLimitKbps: number; +} + +export interface UserGroupCreateInput { + name: string; + /** Download rate limit in Kbps. -1 or omit for unlimited. */ + downloadLimitKbps?: number; + /** Upload rate limit in Kbps. -1 or omit for unlimited. */ + uploadLimitKbps?: number; +} + +// --- Private PSK types --- + +export interface PrivatePSK { + key: string; + name: string; + mac: string | null; + vlanId: number | null; +} + +export interface PrivatePSKCreateInput { + key: string; + name: string; + mac?: string; + vlanId?: number; +} + +// --- WiFi Limit types --- + +export interface ApRadioWifiUsage { + radio: string; + band: string; + activeWlans: number; + limit: number; + remaining: number; + wlanNames: string[]; +} + +export interface ApWifiLimits { + apId: string; + apName: string; + mac: string; + model: string; + radios: ApRadioWifiUsage[]; +} diff --git a/src/types/ConnectWiseTypes.ts b/src/types/ConnectWiseTypes.ts index 2b5e7ae..5f43aa8 100644 --- a/src/types/ConnectWiseTypes.ts +++ b/src/types/ConnectWiseTypes.ts @@ -15,7 +15,7 @@ export interface Company { territory: Territory; market: Market; accountNumber: string; - defaultContact: Contact; + defaultContact: ContactHref; dateAcquired: string; annualRevenue: number; numberOfEmployees: number; @@ -27,7 +27,7 @@ export interface Company { billingTerms: BasicEntity; billToCompany: LinkedCompany; billingSite: LinkedSite; - billingContact: Contact; + billingContact: ContactHref; invoiceDeliveryMethod: BasicEntity; invoiceToEmailAddress: string; deletedFlag: boolean; @@ -91,7 +91,7 @@ export interface LinkedSite extends BasicEntity { }; } -export interface Contact { +export interface ContactHref { id: number; name: string; _info: { @@ -305,3 +305,140 @@ export interface ConfigurationInfo { // Your payload is an array: export type ConfigurationResponse = ConfigurationItem[]; +export interface Contact { + id: number; + firstName: string; + lastName: string; + company: ContactCompany; + site: ContactSite; + relationship: ContactRelationship; + department: ContactDepartment; + inactiveFlag: boolean; + title: string; + marriedFlag: boolean; + childrenFlag: boolean; + disablePortalLoginFlag: boolean; + unsubscribeFlag: boolean; + mobileGuid: string; + facebookUrl: string; + twitterUrl: string; + linkedInUrl: string; + defaultPhoneType: string; + defaultPhoneNbr: string; + defaultBillingFlag: boolean; + defaultFlag: boolean; + companyLocation: ContactCompanyLocation; + communicationItems: ContactCommunicationItem[]; + types: ContactType[]; + customFields: ContactCustomField[]; + photo: ContactPhoto; + ignoreDuplicates: boolean; + _info: ContactInfo; +} + +export interface ContactCompany { + id: number; + identifier: string; + name: string; + _info: { + company_href: string; + mobileGuid: string; + }; +} + +export interface ContactSite { + id: number; + name: string; + _info: { + site_href: string; + mobileGuid: string; + }; +} + +export interface ContactRelationship { + id: number; + name: string; + _info: { + relationship_href: string; + }; +} + +export interface ContactDepartment { + id: number; + name: string; + _info: { + department_href: string; + }; +} + +export interface ContactCompanyLocation { + id: number; + name: string; + _info: { + location_href: string; + }; +} + +export type CommunicationTypeEnum = "Phone" | "Fax" | "Email"; // from CW docs [web:40] + +export interface ContactCommunicationItem { + id: number; + type: { + id: number; + name: string; + _info: { + type_href: string; + }; + }; + value: string; + defaultFlag: boolean; + communicationType: CommunicationTypeEnum; + domain?: string; // only present for email entries +} + +export interface ContactType { + id: number; + name: string; + _info: { + type_href: string; + }; +} + +export interface ContactCustomField { + id: number; + caption: string; + type: string; + entryMethod: string; + numberOfDecimals: number; + value: string | number | boolean | null; + connectWiseId: string; + rowNum: number; + userDefinedFieldRecId: number; + podId: string; +} + +export interface ContactPhoto { + id: number; + name: string; + _info: { + filename: string; + document_href: string; + documentDownload_href: string; + }; +} + +export interface ContactInfo { + lastUpdated: string; // ISO datetime + updatedBy: string; + communications_href: string; + notes_href: string; + tracks_href: string; + portalSecurity_href: string; + activities_href: string; + documents_href: string; + configurations_href: string; + tickets_href: string; + opportunities_href: string; + projects_href: string; + image_href: string; +} diff --git a/src/types/PermissionNodes.ts b/src/types/PermissionNodes.ts index f9a97db..0a2609b 100644 --- a/src/types/PermissionNodes.ts +++ b/src/types/PermissionNodes.ts @@ -15,6 +15,13 @@ export interface PermissionNode { usedIn: string[]; /** Dependencies - other permissions that must be granted alongside this one */ dependencies?: string[]; + /** + * When present, indicates this permission gates individual fields on the + * response object via `processObjectValuePerms`. Each entry is a full + * permission node in the form `.`. Only fields whose + * corresponding permission the user holds are included in the response. + */ + fieldLevelPermissions?: string[]; } export interface PermissionCategory { @@ -56,6 +63,12 @@ export const PERMISSION_NODES = { usedIn: ["src/api/companies/[id]/fetch.ts"], dependencies: ["company.fetch"], }, + { + node: "company.fetch.contacts", + description: "View all company contacts", + usedIn: ["src/api/companies/[id]/fetch.ts"], + dependencies: ["company.fetch"], + }, { node: "company.fetch.many", description: "Fetch multiple companies", @@ -120,6 +133,24 @@ export const PERMISSION_NODES = { ], dependencies: ["credential.fetch"], }, + { + node: "credential.sub_credentials.fetch", + description: "Fetch sub-credentials of a parent credential", + usedIn: ["src/api/credentials/fetchSubCredentials.ts"], + dependencies: ["credential.fetch"], + }, + { + node: "credential.sub_credentials.create", + description: "Create a sub-credential on a parent credential", + usedIn: ["src/api/credentials/addSubCredential.ts"], + dependencies: ["credential.fetch"], + }, + { + node: "credential.sub_credentials.delete", + description: "Remove a sub-credential from a parent credential", + usedIn: ["src/api/credentials/removeSubCredential.ts"], + dependencies: ["credential.fetch"], + }, ], }, @@ -309,6 +340,242 @@ export const PERMISSION_NODES = { }, ], }, + + unifi: { + name: "UniFi Permissions", + description: + "Permissions for accessing and managing UniFi network infrastructure", + permissions: [ + { + node: "unifi.access", + description: + "Gate permission for the entire UniFi API — required for all UniFi routes", + usedIn: [ + "src/api/unifi/sites/fetchAll.ts", + "src/api/unifi/sites/sync.ts", + "src/api/unifi/site/fetch.ts", + "src/api/unifi/site/overview.ts", + "src/api/unifi/site/devices.ts", + "src/api/unifi/site/wifi/fetchAll.ts", + "src/api/unifi/site/wifi/update.ts", + "src/api/unifi/site/wifi/ppskFetchAll.ts", + "src/api/unifi/site/wifi/ppskCreate.ts", + "src/api/unifi/site/networks.ts", + "src/api/unifi/site/link.ts", + "src/api/unifi/site/unlink.ts", + "src/api/companies/[id]/unifiSites.ts", + "src/api/unifi/sites/create.ts", + "src/api/unifi/site/wlanGroups.ts", + "src/api/unifi/site/accessPoints.ts", + "src/api/unifi/site/wifiLimits.ts", + "src/api/unifi/site/speedProfilesFetchAll.ts", + "src/api/unifi/site/speedProfilesCreate.ts", + ], + }, + { + node: "unifi.sites.create", + description: "Create a new site on the UniFi controller", + usedIn: ["src/api/unifi/sites/create.ts"], + dependencies: ["unifi.access"], + }, + { + node: "unifi.sites.fetch", + description: "Fetch a single UniFi site", + usedIn: ["src/api/unifi/site/fetch.ts"], + dependencies: ["unifi.access"], + }, + { + node: "unifi.sites.fetch.many", + description: "Fetch all UniFi sites", + usedIn: ["src/api/unifi/sites/fetchAll.ts"], + dependencies: ["unifi.access"], + }, + { + node: "unifi.sites.sync", + description: "Sync sites from the UniFi controller into the database", + usedIn: ["src/api/unifi/sites/sync.ts"], + dependencies: ["unifi.access"], + }, + { + node: "unifi.sites.link", + description: "Link or unlink a UniFi site to/from a company", + usedIn: ["src/api/unifi/site/link.ts", "src/api/unifi/site/unlink.ts"], + dependencies: ["unifi.access"], + }, + { + node: "unifi.site.overview", + description: "View live site overview from the UniFi controller", + usedIn: ["src/api/unifi/site/overview.ts"], + dependencies: ["unifi.access"], + }, + { + node: "unifi.site.devices", + description: "View live device list from the UniFi controller", + usedIn: ["src/api/unifi/site/devices.ts"], + dependencies: ["unifi.access"], + }, + { + node: "unifi.site.wifi", + description: "View WiFi networks (WLANs) from the UniFi controller", + usedIn: ["src/api/unifi/site/wifi/fetchAll.ts"], + dependencies: ["unifi.access"], + }, + { + node: "unifi.site.wifi.read", + description: + "Field-level gate for WiFi network response data. Each key on the WlanConf object is checked as unifi.site.wifi.read.. Only fields the user has permission for are included in the response.", + usedIn: ["src/api/unifi/site/wifi/fetchAll.ts"], + dependencies: ["unifi.access", "unifi.site.wifi"], + fieldLevelPermissions: [ + "unifi.site.wifi.read.id", + "unifi.site.wifi.read.name", + "unifi.site.wifi.read.siteId", + "unifi.site.wifi.read.enabled", + "unifi.site.wifi.read.security", + "unifi.site.wifi.read.wpaMode", + "unifi.site.wifi.read.wpaEnc", + "unifi.site.wifi.read.wpa3Support", + "unifi.site.wifi.read.wpa3Transition", + "unifi.site.wifi.read.wpa3FastRoaming", + "unifi.site.wifi.read.wpa3Enhanced192", + "unifi.site.wifi.read.passphrase", + "unifi.site.wifi.read.passphraseAutogenerated", + "unifi.site.wifi.read.hideSSID", + "unifi.site.wifi.read.isGuest", + "unifi.site.wifi.read.band", + "unifi.site.wifi.read.bands", + "unifi.site.wifi.read.networkconfId", + "unifi.site.wifi.read.usergroupId", + "unifi.site.wifi.read.apGroupIds", + "unifi.site.wifi.read.apGroupMode", + "unifi.site.wifi.read.pmfMode", + "unifi.site.wifi.read.groupRekey", + "unifi.site.wifi.read.dtimMode", + "unifi.site.wifi.read.dtimNg", + "unifi.site.wifi.read.dtimNa", + "unifi.site.wifi.read.dtim6e", + "unifi.site.wifi.read.l2Isolation", + "unifi.site.wifi.read.fastRoamingEnabled", + "unifi.site.wifi.read.bssTransition", + "unifi.site.wifi.read.uapsdEnabled", + "unifi.site.wifi.read.iappEnabled", + "unifi.site.wifi.read.proxyArp", + "unifi.site.wifi.read.mcastenhanceEnabled", + "unifi.site.wifi.read.macFilterEnabled", + "unifi.site.wifi.read.macFilterPolicy", + "unifi.site.wifi.read.macFilterList", + "unifi.site.wifi.read.radiusDasEnabled", + "unifi.site.wifi.read.radiusMacAuthEnabled", + "unifi.site.wifi.read.radiusMacaclFormat", + "unifi.site.wifi.read.minrateSettingPreference", + "unifi.site.wifi.read.minrateNgEnabled", + "unifi.site.wifi.read.minrateNgDataRateKbps", + "unifi.site.wifi.read.minrateNgAdvertisingRates", + "unifi.site.wifi.read.minrateNaEnabled", + "unifi.site.wifi.read.minrateNaDataRateKbps", + "unifi.site.wifi.read.minrateNaAdvertisingRates", + "unifi.site.wifi.read.settingPreference", + "unifi.site.wifi.read.no2ghzOui", + "unifi.site.wifi.read.privatePreSharedKeysEnabled", + "unifi.site.wifi.read.privatePreSharedKeys", + "unifi.site.wifi.read.saeGroups", + "unifi.site.wifi.read.saePsk", + "unifi.site.wifi.read.schedule", + "unifi.site.wifi.read.scheduleWithDuration", + "unifi.site.wifi.read.bcFilterList", + "unifi.site.wifi.read.externalId", + ], + }, + { + node: "unifi.site.wifi.update", + description: "Update a WiFi network on the UniFi controller", + usedIn: ["src/api/unifi/site/wifi/update.ts"], + dependencies: ["unifi.access", "unifi.site.wifi"], + }, + { + node: "unifi.site.networks", + description: "View network configurations from the UniFi controller", + usedIn: ["src/api/unifi/site/networks.ts"], + dependencies: ["unifi.access"], + }, + { + node: "unifi.site.wlan-groups", + description: + "View WLAN groups (AP broadcasting groups) from the UniFi controller for a site", + usedIn: [ + "src/api/unifi/site/wlanGroups.ts", + "src/api/unifi/site/wlanGroupsCreate.ts", + ], + dependencies: ["unifi.access"], + }, + { + node: "unifi.site.wlan-groups.create", + description: + "Create a new WLAN group (AP broadcasting group) on the UniFi controller", + usedIn: ["src/api/unifi/site/wlanGroupsCreate.ts"], + dependencies: ["unifi.access", "unifi.site.wlan-groups"], + }, + { + node: "unifi.site.access-points", + description: + "View access points (UAPs only) from the UniFi controller for a site", + usedIn: ["src/api/unifi/site/accessPoints.ts"], + dependencies: ["unifi.access"], + }, + { + node: "unifi.site.ap-groups", + description: + "View AP groups from the UniFi controller — shows which access points are grouped together for SSID broadcasting", + usedIn: ["src/api/unifi/site/apGroups.ts"], + dependencies: ["unifi.access"], + }, + { + node: "unifi.site.wifi-limits", + description: + "View WiFi SSID limits per access point per radio band — shows how many SSIDs each AP radio can still accept", + usedIn: ["src/api/unifi/site/wifiLimits.ts"], + dependencies: ["unifi.access"], + }, + { + node: "unifi.site.speed-profiles", + description: + "View speed limit profiles (user groups) from the UniFi controller", + usedIn: [ + "src/api/unifi/site/speedProfilesFetchAll.ts", + "src/api/unifi/site/speedProfilesCreate.ts", + ], + dependencies: ["unifi.access"], + }, + { + node: "unifi.site.speed-profiles.create", + description: + "Create a new speed limit profile (user group) on the UniFi controller", + usedIn: ["src/api/unifi/site/speedProfilesCreate.ts"], + dependencies: ["unifi.access", "unifi.site.speed-profiles"], + }, + { + node: "unifi.site.wifi.ppsk", + description: + "View private pre-shared keys (PPSKs) for a specific WiFi network", + usedIn: [ + "src/api/unifi/site/wifi/ppskFetchAll.ts", + "src/api/unifi/site/wifi/ppskCreate.ts", + ], + dependencies: ["unifi.access", "unifi.site.wifi"], + }, + { + node: "unifi.site.wifi.ppsk.create", + description: + "Create a private pre-shared key on a specific WiFi network", + usedIn: ["src/api/unifi/site/wifi/ppskCreate.ts"], + dependencies: [ + "unifi.access", + "unifi.site.wifi", + "unifi.site.wifi.ppsk", + ], + }, + ], + }, } as const satisfies Record; /** diff --git a/test-unifi-endpoints.ts b/test-unifi-endpoints.ts new file mode 100644 index 0000000..f415b80 --- /dev/null +++ b/test-unifi-endpoints.ts @@ -0,0 +1,133 @@ +// Test script to probe UniFi API endpoints for response shapes +import axios, { AxiosInstance } from "axios"; + +process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; + +const controllerBaseUrl = "https://unifi.totaltech.net"; +const site = "km9b1v8i"; +const username = "admin"; +const password = "Tt$Un1fiIZth3B3$t26"; + +class TestClient { + private client: AxiosInstance; + + constructor(baseURL: string) { + this.client = axios.create({ + baseURL, + validateStatus: (s) => s >= 200 && s < 400, + }); + } + + private persistSession(res: { headers: Record }): void { + const raw = res.headers["set-cookie"]; + if (raw) { + const cookies = (Array.isArray(raw) ? raw : [raw]) as string[]; + const cookieString = cookies.map((c) => c.split(";")[0]).join("; "); + this.client.defaults.headers.common["Cookie"] = cookieString; + } + const csrf = res.headers["x-csrf-token"]; + if (typeof csrf === "string") { + this.client.defaults.headers.common["X-CSRF-Token"] = csrf; + } + } + + async login(): Promise { + try { + const res = await this.client.post("/api/auth/login", { + username, + password, + }); + console.log("Login OK (UniFi OS)", res.status); + this.persistSession(res); + } catch (e) { + const res = await this.client.post("/api/login", { username, password }); + console.log("Login OK (legacy)", res.status); + this.persistSession(res); + } + } + + async tryGet(label: string, paths: string[]): Promise { + for (const path of paths) { + try { + const res = await this.client.get(path); + const data = res.data?.data ?? res.data; + console.log(`\n=== ${label} (${path}) ===`); + console.log(JSON.stringify(data, null, 2)); + return data; + } catch (e: any) { + console.log(` Failed ${path}: ${e.response?.status ?? e.message}`); + } + } + console.log(` Could not fetch ${label} from any path`); + return null; + } +} + +async function main() { + const client = new TestClient(controllerBaseUrl); + await client.login(); + + // 1. WLAN Groups (AP groups in UniFi) + await client.tryGet("WLAN Groups", [ + `/proxy/network/api/s/${site}/rest/wlangroup`, + `/api/s/${site}/rest/wlangroup`, + ]); + + // 2. User Groups (bandwidth/speed limit profiles) + await client.tryGet("User Groups (Speed Profiles)", [ + `/proxy/network/api/s/${site}/rest/usergroup`, + `/api/s/${site}/rest/usergroup`, + ]); + + // 3. Devices - APs only (compact) + const devices = await client.tryGet("Devices", [ + `/proxy/network/api/s/${site}/stat/device`, + ]); + if (devices) { + const aps = devices.filter((d: any) => d.type === "uap"); + console.log(`\n=== APs (${aps.length}) - compact ===`); + aps.forEach((ap: any) => { + console.log( + JSON.stringify({ + _id: ap._id, + name: ap.name, + mac: ap.mac, + model: ap.model, + radio_table: ap.radio_table?.map((r: any) => ({ + radio: r.radio, + name: r.name, + })), + wlangroup_id_ng: ap.wlangroup_id_ng, + wlangroup_id_na: ap.wlangroup_id_na, + vap_table_count: ap.vap_table?.length, + }), + ); + }); + } + + // 4. One full WLAN to see private_preshared_keys structure + const wlans = await client.tryGet("WLANs", [ + `/proxy/network/api/s/${site}/rest/wlanconf`, + ]); + if (wlans) { + // Log just the PPSK-related fields from each WLAN + console.log("\n=== PPSK fields per WLAN ==="); + wlans.forEach((w: any) => { + console.log( + JSON.stringify({ + name: w.name, + _id: w._id, + private_preshared_keys_enabled: w.private_preshared_keys_enabled, + private_preshared_keys: w.private_preshared_keys, + ap_group_ids: w.ap_group_ids, + ap_group_mode: w.ap_group_mode, + wlan_band: w.wlan_band, + wlan_bands: w.wlan_bands, + usergroup_id: w.usergroup_id, + }), + ); + }); + } +} + +main().catch((e) => console.error(e)); diff --git a/testunifi copy.ts b/testunifi copy.ts new file mode 100644 index 0000000..e90dc89 --- /dev/null +++ b/testunifi copy.ts @@ -0,0 +1,123 @@ +// unifi-wifi-list.ts +import axios, { AxiosInstance } from "axios"; + +process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; + +const controllerBaseUrl = "https://unifi.totaltech.net"; +const site = "km9b1v8i"; +const username = "admin"; +const password = "Tt$Un1fiIZth3B3$t26"; + +interface WlanConfRaw { + _id: string; + name?: string; + ssid?: string; + x_passphrase?: string; + [key: string]: unknown; +} + +interface WlanConf { + id: string; + ssid: string; + password: string | null; +} + +class UnifiClient { + private client: AxiosInstance; + + constructor(baseURL: string) { + this.client = axios.create({ + baseURL, + validateStatus: (s) => s >= 200 && s < 400, + }); + } + + private persistSession(res: { headers: Record }): void { + // Cookies + const raw = res.headers["set-cookie"]; + if (raw) { + const cookies = (Array.isArray(raw) ? raw : [raw]) as string[]; + const cookieString = cookies.map((c) => c.split(";")[0]).join("; "); + this.client.defaults.headers.common["Cookie"] = cookieString; + } + // CSRF token (UniFi OS) + const csrf = res.headers["x-csrf-token"]; + if (typeof csrf === "string") { + this.client.defaults.headers.common["X-CSRF-Token"] = csrf; + } + } + + async login(username: string, password: string): Promise { + const body = { username, password }; + + try { + // UniFi OS + const res = await this.client.post("/api/auth/login", body); + console.log("Login OK (UniFi OS)", res.status); + this.persistSession(res); + } catch (e) { + // Legacy controller + console.log("UniFi OS login failed, trying legacy..."); + const res = await this.client.post("/api/login", body); + console.log("Login OK (legacy)", res.status); + this.persistSession(res); + } + } + + private async fetchWlanConfRaw(site: string): Promise { + const paths = [ + `/proxy/network/api/s/${site}/rest/wlanconf`, + `/api/s/${site}/rest/wlanconf`, + ]; + + for (const path of paths) { + try { + const res = await this.client.get(path); + const data = (res.data?.data ?? res.data) as WlanConfRaw[]; + console.log(`Fetched wlan from ${path}`); + return data; + } catch (e) { + console.log( + `Failed ${path}:`, + axios.isAxiosError(e) ? e.response?.status : e, + ); + } + } + + throw new Error("Could not fetch WLAN config from any known path"); + } + + async getWlanConf(site: string): Promise { + const raw = await this.fetchWlanConfRaw(site); + + return raw.map( + (w): WlanConf => ({ + id: w._id, + ssid: (w.name || w.ssid || "").toString(), + password: typeof w.x_passphrase === "string" ? w.x_passphrase : null, + }), + ); + } +} + +async function main() { + const unifi = new UnifiClient(controllerBaseUrl); + + try { + await unifi.login(username, password); + + const wlans = await unifi.getWlanConf(site); + + wlans.forEach((wlan) => { + console.log(`${wlan.ssid}: ${wlan.password ?? ""}`); + }); + } catch (err) { + if (axios.isAxiosError(err)) { + console.error("HTTP error", err.response?.status, err.response?.data); + } else { + console.error("Error", err); + } + } +} + +main().catch((e) => console.error(e)); diff --git a/testunifi.ts b/testunifi.ts new file mode 100644 index 0000000..e90dc89 --- /dev/null +++ b/testunifi.ts @@ -0,0 +1,123 @@ +// unifi-wifi-list.ts +import axios, { AxiosInstance } from "axios"; + +process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; + +const controllerBaseUrl = "https://unifi.totaltech.net"; +const site = "km9b1v8i"; +const username = "admin"; +const password = "Tt$Un1fiIZth3B3$t26"; + +interface WlanConfRaw { + _id: string; + name?: string; + ssid?: string; + x_passphrase?: string; + [key: string]: unknown; +} + +interface WlanConf { + id: string; + ssid: string; + password: string | null; +} + +class UnifiClient { + private client: AxiosInstance; + + constructor(baseURL: string) { + this.client = axios.create({ + baseURL, + validateStatus: (s) => s >= 200 && s < 400, + }); + } + + private persistSession(res: { headers: Record }): void { + // Cookies + const raw = res.headers["set-cookie"]; + if (raw) { + const cookies = (Array.isArray(raw) ? raw : [raw]) as string[]; + const cookieString = cookies.map((c) => c.split(";")[0]).join("; "); + this.client.defaults.headers.common["Cookie"] = cookieString; + } + // CSRF token (UniFi OS) + const csrf = res.headers["x-csrf-token"]; + if (typeof csrf === "string") { + this.client.defaults.headers.common["X-CSRF-Token"] = csrf; + } + } + + async login(username: string, password: string): Promise { + const body = { username, password }; + + try { + // UniFi OS + const res = await this.client.post("/api/auth/login", body); + console.log("Login OK (UniFi OS)", res.status); + this.persistSession(res); + } catch (e) { + // Legacy controller + console.log("UniFi OS login failed, trying legacy..."); + const res = await this.client.post("/api/login", body); + console.log("Login OK (legacy)", res.status); + this.persistSession(res); + } + } + + private async fetchWlanConfRaw(site: string): Promise { + const paths = [ + `/proxy/network/api/s/${site}/rest/wlanconf`, + `/api/s/${site}/rest/wlanconf`, + ]; + + for (const path of paths) { + try { + const res = await this.client.get(path); + const data = (res.data?.data ?? res.data) as WlanConfRaw[]; + console.log(`Fetched wlan from ${path}`); + return data; + } catch (e) { + console.log( + `Failed ${path}:`, + axios.isAxiosError(e) ? e.response?.status : e, + ); + } + } + + throw new Error("Could not fetch WLAN config from any known path"); + } + + async getWlanConf(site: string): Promise { + const raw = await this.fetchWlanConfRaw(site); + + return raw.map( + (w): WlanConf => ({ + id: w._id, + ssid: (w.name || w.ssid || "").toString(), + password: typeof w.x_passphrase === "string" ? w.x_passphrase : null, + }), + ); + } +} + +async function main() { + const unifi = new UnifiClient(controllerBaseUrl); + + try { + await unifi.login(username, password); + + const wlans = await unifi.getWlanConf(site); + + wlans.forEach((wlan) => { + console.log(`${wlan.ssid}: ${wlan.password ?? ""}`); + }); + } catch (err) { + if (axios.isAxiosError(err)) { + console.error("HTTP error", err.response?.status, err.response?.data); + } else { + console.error("Error", err); + } + } +} + +main().catch((e) => console.error(e));