setup unifi wlans

This commit is contained in:
2026-02-22 19:12:34 -06:00
parent 70284bc14e
commit 3c89f24189
66 changed files with 7393 additions and 110 deletions
+10
View File
@@ -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 `<scope>.<field>` (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 `<scope>.<field>` 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 `<scope>.<field>` 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.
+1353 -5
View File
File diff suppressed because it is too large Load Diff
+51 -10
View File
@@ -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.<field>` | 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.<key>`. 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:
+5
View File
@@ -32,6 +32,11 @@ export type User = Prisma.UserModel
*
*/
export type Role = Prisma.RoleModel
/**
* Model UnifiSite
*
*/
export type UnifiSite = Prisma.UnifiSiteModel
/**
* Model Company
*
+5
View File
@@ -54,6 +54,11 @@ export type User = Prisma.UserModel
*
*/
export type Role = Prisma.RoleModel
/**
* Model UnifiSite
*
*/
export type UnifiSite = Prisma.UnifiSiteModel
/**
* Model Company
*
File diff suppressed because one or more lines are too long
+90 -1
View File
@@ -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<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
omit: GlobalOmitOptions
}
meta: {
modelProps: "session" | "user" | "role" | "company" | "credentialType" | "secureValue" | "credential"
modelProps: "session" | "user" | "role" | "unifiSite" | "company" | "credentialType" | "secureValue" | "credential"
txIsolationLevel: TransactionIsolationLevel
}
model: {
@@ -632,6 +633,80 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
}
}
}
UnifiSite: {
payload: Prisma.$UnifiSitePayload<ExtArgs>
fields: Prisma.UnifiSiteFieldRefs
operations: {
findUnique: {
args: Prisma.UnifiSiteFindUniqueArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$UnifiSitePayload> | null
}
findUniqueOrThrow: {
args: Prisma.UnifiSiteFindUniqueOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$UnifiSitePayload>
}
findFirst: {
args: Prisma.UnifiSiteFindFirstArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$UnifiSitePayload> | null
}
findFirstOrThrow: {
args: Prisma.UnifiSiteFindFirstOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$UnifiSitePayload>
}
findMany: {
args: Prisma.UnifiSiteFindManyArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$UnifiSitePayload>[]
}
create: {
args: Prisma.UnifiSiteCreateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$UnifiSitePayload>
}
createMany: {
args: Prisma.UnifiSiteCreateManyArgs<ExtArgs>
result: BatchPayload
}
createManyAndReturn: {
args: Prisma.UnifiSiteCreateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$UnifiSitePayload>[]
}
delete: {
args: Prisma.UnifiSiteDeleteArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$UnifiSitePayload>
}
update: {
args: Prisma.UnifiSiteUpdateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$UnifiSitePayload>
}
deleteMany: {
args: Prisma.UnifiSiteDeleteManyArgs<ExtArgs>
result: BatchPayload
}
updateMany: {
args: Prisma.UnifiSiteUpdateManyArgs<ExtArgs>
result: BatchPayload
}
updateManyAndReturn: {
args: Prisma.UnifiSiteUpdateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$UnifiSitePayload>[]
}
upsert: {
args: Prisma.UnifiSiteUpsertArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$UnifiSitePayload>
}
aggregate: {
args: Prisma.UnifiSiteAggregateArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.AggregateUnifiSite>
}
groupBy: {
args: Prisma.UnifiSiteGroupByArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.UnifiSiteGroupByOutputType>[]
}
count: {
args: Prisma.UnifiSiteCountArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.UnifiSiteCountAggregateOutputType> | number
}
}
}
Company: {
payload: Prisma.$CompanyPayload<ExtArgs>
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
@@ -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',
+1
View File
@@ -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'
+125
View File
@@ -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<Prisma.CompanyCreateWithoutUnifiSitesInput, Prisma.CompanyUncheckedCreateWithoutUnifiSitesInput>
connectOrCreate?: Prisma.CompanyCreateOrConnectWithoutUnifiSitesInput
connect?: Prisma.CompanyWhereUniqueInput
}
export type CompanyUpdateOneWithoutUnifiSitesNestedInput = {
create?: Prisma.XOR<Prisma.CompanyCreateWithoutUnifiSitesInput, Prisma.CompanyUncheckedCreateWithoutUnifiSitesInput>
connectOrCreate?: Prisma.CompanyCreateOrConnectWithoutUnifiSitesInput
upsert?: Prisma.CompanyUpsertWithoutUnifiSitesInput
disconnect?: Prisma.CompanyWhereInput | boolean
delete?: Prisma.CompanyWhereInput | boolean
connect?: Prisma.CompanyWhereUniqueInput
update?: Prisma.XOR<Prisma.XOR<Prisma.CompanyUpdateToOneWithWhereWithoutUnifiSitesInput, Prisma.CompanyUpdateWithoutUnifiSitesInput>, Prisma.CompanyUncheckedUpdateWithoutUnifiSitesInput>
}
export type IntFieldUpdateOperationsInput = {
set?: number
increment?: number
@@ -405,6 +433,62 @@ export type CompanyUpdateOneRequiredWithoutCredentialsNestedInput = {
update?: Prisma.XOR<Prisma.XOR<Prisma.CompanyUpdateToOneWithWhereWithoutCredentialsInput, Prisma.CompanyUpdateWithoutCredentialsInput>, 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<Prisma.CompanyCreateWithoutUnifiSitesInput, Prisma.CompanyUncheckedCreateWithoutUnifiSitesInput>
}
export type CompanyUpsertWithoutUnifiSitesInput = {
update: Prisma.XOR<Prisma.CompanyUpdateWithoutUnifiSitesInput, Prisma.CompanyUncheckedUpdateWithoutUnifiSitesInput>
create: Prisma.XOR<Prisma.CompanyCreateWithoutUnifiSitesInput, Prisma.CompanyUncheckedCreateWithoutUnifiSitesInput>
where?: Prisma.CompanyWhereInput
}
export type CompanyUpdateToOneWithWhereWithoutUnifiSitesInput = {
where?: Prisma.CompanyWhereInput
data: Prisma.XOR<Prisma.CompanyUpdateWithoutUnifiSitesInput, Prisma.CompanyUncheckedUpdateWithoutUnifiSitesInput>
}
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<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
credentials?: boolean | CompanyCountOutputTypeCountCredentialsArgs
unifiSites?: boolean | CompanyCountOutputTypeCountUnifiSitesArgs
}
/**
@@ -487,6 +577,13 @@ export type CompanyCountOutputTypeCountCredentialsArgs<ExtArgs extends runtime.T
where?: Prisma.CredentialWhereInput
}
/**
* CompanyCountOutputType without action
*/
export type CompanyCountOutputTypeCountUnifiSitesArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
where?: Prisma.UnifiSiteWhereInput
}
export type CompanySelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean
@@ -496,6 +593,7 @@ export type CompanySelect<ExtArgs extends runtime.Types.Extensions.InternalArgs
createdAt?: boolean
updatedAt?: boolean
credentials?: boolean | Prisma.Company$credentialsArgs<ExtArgs>
unifiSites?: boolean | Prisma.Company$unifiSitesArgs<ExtArgs>
_count?: boolean | Prisma.CompanyCountOutputTypeDefaultArgs<ExtArgs>
}, ExtArgs["result"]["company"]>
@@ -529,6 +627,7 @@ export type CompanySelectScalar = {
export type CompanyOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "name" | "cw_CompanyId" | "cw_Identifier" | "createdAt" | "updatedAt", ExtArgs["result"]["company"]>
export type CompanyInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
credentials?: boolean | Prisma.Company$credentialsArgs<ExtArgs>
unifiSites?: boolean | Prisma.Company$unifiSitesArgs<ExtArgs>
_count?: boolean | Prisma.CompanyCountOutputTypeDefaultArgs<ExtArgs>
}
export type CompanyIncludeCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {}
@@ -538,6 +637,7 @@ export type $CompanyPayload<ExtArgs extends runtime.Types.Extensions.InternalArg
name: "Company"
objects: {
credentials: Prisma.$CredentialPayload<ExtArgs>[]
unifiSites: Prisma.$UnifiSitePayload<ExtArgs>[]
}
scalars: runtime.Types.Extensions.GetPayloadResult<{
id: string
@@ -941,6 +1041,7 @@ readonly fields: CompanyFieldRefs;
export interface Prisma__CompanyClient<T, Null = never, ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs, GlobalOmitOptions = {}> extends Prisma.PrismaPromise<T> {
readonly [Symbol.toStringTag]: "PrismaPromise"
credentials<T extends Prisma.Company$credentialsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Company$credentialsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$CredentialPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
unifiSites<T extends Prisma.Company$unifiSitesArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Company$unifiSitesArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$UnifiSitePayload<ExtArgs>, 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<ExtArgs extends runtime.Types.Extensions.Int
distinct?: Prisma.CredentialScalarFieldEnum | Prisma.CredentialScalarFieldEnum[]
}
/**
* Company.unifiSites
*/
export type Company$unifiSitesArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
/**
* Select specific fields to fetch from the UnifiSite
*/
select?: Prisma.UnifiSiteSelect<ExtArgs> | null
/**
* Omit specific fields from the UnifiSite
*/
omit?: Prisma.UnifiSiteOmit<ExtArgs> | null
/**
* Choose, which related nodes to fetch as well
*/
include?: Prisma.UnifiSiteInclude<ExtArgs> | null
where?: Prisma.UnifiSiteWhereInput
orderBy?: Prisma.UnifiSiteOrderByWithRelationInput | Prisma.UnifiSiteOrderByWithRelationInput[]
cursor?: Prisma.UnifiSiteWhereUniqueInput
take?: number
skip?: number
distinct?: Prisma.UnifiSiteScalarFieldEnum | Prisma.UnifiSiteScalarFieldEnum[]
}
/**
* Company without action
*/
+362 -1
View File
@@ -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<Prisma.CredentialNullableScalarRelationFilter, Prisma.CredentialWhereInput> | null
subCredentials?: Prisma.CredentialListRelationFilter
type?: Prisma.XOR<Prisma.CredentialTypeScalarRelationFilter, Prisma.CredentialTypeWhereInput>
company?: Prisma.XOR<Prisma.CompanyScalarRelationFilter, Prisma.CompanyWhereInput>
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<Prisma.CredentialNullableScalarRelationFilter, Prisma.CredentialWhereInput> | null
subCredentials?: Prisma.CredentialListRelationFilter
type?: Prisma.XOR<Prisma.CredentialTypeScalarRelationFilter, Prisma.CredentialTypeWhereInput>
company?: Prisma.XOR<Prisma.CompanyScalarRelationFilter, Prisma.CompanyWhereInput>
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.XOR<Prisma.CredentialUpdateToOneWithWhereWithoutSecurevaluesInput, Prisma.CredentialUpdateWithoutSecurevaluesInput>, Prisma.CredentialUncheckedUpdateWithoutSecurevaluesInput>
}
export type CredentialCreateNestedOneWithoutSubCredentialsInput = {
create?: Prisma.XOR<Prisma.CredentialCreateWithoutSubCredentialsInput, Prisma.CredentialUncheckedCreateWithoutSubCredentialsInput>
connectOrCreate?: Prisma.CredentialCreateOrConnectWithoutSubCredentialsInput
connect?: Prisma.CredentialWhereUniqueInput
}
export type CredentialCreateNestedManyWithoutSubCredentialOfInput = {
create?: Prisma.XOR<Prisma.CredentialCreateWithoutSubCredentialOfInput, Prisma.CredentialUncheckedCreateWithoutSubCredentialOfInput> | 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> | Prisma.CredentialCreateWithoutSubCredentialOfInput[] | Prisma.CredentialUncheckedCreateWithoutSubCredentialOfInput[]
connectOrCreate?: Prisma.CredentialCreateOrConnectWithoutSubCredentialOfInput | Prisma.CredentialCreateOrConnectWithoutSubCredentialOfInput[]
createMany?: Prisma.CredentialCreateManySubCredentialOfInputEnvelope
connect?: Prisma.CredentialWhereUniqueInput | Prisma.CredentialWhereUniqueInput[]
}
export type CredentialUpdateOneWithoutSubCredentialsNestedInput = {
create?: Prisma.XOR<Prisma.CredentialCreateWithoutSubCredentialsInput, Prisma.CredentialUncheckedCreateWithoutSubCredentialsInput>
connectOrCreate?: Prisma.CredentialCreateOrConnectWithoutSubCredentialsInput
upsert?: Prisma.CredentialUpsertWithoutSubCredentialsInput
disconnect?: Prisma.CredentialWhereInput | boolean
delete?: Prisma.CredentialWhereInput | boolean
connect?: Prisma.CredentialWhereUniqueInput
update?: Prisma.XOR<Prisma.XOR<Prisma.CredentialUpdateToOneWithWhereWithoutSubCredentialsInput, Prisma.CredentialUpdateWithoutSubCredentialsInput>, Prisma.CredentialUncheckedUpdateWithoutSubCredentialsInput>
}
export type CredentialUpdateManyWithoutSubCredentialOfNestedInput = {
create?: Prisma.XOR<Prisma.CredentialCreateWithoutSubCredentialOfInput, Prisma.CredentialUncheckedCreateWithoutSubCredentialOfInput> | 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> | 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<Prisma.CredentialCreateWithoutSubCredentialsInput, Prisma.CredentialUncheckedCreateWithoutSubCredentialsInput>
}
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<Prisma.CredentialCreateWithoutSubCredentialOfInput, Prisma.CredentialUncheckedCreateWithoutSubCredentialOfInput>
}
export type CredentialCreateManySubCredentialOfInputEnvelope = {
data: Prisma.CredentialCreateManySubCredentialOfInput | Prisma.CredentialCreateManySubCredentialOfInput[]
skipDuplicates?: boolean
}
export type CredentialUpsertWithoutSubCredentialsInput = {
update: Prisma.XOR<Prisma.CredentialUpdateWithoutSubCredentialsInput, Prisma.CredentialUncheckedUpdateWithoutSubCredentialsInput>
create: Prisma.XOR<Prisma.CredentialCreateWithoutSubCredentialsInput, Prisma.CredentialUncheckedCreateWithoutSubCredentialsInput>
where?: Prisma.CredentialWhereInput
}
export type CredentialUpdateToOneWithWhereWithoutSubCredentialsInput = {
where?: Prisma.CredentialWhereInput
data: Prisma.XOR<Prisma.CredentialUpdateWithoutSubCredentialsInput, Prisma.CredentialUncheckedUpdateWithoutSubCredentialsInput>
}
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<Prisma.CredentialUpdateWithoutSubCredentialOfInput, Prisma.CredentialUncheckedUpdateWithoutSubCredentialOfInput>
create: Prisma.XOR<Prisma.CredentialCreateWithoutSubCredentialOfInput, Prisma.CredentialUncheckedCreateWithoutSubCredentialOfInput>
}
export type CredentialUpdateWithWhereUniqueWithoutSubCredentialOfInput = {
where: Prisma.CredentialWhereUniqueInput
data: Prisma.XOR<Prisma.CredentialUpdateWithoutSubCredentialOfInput, Prisma.CredentialUncheckedUpdateWithoutSubCredentialOfInput>
}
export type CredentialUpdateManyWithWhereWithoutSubCredentialOfInput = {
where: Prisma.CredentialScalarWhereInput
data: Prisma.XOR<Prisma.CredentialUpdateManyMutationInput, Prisma.CredentialUncheckedUpdateManyWithoutSubCredentialOfInput>
}
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<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
subCredentials?: boolean | CredentialCountOutputTypeCountSubCredentialsArgs
securevalues?: boolean | CredentialCountOutputTypeCountSecurevaluesArgs
}
@@ -766,6 +1059,13 @@ export type CredentialCountOutputTypeDefaultArgs<ExtArgs extends runtime.Types.E
select?: Prisma.CredentialCountOutputTypeSelect<ExtArgs> | null
}
/**
* CredentialCountOutputType without action
*/
export type CredentialCountOutputTypeCountSubCredentialsArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
where?: Prisma.CredentialWhereInput
}
/**
* CredentialCountOutputType without action
*/
@@ -778,11 +1078,14 @@ export type CredentialSelect<ExtArgs extends runtime.Types.Extensions.InternalAr
id?: boolean
name?: boolean
notes?: boolean
subCredentialOfId?: boolean
typeId?: boolean
fields?: boolean
companyId?: boolean
createdAt?: boolean
updatedAt?: boolean
subCredentialOf?: boolean | Prisma.Credential$subCredentialOfArgs<ExtArgs>
subCredentials?: boolean | Prisma.Credential$subCredentialsArgs<ExtArgs>
type?: boolean | Prisma.CredentialTypeDefaultArgs<ExtArgs>
company?: boolean | Prisma.CompanyDefaultArgs<ExtArgs>
securevalues?: boolean | Prisma.Credential$securevaluesArgs<ExtArgs>
@@ -793,11 +1096,13 @@ export type CredentialSelectCreateManyAndReturn<ExtArgs extends runtime.Types.Ex
id?: boolean
name?: boolean
notes?: boolean
subCredentialOfId?: boolean
typeId?: boolean
fields?: boolean
companyId?: boolean
createdAt?: boolean
updatedAt?: boolean
subCredentialOf?: boolean | Prisma.Credential$subCredentialOfArgs<ExtArgs>
type?: boolean | Prisma.CredentialTypeDefaultArgs<ExtArgs>
company?: boolean | Prisma.CompanyDefaultArgs<ExtArgs>
}, ExtArgs["result"]["credential"]>
@@ -806,11 +1111,13 @@ export type CredentialSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Ex
id?: boolean
name?: boolean
notes?: boolean
subCredentialOfId?: boolean
typeId?: boolean
fields?: boolean
companyId?: boolean
createdAt?: boolean
updatedAt?: boolean
subCredentialOf?: boolean | Prisma.Credential$subCredentialOfArgs<ExtArgs>
type?: boolean | Prisma.CredentialTypeDefaultArgs<ExtArgs>
company?: boolean | Prisma.CompanyDefaultArgs<ExtArgs>
}, 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<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "name" | "notes" | "typeId" | "fields" | "companyId" | "createdAt" | "updatedAt", ExtArgs["result"]["credential"]>
export type CredentialOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "name" | "notes" | "subCredentialOfId" | "typeId" | "fields" | "companyId" | "createdAt" | "updatedAt", ExtArgs["result"]["credential"]>
export type CredentialInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
subCredentialOf?: boolean | Prisma.Credential$subCredentialOfArgs<ExtArgs>
subCredentials?: boolean | Prisma.Credential$subCredentialsArgs<ExtArgs>
type?: boolean | Prisma.CredentialTypeDefaultArgs<ExtArgs>
company?: boolean | Prisma.CompanyDefaultArgs<ExtArgs>
securevalues?: boolean | Prisma.Credential$securevaluesArgs<ExtArgs>
_count?: boolean | Prisma.CredentialCountOutputTypeDefaultArgs<ExtArgs>
}
export type CredentialIncludeCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
subCredentialOf?: boolean | Prisma.Credential$subCredentialOfArgs<ExtArgs>
type?: boolean | Prisma.CredentialTypeDefaultArgs<ExtArgs>
company?: boolean | Prisma.CompanyDefaultArgs<ExtArgs>
}
export type CredentialIncludeUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
subCredentialOf?: boolean | Prisma.Credential$subCredentialOfArgs<ExtArgs>
type?: boolean | Prisma.CredentialTypeDefaultArgs<ExtArgs>
company?: boolean | Prisma.CompanyDefaultArgs<ExtArgs>
}
@@ -845,6 +1157,8 @@ export type CredentialIncludeUpdateManyAndReturn<ExtArgs extends runtime.Types.E
export type $CredentialPayload<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
name: "Credential"
objects: {
subCredentialOf: Prisma.$CredentialPayload<ExtArgs> | null
subCredentials: Prisma.$CredentialPayload<ExtArgs>[]
type: Prisma.$CredentialTypePayload<ExtArgs>
company: Prisma.$CompanyPayload<ExtArgs>
securevalues: Prisma.$SecureValuePayload<ExtArgs>[]
@@ -853,6 +1167,7 @@ export type $CredentialPayload<ExtArgs extends runtime.Types.Extensions.Internal
id: string
name: string
notes: string | null
subCredentialOfId: string | null
typeId: string
fields: runtime.JsonValue
companyId: string
@@ -1252,6 +1567,8 @@ readonly fields: CredentialFieldRefs;
*/
export interface Prisma__CredentialClient<T, Null = never, ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs, GlobalOmitOptions = {}> extends Prisma.PrismaPromise<T> {
readonly [Symbol.toStringTag]: "PrismaPromise"
subCredentialOf<T extends Prisma.Credential$subCredentialOfArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Credential$subCredentialOfArgs<ExtArgs>>): Prisma.Prisma__CredentialClient<runtime.Types.Result.GetResult<Prisma.$CredentialPayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions>
subCredentials<T extends Prisma.Credential$subCredentialsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Credential$subCredentialsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$CredentialPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
type<T extends Prisma.CredentialTypeDefaultArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.CredentialTypeDefaultArgs<ExtArgs>>): Prisma.Prisma__CredentialTypeClient<runtime.Types.Result.GetResult<Prisma.$CredentialTypePayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | Null, Null, ExtArgs, GlobalOmitOptions>
company<T extends Prisma.CompanyDefaultArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.CompanyDefaultArgs<ExtArgs>>): Prisma.Prisma__CompanyClient<runtime.Types.Result.GetResult<Prisma.$CompanyPayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | Null, Null, ExtArgs, GlobalOmitOptions>
securevalues<T extends Prisma.Credential$securevaluesArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Credential$securevaluesArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$SecureValuePayload<ExtArgs>, 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<ExtArgs extends runtime.Types.Extensions.In
limit?: number
}
/**
* Credential.subCredentialOf
*/
export type Credential$subCredentialOfArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
/**
* Select specific fields to fetch from the Credential
*/
select?: Prisma.CredentialSelect<ExtArgs> | null
/**
* Omit specific fields from the Credential
*/
omit?: Prisma.CredentialOmit<ExtArgs> | null
/**
* Choose, which related nodes to fetch as well
*/
include?: Prisma.CredentialInclude<ExtArgs> | null
where?: Prisma.CredentialWhereInput
}
/**
* Credential.subCredentials
*/
export type Credential$subCredentialsArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
/**
* Select specific fields to fetch from the Credential
*/
select?: Prisma.CredentialSelect<ExtArgs> | null
/**
* Omit specific fields from the Credential
*/
omit?: Prisma.CredentialOmit<ExtArgs> | null
/**
* Choose, which related nodes to fetch as well
*/
include?: Prisma.CredentialInclude<ExtArgs> | null
where?: Prisma.CredentialWhereInput
orderBy?: Prisma.CredentialOrderByWithRelationInput | Prisma.CredentialOrderByWithRelationInput[]
cursor?: Prisma.CredentialWhereUniqueInput
take?: number
skip?: number
distinct?: Prisma.CredentialScalarFieldEnum | Prisma.CredentialScalarFieldEnum[]
}
/**
* Credential.securevalues
*/
File diff suppressed because it is too large Load Diff
@@ -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;
+20 -3
View File
@@ -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)
+20 -1
View File
@@ -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);
},
+24
View File
@@ -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"],
}),
);
+2 -1
View File
@@ -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 };
+12 -9
View File
@@ -15,19 +15,22 @@ export default createRoute(
async (c) => {
const body = await c.req.json();
const fieldSchema: z.ZodType<any> = 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);
+12 -11
View File
@@ -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<any> = 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);
+48
View File
@@ -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"],
}),
);
+16
View File
@@ -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);
@@ -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"],
}),
);
+6
View File
@@ -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,
};
@@ -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"],
}),
);
+7
View File
@@ -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;
+1
View File
@@ -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;
+43
View File
@@ -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,
};
+23
View File
@@ -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"],
}),
);
+23
View File
@@ -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"],
}),
);
+21
View File
@@ -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"] }),
);
+20
View File
@@ -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"] }),
);
+26
View File
@@ -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"] }),
);
+21
View File
@@ -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"] }),
);
+21
View File
@@ -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"] }),
);
+40
View File
@@ -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",
],
}),
);
@@ -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"],
}),
);
+21
View File
@@ -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"] }),
);
+31
View File
@@ -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"] }),
);
+47
View File
@@ -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",
],
}),
);
+24
View File
@@ -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"],
}),
);
+117
View File
@@ -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"],
}),
);
+23
View File
@@ -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"],
}),
);
+23
View File
@@ -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"],
}),
);
+38
View File
@@ -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",
],
}),
);
+25
View File
@@ -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"] }),
);
+20
View File
@@ -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"] }),
);
+20
View File
@@ -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"] }),
);
+11
View File
@@ -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);
+61 -14
View File
@@ -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
);
})(),
})),
},
};
}
+88 -11
View File
@@ -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<string, any>;
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<string, any>;
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<string, any>;
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<string, any> {
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,
+29
View File
@@ -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,
};
}
}
+6
View File
@@ -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,
+13 -2
View File
@@ -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() {
-2
View File
@@ -79,8 +79,6 @@ export const credentialTypes = {
});
}
console.log(data.fields);
const credentialType = await prisma.credentialType.create({
data: {
name: data.name,
+288 -26
View File
@@ -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<CredentialController> {
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<CredentialController[]>} - Array of credential controllers
*/
async fetchByCompany(companyId: string): Promise<CredentialController[]> {
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<CredentialController>} - 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<CredentialController> {
// 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<string, any> = {};
@@ -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<string, any> = {};
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<CredentialController>} - The created sub-credential controller
*/
async addSubCredential(
parentId: string,
fieldId: string,
data: {
name: string;
fields: { fieldId: string; value: string }[];
},
): Promise<CredentialController> {
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<string, any> = {};
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<string, any>;
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<void>}
*/
async removeSubCredential(
parentId: string,
subCredentialId: string,
): Promise<void> {
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<string, any>;
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<CredentialController[]>} - Array of sub-credential controllers
*/
async fetchSubCredentials(parentId: string): Promise<CredentialController[]> {
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<void>}
+293
View File
@@ -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<void> {
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<UnifiSite> {
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<UnifiSite[]> {
return prisma.unifiSite.findMany({
include: { company: true },
});
},
/**
* Fetch all UniFi site records linked to a specific company.
*/
async fetchByCompany(companyId: string): Promise<UnifiSite[]> {
return prisma.unifiSite.findMany({
where: { companyId },
});
},
/**
* Link a UniFi site to a company.
*/
async linkToCompany(siteId: string, companyId: string): Promise<UnifiSite> {
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<UnifiSite> {
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<UnifiSite[]> {
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<typeof unifi.updateWlanConf>[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<UnifiSite> {
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<typeof unifi.createWlanGroup>[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<typeof unifi.createUserGroup>[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<typeof unifi.createPrivatePSK>[2],
) {
await ensureLoggedIn();
return unifi.createPrivatePSK(siteId, wlanId, psk);
},
};
@@ -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
}
+29 -8
View File
@@ -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<ValidatedField[]> => {
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,
@@ -0,0 +1,31 @@
import UserController from "../../controllers/UserController";
export const processObjectValuePerms = async <T>(
obj: T,
scope: string, // e.g. "unifi.wifi.read"
user: UserController,
): Promise<Partial<T>> => {
let result: Partial<T> = {};
for (const key in obj) {
if (await user.hasPermission(`${scope}.${key}`)) {
result[key] = obj[key];
}
}
return result;
};
export const processObjectPermMap = async <T extends Record<string, unknown>>(
obj: T,
scope: string,
user: UserController,
): Promise<Record<keyof T, boolean>> => {
const result = {} as Record<keyof T, boolean>;
for (const key in obj) {
result[key] = await user.hasPermission(`${scope}.${key}`);
}
return result;
};
+952
View File
@@ -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<string, unknown> }): 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<void> {
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<WlanConfRaw[]> {
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<WlanConf[]> {
const raw = await this.fetchWlanConfRaw(site);
return raw.map(UnifiClient.parseWlanConf);
}
async updateWlanConf(
site: string,
wlanId: string,
updates: WlanConfUpdate,
): Promise<WlanConf> {
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<SiteListItem[]> {
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<SiteOverview> {
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<number, DeviceState> = {
0: "disconnected",
1: "connected",
2: "pending",
4: "adopting",
5: "adopting",
};
return map[state] ?? "unknown";
}
async getDevices(site: string): Promise<Device[]> {
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<Network[]> {
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<SiteListItem> {
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<WlanGroup[]> {
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<WlanGroup> {
const paths = [
`/proxy/network/api/s/${site}/rest/wlangroup`,
`/api/s/${site}/rest/wlangroup`,
];
const body: Record<string, unknown> = { 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<UserGroup[]> {
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<UserGroup> {
const paths = [
`/proxy/network/api/s/${site}/rest/usergroup`,
`/api/s/${site}/rest/usergroup`,
];
const body: Record<string, unknown> = { 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<ApGroup[]> {
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<ApGroup> {
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<ApGroup> {
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<Device[]> {
const devices = await this.getDevices(site);
return devices.filter((d) => d.type === "uap");
}
// --- WiFi Limits ---
async getWifiLimits(site: string): Promise<ApWifiLimits[]> {
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<string, { wlanNames: Set<string> }>();
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<string, string> = {
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<PrivatePSK[]> {
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<PrivatePSK[]> {
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<string, unknown> = {
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");
}
}
+434
View File
@@ -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[];
}
+140 -3
View File
@@ -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;
}
+267
View File
@@ -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 `<scope>.<fieldName>`. 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.<field>. 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<string, PermissionCategory>;
/**
+133
View File
@@ -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<string, unknown> }): 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<void> {
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<any> {
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));
+123
View File
@@ -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<string, unknown> }): 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<void> {
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<WlanConfRaw[]> {
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<WlanConf[]> {
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 ?? "<no 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));
+123
View File
@@ -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<string, unknown> }): 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<void> {
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<WlanConfRaw[]> {
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<WlanConf[]> {
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 ?? "<no 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));