Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ghadi/engr 638 add endpoint to verify vcs #16

Closed
wants to merge 5 commits into from
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
26 changes: 8 additions & 18 deletions apps/vc-api/src/api/auth/auth.controller.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,27 @@
import { Body, Controller, Get, Inject, Post, Req, Res, UseGuards } from '@nestjs/common';
import { JustaName } from '@justaname.id/sdk';
import { ENVIRONMENT_GETTER, IEnvironmentGetter } from '../../core/applications/environment/ienvironment.getter';
import { AuthSigninApiRequest } from './requests/auth.signin.api.request';
import { Response, Request} from 'express';
import { JwtService } from '@nestjs/jwt';
import moment from 'moment';
import { JwtGuard } from '../../guards/jwt.guard';
import {
ISdkInitializerGetter,
SDK_INITIALIZER_GETTER
} from '../../core/applications/environment/isdk-initializer.getter';

type Siwj = { address: string, subname: string };

@Controller('auth')
export class AuthController {

justaname: JustaName
constructor(
@Inject(ENVIRONMENT_GETTER) private readonly environmentGetter: IEnvironmentGetter,

@Inject(SDK_INITIALIZER_GETTER) private readonly sdkInitializerGetter: ISdkInitializerGetter,
private readonly jwtService: JwtService
) {
this.justaname = JustaName.init({
config: {
chainId: this.environmentGetter.getChainId(),
domain: this.environmentGetter.getSiweDomain(),
origin:this.environmentGetter.getSiweOrigin(),
},
ensDomain: this.environmentGetter.getEnsDomain(),
providerUrl: (this.environmentGetter.getChainId() === 1 ? 'https://mainnet.infura.io/v3/' :'https://sepolia.infura.io/v3/') +this.environmentGetter.getInfuraProjectId()
})
}
) {}

@Get('nonce')
async getNonce() {
return this.justaname.signIn.generateNonce()
return this.sdkInitializerGetter.getInitializedSdk().signIn.generateNonce()
}

