Skip to content
Snippets Groups Projects
Verified Commit 06496cbb authored by Konstantin Tsabolov's avatar Konstantin Tsabolov
Browse files

Merge branch 'main' into chore/move-docker-compose-to-root

parents 221e5385 ae8024d2
No related branches found
No related tags found
No related merge requests found
Showing
with 383 additions and 367 deletions
import { ClientsModule } from '@nestjs/microservices';
import { Test } from '@nestjs/testing';
import { NATS_CLIENT } from '../../common/constants.js';
import { InvitationsController } from '../invitations.controller.js';
import { InvitationsModule } from '../invitations.module.js';
import { InvitationsService } from '../invitations.service.js';
describe('InvitationsModule', () => {
let invitationsService: InvitationsService;
let invitationsController: InvitationsController;
beforeEach(async () => {
const moduleRef = await Test.createTestingModule({
imports: [
ClientsModule.registerAsync({
isGlobal: true,
clients: [{ name: NATS_CLIENT, useFactory: () => ({}) }],
}),
InvitationsModule,
],
}).compile();
invitationsService = moduleRef.get<InvitationsService>(InvitationsService);
invitationsController = moduleRef.get<InvitationsController>(
InvitationsController,
);
});
it('should be defined', () => {
expect(invitationsService).toBeDefined();
expect(invitationsService).toBeInstanceOf(InvitationsService);
expect(invitationsController).toBeDefined();
expect(invitationsController).toBeInstanceOf(InvitationsController);
});
});
import type { ClientProxy } from '@nestjs/microservices';
import type { TestingModule } from '@nestjs/testing';
import { Test } from '@nestjs/testing';
import {
EventDidcommConnectionsCreateInvitation,
EventDidcommConnectionsReceiveInvitationFromUrl,
} from '@ocm/shared';
import { Subject, of, takeUntil } from 'rxjs';
import { NATS_CLIENT } from '../../common/constants.js';
import { InvitationsService } from '../invitations.service.js';
describe('InvitationsService', () => {
let service: InvitationsService;
let natsClient: ClientProxy;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
InvitationsService,
{
provide: NATS_CLIENT,
useValue: {
send: jest.fn(),
},
},
],
}).compile();
service = module.get<InvitationsService>(InvitationsService);
natsClient = module.get<ClientProxy>(NATS_CLIENT);
});
describe('createInvitation', () => {
it('should return an invitation', (done) => {
const unsubscribe$ = new Subject<void>();
const tenantId = 'exampleTenantId';
const expectedResult: EventDidcommConnectionsCreateInvitation['data'] = {
invitationUrl: 'https://example.com/invitation',
};
jest
.spyOn(natsClient, 'send')
.mockReturnValue(
of(
new EventDidcommConnectionsCreateInvitation(
expectedResult,
tenantId,
),
),
);
service
.createInvitation(tenantId)
.pipe(takeUntil(unsubscribe$))
.subscribe((result) => {
expect(natsClient.send).toHaveBeenCalledWith(
EventDidcommConnectionsCreateInvitation.token,
{ tenantId },
);
expect(result).toStrictEqual(expectedResult);
unsubscribe$.next();
unsubscribe$.complete();
done();
});
});
});
describe('receiveInvitationFromURL', () => {
it('should return a connection', (done) => {
const unsubscribe$ = new Subject<void>();
const tenantId = 'exampleTenantId';
const invitationUrl = 'https://example.com/invitation';
const expectedResult =
{} as EventDidcommConnectionsReceiveInvitationFromUrl['data'];
jest
.spyOn(natsClient, 'send')
.mockReturnValue(
of(
new EventDidcommConnectionsReceiveInvitationFromUrl(
expectedResult,
tenantId,
),
),
);
service
.receiveInvitationFromURL(tenantId, invitationUrl)
.pipe(takeUntil(unsubscribe$))
.subscribe((result) => {
expect(natsClient.send).toHaveBeenCalledWith(
EventDidcommConnectionsReceiveInvitationFromUrl.token,
{ tenantId, invitationUrl },
);
expect(result).toStrictEqual(expectedResult);
unsubscribe$.next();
unsubscribe$.complete();
done();
});
});
});
});
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString, IsUrl } from 'class-validator';
export class ReceiveInvitationPayload {
@IsString()
@IsNotEmpty()
@IsUrl({ require_tld: false })
@ApiProperty({
description: 'The invitation URL to receive',
example: 'https://example.com/invitation',
})
public invitationUrl: string;
}
import {
Body,
Controller,
HttpStatus,
Post,
Query,
UseInterceptors,
UsePipes,
ValidationPipe,
} from '@nestjs/common';
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { MultitenancyParams, ResponseFormatInterceptor } from '@ocm/shared';
import { ReceiveInvitationPayload } from './dto/receive-invitation.dto.js';
import { InvitationsService } from './invitations.service.js';
@Controller()
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
@UseInterceptors(ResponseFormatInterceptor)
@ApiTags('Invitations')
export class InvitationsController {
public constructor(private readonly service: InvitationsService) {}
@Post()
@ApiOperation({
summary: 'Create a new invitation',
description: 'This call creates a new invitation for a given tenant',
})
@ApiResponse({
status: HttpStatus.OK,
description: 'Invitation created successfully',
content: {
'application/json': {
schema: {},
examples: {
'Invitation created successfully': {
value: {
statusCode: 200,
message: 'Invitation created successfully',
data: {
invitationUrl: 'https://example.com/invitation',
},
},
},
},
},
},
})
@ApiResponse({
status: HttpStatus.NOT_FOUND,
description: 'Tenant not found',
content: {
'application/json': {
schema: {},
examples: {
'Tenant not found': {
value: {
statusCode: 404,
message: 'Tenant not found',
data: null,
},
},
},
},
},
})
@ApiResponse({
status: HttpStatus.INTERNAL_SERVER_ERROR,
description: 'Failed to create invitation',
content: {
'application/json': {
schema: {},
examples: {
'Failed to create invitation': {
value: {
statusCode: 500,
message: 'Failed to create invitation',
data: null,
},
},
},
},
},
})
public createInvitation(
@Query() { tenantId }: MultitenancyParams,
): ReturnType<InvitationsService['createInvitation']> {
return this.service.createInvitation(tenantId);
}
@Post('receive')
@ApiOperation({
summary: 'Receive an invitation',
description: 'This call receives an invitation for a given tenant',
})
@ApiResponse({
status: HttpStatus.OK,
description: 'Invitation received successfully',
content: {
'application/json': {
schema: {},
examples: {
'Invitation received successfully': {
value: {
statusCode: 200,
message: 'Invitation received successfully',
data: {
connectionId: '123',
},
},
},
},
},
},
})
@ApiResponse({
status: HttpStatus.NOT_FOUND,
description: 'Tenant not found',
content: {
'application/json': {
schema: {},
examples: {
'Tenant not found': {
value: {
statusCode: 404,
message: 'Tenant not found',
data: null,
},
},
},
},
},
})
@ApiResponse({
status: HttpStatus.INTERNAL_SERVER_ERROR,
description: 'Failed to receive invitation',
content: {
'application/json': {
schema: {},
examples: {
'Failed to receive invitation': {
value: {
statusCode: 500,
message: 'Failed to receive invitation',
data: null,
},
},
},
},
},
})
public receiveInvitation(
@Query() { tenantId }: MultitenancyParams,
@Body() { invitationUrl }: ReceiveInvitationPayload,
): ReturnType<InvitationsService['receiveInvitationFromURL']> {
return this.service.receiveInvitationFromURL(tenantId, invitationUrl);
}
}
import { Module } from '@nestjs/common';
import { InvitationsController } from './invitations.controller.js';
import { InvitationsService } from './invitations.service.js';
@Module({
providers: [InvitationsService],
controllers: [InvitationsController],
})
export class InvitationsModule {}
import type {
EventDidcommConnectionsCreateInvitationInput,
EventDidcommConnectionsReceiveInvitationFromUrlInput,
} from '@ocm/shared';
import type { Observable } from 'rxjs';
import { Inject, Injectable } from '@nestjs/common';
import { ClientProxy } from '@nestjs/microservices';
import {
EventDidcommConnectionsCreateInvitation,
EventDidcommConnectionsReceiveInvitationFromUrl,
} from '@ocm/shared';
import { map } from 'rxjs';
import { NATS_CLIENT } from '../common/constants.js';
@Injectable()
export class InvitationsService {
public constructor(
@Inject(NATS_CLIENT) private readonly natsClient: ClientProxy,
) {}
public createInvitation(
tenantId: string,
): Observable<EventDidcommConnectionsCreateInvitation['data']> {
return this.natsClient
.send<
EventDidcommConnectionsCreateInvitation,
EventDidcommConnectionsCreateInvitationInput
>(EventDidcommConnectionsCreateInvitation.token, { tenantId })
.pipe(map(({ data }) => data));
}
public receiveInvitationFromURL(
tenantId: string,
invitationUrl: string,
): Observable<EventDidcommConnectionsReceiveInvitationFromUrl['data']> {
return this.natsClient
.send<
EventDidcommConnectionsReceiveInvitationFromUrl,
EventDidcommConnectionsReceiveInvitationFromUrlInput
>(EventDidcommConnectionsReceiveInvitationFromUrl.token, {
tenantId,
invitationUrl,
})
.pipe(map(({ data }) => data));
}
}
/* c8 ignore start */
import type { MicroserviceOptions } from '@nestjs/microservices'; import type { MicroserviceOptions } from '@nestjs/microservices';
import { VersioningType } from '@nestjs/common'; import { VersioningType } from '@nestjs/common';
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
import { HttpAdapterHost, NestFactory } from '@nestjs/core'; import { NestFactory } from '@nestjs/core';
import { Transport } from '@nestjs/microservices'; import { Transport } from '@nestjs/microservices';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import AppModule from './app.module.js'; import { Application } from './application.js';
import AllExceptionsFilter from './utils/exceptionsFilter.js';
import logger from './utils/logger.js';
const app = await NestFactory.create(AppModule); const app = await NestFactory.create(Application);
const configService = app.get(ConfigService); const configService = app.get(ConfigService);
app.enableCors(); app.enableCors();
...@@ -22,15 +21,14 @@ app.connectMicroservice<MicroserviceOptions>({ ...@@ -22,15 +21,14 @@ app.connectMicroservice<MicroserviceOptions>({
}); });
app.enableVersioning({ app.enableVersioning({
defaultVersion: ['1', '2'], defaultVersion: ['1'],
type: VersioningType.URI, type: VersioningType.URI,
}); });
const swaggerConfig = new DocumentBuilder() const swaggerConfig = new DocumentBuilder()
.setTitle('Gaia-x Connection Manager API') .setTitle('Gaia-X Connection Manager API')
.setDescription('API documentation for GAIA-X Connection Manager') .setDescription('API documentation for GAIA-X Connection Manager')
.setVersion('1.0') .setVersion('1.0')
.addServer(`http://localhost:${configService.get('PORT')}`)
.build(); .build();
const document = SwaggerModule.createDocument(app, swaggerConfig); const document = SwaggerModule.createDocument(app, swaggerConfig);
...@@ -38,9 +36,5 @@ const document = SwaggerModule.createDocument(app, swaggerConfig); ...@@ -38,9 +36,5 @@ const document = SwaggerModule.createDocument(app, swaggerConfig);
SwaggerModule.setup('/swagger', app, document); SwaggerModule.setup('/swagger', app, document);
await app.startAllMicroservices(); await app.startAllMicroservices();
const httpAdapter = app.get(HttpAdapterHost); await app.listen(configService.get('http.port') as number);
app.useGlobalFilters(new AllExceptionsFilter(httpAdapter)); /* c8 ignore stop */
await app.listen(configService.get('PORT') || 3000, () => {
logger.info(`Listening on Port:${configService.get('PORT')}` || 3000);
});
import type { NestMiddleware } from '@nestjs/common';
import type { NextFunction, Request, Response } from 'express';
import { HttpStatus, Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import jwt from 'jsonwebtoken';
import jwksClient from 'jwks-rsa';
import logger from '../utils/logger.js';
@Injectable()
export class AuthMiddleware implements NestMiddleware {
public constructor(private readonly configService: ConfigService) {}
/* eslint-disable */
async use(req: Request, res: Response, next: NextFunction) {
if (this.configService.get('auth.useAuth') === 'false') {
return next();
}
logger.info('Request at middleware');
const authHeader = req.headers.authorization;
const authToken = authHeader && authHeader.split(' ')[1];
if (!authToken) {
logger.error('No access token provided.');
res.json({
status: HttpStatus.UNAUTHORIZED,
message: 'Unauthorized. No Access token provided.',
data: undefined,
});
return;
}
const getKey = (
header: jwt.JwtHeader,
callback: jwt.SigningKeyCallback,
): void => {
const jwksUri = this.configService.get('auth.tokenUrl') || '';
const client = jwksClient({ jwksUri, timeout: 30000 });
client
.getSigningKey(header.kid)
.then((key) => callback(null, key.getPublicKey()))
.catch(callback);
};
function verify(token: string): Promise<any> | undefined {
return new Promise(
(resolve: (decoded: any) => void, reject: (error: Error) => void) => {
const verifyCallback: jwt.VerifyCallback<jwt.JwtPayload | string> = (
error: jwt.VerifyErrors | null,
decoded: any,
): void => {
if (error) {
return reject(error);
}
return resolve(decoded);
};
jwt.verify(token, getKey, verifyCallback);
},
);
}
const result = await verify(authToken);
if (!result) {
logger.error('Invalid access token provided.');
res.json({
status: HttpStatus.UNAUTHORIZED,
message: 'Unauthorized. Invalid Access token provided.',
data: undefined,
});
return;
}
next();
}
/* eslint-enable */
}
export default {
AuthMiddleware,
};
import PrismaModule from './prisma.module.js';
describe('Check if the module is working', () => {
it('should be defined', () => {
expect(PrismaModule).toBeDefined();
});
});
import { Module } from '@nestjs/common';
import PrismaService from './prisma.service.js';
@Module({
providers: [PrismaService],
exports: [PrismaService],
})
export default class PrismaModule {}
import type { OnModuleDestroy, OnModuleInit } from '@nestjs/common';
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { PrismaClient } from '@prisma/client';
@Injectable()
export default class PrismaService
extends PrismaClient
implements OnModuleInit, OnModuleDestroy
{
public constructor(private configService: ConfigService) {
super({
datasources: {
db: {
url: configService.get('DATABASE_URL'),
},
},
});
}
public async onModuleInit() {
await this.$connect();
}
public async onModuleDestroy() {
await this.$disconnect();
}
}
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model Connection {
id String @id @default(uuid())
connectionId String @unique @map("connection_id")
status String
participantDid String @unique @map("participant_did")
theirDid String @map("their_did")
theirLabel String @map("their_label")
createdDate DateTime @default(now()) @map("created_date")
updatedDate DateTime @default(now()) @map("updated_date")
isActive Boolean @default(true) @map("is_active")
isReceived Boolean @map("is_received")
}
model ShortUrlConnection {
id String @id @default(uuid())
connectionUrl String
}
import type { ArgumentsHost, ExceptionFilter } from '@nestjs/common';
import type { Request } from 'express';
import { Catch, HttpException, HttpStatus, Logger } from '@nestjs/common';
import { HttpAdapterHost } from '@nestjs/core';
import { Prisma } from '@prisma/client';
const { PrismaClientKnownRequestError, PrismaClientValidationError } = Prisma;
@Catch()
export default class AllExceptionsFilter implements ExceptionFilter {
public constructor(private readonly httpAdapterHost: HttpAdapterHost) {}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public catch(exception: any, host: ArgumentsHost): void {
const { httpAdapter } = this.httpAdapterHost;
const ctx = host.switchToHttp();
const request = ctx.getRequest<Request>();
let httpStatus = HttpStatus.INTERNAL_SERVER_ERROR;
let message = '';
switch (exception.constructor) {
case HttpException:
httpStatus = (exception as HttpException).getStatus();
message = exception?.message || 'Internal server error';
break;
case PrismaClientKnownRequestError:
switch (exception.code) {
case 'P2002': // Unique constraint failed on the {constraint}
case 'P2000': // The provided value for the column is too long for the column's type. Column: {column_name}
case 'P2001': // The record searched for in the where condition ({model_name}.{argument_name} = {argument_value}) does not exist
case 'P2005': // The value {field_value} stored in the database for the field {field_name} is invalid for the field's type
case 'P2006': // The provided value {field_value} for {model_name} field {field_name} is not valid
case 'P2010': // Raw query failed. Code: {code}. Message: {message}
case 'P2011': // Null constraint violation on the {constraint}
case 'P2017': // The records for relation {relation_name} between the {parent_name} and {child_name} models are not connected.
case 'P2021': // The table {table} does not exist in the current database.
case 'P2022': // The column {column} does not exist in the current database.
httpStatus = HttpStatus.BAD_REQUEST;
message = exception?.message;
break;
case 'P2018': // The required connected records were not found. {details}
case 'P2025': // An operation failed because it depends on one or more records that were required but not found. {cause}
case 'P2015': // A related record could not be found. {details}
httpStatus = HttpStatus.NOT_FOUND;
message = exception?.message;
break;
default:
httpStatus = HttpStatus.INTERNAL_SERVER_ERROR;
message = exception?.message || 'Internal server error';
}
break;
case PrismaClientValidationError:
httpStatus = HttpStatus.BAD_REQUEST;
message = exception?.message;
break;
default:
httpStatus =
exception.response?.status || HttpStatus.INTERNAL_SERVER_ERROR;
message =
exception.response?.data?.message ||
exception?.message ||
'Internal server error';
}
Logger.error(
'Exception Filter :',
message,
(exception as Error).stack,
`${request.method} ${request.url}`,
);
const responseBody = {
statusCode: httpStatus,
timestamp: new Date().toISOString(),
message,
};
httpAdapter.reply(ctx.getResponse(), responseBody, httpStatus);
}
}
import fs from 'node:fs';
describe('Logger', () => {
it('should create a directory if not exists', async () => {
jest.spyOn(fs, 'existsSync').mockImplementation(() => false);
jest.spyOn(fs, 'mkdirSync').mockImplementation(() => 'mocked');
const logger = await import('./logger.js');
expect(logger).toBeDefined();
expect(fs.existsSync).toHaveBeenCalled();
expect(fs.mkdirSync).toHaveBeenCalled();
});
});
import type { Logger } from 'winston';
import { ecsFormat } from '@elastic/ecs-winston-format';
import { createLogger, transports } from 'winston';
const logger: Logger = createLogger({
format: ecsFormat({ convertReqRes: true }),
transports: [new transports.Console()],
});
logger.on('error', (error) => {
// eslint-disable-next-line no-console
console.error('Error in logger caught', error);
});
export default logger;
import type { NatsConnection } from 'nats';
import { connect, StringCodec } from 'nats';
const sc = StringCodec();
export default class Nats {
private static nc: NatsConnection | null = null;
public static async initialize(natsConfig: {
servers: Array<string> | string;
name: string;
}): Promise<NatsConnection | null> {
this.nc = await connect(natsConfig);
return this.nc;
}
public static async publish(subject: string, payload: string) {
if (this.nc) {
this.nc.publish(subject, sc.encode(payload));
} else {
throw new Error('Initialize Nats First!!');
}
}
public static async subscribe(
subject: string,
cb: (...args: unknown[]) => unknown,
) {
if (this.nc) {
const sub = this.nc.subscribe(subject);
for await (const m of sub) {
cb(sc.decode(m.data));
}
} else {
throw new Error('Initialize Nats First!!');
}
}
}
import pagination from './pagination.js';
describe('Check if the module is working', () => {
it('should be defined', () => {
expect(pagination).toBeDefined();
});
it('should be return default value', () => {
const result = { skip: 0, take: 1000 };
expect(pagination(0, 0)).toStrictEqual(result);
});
it('should be return next page value', () => {
const result = { skip: 0, take: 10 };
expect(pagination(10, 0)).toStrictEqual(result);
});
});
const pagination = (pageSize: number, page: number) => {
const query: {
skip?: number;
take?: number;
} = {};
if (pageSize && (page || page === 0)) {
query.skip = page * pageSize;
query.take = pageSize;
} else {
query.skip = 0;
query.take = 1000;
}
return query;
};
export default pagination;
/** @type {import('jest').Config} */
import config from '../jest.config.js';
export default {
...config,
rootDir: '.',
testRegex: '.*\\.e2e-spec\\.ts$',
};
...@@ -24,15 +24,13 @@ ...@@ -24,15 +24,13 @@
"test:e2e": "jest --config ./test/jest.config.js" "test:e2e": "jest --config ./test/jest.config.js"
}, },
"dependencies": { "dependencies": {
"@nestjs/axios": "^3.0.1",
"@nestjs/common": "^10.2.10", "@nestjs/common": "^10.2.10",
"@nestjs/config": "^3.1.1", "@nestjs/config": "^3.1.1",
"@nestjs/core": "^10.2.10", "@nestjs/core": "^10.2.10",
"@nestjs/microservices": "^10.2.10", "@nestjs/microservices": "^10.2.10",
"@nestjs/platform-express": "^10.2.8", "@nestjs/platform-express": "^10.2.8",
"@nestjs/swagger": "^7.1.16", "@nestjs/swagger": "^7.1.16",
"@nestjs/terminus": "^10.1.1", "@ocm/shared": "workspace:*",
"axios": "^1.6.2",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"class-validator": "^0.14.0", "class-validator": "^0.14.0",
"express": "^4.17.3", "express": "^4.17.3",
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment