Skip to content
Closed
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
3 changes: 3 additions & 0 deletions .github/workflows/backend-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,8 @@ jobs:
- name: Build
run: npm run build

- name: Swagger coverage check
run: npm test -- --runInBand src/swagger-docs.spec.ts

- name: Test
run: npm run test
1,485 changes: 864 additions & 621 deletions apps/backend/pnpm-lock.yaml

Large diffs are not rendered by default.

22 changes: 22 additions & 0 deletions apps/backend/src/api-key/api-key.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,16 @@
Param,
UseGuards,
Req,
HttpStatus,
} from '@nestjs/common';
import {
ApiBearerAuth,

Check failure on line 14 in apps/backend/src/api-key/api-key.controller.ts

View workflow job for this annotation

GitHub Actions / Build and Test

'ApiBearerAuth' is defined but never used
ApiBody,
ApiOperation,
ApiParam,
ApiResponse,
ApiTags,

Check failure on line 19 in apps/backend/src/api-key/api-key.controller.ts

View workflow job for this annotation

GitHub Actions / Build and Test

'ApiTags' is defined but never used
} from '@nestjs/swagger';
import { AuthGuard } from '../modules/auth/middleware/auth.guard';
import { ApiKeyGuard } from './guards/api-key.guard';
import { ApiKeysService } from './api-key.service';
Expand All @@ -28,12 +37,20 @@
constructor(private readonly apiKeysService: ApiKeysService) {}

@Post()
@ApiOperation({ summary: 'Create an API key for the authenticated user' })
@ApiBody({ type: CreateApiKeyDto, description: 'API key creation payload' })
@ApiResponse({ status: HttpStatus.CREATED, description: 'API key created successfully' })
@ApiResponse({ status: HttpStatus.BAD_REQUEST, description: 'Invalid API key payload' })
@ApiResponse({ status: HttpStatus.UNAUTHORIZED, description: 'Authentication required' })
async create(@Req() req: AuthenticatedRequest, @Body() dto: CreateApiKeyDto) {
const userId = req.user.sub;
return this.apiKeysService.create(userId, dto);
}

@Get()
@ApiOperation({ summary: 'List API keys for the authenticated user' })
@ApiResponse({ status: HttpStatus.OK, description: 'API keys retrieved successfully' })
@ApiResponse({ status: HttpStatus.UNAUTHORIZED, description: 'Authentication required' })
async list(@Req() req: AuthenticatedRequest) {
const userId = req.user.sub;
return this.apiKeysService.list(userId);
Expand All @@ -56,6 +73,11 @@
}

