Skip to content
Snippets Groups Projects
Unverified Commit 18d9298c authored by Konstantin Tsabolov's avatar Konstantin Tsabolov
Browse files

chore: remove principal-manager service

parent be89b5b8
No related branches found
No related tags found
2 merge requests!9feat(ssi): Establish a trusted connection with yourself,!8Project house-keeping, refactoring and reorganizing
Showing
with 0 additions and 623 deletions
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),
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,
},
DATABASE: {
type: 'postgres',
port: 5432,
synchronize: false,
logging: false,
entities: [`${parentDirectory}/../**/**.model{.ts,.js}`],
},
ECSURL: process.env.ECSURL,
});
export default config;
import Joi from 'joi';
const validationSchema = Joi.object({
DATABASE_URL: Joi.string().required(),
NATS_URL: Joi.string().required(),
PORT: Joi.number().required(),
USE_AUTH: Joi.string(),
OAUTH_CLIENT_ID: Joi.string(),
OAUTH_CLIENT_SECRET: Joi.string(),
OAUTH_TOKEN_URL: Joi.string(),
});
export default validationSchema;
import { Controller, Get, Version, HttpStatus } from '@nestjs/common';
import type ResponseType from '../common/response.js';
@Controller('health')
export default class HealthController {
res: ResponseType;
@Version(['1'])
@Get()
getHealth() {
this.res = {
statusCode: HttpStatus.OK,
message: `${new Date()}`,
};
return this.res;
}
}
import { HttpStatus } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import HealthController from './health.controller';
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 { VersioningType } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { NestFactory } from '@nestjs/core';
import { MicroserviceOptions, Transport } from '@nestjs/microservices';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import AppModule from './app.module.js';
import logger from './utils/logger.js';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const configService = app.get(ConfigService);
app.enableCors();
app.connectMicroservice<MicroserviceOptions>({
transport: Transport.NATS,
options: {
servers: [configService.get('nats')?.url],
},
});
app.enableVersioning({
defaultVersion: ['1', '2'],
type: VersioningType.URI,
});
const swaggerConfig = new DocumentBuilder()
.setTitle('XFSC Principal Manager API')
.setDescription('API documentation for XFSC Principal Manager')
.setVersion('1.0')
.build();
const document = SwaggerModule.createDocument(app, swaggerConfig);
SwaggerModule.setup('/swagger', app, document);
await app.startAllMicroservices();
await app.listen(configService.get('PORT') || 3000, () => {
logger.info(`Listening on Port:${configService.get('PORT')}` || 3000);
});
}
bootstrap();
import { HttpStatus, Injectable, NestMiddleware } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import type { NextFunction, Request, Response } from 'express';
import jwt from 'jsonwebtoken';
import jwksClient from 'jwks-rsa';
import logger from '../utils/logger.js';
@Injectable()
export class AuthMiddleware implements NestMiddleware {
constructor(private readonly configService: ConfigService) {}
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<unknown> | undefined {
return new Promise(
(
resolve: (decoded: unknown) => void,
reject: (error: Error) => void,
) => {
const verifyCallback: jwt.VerifyCallback<jwt.JwtPayload | string> = (
error: jwt.VerifyErrors | null,
decoded: unknown,
): 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 { HttpStatus } from '@nestjs/common';
import NatsClientService from '../../client/nats.client.js';
import PrincipalService from '../services/service.js';
import PrincipalController from './controller.js';
const STUB_CONNECTION_COMPLETE = {
status: 'complete',
connectionId: 'connectionId',
theirLabel: 'theirLabel',
participantId: 'participantId',
participantDID: 'participantDID',
theirDid: 'theirDid',
};
const STUB_CONNECTION_COMPLETE_2 = {
status: 'incomplete',
connectionId: 'connectionId',
theirLabel: 'theirLabel',
participantId: 'participantId',
participantDID: 'participantDID',
theirDid: 'theirDid',
};
describe.skip('Check if the controller is working', () => {
let principalService: PrincipalService;
let principalController: PrincipalController;
let natsClientService: NatsClientService;
beforeEach(async () => {
principalService = new PrincipalService(natsClientService);
principalController = new PrincipalController(principalService);
});
it('should be defined', () => {
expect(PrincipalController).toBeDefined();
});
it('should throw a bad request if status is not complete', async () => {
const response = await principalController.connectionComplete(
STUB_CONNECTION_COMPLETE_2,
);
expect(response.statusCode).toBe(HttpStatus.BAD_REQUEST);
expect(response.message).toBe('Connection status should be Complete');
expect(response.data).toBeUndefined();
expect(response.error).toBeUndefined();
});
it('should return a success response if status is complete', async () => {
jest
.spyOn(principalService, 'OfferMembershipCredential')
.mockResolvedValueOnce({
statusCode: HttpStatus.OK,
message: 'Status connection received',
});
const response = await principalController.connectionComplete(
STUB_CONNECTION_COMPLETE,
);
expect(response.statusCode).toBe(HttpStatus.OK);
expect(response.message).toBe('Status connection received');
expect(response.data).toBeUndefined();
expect(response.error).toBeUndefined();
});
});
import {
BadRequestException,
Body,
Controller,
HttpException,
HttpStatus,
Post,
Req,
Res,
Version, // Post, Version, Body, Res, Req,
} from '@nestjs/common';
import { MessagePattern } from '@nestjs/microservices';
import { isURL } from 'class-validator';
import type { Request, Response } from 'express';
import { NATSServices } from '../../common/constants.js';
import type ResponseType from '../../common/response.js';
import logger from '../../utils/logger.js';
import MapUserInfoDTO from '../entities/mapUserInfoDTO.entity.js';
import OfferMembershipCredentialDto from '../entities/offerMembershipCredentialDto.entity.js';
import PrincipalService from '../services/service.js';
@Controller()
export default class PrincipalController {
name: string;
constructor(private readonly principalService: PrincipalService) {}
@MessagePattern({
endpoint: `${NATSServices.SERVICE_NAME}/connectionCompleteStatus`,
})
async connectionComplete(data: OfferMembershipCredentialDto) {
logger.info(
`call from connection manager for OfferMembershipCredentialDto ${OfferMembershipCredentialDto}`,
);
let response: ResponseType = {
statusCode: HttpStatus.OK,
message: 'Status connection received',
};
if (data.status.toUpperCase() === 'COMPLETE') {
this.principalService.OfferMembershipCredential(data);
return response;
}
response = {
statusCode: HttpStatus.BAD_REQUEST,
message: 'Connection status should be Complete',
};
return response;
}
@Version(['1'])
@Post('map-user-info')
async mapUserInfo(
@Body() tokenBody: MapUserInfoDTO,
@Res() response: Response,
@Req() req: Request, // eslint-disable-line @typescript-eslint/no-unused-vars
) {
try {
const { userInfoURL, userData } = tokenBody;
if (
(!userData ||
typeof userData !== 'object' ||
Object.keys(userData).length === 0) &&
(!userInfoURL || !isURL(userInfoURL))
) {
throw new BadRequestException('Invalid user data or user info url');
}
const res = {
statusCode: HttpStatus.CREATED,
message: 'User info mapped successfully',
data: await this.principalService.mapUserInfo(tokenBody),
};
return response.send(res);
} catch (error: unknown) {
throw new HttpException(
Reflect.get(error || {}, 'message') || 'Internal server error',
Reflect.get(error || {}, 'status') || 500,
);
}
}
// listen for complete connection event and filter based
// on matching connection ids from database that have userInfo
// once COMPLETE:
// * map userInfo to VC
// * issue VC to did of matching complete connection ID
// * if (issuing successful) delete record from DB
}
import { ApiProperty } from '@nestjs/swagger';
export type UserData = {
[key: string]: unknown;
};
export default class MapUserInfoDTO {
@ApiProperty()
userInfoURL: string;
@ApiProperty({ type: {} })
userData: UserData;
}
import { IsString, IsNotEmpty } from 'class-validator';
export default class OfferMembershipCredentialDto {
@IsString()
@IsNotEmpty()
status: string;
@IsString()
@IsNotEmpty()
connectionId: string;
@IsString()
@IsNotEmpty()
theirLabel: string;
@IsString()
@IsNotEmpty()
participantDID: string;
@IsString()
@IsNotEmpty()
theirDid: string;
}
import PrincipalModule from './module';
describe('Check if the module is working', () => {
it('should be defined', () => {
expect(PrincipalModule).toBeDefined();
});
});
import { HttpModule } from '@nestjs/axios';
import { Module } from '@nestjs/common';
import { ClientsModule, Transport } from '@nestjs/microservices';
import NatsClientService from '../client/nats.client.js';
import { NATSServices } from '../common/constants.js';
import config from '../config/config.js';
import PrismaService from '../prisma/prisma.service.js';
import PrincipalController from './controller/controller.js';
import PrincipalService from './services/service.js';
@Module({
imports: [
HttpModule,
ClientsModule.register([
{
name: NATSServices.SERVICE_NAME,
transport: Transport.NATS,
options: {
servers: [config().nats.url as string],
},
},
]),
],
controllers: [PrincipalController],
providers: [PrincipalService, PrismaService, NatsClientService],
})
export default class PrincipalModule {}
import { HttpStatus } from '@nestjs/common';
import { ClientProxy } from '@nestjs/microservices';
import NatsClientService from '../../client/nats.client.js';
import PrincipalService from './service.js';
describe.skip('Check if the service is working', () => {
let principalService: PrincipalService;
let natsClient: NatsClientService;
let client: ClientProxy;
beforeEach(() => {
natsClient = new NatsClientService(client);
principalService = new PrincipalService(natsClient);
});
it('should be defined', () => {
expect(PrincipalService).toBeDefined();
});
it('should respond correctly', async () => {
jest.spyOn(natsClient, 'OfferMembershipCredential').mockResolvedValueOnce({
statusCode: HttpStatus.OK,
message: 'Status connection received',
});
const response = await principalService.OfferMembershipCredential({
status: 'complete',
connectionId: 'connectionId',
theirLabel: 'theirLabel',
participantId: 'participantId',
participantDID: 'participantDID',
});
expect(response.statusCode).toBe(HttpStatus.OK);
expect(response.message).toBe('Status connection received');
expect(response.data).toBeUndefined();
expect(response.error).toBeUndefined();
});
});
import { HttpService } from '@nestjs/axios';
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import NatsClientService from '../../client/nats.client.js';
import {
AttestationManagerUrl,
ConnectionManagerUrl,
CreateMemberConnection,
SaveUserInfo,
} from '../../common/constants.js';
import ResponseType from '../../common/response.js';
import MapUserInfoDTO from '../entities/mapUserInfoDTO.entity.js';
import OfferMembershipCredentialDto from '../entities/offerMembershipCredentialDto.entity.js';
@Injectable()
export default class PrincipalService {
constructor(
private readonly natsClient: NatsClientService,
private readonly httpService: HttpService,
) {}
async OfferMembershipCredential(data: OfferMembershipCredentialDto) {
const response: ResponseType = {
statusCode: HttpStatus.OK,
message: 'Status connection received',
};
this.natsClient.OfferMembershipCredential(data);
return response;
}
async mapUserInfo({
userData,
userInfoURL,
}: MapUserInfoDTO): Promise<unknown> {
try {
let userInfo;
if (userData) {
userInfo = userData;
}
if (!userInfo && userInfoURL) {
const response = await this.httpService.axiosRef.get(userInfoURL, {
headers: {
// eslint is going to throw error - ignore it
// Authorization: `${req.headers.get('Authorization')}`,
},
});
userInfo = response.data;
}
const createConnectionBody = {
autoAcceptConnection: true,
};
const userDetails = {
connectionId: '',
autoAcceptCredential: 'never',
userInfo,
};
const { data: connection } = await this.httpService.axiosRef.post(
`${ConnectionManagerUrl}/${CreateMemberConnection}`,
createConnectionBody,
);
userDetails.connectionId = connection.data?.connection?.id;
const { data: savedUserInfo } = await this.httpService.axiosRef.post(
`${AttestationManagerUrl}/${SaveUserInfo}`,
userDetails,
);
return {
invitationUrl: connection.data.invitationUrl,
userInfo: savedUserInfo.data,
};
} catch (error: unknown) {
throw new HttpException(
Reflect.get(error || {}, 'message') || 'Internal server error',
Reflect.get(error || {}, 'status') || 500,
);
}
}
}
import PrismaModule from './prisma.module';
describe('Check if the module is working', () => {
it('should be defined', () => {
expect(PrismaModule).toBeDefined();
});
});
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import PrismaService from './prisma.service.js';
@Module({
imports: [ConfigModule],
controllers: [],
providers: [PrismaService],
exports: [PrismaService],
})
export default class PrismaModule {}
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { PrismaClient } from '@prisma/client';
@Injectable()
export default class PrismaService
extends PrismaClient
implements OnModuleInit, OnModuleDestroy
{
constructor(configService: ConfigService) {
super({
datasources: {
db: {
url: configService.get('DATABASE_URL'),
},
},
});
}
async onModuleInit() {
await this.$connect();
}
async onModuleDestroy() {
await this.$disconnect();
}
}
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model Principal {
id String @id @default(uuid())
}
describe('Logger', () => {
it('should create a directory if not exists', async () => {
// const dir = './logs';
// 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();
});
});
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