690419:1831 feat: update CI/CD to use SSH key authentication #05
CI / CD Pipeline / build (push) Failing after 4m57s
CI / CD Pipeline / deploy (push) Has been skipped

This commit is contained in:
2026-04-19 18:31:30 +07:00
parent 733f3c3987
commit 13745e5874
61 changed files with 6709 additions and 1241 deletions
@@ -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 || [
'รอสักครู่แล้วลองใหม่',
'แจ้งผู้ดูแลระบบหากยังพบปัญหา',
]
);
}
}
+1
View File
@@ -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(() => {