Skip to content

Commit

Permalink
Merge pull request #120 from nevermined-io/feature/subscriptions
Browse files Browse the repository at this point in the history
      feat: added new subscriptions module
  • Loading branch information
r-marques authored Mar 2, 2023
2 parents d9e5a39 + 2561de8 commit 8efecb2
Show file tree
Hide file tree
Showing 15 changed files with 708 additions and 185 deletions.
3 changes: 3 additions & 0 deletions .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,6 @@ ARGO_HOST=http://localhost:2746/
ARGO_NAMESPACE=argo
ARGO_AUTH_TOKEN="Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IjZ1QThNV1VqQ1l1S2RRNXdSMlMxWnpqM1dWU0lhRTNZV0lObVJUeXZ6YTAifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJudm0tZGlzYyIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VjcmV0Lm5hbWUiOiJhcmdvLXdvcmtmbG93LnNlcnZpY2UtYWNjb3VudC10b2tlbiIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50Lm5hbWUiOiJhcmdvLXdvcmtmbG93Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQudWlkIjoiMmU5ZmJhYTgtNDE1ZS00MDg1LWFhNzktNWY2MTQ0ZmIwZDcwIiwic3ViIjoic3lzdGVtOnNlcnZpY2VhY2NvdW50Om52bS1kaXNjOmFyZ28td29ya2Zsb3cifQ.SLpDBbEgB0RjIH38xloZjB07N0UBVmswOg5-pQl_K3vqpb15XM5TpWlQpcli5qQKuGGcNttxaxWxDByZ2tjgLFDCDzKle1zDCKz7j0089ZE3mLkDO6hd4Ndb52X8suAWSNdWOCxX0zI3v_JGozEhgnBfxg_pk-aA0hJL7CBnxpPjMD6NuiboaRGwQD4d3Q1mbF6XGRpI_76w3zB8eO3GoIZ-Yrr-8U-5eAhuMuSyMDAX7nr7BF1ci2487q0gFaV2sIY2eVjbnrIxpQKqZ0dg86ZwgwffKeo4RXEYMvB4pJkyEgxhTWyhBbYw_fS-WC5qvXM6r0x_aVll8M0iGe-iWA"
NO_GRAPH=true

JWT_SUBSCRIPTION_SECRET_KEY="12345678901234567890123456789012"
NEVERMINED_PROXY_URI=https://proxy.nevermined.one
5 changes: 5 additions & 0 deletions config/from-env.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ const keys = [
'ARGO_AUTH_TOKEN',
'COMPUTE_PROVIDER_KEYFILE',
'COMPUTE_PROVIDER_PASSWORD',

'JWT_SUBSCRIPTION_SECRET_KEY',
'NEVERMINED_PROXY_URI',
'SUBSCRIPTION_DEFAULT_EXPIRY_TIME',
'NETWORK_AVERAGE_BLOCK_TIME',
]

