690419:1831 feat: update CI/CD to use SSH key authentication #05
This commit is contained in:
@@ -2,7 +2,7 @@ import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { AbilityFactory, ScopeContext } from './ability.factory';
|
||||
import { User } from '../../../modules/user/entities/user.entity';
|
||||
import { UserAssignment } from '../../../modules/user/entities/user-assignment.entity';
|
||||
import { Role } from '../../../modules/auth/entities/role.entity';
|
||||
import { Role } from '../../../modules/user/entities/role.entity';
|
||||
|
||||
describe('AbilityFactory', () => {
|
||||
let factory: AbilityFactory;
|
||||
|
||||
@@ -14,6 +14,7 @@ export enum ErrorType {
|
||||
DATABASE_ERROR = 'DATABASE_ERROR',
|
||||
EXTERNAL_SERVICE = 'EXTERNAL_SERVICE',
|
||||
INFRASTRUCTURE = 'INFRASTRUCTURE',
|
||||
SERVICE_UNAVAILABLE = 'SERVICE_UNAVAILABLE', // 503 — ระบบไม่พร้อมให้บริการชั่วคราว (Redlock fail, Redis down)
|
||||
}
|
||||
|
||||
// ระดับความรุนแรงของ Error
|
||||
@@ -49,6 +50,8 @@ export function getStatusCode(type: ErrorType): number {
|
||||
case ErrorType.EXTERNAL_SERVICE:
|
||||
case ErrorType.INFRASTRUCTURE:
|
||||
return HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
case ErrorType.SERVICE_UNAVAILABLE:
|
||||
return HttpStatus.SERVICE_UNAVAILABLE; // 503
|
||||
default:
|
||||
return HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
}
|
||||
@@ -233,3 +236,27 @@ export class DatabaseException extends BaseException {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Service Unavailable (503) - ระบบไม่พร้อมให้บริการชั่วคราว
|
||||
// ADR-021 C1: Redlock Fail-closed — retry ครบแล้ว ยัง acquire ไม่ได้
|
||||
export class ServiceUnavailableException extends BaseException {
|
||||
constructor(
|
||||
code: string,
|
||||
message: string,
|
||||
userMessage?: string,
|
||||
recoveryActions?: string[]
|
||||
) {
|
||||
super(
|
||||
ErrorType.SERVICE_UNAVAILABLE,
|
||||
code,
|
||||
message,
|
||||
userMessage || 'ระบบยุ่งชั่วคราว กรุณาลองใหม่ภายหลัง',
|
||||
ErrorSeverity.HIGH,
|
||||
undefined,
|
||||
recoveryActions || [
|
||||
'รอสักครู่แล้วลองใหม่',
|
||||
'แจ้งผู้ดูแลระบบหากยังพบปัญหา',
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ export {
|
||||
WorkflowException,
|
||||
SystemException,
|
||||
DatabaseException,
|
||||
ServiceUnavailableException,
|
||||
} from './base.exception';
|
||||
|
||||
export type { ValidationErrorDetail, ErrorPayload } from './base.exception';
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { BullModule } from '@nestjs/bullmq';
|
||||
import { ScheduleModule } from '@nestjs/schedule'; // ✅ Import
|
||||
import { FileStorageService } from './file-storage.service.js';
|
||||
import { FileStorageController } from './file-storage.controller.js';
|
||||
@@ -12,6 +13,7 @@ import { UserModule } from '../../modules/user/user.module';
|
||||
TypeOrmModule.forFeature([Attachment]),
|
||||
ScheduleModule.forRoot(), // ✅ เปิดใช้งาน Cron Job],
|
||||
UserModule,
|
||||
BullModule.registerQueue({ name: 'rag:ocr' }),
|
||||
],
|
||||
controllers: [FileStorageController],
|
||||
providers: [
|
||||
|
||||
@@ -4,10 +4,13 @@ import {
|
||||
NotFoundException,
|
||||
BadRequestException,
|
||||
Logger,
|
||||
Optional,
|
||||
} from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository, In } from 'typeorm';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { InjectQueue } from '@nestjs/bullmq';
|
||||
import { Queue } from 'bullmq';
|
||||
import * as fs from 'fs-extra';
|
||||
import * as path from 'path';
|
||||
import * as crypto from 'crypto';
|
||||
@@ -24,7 +27,8 @@ export class FileStorageService {
|
||||
constructor(
|
||||
@InjectRepository(Attachment)
|
||||
private attachmentRepository: Repository<Attachment>,
|
||||
private configService: ConfigService
|
||||
private configService: ConfigService,
|
||||
@Optional() @InjectQueue('rag:ocr') private readonly ragOcrQueue?: Queue
|
||||
) {
|
||||
// ใช้ env vars จาก docker-compose สำหรับ Production
|
||||
// ถ้าไม่ได้กำหนดจะ fallback เป็น ./uploads/temp และ ./uploads/permanent
|
||||
@@ -90,7 +94,18 @@ export class FileStorageService {
|
||||
*/
|
||||
async commit(
|
||||
tempIds: string[],
|
||||
options?: { issueDate?: Date; documentType?: string }
|
||||
options?: {
|
||||
issueDate?: Date;
|
||||
documentType?: string;
|
||||
ragMeta?: {
|
||||
docType: string;
|
||||
docNumber: string | null;
|
||||
revision: string | null;
|
||||
projectCode: string;
|
||||
projectPublicId: string;
|
||||
classification: 'PUBLIC' | 'INTERNAL' | 'CONFIDENTIAL';
|
||||
};
|
||||
}
|
||||
): Promise<Attachment[]> {
|
||||
if (!tempIds || tempIds.length === 0) {
|
||||
return [];
|
||||
@@ -149,7 +164,27 @@ export class FileStorageService {
|
||||
att.expiresAt = undefined; // เคลียร์วันหมดอายุ
|
||||
att.referenceDate = effectiveDate; // Save reference date
|
||||
|
||||
committedAttachments.push(await this.attachmentRepository.save(att));
|
||||
const saved = await this.attachmentRepository.save(att);
|
||||
committedAttachments.push(saved);
|
||||
|
||||
if (this.ragOcrQueue && options?.ragMeta) {
|
||||
await this.ragOcrQueue
|
||||
.add(
|
||||
'ocr',
|
||||
{
|
||||
attachmentPublicId: saved.publicId,
|
||||
filePath: saved.filePath,
|
||||
...options.ragMeta,
|
||||
},
|
||||
{ jobId: saved.publicId }
|
||||
)
|
||||
.catch((err: unknown) => {
|
||||
this.logger.error(
|
||||
`Failed to enqueue rag:ocr for ${saved.publicId}`,
|
||||
err instanceof Error ? err.stack : String(err)
|
||||
);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this.logger.error(`File missing during commit: ${oldPath}`);
|
||||
throw new NotFoundException(
|
||||
|
||||
@@ -10,10 +10,15 @@ import { of, lastValueFrom } from 'rxjs';
|
||||
import { Request } from 'express';
|
||||
import type { Socket } from 'net';
|
||||
|
||||
type MockAuditLogRepo = {
|
||||
create: jest.Mock;
|
||||
save: jest.Mock;
|
||||
};
|
||||
|
||||
describe('AuditLogInterceptor', () => {
|
||||
let interceptor: AuditLogInterceptor;
|
||||
let reflector: Reflector;
|
||||
let auditLogRepo: jest.Mocked<Partial<typeof AuditLog.prototype.constructor>>;
|
||||
let auditLogRepo: MockAuditLogRepo;
|
||||
|
||||
const createMockUser = (userId: number): User => {
|
||||
const user = new User();
|
||||
@@ -55,7 +60,7 @@ describe('AuditLogInterceptor', () => {
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockRepository = {
|
||||
const mockRepository: MockAuditLogRepo = {
|
||||
create: jest.fn().mockReturnValue({}),
|
||||
save: jest.fn().mockResolvedValue({}),
|
||||
};
|
||||
@@ -78,7 +83,7 @@ describe('AuditLogInterceptor', () => {
|
||||
|
||||
interceptor = module.get<AuditLogInterceptor>(AuditLogInterceptor);
|
||||
reflector = module.get<Reflector>(Reflector);
|
||||
auditLogRepo = module.get(getRepositoryToken(AuditLog));
|
||||
auditLogRepo = module.get<MockAuditLogRepo>(getRepositoryToken(AuditLog));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
||||
Reference in New Issue
Block a user