CREDENTIAL TYPE MANAGEMENT WORKS

This commit is contained in:
2026-02-14 15:15:49 -06:00
parent b7637334a6
commit cdae4d47a4
46 changed files with 7621 additions and 41 deletions
+322
View File
@@ -0,0 +1,322 @@
import { z } from "zod";
import {
Credential,
CredentialType,
Company,
SecureValue,
} from "../../generated/prisma/client";
import { prisma } from "../constants";
import { fieldValidator } from "../modules/credentials/fieldValidator";
import {
CredentialField,
CredentialTypeField,
} from "../modules/credentials/credentialTypeDefs";
import { generateSecureValue } from "../modules/credentials/generateSecureValue";
import { readSecureValue } from "../modules/credentials/readSecureValue";
import GenericError from "../Errors/GenericError";
/**
* Credential Controller
*
* This class manages credential data, including field validation,
* secure value storage/retrieval, and credential updates.
*/
export class CredentialController {
public readonly id: string;
public name: string;
public readonly typeId: string;
public readonly companyId: string;
public fields: any;
private _type: CredentialType;
private _company: Company;
private _secureValues: SecureValue[];
public readonly createdAt: Date;
public updatedAt: Date;
constructor(
credentialData: Credential & {
type: CredentialType;
company: Company;
securevalues: SecureValue[];
},
) {
this.id = credentialData.id;
this.name = credentialData.name;
this.typeId = credentialData.typeId;
this.companyId = credentialData.companyId;
this.fields = credentialData.fields;
this._type = credentialData.type;
this._company = credentialData.company;
this._secureValues = credentialData.securevalues;
this.createdAt = credentialData.createdAt;
this.updatedAt = credentialData.updatedAt;
}
/**
* Update Internal Values
*
* Internal method to update all internal values when we query the database.
* This keeps everything up-to-date even when we pass around the credential controller.
*
* @param credentialData - Credential object from Prisma
*/
private _updateInternalValues(credentialData: Credential) {
this.name = credentialData.name;
this.fields = credentialData.fields;
this.updatedAt = credentialData.updatedAt;
}
/**
* Validate and Update Fields
*
* This method validates the submitted fields against the credential type's
* acceptable fields, then updates the credential in the database.
* Secure fields are encrypted and stored in the SecureValue table.
*
* @param fields - The fields to validate and update
* @returns {Promise<CredentialController>} - The updated credential controller
*/
async validateAndUpdateFields(
fields: CredentialField[],
): Promise<CredentialController> {
// Get acceptable fields from the credential type
const acceptableFields = this._type.fields as any as CredentialTypeField[];
// Validate the fields
const validatedFields = await fieldValidator(fields, acceptableFields);
// Separate secure and non-secure fields
const secureFields = validatedFields.filter((f) => f.secure);
const nonSecureFields = validatedFields.filter((f) => !f.secure);
// Process secure fields - encrypt and store in SecureValue table
await Promise.all(
secureFields.map(async (field) => {
const { encrypted, hash } = generateSecureValue(field.value);
// Check if a secure value already exists for this field
const existingSecureValue = await prisma.secureValue.findFirst({
where: {
credentialId: this.id,
name: field.fieldId,
},
});
if (existingSecureValue) {
// Update existing secure value
await prisma.secureValue.update({
where: { id: existingSecureValue.id },
data: {
content: encrypted,
hash,
},
});
} else {
// Create new secure value
await prisma.secureValue.create({
data: {
name: field.fieldId,
content: encrypted,
hash,
credentialId: this.id,
},
});
}
}),
);
// Build fields object for non-secure fields
const fieldsObject: Record<string, any> = {};
nonSecureFields.forEach((field) => {
fieldsObject[field.fieldId] = field.value;
});
// Update the credential with non-secure fields
const updatedCredential = await prisma.credential.update({
where: { id: this.id },
data: {
fields: fieldsObject,
},
});
this._updateInternalValues(updatedCredential);
// Refresh secure values
const secureValues = await prisma.secureValue.findMany({
where: { credentialId: this.id },
});
this._secureValues = secureValues;
return this;
}
/**
* Fetch All Field Values
*
* Returns all field values (both secure and non-secure) for this credential.
* Secure field values are NOT decrypted - use `readSecureFieldValue` for that.
*
* @returns {Promise<CredentialField[]>} - Array of all fields with their encrypted values
*/
async fetchAllFieldValues(): Promise<CredentialField[]> {
const fields: CredentialField[] = [];
// Add non-secure fields from the fields JSON
const nonSecureFields = this.fields as Record<string, any>;
Object.entries(nonSecureFields || {}).forEach(([fieldId, value]) => {
fields.push({
id: `${this.id}-${fieldId}`, // Generate a consistent ID
fieldId,
value: value as string,
});
});
// Add secure fields from SecureValue table (encrypted)
this._secureValues.forEach((secureValue) => {
fields.push({
id: secureValue.id,
fieldId: secureValue.name,
value: secureValue.content, // Encrypted value
});
});
return fields;
}
/**
* Read Secure Field Value
*
* Decrypts and returns the value of a specific secure field.
*
* @param fieldId - The field ID to read
* @returns {Promise<string>} - The decrypted field value
*/
async readSecureFieldValue(fieldId: string): Promise<string> {
const secureValue = this._secureValues.find((sv) => sv.name === fieldId);
if (!secureValue) {
throw new GenericError({
message: `Secure field not found: ${fieldId}`,
name: "SecureFieldNotFound",
cause: `No secure value exists for field '${fieldId}' in credential '${this.id}'`,
status: 404,
});
}
// Decrypt the value
const decryptedValue = readSecureValue(
secureValue.content,
secureValue.hash,
);
return decryptedValue;
}
/**
* Read All Secure Values
*
* Decrypts and returns all secure field values for this credential.
*
* @returns {Promise<Record<string, string>>} - Object mapping field IDs to decrypted values
*/
async readAllSecureValues(): Promise<Record<string, string>> {
const secureValues: Record<string, string> = {};
await Promise.all(
this._secureValues.map(async (secureValue) => {
const decryptedValue = readSecureValue(
secureValue.content,
secureValue.hash,
);
secureValues[secureValue.name] = decryptedValue;
}),
);
return secureValues;
}
/**
* Update Credential
*
* Update the credential name or other basic properties.
*
* @param data - Partial credential data to update
* @returns {Promise<CredentialController>} - The updated credential controller
*/
async update(
data: Partial<Pick<Credential, "name">>,
): Promise<CredentialController> {
const pData = z
.object({
name: z.string().optional(),
})
.strict()
.parse(data);
const updatedCredential = await prisma.credential.update({
where: { id: this.id },
data: pData,
});
this._updateInternalValues(updatedCredential);
return this;
}
/**
* Get Credential Type
*
* Returns the credential type information.
*
* @returns {CredentialType} - The credential type
*/
getType(): CredentialType {
return this._type;
}
/**
* Get Company
*
* Returns the company this credential belongs to.
*
* @returns {Company} - The company
*/
getCompany(): Company {
return this._company;
}
/**
* To JSON
*
* Create an object that can be safely returned to the user.
* Secure values are not included by default.
*
* @param opts - Options to change the output
* @returns - An object that is JSON friendly
*/
toJson(opts?: { includeSecureValues?: boolean }) {
return {
id: this.id,
name: this.name,
typeId: this.typeId,
companyId: this.companyId,
fields: this.fields,
type: {
id: this._type.id,
name: this._type.name,
fields: this._type.fields,
permissionScope: this._type.permissionScope,
},
company: {
id: this._company.id,
name: this._company.name,
},
secureFieldIds: opts?.includeSecureValues
? this._secureValues.map((sv) => sv.name)
: undefined,
createdAt: this.createdAt,
updatedAt: this.updatedAt,
};
}
}
+178
View File
@@ -0,0 +1,178 @@
import { z } from "zod";
import { CredentialType, Credential } from "../../generated/prisma/client";
import { prisma } from "../constants";
import { CredentialTypeField } from "../modules/credentials/credentialTypeDefs";
import { CredentialController } from "./CredentialController";
/**
* Credential Type Controller
*
* This class manages credential type data, including field definitions,
* permission scopes, and associated credentials.
*/
export class CredentialTypeController {
public readonly id: string;
public name: string;
public permissionScope: string;
public icon: string | null;
public fields: CredentialTypeField[];
private _credentials: Credential[];
public readonly createdAt: Date;
public updatedAt: Date;
constructor(
credentialTypeData: CredentialType & {
credentials: Credential[];
},
) {
this.id = credentialTypeData.id;
this.name = credentialTypeData.name;
this.permissionScope = credentialTypeData.permissionScope;
this.icon = credentialTypeData.icon;
this.fields = credentialTypeData.fields! as any as CredentialTypeField[];
this._credentials = credentialTypeData.credentials;
this.createdAt = credentialTypeData.createdAt;
this.updatedAt = credentialTypeData.updatedAt;
}
/**
* Update Internal Values
*
* Internal method to update all internal values when we query the database.
* This keeps everything up-to-date even when we pass around the credential type controller.
*
* @param credentialTypeData - CredentialType object from Prisma
*/
private _updateInternalValues(credentialTypeData: CredentialType) {
this.name = credentialTypeData.name;
this.permissionScope = credentialTypeData.permissionScope;
this.icon = credentialTypeData.icon;
this.fields = credentialTypeData.fields! as any as CredentialTypeField[];
this.updatedAt = credentialTypeData.updatedAt;
}
/**
* Update Credential Type
*
* Update the credential type's name, permission scope, icon, or fields.
*
* @param data - Partial credential type data to update
* @returns {Promise<CredentialTypeController>} - The updated credential type controller
*/
async update(
data: Partial<
Pick<CredentialType, "name" | "permissionScope" | "icon"> & {
fields: CredentialTypeField[];
}
>,
): Promise<CredentialTypeController> {
const pData = z
.object({
name: z.string().optional(),
permissionScope: z.string().optional(),
icon: z.string().optional(),
fields: z.array(z.any()).optional(),
})
.strict()
.parse(data);
const updatedCredentialType = await prisma.credentialType.update({
where: { id: this.id },
data: pData,
});
this._updateInternalValues(updatedCredentialType);
return this;
}
/**
* Get Field Definition
*
* Get the definition for a specific field by its ID.
*
* @param fieldId - The field ID to look up
* @returns {CredentialTypeField | undefined} - The field definition or undefined
*/
getFieldDefinition(fieldId: string): CredentialTypeField | undefined {
return this.fields.find((f) => f.id === fieldId);
}
/**
* Get Required Fields
*
* Returns all fields that are marked as required.
*
* @returns {CredentialTypeField[]} - Array of required fields
*/
getRequiredFields(): CredentialTypeField[] {
return this.fields.filter((f) => f.required);
}
/**
* Get Secure Fields
*
* Returns all fields that should be stored securely (encrypted).
*
* @returns {CredentialTypeField[]} - Array of secure fields
*/
getSecureFields(): CredentialTypeField[] {
return this.fields.filter((f) => f.secure);
}
/**
* Fetch Credentials
*
* Fetch all credentials that use this credential type.
*
* @returns {Promise<CredentialController[]>} - Array of credential controllers
*/
async fetchCredentials(): Promise<CredentialController[]> {
const credentials = await prisma.credential.findMany({
where: { typeId: this.id },
include: {
type: true,
company: true,
securevalues: true,
},
});
return credentials.map((cred) => new CredentialController(cred));
}
/**
* Count Credentials
*
* Count how many credentials use this credential type.
*
* @returns {number} - Number of credentials using this type
*/
countCredentials(): number {
return this._credentials.length;
}
/**
* To JSON
*
* Create an object that can be safely returned to the user.
*
* @param opts - Options to change the output
* @returns - An object that is JSON friendly
*/
toJson(opts?: { includeCredentialCount?: boolean }) {
return {
id: this.id,
name: this.name,
permissionScope: this.permissionScope,
icon: this.icon,
fields: this.fields,
credentialCount: opts?.includeCredentialCount
? this._credentials.length
: undefined,
createdAt: this.createdAt,
updatedAt: this.updatedAt,
};
}
}