const config = {}
Expand Down
11 changes: 6 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "node-ts",
"version": "1.0.0",
"version": "1.1.0",
"description": "Nevermined Node",
"main": "main.ts",
"scripts": {
Expand Down Expand Up @@ -34,9 +34,9 @@
"@nestjs/swagger": "^5.2.0",
"@nestjs/typeorm": "^8.0.3",
"@nevermined-io/argo-workflows-api": "^0.1.3",
"@nevermined-io/sdk-dtp": "0.3.0",
"@nevermined-io/sdk": "1.0.0",
"@nevermined-io/passport-nevermined": "^0.1.1",
"@nevermined-io/sdk": "1.1.0",
"@nevermined-io/sdk-dtp": "0.4.0",
"@sideway/address": "^4.1.3",
"@sideway/formula": "^3.0.1",
"@sideway/pinpoint": "^2.0.0",
Expand All @@ -56,7 +56,7 @@
"formdata-polyfill": "^4.0.10",
"ipfs-http-client-lite": "^0.3.0",
"joi": "^17.6.0",
"jose": "^4.11.2",
"jose": "^4.13.0",
"js-yaml": "4.1.0",
"jsonwebtoken": "^9.0.0",
"lodash": "^4.17.21",
Expand All @@ -79,7 +79,8 @@
"@commitlint/config-conventional": "^17.4.2",
"@faker-js/faker": "^6.0.0-beta.0",
"@golevelup/ts-jest": "0.3.4",
"@nestjs/cli": "^8.2.2",
"@nestjs/cli": "^9.2.0",
"@nestjs/schematics": "^9.0.4",
"@nestjs/testing": "^8.4.0",
"@types/jest": "^29.2.3",
"@types/jsonwebtoken": "^8.5.8",
Expand Down
3 changes: 2 additions & 1 deletion src/access/access.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,10 +143,11 @@ export class AccessController {
nft_amount: BigNumber.from(transferData.nftAmount || '0'),
buyer: (req.user || {}).buyer,
}
console.log(template, nevermined.assets.servicePlugin[template])

const plugin = nevermined.assets.servicePlugin[template]
const [from] = await nevermined.accounts.list()
await plugin.process(params, from, undefined)

return 'success'
}

Expand Down
2 changes: 2 additions & 0 deletions src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { AccessModule } from './access/access.module'
import { NeverminedModule } from './shared/nevermined/nvm.module'
import { ComputeModule } from './compute/compute.module'
import { HttpLoggerMiddleware } from './common/middlewares/http-logger/http-logger.middleware'
import { SubscriptionsModule } from './subscriptions/subscriptions.module'

