CREDENTIAL TYPE MANAGEMENT WORKS
This commit is contained in:
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user