mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 18:48:11 +00:00
112 lines
3.7 KiB
TypeScript
112 lines
3.7 KiB
TypeScript
import { Injectable } from '@nestjs/common';
|
|
import { Readable } from 'stream';
|
|
import { DataSource } from 'typeorm';
|
|
|
|
import { FilesService } from '../files/files.service';
|
|
import { FileRecord } from '../files/entities/file.entity';
|
|
import { MinioService } from '../minio/minio.service';
|
|
import { SignaturesRepository } from './signatures.repository';
|
|
import { SavedSignature } from './entities/saved-signature.entity';
|
|
import { SavedSignatureDto } from './dto/save-signature.dto';
|
|
|
|
export interface UpsertSignatureInput {
|
|
userId: string;
|
|
signerDisplayName: string;
|
|
signatureImageBase64: string;
|
|
}
|
|
|
|
@Injectable()
|
|
export class SignaturesService {
|
|
constructor(
|
|
private readonly signaturesRepository: SignaturesRepository,
|
|
private readonly filesService: FilesService,
|
|
private readonly minioService: MinioService,
|
|
private readonly dataSource: DataSource,
|
|
) {}
|
|
|
|
/** Saved signature for a user, with the image inlined as a data URL (or null). */
|
|
async getForUser(userId: string): Promise<SavedSignatureDto | null> {
|
|
const saved = await this.signaturesRepository.findByUserId(userId);
|
|
if (!saved) return null;
|
|
return {
|
|
signerDisplayName: saved.signerDisplayName,
|
|
signatureImageUrl: await this.inlineImageUrl(saved.signatureFile?.url),
|
|
};
|
|
}
|
|
|
|
/** Insert or update the user's reusable signature, storing the image in MinIO. */
|
|
async upsertForUser(input: UpsertSignatureInput): Promise<SavedSignature> {
|
|
const buffer = this.decodeSignatureImage(input.signatureImageBase64);
|
|
const file: Express.Multer.File = {
|
|
fieldname: 'signature',
|
|
originalname: `signature-${input.userId}.png`,
|
|
encoding: '7bit',
|
|
mimetype: 'image/png',
|
|
size: buffer.length,
|
|
buffer,
|
|
stream: Readable.from(buffer),
|
|
destination: '',
|
|
filename: '',
|
|
path: '',
|
|
};
|
|
|
|
// Capture the previously referenced file so we can remove it only AFTER the
|
|
// saved_signatures row is repointed — deleting it first would violate the
|
|
// FK constraint (saved_signatures.signature_file_id -> files.id).
|
|
const existing = await this.signaturesRepository.findByUserId(input.userId);
|
|
const previousFileId = existing?.signatureFileId ?? null;
|
|
|
|
const fileRecord = await this.filesService.upload({
|
|
resourceId: input.userId,
|
|
resource: 'saved_signatures',
|
|
code: 'signature',
|
|
file,
|
|
});
|
|
|
|
const saved = await this.signaturesRepository.upsert({
|
|
userId: input.userId,
|
|
signerDisplayName: input.signerDisplayName,
|
|
signatureFileId: fileRecord.id,
|
|
});
|
|
|
|
if (previousFileId && previousFileId !== fileRecord.id) {
|
|
await this.dataSource
|
|
.getRepository(FileRecord)
|
|
.delete({ id: previousFileId });
|
|
}
|
|
|
|
return saved;
|
|
}
|
|
|
|
private async inlineImageUrl(
|
|
url?: string | null,
|
|
): Promise<string | null> {
|
|
if (!url) return null;
|
|
if (url.startsWith('data:')) return url;
|
|
try {
|
|
const objectName = this.minioService.getObjectNameFromUrl(url);
|
|
const stream = await this.minioService.getFileStream(objectName);
|
|
const buffer = await this.streamToBuffer(stream);
|
|
return `data:image/png;base64,${buffer.toString('base64')}`;
|
|
} catch {
|
|
return url;
|
|
}
|
|
}
|
|
|
|
private decodeSignatureImage(base64: string): Buffer {
|
|
const raw = base64.includes(',') ? base64.split(',')[1]! : base64;
|
|
return Buffer.from(raw, 'base64');
|
|
}
|
|
|
|
private streamToBuffer(stream: Readable): Promise<Buffer> {
|
|
return new Promise((resolve, reject) => {
|
|
const chunks: Buffer[] = [];
|
|
stream.on('data', (chunk: Buffer | string) => {
|
|
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
});
|
|
stream.on('error', reject);
|
|
stream.on('end', () => resolve(Buffer.concat(chunks)));
|
|
});
|
|
}
|
|
}
|