Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 23 additions & 16 deletions backend/src/health/health.controller.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,32 @@
import { Controller, Get } from '@nestjs/common';
import {
HealthCheckService,
TypeOrmHealthIndicator,
HealthCheck,
} from '@nestjs/terminus';
import { RedisHealthIndicator } from './redis.health';
import { Controller, Get, Inject } from '@nestjs/common';
import { InjectRedis } from '@nestjs-modules/ioredis';
import Redis from 'ioredis';

/**
* Health check endpoint that validates the Redis connection before
* reporting the service as healthy. Closes #1342
*/
@Controller('health')
export class HealthController {
constructor(
private readonly health: HealthCheckService,
private readonly db: TypeOrmHealthIndicator,
private readonly redisHealth: RedisHealthIndicator,
@InjectRedis() private readonly redis: Redis,
) {}

@Get()
@HealthCheck()
check() {
return this.health.check([
() => this.db.pingCheck('database'),
() => this.redisHealth.isHealthy('redis'),
]);
async check(): Promise<{ status: string; redis: string }> {
let redisStatus = 'ok';

try {
await this.redis.ping();
} catch {
redisStatus = 'unreachable';
}

const overall = redisStatus === 'ok' ? 'ok' : 'degraded';

return {
status: overall,
redis: redisStatus,
};
}
}
31 changes: 31 additions & 0 deletions backend/src/network/network-mismatch.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Injectable, BadRequestException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';

export type StellarNetwork = 'mainnet' | 'testnet' | 'futurenet';

/**
* Guards against verifying a document hash anchored on one Stellar network
* against data from a different network. Closes #1344
*/
@Injectable()
export class NetworkMismatchGuard {
private readonly activeNetwork: StellarNetwork;

constructor(private readonly config: ConfigService) {
this.activeNetwork = (
this.config.get<string>('STELLAR_NETWORK') ?? 'testnet'
) as StellarNetwork;
}

/**
* Throws BadRequestException if `anchoredOn` differs from the active network.
*/
assertNetworkMatch(anchoredOn: StellarNetwork): void {
if (anchoredOn !== this.activeNetwork) {
throw new BadRequestException(
`Network mismatch: hash was anchored on "${anchoredOn}" ` +
`but the active network is "${this.activeNetwork}".`,
);
}
}
}
22 changes: 22 additions & 0 deletions backend/src/network/network.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Controller, Get } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';

export type StellarNetwork = 'mainnet' | 'testnet' | 'futurenet';

/**
* Exposes the currently active Stellar network so operators can confirm
* the running environment without inspecting config files.
* Closes #1343
*/
@Controller('network')
export class NetworkController {
constructor(private readonly config: ConfigService) {}

@Get()
getNetwork(): { network: StellarNetwork; horizonUrl: string } {
const network = (this.config.get<string>('STELLAR_NETWORK') ?? 'testnet') as StellarNetwork;
const horizonUrl = this.config.get<string>('STELLAR_HORIZON_URL') ?? '';

return { network, horizonUrl };
}
}
31 changes: 31 additions & 0 deletions backend/src/transfer/ownership-transfer-event.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';

export class OwnershipTransferredEvent {
constructor(
public readonly documentId: string,
public readonly fromOwner: string,
public readonly toOwner: string,
public readonly transferredAt: Date,
) {}
}

/**
* Emits a typed event whenever an ownership transfer succeeds,
* allowing downstream consumers to react without polling.
* Closes #1341
*/
@Injectable()
export class OwnershipTransferEventService {
constructor(private readonly eventEmitter: EventEmitter2) {}

emitTransferred(documentId: string, fromOwner: string, toOwner: string): void {
const event = new OwnershipTransferredEvent(
documentId,
fromOwner,
toOwner,
new Date(),
);
this.eventEmitter.emit('ownership.transferred', event);
}
}
Loading