@Delete(':id')
@ApiOperation({ summary: 'Revoke an API key by ID' })
@ApiParam({ name: 'id', description: 'API key ID' })
@ApiResponse({ status: HttpStatus.OK, description: 'API key revoked successfully' })
@ApiResponse({ status: HttpStatus.NOT_FOUND, description: 'API key not found' })
@ApiResponse({ status: HttpStatus.UNAUTHORIZED, description: 'Authentication required' })
async revoke(@Req() req: AuthenticatedRequest, @Param('id') id: string) {
const userId = req.user.sub;
return this.apiKeysService.revoke(id, userId);
Expand Down
6 changes: 5 additions & 1 deletion apps/backend/src/app-version/app-version.controller.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import { Controller, Get } from '@nestjs/common';
import { Controller, Get, HttpStatus } from '@nestjs/common';
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { AppVersionService, AppVersionInfo } from './app-version.service';

@Controller('api/app')
@ApiTags('app-version')
export class AppVersionController {
constructor(private readonly appVersionService: AppVersionService) {}

@Get('version')
@ApiOperation({ summary: 'Get the current app version metadata' })
@ApiResponse({ status: HttpStatus.OK, description: 'App version information retrieved successfully' })
getVersion(): AppVersionInfo {
return this.appVersionService.getVersionInfo();
}
Expand Down
6 changes: 5 additions & 1 deletion apps/backend/src/app.controller.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import { Controller, Get } from '@nestjs/common';
import { Controller, Get, HttpStatus } from '@nestjs/common';
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { AppService } from './app.service';

@Controller()
@ApiTags('root')
export class AppController {
constructor(private readonly appService: AppService) {}

@Get()
@ApiOperation({ summary: 'Get the backend service welcome message' })
@ApiResponse({ status: HttpStatus.OK, description: 'Welcome message retrieved successfully' })
getHello(): string {
return this.appService.getHello();
}
Expand Down
29 changes: 26 additions & 3 deletions apps/backend/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,36 @@ async function bootstrap() {

const swaggerConfig = new DocumentBuilder()
.setTitle('Vaultix Backend API')
.setDescription('Vaultix backend endpoints')
.setDescription([
'Vaultix backend endpoints',
'',
'## WebSocket events',
'- `connected`: emitted after successful authentication with `{ userId, socketId }`.',
'- `joinedEscrow`: emitted after joining an escrow room.',
'- `escrow:status_changed`: emitted when escrow state changes.',
'- `escrow:milestone_released`: emitted when a milestone is released.',
'- `escrow:dispute_filed` / `escrow:dispute_resolved`: emitted for dispute lifecycle updates.',
'- `notification:new`: emitted to the authenticated user when a notification is published.',
].join('\n'))
.setVersion('1.0')
.addBearerAuth()
.addBearerAuth(
{
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
description: 'JWT access token issued by the auth endpoints',
},
'access-token',
)
.build();

const swaggerDocument = SwaggerModule.createDocument(app, swaggerConfig);
SwaggerModule.setup('api/docs', app, swaggerDocument);
SwaggerModule.setup('api/docs', app, swaggerDocument, {
customSiteTitle: 'Vaultix API Docs',
swaggerOptions: {
persistAuthorization: true,
},
});

await app.listen(process.env.PORT ?? 3000);
}
Expand Down
44 changes: 44 additions & 0 deletions apps/backend/src/modules/admin/admin.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@ import {
HttpStatus,
HttpCode,
} from '@nestjs/common';
import {
ApiBearerAuth,
ApiOperation,
ApiParam,
ApiQuery,
ApiResponse,
ApiTags,
} from '@nestjs/swagger';
import { AuthGuard } from '../auth/middleware/auth.guard';
import { AdminGuard } from '../auth/middleware/admin.guard';
import { AdminService } from './admin.service';
Expand Down Expand Up @@ -39,6 +47,8 @@ interface PaginationQuery {
}

@Controller('admin')
@ApiTags('admin')
@ApiBearerAuth('access-token')
@UseGuards(AuthGuard, AdminGuard)
export class AdminController {
constructor(
Expand All @@ -47,6 +57,18 @@ export class AdminController {
) {}

@Get('audit-logs')
@ApiOperation({ summary: 'List admin audit logs with optional filters' })
@ApiQuery({ name: 'actorId', required: false, description: 'Filter by actor user ID' })
@ApiQuery({ name: 'actionType', required: false, description: 'Filter by action type' })
@ApiQuery({ name: 'resourceType', required: false, description: 'Filter by resource type' })
@ApiQuery({ name: 'resourceId', required: false, description: 'Filter by resource ID' })
@ApiQuery({ name: 'from', required: false, description: 'Filter logs created after this date' })
@ApiQuery({ name: 'to', required: false, description: 'Filter logs created before this date' })
@ApiQuery({ name: 'page', required: false, description: 'Page number' })
@ApiQuery({ name: 'pageSize', required: false, description: 'Page size' })
@ApiResponse({ status: HttpStatus.OK, description: 'Audit logs retrieved successfully' })
@ApiResponse({ status: HttpStatus.UNAUTHORIZED, description: 'Authentication required' })
@ApiResponse({ status: HttpStatus.FORBIDDEN, description: 'Admin privileges required' })
async getAuditLogs(
@Query('actorId') actorId?: string,
@Query('actionType') actionType?: string,
Expand Down Expand Up @@ -75,11 +97,22 @@ export class AdminController {
}

@Get('escrows')
@ApiOperation({ summary: 'List escrows across the platform for admin review' })
@ApiQuery({ name: 'status', required: false, description: 'Filter by escrow status' })
@ApiResponse({ status: HttpStatus.OK, description: 'Escrows retrieved successfully' })
@ApiResponse({ status: HttpStatus.UNAUTHORIZED, description: 'Authentication required' })
@ApiResponse({ status: HttpStatus.FORBIDDEN, description: 'Admin privileges required' })
async getAllEscrows(@Query() query: EscrowQuery) {
return this.adminService.getAllEscrows(query);
}

@Get('users')
@ApiOperation({ summary: 'List platform users for admin review' })
@ApiQuery({ name: 'page', required: false, description: 'Page number' })
@ApiQuery({ name: 'limit', required: false, description: 'Page size' })
@ApiResponse({ status: HttpStatus.OK, description: 'Users retrieved successfully' })
@ApiResponse({ status: HttpStatus.UNAUTHORIZED, description: 'Authentication required' })
@ApiResponse({ status: HttpStatus.FORBIDDEN, description: 'Admin privileges required' })
async getAllUsers(@Query() query: PaginationQuery) {
const page = query.page ?? 1;
const limit = query.limit ?? 20;
Expand All @@ -88,12 +121,23 @@ export class AdminController {
}

@Get('stats')
@ApiOperation({ summary: 'Get aggregate platform statistics' })
@ApiResponse({ status: HttpStatus.OK, description: 'Platform stats retrieved successfully' })
@ApiResponse({ status: HttpStatus.UNAUTHORIZED, description: 'Authentication required' })
@ApiResponse({ status: HttpStatus.FORBIDDEN, description: 'Admin privileges required' })
async getStats() {
return this.adminService.getPlatformStats();
}

@Post('users/:id/suspend')
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: 'Suspend a platform user account' })
@ApiParam({ name: 'id', description: 'User ID to suspend' })
@ApiQuery({ name: 'actorId', required: false, description: 'Admin actor identifier' })
@ApiResponse({ status: HttpStatus.OK, description: 'User suspended successfully' })
@ApiResponse({ status: HttpStatus.BAD_REQUEST, description: 'Invalid suspend request' })
@ApiResponse({ status: HttpStatus.UNAUTHORIZED, description: 'Authentication required' })
@ApiResponse({ status: HttpStatus.FORBIDDEN, description: 'Admin privileges required' })
async suspendUser(
@Param('id') id: string,
@Query('actorId') actorId?: string,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { Body, Controller, Post, UseGuards } from '@nestjs/common';
import { Body, Controller, Post, UseGuards, HttpStatus } from '@nestjs/common';
import {
ApiBearerAuth,
ApiBody,
ApiOperation,
ApiResponse,
ApiTags,
} from '@nestjs/swagger';
import { ConsistencyCheckerService } from '../services/consistency-checker.service';
import {
ConsistencyCheckRequest,
Expand All @@ -7,11 +14,19 @@ import {
import { AdminGuard } from '../../auth/middleware/admin.guard';

@Controller('admin/escrows')
@ApiTags('admin/escrows')
@ApiBearerAuth('access-token')
@UseGuards(AdminGuard)
export class AdminEscrowConsistencyController {
constructor(private readonly checker: ConsistencyCheckerService) {}

@Post('consistency-check')
@ApiOperation({ summary: 'Run an escrow consistency check across the database and on-chain state' })
@ApiBody({ description: 'Consistency check request payload', schema: { oneOf: [{ type: 'object', properties: { escrowIds: { type: 'array', items: { type: 'number' } } } }, { type: 'object', properties: { fromId: { type: 'number' }, toId: { type: 'number' } } }] } })
@ApiResponse({ status: HttpStatus.OK, description: 'Consistency check completed' })
@ApiResponse({ status: HttpStatus.BAD_REQUEST, description: 'Invalid consistency check payload' })
@ApiResponse({ status: HttpStatus.UNAUTHORIZED, description: 'Authentication required' })
@ApiResponse({ status: HttpStatus.FORBIDDEN, description: 'Admin privileges required' })
async checkConsistency(
@Body() body: ConsistencyCheckRequest,
): Promise<ConsistencyCheckResponse> {
Expand Down
41 changes: 41 additions & 0 deletions apps/backend/src/modules/assets/admin-assets.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,38 +7,79 @@ import {
Param,
Delete,
UseGuards,
HttpStatus,
} from '@nestjs/common';
import {
ApiBearerAuth,
ApiBody,
ApiOperation,
ApiParam,
ApiResponse,
ApiTags,
} from '@nestjs/swagger';
import { AssetsService } from './assets.service';
import { CreateAssetDto, UpdateAssetDto } from './dto/asset.dto';
import { AuthGuard } from '../auth/middleware/auth.guard';
import { AdminGuard } from '../auth/middleware/admin.guard';

@Controller('admin/assets')
@ApiTags('admin/assets')
@ApiBearerAuth('access-token')
@UseGuards(AuthGuard, AdminGuard)
export class AdminAssetsController {
constructor(private readonly assetsService: AssetsService) {}

@Post()
@ApiOperation({ summary: 'Create a new asset' })
@ApiBody({ type: CreateAssetDto, description: 'Asset creation payload' })
@ApiResponse({ status: HttpStatus.CREATED, description: 'Asset created successfully' })
@ApiResponse({ status: HttpStatus.BAD_REQUEST, description: 'Invalid asset payload' })
@ApiResponse({ status: HttpStatus.UNAUTHORIZED, description: 'Authentication required' })
@ApiResponse({ status: HttpStatus.FORBIDDEN, description: 'Admin privileges required' })
create(@Body() createAssetDto: CreateAssetDto) {
return this.assetsService.create(createAssetDto);
}

@Get()
@ApiOperation({ summary: 'List all assets including inactive ones' })
@ApiResponse({ status: HttpStatus.OK, description: 'Assets retrieved successfully' })
@ApiResponse({ status: HttpStatus.UNAUTHORIZED, description: 'Authentication required' })
@ApiResponse({ status: HttpStatus.FORBIDDEN, description: 'Admin privileges required' })
findAll() {
return this.assetsService.findAll(false);
}

@Get(':id')
@ApiOperation({ summary: 'Get an asset by ID' })
@ApiParam({ name: 'id', description: 'Asset ID' })
@ApiResponse({ status: HttpStatus.OK, description: 'Asset retrieved successfully' })
@ApiResponse({ status: HttpStatus.NOT_FOUND, description: 'Asset not found' })
@ApiResponse({ status: HttpStatus.UNAUTHORIZED, description: 'Authentication required' })
@ApiResponse({ status: HttpStatus.FORBIDDEN, description: 'Admin privileges required' })
findOne(@Param('id') id: string) {
return this.assetsService.findOne(id);
}

@Patch(':id')
@ApiOperation({ summary: 'Update an existing asset' })
@ApiParam({ name: 'id', description: 'Asset ID' })
@ApiBody({ type: UpdateAssetDto, description: 'Asset update payload' })
@ApiResponse({ status: HttpStatus.OK, description: 'Asset updated successfully' })
@ApiResponse({ status: HttpStatus.BAD_REQUEST, description: 'Invalid asset payload' })
@ApiResponse({ status: HttpStatus.NOT_FOUND, description: 'Asset not found' })
@ApiResponse({ status: HttpStatus.UNAUTHORIZED, description: 'Authentication required' })
@ApiResponse({ status: HttpStatus.FORBIDDEN, description: 'Admin privileges required' })
update(@Param('id') id: string, @Body() updateAssetDto: UpdateAssetDto) {
return this.assetsService.update(id, updateAssetDto);
}

@Delete(':id')
@ApiOperation({ summary: 'Delete an asset by ID' })
@ApiParam({ name: 'id', description: 'Asset ID' })
@ApiResponse({ status: HttpStatus.OK, description: 'Asset deleted successfully' })
@ApiResponse({ status: HttpStatus.NOT_FOUND, description: 'Asset not found' })
@ApiResponse({ status: HttpStatus.UNAUTHORIZED, description: 'Authentication required' })
@ApiResponse({ status: HttpStatus.FORBIDDEN, description: 'Admin privileges required' })
remove(@Param('id') id: string) {
return this.assetsService.remove(id);
}
Expand Down
18 changes: 16 additions & 2 deletions apps/backend/src/modules/assets/assets.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,13 @@ import {
import { Request as ExpressRequest } from 'express';
import { AssetsService } from './assets.service';
import { AuthGuard } from '../auth/middleware/auth.guard';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import {
ApiBearerAuth,
ApiOperation,
ApiQuery,
ApiResponse,
ApiTags,
} from '@nestjs/swagger';

interface AuthenticatedRequest extends ExpressRequest {
user: {
Expand All @@ -26,13 +32,21 @@ export class AssetsController {
constructor(private readonly assetsService: AssetsService) {}

@Get()
@ApiOperation({ summary: 'List all active assets' })
@ApiResponse({ status: 200, description: 'Assets retrieved successfully' })
async findAllActive() {
return this.assetsService.findAll(true);
}

@Get('balance')
@UseGuards(AuthGuard)
@ApiBearerAuth()
@ApiBearerAuth('access-token')
@ApiOperation({ summary: 'Get the authenticated wallet balance for an asset' })
@ApiQuery({ name: 'assetCode', required: true, description: 'Asset code to retrieve the balance for' })
@ApiQuery({ name: 'issuer', required: false, description: 'Asset issuer for non-native assets' })
@ApiResponse({ status: 200, description: 'Asset balance retrieved successfully' })
@ApiResponse({ status: 400, description: 'Asset code is required' })
@ApiResponse({ status: 401, description: 'Authentication required' })
async getBalance(
@Query('assetCode') assetCode: string,
@Query('issuer') issuer: string | undefined,
Expand Down
Loading
Loading