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
+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);
},
};