Skip to content
Snippets Groups Projects
Commit 29bb32bb authored by Steffen Schulze's avatar Steffen Schulze
Browse files

Merge branch 'chore/refactor-proof-manager' into 'main'

Refactor proof manager

See merge request eclipse/xfsc/ocm/ocm-engine!24
parents 8287e885 fdfc1641
No related branches found
No related tags found
No related merge requests found
Showing
with 161 additions and 488 deletions
......@@ -7,7 +7,7 @@ export default {
moduleFileExtensions: ['js', 'ts'],
testEnvironment: 'node',
transform: {
'^.+\\.(ts|js)$': [
'^.+\\.(js|ts)$': [
'@swc/jest',
{
...swcConfig,
......@@ -31,10 +31,9 @@ export default {
: ['text-summary', 'html'],
coveragePathIgnorePatterns: [
'<rootDir>/node_modules/',
'<rootDir>/test/',
'<rootDir>/coverage/',
'<rootDir>/dist/',
'<rootDir>/**/test',
'__tests__',
'@types',
'.dto.(t|j)s',
'.enum.ts',
......
......@@ -16,8 +16,7 @@
"prisma:generate": "prisma generate --schema=./src/prisma/schema.prisma",
"prisma:migrate": "prisma migrate deploy --schema=./src/prisma/schema.prisma",
"prisma:studio": "prisma studio",
"start": "nest start",
"start:dev": "nest start --watch --preserveWatchOutput",
"start": "nest start --watch --preserveWatchOutput",
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
......@@ -25,53 +24,36 @@
"test:e2e": "jest --config ./test/jest.config.js"
},
"dependencies": {
"@elastic/ecs-winston-format": "^1.5.0",
"@nestjs/axios": "^3.0.1",
"@nestjs/common": "^10.2.8",
"@nestjs/common": "^10.2.10",
"@nestjs/config": "^3.1.1",
"@nestjs/core": "^10.2.8",
"@nestjs/core": "^10.2.10",
"@nestjs/mapped-types": "^2.0.4",
"@nestjs/microservices": "^10.2.8",
"@nestjs/microservices": "^10.2.10",
"@nestjs/platform-express": "^10.2.8",
"@nestjs/swagger": "^7.1.16",
"@nestjs/terminus": "^10.1.1",
"@prisma/client": "^5.6.0",
"@ocm/shared": "workspace:*",
"class-validator": "^0.14.0",
"class-transformer": "^0.5.1",
"express": "^4.17.3",
"joi": "^17.11.0",
"js-base64": "^3.7.2",
"jsonwebtoken": "^9.0.2",
"jwks-rsa": "^3.1.0",
"moment": "^2.29.4",
"nats": "^2.18.0",
"pg": "^8.11.3",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.8.1",
"winston": "^3.11.0",
"winston-elasticsearch": "^0.17.4"
"rxjs": "^7.8.1"
},
"devDependencies": {
"@jest/globals": "^29.7.0",
"@nestjs/cli": "^10.2.1",
"@nestjs/schematics": "^10.0.3",
"@nestjs/testing": "^10.2.8",
"@nestjs/testing": "^10.2.10",
"@swc/cli": "^0.1.62",
"@swc/core": "^1.3.96",
"@swc/jest": "^0.2.29",
"@types/express": "^4.17.21",
"@types/jest": "^29.5.8",
"@types/jsonwebtoken": "^9.0.5",
"@types/node": "^20.9.0",
"@types/supertest": "^2.0.16",
"@types/jest": "^29.5.9",
"@types/node": "^20.9.3",
"dotenv-cli": "^7.3.0",
"jest": "^29.7.0",
"node-mocks-http": "^1.13.0",
"prisma": "^5.6.0",
"rimraf": "^5.0.5",
"source-map-support": "^0.5.21",
"supertest": "^6.3.3",
"swagger-ui-express": "^5.0.0",
"ts-node": "^10.9.1",
"typescript": "^5.2.2"
"typescript": "^5.3.2"
}
}
import type { INestApplication } from '@nestjs/common';
import type { TestingModule } from '@nestjs/testing';
import { Test } from '@nestjs/testing';
import AppModule from './app.module.js';
import { Application } from '../application.js';
describe('App Module', () => {
describe('Application', () => {
let app: INestApplication;
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
const moduleFixture = await Test.createTestingModule({
imports: [Application],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
it('should work', () => {
expect(true).toBe(true);
});
afterAll(async () => {
await app.close();
});
it('should be defined', () => {
expect(app).toBeDefined();
});
});
import type { MiddlewareConsumer, NestModule } from '@nestjs/common';
import { Module, RequestMethod } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { APP_FILTER } from '@nestjs/core';
import { TerminusModule } from '@nestjs/terminus';
import ExceptionHandler from './common/exception.handler.js';
import config from './config/config.js';
import validationSchema from './config/validation.js';
import HealthController from './health/health.controller.js';
import { AuthMiddleware } from './middleware/auth.middleware.js';
import PresentationProofsModule from './presentationProof/module.js';
@Module({
imports: [
TerminusModule,
ConfigModule.forRoot({
isGlobal: true,
load: [config],
validationSchema,
}),
PresentationProofsModule,
],
controllers: [HealthController],
providers: [
{
provide: APP_FILTER,
useClass: ExceptionHandler,
},
],
})
export default class AppModule implements NestModule {
public configure(consumer: MiddlewareConsumer) {
// eslint-disable-line
consumer
.apply(AuthMiddleware)
.exclude({
path: 'v1/health',
method: RequestMethod.GET,
})
.forRoutes('*');
}
}
import type { ConfigType } from '@nestjs/config';
import type { ClientProvider } from '@nestjs/microservices';
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { RouterModule } from '@nestjs/core';
import { ClientsModule, Transport } from '@nestjs/microservices';
import { HealthModule } from '@ocm/shared';
import { NATS_CLIENT } from './common/constants.js';
import { httpConfig } from './config/http.config.js';
import { natsConfig } from './config/nats.config.js';
import { ssiConfig } from './config/ssi.config.js';
import { validationSchema } from './config/validation.js';
import { ProofsModule } from './proofs/proofs.module.js';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
load: [httpConfig, natsConfig, ssiConfig],
cache: true,
expandVariables: true,
validationSchema,
validationOptions: {
allowUnknown: true,
abortEarly: true,
},
}),
ClientsModule.registerAsync({
isGlobal: true,
clients: [
{
name: NATS_CLIENT,
inject: [natsConfig.KEY],
useFactory: (config: ConfigType<typeof natsConfig>) => {
const provider: Required<ClientProvider> = {
transport: Transport.NATS,
options: {
servers: config.url as string,
},
};
if ('user' in config && 'password' in config) {
provider.options.user = config.user as string;
provider.options.pass = config.password as string;
}
return provider;
},
},
],
}),
HealthModule.registerAsync({
inject: [natsConfig.KEY],
useFactory: (config: ConfigType<typeof natsConfig>) => {
const options: Parameters<typeof HealthModule.register>[0] = {};
if (config.monitoringUrl) {
options.nats = {
monitoringUrl: config.monitoringUrl as string,
};
}
return options;
},
}),
ProofsModule,
RouterModule.register([
{ module: HealthModule, path: '/health' },
{ module: ProofsModule, path: '/proofs' },
]),
],
})
export class Application {}
import type PresentationSubscriptionEndpointDto from '../presentationProof/entities/presentationSubscribeEndPoint.entity.js';
import { Inject, Injectable } from '@nestjs/common';
import { ClientProxy } from '@nestjs/microservices';
import { lastValueFrom } from 'rxjs';
import { ATTESTATION, Connection, NATSServices } from '../common/constants.js';
@Injectable()
export default class NatsClientService {
public constructor(
@Inject(NATSServices.SERVICE_NAME) private client: ClientProxy,
) {}
public getConnectionById(connectionId: string) {
const pattern = {
endpoint: `${Connection.NATS_ENDPOINT}/${Connection.GET_CONNECTION_By_ID}`,
};
const payload = { connectionId };
return lastValueFrom(this.client.send(pattern, payload));
}
public publishPresentation(data: PresentationSubscriptionEndpointDto) {
this.client.emit(
`${NATSServices.SERVICE_NAME}/${NATSServices.PRESENTATION_SUBSCRIBER_ENDPOINT}`,
data,
);
}
public getCredentialsTypeDetails(type: string) {
const pattern = {
endpoint: `${ATTESTATION.ATTESTATION_MANAGER_SERVICE}/${ATTESTATION.GET_MEMBERSHIP_CREDENTIALS_DETAILS}`,
};
const payload = { type };
return lastValueFrom(this.client.send(pattern, payload));
}
public makeConnectionTrusted(connectionId: string) {
const pattern = {
endpoint: `${Connection.NATS_ENDPOINT}/${Connection.MAKE_CONNECTION_TRUSTED}`,
};
const payload = { connectionId };
return lastValueFrom(this.client.send(pattern, payload));
}
}
// import { ClientProxy } from '@nestjs/microservices';
import NatsClientService from './nats.client';
describe('Check if the nats client is working', () => {
// let natsClient: NatsClientService;
// let client: ClientProxy;
// beforeEach(() => {
// natsClient = new NatsClientService(client);
// });
// jest.mock('rxjs', () => {
// const original = jest.requireActual('rxjs');
// return {
// ...original,
// lastValueFrom: () => new Promise((resolve, reject) => {
// resolve(true);
// }),
// };
// });
it('should be defined', () => {
expect(NatsClientService).toBeDefined();
});
// it('should call the offer membership credential endpoint', async () => {
// const data = {
// status: 'complete',
// connectionId: 'connectionId',
// theirLabel: 'theirLabel',
// participantId: 'participantId',
// participantDID: 'participantDID'
// };
// jest.spyOn(client, 'send').mockReturnValue(of(data));
// const response = await natsClient.OfferMembershipCredential(data);
// expect(response).toBeTruthy();
// });
});
import { HttpService } from '@nestjs/axios';
import { Injectable } from '@nestjs/common';
import { lastValueFrom, map } from 'rxjs';
@Injectable()
export default class RestClientService {
public constructor(private readonly httpService: HttpService) {}
public async post(url: string, payload: object) {
return lastValueFrom(
this.httpService
.post(url, payload)
.pipe(map((response) => response.data)),
);
}
public async get(url: string) {
return lastValueFrom(
this.httpService.get(url).pipe(map((response) => response.data)),
);
}
}
export enum LoggerConfig {
FILE_PATH = 'logs/log.json',
LOG_DIR = './logs',
}
export enum NATSServices {
SERVICE_NAME = 'PROOF_MANAGER_SERVICE',
PRESENTATION_SUBSCRIBER_ENDPOINT = 'PresentationSubscriberEndpoint',
}
export enum Abstraction {
NATS_ENDPOINT = 'SSI_ABSTRACTION_SERVICE',
PROOF_STATE_CHANGED = 'ProofStateChanged',
}
export enum Connection {
GET_CONNECTION_By_ID = 'getConnectionById',
NATS_ENDPOINT = 'CONNECTION_MANAGER_SERVICE',
MAKE_CONNECTION_TRUSTED = 'makeConnectionTrusted',
}
export enum ATTESTATION {
ATTESTATION_MANAGER_SERVICE = 'ATTESTATION_MANAGER_SERVICE',
GET_MEMBERSHIP_CREDENTIALS_DETAILS = 'getCredentialsTypeDetails',
CREDENTIAL_TYPE = 'principalMemberCredential',
}
export enum States {
RequestSent = 'request-sent',
PresentationReceived = 'presentation-received',
Done = 'done',
}
export const SERVICE_NAME = 'PROOF_MANAGER_SERVICE';
export const NATS_CLIENT = Symbol('NATS_CLIENT');
import type ResponseType from './response.js';
import type { ArgumentsHost, ExceptionFilter } from '@nestjs/common';
import { Catch, HttpException, HttpStatus } from '@nestjs/common';
import { HttpAdapterHost } from '@nestjs/core';
@Catch()
export default class ExceptionHandler implements ExceptionFilter {
public constructor(private readonly httpAdapterHost: HttpAdapterHost) {}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public catch(exception: any, host: ArgumentsHost): void {
// In certain situations `httpAdapter` might not be available in the
// constructor method, thus we should resolve it here.
const { httpAdapter } = this.httpAdapterHost;
const ctx = host.switchToHttp();
const response = ctx.getResponse();
let statusCode = HttpStatus.INTERNAL_SERVER_ERROR;
let message =
exception.message.error || exception.message || 'Something went wrong!';
if (exception instanceof HttpException) {
const errorResponse: string | object = exception.getResponse();
statusCode = exception.getStatus();
message =
(typeof errorResponse === 'object' &&
Reflect.get(errorResponse, 'error')) ||
message;
}
const responseBody: ResponseType = {
statusCode,
message,
error: exception.message,
};
httpAdapter.reply(response, responseBody, statusCode);
}
}
export default interface ResponseType {
statusCode: number;
message: string;
data?: unknown;
error?: unknown;
}
import { fileURLToPath } from 'node:url';
const parentDirectory = fileURLToPath(new URL('..', import.meta.url));
const config = () => ({
PORT: Number(process.env.PORT),
APP_URL: process.env.PROOF_MANAGER_URL,
nats: {
url: process.env.NATS_URL,
},
auth: {
useAuth: process.env.USE_AUTH || 'false',
clientId: process.env.OAUTH_CLIENT_ID,
clientSecret: process.env.OAUTH_CLIENT_SECRET,
tokenUrl: process.env.OAUTH_TOKEN_URL,
},
agent: {
agentUrl: process.env.AGENT_URL,
didcommUrl: process.env.DIDCOMM_URL,
},
DATABASE: {
type: 'postgres',
port: 5432,
synchronize: false,
logging: false,
entities: [`${parentDirectory}/../**/**.model{.ts,.js}`],
},
ECSURL: process.env.ECSURL,
ACCEPT_PRESENTATION_CONFIG: process.env.ACCEPT_PRESENTATION_CONFIG,
});
export default config;
import { registerAs } from '@nestjs/config';
export const httpConfig = registerAs('http', () => ({
host: process.env.HOST || '0.0.0.0',
port: Number(process.env.PORT) || 3000,
}));
import { registerAs } from '@nestjs/config';
export const natsConfig = registerAs('nats', () => ({
url: process.env.NATS_URL || 'nats://localhost:4222',
user: process.env.NATS_USER,
password: process.env.NATS_PASSWORD,
monitoringUrl: process.env.NATS_MONITORING_URL || 'http://localhost:8222',
}));
import { registerAs } from '@nestjs/config';
export const ssiConfig = registerAs('ssi', () => ({
agentUrl: process.env.SSI_AGENT_URL || 'http://localhost:3010',
}));
import Joi from 'joi';
const validationSchema = Joi.object({
AGENT_URL: Joi.string().required(),
DATABASE_URL: Joi.string().required(),
NATS_URL: Joi.string().required(),
PORT: Joi.number().required(),
ACCEPT_PRESENTATION_CONFIG: Joi.string().required(),
USE_AUTH: Joi.string(),
OAUTH_CLIENT_ID: Joi.string(),
OAUTH_CLIENT_SECRET: Joi.string(),
OAUTH_TOKEN_URL: Joi.string(),
});
export const validationSchema = Joi.object({
HTTP_HOST: Joi.string(),
HTTP_PORT: Joi.number(),
NATS_URL: Joi.string().uri(),
NATS_USER: Joi.string().optional(),
NATS_PASSWORD: Joi.string().optional(),
NATS_MONITORING_URL: Joi.string().uri(),
export default validationSchema;
SSI_AGENT_URL: Joi.string().uri(),
});
import type ResponseType from '../common/response.js';
import { Controller, Get, HttpStatus, Version } from '@nestjs/common';
import { ApiOperation, ApiResponse } from '@nestjs/swagger';
@Controller('health')
export default class HealthController {
public res: ResponseType;
@Version(['1'])
@Get()
@ApiOperation({
summary: 'Health check',
description:
'This call provides the capability to check the service is working and up. The call returns 200 Status Code and current server time in json body',
})
@ApiResponse({
status: HttpStatus.OK,
description: 'Service is up and running.',
content: {
'application/json': {
schema: {},
examples: {
'Service is up and running.': {
value: {
statusCode: 200,
message:
'Thu Jan 01 1970 00:00:00 GMT+0000 (Coordinated Universal Time)',
},
},
},
},
},
})
public getHealth() {
this.res = {
statusCode: HttpStatus.OK,
message: `${new Date()}`,
};
return this.res;
}
}
import type { TestingModule } from '@nestjs/testing';
import { HttpStatus } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import HealthController from './health.controller.js';
describe('Health', () => {
let healthController: HealthController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [],
controllers: [HealthController],
providers: [],
}).compile();
healthController = module.get<HealthController>(HealthController);
});
it('should be defined', () => {
expect(healthController).toBeDefined();
});
it('should call getHealth', () => {
const response = healthController.getHealth();
expect(response.statusCode).toBe(HttpStatus.OK);
});
});
import type { MicroserviceOptions } from '@nestjs/microservices';
/* c8 ignore start */
import type { ConfigType } from '@nestjs/config';
import type { MicroserviceOptions, NatsOptions } from '@nestjs/microservices';
import { VersioningType } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { HttpAdapterHost, NestFactory } from '@nestjs/core';
import { Logger, VersioningType } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { Transport } from '@nestjs/microservices';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import AppModule from './app.module.js';
import AllExceptionsFilter from './utils/exceptionsFilter.js';
import logger from './utils/logger.js';
import { Application } from './application.js';
import { httpConfig } from './config/http.config.js';
import { natsConfig } from './config/nats.config.js';
const app = await NestFactory.create(AppModule);
const configService = app.get(ConfigService);
const app = await NestFactory.create(Application);
app.enableCors();
app.connectMicroservice<MicroserviceOptions>({
const { url, user, password } = app.get(natsConfig.KEY) as ConfigType<
typeof natsConfig
>;
const microserviceOptions: Required<NatsOptions> = {
transport: Transport.NATS,
options: {
servers: [configService.get('nats')?.url],
servers: [url],
},
});
};
if (user && password) {
microserviceOptions.options.user = user;
microserviceOptions.options.pass = password;
}
app.connectMicroservice<MicroserviceOptions>(microserviceOptions);
app.enableVersioning({
defaultVersion: ['1'],
......@@ -26,10 +38,9 @@ app.enableVersioning({
});
const swaggerConfig = new DocumentBuilder()
.setTitle('Gaia-x Proof Manager API')
.setDescription('API documentation for GAIA-X Proof Manager')
.setTitle('Gaia-X OCM Proof Manager API')
.setDescription('API documentation for Gaia-X OCM Proof Manager')
.setVersion('1.0')
.addServer(`localhost:${configService.get('PORT')}`)
.build();
const document = SwaggerModule.createDocument(app, swaggerConfig);
......@@ -37,9 +48,8 @@ const document = SwaggerModule.createDocument(app, swaggerConfig);
SwaggerModule.setup('/swagger', app, document);
await app.startAllMicroservices();
const httpAdapter = app.get(HttpAdapterHost);
app.useGlobalFilters(new AllExceptionsFilter(httpAdapter));
const { host, port } = app.get(httpConfig.KEY) as ConfigType<typeof httpConfig>;
await app.listen(port as number, host as string);
await app.listen(configService.get('PORT') || 3000, () => {
logger.info(`Listening on Port:${configService.get('PORT')}` || 3000);
});
Logger.log(`Application is running on: ${await app.getUrl()}`);
/* c8 ignore stop */
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 as string);
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,
};
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