-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: added new subscriptions module
- Loading branch information
Showing
10 changed files
with
437 additions
and
174 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import { Test, TestingModule } from '@nestjs/testing' | ||
import { SubscriptionsController } from './subscriptions.controller' | ||
|
||
describe('SubscriptionsController', () => { | ||
let controller: SubscriptionsController | ||
|
||
beforeEach(async () => { | ||
const moduleRef: TestingModule = await Test.createTestingModule({ | ||
controllers: [SubscriptionsController], | ||
}).compile() | ||
|
||
controller = moduleRef.get<SubscriptionsController>(SubscriptionsController) | ||
}) | ||
|
||
it('should be defined', () => { | ||
expect(controller).toBeDefined() | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
import { Controller, ForbiddenException, Get, Param, Req } from '@nestjs/common' | ||
import { ApiBearerAuth, ApiOperation, ApiResponse } from '@nestjs/swagger' | ||
import { LoginDto } from '../auth/dto/login.dto' | ||
import { SubscriptionsService } from './subscriptions.service' | ||
|
||
@Controller('subscriptions') | ||
export class SubscriptionsController { | ||
constructor(private subscriptionService: SubscriptionsService) {} | ||
|
||
@Get(':did') | ||
@ApiOperation({ | ||
description: 'Get and access token for a subscription', | ||
}) | ||
@ApiResponse({ | ||
status: 200, | ||
description: 'Returns the access token', | ||
type: LoginDto, | ||
}) | ||
@ApiResponse({ | ||
status: 401, | ||
description: 'Unauthorized access', | ||
}) | ||
@ApiBearerAuth('Authorization') | ||
async getAccessToken(@Req() req, @Param('did') did: string): Promise<LoginDto> { | ||
// get subscription data | ||
const { contractAddress, numberNfts, endpoints, headers } = | ||
await this.subscriptionService.validateDid(did) | ||
|
||
// validate that the subscription is valid | ||
const isValid = this.subscriptionService.isSubscriptionValid( | ||
contractAddress, | ||
numberNfts, | ||
req.user.iss, | ||
) | ||
|
||
if (!isValid) { | ||
throw new ForbiddenException(`user ${req.user.iss} has not access to subscription ${did}`) | ||
} | ||
|
||
// get access token | ||
const accessToken = await this.subscriptionService.generateToken( | ||
did, | ||
req.user.iss, | ||
endpoints, | ||
headers, | ||
) | ||
|
||
return { access_token: accessToken } | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import { Module } from '@nestjs/common' | ||
import { NeverminedModule } from '../shared/nevermined/nvm.module' | ||
import { SubscriptionsController } from './subscriptions.controller' | ||
import { SubscriptionsService } from './subscriptions.service' | ||
|
||
@Module({ | ||
controllers: [SubscriptionsController], | ||
providers: [SubscriptionsService], | ||
imports: [NeverminedModule], | ||
}) | ||
export class SubscriptionsModule {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import { Test, TestingModule } from '@nestjs/testing' | ||
import { SubscriptionsService } from './subscriptions.service' | ||
|
||
describe('SubscriptionsService', () => { | ||
let service: SubscriptionsService | ||
|
||
beforeEach(async () => { | ||
const moduleRef: TestingModule = await Test.createTestingModule({ | ||
providers: [SubscriptionsService], | ||
}).compile() | ||
|
||
service = moduleRef.get<SubscriptionsService>(SubscriptionsService) | ||
}) | ||
|
||
it('should be defined', () => { | ||
expect(service).toBeDefined() | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
import { BadRequestException, Injectable } from '@nestjs/common' | ||
import { DDOServiceNotFoundError, findServiceConditionByName, Service } from '@nevermined-io/sdk' | ||
import { NeverminedService } from '../shared/nevermined/nvm.service' | ||
import jose from 'jose' | ||
|
||
export interface SubscriptionData { | ||
numberNfts: number | ||
contractAddress: string | ||
endpoints: string[] | ||
headers: { [key: string]: string }[] | ||
} | ||
|
||
@Injectable() | ||
export class SubscriptionsService { | ||
constructor(private nvmService: NeverminedService) {} | ||
|
||
/** | ||
* Validates if a DID has an associated subscription | ||
* | ||
* @param did - The DID of the asset with an associated subscription | ||
* | ||
* @throws {@link BadRequestException} | ||
* @returns {@link SubscriptionData} | ||
*/ | ||
public async validateDid(did: string): Promise<SubscriptionData> { | ||
// get the DDO | ||
const ddo = await this.nvmService.nevermined.assets.resolve(did) | ||
if (!ddo) { | ||
throw new BadRequestException(`${did} not found.`) | ||
} | ||
|
||
// get the nft-access service | ||
let nftAccessService: Service<'nft-access'> | ||
try { | ||
nftAccessService = ddo.findServiceByType('nft-access') | ||
} catch (e) { | ||
if (e instanceof DDOServiceNotFoundError) { | ||
throw new BadRequestException(`${did} does not contain an 'nft-access' service`) | ||
} else { | ||
throw e | ||
} | ||
} | ||
|
||
// get the nft-holder condition | ||
const nftHolderCondition = findServiceConditionByName(nftAccessService, 'nftHolder') | ||
const numberNfts = Number( | ||
nftHolderCondition.parameters.find((p) => p.name === '_numberNfts').value, | ||
) | ||
const contractAddress = nftHolderCondition.parameters.find((p) => p.name === '_contractAddress') | ||
.value as string | ||
|
||
// get the web-service endpoints | ||
const metadata = ddo.findServiceByType('metadata') | ||
if (!metadata.attributes.main.webService) { | ||
throw new BadRequestException(`${did} does not contain any web services`) | ||
} | ||
const endpoints = metadata.attributes.main.webService.endpoints.flatMap((e) => Object.values(e)) | ||
|
||
// decrypt the headers | ||
const headers = await this.nvmService.decrypt( | ||
metadata.attributes.main.webService.encryptedAttributes, | ||
'PSK-RSA', | ||
) | ||
|
||
return { | ||
numberNfts, | ||
contractAddress, | ||
endpoints, | ||
headers, | ||
} | ||
} | ||
|
||
/** | ||
* Validates if a subscription is valid or not | ||
* | ||
* @param contractAddress - The NFT-721 contract address | ||
* @param numberNfts - Amount of `contractAddress` nfts a user needs to hold in order to get access to the subscription | ||
* @param userAddress - The ethereum address of the user | ||
* | ||
* @returns {@link boolean} | ||
*/ | ||
public async isSubscriptionValid( | ||
contractAddress: string, | ||
numberNfts: number, | ||
userAddress: string, | ||
): Promise<boolean> { | ||
const nft = await this.nvmService.nevermined.contracts.loadNft721(contractAddress) | ||
const balance = await nft.balanceOf(userAddress) | ||
return balance.toNumber() >= numberNfts | ||
} | ||
|
||
public async generateToken( | ||
did: string, | ||
userAddress: string, | ||
endpoints: any, | ||
headers?: any, | ||
): Promise<string> { | ||
const JWT_SECRET_PHRASE = process.env.JWT_SECRET_PHRASE || '12345678901234567890123456789012' | ||
const JWT_SECRET = Uint8Array.from(JWT_SECRET_PHRASE.split('').map((x) => parseInt(x))) | ||
return await new jose.EncryptJWT({ | ||
did: did, | ||
userId: userAddress, | ||
endpoints, | ||
headers, | ||
}) | ||
.setProtectedHeader({ alg: 'dir', enc: 'A128CBC-HS256' }) | ||
.setIssuedAt() | ||
.setExpirationTime('1w') | ||
.encrypt(JWT_SECRET) | ||
} | ||
} |
Oops, something went wrong.