@Post('signin')
Expand All @@ -40,7 +30,7 @@ export class AuthController {
@Res() res: Response,
@Req() req: Request
) {
const { data: message, subname } = await this.justaname.signIn.signIn(body.message, body.signature)
const { data: message, subname } = await this.sdkInitializerGetter.getInitializedSdk().signIn.signIn(body.message, body.signature)

if (!message) {
res.status(500).json({ message: 'No message returned.' });
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { VerifyRecordsApiRequest } from '../requests/verify-records.api.request';
import { VerifyRecordsRequest } from '../../../core/applications/verify-records/requests/verify-records.request';
import { VeirfyRecordsResponse } from '../../../core/applications/verify-records/response/verify-records.response';
import { VerifyRecordsApiResponse } from '../responses/verify-records.api.response';

export const VERIFY_RECORDS_CONTROLLER_MAPPER = "VERIFY_RECORDS_CONTROLLER_MAPPER";

export interface IVerifyRecordsControllerMapper {
mapVerifyRecordsApiRequestToVerifyRecordsRequest(
verifyRecordsApiRequest: VerifyRecordsApiRequest,
): VerifyRecordsRequest;

mapVerifyRecordsResponseToVerifyRecordsApiResponse(
verifyRecordsResponse: VeirfyRecordsResponse[]
): VerifyRecordsApiResponse[];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Injectable } from '@nestjs/common';
import { IVerifyRecordsControllerMapper } from './iverify-records.controller.mapper';
import { VerifyRecordsApiRequest } from '../requests/verify-records.api.request';
import { VerifyRecordsRequest } from '../../../core/applications/verify-records/requests/verify-records.request';
import { VeirfyRecordsResponse } from '../../../core/applications/verify-records/response/verify-records.response';
import { VerifyRecordsApiResponse } from '../responses/verify-records.api.response';

@Injectable()
export class VerifyRecordsControllerMapper implements IVerifyRecordsControllerMapper {
constructor() {}

mapVerifyRecordsApiRequestToVerifyRecordsRequest(
verifyRecordsApiRequest: VerifyRecordsApiRequest,
): VerifyRecordsRequest {
return {
subname: verifyRecordsApiRequest.subname,
chainId: verifyRecordsApiRequest.chainId,
recordsToVerify: verifyRecordsApiRequest.recordsToVerify,
issuer: verifyRecordsApiRequest.issuer,
};
}

mapVerifyRecordsResponseToVerifyRecordsApiResponse(verifyRecordsResponses: VeirfyRecordsResponse[]): VerifyRecordsApiResponse[] {
return verifyRecordsResponses.map(verifyRecordsResponse => ({
records: {
...verifyRecordsResponse
}
}));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { IsArray, IsInt, IsOptional, IsString } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';

export class VerifyRecordsApiRequest {
@ApiProperty()
@IsString()
subname: string;

@ApiProperty()
@Type(() => Number)
@IsInt()
chainId: number;

@ApiProperty()
@IsArray()
recordsToVerify: string[];

@ApiProperty()
@IsOptional()
issuer?: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { ApiProperty } from '@nestjs/swagger';

export class VerifyRecordsApiResponse {
@ApiProperty({
type: 'object',
additionalProperties: {
type: 'boolean'
},
example: {
record1: true,
record2: false
}
})
records: { [key: string]: boolean };
}
28 changes: 28 additions & 0 deletions apps/vc-api/src/api/verify-records/verify-records.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Controller, Get, Inject, Param, Query } from '@nestjs/common';
import {
IVerifyRecordsService,
VERIFY_RECORDS_SERVICE
} from '../../core/applications/verify-records/iverify-records.service';
import { VerifyRecordsApiRequest } from './requests/verify-records.api.request';
import { IVerifyRecordsControllerMapper } from './mapper/iverify-records.controller.mapper';

@Controller('verify-records')
export class VerifyRecordsController {

constructor(
@Inject(VERIFY_RECORDS_SERVICE) private readonly verifyRecordsService: IVerifyRecordsService,
@Inject('VERIFY_RECORDS_CONTROLLER_MAPPER') private readonly verifyRecordsControllerMapper: IVerifyRecordsControllerMapper
) {}
@Get('')
async verifyRecords(
@Query() query: VerifyRecordsApiRequest
) {
const response = await this.verifyRecordsService.verifyRecords(
this.verifyRecordsControllerMapper.mapVerifyRecordsApiRequestToVerifyRecordsRequest(
query
)
);

return this.verifyRecordsControllerMapper.mapVerifyRecordsResponseToVerifyRecordsApiResponse(response);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { JustaName } from '@justaname.id/sdk';

export const SDK_INITIALIZER_GETTER = 'SDK_INITIALIZER_GETTER'

export interface ISdkInitializerGetter {
getInitializedSdk(): JustaName;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Subname } from '../../domain/entities/subname';

export const SUBNAME_RECORDS_FETCHER = 'SUBNAME_RECORDS_FETCHER';

export interface ISubnameRecordsFetcher {
fetchRecords(subname: string, chainId: number): Promise<Subname>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { VerifyRecordsRequest } from './requests/verify-records.request';
import { VeirfyRecordsResponse } from './response/verify-records.response';

export const VERIFY_RECORDS_SERVICE = 'VERIFY_RECORDS_SERVICE';

export interface IVerifyRecordsService {
verifyRecords(verifyRecordsRequest: VerifyRecordsRequest): Promise<VeirfyRecordsResponse[]>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export class VerifyRecordsRequest {
subname: string;
recordsToVerify: string[];
chainId: number;
issuer: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export class VeirfyRecordsResponse {
[key: string]: boolean;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { IVerifyRecordsService } from './iverify-records.service';
import { Inject, Injectable } from '@nestjs/common';
import { ISubnameRecordsFetcher, SUBNAME_RECORDS_FETCHER } from './isubname-records.fetcher';
import { VerifyRecordsRequest } from './requests/verify-records.request';
import { VeirfyRecordsResponse } from './response/verify-records.response';
import { Subname } from '../../domain/entities/subname';
import { ENVIRONMENT_GETTER, IEnvironmentGetter } from '../environment/ienvironment.getter';

@Injectable()
export class VerifyRecordsService implements IVerifyRecordsService {
private chainIdMapping = {
"mainnet": 1,
"sepolia": 11155111
};

domain: string;
constructor(
@Inject(SUBNAME_RECORDS_FETCHER)
private readonly subnameRecordsFetcher: ISubnameRecordsFetcher,
@Inject(ENVIRONMENT_GETTER) private readonly environmentGetter: IEnvironmentGetter,
) {
this.domain = this.environmentGetter.getEnsDomain();
}

async verifyRecords(verifyRecordsRequest: VerifyRecordsRequest): Promise<VeirfyRecordsResponse[]> {
const { subname, chainId, recordsToVerify, issuer } = verifyRecordsRequest;

const validIssuer = issuer ? issuer : this.domain;

if (chainId !== 1 && chainId !== 11155111) {
throw new Error('Invalid chainId');
}

const subnameRecords = await this.subnameRecordsFetcher.fetchRecords(subname, chainId);

const responses: VeirfyRecordsResponse[] = [];

for (const record of recordsToVerify) {
const response = this._recordVerifier(record, subnameRecords, chainId, validIssuer);
responses.push(response);
}

return responses;
}

private _recordVerifier(record: string, subnameRecords: Subname, chainId: number, issuer: string): VeirfyRecordsResponse {
// 1) check if record exists in subnameRecords, if not return false
const foundRecord = subnameRecords.metadata.textRecords.find((item) => item.key === record);

if (!foundRecord) {
return {
[record]: false
}
}

// 2) check if record_issuer exists in subnameRecords, if not return false
const foundRecordIssuer = subnameRecords.metadata.textRecords.find((item) => item.key === `${record}_${issuer}`);

if (!foundRecordIssuer) {
return {
[record]: false
}
}

// 3) parse the value of record_issuer
const vc = JSON.parse(foundRecordIssuer.value);

// 4) check the expirationDate, if expired return false
const currentDate = new Date();
const expirationDate = new Date(vc.expirationDate);
if (expirationDate < currentDate) {
return {
[record]: false
};
}

// 5) check if it belongs to the subname, if not return false
const didSubnameWithFragment = vc.credentialSubject.did.split(':')[3];
const didSubname = didSubnameWithFragment.split('#')[0];

if (didSubname !== subnameRecords.subname) {
return {
[record]: false
};
}

// 6) check the issuer did, if not return false
const issuerDid = vc.issuer.id.split(':');
const issuerChain = issuerDid[2];
const issuerNameFragment = issuerDid[3];
const issuerName = issuerNameFragment.split('#')[0];

if (issuerName !== issuer || this.chainIdMapping[issuerChain] !== chainId) {
return {
[record]: false
};
}

// 7) check if it's on the correct chain, if not return false (for both the issuer did and credential subject did, and the chainId in the proof)
const subjectDid = vc.credentialSubject.did.split(':');
const subjectChain = subjectDid[2]; // Extract chain from DID
if (this.chainIdMapping[subjectChain] !== chainId || Number(vc.proof.eip712.domain.chainId) !== chainId) {
return {
[record]: false
};
}

// 8) check that the value of the username of the credentialSubject matches the value of the record inside the subnameRecords, if not return false
if (vc.credentialSubject.username !== foundRecord.value) {
return {
[record]: false
};
}

return { [record]: true };
}
}
25 changes: 25 additions & 0 deletions apps/vc-api/src/external/sdk-initializer/sdk-initializer.getter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { ISdkInitializerGetter } from '../../core/applications/environment/isdk-initializer.getter';
import { Inject, Injectable } from '@nestjs/common';
import { JustaName } from '@justaname.id/sdk';
import { ENVIRONMENT_GETTER, IEnvironmentGetter } from '../../core/applications/environment/ienvironment.getter';

@Injectable()
export class SdkInitializerGetter implements ISdkInitializerGetter {
justaname: JustaName
constructor(
@Inject(ENVIRONMENT_GETTER) private readonly environmentGetter: IEnvironmentGetter,
) {}

getInitializedSdk(): JustaName {
return this.justaname = JustaName.init({
config: {
chainId: this.environmentGetter.getChainId(),
domain: this.environmentGetter.getSiweDomain(),
origin:this.environmentGetter.getSiweOrigin(),
},
ensDomain: this.environmentGetter.getEnsDomain(),
providerUrl: (this.environmentGetter.getChainId() === 1 ? 'https://mainnet.infura.io/v3/' :'https://sepolia.infura.io/v3/') + this.environmentGetter.getInfuraProjectId()
})
}

}
Loading
Loading