@Module({
imports: [
Expand All @@ -23,6 +24,7 @@ import { HttpLoggerMiddleware } from './common/middlewares/http-logger/http-logg
AuthModule,
NeverminedModule,
ComputeModule,
SubscriptionsModule,
],
})
export class ApplicationModule {
Expand Down
2 changes: 2 additions & 0 deletions src/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ import { EncryptModule } from './encrypt/encrypt.module'
import { InfoModule } from './info/info.module'
import { AccessModule } from './access/access.module'
import { ComputeModule } from './compute/compute.module'
import { SubscriptionsModule } from './subscriptions/subscriptions.module'

const exposeCompute: boolean = process.env.ENABLE_COMPUTE === 'true'

export const routes: Routes = [
{ path: '/api/v1/node/services/encrypt', module: EncryptModule },
{ path: '/api/v1/node/services/oauth', module: AuthModule },
{ path: '/api/v1/node/services/subscriptions', module: SubscriptionsModule },
{ path: '/api/v1/node/services', module: AccessModule },
{ path: '/', module: InfoModule },
]
Expand Down
35 changes: 35 additions & 0 deletions src/shared/config/config.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,13 @@ export interface ComputeConfig {
compute_provider_password: string
}

export interface SubscriptionsConfig {
jwtSecret: Uint8Array
neverminedProxyUri: string
defaultExpiryTime: number
averageBlockTime: number
}

const configProfile = require('../../../config')

const DOTENV_SCHEMA = Joi.object({
Expand All @@ -37,6 +44,13 @@ const DOTENV_SCHEMA = Joi.object({
.default('development'),
JWT_SECRET_KEY: Joi.string().required().error(new Error('JWT_SECRET_KEY is required!')),
JWT_EXPIRY_KEY: Joi.string().default('60m'),
JWT_SUBSCRIPTION_SECRET_KEY: Joi.string()
.required()
.error(new Error('JWT_SUBSCRIPTION_SECRET_KEY is required!')),
// defaults to 2 years in seconds
SUBSCRIPTION_DEFAULT_EXPIRY_TIME: Joi.number().default(60 * 60 * 24 * 365 * 2),
// Used to calculate expiry time of subscriptions in milliseconds
NETWORK_AVERAGE_BLOCK_TIME: Joi.number().default(2100),
server: Joi.object({
port: Joi.number().default(3000),
}),
Expand Down Expand Up @@ -71,6 +85,7 @@ const DOTENV_SCHEMA = Joi.object({
ARGO_AUTH_TOKEN: Joi.string(),
COMPUTE_PROVIDER_KEYFILE: Joi.string(),
COMPUTE_PROVIDER_PASSWORD: Joi.string(),
NEVERMINED_PROXY_URI: Joi.string(),
})

type DotenvSchemaKeys =
Expand Down Expand Up @@ -105,11 +120,16 @@ type DotenvSchemaKeys =
| 'ARGO_AUTH_TOKEN'
| 'COMPUTE_PROVIDER_KEYFILE'
| 'COMPUTE_PROVIDER_PASSWORD'
| 'JWT_SUBSCRIPTION_SECRET_KEY'
| 'NEVERMINED_PROXY_URI'
| 'SUBSCRIPTION_DEFAULT_EXPIRY_TIME'
| 'NETWORK_AVERAGE_BLOCK_TIME'

export class ConfigService {
private readonly envConfig: EnvConfig
private readonly crypto: CryptoConfig
private readonly compute: ComputeConfig
private readonly subscriptions: SubscriptionsConfig

constructor() {
this.envConfig = this.validateInput(configProfile)
Expand All @@ -130,6 +150,17 @@ export class ConfigService {
readFileSync(this.get('COMPUTE_PROVIDER_KEYFILE')).toString(),
compute_provider_password: this.get('COMPUTE_PROVIDER_PASSWORD'),
}

this.subscriptions = {
jwtSecret: Uint8Array.from(
this.get<string>('JWT_SUBSCRIPTION_SECRET_KEY')
.split('')
.map((x) => parseInt(x)),
),
neverminedProxyUri: this.get<string>('NEVERMINED_PROXY_URI'),
defaultExpiryTime: this.get<number>('SUBSCRIPTION_DEFAULT_EXPIRY_TIME'),
averageBlockTime: this.get<number>('NETWORK_AVERAGE_BLOCK_TIME'),
}
}

get<T>(path: DotenvSchemaKeys): T | undefined {
Expand All @@ -148,6 +179,10 @@ export class ConfigService {
return this.compute
}

subscriptionsConfig(): SubscriptionsConfig {
return this.subscriptions
}

getProviderBabyjub() {
return {
x: this.envConfig.PROVIDER_BABYJUB_PUBLIC1 || '',
Expand Down
17 changes: 14 additions & 3 deletions src/shared/nevermined/nvm.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,8 @@ export class NeverminedService {
const name = file_attributes.name
const auth_method = asset.findServiceByType('authorization').service || 'RSAES-OAEP'
if (auth_method === 'RSAES-OAEP') {
const filelist = JSON.parse(
await decrypt(this.config.cryptoConfig(), service.attributes.encryptedFiles, 'PSK-RSA'),
)
const filelist = this.decrypt(service.attributes.encryptedFiles, 'PSK-RSA')

// download url or what?
const url: string = filelist[index].url
return { url, content_type, dtp: this.isDTP(service.attributes.main), name }
Expand All @@ -98,6 +97,18 @@ export class NeverminedService {
throw new BadRequestException()
}

/**
* Decrypts a an encrypted JSON object
*
* @param encryptedJson - The encrypted json object as a string
* @param encryptionMethod - The encryption method used. Currently only PSK-RSA is supported
*
* @returns The decrypted JSON object
*/
async decrypt(encryptedJson: string, encryptionMethod: string): Promise<any> {
return JSON.parse(await decrypt(this.config.cryptoConfig(), encryptedJson, encryptionMethod))
}

async downloadAsset(
did: string,
index: number,
Expand Down
18 changes: 18 additions & 0 deletions src/subscriptions/dto/token.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { ApiProperty } from '@nestjs/swagger'
import { IsString } from 'class-validator'

export class SubscriptionTokenDto {
@ApiProperty({
description: 'The Authorization Bearer token',
example: 'eyJhbGciOiJSUzI1NiIsImtpZCI6IjIyIn0.eyJpc3Mi[...omitted for brevity...]',
})
@IsString()
accessToken: string

@ApiProperty({
description: 'The proxy uri that should be used with the provided token',
example: 'https://proxy.nevermined.one',
})
@IsString()
neverminedProxyUri: string
}
23 changes: 23 additions & 0 deletions src/subscriptions/subscriptions.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Test, TestingModule } from '@nestjs/testing'
import { ConfigModule } from '../shared/config/config.module'
import { NeverminedModule } from '../shared/nevermined/nvm.module'
import { SubscriptionsController } from './subscriptions.controller'
import { SubscriptionsService } from './subscriptions.service'

describe('SubscriptionsController', () => {
let controller: SubscriptionsController

beforeEach(async () => {
const moduleRef: TestingModule = await Test.createTestingModule({
controllers: [SubscriptionsController],
providers: [SubscriptionsService],
imports: [NeverminedModule, ConfigModule],
}).compile()

controller = moduleRef.get<SubscriptionsController>(SubscriptionsController)
})

it('should be defined', () => {
expect(controller).toBeDefined()
})
})
61 changes: 61 additions & 0 deletions src/subscriptions/subscriptions.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { Controller, ForbiddenException, Get, Param, Req } from '@nestjs/common'
import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'
import { SubscriptionTokenDto } from './dto/token.dto'
import { SubscriptionsService } from './subscriptions.service'

@ApiTags('Subscriptions')
@Controller()
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: SubscriptionTokenDto,
})
@ApiResponse({
status: 401,
description: 'Unauthorized access',
})
@ApiBearerAuth('Authorization')
async getAccessToken(@Req() req, @Param('did') did: string): Promise<SubscriptionTokenDto> {
// get subscription data
const { contractAddress, numberNfts, endpoints, headers } =
await this.subscriptionService.validateDid(did)

// validate that the subscription is valid
const isValid = await this.subscriptionService.isSubscriptionValid(
contractAddress,
numberNfts,
req.user.address,
)

if (!isValid) {
throw new ForbiddenException(`user ${req.user.iss} has not access to subscription ${did}`)
}

// get expiry time
const expiryTime = await this.subscriptionService.getExpirationTime(
contractAddress,
req.user.address,
)

// get access token
const accessToken = await this.subscriptionService.generateToken(
did,
req.user.iss,
endpoints,
expiryTime,
headers,
)

return {
accessToken: accessToken,
neverminedProxyUri: this.subscriptionService.neverminedProxyUri,
}
}
}
12 changes: 12 additions & 0 deletions src/subscriptions/subscriptions.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Module } from '@nestjs/common'
import { ConfigModule } from '../shared/config/config.module'
import { NeverminedModule } from '../shared/nevermined/nvm.module'
import { SubscriptionsController } from './subscriptions.controller'
import { SubscriptionsService } from './subscriptions.service'

@Module({
controllers: [SubscriptionsController],
providers: [SubscriptionsService],
imports: [NeverminedModule, ConfigModule],
})
export class SubscriptionsModule {}
21 changes: 21 additions & 0 deletions src/subscriptions/subscriptions.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Test, TestingModule } from '@nestjs/testing'
import { ConfigModule } from '../shared/config/config.module'
import { NeverminedModule } from '../shared/nevermined/nvm.module'
import { SubscriptionsService } from './subscriptions.service'

describe('SubscriptionsService', () => {
let service: SubscriptionsService

beforeEach(async () => {
const moduleRef: TestingModule = await Test.createTestingModule({
providers: [SubscriptionsService],
imports: [NeverminedModule, ConfigModule],
}).compile()

service = moduleRef.get<SubscriptionsService>(SubscriptionsService)
})

it('should be defined', () => {
expect(service).toBeDefined()
})
})
Loading

0 comments on commit 8efecb2

Please sign in to comment.