Skip to content
16 changes: 16 additions & 0 deletions backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,19 @@ AUDIT_RETENTION_DAYS=365
AUDIT_RETENTION_SWEEP_HOURS=24
# Set to "false" to disable the background retention worker
AUDIT_RETENTION_ENABLED=true

# Backup system
# Enable/disable automatic backups
BACKUP_AUTO_ENABLED=true
# Health check interval in hours
BACKUP_HEALTH_CHECK_INTERVAL_HOURS=6
# Enable/disable health monitoring
BACKUP_HEALTH_CHECK_ENABLED=true
# Rotation check interval in days
BACKUP_ROTATION_CHECK_INTERVAL_DAYS=7
# Enable/disable rotation
BACKUP_ROTATION_ENABLED=true
# Restoration test interval in days
BACKUP_RESTORATION_TEST_INTERVAL_DAYS=30
# Enable/disable restoration tests
BACKUP_RESTORATION_TEST_ENABLED=true
2 changes: 2 additions & 0 deletions backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { SorobanModule } from './soroban/soroban.module';
import { IndexerModule } from './indexer/indexer.module';
import { HealthModule } from './health/health.module';
import { AuditModule } from './audit/audit.module';
import { BackupModule } from './backup/backup.module';

@Module({
imports: [
Expand Down Expand Up @@ -70,6 +71,7 @@ import { AuditModule } from './audit/audit.module';
IndexerModule,
HealthModule,
AuditModule,
BackupModule,
],
providers: [
{
Expand Down
143 changes: 143 additions & 0 deletions backend/src/backup/backup.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import { Test, TestingModule } from '@nestjs/testing';
import { BackupController } from './backup.controller';
import { BackupService } from './backup.service';

describe('BackupController', () => {
let controller: BackupController;
let mockBackupService: any;

beforeEach(async () => {
mockBackupService = {
createBackup: jest.fn(),
getBackups: jest.fn(),
restoreBackup: jest.fn(),
runHealthCheck: jest.fn(),
runRotation: jest.fn(),
runRestorationTests: jest.fn(),
getAlerts: jest.fn(),
acknowledgeAlert: jest.fn(),
getConfig: jest.fn(),
updateConfig: jest.fn(),
};

const module: TestingModule = await Test.createTestingModule({
controllers: [BackupController],
providers: [
{ provide: BackupService, useValue: mockBackupService },
],
}).compile();

controller = module.get<BackupController>(BackupController);
});

it('should be defined', () => {
expect(controller).toBeDefined();
});

describe('createBackup', () => {
it('should create a backup', async () => {
const dto = { walletAddress: 'test-wallet', backupType: 'full' };
mockBackupService.createBackup.mockResolvedValue({ id: 'test-id' });

const result = await controller.createBackup(dto as any);

expect(result).toEqual({ id: 'test-id' });
expect(mockBackupService.createBackup).toHaveBeenCalledWith(dto);
});
});

describe('getBackups', () => {
it('should get backups for a wallet', async () => {
mockBackupService.getBackups.mockResolvedValue([{ id: '1' }]);

const result = await controller.getBackups('test-wallet');

expect(result).toEqual([{ id: '1' }]);
expect(mockBackupService.getBackups).toHaveBeenCalledWith('test-wallet');
});
});

describe('restoreBackup', () => {
it('should restore a backup', async () => {
const dto = { backupId: 'test-id', walletAddress: 'test-wallet' };
mockBackupService.restoreBackup.mockResolvedValue({ success: true, restored: {}, dryRun: false });

const result = await controller.restoreBackup(dto as any);

expect(result.success).toBe(true);
expect(mockBackupService.restoreBackup).toHaveBeenCalledWith(dto);
});
});

describe('getHealthStatus', () => {
it('should get health status', async () => {
mockBackupService.runHealthCheck.mockResolvedValue({ checked: 5, healthy: 4 });

const result = await controller.getHealthStatus();

expect(result).toEqual({ checked: 5, healthy: 4 });
});
});

describe('triggerRotation', () => {
it('should trigger rotation', async () => {
mockBackupService.runRotation.mockResolvedValue({ totalChecked: 10, deleted: 2, kept: 8 });

const result = await controller.triggerRotation();

expect(result).toEqual({ totalChecked: 10, deleted: 2, kept: 8 });
});
});

describe('triggerRestorationTest', () => {
it('should trigger restoration test', async () => {
mockBackupService.runRestorationTests.mockResolvedValue({ tested: 3, passed: 2, failed: 1 });

const result = await controller.triggerRestorationTest();

expect(result).toEqual({ tested: 3, passed: 2, failed: 1 });
});
});

describe('getAlerts', () => {
it('should get alerts', async () => {
mockBackupService.getAlerts.mockResolvedValue([{ id: '1', message: 'test' }]);

const result = await controller.getAlerts('test-wallet', 'true');

expect(result).toEqual([{ id: '1', message: 'test' }]);
expect(mockBackupService.getAlerts).toHaveBeenCalledWith('test-wallet', true);
});
});

describe('acknowledgeAlert', () => {
it('should acknowledge an alert', async () => {
mockBackupService.acknowledgeAlert.mockResolvedValue({ id: '1', acknowledged: true });

const result = await controller.acknowledgeAlert('1');

expect(result.acknowledged).toBe(true);
});
});

describe('getConfig', () => {
it('should get config', async () => {
mockBackupService.getConfig.mockResolvedValue({ walletAddress: 'test-wallet' });

const result = await controller.getConfig('test-wallet');

expect(result.walletAddress).toBe('test-wallet');
});
});

describe('updateConfig', () => {
it('should update config', async () => {
const dto = { backupIntervalHours: 12 };
mockBackupService.updateConfig.mockResolvedValue({ walletAddress: 'test-wallet', backupIntervalHours: 12 });

const result = await controller.updateConfig('test-wallet', dto as any);

expect(result.backupIntervalHours).toBe(12);
});
});
});
85 changes: 85 additions & 0 deletions backend/src/backup/backup.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import {
Controller,
Get,
Post,
Put,
Body,
Param,
Query,
UseGuards,
HttpCode,
HttpStatus,
} from '@nestjs/common';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
import { BackupService } from './backup.service';
import { CreateBackupDto } from './dto/create-backup.dto';
import { UpdateBackupConfigDto } from './dto/update-backup-config.dto';
import { RestoreBackupDto } from './dto/restore-backup.dto';

@Controller('backup')
@UseGuards(JwtAuthGuard)
export class BackupController {
constructor(private readonly backupService: BackupService) {}

@Post()
async createBackup(@Body() dto: CreateBackupDto) {
return this.backupService.createBackup(dto);
}

@Get('wallet/:walletAddress')
async getBackups(@Param('walletAddress') walletAddress: string) {
return this.backupService.getBackups(walletAddress);
}

@Post('restore')
@HttpCode(HttpStatus.OK)
async restoreBackup(@Body() dto: RestoreBackupDto) {
return this.backupService.restoreBackup(dto);
}

@Get('health')
async getHealthStatus() {
return this.backupService.runHealthCheck();
}

@Post('rotation')
@HttpCode(HttpStatus.OK)
async triggerRotation() {
return this.backupService.runRotation();
}

@Post('restoration-test')
@HttpCode(HttpStatus.OK)
async triggerRestorationTest() {
return this.backupService.runRestorationTests();
}

@Get('alerts/:walletAddress')
async getAlerts(
@Param('walletAddress') walletAddress: string,
@Query('unacknowledged') unacknowledged?: string,
) {
return this.backupService.getAlerts(
walletAddress,
unacknowledged === 'true',
);
}

@Put('alerts/:alertId/acknowledge')
async acknowledgeAlert(@Param('alertId') alertId: string) {
return this.backupService.acknowledgeAlert(alertId);
}

@Get('config/:walletAddress')
async getConfig(@Param('walletAddress') walletAddress: string) {
return this.backupService.getConfig(walletAddress);
}

@Put('config/:walletAddress')
async updateConfig(
@Param('walletAddress') walletAddress: string,
@Body() dto: UpdateBackupConfigDto,
) {
return this.backupService.updateConfig(walletAddress, dto);
}
}
27 changes: 27 additions & 0 deletions backend/src/backup/backup.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ScheduleModule } from '@nestjs/schedule';
import { BackupService } from './backup.service';
import { BackupController } from './backup.controller';
import { BackupRecord } from './entities/backup-record.entity';
import { BackupAlert } from './entities/backup-alert.entity';
import { BackupConfig } from './entities/backup-config.entity';
import { IpfsModule } from '../ipfs/ipfs.module';
import { IdentityModule } from '../identity/identity.module';
import { VerificationModule } from '../verification/verification.module';
import { DataSharingModule } from '../data-sharing/data-sharing.module';

@Module({
imports: [
TypeOrmModule.forFeature([BackupRecord, BackupAlert, BackupConfig]),
ScheduleModule.forRoot(),
IpfsModule,
IdentityModule,
VerificationModule,
DataSharingModule,
],
controllers: [BackupController],
providers: [BackupService],
exports: [BackupService],
})
export class BackupModule {}
Loading
Loading