215 lines
6.7 KiB
TypeScript
215 lines
6.7 KiB
TypeScript
import { Injectable, Inject } from "@nestjs/common";
|
|
import { Logger } from "nestjs-pino";
|
|
import { z } from "zod";
|
|
|
|
// Simple Zod schemas for mapping validation (matching database types)
|
|
const createMappingRequestSchema = z.object({
|
|
userId: z.string().uuid(),
|
|
whmcsClientId: z.number().int().positive(),
|
|
sfAccountId: z.string().optional(),
|
|
});
|
|
|
|
const updateMappingRequestSchema = z.object({
|
|
whmcsClientId: z.number().int().positive().optional(),
|
|
sfAccountId: z.string().optional(),
|
|
});
|
|
|
|
const userIdMappingSchema = z.object({
|
|
id: z.string().uuid(),
|
|
userId: z.string().uuid(),
|
|
whmcsClientId: z.number().int().positive(),
|
|
sfAccountId: z.string().nullable(),
|
|
createdAt: z.date(),
|
|
updatedAt: z.date(),
|
|
});
|
|
|
|
export type CreateMappingRequest = z.infer<typeof createMappingRequestSchema>;
|
|
export type UpdateMappingRequest = z.infer<typeof updateMappingRequestSchema>;
|
|
export type UserIdMapping = z.infer<typeof userIdMappingSchema>;
|
|
|
|
// Legacy interface for backward compatibility
|
|
export interface MappingValidationResult {
|
|
isValid: boolean;
|
|
errors: string[];
|
|
warnings: string[];
|
|
}
|
|
|
|
@Injectable()
|
|
export class MappingValidatorService {
|
|
constructor(@Inject(Logger) private readonly logger: Logger) {}
|
|
|
|
validateCreateRequest(request: CreateMappingRequest): MappingValidationResult {
|
|
const validationResult = createMappingRequestSchema.safeParse(request);
|
|
|
|
if (validationResult.success) {
|
|
const warnings: string[] = [];
|
|
if (!request.sfAccountId) {
|
|
warnings.push("Salesforce account ID not provided - mapping will be incomplete");
|
|
}
|
|
return { isValid: true, errors: [], warnings };
|
|
}
|
|
|
|
const errors = validationResult.error.issues.map(issue => issue.message);
|
|
this.logger.warn({ request, errors }, "Create mapping request validation failed");
|
|
|
|
return { isValid: false, errors, warnings: [] };
|
|
}
|
|
|
|
validateUpdateRequest(userId: string, request: UpdateMappingRequest): MappingValidationResult {
|
|
// First validate userId
|
|
const userIdValidation = z.string().uuid().safeParse(userId);
|
|
if (!userIdValidation.success) {
|
|
return {
|
|
isValid: false,
|
|
errors: ["User ID must be a valid UUID"],
|
|
warnings: [],
|
|
};
|
|
}
|
|
|
|
// Then validate the update request
|
|
const validationResult = updateMappingRequestSchema.safeParse(request);
|
|
|
|
if (validationResult.success) {
|
|
return { isValid: true, errors: [], warnings: [] };
|
|
}
|
|
|
|
const errors = validationResult.error.issues.map(issue => issue.message);
|
|
this.logger.warn({ userId, request, errors }, "Update mapping request validation failed");
|
|
|
|
return { isValid: false, errors, warnings: [] };
|
|
}
|
|
|
|
validateExistingMapping(mapping: UserIdMapping): MappingValidationResult {
|
|
const validationResult = userIdMappingSchema.safeParse(mapping);
|
|
|
|
if (validationResult.success) {
|
|
const warnings: string[] = [];
|
|
if (!mapping.sfAccountId) {
|
|
warnings.push("Mapping is missing Salesforce account ID");
|
|
}
|
|
return { isValid: true, errors: [], warnings };
|
|
}
|
|
|
|
const errors = validationResult.error.issues.map(issue => issue.message);
|
|
this.logger.warn({ mapping, errors }, "Existing mapping validation failed");
|
|
|
|
return { isValid: false, errors, warnings: [] };
|
|
}
|
|
|
|
validateBulkMappings(
|
|
mappings: CreateMappingRequest[]
|
|
): Array<{ index: number; validation: MappingValidationResult }> {
|
|
return mappings.map((mapping, index) => ({
|
|
index,
|
|
validation: this.validateCreateRequest(mapping),
|
|
}));
|
|
}
|
|
|
|
validateNoConflicts(
|
|
request: CreateMappingRequest,
|
|
existingMappings: UserIdMapping[]
|
|
): MappingValidationResult {
|
|
const errors: string[] = [];
|
|
const warnings: string[] = [];
|
|
|
|
// First validate the request format
|
|
const formatValidation = this.validateCreateRequest(request);
|
|
if (!formatValidation.isValid) {
|
|
return formatValidation;
|
|
}
|
|
|
|
// Check for conflicts
|
|
const duplicateUser = existingMappings.find(m => m.userId === request.userId);
|
|
if (duplicateUser) {
|
|
errors.push(`User ${request.userId} already has a mapping`);
|
|
}
|
|
|
|
const duplicateWhmcs = existingMappings.find(m => m.whmcsClientId === request.whmcsClientId);
|
|
if (duplicateWhmcs) {
|
|
errors.push(
|
|
`WHMCS client ${request.whmcsClientId} is already mapped to user ${duplicateWhmcs.userId}`
|
|
);
|
|
}
|
|
|
|
if (request.sfAccountId) {
|
|
const duplicateSf = existingMappings.find(m => m.sfAccountId === request.sfAccountId);
|
|
if (duplicateSf) {
|
|
warnings.push(
|
|
`Salesforce account ${request.sfAccountId} is already mapped to user ${duplicateSf.userId}`
|
|
);
|
|
}
|
|
}
|
|
|
|
return { isValid: errors.length === 0, errors, warnings };
|
|
}
|
|
|
|
validateDeletion(mapping: UserIdMapping): MappingValidationResult {
|
|
const errors: string[] = [];
|
|
const warnings: string[] = [];
|
|
|
|
if (!mapping) {
|
|
errors.push("Cannot delete non-existent mapping");
|
|
return { isValid: false, errors, warnings };
|
|
}
|
|
|
|
// Validate the mapping format
|
|
const formatValidation = this.validateExistingMapping(mapping);
|
|
if (!formatValidation.isValid) {
|
|
return formatValidation;
|
|
}
|
|
|
|
warnings.push(
|
|
"Deleting this mapping will prevent access to WHMCS/Salesforce data for this user"
|
|
);
|
|
if (mapping.sfAccountId) {
|
|
warnings.push(
|
|
"This mapping includes Salesforce integration - deletion will affect case management"
|
|
);
|
|
}
|
|
|
|
return { isValid: true, errors, warnings };
|
|
}
|
|
|
|
sanitizeCreateRequest(request: CreateMappingRequest): CreateMappingRequest {
|
|
// Use Zod parsing to sanitize and validate
|
|
const validationResult = createMappingRequestSchema.safeParse({
|
|
userId: request.userId?.trim(),
|
|
whmcsClientId: request.whmcsClientId,
|
|
sfAccountId: request.sfAccountId?.trim() || undefined,
|
|
});
|
|
|
|
if (validationResult.success) {
|
|
return validationResult.data;
|
|
}
|
|
|
|
// Fallback to original behavior if validation fails
|
|
return {
|
|
userId: request.userId?.trim(),
|
|
whmcsClientId: request.whmcsClientId,
|
|
sfAccountId: request.sfAccountId?.trim() || undefined,
|
|
};
|
|
}
|
|
|
|
sanitizeUpdateRequest(request: UpdateMappingRequest): UpdateMappingRequest {
|
|
const sanitized: any = {};
|
|
|
|
if (request.whmcsClientId !== undefined) {
|
|
sanitized.whmcsClientId = request.whmcsClientId;
|
|
}
|
|
|
|
if (request.sfAccountId !== undefined) {
|
|
sanitized.sfAccountId = request.sfAccountId?.trim() || undefined;
|
|
}
|
|
|
|
// Use Zod parsing to validate the sanitized data
|
|
const validationResult = updateMappingRequestSchema.safeParse(sanitized);
|
|
|
|
if (validationResult.success) {
|
|
return validationResult.data;
|
|
}
|
|
|
|
// Fallback to sanitized data if validation fails
|
|
return sanitized;
|
|
}
|
|
}
|