diff --git a/bruno/collections/Rafiki/Rafiki Admin APIs/Create Tenant.bru b/bruno/collections/Rafiki/Rafiki Admin APIs/Create Tenant.bru new file mode 100644 index 0000000000..32bfc96311 --- /dev/null +++ b/bruno/collections/Rafiki/Rafiki Admin APIs/Create Tenant.bru @@ -0,0 +1,40 @@ +meta { + name: Create Tenant + type: graphql + seq: 50 +} + +post { + url: {{RafikiGraphqlHost}}/graphql + body: graphql + auth: none +} + +body:graphql { + mutation CreateTenant($input: CreateTenantInput!) { + createTenant(input: $input) { + tenant { + id + } + } + } +} + +body:graphql:vars { + { + "input": { + "idpConsentEndpoint": "https://interledger.org/consent", + "idpSecret": "myVerySecureSecret", + "endpoints": [ + { + "type": "RatesUrl", + "value": "https://interledger.org/rates" + }, + { + "type": "WebhookBaseUrl", + "value": "https://interledger.org/webhooks" + } + ] + } + } +} diff --git a/bruno/collections/Rafiki/Rafiki Admin APIs/Get Tenant.bru b/bruno/collections/Rafiki/Rafiki Admin APIs/Get Tenant.bru new file mode 100644 index 0000000000..0e8a9e0b0e --- /dev/null +++ b/bruno/collections/Rafiki/Rafiki Admin APIs/Get Tenant.bru @@ -0,0 +1,36 @@ +meta { + name: Get Tenant + type: graphql + seq: 51 +} + +post { + url: {{RafikiGraphqlHost}}/graphql + body: graphql + auth: none +} + +body:graphql { + query GetTenant($id: ID!) { + tenant(id:$id) { + id + kratosIdentityId + createdAt + updatedAt + endpoints { + pageInfo { + startCursor + endCursor + hasNextPage + hasPreviousPage + } + } + } + } +} + +body:graphql:vars { + { + "id": "7a0c75bd-6c09-4d38-b013-af89ab91557a" + } +} diff --git a/bruno/collections/Rafiki/Rafiki Admin APIs/Get Tenants.bru b/bruno/collections/Rafiki/Rafiki Admin APIs/Get Tenants.bru new file mode 100644 index 0000000000..a83ebf496b --- /dev/null +++ b/bruno/collections/Rafiki/Rafiki Admin APIs/Get Tenants.bru @@ -0,0 +1,11 @@ +meta { + name: Get Tenants + type: graphql + seq: 52 +} + +post { + url: {{RafikiGraphqlHost}}/graphql + body: none + auth: none +} diff --git a/bruno/collections/Rafiki/Rafiki Admin Auth APIs/Create Tenant.bru b/bruno/collections/Rafiki/Rafiki Admin Auth APIs/Create Tenant.bru new file mode 100644 index 0000000000..f4fbf0367b --- /dev/null +++ b/bruno/collections/Rafiki/Rafiki Admin Auth APIs/Create Tenant.bru @@ -0,0 +1,11 @@ +meta { + name: Create Tenant + type: graphql + seq: 4 +} + +post { + url: {{RafikiAuthGraphqlHost}}/graphql + body: none + auth: none +} diff --git a/localenv/cloud-nine-wallet/dbinit.sql b/localenv/cloud-nine-wallet/dbinit.sql index cb8895204f..0fe8c3e88b 100644 --- a/localenv/cloud-nine-wallet/dbinit.sql +++ b/localenv/cloud-nine-wallet/dbinit.sql @@ -9,3 +9,7 @@ ALTER DATABASE cloud_nine_wallet_auth OWNER TO cloud_nine_wallet_auth; CREATE USER happy_life_bank_backend WITH PASSWORD 'happy_life_bank_backend'; CREATE DATABASE happy_life_bank_backend; ALTER DATABASE happy_life_bank_backend OWNER TO happy_life_bank_backend; + +CREATE USER happy_life_bank_auth WITH PASSWORD 'happy_life_bank_auth'; +CREATE DATABASE happy_life_bank_auth; +ALTER DATABASE happy_life_bank_auth OWNER TO happy_life_bank_auth; diff --git a/localenv/cloud-nine-wallet/docker-compose.yml b/localenv/cloud-nine-wallet/docker-compose.yml index 99239ccecf..cb2eb8b04b 100644 --- a/localenv/cloud-nine-wallet/docker-compose.yml +++ b/localenv/cloud-nine-wallet/docker-compose.yml @@ -73,8 +73,10 @@ services: REDIS_URL: redis://shared-redis:6379/0 WALLET_ADDRESS_URL: ${CLOUD_NINE_WALLET_ADDRESS_URL:-https://cloud-nine-wallet-backend/.well-known/pay} ILP_CONNECTOR_URL: ${CLOUD_NINE_CONNECTOR_URL:-http://cloud-nine-wallet-backend:3002} + AUTH_ADMIN_URL: http://cloud-nine-wallet-auth:3003/graphql ENABLE_TELEMETRY: true KEY_ID: 7097F83B-CB84-469E-96C6-2141C72E22C0 + AUTH_ADMIN_API_SECRET: rPoZpe9tVyBNCigm05QDco7WLcYa0xMao7lO5KG1XG4= depends_on: - shared-database - shared-redis diff --git a/localenv/cloud-nine-wallet/seed.yml b/localenv/cloud-nine-wallet/seed.yml index eff7d687df..81d058e509 100644 --- a/localenv/cloud-nine-wallet/seed.yml +++ b/localenv/cloud-nine-wallet/seed.yml @@ -1,3 +1,17 @@ +tenants: + - name: PrimaryTenant + idpConsentUrl: https://interledger.org/consent + idpSecret: myVerySecureSecret + endpoints: [ + { + "type": "RatesUrl", + "value": "https://interledger.org/rates" + }, + { + "type": "WebhookBaseUrl", + "value": "https://interledger.org/webhooks" + } + ] assets: - code: USD scale: 2 diff --git a/localenv/happy-life-bank/docker-compose.yml b/localenv/happy-life-bank/docker-compose.yml index 5a3c37daa4..489657021e 100644 --- a/localenv/happy-life-bank/docker-compose.yml +++ b/localenv/happy-life-bank/docker-compose.yml @@ -67,6 +67,7 @@ services: WALLET_ADDRESS_URL: ${HAPPY_LIFE_BANK_WALLET_ADDRESS_URL:-https://happy-life-bank-backend/.well-known/pay} ENABLE_TELEMETRY: true KEY_ID: 53f2d913-e98a-40b9-b270-372d0547f23d + AUTH_ADMIN_URL: http://happy-life-bank-auth:4003 depends_on: - cloud-nine-backend happy-life-auth: diff --git a/localenv/mock-account-servicing-entity/generated/graphql.ts b/localenv/mock-account-servicing-entity/generated/graphql.ts index d5b26bbf7e..d2f1d62ed5 100644 --- a/localenv/mock-account-servicing-entity/generated/graphql.ts +++ b/localenv/mock-account-servicing-entity/generated/graphql.ts @@ -197,6 +197,8 @@ export type CreateIncomingPaymentInput = { incomingAmount?: InputMaybe; /** Additional metadata associated with the incoming payment. */ metadata?: InputMaybe; + /** ID of the tenant */ + tenantId: Scalars['ID']['input']; /** Id of the wallet address under which the incoming payment will be created */ walletAddressId: Scalars['String']['input']; }; @@ -252,6 +254,8 @@ export type CreateOutgoingPaymentInput = { metadata?: InputMaybe; /** Id of the corresponding quote for that outgoing payment */ quoteId: Scalars['String']['input']; + /** ID of a tenant */ + tenantId: Scalars['ID']['input']; /** Id of the wallet address under which the outgoing payment will be created */ walletAddressId: Scalars['String']['input']; }; @@ -311,6 +315,8 @@ export type CreateQuoteInput = { receiveAmount?: InputMaybe; /** Wallet address URL of the receiver */ receiver: Scalars['String']['input']; + /** ID of the tenant */ + tenantId: Scalars['ID']['input']; /** Id of the wallet address under which the quote will be created */ walletAddressId: Scalars['String']['input']; }; @@ -333,6 +339,25 @@ export type CreateReceiverResponse = { receiver?: Maybe; }; +export type CreateTenantEndpointsInput = { + type: TenantEndpointType; + value: Scalars['String']['input']; +}; + +export type CreateTenantInput = { + /** List of endpoints types for the tenant */ + endpoints: Array; + /** IDP Endpoint */ + idpConsentEndpoint: Scalars['String']['input']; + /** IDP Secret */ + idpSecret: Scalars['String']['input']; +}; + +export type CreateTenantMutationResponse = { + __typename?: 'CreateTenantMutationResponse'; + tenant: Tenant; +}; + export type CreateWalletAddressInput = { /** Additional properties associated with the [walletAddress]. */ additionalProperties?: InputMaybe>; @@ -342,6 +367,8 @@ export type CreateWalletAddressInput = { idempotencyKey?: InputMaybe; /** Public name associated with the wallet address */ publicName?: InputMaybe; + /** ID of a tenant */ + tenantId: Scalars['ID']['input']; /** Wallet Address URL */ url: Scalars['String']['input']; }; @@ -657,6 +684,8 @@ export type Mutation = { createQuote: QuoteResponse; /** Create an internal or external Open Payments Incoming Payment. The receiver has a wallet address on either this or another Open Payments resource server. */ createReceiver: CreateReceiverResponse; + /** Create tenant */ + createTenant: CreateTenantMutationResponse; /** Create a wallet address */ createWalletAddress: CreateWalletAddressMutationResponse; /** Add a public key to a wallet address that is used to verify Open Payments requests. */ @@ -777,6 +806,11 @@ export type MutationCreateReceiverArgs = { }; +export type MutationCreateTenantArgs = { + input: CreateTenantInput; +}; + + export type MutationCreateWalletAddressArgs = { input: CreateWalletAddressInput; }; @@ -1053,6 +1087,10 @@ export type Query = { quote?: Maybe; /** Get an local or remote Open Payments Incoming Payment. The receiver has a wallet address on either this or another Open Payments resource server. */ receiver?: Maybe; + /** Fetch a tenant */ + tenant?: Maybe; + /** Fetch a page of tenants */ + tenants: TenantsConnection; /** Fetch a wallet address. */ walletAddress?: Maybe; /** Fetch a page of wallet addresses. */ @@ -1136,6 +1174,20 @@ export type QueryReceiverArgs = { }; +export type QueryTenantArgs = { + id: Scalars['ID']['input']; +}; + + +export type QueryTenantsArgs = { + after?: InputMaybe; + before?: InputMaybe; + first?: InputMaybe; + last?: InputMaybe; + sortOrder?: InputMaybe; +}; + + export type QueryWalletAddressArgs = { id: Scalars['String']['input']; }; @@ -1253,6 +1305,55 @@ export enum SortOrder { Desc = 'DESC' } +export type Tenant = Model & { + __typename?: 'Tenant'; + /** Date-time of creation */ + createdAt: Scalars['String']['output']; + /** List of tenant endpoints associated with this tenant */ + endpoints: Array; + /** Tenant ID that is used in subsequent resources */ + id: Scalars['ID']['output']; + /** Kratos identity ID */ + kratosIdentityId: Scalars['String']['output']; + /** Date-time of the update */ + updatedAt: Scalars['String']['output']; +}; + +export type TenantEdge = { + __typename?: 'TenantEdge'; + cursor: Scalars['String']['output']; + node: Tenant; +}; + +export type TenantEndpoint = { + __typename?: 'TenantEndpoint'; + type: TenantEndpointType; + value: Scalars['String']['output']; +}; + +export type TenantEndpointEdge = { + __typename?: 'TenantEndpointEdge'; + cursor: Scalars['String']['output']; + node?: Maybe; +}; + +export enum TenantEndpointType { + RatesUrl = 'RatesUrl', + WebhookBaseUrl = 'WebhookBaseUrl' +} + +export type TenantEndpointsConnection = { + __typename?: 'TenantEndpointsConnection'; + edges: Array; + pageInfo: PageInfo; +}; + +export type TenantsConnection = { + __typename?: 'TenantsConnection'; + edges: Array; + pageInfo: PageInfo; +}; + export enum TransferType { /** Deposit transfer type. */ Deposit = 'DEPOSIT', @@ -1563,7 +1664,7 @@ export type DirectiveResolverFn> = { BasePayment: ( Partial ) | ( Partial ) | ( Partial ); - Model: ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ); + Model: ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ); }; /** Mapping between all available schema types and the resolvers types */ @@ -1601,6 +1702,9 @@ export type ResolversTypes = { CreateQuoteInput: ResolverTypeWrapper>; CreateReceiverInput: ResolverTypeWrapper>; CreateReceiverResponse: ResolverTypeWrapper>; + CreateTenantEndpointsInput: ResolverTypeWrapper>; + CreateTenantInput: ResolverTypeWrapper>; + CreateTenantMutationResponse: ResolverTypeWrapper>; CreateWalletAddressInput: ResolverTypeWrapper>; CreateWalletAddressKeyInput: ResolverTypeWrapper>; CreateWalletAddressKeyMutationResponse: ResolverTypeWrapper>; @@ -1670,6 +1774,13 @@ export type ResolversTypes = { SetFeeResponse: ResolverTypeWrapper>; SortOrder: ResolverTypeWrapper>; String: ResolverTypeWrapper>; + Tenant: ResolverTypeWrapper>; + TenantEdge: ResolverTypeWrapper>; + TenantEndpoint: ResolverTypeWrapper>; + TenantEndpointEdge: ResolverTypeWrapper>; + TenantEndpointType: ResolverTypeWrapper>; + TenantEndpointsConnection: ResolverTypeWrapper>; + TenantsConnection: ResolverTypeWrapper>; TransferType: ResolverTypeWrapper>; TriggerWalletAddressEventsInput: ResolverTypeWrapper>; TriggerWalletAddressEventsMutationResponse: ResolverTypeWrapper>; @@ -1731,6 +1842,9 @@ export type ResolversParentTypes = { CreateQuoteInput: Partial; CreateReceiverInput: Partial; CreateReceiverResponse: Partial; + CreateTenantEndpointsInput: Partial; + CreateTenantInput: Partial; + CreateTenantMutationResponse: Partial; CreateWalletAddressInput: Partial; CreateWalletAddressKeyInput: Partial; CreateWalletAddressKeyMutationResponse: Partial; @@ -1792,6 +1906,12 @@ export type ResolversParentTypes = { SetFeeInput: Partial; SetFeeResponse: Partial; String: Partial; + Tenant: Partial; + TenantEdge: Partial; + TenantEndpoint: Partial; + TenantEndpointEdge: Partial; + TenantEndpointsConnection: Partial; + TenantsConnection: Partial; TriggerWalletAddressEventsInput: Partial; TriggerWalletAddressEventsMutationResponse: Partial; UInt8: Partial; @@ -1913,6 +2033,11 @@ export type CreateReceiverResponseResolvers; }; +export type CreateTenantMutationResponseResolvers = { + tenant?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type CreateWalletAddressKeyMutationResponseResolvers = { walletAddressKey?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; @@ -2016,7 +2141,7 @@ export type LiquidityMutationResponseResolvers = { - __resolveType: TypeResolveFn<'AccountingTransfer' | 'Asset' | 'Fee' | 'IncomingPayment' | 'OutgoingPayment' | 'Payment' | 'Peer' | 'WalletAddress' | 'WalletAddressKey' | 'WebhookEvent', ParentType, ContextType>; + __resolveType: TypeResolveFn<'AccountingTransfer' | 'Asset' | 'Fee' | 'IncomingPayment' | 'OutgoingPayment' | 'Payment' | 'Peer' | 'Tenant' | 'WalletAddress' | 'WalletAddressKey' | 'WebhookEvent', ParentType, ContextType>; createdAt?: Resolver; id?: Resolver; }; @@ -2037,6 +2162,7 @@ export type MutationResolvers, ParentType, ContextType, RequireFields>; createQuote?: Resolver>; createReceiver?: Resolver>; + createTenant?: Resolver>; createWalletAddress?: Resolver>; createWalletAddressKey?: Resolver, ParentType, ContextType, RequireFields>; createWalletAddressWithdrawal?: Resolver, ParentType, ContextType, RequireFields>; @@ -2162,6 +2288,8 @@ export type QueryResolvers>; quote?: Resolver, ParentType, ContextType, RequireFields>; receiver?: Resolver, ParentType, ContextType, RequireFields>; + tenant?: Resolver, ParentType, ContextType, RequireFields>; + tenants?: Resolver>; walletAddress?: Resolver, ParentType, ContextType, RequireFields>; walletAddresses?: Resolver>; webhookEvents?: Resolver>; @@ -2219,6 +2347,45 @@ export type SetFeeResponseResolvers; }; +export type TenantResolvers = { + createdAt?: Resolver; + endpoints?: Resolver, ParentType, ContextType>; + id?: Resolver; + kratosIdentityId?: Resolver; + updatedAt?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type TenantEdgeResolvers = { + cursor?: Resolver; + node?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type TenantEndpointResolvers = { + type?: Resolver; + value?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type TenantEndpointEdgeResolvers = { + cursor?: Resolver; + node?: Resolver, ParentType, ContextType>; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type TenantEndpointsConnectionResolvers = { + edges?: Resolver, ParentType, ContextType>; + pageInfo?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type TenantsConnectionResolvers = { + edges?: Resolver, ParentType, ContextType>; + pageInfo?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type TriggerWalletAddressEventsMutationResponseResolvers = { count?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; @@ -2338,6 +2505,7 @@ export type Resolvers = { CreateOrUpdatePeerByUrlMutationResponse?: CreateOrUpdatePeerByUrlMutationResponseResolvers; CreatePeerMutationResponse?: CreatePeerMutationResponseResolvers; CreateReceiverResponse?: CreateReceiverResponseResolvers; + CreateTenantMutationResponse?: CreateTenantMutationResponseResolvers; CreateWalletAddressKeyMutationResponse?: CreateWalletAddressKeyMutationResponseResolvers; CreateWalletAddressMutationResponse?: CreateWalletAddressMutationResponseResolvers; DeleteAssetMutationResponse?: DeleteAssetMutationResponseResolvers; @@ -2375,6 +2543,12 @@ export type Resolvers = { Receiver?: ReceiverResolvers; RevokeWalletAddressKeyMutationResponse?: RevokeWalletAddressKeyMutationResponseResolvers; SetFeeResponse?: SetFeeResponseResolvers; + Tenant?: TenantResolvers; + TenantEdge?: TenantEdgeResolvers; + TenantEndpoint?: TenantEndpointResolvers; + TenantEndpointEdge?: TenantEndpointEdgeResolvers; + TenantEndpointsConnection?: TenantEndpointsConnectionResolvers; + TenantsConnection?: TenantsConnectionResolvers; TriggerWalletAddressEventsMutationResponse?: TriggerWalletAddressEventsMutationResponseResolvers; UInt8?: GraphQLScalarType; UInt64?: GraphQLScalarType; diff --git a/packages/auth/codegen.yml b/packages/auth/codegen.yml index e5e158fa22..cff33d3bc1 100644 --- a/packages/auth/codegen.yml +++ b/packages/auth/codegen.yml @@ -12,3 +12,16 @@ generates: src/graphql/generated/graphql.schema.json: plugins: - 'introspection' + ../../packages/backend/src/generated/graphql.ts: + plugins: + - 'typescript' + - 'typescript-resolvers' + - 'typescript-operations' + config: + omitOperationSuffix: true + defaultMapper: Partial<{T}> + # By default it is T | null. But the service code uses typescript's optional types (`foo?: T`) which are `T | undefined`. This saves the trouble of converting them explicitly. + inputMaybeValue: T | undefined + scalars: + UInt8: number + UInt64: bigint diff --git a/packages/auth/migrations/20240827131401_create_tenants_tables.js b/packages/auth/migrations/20240827131401_create_tenants_tables.js new file mode 100644 index 0000000000..4aa68c3ff4 --- /dev/null +++ b/packages/auth/migrations/20240827131401_create_tenants_tables.js @@ -0,0 +1,22 @@ +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.up = function (knex) { + return knex.schema.createTable('tenants', function (table) { + table.uuid('id').primary() + table.string('idpConsentEndpoint').notNullable() + table.string('idpSecret').notNullable() + table.timestamp('createdAt').defaultTo(knex.fn.now()) + table.timestamp('updatedAt').defaultTo(knex.fn.now()) + table.timestamp('deletedAt').nullable().defaultTo(null) + }) +} + +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.down = function (knex) { + return knex.schema.dropTable('tenants') +} diff --git a/packages/auth/migrations/20240827131417_update_grants_with_tenantId.js b/packages/auth/migrations/20240827131417_update_grants_with_tenantId.js new file mode 100644 index 0000000000..98ca85fbe7 --- /dev/null +++ b/packages/auth/migrations/20240827131417_update_grants_with_tenantId.js @@ -0,0 +1,21 @@ +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.up = function (knex) { + return knex.schema.table('grants', function (table) { + table.uuid('tenantId').notNullable() + table.foreign('tenantId').references('id').inTable('tenants') + }) +} + +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.down = function (knex) { + return knex.schema.table('grants', function (table) { + table.dropForeign(['tenantId']) + table.dropColumn('tenantId') + }) +} diff --git a/packages/auth/src/app.ts b/packages/auth/src/app.ts index 5d45deab10..dc27684dbf 100644 --- a/packages/auth/src/app.ts +++ b/packages/auth/src/app.ts @@ -54,6 +54,7 @@ import { Redis } from 'ioredis' import { LoggingPlugin } from './graphql/plugin' import { gnapServerErrorMiddleware } from './shared/gnapErrors' import { verifyApiSignature } from './shared/utils' +import { TenantService } from './tenants/service' export interface AppContextData extends DefaultContext { logger: Logger @@ -102,6 +103,7 @@ export interface AppServices { grantRoutes: Promise interactionRoutes: Promise redis: Promise + tenantService: Promise } export type AppContainer = IocContract diff --git a/packages/auth/src/config/app.ts b/packages/auth/src/config/app.ts index d490536da4..19ca134993 100644 --- a/packages/auth/src/config/app.ts +++ b/packages/auth/src/config/app.ts @@ -42,6 +42,8 @@ export const Config = { 'AUTH_DATABASE_URL', 'postgresql://postgres:password@localhost:5432/auth_development' ), + + //TODO: move these two identity stuff from env to tenant identityServerUrl: envString('IDENTITY_SERVER_URL'), identityServerSecret: envString('IDENTITY_SERVER_SECRET'), authServerUrl: envString('AUTH_SERVER_URL'), diff --git a/packages/auth/src/grant/model.ts b/packages/auth/src/grant/model.ts index 54cfb39201..c5505ac02a 100644 --- a/packages/auth/src/grant/model.ts +++ b/packages/auth/src/grant/model.ts @@ -78,6 +78,7 @@ export class Grant extends BaseModel { public clientNonce?: string // client-generated nonce for post-interaction hash public lastContinuedAt!: Date + public tenantId!: string public $beforeInsert(context: QueryContext): void { super.$beforeInsert(context) diff --git a/packages/auth/src/graphql/generated/graphql.schema.json b/packages/auth/src/graphql/generated/graphql.schema.json index 420f2498dc..83047899f7 100644 --- a/packages/auth/src/graphql/generated/graphql.schema.json +++ b/packages/auth/src/graphql/generated/graphql.schema.json @@ -127,6 +127,146 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "INPUT_OBJECT", + "name": "CreateTenantInput", + "description": null, + "fields": null, + "inputFields": [ + { + "name": "idpConsentEndpoint", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "idpSecret", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "tenantId", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "CreateTenantMutationResponse", + "description": null, + "fields": [ + { + "name": "tenant", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Tenant", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "DeleteTenantInput", + "description": null, + "fields": null, + "inputFields": [ + { + "name": "tenantId", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "DeleteTenantMutationResponse", + "description": null, + "fields": [ + { + "name": "success", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, { "kind": "INPUT_OBJECT", "name": "FilterFinalizationReason", @@ -720,6 +860,72 @@ "name": "Mutation", "description": null, "fields": [ + { + "name": "createTenant", + "description": "Create Tenant", + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "CreateTenantInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "CreateTenantMutationResponse", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deleteTenant", + "description": "Delete Tenant", + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "DeleteTenantInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "DeleteTenantMutationResponse", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "revokeGrant", "description": "Revoke Grant", @@ -1105,6 +1311,33 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "OBJECT", + "name": "Tenant", + "description": null, + "fields": [ + { + "name": "id", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, { "kind": "SCALAR", "name": "UInt8", diff --git a/packages/auth/src/graphql/generated/graphql.ts b/packages/auth/src/graphql/generated/graphql.ts index dfc10637f5..f4edff0ef3 100644 --- a/packages/auth/src/graphql/generated/graphql.ts +++ b/packages/auth/src/graphql/generated/graphql.ts @@ -34,6 +34,26 @@ export type Access = Model & { type: Scalars['String']['output']; }; +export type CreateTenantInput = { + idpConsentEndpoint: Scalars['String']['input']; + idpSecret: Scalars['String']['input']; + tenantId: Scalars['ID']['input']; +}; + +export type CreateTenantMutationResponse = { + __typename?: 'CreateTenantMutationResponse'; + tenant: Tenant; +}; + +export type DeleteTenantInput = { + tenantId: Scalars['ID']['input']; +}; + +export type DeleteTenantMutationResponse = { + __typename?: 'DeleteTenantMutationResponse'; + success: Scalars['Boolean']['output']; +}; + export type FilterFinalizationReason = { in?: InputMaybe>; notIn?: InputMaybe>; @@ -121,11 +141,25 @@ export type Model = { export type Mutation = { __typename?: 'Mutation'; + /** Create Tenant */ + createTenant: CreateTenantMutationResponse; + /** Delete Tenant */ + deleteTenant: DeleteTenantMutationResponse; /** Revoke Grant */ revokeGrant: RevokeGrantMutationResponse; }; +export type MutationCreateTenantArgs = { + input: CreateTenantInput; +}; + + +export type MutationDeleteTenantArgs = { + input: DeleteTenantInput; +}; + + export type MutationRevokeGrantArgs = { input: RevokeGrantInput; }; @@ -190,6 +224,11 @@ export enum SortOrder { Desc = 'DESC' } +export type Tenant = { + __typename?: 'Tenant'; + id: Scalars['ID']['output']; +}; + export type ResolverTypeWrapper = Promise | T; @@ -267,6 +306,10 @@ export type ResolversInterfaceTypes> = { export type ResolversTypes = { Access: ResolverTypeWrapper>; Boolean: ResolverTypeWrapper>; + CreateTenantInput: ResolverTypeWrapper>; + CreateTenantMutationResponse: ResolverTypeWrapper>; + DeleteTenantInput: ResolverTypeWrapper>; + DeleteTenantMutationResponse: ResolverTypeWrapper>; FilterFinalizationReason: ResolverTypeWrapper>; FilterGrantState: ResolverTypeWrapper>; FilterString: ResolverTypeWrapper>; @@ -288,6 +331,7 @@ export type ResolversTypes = { RevokeGrantMutationResponse: ResolverTypeWrapper>; SortOrder: ResolverTypeWrapper>; String: ResolverTypeWrapper>; + Tenant: ResolverTypeWrapper>; UInt8: ResolverTypeWrapper>; UInt64: ResolverTypeWrapper>; }; @@ -296,6 +340,10 @@ export type ResolversTypes = { export type ResolversParentTypes = { Access: Partial; Boolean: Partial; + CreateTenantInput: Partial; + CreateTenantMutationResponse: Partial; + DeleteTenantInput: Partial; + DeleteTenantMutationResponse: Partial; FilterFinalizationReason: Partial; FilterGrantState: Partial; FilterString: Partial; @@ -314,6 +362,7 @@ export type ResolversParentTypes = { RevokeGrantInput: Partial; RevokeGrantMutationResponse: Partial; String: Partial; + Tenant: Partial; UInt8: Partial; UInt64: Partial; }; @@ -328,6 +377,16 @@ export type AccessResolvers; }; +export type CreateTenantMutationResponseResolvers = { + tenant?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type DeleteTenantMutationResponseResolvers = { + success?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type GrantResolvers = { access?: Resolver, ParentType, ContextType>; client?: Resolver; @@ -365,6 +424,8 @@ export type ModelResolvers = { + createTenant?: Resolver>; + deleteTenant?: Resolver>; revokeGrant?: Resolver>; }; @@ -393,6 +454,11 @@ export type RevokeGrantMutationResponseResolvers; }; +export type TenantResolvers = { + id?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export interface UInt8ScalarConfig extends GraphQLScalarTypeConfig { name: 'UInt8'; } @@ -403,6 +469,8 @@ export interface UInt64ScalarConfig extends GraphQLScalarTypeConfig = { Access?: AccessResolvers; + CreateTenantMutationResponse?: CreateTenantMutationResponseResolvers; + DeleteTenantMutationResponse?: DeleteTenantMutationResponseResolvers; Grant?: GrantResolvers; GrantEdge?: GrantEdgeResolvers; GrantsConnection?: GrantsConnectionResolvers; @@ -413,6 +481,7 @@ export type Resolvers = { PaymentAmount?: PaymentAmountResolvers; Query?: QueryResolvers; RevokeGrantMutationResponse?: RevokeGrantMutationResponseResolvers; + Tenant?: TenantResolvers; UInt8?: GraphQLScalarType; UInt64?: GraphQLScalarType; }; diff --git a/packages/auth/src/graphql/resolvers/index.ts b/packages/auth/src/graphql/resolvers/index.ts index ace34af505..812c5fbf27 100644 --- a/packages/auth/src/graphql/resolvers/index.ts +++ b/packages/auth/src/graphql/resolvers/index.ts @@ -1,6 +1,7 @@ import { Resolvers } from '../generated/graphql' import { getGrantById, getGrants, revokeGrant } from './grant' +import { createTenant } from './tenant' import { GraphQLBigInt, GraphQLUInt8 } from '../scalars' export const resolvers: Resolvers = { @@ -11,6 +12,7 @@ export const resolvers: Resolvers = { grant: getGrantById }, Mutation: { - revokeGrant + revokeGrant, + createTenant } } diff --git a/packages/auth/src/graphql/resolvers/tenant.test.ts b/packages/auth/src/graphql/resolvers/tenant.test.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/auth/src/graphql/resolvers/tenant.ts b/packages/auth/src/graphql/resolvers/tenant.ts new file mode 100644 index 0000000000..d027c27d53 --- /dev/null +++ b/packages/auth/src/graphql/resolvers/tenant.ts @@ -0,0 +1,51 @@ +import { GraphQLError } from 'graphql' +import { ApolloContext } from '../../app' +import { + errorToCode, + errorToMessage, + isTenantError, + TenantError +} from '../../tenants/errors' +import { MutationResolvers, ResolversTypes } from '../generated/graphql' + +export const createTenant: MutationResolvers['createTenant'] = + async ( + _, + args, + ctx + ): Promise => { + const tenantService = await ctx.container.use('tenantService') + + const tenantOrError = await tenantService.create(args.input) + + if (isTenantError(tenantOrError)) { + throw new GraphQLError(errorToMessage[tenantOrError], { + extensions: { + code: errorToCode[tenantOrError] + } + }) + } + + return { tenant: tenantOrError } + } + +export const deleteTenant: MutationResolvers['deleteTenant'] = + async ( + _, + args, + ctx + ): Promise => { + const tenantService = await ctx.container.use('tenantService') + + const tenant = await tenantService.delete(args.input.tenantId) + if (!tenant) { + throw new GraphQLError(errorToMessage[TenantError.UnknownTenant], { + extensions: { + code: errorToCode[TenantError.UnknownTenant] + } + }) + } + return { + success: true + } + } diff --git a/packages/auth/src/graphql/schema.graphql b/packages/auth/src/graphql/schema.graphql index a80d651089..e65bd7eb5b 100644 --- a/packages/auth/src/graphql/schema.graphql +++ b/packages/auth/src/graphql/schema.graphql @@ -22,6 +22,12 @@ type Query { type Mutation { "Revoke Grant" revokeGrant(input: RevokeGrantInput!): RevokeGrantMutationResponse! + + "Create Tenant" + createTenant(input: CreateTenantInput!): CreateTenantMutationResponse! + + "Delete Tenant" + deleteTenant(input: DeleteTenantInput!): DeleteTenantMutationResponse! } type PageInfo { @@ -45,6 +51,16 @@ type GrantEdge { cursor: String! } +input CreateTenantInput { + tenantId: ID! + idpSecret: String! + idpConsentEndpoint: String! +} + +input DeleteTenantInput { + tenantId: ID! +} + input GrantFilter { identifier: FilterString state: FilterGrantState @@ -127,6 +143,18 @@ type RevokeGrantMutationResponse { id: ID! } +type DeleteTenantMutationResponse { + success: Boolean! +} + +type CreateTenantMutationResponse { + tenant: Tenant! +} + +type Tenant { + id: ID! +} + enum GrantState { "grant request is determining what state to enter next" PROCESSING diff --git a/packages/auth/src/index.ts b/packages/auth/src/index.ts index 2315fe083b..c0fccfb93f 100644 --- a/packages/auth/src/index.ts +++ b/packages/auth/src/index.ts @@ -21,6 +21,7 @@ import { import { createInteractionService } from './interaction/service' import { getTokenIntrospectionOpenAPI } from 'token-introspection' import { Redis } from 'ioredis' +import { createTenantService } from './tenants/service' const container = initIocContainer(Config) const app = new App(container) @@ -92,6 +93,18 @@ export function initIocContainer( }) }) + container.singleton( + 'tenantService', + async (deps: IocContract) => { + const [logger, knex] = await Promise.all([ + deps.use('logger'), + deps.use('knex') + ]) + + return createTenantService({ logger, knex }) + } + ) + container.singleton( 'accessService', async (deps: IocContract) => { diff --git a/packages/auth/src/tenants/errors.ts b/packages/auth/src/tenants/errors.ts new file mode 100644 index 0000000000..efbd0d55b6 --- /dev/null +++ b/packages/auth/src/tenants/errors.ts @@ -0,0 +1,24 @@ +import { GraphQLErrorCode } from '../graphql/errors' + +export enum TenantError { + UnknownError = 'UnknownError', + UnknownTenant = 'UnknownTenant' +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const isTenantError = (t: any): t is TenantError => + Object.values(TenantError).includes(t) + +export const errorToCode: { + [key in TenantError]: GraphQLErrorCode +} = { + [TenantError.UnknownError]: GraphQLErrorCode.InternalServerError, + [TenantError.UnknownTenant]: GraphQLErrorCode.NotFound +} + +export const errorToMessage: { + [key in TenantError]: string +} = { + [TenantError.UnknownError]: 'Unknown error', + [TenantError.UnknownTenant]: 'Unknown Tenant' +} diff --git a/packages/auth/src/tenants/model.ts b/packages/auth/src/tenants/model.ts new file mode 100644 index 0000000000..452e44e56d --- /dev/null +++ b/packages/auth/src/tenants/model.ts @@ -0,0 +1,11 @@ +import { BaseModel } from '../shared/baseModel' + +export class Tenant extends BaseModel { + public static get tableName(): string { + return 'tenants' + } + + public idpConsentEndpoint!: string + public idpSecret!: string + public deletedAt?: Date +} diff --git a/packages/auth/src/tenants/service.ts b/packages/auth/src/tenants/service.ts new file mode 100644 index 0000000000..d23462e36e --- /dev/null +++ b/packages/auth/src/tenants/service.ts @@ -0,0 +1,66 @@ +import { BaseService } from '../shared/baseService' +import { TenantError } from './errors' +import { Tenant } from './model' + +export interface CreateOptions { + tenantId: string + idpConsentEndpoint: string + idpSecret: string +} + +export interface TenantService { + create(createOptions: CreateOptions): Promise + delete(tenantId: string): Promise +} + +type ServiceDependencies = BaseService + +export async function createTenantService({ + logger, + knex +}: ServiceDependencies): Promise { + const deps: ServiceDependencies = { + logger: logger.child({ + service: 'TenantService' + }), + knex + } + + return { + create: (options: CreateOptions) => createTenant(deps, options), + delete: (id: string) => deleteTenant(deps, id) + } +} + +async function deleteTenant( + deps: ServiceDependencies, + id: string +): Promise { + return Tenant.query(deps.knex).deleteById(id).returning('*').first() +} + +async function createTenant( + deps: ServiceDependencies, + options: CreateOptions +): Promise { + const tenantData = { + id: options.tenantId, + idpConsentEndpoint: options.idpConsentEndpoint, + idpSecret: options.idpSecret + } + + try { + const result = await Tenant.query(deps.knex).insert(tenantData) + return result + } catch (err) { + deps.logger.warn( + { + tenantId: tenantData.id, + idpConsentEndpoint: tenantData.idpConsentEndpoint + }, + 'Unable to create tenant' + ) + + throw err + } +} diff --git a/packages/backend/migrations/20240827115808_create_tenants_tables.js b/packages/backend/migrations/20240827115808_create_tenants_tables.js new file mode 100644 index 0000000000..cfa2582e1d --- /dev/null +++ b/packages/backend/migrations/20240827115808_create_tenants_tables.js @@ -0,0 +1,21 @@ +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.up = function (knex) { + return knex.schema.createTable('tenants', function (table) { + table.uuid('id').primary() + table.string('kratosIdentityId').notNullable() + table.timestamp('createdAt').defaultTo(knex.fn.now()) + table.timestamp('updatedAt').defaultTo(knex.fn.now()) + table.timestamp('deletedAt').nullable().defaultTo(null) + }) +} + +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.down = function (knex) { + return knex.schema.dropTable('tenants') +} diff --git a/packages/backend/migrations/20240827115832_update_resources_with_tenantId.js b/packages/backend/migrations/20240827115832_update_resources_with_tenantId.js new file mode 100644 index 0000000000..6d9f201a57 --- /dev/null +++ b/packages/backend/migrations/20240827115832_update_resources_with_tenantId.js @@ -0,0 +1,55 @@ +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.up = function (knex) { + return knex.schema + .table('quotes', function (table) { + table.uuid('tenantId').notNullable() + table.foreign('tenantId').references('id').inTable('tenants') + }) + .table('incomingPayments', function (table) { + table.uuid('tenantId').notNullable() + table.foreign('tenantId').references('id').inTable('tenants') + }) + .table('outgoingPayments', function (table) { + table.uuid('tenantId').notNullable() + table.foreign('tenantId').references('id').inTable('tenants') + }) + .table('walletAddresses', function (table) { + table.uuid('tenantId').notNullable() + table.foreign('tenantId').references('id').inTable('tenants') + }) + .table('grants', function (table) { + table.uuid('tenantId').notNullable() + table.foreign('tenantId').references('id').inTable('tenants') + }) +} + +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.down = function (knex) { + return knex.schema + .table('quotes', function (table) { + table.dropForeign(['tenantId']) + table.dropColumn('tenantId') + }) + .table('incomingPayments', function (table) { + table.dropForeign(['tenantId']) + table.dropColumn('tenantId') + }) + .table('outgoingPayments', function (table) { + table.dropForeign(['tenantId']) + table.dropColumn('tenantId') + }) + .table('walletAddresses', function (table) { + table.dropForeign(['tenantId']) + table.dropColumn('tenantId') + }) + .table('grants', function (table) { + table.dropForeign(['tenantId']) + table.dropColumn('tenantId') + }) +} diff --git a/packages/backend/migrations/20240827144715_create_tenant_endpoints_table.js b/packages/backend/migrations/20240827144715_create_tenant_endpoints_table.js new file mode 100644 index 0000000000..efa68a8193 --- /dev/null +++ b/packages/backend/migrations/20240827144715_create_tenant_endpoints_table.js @@ -0,0 +1,26 @@ +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.up = function (knex) { + return knex.schema.createTable('tenantEndpoints', function (table) { + table.uuid('tenantId').notNullable() + table.enum('type', ['WebhookBaseUrl', 'RatesUrl']) + + table.string('value').notNullable() + table.timestamp('createdAt').defaultTo(knex.fn.now()) + table.timestamp('updatedAt').defaultTo(knex.fn.now()) + table.timestamp('deletedAt').nullable().defaultTo(null) + + table.foreign('tenantId').references('id').inTable('tenants') + table.primary(['tenantId', 'type']) + }) +} + +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.down = function (knex) { + return knex.schema.dropTable('tenantEndpoints') +} diff --git a/packages/backend/src/app.ts b/packages/backend/src/app.ts index c56544f38f..d0cf7416c3 100644 --- a/packages/backend/src/app.ts +++ b/packages/backend/src/app.ts @@ -100,6 +100,7 @@ import { import { LoggingPlugin } from './graphql/plugin' import { GrantService } from './open_payments/grant/service' import { AuthServerService } from './open_payments/authServer/service' +import { TenantService } from './tenant/service' export interface AppContextData { logger: Logger container: AppContainer @@ -254,6 +255,7 @@ export interface AppServices { tigerBeetle?: Promise paymentMethodHandlerService: Promise ilpPaymentService: Promise + tenantService: Promise } export type AppContainer = IocContract diff --git a/packages/backend/src/config/app.ts b/packages/backend/src/config/app.ts index 29a3c7070a..b422a0ec46 100644 --- a/packages/backend/src/config/app.ts +++ b/packages/backend/src/config/app.ts @@ -77,6 +77,9 @@ export const Config = { ), adminPort: envInt('ADMIN_PORT', 3001), openPaymentsUrl: envString('OPEN_PAYMENTS_URL'), + authAdminApiUrl: envString('AUTH_ADMIN_URL'), + authAdminApiSecret: envString('AUTH_ADMIN_API_SECRET', ''), + authAdminApiSignatureVersion: envInt('ADMIN_API_SIGNATURE_VERSION', 1), openPaymentsPort: envInt('OPEN_PAYMENTS_PORT', 3003), connectorPort: envInt('CONNECTOR_PORT', 3002), autoPeeringServerPort: envInt('AUTO_PEERING_SERVER_PORT', 3005), diff --git a/packages/backend/src/generated/graphql.ts b/packages/backend/src/generated/graphql.ts new file mode 100644 index 0000000000..94722173c2 --- /dev/null +++ b/packages/backend/src/generated/graphql.ts @@ -0,0 +1,488 @@ +import { GraphQLResolveInfo, GraphQLScalarType, GraphQLScalarTypeConfig } from 'graphql'; +export type Maybe = T | null; +export type InputMaybe = T | undefined; +export type Exact = { [K in keyof T]: T[K] }; +export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; +export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; +export type MakeEmpty = { [_ in K]?: never }; +export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; +export type RequireFields = Omit & { [P in K]-?: NonNullable }; +/** All built-in and custom scalars, mapped to their actual values */ +export type Scalars = { + ID: { input: string; output: string; } + String: { input: string; output: string; } + Boolean: { input: boolean; output: boolean; } + Int: { input: number; output: number; } + Float: { input: number; output: number; } + UInt8: { input: number; output: number; } + UInt64: { input: bigint; output: bigint; } +}; + +export type Access = Model & { + __typename?: 'Access'; + /** Access action (create, read, list or complete) */ + actions: Array>; + /** Date-time of creation */ + createdAt: Scalars['String']['output']; + /** Access id */ + id: Scalars['ID']['output']; + /** Wallet address of a sub-resource (incoming payment, outgoing payment, or quote) */ + identifier?: Maybe; + /** Payment limits */ + limits?: Maybe; + /** Access type (incoming payment, outgoing payment, or quote) */ + type: Scalars['String']['output']; +}; + +export type CreateTenantInput = { + idpConsentEndpoint: Scalars['String']['input']; + idpSecret: Scalars['String']['input']; + tenantId: Scalars['ID']['input']; +}; + +export type CreateTenantMutationResponse = { + __typename?: 'CreateTenantMutationResponse'; + tenant: Tenant; +}; + +export type DeleteTenantInput = { + tenantId: Scalars['ID']['input']; +}; + +export type DeleteTenantMutationResponse = { + __typename?: 'DeleteTenantMutationResponse'; + success: Scalars['Boolean']['output']; +}; + +export type FilterFinalizationReason = { + in?: InputMaybe>; + notIn?: InputMaybe>; +}; + +export type FilterGrantState = { + in?: InputMaybe>; + notIn?: InputMaybe>; +}; + +export type FilterString = { + in?: InputMaybe>; +}; + +export type Grant = Model & { + __typename?: 'Grant'; + /** Access details */ + access: Array; + /** Wallet address of the grantee's account */ + client: Scalars['String']['output']; + /** Date-time of creation */ + createdAt: Scalars['String']['output']; + /** Reason a grant was finalized */ + finalizationReason?: Maybe; + /** Grant id */ + id: Scalars['ID']['output']; + /** State of the grant */ + state: GrantState; +}; + +export type GrantEdge = { + __typename?: 'GrantEdge'; + cursor: Scalars['String']['output']; + node: Grant; +}; + +export type GrantFilter = { + finalizationReason?: InputMaybe; + identifier?: InputMaybe; + state?: InputMaybe; +}; + +export enum GrantFinalization { + /** grant was issued */ + Issued = 'ISSUED', + /** grant was rejected */ + Rejected = 'REJECTED', + /** grant was revoked */ + Revoked = 'REVOKED' +} + +export enum GrantState { + /** grant was approved */ + Approved = 'APPROVED', + /** grant was finalized and no more access tokens or interactions can be made on it */ + Finalized = 'FINALIZED', + /** grant request is awaiting interaction */ + Pending = 'PENDING', + /** grant request is determining what state to enter next */ + Processing = 'PROCESSING' +} + +export type GrantsConnection = { + __typename?: 'GrantsConnection'; + edges: Array; + pageInfo: PageInfo; +}; + +export type LimitData = { + __typename?: 'LimitData'; + /** Amount to debit */ + debitAmount?: Maybe; + /** Interval between payments */ + interval?: Maybe; + /** Amount to receive */ + receiveAmount?: Maybe; + /** Wallet address URL of the receiver */ + receiver?: Maybe; +}; + +export type Model = { + createdAt: Scalars['String']['output']; + id: Scalars['ID']['output']; +}; + +export type Mutation = { + __typename?: 'Mutation'; + /** Create Tenant */ + createTenant: CreateTenantMutationResponse; + /** Delete Tenant */ + deleteTenant: DeleteTenantMutationResponse; + /** Revoke Grant */ + revokeGrant: RevokeGrantMutationResponse; +}; + + +export type MutationCreateTenantArgs = { + input: CreateTenantInput; +}; + + +export type MutationDeleteTenantArgs = { + input: DeleteTenantInput; +}; + + +export type MutationRevokeGrantArgs = { + input: RevokeGrantInput; +}; + +export type PageInfo = { + __typename?: 'PageInfo'; + /** Paginating forwards: the cursor to continue. */ + endCursor?: Maybe; + /** Paginating forwards: Are there more pages? */ + hasNextPage: Scalars['Boolean']['output']; + /** Paginating backwards: Are there more pages? */ + hasPreviousPage: Scalars['Boolean']['output']; + /** Paginating backwards: the cursor to continue. */ + startCursor?: Maybe; +}; + +export type PaymentAmount = { + __typename?: 'PaymentAmount'; + /** [ISO 4217 currency code](https://en.wikipedia.org/wiki/ISO_4217), e.g. `USD` */ + assetCode: Scalars['String']['output']; + /** Difference in orders of magnitude between the standard unit of an asset and a corresponding fractional unit */ + assetScale: Scalars['UInt8']['output']; + value: Scalars['UInt64']['output']; +}; + +export type Query = { + __typename?: 'Query'; + /** Fetch a grant */ + grant: Grant; + /** Fetch a page of grants. */ + grants: GrantsConnection; +}; + + +export type QueryGrantArgs = { + id: Scalars['ID']['input']; +}; + + +export type QueryGrantsArgs = { + after?: InputMaybe; + before?: InputMaybe; + filter?: InputMaybe; + first?: InputMaybe; + last?: InputMaybe; + sortOrder?: InputMaybe; +}; + +export type RevokeGrantInput = { + grantId: Scalars['String']['input']; +}; + +export type RevokeGrantMutationResponse = { + __typename?: 'RevokeGrantMutationResponse'; + id: Scalars['ID']['output']; +}; + +export enum SortOrder { + /** Choose ascending order for results. */ + Asc = 'ASC', + /** Choose descending order for results. */ + Desc = 'DESC' +} + +export type Tenant = { + __typename?: 'Tenant'; + id: Scalars['ID']['output']; +}; + + + +export type ResolverTypeWrapper = Promise | T; + + +export type ResolverWithResolve = { + resolve: ResolverFn; +}; +export type Resolver = ResolverFn | ResolverWithResolve; + +export type ResolverFn = ( + parent: TParent, + args: TArgs, + context: TContext, + info: GraphQLResolveInfo +) => Promise | TResult; + +export type SubscriptionSubscribeFn = ( + parent: TParent, + args: TArgs, + context: TContext, + info: GraphQLResolveInfo +) => AsyncIterable | Promise>; + +export type SubscriptionResolveFn = ( + parent: TParent, + args: TArgs, + context: TContext, + info: GraphQLResolveInfo +) => TResult | Promise; + +export interface SubscriptionSubscriberObject { + subscribe: SubscriptionSubscribeFn<{ [key in TKey]: TResult }, TParent, TContext, TArgs>; + resolve?: SubscriptionResolveFn; +} + +export interface SubscriptionResolverObject { + subscribe: SubscriptionSubscribeFn; + resolve: SubscriptionResolveFn; +} + +export type SubscriptionObject = + | SubscriptionSubscriberObject + | SubscriptionResolverObject; + +export type SubscriptionResolver = + | ((...args: any[]) => SubscriptionObject) + | SubscriptionObject; + +export type TypeResolveFn = ( + parent: TParent, + context: TContext, + info: GraphQLResolveInfo +) => Maybe | Promise>; + +export type IsTypeOfResolverFn = (obj: T, context: TContext, info: GraphQLResolveInfo) => boolean | Promise; + +export type NextResolverFn = () => Promise; + +export type DirectiveResolverFn = ( + next: NextResolverFn, + parent: TParent, + args: TArgs, + context: TContext, + info: GraphQLResolveInfo +) => TResult | Promise; + + +/** Mapping of interface types */ +export type ResolversInterfaceTypes> = { + Model: ( Partial ) | ( Partial ); +}; + +/** Mapping between all available schema types and the resolvers types */ +export type ResolversTypes = { + Access: ResolverTypeWrapper>; + Boolean: ResolverTypeWrapper>; + CreateTenantInput: ResolverTypeWrapper>; + CreateTenantMutationResponse: ResolverTypeWrapper>; + DeleteTenantInput: ResolverTypeWrapper>; + DeleteTenantMutationResponse: ResolverTypeWrapper>; + FilterFinalizationReason: ResolverTypeWrapper>; + FilterGrantState: ResolverTypeWrapper>; + FilterString: ResolverTypeWrapper>; + Grant: ResolverTypeWrapper>; + GrantEdge: ResolverTypeWrapper>; + GrantFilter: ResolverTypeWrapper>; + GrantFinalization: ResolverTypeWrapper>; + GrantState: ResolverTypeWrapper>; + GrantsConnection: ResolverTypeWrapper>; + ID: ResolverTypeWrapper>; + Int: ResolverTypeWrapper>; + LimitData: ResolverTypeWrapper>; + Model: ResolverTypeWrapper['Model']>; + Mutation: ResolverTypeWrapper<{}>; + PageInfo: ResolverTypeWrapper>; + PaymentAmount: ResolverTypeWrapper>; + Query: ResolverTypeWrapper<{}>; + RevokeGrantInput: ResolverTypeWrapper>; + RevokeGrantMutationResponse: ResolverTypeWrapper>; + SortOrder: ResolverTypeWrapper>; + String: ResolverTypeWrapper>; + Tenant: ResolverTypeWrapper>; + UInt8: ResolverTypeWrapper>; + UInt64: ResolverTypeWrapper>; +}; + +/** Mapping between all available schema types and the resolvers parents */ +export type ResolversParentTypes = { + Access: Partial; + Boolean: Partial; + CreateTenantInput: Partial; + CreateTenantMutationResponse: Partial; + DeleteTenantInput: Partial; + DeleteTenantMutationResponse: Partial; + FilterFinalizationReason: Partial; + FilterGrantState: Partial; + FilterString: Partial; + Grant: Partial; + GrantEdge: Partial; + GrantFilter: Partial; + GrantsConnection: Partial; + ID: Partial; + Int: Partial; + LimitData: Partial; + Model: ResolversInterfaceTypes['Model']; + Mutation: {}; + PageInfo: Partial; + PaymentAmount: Partial; + Query: {}; + RevokeGrantInput: Partial; + RevokeGrantMutationResponse: Partial; + String: Partial; + Tenant: Partial; + UInt8: Partial; + UInt64: Partial; +}; + +export type AccessResolvers = { + actions?: Resolver>, ParentType, ContextType>; + createdAt?: Resolver; + id?: Resolver; + identifier?: Resolver, ParentType, ContextType>; + limits?: Resolver, ParentType, ContextType>; + type?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type CreateTenantMutationResponseResolvers = { + tenant?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type DeleteTenantMutationResponseResolvers = { + success?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type GrantResolvers = { + access?: Resolver, ParentType, ContextType>; + client?: Resolver; + createdAt?: Resolver; + finalizationReason?: Resolver, ParentType, ContextType>; + id?: Resolver; + state?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type GrantEdgeResolvers = { + cursor?: Resolver; + node?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type GrantsConnectionResolvers = { + edges?: Resolver, ParentType, ContextType>; + pageInfo?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type LimitDataResolvers = { + debitAmount?: Resolver, ParentType, ContextType>; + interval?: Resolver, ParentType, ContextType>; + receiveAmount?: Resolver, ParentType, ContextType>; + receiver?: Resolver, ParentType, ContextType>; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type ModelResolvers = { + __resolveType: TypeResolveFn<'Access' | 'Grant', ParentType, ContextType>; + createdAt?: Resolver; + id?: Resolver; +}; + +export type MutationResolvers = { + createTenant?: Resolver>; + deleteTenant?: Resolver>; + revokeGrant?: Resolver>; +}; + +export type PageInfoResolvers = { + endCursor?: Resolver, ParentType, ContextType>; + hasNextPage?: Resolver; + hasPreviousPage?: Resolver; + startCursor?: Resolver, ParentType, ContextType>; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type PaymentAmountResolvers = { + assetCode?: Resolver; + assetScale?: Resolver; + value?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type QueryResolvers = { + grant?: Resolver>; + grants?: Resolver>; +}; + +export type RevokeGrantMutationResponseResolvers = { + id?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type TenantResolvers = { + id?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export interface UInt8ScalarConfig extends GraphQLScalarTypeConfig { + name: 'UInt8'; +} + +export interface UInt64ScalarConfig extends GraphQLScalarTypeConfig { + name: 'UInt64'; +} + +export type Resolvers = { + Access?: AccessResolvers; + CreateTenantMutationResponse?: CreateTenantMutationResponseResolvers; + DeleteTenantMutationResponse?: DeleteTenantMutationResponseResolvers; + Grant?: GrantResolvers; + GrantEdge?: GrantEdgeResolvers; + GrantsConnection?: GrantsConnectionResolvers; + LimitData?: LimitDataResolvers; + Model?: ModelResolvers; + Mutation?: MutationResolvers; + PageInfo?: PageInfoResolvers; + PaymentAmount?: PaymentAmountResolvers; + Query?: QueryResolvers; + RevokeGrantMutationResponse?: RevokeGrantMutationResponseResolvers; + Tenant?: TenantResolvers; + UInt8?: GraphQLScalarType; + UInt64?: GraphQLScalarType; +}; + diff --git a/packages/backend/src/graphql/generated/graphql.schema.json b/packages/backend/src/graphql/generated/graphql.schema.json index a43aafe99d..447f462915 100644 --- a/packages/backend/src/graphql/generated/graphql.schema.json +++ b/packages/backend/src/graphql/generated/graphql.schema.json @@ -1252,6 +1252,22 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "tenantId", + "description": "ID of the tenant", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "walletAddressId", "description": "Id of the wallet address under which the incoming payment will be created", @@ -1587,6 +1603,22 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "tenantId", + "description": "ID of a tenant", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "walletAddressId", "description": "Id of the wallet address under which the outgoing payment will be created", @@ -1958,6 +1990,22 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "tenantId", + "description": "ID of the tenant", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "walletAddressId", "description": "Id of the wallet address under which the quote will be created", @@ -2077,6 +2125,143 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "INPUT_OBJECT", + "name": "CreateTenantEndpointsInput", + "description": null, + "fields": null, + "inputFields": [ + { + "name": "type", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "TenantEndpointType", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "value", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "CreateTenantInput", + "description": null, + "fields": null, + "inputFields": [ + { + "name": "endpoints", + "description": "List of endpoints types for the tenant", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "CreateTenantEndpointsInput", + "ofType": null + } + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "idpConsentEndpoint", + "description": "IDP Endpoint", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "idpSecret", + "description": "IDP Secret", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "CreateTenantMutationResponse", + "description": null, + "fields": [ + { + "name": "tenant", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Tenant", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, { "kind": "INPUT_OBJECT", "name": "CreateWalletAddressInput", @@ -2143,6 +2328,22 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "tenantId", + "description": "ID of a tenant", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "url", "description": "Wallet Address URL", @@ -3960,6 +4161,11 @@ "name": "Peer", "ofType": null }, + { + "kind": "OBJECT", + "name": "Tenant", + "ofType": null + }, { "kind": "OBJECT", "name": "WalletAddress", @@ -4461,6 +4667,39 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "createTenant", + "description": "Create tenant", + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "CreateTenantInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "CreateTenantMutationResponse", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "createWalletAddress", "description": "Create a wallet address", @@ -6660,8 +6899,8 @@ "deprecationReason": null }, { - "name": "walletAddress", - "description": "Fetch a wallet address.", + "name": "tenant", + "description": "Fetch a tenant", "args": [ { "name": "id", @@ -6671,7 +6910,7 @@ "name": null, "ofType": { "kind": "SCALAR", - "name": "String", + "name": "ID", "ofType": null } }, @@ -6682,15 +6921,15 @@ ], "type": { "kind": "OBJECT", - "name": "WalletAddress", + "name": "Tenant", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "walletAddresses", - "description": "Fetch a page of wallet addresses.", + "name": "tenants", + "description": "Fetch a page of tenants", "args": [ { "name": "after", @@ -6758,7 +6997,7 @@ "name": null, "ofType": { "kind": "OBJECT", - "name": "WalletAddressesConnection", + "name": "TenantsConnection", "ofType": null } }, @@ -6766,8 +7005,37 @@ "deprecationReason": null }, { - "name": "webhookEvents", - "description": "Fetch a page of webhook events", + "name": "walletAddress", + "description": "Fetch a wallet address.", + "args": [ + { + "name": "id", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "WalletAddress", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "walletAddresses", + "description": "Fetch a page of wallet addresses.", "args": [ { "name": "after", @@ -6794,9 +7062,86 @@ "deprecationReason": null }, { - "name": "filter", - "description": "Filter webhook events based on specific criteria.", - "type": { + "name": "first", + "description": "Paginating forwards: The first **n** elements from the page.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Paginating backwards: The last **n** elements from the page.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "sortOrder", + "description": "Ascending or descending order of creation.", + "type": { + "kind": "ENUM", + "name": "SortOrder", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "WalletAddressesConnection", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "webhookEvents", + "description": "Fetch a page of webhook events", + "args": [ + { + "name": "after", + "description": "Paginating forwards: the cursor before the the requested page.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Paginating backwards: the cursor after the the requested page.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "filter", + "description": "Filter webhook events based on specific criteria.", + "type": { "kind": "INPUT_OBJECT", "name": "WebhookEventFilter", "ofType": null @@ -7444,6 +7789,361 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "OBJECT", + "name": "Tenant", + "description": null, + "fields": [ + { + "name": "createdAt", + "description": "Date-time of creation", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "endpoints", + "description": "List of tenant endpoints associated with this tenant", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "TenantEndpoint", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "id", + "description": "Tenant ID that is used in subsequent resources", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "kratosIdentityId", + "description": "Kratos identity ID", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updatedAt", + "description": "Date-time of the update", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "Model", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "TenantEdge", + "description": null, + "fields": [ + { + "name": "cursor", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "node", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Tenant", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "TenantEndpoint", + "description": null, + "fields": [ + { + "name": "type", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "TenantEndpointType", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "value", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "TenantEndpointEdge", + "description": null, + "fields": [ + { + "name": "cursor", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "node", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "TenantEndpoint", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "TenantEndpointType", + "description": null, + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "RatesUrl", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "WebhookBaseUrl", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "TenantEndpointsConnection", + "description": null, + "fields": [ + { + "name": "edges", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "TenantEndpointEdge", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pageInfo", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "PageInfo", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "TenantsConnection", + "description": null, + "fields": [ + { + "name": "edges", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "TenantEdge", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pageInfo", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "PageInfo", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, { "kind": "ENUM", "name": "TransferType", diff --git a/packages/backend/src/graphql/generated/graphql.ts b/packages/backend/src/graphql/generated/graphql.ts index d5b26bbf7e..d2f1d62ed5 100644 --- a/packages/backend/src/graphql/generated/graphql.ts +++ b/packages/backend/src/graphql/generated/graphql.ts @@ -197,6 +197,8 @@ export type CreateIncomingPaymentInput = { incomingAmount?: InputMaybe; /** Additional metadata associated with the incoming payment. */ metadata?: InputMaybe; + /** ID of the tenant */ + tenantId: Scalars['ID']['input']; /** Id of the wallet address under which the incoming payment will be created */ walletAddressId: Scalars['String']['input']; }; @@ -252,6 +254,8 @@ export type CreateOutgoingPaymentInput = { metadata?: InputMaybe; /** Id of the corresponding quote for that outgoing payment */ quoteId: Scalars['String']['input']; + /** ID of a tenant */ + tenantId: Scalars['ID']['input']; /** Id of the wallet address under which the outgoing payment will be created */ walletAddressId: Scalars['String']['input']; }; @@ -311,6 +315,8 @@ export type CreateQuoteInput = { receiveAmount?: InputMaybe; /** Wallet address URL of the receiver */ receiver: Scalars['String']['input']; + /** ID of the tenant */ + tenantId: Scalars['ID']['input']; /** Id of the wallet address under which the quote will be created */ walletAddressId: Scalars['String']['input']; }; @@ -333,6 +339,25 @@ export type CreateReceiverResponse = { receiver?: Maybe; }; +export type CreateTenantEndpointsInput = { + type: TenantEndpointType; + value: Scalars['String']['input']; +}; + +export type CreateTenantInput = { + /** List of endpoints types for the tenant */ + endpoints: Array; + /** IDP Endpoint */ + idpConsentEndpoint: Scalars['String']['input']; + /** IDP Secret */ + idpSecret: Scalars['String']['input']; +}; + +export type CreateTenantMutationResponse = { + __typename?: 'CreateTenantMutationResponse'; + tenant: Tenant; +}; + export type CreateWalletAddressInput = { /** Additional properties associated with the [walletAddress]. */ additionalProperties?: InputMaybe>; @@ -342,6 +367,8 @@ export type CreateWalletAddressInput = { idempotencyKey?: InputMaybe; /** Public name associated with the wallet address */ publicName?: InputMaybe; + /** ID of a tenant */ + tenantId: Scalars['ID']['input']; /** Wallet Address URL */ url: Scalars['String']['input']; }; @@ -657,6 +684,8 @@ export type Mutation = { createQuote: QuoteResponse; /** Create an internal or external Open Payments Incoming Payment. The receiver has a wallet address on either this or another Open Payments resource server. */ createReceiver: CreateReceiverResponse; + /** Create tenant */ + createTenant: CreateTenantMutationResponse; /** Create a wallet address */ createWalletAddress: CreateWalletAddressMutationResponse; /** Add a public key to a wallet address that is used to verify Open Payments requests. */ @@ -777,6 +806,11 @@ export type MutationCreateReceiverArgs = { }; +export type MutationCreateTenantArgs = { + input: CreateTenantInput; +}; + + export type MutationCreateWalletAddressArgs = { input: CreateWalletAddressInput; }; @@ -1053,6 +1087,10 @@ export type Query = { quote?: Maybe; /** Get an local or remote Open Payments Incoming Payment. The receiver has a wallet address on either this or another Open Payments resource server. */ receiver?: Maybe; + /** Fetch a tenant */ + tenant?: Maybe; + /** Fetch a page of tenants */ + tenants: TenantsConnection; /** Fetch a wallet address. */ walletAddress?: Maybe; /** Fetch a page of wallet addresses. */ @@ -1136,6 +1174,20 @@ export type QueryReceiverArgs = { }; +export type QueryTenantArgs = { + id: Scalars['ID']['input']; +}; + + +export type QueryTenantsArgs = { + after?: InputMaybe; + before?: InputMaybe; + first?: InputMaybe; + last?: InputMaybe; + sortOrder?: InputMaybe; +}; + + export type QueryWalletAddressArgs = { id: Scalars['String']['input']; }; @@ -1253,6 +1305,55 @@ export enum SortOrder { Desc = 'DESC' } +export type Tenant = Model & { + __typename?: 'Tenant'; + /** Date-time of creation */ + createdAt: Scalars['String']['output']; + /** List of tenant endpoints associated with this tenant */ + endpoints: Array; + /** Tenant ID that is used in subsequent resources */ + id: Scalars['ID']['output']; + /** Kratos identity ID */ + kratosIdentityId: Scalars['String']['output']; + /** Date-time of the update */ + updatedAt: Scalars['String']['output']; +}; + +export type TenantEdge = { + __typename?: 'TenantEdge'; + cursor: Scalars['String']['output']; + node: Tenant; +}; + +export type TenantEndpoint = { + __typename?: 'TenantEndpoint'; + type: TenantEndpointType; + value: Scalars['String']['output']; +}; + +export type TenantEndpointEdge = { + __typename?: 'TenantEndpointEdge'; + cursor: Scalars['String']['output']; + node?: Maybe; +}; + +export enum TenantEndpointType { + RatesUrl = 'RatesUrl', + WebhookBaseUrl = 'WebhookBaseUrl' +} + +export type TenantEndpointsConnection = { + __typename?: 'TenantEndpointsConnection'; + edges: Array; + pageInfo: PageInfo; +}; + +export type TenantsConnection = { + __typename?: 'TenantsConnection'; + edges: Array; + pageInfo: PageInfo; +}; + export enum TransferType { /** Deposit transfer type. */ Deposit = 'DEPOSIT', @@ -1563,7 +1664,7 @@ export type DirectiveResolverFn> = { BasePayment: ( Partial ) | ( Partial ) | ( Partial ); - Model: ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ); + Model: ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ); }; /** Mapping between all available schema types and the resolvers types */ @@ -1601,6 +1702,9 @@ export type ResolversTypes = { CreateQuoteInput: ResolverTypeWrapper>; CreateReceiverInput: ResolverTypeWrapper>; CreateReceiverResponse: ResolverTypeWrapper>; + CreateTenantEndpointsInput: ResolverTypeWrapper>; + CreateTenantInput: ResolverTypeWrapper>; + CreateTenantMutationResponse: ResolverTypeWrapper>; CreateWalletAddressInput: ResolverTypeWrapper>; CreateWalletAddressKeyInput: ResolverTypeWrapper>; CreateWalletAddressKeyMutationResponse: ResolverTypeWrapper>; @@ -1670,6 +1774,13 @@ export type ResolversTypes = { SetFeeResponse: ResolverTypeWrapper>; SortOrder: ResolverTypeWrapper>; String: ResolverTypeWrapper>; + Tenant: ResolverTypeWrapper>; + TenantEdge: ResolverTypeWrapper>; + TenantEndpoint: ResolverTypeWrapper>; + TenantEndpointEdge: ResolverTypeWrapper>; + TenantEndpointType: ResolverTypeWrapper>; + TenantEndpointsConnection: ResolverTypeWrapper>; + TenantsConnection: ResolverTypeWrapper>; TransferType: ResolverTypeWrapper>; TriggerWalletAddressEventsInput: ResolverTypeWrapper>; TriggerWalletAddressEventsMutationResponse: ResolverTypeWrapper>; @@ -1731,6 +1842,9 @@ export type ResolversParentTypes = { CreateQuoteInput: Partial; CreateReceiverInput: Partial; CreateReceiverResponse: Partial; + CreateTenantEndpointsInput: Partial; + CreateTenantInput: Partial; + CreateTenantMutationResponse: Partial; CreateWalletAddressInput: Partial; CreateWalletAddressKeyInput: Partial; CreateWalletAddressKeyMutationResponse: Partial; @@ -1792,6 +1906,12 @@ export type ResolversParentTypes = { SetFeeInput: Partial; SetFeeResponse: Partial; String: Partial; + Tenant: Partial; + TenantEdge: Partial; + TenantEndpoint: Partial; + TenantEndpointEdge: Partial; + TenantEndpointsConnection: Partial; + TenantsConnection: Partial; TriggerWalletAddressEventsInput: Partial; TriggerWalletAddressEventsMutationResponse: Partial; UInt8: Partial; @@ -1913,6 +2033,11 @@ export type CreateReceiverResponseResolvers; }; +export type CreateTenantMutationResponseResolvers = { + tenant?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type CreateWalletAddressKeyMutationResponseResolvers = { walletAddressKey?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; @@ -2016,7 +2141,7 @@ export type LiquidityMutationResponseResolvers = { - __resolveType: TypeResolveFn<'AccountingTransfer' | 'Asset' | 'Fee' | 'IncomingPayment' | 'OutgoingPayment' | 'Payment' | 'Peer' | 'WalletAddress' | 'WalletAddressKey' | 'WebhookEvent', ParentType, ContextType>; + __resolveType: TypeResolveFn<'AccountingTransfer' | 'Asset' | 'Fee' | 'IncomingPayment' | 'OutgoingPayment' | 'Payment' | 'Peer' | 'Tenant' | 'WalletAddress' | 'WalletAddressKey' | 'WebhookEvent', ParentType, ContextType>; createdAt?: Resolver; id?: Resolver; }; @@ -2037,6 +2162,7 @@ export type MutationResolvers, ParentType, ContextType, RequireFields>; createQuote?: Resolver>; createReceiver?: Resolver>; + createTenant?: Resolver>; createWalletAddress?: Resolver>; createWalletAddressKey?: Resolver, ParentType, ContextType, RequireFields>; createWalletAddressWithdrawal?: Resolver, ParentType, ContextType, RequireFields>; @@ -2162,6 +2288,8 @@ export type QueryResolvers>; quote?: Resolver, ParentType, ContextType, RequireFields>; receiver?: Resolver, ParentType, ContextType, RequireFields>; + tenant?: Resolver, ParentType, ContextType, RequireFields>; + tenants?: Resolver>; walletAddress?: Resolver, ParentType, ContextType, RequireFields>; walletAddresses?: Resolver>; webhookEvents?: Resolver>; @@ -2219,6 +2347,45 @@ export type SetFeeResponseResolvers; }; +export type TenantResolvers = { + createdAt?: Resolver; + endpoints?: Resolver, ParentType, ContextType>; + id?: Resolver; + kratosIdentityId?: Resolver; + updatedAt?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type TenantEdgeResolvers = { + cursor?: Resolver; + node?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type TenantEndpointResolvers = { + type?: Resolver; + value?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type TenantEndpointEdgeResolvers = { + cursor?: Resolver; + node?: Resolver, ParentType, ContextType>; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type TenantEndpointsConnectionResolvers = { + edges?: Resolver, ParentType, ContextType>; + pageInfo?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type TenantsConnectionResolvers = { + edges?: Resolver, ParentType, ContextType>; + pageInfo?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type TriggerWalletAddressEventsMutationResponseResolvers = { count?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; @@ -2338,6 +2505,7 @@ export type Resolvers = { CreateOrUpdatePeerByUrlMutationResponse?: CreateOrUpdatePeerByUrlMutationResponseResolvers; CreatePeerMutationResponse?: CreatePeerMutationResponseResolvers; CreateReceiverResponse?: CreateReceiverResponseResolvers; + CreateTenantMutationResponse?: CreateTenantMutationResponseResolvers; CreateWalletAddressKeyMutationResponse?: CreateWalletAddressKeyMutationResponseResolvers; CreateWalletAddressMutationResponse?: CreateWalletAddressMutationResponseResolvers; DeleteAssetMutationResponse?: DeleteAssetMutationResponseResolvers; @@ -2375,6 +2543,12 @@ export type Resolvers = { Receiver?: ReceiverResolvers; RevokeWalletAddressKeyMutationResponse?: RevokeWalletAddressKeyMutationResponseResolvers; SetFeeResponse?: SetFeeResponseResolvers; + Tenant?: TenantResolvers; + TenantEdge?: TenantEdgeResolvers; + TenantEndpoint?: TenantEndpointResolvers; + TenantEndpointEdge?: TenantEndpointEdgeResolvers; + TenantEndpointsConnection?: TenantEndpointsConnectionResolvers; + TenantsConnection?: TenantsConnectionResolvers; TriggerWalletAddressEventsMutationResponse?: TriggerWalletAddressEventsMutationResponseResolvers; UInt8?: GraphQLScalarType; UInt64?: GraphQLScalarType; diff --git a/packages/backend/src/graphql/resolvers/incoming_payment.ts b/packages/backend/src/graphql/resolvers/incoming_payment.ts index 0931c553f2..b9d60b83e6 100644 --- a/packages/backend/src/graphql/resolvers/incoming_payment.ts +++ b/packages/backend/src/graphql/resolvers/incoming_payment.ts @@ -94,7 +94,8 @@ export const createIncomingPayment: MutationResolvers['createInco ? undefined : new Date(args.input.expiresAt), incomingAmount: args.input.incomingAmount, - metadata: args.input.metadata + metadata: args.input.metadata, + tenantId: args.input.tenantId }) if (isIncomingPaymentError(incomingPaymentOrError)) { throw new GraphQLError(errorToMessage[incomingPaymentOrError], { diff --git a/packages/backend/src/graphql/resolvers/index.ts b/packages/backend/src/graphql/resolvers/index.ts index 32d1bb2c42..e6f05f04ae 100644 --- a/packages/backend/src/graphql/resolvers/index.ts +++ b/packages/backend/src/graphql/resolvers/index.ts @@ -67,6 +67,8 @@ import { GraphQLJSONObject } from 'graphql-scalars' import { getCombinedPayments } from './combined_payments' import { createOrUpdatePeerByUrl } from './auto-peering' import { getAccountingTransfers } from './accounting_transfer' +import { createTenant, getTenant, getTenants } from './tenant' +import { getTenantEndpoints } from './tenant_endpoints' export const resolvers: Resolvers = { UInt8: GraphQLUInt8, @@ -95,7 +97,9 @@ export const resolvers: Resolvers = { webhookEvents: getWebhookEvents, payments: getCombinedPayments, accountingTransfers: getAccountingTransfers, - receiver: getReceiver + receiver: getReceiver, + tenant: getTenant, + tenants: getTenants }, WalletAddress: { liquidity: getWalletAddressLiquidity, @@ -147,6 +151,7 @@ export const resolvers: Resolvers = { depositOutgoingPaymentLiquidity, createIncomingPaymentWithdrawal, createOutgoingPaymentWithdrawal, - setFee + setFee, + createTenant } } diff --git a/packages/backend/src/graphql/resolvers/tenant.ts b/packages/backend/src/graphql/resolvers/tenant.ts new file mode 100644 index 0000000000..9d023ceda9 --- /dev/null +++ b/packages/backend/src/graphql/resolvers/tenant.ts @@ -0,0 +1,112 @@ +import { GraphQLError } from 'graphql' +import { ApolloContext } from '../../app' +import { + errorToCode, + errorToMessage, + isTenantError, + TenantError +} from '../../tenant/errors' +import { Tenant as SchemaTenant } from '../generated/graphql' +import { + MutationResolvers, + QueryResolvers, + ResolversTypes, + TenantEndpointType +} from '../generated/graphql' +import { Tenant } from '../../tenant/model' +import { Pagination, SortOrder } from '../../shared/baseModel' +import { getPageInfo } from '../../shared/pagination' +import { EndpointType, TenantEndpoint } from '../../tenant/endpoints/model' +import { tenantEndpointToGraphql } from './tenant_endpoints' + +const mapTenantEndpointTypeToModelEndpointType = { + [TenantEndpointType.RatesUrl]: EndpointType.RatesUrl, + [TenantEndpointType.WebhookBaseUrl]: EndpointType.WebhookBaseUrl +} + +export const getTenants: QueryResolvers['tenants'] = async ( + _, + args, + ctx +): Promise => { + const tenantService = await ctx.container.use('tenantService') + const { sortOrder, ...pagination } = args + const order = sortOrder === 'ASC' ? SortOrder.Asc : SortOrder.Desc + const tenants = await tenantService.getPage(pagination, order) + const pageInfo = await getPageInfo({ + getPage: (pagination: Pagination, sortOrder?: SortOrder) => + tenantService.getPage(pagination, sortOrder), + page: tenants, + sortOrder: order + }) + + return { + pageInfo, + edges: tenants.map((tenant: Tenant) => ({ + cursor: tenant.id, + node: tenantToGraphql(tenant) + })) + } +} + +export const getTenant: QueryResolvers['tenant'] = async ( + _, + args, + ctx +): Promise => { + const tenantService = await ctx.container.use('tenantService') + const tenant = await tenantService.get(args.id) + + if (!tenant) { + throw new GraphQLError(errorToMessage[TenantError.UnknownTenant], { + extensions: { + code: errorToCode[TenantError.UnknownTenant] + } + }) + } + + return tenantToGraphql(tenant) +} + +export const createTenant: MutationResolvers['createTenant'] = + async ( + _, + args, + ctx + ): Promise => { + const tenantService = await ctx.container.use('tenantService') + + const tenantOrError = await tenantService.create({ + idpConsentEndpoint: args.input.idpConsentEndpoint, + idpSecret: args.input.idpSecret, + endpoints: args.input.endpoints.map((endpoint) => { + return { + value: endpoint.value, + type: mapTenantEndpointTypeToModelEndpointType[endpoint.type] + } + }) + }) + + if (isTenantError(tenantOrError)) { + throw new GraphQLError(errorToMessage[tenantOrError], { + extensions: { + code: errorToCode[tenantOrError] + } + }) + } + + return { + tenant: tenantToGraphql(tenantOrError) + } + } + +export function tenantToGraphql(tenant: Tenant): SchemaTenant { + return { + id: tenant.id, + kratosIdentityId: tenant.kratosIdentityId, + //we should probably paginate this, but for now, that we only have like two endpoints it should be ok + endpoints: tenant.endpoints.map(tenantEndpointToGraphql), + createdAt: new Date(tenant.createdAt).toISOString(), + updatedAt: new Date(tenant.updatedAt).toISOString() + } +} diff --git a/packages/backend/src/graphql/resolvers/tenant_endpoints.ts b/packages/backend/src/graphql/resolvers/tenant_endpoints.ts new file mode 100644 index 0000000000..14197d73ce --- /dev/null +++ b/packages/backend/src/graphql/resolvers/tenant_endpoints.ts @@ -0,0 +1,63 @@ +import { ApolloContext } from '../../app' +import { Pagination, SortOrder } from '../../shared/baseModel' +import { getPageInfo } from '../../shared/pagination' +import { EndpointType, TenantEndpoint } from '../../tenant/endpoints/model' +import { + ResolversTypes, + TenantResolvers, + TenantEndpoint as SchemaTenantEndpoint, + TenantEndpointType +} from '../generated/graphql' + +export const mapTenantEndpointTypeToModelEndpointType = { + [EndpointType.RatesUrl]: TenantEndpointType.RatesUrl, + [EndpointType.WebhookBaseUrl]: TenantEndpointType.WebhookBaseUrl +} + +export const getTenantEndpoints: TenantResolvers['endpoints'] = + async ( + parent, + args, + ctx + ): Promise => { + if (!parent.id) { + throw new Error('missing tenant id') + } + const tenantEndpointService = await ctx.container.use( + 'tenantEndpointService' + ) + + const { sortOrder, ...pagination } = args + const order = sortOrder === 'ASC' ? SortOrder.Asc : SortOrder.Desc + + const tenantEndpoints = await tenantEndpointService.getPage( + parent.id, + pagination, + order + ) + + console.log('TENANT ENDPOINTS: ', tenantEndpoints) + + const pageInfo = await getPageInfo({ + getPage: (pagination_?: Pagination, sortOrder_?: SortOrder) => + tenantEndpointService.getPage(parent.id!, pagination_, sortOrder_), + page: tenantEndpoints + }) + + return { + pageInfo, + edges: tenantEndpoints.map((endpoint: TenantEndpoint) => ({ + cursor: `${endpoint.tenantId}${endpoint.type}`, + node: tenantEndpointToGraphql(endpoint) + })) + } + } + +export function tenantEndpointToGraphql( + tenantEndpoint: TenantEndpoint +): SchemaTenantEndpoint { + return { + type: mapTenantEndpointTypeToModelEndpointType[tenantEndpoint.type], + value: tenantEndpoint.value + } +} diff --git a/packages/backend/src/graphql/resolvers/wallet_address.ts b/packages/backend/src/graphql/resolvers/wallet_address.ts index 1731699fc9..b9082b4c60 100644 --- a/packages/backend/src/graphql/resolvers/wallet_address.ts +++ b/packages/backend/src/graphql/resolvers/wallet_address.ts @@ -90,7 +90,8 @@ export const createWalletAddress: MutationResolvers['createWallet assetId: args.input.assetId, additionalProperties: addProps, publicName: args.input.publicName, - url: args.input.url + url: args.input.url, + tenantId: args.input.tenantId } const walletAddressOrError = await walletAddressService.create(options) diff --git a/packages/backend/src/graphql/schema.graphql b/packages/backend/src/graphql/schema.graphql index cc7309c3d5..eb90571157 100644 --- a/packages/backend/src/graphql/schema.graphql +++ b/packages/backend/src/graphql/schema.graphql @@ -1,4 +1,21 @@ type Query { + "Fetch a tenant" + tenant(id: ID!): Tenant + + "Fetch a page of tenants" + tenants( + "Paginating forwards: the cursor before the the requested page." + after: String + "Paginating backwards: the cursor after the the requested page." + before: String + "Paginating forwards: The first **n** elements from the page." + first: Int + "Paginating backwards: The last **n** elements from the page." + last: Int + "Ascending or descending order of creation." + sortOrder: SortOrder + ): TenantsConnection! + "Fetch an asset" asset(id: String!): Asset @@ -129,6 +146,9 @@ type Mutation { "Delete an asset" deleteAsset(input: DeleteAssetInput!): DeleteAssetMutationResponse! + "Create tenant" + createTenant(input: CreateTenantInput!): CreateTenantMutationResponse! + "Deposit asset liquidity" depositAssetLiquidity( input: DepositAssetLiquidityInput! @@ -283,6 +303,26 @@ type PageInfo { startCursor: String } +type TenantEndpointsConnection { + pageInfo: PageInfo! + edges: [TenantEndpointEdge!]! +} + +type TenantEndpointEdge { + node: TenantEndpoint + cursor: String! +} + +type TenantsConnection { + pageInfo: PageInfo! + edges: [TenantEdge!]! +} + +type TenantEdge { + node: Tenant! + cursor: String! +} + type AssetsConnection { pageInfo: PageInfo! edges: [AssetEdge!]! @@ -590,6 +630,11 @@ type Asset implements Model { createdAt: String! } +enum TenantEndpointType { + WebhookBaseUrl + RatesUrl +} + enum SortOrder { "Choose ascending order for results." ASC @@ -1026,6 +1071,8 @@ input CreateQuoteInput { receiver: String! "Unique key to ensure duplicate or retried requests are processed only once. See [idempotence](https://en.wikipedia.org/wiki/Idempotence)" idempotencyKey: String + "ID of the tenant" + tenantId: ID! } type QuoteResponse { @@ -1041,6 +1088,8 @@ input CreateOutgoingPaymentInput { metadata: JSONObject "Unique key to ensure duplicate or retried requests are processed only once. See [idempotence](https://en.wikipedia.org/wiki/Idempotence)" idempotencyKey: String + "ID of a tenant" + tenantId: ID! } input CancelOutgoingPaymentInput { @@ -1074,6 +1123,8 @@ input CreateIncomingPaymentInput { incomingAmount: AmountInput "Unique key to ensure duplicate or retried requests are processed only once. See [idempotence](https://en.wikipedia.org/wiki/Idempotence)" idempotencyKey: String + "ID of the tenant" + tenantId: ID! } input CreateReceiverInput { @@ -1101,6 +1152,22 @@ type CreateReceiverResponse { receiver: Receiver } +input CreateTenantEndpointsInput { + type: TenantEndpointType! + value: String! +} + +input CreateTenantInput { + "List of endpoints types for the tenant" + endpoints: [CreateTenantEndpointsInput!]! + + "IDP Endpoint" + idpConsentEndpoint: String! + + "IDP Secret" + idpSecret: String! +} + input CreateWalletAddressInput { "Asset of the wallet address" assetId: String! @@ -1112,6 +1179,8 @@ input CreateWalletAddressInput { idempotencyKey: String "Additional properties associated with the [walletAddress]." additionalProperties: [AdditionalPropertyInput!] + "ID of a tenant" + tenantId: ID! } input AdditionalPropertyInput { @@ -1308,6 +1377,28 @@ type DeletePeerMutationResponse { success: Boolean! } +type CreateTenantMutationResponse { + tenant: Tenant! +} + +type TenantEndpoint { + type: TenantEndpointType! + value: String! +} + +type Tenant implements Model { + "Tenant ID that is used in subsequent resources" + id: ID! + "Kratos identity ID" + kratosIdentityId: String! + "Date-time of creation" + createdAt: String! + "Date-time of the update" + updatedAt: String! + "List of tenant endpoints associated with this tenant" + endpoints: [TenantEndpoint!]! +} + type LiquidityMutationResponse { success: Boolean! } diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index a34d784ef7..a855dc30cf 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -7,6 +7,8 @@ import { createClient } from 'tigerbeetle-node' import { createClient as createIntrospectionClient } from 'token-introspection' import net from 'net' import dns from 'dns' +import { createHmac } from 'crypto' +import { print } from 'graphql/language/printer' import { createAuthenticatedClient as createOpenPaymentsClient, @@ -56,6 +58,17 @@ import { createStreamCredentialsService } from './payment-method/ilp/stream-cred import { createRatesService } from './rates/service' import { TelemetryService, createTelemetryService } from './telemetry/service' import { createWebhookService } from './webhook/service' +import { createTenantService } from './tenant/service' +import { + ApolloClient, + ApolloLink, + createHttpLink, + InMemoryCache +} from '@apollo/client' +import { onError } from '@apollo/client/link/error' +import { setContext } from '@apollo/client/link/context' +import { canonicalize } from 'json-canonicalize' +import { createTenantEndpointService } from './tenant/endpoints/service' BigInt.prototype.toJSON = function () { return this.toString() @@ -440,6 +453,106 @@ export function initIocContainer( return createIlpPaymentService(serviceDependencies) }) + container.singleton('apolloClient', async (deps) => { + const [logger, config] = await Promise.all([ + deps.use('logger'), + deps.use('config') + ]) + + const httpLink = createHttpLink({ + uri: config.authAdminApiUrl + }) + + const errorLink = onError(({ graphQLErrors }) => { + if (graphQLErrors) { + logger.error(graphQLErrors) + graphQLErrors.map(({ extensions }) => { + if (extensions && extensions.code === 'UNAUTHENTICATED') { + logger.error('UNAUTHENTICATED') + } + + if (extensions && extensions.code === 'FORBIDDEN') { + logger.error('FORBIDDEN') + } + }) + } + }) + + const authLink = setContext((request, { headers }) => { + if (!config.authAdminApiSecret || !config.authAdminApiSignatureVersion) + return { headers } + const timestamp = Math.round(new Date().getTime() / 1000) + const version = config.authAdminApiSignatureVersion + + const { query, variables, operationName } = request + const formattedRequest = { + variables, + operationName, + query: print(query) + } + + const payload = `${timestamp}.${canonicalize(formattedRequest)}` + const hmac = createHmac('sha256', config.authAdminApiSecret) + hmac.update(payload) + const digest = hmac.digest('hex') + + return { + headers: { + ...headers, + signature: `t=${timestamp}, v${version}=${digest}` + } + } + }) + + const link = ApolloLink.from([errorLink, authLink, httpLink]) + + const client = new ApolloClient({ + cache: new InMemoryCache({}), + link: link, + defaultOptions: { + query: { + fetchPolicy: 'no-cache' + }, + mutate: { + fetchPolicy: 'no-cache' + }, + watchQuery: { + fetchPolicy: 'no-cache' + } + } + }) + + return client + }) + + container.singleton('tenantEndpointService', async (deps) => { + const [logger, knex] = await Promise.all([ + deps.use('logger'), + deps.use('knex') + ]) + + return createTenantEndpointService({ knex, logger }) + }) + + container.singleton('tenantService', async (deps) => { + const [logger, knex, config, apolloClient, tenantEndpointService] = + await Promise.all([ + deps.use('logger'), + deps.use('knex'), + deps.use('config'), + deps.use('apolloClient'), + deps.use('tenantEndpointService') + ]) + + return createTenantService({ + logger, + knex, + config, + apolloClient, + tenantEndpointService + }) + }) + container.singleton('paymentMethodHandlerService', async (deps) => { return createPaymentMethodHandlerService({ logger: await deps.use('logger'), diff --git a/packages/backend/src/open_payments/grant/model.ts b/packages/backend/src/open_payments/grant/model.ts index ed2c77daa7..ce73fbc385 100644 --- a/packages/backend/src/open_payments/grant/model.ts +++ b/packages/backend/src/open_payments/grant/model.ts @@ -32,6 +32,7 @@ export class Grant extends BaseModel { public managementId!: string public accessType!: AccessType public accessActions!: AccessAction[] + public tenantId!: string public expiresAt?: Date | null public deletedAt?: Date diff --git a/packages/backend/src/open_payments/grant/service.ts b/packages/backend/src/open_payments/grant/service.ts index a57f259763..0ea8b10c34 100644 --- a/packages/backend/src/open_payments/grant/service.ts +++ b/packages/backend/src/open_payments/grant/service.ts @@ -40,6 +40,7 @@ interface GrantOptions { authServer: string accessType: AccessType accessActions: AccessAction[] + tenantId: string } interface UpdateOptions { @@ -98,6 +99,7 @@ export async function getExistingGrant( // all options.accessActions are a subset of saved accessActions // e.g. if [ReadAll, Create] is saved, requesting just [Create] would still match .andWhere('accessActions', '@>', options.accessActions) + .andWhere('tenantId', options.tenantId) .withGraphJoined('authServer') } @@ -148,6 +150,7 @@ async function requestNewGrant( accessToken: openPaymentsGrant.access_token.value, managementId: retrieveManagementId(openPaymentsGrant.access_token.manage), authServerId, + tenantId: options.tenantId, expiresAt: openPaymentsGrant.access_token.expires_in ? new Date( Date.now() + openPaymentsGrant.access_token.expires_in * 1000 diff --git a/packages/backend/src/open_payments/payment/incoming/model.ts b/packages/backend/src/open_payments/payment/incoming/model.ts index b22f086af7..71913b1df4 100644 --- a/packages/backend/src/open_payments/payment/incoming/model.ts +++ b/packages/backend/src/open_payments/payment/incoming/model.ts @@ -108,6 +108,8 @@ export class IncomingPayment public readonly assetId!: string public asset!: Asset + public tenantId!: string + private incomingAmountValue?: bigint | null private receivedAmountValue?: bigint diff --git a/packages/backend/src/open_payments/payment/incoming/service.ts b/packages/backend/src/open_payments/payment/incoming/service.ts index 6d7bd48e0a..b71417aa78 100644 --- a/packages/backend/src/open_payments/payment/incoming/service.ts +++ b/packages/backend/src/open_payments/payment/incoming/service.ts @@ -31,6 +31,7 @@ export interface CreateIncomingPaymentOptions { expiresAt?: Date incomingAmount?: Amount metadata?: Record + tenantId: string } export interface IncomingPaymentService @@ -91,7 +92,8 @@ async function createIncomingPayment( client, expiresAt, incomingAmount, - metadata + metadata, + tenantId }: CreateIncomingPaymentOptions, trx?: Knex.Transaction ): Promise { @@ -131,7 +133,8 @@ async function createIncomingPayment( incomingAmount, metadata, state: IncomingPaymentState.Pending, - processAt: expiresAt + processAt: expiresAt, + tenantId }) .withGraphFetched('[asset, walletAddress]') diff --git a/packages/backend/src/open_payments/payment/outgoing/model.ts b/packages/backend/src/open_payments/payment/outgoing/model.ts index 130e9391b6..7fe651d68b 100644 --- a/packages/backend/src/open_payments/payment/outgoing/model.ts +++ b/packages/backend/src/open_payments/payment/outgoing/model.ts @@ -50,6 +50,8 @@ export class OutgoingPayment public grantId?: string + public tenantId!: string + public get receiver(): string { return this.quote.receiver } diff --git a/packages/backend/src/open_payments/payment/outgoing/service.ts b/packages/backend/src/open_payments/payment/outgoing/service.ts index 050700aca1..507ef0d857 100644 --- a/packages/backend/src/open_payments/payment/outgoing/service.ts +++ b/packages/backend/src/open_payments/payment/outgoing/service.ts @@ -172,13 +172,14 @@ export type CancelOutgoingPaymentOptions = { reason?: string } -export type CreateOutgoingPaymentOptions = +export type CreateOutgoingPaymentOptions = { tenantId: string } & ( | CreateFromQuote | CreateFromIncomingPayment +) export function isCreateFromIncomingPayment( options: CreateOutgoingPaymentOptions -): options is CreateFromIncomingPayment { +): options is CreateFromIncomingPayment & { tenantId: string } { return 'incomingPayment' in options && 'debitAmount' in options } @@ -224,7 +225,8 @@ async function createOutgoingPayment( receiver: incomingPayment, debitAmount, method: 'ilp', - walletAddressId + walletAddressId, + tenantId: options.tenantId }) if (isQuoteError(quoteOrError)) { @@ -261,7 +263,8 @@ async function createOutgoingPayment( client: options.client, metadata: options.metadata, state: OutgoingPaymentState.Funding, - grantId + grantId, + tenantId: options.tenantId }) .withGraphFetched('[quote.asset, walletAddress]') diff --git a/packages/backend/src/open_payments/payment/outgoing/worker.ts b/packages/backend/src/open_payments/payment/outgoing/worker.ts index 4c9e35cf60..0df698f960 100644 --- a/packages/backend/src/open_payments/payment/outgoing/worker.ts +++ b/packages/backend/src/open_payments/payment/outgoing/worker.ts @@ -5,6 +5,7 @@ import { OutgoingPayment, OutgoingPaymentState } from './model' import { LifecycleError, PaymentError } from './errors' import * as lifecycle from './lifecycle' import { PaymentMethodHandlerError } from '../../../payment-method/handler/errors' +import { trace, Span } from '@opentelemetry/api' // First retry waits 10 seconds, second retry waits 20 (more) seconds, etc. export const RETRY_BACKOFF_SECONDS = 10 @@ -15,23 +16,33 @@ const MAX_STATE_ATTEMPTS = 5 export async function processPendingPayment( deps_: ServiceDependencies ): Promise { - return deps_.knex.transaction(async (trx) => { - const payment = await getPendingPayment(trx) - if (!payment) return - - await handlePaymentLifecycle( - { - ...deps_, - knex: trx, - logger: deps_.logger.child({ - payment: payment.id, - from_state: payment.state - }) - }, - payment - ) - return payment.id - }) + const tracer = trace.getTracer('outgoing_payment_worker') + + return tracer.startActiveSpan( + 'outgoingPaymentLifecycle', + async (span: Span) => { + const paymentId = await deps_.knex.transaction(async (trx) => { + const payment = await getPendingPayment(trx) + if (!payment) return + + await handlePaymentLifecycle( + { + ...deps_, + knex: trx, + logger: deps_.logger.child({ + payment: payment.id, + from_state: payment.state + }) + }, + payment + ) + return payment.id + }) + + span.end() + return paymentId + } + ) } // Fetch (and lock) a payment for work. diff --git a/packages/backend/src/open_payments/quote/model.ts b/packages/backend/src/open_payments/quote/model.ts index 39631f7240..941b69d296 100644 --- a/packages/backend/src/open_payments/quote/model.ts +++ b/packages/backend/src/open_payments/quote/model.ts @@ -33,6 +33,8 @@ export class Quote extends WalletAddressSubresource { public feeId?: string public fee?: Fee + public tenantId!: string + static get relationMappings() { return { ...super.relationMappings, diff --git a/packages/backend/src/open_payments/quote/service.ts b/packages/backend/src/open_payments/quote/service.ts index 04d9df7f87..8fbe06d407 100644 --- a/packages/backend/src/open_payments/quote/service.ts +++ b/packages/backend/src/open_payments/quote/service.ts @@ -76,9 +76,10 @@ interface QuoteOptionsWithReceiveAmount extends QuoteOptionsBase { debitAmount?: never } -export type CreateQuoteOptions = +export type CreateQuoteOptions = { tenantId: string } & ( | QuoteOptionsWithDebitAmount | QuoteOptionsWithReceiveAmount +) async function createQuote( deps: ServiceDependencies, @@ -145,7 +146,8 @@ async function createQuote( expiresAt: new Date(0), // expiresAt is patched in finalizeQuote client: options.client, feeId: sendingFee?.id, - estimatedExchangeRate: quote.estimatedExchangeRate + estimatedExchangeRate: quote.estimatedExchangeRate, + tenantId: options.tenantId }) .withGraphFetched('[asset, fee, walletAddress]') diff --git a/packages/backend/src/open_payments/wallet_address/model.ts b/packages/backend/src/open_payments/wallet_address/model.ts index 81dd603a1d..dbd3141134 100644 --- a/packages/backend/src/open_payments/wallet_address/model.ts +++ b/packages/backend/src/open_payments/wallet_address/model.ts @@ -61,6 +61,8 @@ export class WalletAddress public processAt!: Date | null public deactivatedAt!: Date | null + public tenantId!: string + public get isActive() { return !this.deactivatedAt || this.deactivatedAt > new Date() } diff --git a/packages/backend/src/open_payments/wallet_address/service.ts b/packages/backend/src/open_payments/wallet_address/service.ts index 3a62c5e865..db072807ba 100644 --- a/packages/backend/src/open_payments/wallet_address/service.ts +++ b/packages/backend/src/open_payments/wallet_address/service.ts @@ -39,6 +39,7 @@ export interface CreateOptions extends Options { url: string assetId: string additionalProperties?: WalletAddressAdditionalPropertyInput[] + tenantId: string } type Status = 'ACTIVE' | 'INACTIVE' @@ -169,7 +170,8 @@ async function createWalletAddress( url: options.url, publicName: options.publicName, assetId: options.assetId, - additionalProperties: additionalProperties + additionalProperties: additionalProperties, + tenantId: options.tenantId }) .withGraphFetched('asset') } catch (err) { diff --git a/packages/backend/src/rates/service.test.ts b/packages/backend/src/rates/service.test.ts index 5778a54fc6..8145a3cd56 100644 --- a/packages/backend/src/rates/service.test.ts +++ b/packages/backend/src/rates/service.test.ts @@ -6,6 +6,7 @@ import { initIocContainer } from '../' import { AppServices } from '../app' import { CacheDataStore } from '../middleware/cache/data-stores' import { mockRatesApi } from '../tests/rates' +import { AxiosInstance } from 'axios' const nock = (global as unknown as { nock: typeof import('nock') }).nock @@ -214,23 +215,27 @@ describe('Rates service', function () { expect(apiRequestCount).toBe(2) }) - it('prefetches when the cached request is old', async () => { + it('returns new rates after cache expires', async () => { await expect(service.rates('USD')).resolves.toEqual(usdRates) - jest.advanceTimersByTime(exchangeRatesLifetime * 0.5 + 1) - // ... cache isn't expired yet, but it will be soon - await expect(service.rates('USD')).resolves.toEqual(usdRates) - expect(apiRequestCount).toBe(1) - - // Invalidate the cache. - jest.advanceTimersByTime(exchangeRatesLifetime * 0.5 + 1) + jest.advanceTimersByTime(exchangeRatesLifetime + 1) await expect(service.rates('USD')).resolves.toEqual(usdRates) - // The prefetch response is promoted to the cache. expect(apiRequestCount).toBe(2) }) - it('cannot use an expired cache', async () => { - await expect(service.rates('USD')).resolves.toEqual(usdRates) - jest.advanceTimersByTime(exchangeRatesLifetime + 1) + it('returns rates on second request (first one was error)', async () => { + jest + .spyOn( + (service as RatesService & { axios: AxiosInstance }).axios, + 'get' + ) + .mockImplementationOnce(() => { + apiRequestCount++ + throw new Error() + }) + + await expect(service.rates('USD')).rejects.toThrow( + 'Could not fetch rates' + ) await expect(service.rates('USD')).resolves.toEqual(usdRates) expect(apiRequestCount).toBe(2) }) diff --git a/packages/backend/src/rates/service.ts b/packages/backend/src/rates/service.ts index 5a6ed7322e..f4cae1f819 100644 --- a/packages/backend/src/rates/service.ts +++ b/packages/backend/src/rates/service.ts @@ -1,5 +1,5 @@ import { BaseService } from '../shared/baseService' -import Axios, { AxiosInstance } from 'axios' +import Axios, { AxiosInstance, isAxiosError } from 'axios' import { convert, ConvertOptions } from './util' import { createInMemoryDataStore } from '../middleware/cache/data-stores/in-memory' import { CacheDataStore } from '../middleware/cache/data-stores' @@ -74,29 +74,13 @@ class RatesServiceImpl implements RatesService { } private async getRates(baseAssetCode: string): Promise { - const [cachedRate, cachedExpiry] = await Promise.all([ - this.cachedRates.get(baseAssetCode), - this.cachedRates.getKeyExpiry(baseAssetCode) - ]) - - if (cachedRate && cachedExpiry) { - const isCloseToExpiry = - cachedExpiry.getTime() < - Date.now() + 0.5 * this.deps.exchangeRatesLifetime - - if (isCloseToExpiry) { - this.fetchNewRatesAndCache(baseAssetCode) // don't await, just get new rates for later - } + const cachedRate = await this.cachedRates.get(baseAssetCode) + if (cachedRate) { return JSON.parse(cachedRate) } - try { - return await this.fetchNewRatesAndCache(baseAssetCode) - } catch (err) { - this.cachedRates.delete(baseAssetCode) - throw err - } + return await this.fetchNewRatesAndCache(baseAssetCode) } private async fetchNewRatesAndCache(baseAssetCode: string): Promise { @@ -106,12 +90,32 @@ class RatesServiceImpl implements RatesService { this.inProgressRequests[baseAssetCode] = this.fetchNewRates(baseAssetCode) } - const rates = await this.inProgressRequests[baseAssetCode] + try { + const rates = await this.inProgressRequests[baseAssetCode] - delete this.inProgressRequests[baseAssetCode] + await this.cachedRates.set(baseAssetCode, JSON.stringify(rates)) + return rates + } catch (err) { + const errorMessage = 'Could not fetch rates' + + this.deps.logger.error( + { + ...(isAxiosError(err) + ? { + errorMessage: err.message, + errorCode: err.code, + errorStatus: err.status + } + : { err }), + url: this.deps.exchangeRatesUrl + }, + errorMessage + ) - await this.cachedRates.set(baseAssetCode, JSON.stringify(rates)) - return rates + throw new Error(errorMessage) + } finally { + delete this.inProgressRequests[baseAssetCode] + } } private async fetchNewRates(baseAssetCode: string): Promise { @@ -120,12 +124,9 @@ class RatesServiceImpl implements RatesService { return { base: baseAssetCode, rates: {} } } - const res = await this.axios - .get(url, { params: { base: baseAssetCode } }) - .catch((err) => { - this.deps.logger.warn({ err: err.message }, 'price request error') - throw err - }) + const res = await this.axios.get(url, { + params: { base: baseAssetCode } + }) const { base, rates } = res.data this.checkBaseAsset(base) diff --git a/packages/backend/src/shared/baseModel.ts b/packages/backend/src/shared/baseModel.ts index 94e9c383b7..015ce323bb 100644 --- a/packages/backend/src/shared/baseModel.ts +++ b/packages/backend/src/shared/baseModel.ts @@ -115,26 +115,19 @@ export class PaginationModel extends DbErrors(Model) { static QueryBuilder = PaginationQueryBuilder } -export abstract class BaseModel extends PaginationModel { +export abstract class WeakModel extends PaginationModel { public static get modelPaths(): string[] { return [__dirname] } - public id!: string public createdAt!: Date public updatedAt!: Date - public $beforeInsert(context: QueryContext): void { - super.$beforeInsert(context) - this.id = this.id || uuid() - } - public $beforeUpdate(_opts: ModelOptions, _queryContext: QueryContext): void { this.updatedAt = new Date() } $formatJson(json: Pojo): Pojo { - json = super.$formatJson(json) return { ...json, createdAt: json.createdAt.toISOString(), @@ -142,3 +135,12 @@ export abstract class BaseModel extends PaginationModel { } } } + +export abstract class BaseModel extends WeakModel { + public id!: string + + public $beforeInsert(context: QueryContext): void { + super.$beforeInsert(context) + this.id = this.id || uuid() + } +} diff --git a/packages/backend/src/shared/utils.ts b/packages/backend/src/shared/utils.ts index 3f34098523..172af2ab77 100644 --- a/packages/backend/src/shared/utils.ts +++ b/packages/backend/src/shared/utils.ts @@ -147,6 +147,21 @@ function verifyApiSignatureDigest( return digest === signatureDigest } +export function generateApiSignature( + secret: string, + version: number, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + body: any +): string { + const timestamp = Math.round(new Date().getTime() / 1000) + const payload = `${timestamp}.${canonicalize(body)}` + const hmac = createHmac('sha256', secret) + hmac.update(payload) + const digest = hmac.digest('hex') + + return `t=${timestamp}, v${version}=${digest}` +} + async function canApiSignatureBeProcessed( signature: string, ctx: AppContext, diff --git a/packages/backend/src/tenant/endpoints/errors.ts b/packages/backend/src/tenant/endpoints/errors.ts new file mode 100644 index 0000000000..92aab4e4dc --- /dev/null +++ b/packages/backend/src/tenant/endpoints/errors.ts @@ -0,0 +1,21 @@ +import { GraphQLErrorCode } from '../../graphql/errors' + +export enum TenantEndpointError { + UnknownError = 'UnknownError' +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const isTenantError = (t: any): t is TenantEndpointError => + Object.values(TenantEndpointError).includes(t) + +export const errorToCode: { + [key in TenantEndpointError]: GraphQLErrorCode +} = { + [TenantEndpointError.UnknownError]: GraphQLErrorCode.InternalServerError +} + +export const errorToMessage: { + [key in TenantEndpointError]: string +} = { + [TenantEndpointError.UnknownError]: 'Unknown error' +} diff --git a/packages/backend/src/tenant/endpoints/model.ts b/packages/backend/src/tenant/endpoints/model.ts new file mode 100644 index 0000000000..53860b770c --- /dev/null +++ b/packages/backend/src/tenant/endpoints/model.ts @@ -0,0 +1,16 @@ +import { WeakModel } from '../../shared/baseModel' + +export enum EndpointType { + WebhookBaseUrl = 'WebhookBaseUrl', + RatesUrl = 'RatesUrl' +} + +export class TenantEndpoint extends WeakModel { + public static get tableName(): string { + return 'tenantEndpoints' + } + + public type!: EndpointType + public value!: string + public tenantId!: string +} diff --git a/packages/backend/src/tenant/endpoints/service.ts b/packages/backend/src/tenant/endpoints/service.ts new file mode 100644 index 0000000000..83c314e761 --- /dev/null +++ b/packages/backend/src/tenant/endpoints/service.ts @@ -0,0 +1,91 @@ +import { TransactionOrKnex } from 'objection' +import { BaseService } from '../../shared/baseService' +import { TenantEndpointError } from './errors' +import { EndpointType, TenantEndpoint } from './model' +import { Pagination, SortOrder } from '../../shared/baseModel' + +export interface EndpointOptions { + value: string + type: EndpointType +} + +export interface CreateOptions { + endpoints: EndpointOptions[] + tenantId: string + trx?: TransactionOrKnex +} + +export interface TenantEndpointService { + create( + createOptions: CreateOptions + ): Promise + getForTenant(tenantId: string): Promise + getPage( + pagination?: Pagination, + sortOrder?: SortOrder + ): Promise +} + +export interface ServiceDependencies extends BaseService { + knex: TransactionOrKnex +} + +export async function createTenantEndpointService( + deps_: ServiceDependencies +): Promise { + const deps: ServiceDependencies = { + logger: deps_.logger.child({ + service: 'TenantEndpointService' + }), + knex: deps_.knex + } + + return { + create: (createOptions: CreateOptions) => { + if (!createOptions.trx) { + createOptions.trx = deps.knex + } + return createTenantEndpoint(deps, createOptions) + }, + getForTenant: (tenantId: string) => getEndpointsForTenant(deps, tenantId), + getPage: (pagination?, sortOrder?) => + getTenantEndpointsPage(deps, pagination, sortOrder) + } +} + +async function getTenantEndpointsPage( + deps: ServiceDependencies, + pagination?: Pagination, + sortOrder?: SortOrder +) { + console.log('GET TENANT ENDPOINTS PAGE') + const data = await TenantEndpoint.query(deps.knex) + .returning(['type', 'value', 'createdAt', 'updatedAt']) + .getPage(pagination, sortOrder) + console.log('DATA: ', data) + return data +} + +async function createTenantEndpoint( + deps: ServiceDependencies, + createOptions: CreateOptions +): Promise { + const tenantEndpointsData = createOptions.endpoints.map((endpoint) => ({ + type: endpoint.type, + value: endpoint.value, + tenantId: createOptions.tenantId + })) + + return await TenantEndpoint.query(createOptions.trx) + .insert(tenantEndpointsData) + .returning(['type', 'value', 'createdAt', 'updatedAt']) +} + +async function getEndpointsForTenant( + deps: ServiceDependencies, + tenantId: string +): Promise { + return TenantEndpoint.query(deps.knex) + .where('tenantId', tenantId) + .returning(['type', 'value', 'createdAt', 'updatedAt']) +} diff --git a/packages/backend/src/tenant/errors.ts b/packages/backend/src/tenant/errors.ts new file mode 100644 index 0000000000..8d06abd6e2 --- /dev/null +++ b/packages/backend/src/tenant/errors.ts @@ -0,0 +1,24 @@ +import { GraphQLErrorCode } from '../graphql/errors' + +export enum TenantError { + UnknownTenant = 'UnknownTenant', + UnknownError = 'UnknownError' +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const isTenantError = (t: any): t is TenantError => + Object.values(TenantError).includes(t) + +export const errorToCode: { + [key in TenantError]: GraphQLErrorCode +} = { + [TenantError.UnknownTenant]: GraphQLErrorCode.NotFound, + [TenantError.UnknownError]: GraphQLErrorCode.InternalServerError +} + +export const errorToMessage: { + [key in TenantError]: string +} = { + [TenantError.UnknownError]: 'Unknown error', + [TenantError.UnknownTenant]: 'Unknown tenant' +} diff --git a/packages/backend/src/tenant/model.ts b/packages/backend/src/tenant/model.ts new file mode 100644 index 0000000000..6f86ec70f9 --- /dev/null +++ b/packages/backend/src/tenant/model.ts @@ -0,0 +1,26 @@ +import { Model } from 'objection' +import { BaseModel, WeakModel } from '../shared/baseModel' +import { TenantEndpoint } from './endpoints/model' + +export class Tenant extends BaseModel { + public static get tableName(): string { + return 'tenants' + } + + public static get relationMappings() { + return { + endpoints: { + relation: Model.HasManyRelation, + modelClass: TenantEndpoint, + join: { + from: 'tenants.id', + to: 'tenantEndpoints.tenantId' + } + } + } + } + + public kratosIdentityId!: string + public deletedAt?: Date + public endpoints!: TenantEndpoint[] +} diff --git a/packages/backend/src/tenant/service.test.ts b/packages/backend/src/tenant/service.test.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/backend/src/tenant/service.ts b/packages/backend/src/tenant/service.ts new file mode 100644 index 0000000000..24e9f1bfb8 --- /dev/null +++ b/packages/backend/src/tenant/service.ts @@ -0,0 +1,131 @@ +import { TransactionOrKnex } from 'objection' +import { BaseService } from '../shared/baseService' +import { TenantError } from './errors' +import { Tenant } from './model' +import { IAppConfig } from '../config/app' +import { ApolloClient, gql, NormalizedCacheObject } from '@apollo/client' +import { + Tenant as AuthTenant, + CreateTenantInput as CreateAuthTenantInput +} from '../generated/graphql' +import { v4 as uuidv4 } from 'uuid' +import { Pagination, SortOrder } from '../shared/baseModel' +import { EndpointOptions, TenantEndpointService } from './endpoints/service' +import { TenantEndpoint } from './endpoints/model' + +export interface CreateTenantOptions { + idpConsentEndpoint: string + idpSecret: string + endpoints: EndpointOptions[] +} + +export interface TenantService { + get(id: string): Promise + getPage(pagination?: Pagination, sortOrder?: SortOrder): Promise + create(createOptions: CreateTenantOptions): Promise +} + +export interface ServiceDependencies extends BaseService { + knex: TransactionOrKnex + config: IAppConfig + apolloClient: ApolloClient + tenantEndpointService: TenantEndpointService +} + +export async function createTenantService( + deps_: ServiceDependencies +): Promise { + const deps: ServiceDependencies = { + logger: deps_.logger.child({ + service: 'TenantService' + }), + knex: deps_.knex, + config: deps_.config, + apolloClient: deps_.apolloClient, + tenantEndpointService: deps_.tenantEndpointService + } + + return { + get: (id: string) => getTenant(deps, id), + getPage: (pagination?, sortOrder?) => + getTenantsPage(deps, pagination, sortOrder), + create: (options: CreateTenantOptions) => createTenant(deps, options) + } +} + +async function getTenantsPage( + deps: ServiceDependencies, + pagination?: Pagination, + sortOrder?: SortOrder +): Promise { + return await Tenant.query(deps.knex) + .getPage(pagination, sortOrder) +} + +async function getTenant( + deps: ServiceDependencies, + id: string +): Promise { + return Tenant.query(deps.knex) + .withGraphFetched('endpoints') + .findById(id) +} + +async function createTenant( + deps: ServiceDependencies, + options: CreateTenantOptions +): Promise { + /** + * 1. Open DB transaction + * 2. Insert tenant data into DB + * 3. Call Auth Admin API to create tenant + * 3.1 if success, commit DB trx and return tenant data + * 3.2 if error, rollback DB trx and return error + */ + return deps.knex.transaction(async (trx) => { + let tenant: Tenant + try { + // create tenant on backend + tenant = await Tenant.query(trx).insert({ + kratosIdentityId: uuidv4() + }) + + await deps.tenantEndpointService.create({ + endpoints: options.endpoints, + tenantId: tenant.id, + trx + }) + + // call auth admin api + const mutation = gql` + mutation CreateAuthTenant($input: CreateTenantInput!) { + createTenant(input: $input) { + tenant { + id + } + } + } + ` + const variables = { + input: { + tenantId: tenant.id, + idpSecret: options.idpSecret, + idpConsentEndpoint: options.idpConsentEndpoint + } + } + + await deps.apolloClient.mutate< + AuthTenant, + { input: CreateAuthTenantInput } + >({ + mutation, + variables + }) + } catch (err) { + await trx.rollback() + throw err + } + + return tenant + }) +} diff --git a/packages/backend/src/tests/apiSignature.ts b/packages/backend/src/tests/apiSignature.ts deleted file mode 100644 index 9d80e00056..0000000000 --- a/packages/backend/src/tests/apiSignature.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { createHmac } from 'crypto' -import { canonicalize } from 'json-canonicalize' - -export function generateApiSignature( - secret: string, - version: number, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - body: any -): string { - const timestamp = Math.round(new Date().getTime() / 1000) - const payload = `${timestamp}.${canonicalize(body)}` - const hmac = createHmac('sha256', secret) - hmac.update(payload) - const digest = hmac.digest('hex') - - return `t=${timestamp}, v${version}=${digest}` -} diff --git a/packages/frontend/app/generated/graphql.ts b/packages/frontend/app/generated/graphql.ts index bc7e5fabb1..eaec30b692 100644 --- a/packages/frontend/app/generated/graphql.ts +++ b/packages/frontend/app/generated/graphql.ts @@ -197,6 +197,8 @@ export type CreateIncomingPaymentInput = { incomingAmount?: InputMaybe; /** Additional metadata associated with the incoming payment. */ metadata?: InputMaybe; + /** ID of the tenant */ + tenantId: Scalars['ID']['input']; /** Id of the wallet address under which the incoming payment will be created */ walletAddressId: Scalars['String']['input']; }; @@ -252,6 +254,8 @@ export type CreateOutgoingPaymentInput = { metadata?: InputMaybe; /** Id of the corresponding quote for that outgoing payment */ quoteId: Scalars['String']['input']; + /** ID of a tenant */ + tenantId: Scalars['ID']['input']; /** Id of the wallet address under which the outgoing payment will be created */ walletAddressId: Scalars['String']['input']; }; @@ -311,6 +315,8 @@ export type CreateQuoteInput = { receiveAmount?: InputMaybe; /** Wallet address URL of the receiver */ receiver: Scalars['String']['input']; + /** ID of the tenant */ + tenantId: Scalars['ID']['input']; /** Id of the wallet address under which the quote will be created */ walletAddressId: Scalars['String']['input']; }; @@ -333,6 +339,25 @@ export type CreateReceiverResponse = { receiver?: Maybe; }; +export type CreateTenantEndpointsInput = { + type: TenantEndpointType; + value: Scalars['String']['input']; +}; + +export type CreateTenantInput = { + /** List of endpoints types for the tenant */ + endpoints: Array; + /** IDP Endpoint */ + idpConsentEndpoint: Scalars['String']['input']; + /** IDP Secret */ + idpSecret: Scalars['String']['input']; +}; + +export type CreateTenantMutationResponse = { + __typename?: 'CreateTenantMutationResponse'; + tenant: Tenant; +}; + export type CreateWalletAddressInput = { /** Additional properties associated with the [walletAddress]. */ additionalProperties?: InputMaybe>; @@ -342,6 +367,8 @@ export type CreateWalletAddressInput = { idempotencyKey?: InputMaybe; /** Public name associated with the wallet address */ publicName?: InputMaybe; + /** ID of a tenant */ + tenantId: Scalars['ID']['input']; /** Wallet Address URL */ url: Scalars['String']['input']; }; @@ -657,6 +684,8 @@ export type Mutation = { createQuote: QuoteResponse; /** Create an internal or external Open Payments Incoming Payment. The receiver has a wallet address on either this or another Open Payments resource server. */ createReceiver: CreateReceiverResponse; + /** Create tenant */ + createTenant: CreateTenantMutationResponse; /** Create a wallet address */ createWalletAddress: CreateWalletAddressMutationResponse; /** Add a public key to a wallet address that is used to verify Open Payments requests. */ @@ -777,6 +806,11 @@ export type MutationCreateReceiverArgs = { }; +export type MutationCreateTenantArgs = { + input: CreateTenantInput; +}; + + export type MutationCreateWalletAddressArgs = { input: CreateWalletAddressInput; }; @@ -1053,6 +1087,10 @@ export type Query = { quote?: Maybe; /** Get an local or remote Open Payments Incoming Payment. The receiver has a wallet address on either this or another Open Payments resource server. */ receiver?: Maybe; + /** Fetch a tenant */ + tenant?: Maybe; + /** Fetch a page of tenants */ + tenants: TenantsConnection; /** Fetch a wallet address. */ walletAddress?: Maybe; /** Fetch a page of wallet addresses. */ @@ -1136,6 +1174,20 @@ export type QueryReceiverArgs = { }; +export type QueryTenantArgs = { + id: Scalars['ID']['input']; +}; + + +export type QueryTenantsArgs = { + after?: InputMaybe; + before?: InputMaybe; + first?: InputMaybe; + last?: InputMaybe; + sortOrder?: InputMaybe; +}; + + export type QueryWalletAddressArgs = { id: Scalars['String']['input']; }; @@ -1253,6 +1305,55 @@ export enum SortOrder { Desc = 'DESC' } +export type Tenant = Model & { + __typename?: 'Tenant'; + /** Date-time of creation */ + createdAt: Scalars['String']['output']; + /** List of tenant endpoints associated with this tenant */ + endpoints: Array; + /** Tenant ID that is used in subsequent resources */ + id: Scalars['ID']['output']; + /** Kratos identity ID */ + kratosIdentityId: Scalars['String']['output']; + /** Date-time of the update */ + updatedAt: Scalars['String']['output']; +}; + +export type TenantEdge = { + __typename?: 'TenantEdge'; + cursor: Scalars['String']['output']; + node: Tenant; +}; + +export type TenantEndpoint = { + __typename?: 'TenantEndpoint'; + type: TenantEndpointType; + value: Scalars['String']['output']; +}; + +export type TenantEndpointEdge = { + __typename?: 'TenantEndpointEdge'; + cursor: Scalars['String']['output']; + node?: Maybe; +}; + +export enum TenantEndpointType { + RatesUrl = 'RatesUrl', + WebhookBaseUrl = 'WebhookBaseUrl' +} + +export type TenantEndpointsConnection = { + __typename?: 'TenantEndpointsConnection'; + edges: Array; + pageInfo: PageInfo; +}; + +export type TenantsConnection = { + __typename?: 'TenantsConnection'; + edges: Array; + pageInfo: PageInfo; +}; + export enum TransferType { /** Deposit transfer type. */ Deposit = 'DEPOSIT', @@ -1563,7 +1664,7 @@ export type DirectiveResolverFn> = { BasePayment: ( Partial ) | ( Partial ) | ( Partial ); - Model: ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ); + Model: ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ); }; /** Mapping between all available schema types and the resolvers types */ @@ -1601,6 +1702,9 @@ export type ResolversTypes = { CreateQuoteInput: ResolverTypeWrapper>; CreateReceiverInput: ResolverTypeWrapper>; CreateReceiverResponse: ResolverTypeWrapper>; + CreateTenantEndpointsInput: ResolverTypeWrapper>; + CreateTenantInput: ResolverTypeWrapper>; + CreateTenantMutationResponse: ResolverTypeWrapper>; CreateWalletAddressInput: ResolverTypeWrapper>; CreateWalletAddressKeyInput: ResolverTypeWrapper>; CreateWalletAddressKeyMutationResponse: ResolverTypeWrapper>; @@ -1670,6 +1774,13 @@ export type ResolversTypes = { SetFeeResponse: ResolverTypeWrapper>; SortOrder: ResolverTypeWrapper>; String: ResolverTypeWrapper>; + Tenant: ResolverTypeWrapper>; + TenantEdge: ResolverTypeWrapper>; + TenantEndpoint: ResolverTypeWrapper>; + TenantEndpointEdge: ResolverTypeWrapper>; + TenantEndpointType: ResolverTypeWrapper>; + TenantEndpointsConnection: ResolverTypeWrapper>; + TenantsConnection: ResolverTypeWrapper>; TransferType: ResolverTypeWrapper>; TriggerWalletAddressEventsInput: ResolverTypeWrapper>; TriggerWalletAddressEventsMutationResponse: ResolverTypeWrapper>; @@ -1731,6 +1842,9 @@ export type ResolversParentTypes = { CreateQuoteInput: Partial; CreateReceiverInput: Partial; CreateReceiverResponse: Partial; + CreateTenantEndpointsInput: Partial; + CreateTenantInput: Partial; + CreateTenantMutationResponse: Partial; CreateWalletAddressInput: Partial; CreateWalletAddressKeyInput: Partial; CreateWalletAddressKeyMutationResponse: Partial; @@ -1792,6 +1906,12 @@ export type ResolversParentTypes = { SetFeeInput: Partial; SetFeeResponse: Partial; String: Partial; + Tenant: Partial; + TenantEdge: Partial; + TenantEndpoint: Partial; + TenantEndpointEdge: Partial; + TenantEndpointsConnection: Partial; + TenantsConnection: Partial; TriggerWalletAddressEventsInput: Partial; TriggerWalletAddressEventsMutationResponse: Partial; UInt8: Partial; @@ -1913,6 +2033,11 @@ export type CreateReceiverResponseResolvers; }; +export type CreateTenantMutationResponseResolvers = { + tenant?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type CreateWalletAddressKeyMutationResponseResolvers = { walletAddressKey?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; @@ -2016,7 +2141,7 @@ export type LiquidityMutationResponseResolvers = { - __resolveType: TypeResolveFn<'AccountingTransfer' | 'Asset' | 'Fee' | 'IncomingPayment' | 'OutgoingPayment' | 'Payment' | 'Peer' | 'WalletAddress' | 'WalletAddressKey' | 'WebhookEvent', ParentType, ContextType>; + __resolveType: TypeResolveFn<'AccountingTransfer' | 'Asset' | 'Fee' | 'IncomingPayment' | 'OutgoingPayment' | 'Payment' | 'Peer' | 'Tenant' | 'WalletAddress' | 'WalletAddressKey' | 'WebhookEvent', ParentType, ContextType>; createdAt?: Resolver; id?: Resolver; }; @@ -2037,6 +2162,7 @@ export type MutationResolvers, ParentType, ContextType, RequireFields>; createQuote?: Resolver>; createReceiver?: Resolver>; + createTenant?: Resolver>; createWalletAddress?: Resolver>; createWalletAddressKey?: Resolver, ParentType, ContextType, RequireFields>; createWalletAddressWithdrawal?: Resolver, ParentType, ContextType, RequireFields>; @@ -2162,6 +2288,8 @@ export type QueryResolvers>; quote?: Resolver, ParentType, ContextType, RequireFields>; receiver?: Resolver, ParentType, ContextType, RequireFields>; + tenant?: Resolver, ParentType, ContextType, RequireFields>; + tenants?: Resolver>; walletAddress?: Resolver, ParentType, ContextType, RequireFields>; walletAddresses?: Resolver>; webhookEvents?: Resolver>; @@ -2219,6 +2347,45 @@ export type SetFeeResponseResolvers; }; +export type TenantResolvers = { + createdAt?: Resolver; + endpoints?: Resolver, ParentType, ContextType>; + id?: Resolver; + kratosIdentityId?: Resolver; + updatedAt?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type TenantEdgeResolvers = { + cursor?: Resolver; + node?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type TenantEndpointResolvers = { + type?: Resolver; + value?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type TenantEndpointEdgeResolvers = { + cursor?: Resolver; + node?: Resolver, ParentType, ContextType>; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type TenantEndpointsConnectionResolvers = { + edges?: Resolver, ParentType, ContextType>; + pageInfo?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type TenantsConnectionResolvers = { + edges?: Resolver, ParentType, ContextType>; + pageInfo?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type TriggerWalletAddressEventsMutationResponseResolvers = { count?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; @@ -2338,6 +2505,7 @@ export type Resolvers = { CreateOrUpdatePeerByUrlMutationResponse?: CreateOrUpdatePeerByUrlMutationResponseResolvers; CreatePeerMutationResponse?: CreatePeerMutationResponseResolvers; CreateReceiverResponse?: CreateReceiverResponseResolvers; + CreateTenantMutationResponse?: CreateTenantMutationResponseResolvers; CreateWalletAddressKeyMutationResponse?: CreateWalletAddressKeyMutationResponseResolvers; CreateWalletAddressMutationResponse?: CreateWalletAddressMutationResponseResolvers; DeleteAssetMutationResponse?: DeleteAssetMutationResponseResolvers; @@ -2375,6 +2543,12 @@ export type Resolvers = { Receiver?: ReceiverResolvers; RevokeWalletAddressKeyMutationResponse?: RevokeWalletAddressKeyMutationResponseResolvers; SetFeeResponse?: SetFeeResponseResolvers; + Tenant?: TenantResolvers; + TenantEdge?: TenantEdgeResolvers; + TenantEndpoint?: TenantEndpointResolvers; + TenantEndpointEdge?: TenantEndpointEdgeResolvers; + TenantEndpointsConnection?: TenantEndpointsConnectionResolvers; + TenantsConnection?: TenantsConnectionResolvers; TriggerWalletAddressEventsMutationResponse?: TriggerWalletAddressEventsMutationResponseResolvers; UInt8?: GraphQLScalarType; UInt64?: GraphQLScalarType; diff --git a/packages/mock-account-service-lib/src/generated/graphql.ts b/packages/mock-account-service-lib/src/generated/graphql.ts index d5b26bbf7e..d2f1d62ed5 100644 --- a/packages/mock-account-service-lib/src/generated/graphql.ts +++ b/packages/mock-account-service-lib/src/generated/graphql.ts @@ -197,6 +197,8 @@ export type CreateIncomingPaymentInput = { incomingAmount?: InputMaybe; /** Additional metadata associated with the incoming payment. */ metadata?: InputMaybe; + /** ID of the tenant */ + tenantId: Scalars['ID']['input']; /** Id of the wallet address under which the incoming payment will be created */ walletAddressId: Scalars['String']['input']; }; @@ -252,6 +254,8 @@ export type CreateOutgoingPaymentInput = { metadata?: InputMaybe; /** Id of the corresponding quote for that outgoing payment */ quoteId: Scalars['String']['input']; + /** ID of a tenant */ + tenantId: Scalars['ID']['input']; /** Id of the wallet address under which the outgoing payment will be created */ walletAddressId: Scalars['String']['input']; }; @@ -311,6 +315,8 @@ export type CreateQuoteInput = { receiveAmount?: InputMaybe; /** Wallet address URL of the receiver */ receiver: Scalars['String']['input']; + /** ID of the tenant */ + tenantId: Scalars['ID']['input']; /** Id of the wallet address under which the quote will be created */ walletAddressId: Scalars['String']['input']; }; @@ -333,6 +339,25 @@ export type CreateReceiverResponse = { receiver?: Maybe; }; +export type CreateTenantEndpointsInput = { + type: TenantEndpointType; + value: Scalars['String']['input']; +}; + +export type CreateTenantInput = { + /** List of endpoints types for the tenant */ + endpoints: Array; + /** IDP Endpoint */ + idpConsentEndpoint: Scalars['String']['input']; + /** IDP Secret */ + idpSecret: Scalars['String']['input']; +}; + +export type CreateTenantMutationResponse = { + __typename?: 'CreateTenantMutationResponse'; + tenant: Tenant; +}; + export type CreateWalletAddressInput = { /** Additional properties associated with the [walletAddress]. */ additionalProperties?: InputMaybe>; @@ -342,6 +367,8 @@ export type CreateWalletAddressInput = { idempotencyKey?: InputMaybe; /** Public name associated with the wallet address */ publicName?: InputMaybe; + /** ID of a tenant */ + tenantId: Scalars['ID']['input']; /** Wallet Address URL */ url: Scalars['String']['input']; }; @@ -657,6 +684,8 @@ export type Mutation = { createQuote: QuoteResponse; /** Create an internal or external Open Payments Incoming Payment. The receiver has a wallet address on either this or another Open Payments resource server. */ createReceiver: CreateReceiverResponse; + /** Create tenant */ + createTenant: CreateTenantMutationResponse; /** Create a wallet address */ createWalletAddress: CreateWalletAddressMutationResponse; /** Add a public key to a wallet address that is used to verify Open Payments requests. */ @@ -777,6 +806,11 @@ export type MutationCreateReceiverArgs = { }; +export type MutationCreateTenantArgs = { + input: CreateTenantInput; +}; + + export type MutationCreateWalletAddressArgs = { input: CreateWalletAddressInput; }; @@ -1053,6 +1087,10 @@ export type Query = { quote?: Maybe; /** Get an local or remote Open Payments Incoming Payment. The receiver has a wallet address on either this or another Open Payments resource server. */ receiver?: Maybe; + /** Fetch a tenant */ + tenant?: Maybe; + /** Fetch a page of tenants */ + tenants: TenantsConnection; /** Fetch a wallet address. */ walletAddress?: Maybe; /** Fetch a page of wallet addresses. */ @@ -1136,6 +1174,20 @@ export type QueryReceiverArgs = { }; +export type QueryTenantArgs = { + id: Scalars['ID']['input']; +}; + + +export type QueryTenantsArgs = { + after?: InputMaybe; + before?: InputMaybe; + first?: InputMaybe; + last?: InputMaybe; + sortOrder?: InputMaybe; +}; + + export type QueryWalletAddressArgs = { id: Scalars['String']['input']; }; @@ -1253,6 +1305,55 @@ export enum SortOrder { Desc = 'DESC' } +export type Tenant = Model & { + __typename?: 'Tenant'; + /** Date-time of creation */ + createdAt: Scalars['String']['output']; + /** List of tenant endpoints associated with this tenant */ + endpoints: Array; + /** Tenant ID that is used in subsequent resources */ + id: Scalars['ID']['output']; + /** Kratos identity ID */ + kratosIdentityId: Scalars['String']['output']; + /** Date-time of the update */ + updatedAt: Scalars['String']['output']; +}; + +export type TenantEdge = { + __typename?: 'TenantEdge'; + cursor: Scalars['String']['output']; + node: Tenant; +}; + +export type TenantEndpoint = { + __typename?: 'TenantEndpoint'; + type: TenantEndpointType; + value: Scalars['String']['output']; +}; + +export type TenantEndpointEdge = { + __typename?: 'TenantEndpointEdge'; + cursor: Scalars['String']['output']; + node?: Maybe; +}; + +export enum TenantEndpointType { + RatesUrl = 'RatesUrl', + WebhookBaseUrl = 'WebhookBaseUrl' +} + +export type TenantEndpointsConnection = { + __typename?: 'TenantEndpointsConnection'; + edges: Array; + pageInfo: PageInfo; +}; + +export type TenantsConnection = { + __typename?: 'TenantsConnection'; + edges: Array; + pageInfo: PageInfo; +}; + export enum TransferType { /** Deposit transfer type. */ Deposit = 'DEPOSIT', @@ -1563,7 +1664,7 @@ export type DirectiveResolverFn> = { BasePayment: ( Partial ) | ( Partial ) | ( Partial ); - Model: ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ); + Model: ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ); }; /** Mapping between all available schema types and the resolvers types */ @@ -1601,6 +1702,9 @@ export type ResolversTypes = { CreateQuoteInput: ResolverTypeWrapper>; CreateReceiverInput: ResolverTypeWrapper>; CreateReceiverResponse: ResolverTypeWrapper>; + CreateTenantEndpointsInput: ResolverTypeWrapper>; + CreateTenantInput: ResolverTypeWrapper>; + CreateTenantMutationResponse: ResolverTypeWrapper>; CreateWalletAddressInput: ResolverTypeWrapper>; CreateWalletAddressKeyInput: ResolverTypeWrapper>; CreateWalletAddressKeyMutationResponse: ResolverTypeWrapper>; @@ -1670,6 +1774,13 @@ export type ResolversTypes = { SetFeeResponse: ResolverTypeWrapper>; SortOrder: ResolverTypeWrapper>; String: ResolverTypeWrapper>; + Tenant: ResolverTypeWrapper>; + TenantEdge: ResolverTypeWrapper>; + TenantEndpoint: ResolverTypeWrapper>; + TenantEndpointEdge: ResolverTypeWrapper>; + TenantEndpointType: ResolverTypeWrapper>; + TenantEndpointsConnection: ResolverTypeWrapper>; + TenantsConnection: ResolverTypeWrapper>; TransferType: ResolverTypeWrapper>; TriggerWalletAddressEventsInput: ResolverTypeWrapper>; TriggerWalletAddressEventsMutationResponse: ResolverTypeWrapper>; @@ -1731,6 +1842,9 @@ export type ResolversParentTypes = { CreateQuoteInput: Partial; CreateReceiverInput: Partial; CreateReceiverResponse: Partial; + CreateTenantEndpointsInput: Partial; + CreateTenantInput: Partial; + CreateTenantMutationResponse: Partial; CreateWalletAddressInput: Partial; CreateWalletAddressKeyInput: Partial; CreateWalletAddressKeyMutationResponse: Partial; @@ -1792,6 +1906,12 @@ export type ResolversParentTypes = { SetFeeInput: Partial; SetFeeResponse: Partial; String: Partial; + Tenant: Partial; + TenantEdge: Partial; + TenantEndpoint: Partial; + TenantEndpointEdge: Partial; + TenantEndpointsConnection: Partial; + TenantsConnection: Partial; TriggerWalletAddressEventsInput: Partial; TriggerWalletAddressEventsMutationResponse: Partial; UInt8: Partial; @@ -1913,6 +2033,11 @@ export type CreateReceiverResponseResolvers; }; +export type CreateTenantMutationResponseResolvers = { + tenant?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type CreateWalletAddressKeyMutationResponseResolvers = { walletAddressKey?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; @@ -2016,7 +2141,7 @@ export type LiquidityMutationResponseResolvers = { - __resolveType: TypeResolveFn<'AccountingTransfer' | 'Asset' | 'Fee' | 'IncomingPayment' | 'OutgoingPayment' | 'Payment' | 'Peer' | 'WalletAddress' | 'WalletAddressKey' | 'WebhookEvent', ParentType, ContextType>; + __resolveType: TypeResolveFn<'AccountingTransfer' | 'Asset' | 'Fee' | 'IncomingPayment' | 'OutgoingPayment' | 'Payment' | 'Peer' | 'Tenant' | 'WalletAddress' | 'WalletAddressKey' | 'WebhookEvent', ParentType, ContextType>; createdAt?: Resolver; id?: Resolver; }; @@ -2037,6 +2162,7 @@ export type MutationResolvers, ParentType, ContextType, RequireFields>; createQuote?: Resolver>; createReceiver?: Resolver>; + createTenant?: Resolver>; createWalletAddress?: Resolver>; createWalletAddressKey?: Resolver, ParentType, ContextType, RequireFields>; createWalletAddressWithdrawal?: Resolver, ParentType, ContextType, RequireFields>; @@ -2162,6 +2288,8 @@ export type QueryResolvers>; quote?: Resolver, ParentType, ContextType, RequireFields>; receiver?: Resolver, ParentType, ContextType, RequireFields>; + tenant?: Resolver, ParentType, ContextType, RequireFields>; + tenants?: Resolver>; walletAddress?: Resolver, ParentType, ContextType, RequireFields>; walletAddresses?: Resolver>; webhookEvents?: Resolver>; @@ -2219,6 +2347,45 @@ export type SetFeeResponseResolvers; }; +export type TenantResolvers = { + createdAt?: Resolver; + endpoints?: Resolver, ParentType, ContextType>; + id?: Resolver; + kratosIdentityId?: Resolver; + updatedAt?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type TenantEdgeResolvers = { + cursor?: Resolver; + node?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type TenantEndpointResolvers = { + type?: Resolver; + value?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type TenantEndpointEdgeResolvers = { + cursor?: Resolver; + node?: Resolver, ParentType, ContextType>; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type TenantEndpointsConnectionResolvers = { + edges?: Resolver, ParentType, ContextType>; + pageInfo?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type TenantsConnectionResolvers = { + edges?: Resolver, ParentType, ContextType>; + pageInfo?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type TriggerWalletAddressEventsMutationResponseResolvers = { count?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; @@ -2338,6 +2505,7 @@ export type Resolvers = { CreateOrUpdatePeerByUrlMutationResponse?: CreateOrUpdatePeerByUrlMutationResponseResolvers; CreatePeerMutationResponse?: CreatePeerMutationResponseResolvers; CreateReceiverResponse?: CreateReceiverResponseResolvers; + CreateTenantMutationResponse?: CreateTenantMutationResponseResolvers; CreateWalletAddressKeyMutationResponse?: CreateWalletAddressKeyMutationResponseResolvers; CreateWalletAddressMutationResponse?: CreateWalletAddressMutationResponseResolvers; DeleteAssetMutationResponse?: DeleteAssetMutationResponseResolvers; @@ -2375,6 +2543,12 @@ export type Resolvers = { Receiver?: ReceiverResolvers; RevokeWalletAddressKeyMutationResponse?: RevokeWalletAddressKeyMutationResponseResolvers; SetFeeResponse?: SetFeeResponseResolvers; + Tenant?: TenantResolvers; + TenantEdge?: TenantEdgeResolvers; + TenantEndpoint?: TenantEndpointResolvers; + TenantEndpointEdge?: TenantEndpointEdgeResolvers; + TenantEndpointsConnection?: TenantEndpointsConnectionResolvers; + TenantsConnection?: TenantsConnectionResolvers; TriggerWalletAddressEventsMutationResponse?: TriggerWalletAddressEventsMutationResponseResolvers; UInt8?: GraphQLScalarType; UInt64?: GraphQLScalarType; diff --git a/packages/mock-account-service-lib/src/requesters.ts b/packages/mock-account-service-lib/src/requesters.ts index e6edd93bb9..308ea7788b 100644 --- a/packages/mock-account-service-lib/src/requesters.ts +++ b/packages/mock-account-service-lib/src/requesters.ts @@ -12,10 +12,17 @@ import type { SetFeeResponse, FeeType, CreateOrUpdatePeerByUrlMutationResponse, - CreateOrUpdatePeerByUrlInput + CreateOrUpdatePeerByUrlInput, + TenantEndpointType, + CreateTenantMutationResponse } from './generated/graphql' import { v4 as uuid } from 'uuid' +export type EndpointType = { + type: TenantEndpointType + value: string +} + export function createRequesters( apolloClient: ApolloClient, logger: Logger @@ -25,6 +32,11 @@ export function createRequesters( scale: number, liquidityThreshold: number ) => Promise + createTenant: ( + idpConsentUrl: string, + idpSecret: string, + endpoints: EndpointType[] + ) => Promise createPeer: ( staticIlpAddress: string, outgoingEndpoint: string, @@ -50,7 +62,8 @@ export function createRequesters( createWalletAddress: ( accountName: string, accountUrl: string, - assetId: string + assetId: string, + tenantId: string ) => Promise createWalletAddressKey: ({ walletAddressId, @@ -69,6 +82,8 @@ export function createRequesters( return { createAsset: (code, scale, liquidityThreshold) => createAsset(apolloClient, code, scale, liquidityThreshold), + createTenant: (idpConsentUrl, idpSecret, endpoints) => + createTenant(apolloClient, logger, idpConsentUrl, idpSecret, endpoints), createPeer: ( staticIlpAddress, outgoingEndpoint, @@ -93,13 +108,14 @@ export function createRequesters( depositPeerLiquidity(apolloClient, logger, peerId, amount, transferUid), depositAssetLiquidity: (assetId, amount, transferId) => depositAssetLiquidity(apolloClient, logger, assetId, amount, transferId), - createWalletAddress: (accountName, accountUrl, assetId) => + createWalletAddress: (accountName, accountUrl, assetId, tenantId) => createWalletAddress( apolloClient, logger, accountName, accountUrl, - assetId + assetId, + tenantId ), createWalletAddressKey: ({ walletAddressId, jwk }) => createWalletAddressKey(apolloClient, logger, { walletAddressId, jwk }), @@ -146,6 +162,45 @@ export async function createAsset( }) } +export async function createTenant( + apolloClient: ApolloClient, + logger: Logger, + idpConsentUrl: string, + idpSecret: string, + endpoints: EndpointType[] +): Promise { + const createTenantMutation = gql` + mutation CreateTenant($input: CreateTenantInput!) { + createTenant(input: $input) { + tenant { + id + } + } + } + ` + + const createTenantInput = { + input: { + idpConsentUrl, + idpSecret, + endpoints + } + } + + return apolloClient + .mutate({ + mutation: createTenantMutation, + variables: createTenantInput + }) + .then(({ data }): CreateTenantMutationResponse => { + logger.debug(data) + if (!data.createTenant.tenant) { + throw new Error('Data was empty') + } + return data.createTenant + }) +} + export async function createPeer( apolloClient: ApolloClient, logger: Logger, @@ -313,7 +368,8 @@ export async function createWalletAddress( logger: Logger, accountName: string, accountUrl: string, - assetId: string + assetId: string, + tenantId: string ): Promise { const createWalletAddressMutation = gql` mutation CreateWalletAddress($input: CreateWalletAddressInput!) { @@ -330,7 +386,8 @@ export async function createWalletAddress( assetId, url: accountUrl, publicName: accountName, - additionalProperties: [] + additionalProperties: [], + tenantId } return apolloClient diff --git a/packages/mock-account-service-lib/src/seed.ts b/packages/mock-account-service-lib/src/seed.ts index eb1fc20515..bf016a556a 100644 --- a/packages/mock-account-service-lib/src/seed.ts +++ b/packages/mock-account-service-lib/src/seed.ts @@ -30,6 +30,7 @@ export async function setupFromSeed( const logger = createLogger(loggerOptions) const { + createTenant, createAsset, depositAssetLiquidity, setFee, @@ -40,6 +41,17 @@ export async function setupFromSeed( createWalletAddressKey } = createRequesters(apolloClient, logger) + const tenants: Record = {} + for (const { name, idpConsentUrl, idpSecret, endpoints } of config.seed + .tenants) { + const { tenant } = await createTenant(idpConsentUrl, idpSecret, endpoints) + if (!tenant) { + throw new Error('error creating tenant') + } + + tenants[name] = tenant.id + } + const assets: Record = {} for (const { code, scale, liquidity, liquidityThreshold } of config.seed .assets) { @@ -129,7 +141,8 @@ export async function setupFromSeed( const walletAddress = await createWalletAddress( account.name, `${config.publicHost}/${account.path}`, - accountAsset.id + accountAsset.id, + tenants['PrimaryTenant'] ) await mockAccounts.setWalletAddress( diff --git a/packages/mock-account-service-lib/src/types.ts b/packages/mock-account-service-lib/src/types.ts index 76c90bfda8..27ef819463 100644 --- a/packages/mock-account-service-lib/src/types.ts +++ b/packages/mock-account-service-lib/src/types.ts @@ -1,4 +1,5 @@ import type { KeyObject } from 'crypto' +import { EndpointType } from './requesters' interface Asset { code: string @@ -32,7 +33,15 @@ export interface Fee { scale: number } +export interface Tenant { + name: string + idpConsentUrl: string + idpSecret: string + endpoints: EndpointType[] +} + export interface SeedInstance { + tenants: Array assets: Array peeringAsset: string peers: Array diff --git a/test/integration/lib/generated/graphql.ts b/test/integration/lib/generated/graphql.ts index d5b26bbf7e..d2f1d62ed5 100644 --- a/test/integration/lib/generated/graphql.ts +++ b/test/integration/lib/generated/graphql.ts @@ -197,6 +197,8 @@ export type CreateIncomingPaymentInput = { incomingAmount?: InputMaybe; /** Additional metadata associated with the incoming payment. */ metadata?: InputMaybe; + /** ID of the tenant */ + tenantId: Scalars['ID']['input']; /** Id of the wallet address under which the incoming payment will be created */ walletAddressId: Scalars['String']['input']; }; @@ -252,6 +254,8 @@ export type CreateOutgoingPaymentInput = { metadata?: InputMaybe; /** Id of the corresponding quote for that outgoing payment */ quoteId: Scalars['String']['input']; + /** ID of a tenant */ + tenantId: Scalars['ID']['input']; /** Id of the wallet address under which the outgoing payment will be created */ walletAddressId: Scalars['String']['input']; }; @@ -311,6 +315,8 @@ export type CreateQuoteInput = { receiveAmount?: InputMaybe; /** Wallet address URL of the receiver */ receiver: Scalars['String']['input']; + /** ID of the tenant */ + tenantId: Scalars['ID']['input']; /** Id of the wallet address under which the quote will be created */ walletAddressId: Scalars['String']['input']; }; @@ -333,6 +339,25 @@ export type CreateReceiverResponse = { receiver?: Maybe; }; +export type CreateTenantEndpointsInput = { + type: TenantEndpointType; + value: Scalars['String']['input']; +}; + +export type CreateTenantInput = { + /** List of endpoints types for the tenant */ + endpoints: Array; + /** IDP Endpoint */ + idpConsentEndpoint: Scalars['String']['input']; + /** IDP Secret */ + idpSecret: Scalars['String']['input']; +}; + +export type CreateTenantMutationResponse = { + __typename?: 'CreateTenantMutationResponse'; + tenant: Tenant; +}; + export type CreateWalletAddressInput = { /** Additional properties associated with the [walletAddress]. */ additionalProperties?: InputMaybe>; @@ -342,6 +367,8 @@ export type CreateWalletAddressInput = { idempotencyKey?: InputMaybe; /** Public name associated with the wallet address */ publicName?: InputMaybe; + /** ID of a tenant */ + tenantId: Scalars['ID']['input']; /** Wallet Address URL */ url: Scalars['String']['input']; }; @@ -657,6 +684,8 @@ export type Mutation = { createQuote: QuoteResponse; /** Create an internal or external Open Payments Incoming Payment. The receiver has a wallet address on either this or another Open Payments resource server. */ createReceiver: CreateReceiverResponse; + /** Create tenant */ + createTenant: CreateTenantMutationResponse; /** Create a wallet address */ createWalletAddress: CreateWalletAddressMutationResponse; /** Add a public key to a wallet address that is used to verify Open Payments requests. */ @@ -777,6 +806,11 @@ export type MutationCreateReceiverArgs = { }; +export type MutationCreateTenantArgs = { + input: CreateTenantInput; +}; + + export type MutationCreateWalletAddressArgs = { input: CreateWalletAddressInput; }; @@ -1053,6 +1087,10 @@ export type Query = { quote?: Maybe; /** Get an local or remote Open Payments Incoming Payment. The receiver has a wallet address on either this or another Open Payments resource server. */ receiver?: Maybe; + /** Fetch a tenant */ + tenant?: Maybe; + /** Fetch a page of tenants */ + tenants: TenantsConnection; /** Fetch a wallet address. */ walletAddress?: Maybe; /** Fetch a page of wallet addresses. */ @@ -1136,6 +1174,20 @@ export type QueryReceiverArgs = { }; +export type QueryTenantArgs = { + id: Scalars['ID']['input']; +}; + + +export type QueryTenantsArgs = { + after?: InputMaybe; + before?: InputMaybe; + first?: InputMaybe; + last?: InputMaybe; + sortOrder?: InputMaybe; +}; + + export type QueryWalletAddressArgs = { id: Scalars['String']['input']; }; @@ -1253,6 +1305,55 @@ export enum SortOrder { Desc = 'DESC' } +export type Tenant = Model & { + __typename?: 'Tenant'; + /** Date-time of creation */ + createdAt: Scalars['String']['output']; + /** List of tenant endpoints associated with this tenant */ + endpoints: Array; + /** Tenant ID that is used in subsequent resources */ + id: Scalars['ID']['output']; + /** Kratos identity ID */ + kratosIdentityId: Scalars['String']['output']; + /** Date-time of the update */ + updatedAt: Scalars['String']['output']; +}; + +export type TenantEdge = { + __typename?: 'TenantEdge'; + cursor: Scalars['String']['output']; + node: Tenant; +}; + +export type TenantEndpoint = { + __typename?: 'TenantEndpoint'; + type: TenantEndpointType; + value: Scalars['String']['output']; +}; + +export type TenantEndpointEdge = { + __typename?: 'TenantEndpointEdge'; + cursor: Scalars['String']['output']; + node?: Maybe; +}; + +export enum TenantEndpointType { + RatesUrl = 'RatesUrl', + WebhookBaseUrl = 'WebhookBaseUrl' +} + +export type TenantEndpointsConnection = { + __typename?: 'TenantEndpointsConnection'; + edges: Array; + pageInfo: PageInfo; +}; + +export type TenantsConnection = { + __typename?: 'TenantsConnection'; + edges: Array; + pageInfo: PageInfo; +}; + export enum TransferType { /** Deposit transfer type. */ Deposit = 'DEPOSIT', @@ -1563,7 +1664,7 @@ export type DirectiveResolverFn> = { BasePayment: ( Partial ) | ( Partial ) | ( Partial ); - Model: ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ); + Model: ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ); }; /** Mapping between all available schema types and the resolvers types */ @@ -1601,6 +1702,9 @@ export type ResolversTypes = { CreateQuoteInput: ResolverTypeWrapper>; CreateReceiverInput: ResolverTypeWrapper>; CreateReceiverResponse: ResolverTypeWrapper>; + CreateTenantEndpointsInput: ResolverTypeWrapper>; + CreateTenantInput: ResolverTypeWrapper>; + CreateTenantMutationResponse: ResolverTypeWrapper>; CreateWalletAddressInput: ResolverTypeWrapper>; CreateWalletAddressKeyInput: ResolverTypeWrapper>; CreateWalletAddressKeyMutationResponse: ResolverTypeWrapper>; @@ -1670,6 +1774,13 @@ export type ResolversTypes = { SetFeeResponse: ResolverTypeWrapper>; SortOrder: ResolverTypeWrapper>; String: ResolverTypeWrapper>; + Tenant: ResolverTypeWrapper>; + TenantEdge: ResolverTypeWrapper>; + TenantEndpoint: ResolverTypeWrapper>; + TenantEndpointEdge: ResolverTypeWrapper>; + TenantEndpointType: ResolverTypeWrapper>; + TenantEndpointsConnection: ResolverTypeWrapper>; + TenantsConnection: ResolverTypeWrapper>; TransferType: ResolverTypeWrapper>; TriggerWalletAddressEventsInput: ResolverTypeWrapper>; TriggerWalletAddressEventsMutationResponse: ResolverTypeWrapper>; @@ -1731,6 +1842,9 @@ export type ResolversParentTypes = { CreateQuoteInput: Partial; CreateReceiverInput: Partial; CreateReceiverResponse: Partial; + CreateTenantEndpointsInput: Partial; + CreateTenantInput: Partial; + CreateTenantMutationResponse: Partial; CreateWalletAddressInput: Partial; CreateWalletAddressKeyInput: Partial; CreateWalletAddressKeyMutationResponse: Partial; @@ -1792,6 +1906,12 @@ export type ResolversParentTypes = { SetFeeInput: Partial; SetFeeResponse: Partial; String: Partial; + Tenant: Partial; + TenantEdge: Partial; + TenantEndpoint: Partial; + TenantEndpointEdge: Partial; + TenantEndpointsConnection: Partial; + TenantsConnection: Partial; TriggerWalletAddressEventsInput: Partial; TriggerWalletAddressEventsMutationResponse: Partial; UInt8: Partial; @@ -1913,6 +2033,11 @@ export type CreateReceiverResponseResolvers; }; +export type CreateTenantMutationResponseResolvers = { + tenant?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type CreateWalletAddressKeyMutationResponseResolvers = { walletAddressKey?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; @@ -2016,7 +2141,7 @@ export type LiquidityMutationResponseResolvers = { - __resolveType: TypeResolveFn<'AccountingTransfer' | 'Asset' | 'Fee' | 'IncomingPayment' | 'OutgoingPayment' | 'Payment' | 'Peer' | 'WalletAddress' | 'WalletAddressKey' | 'WebhookEvent', ParentType, ContextType>; + __resolveType: TypeResolveFn<'AccountingTransfer' | 'Asset' | 'Fee' | 'IncomingPayment' | 'OutgoingPayment' | 'Payment' | 'Peer' | 'Tenant' | 'WalletAddress' | 'WalletAddressKey' | 'WebhookEvent', ParentType, ContextType>; createdAt?: Resolver; id?: Resolver; }; @@ -2037,6 +2162,7 @@ export type MutationResolvers, ParentType, ContextType, RequireFields>; createQuote?: Resolver>; createReceiver?: Resolver>; + createTenant?: Resolver>; createWalletAddress?: Resolver>; createWalletAddressKey?: Resolver, ParentType, ContextType, RequireFields>; createWalletAddressWithdrawal?: Resolver, ParentType, ContextType, RequireFields>; @@ -2162,6 +2288,8 @@ export type QueryResolvers>; quote?: Resolver, ParentType, ContextType, RequireFields>; receiver?: Resolver, ParentType, ContextType, RequireFields>; + tenant?: Resolver, ParentType, ContextType, RequireFields>; + tenants?: Resolver>; walletAddress?: Resolver, ParentType, ContextType, RequireFields>; walletAddresses?: Resolver>; webhookEvents?: Resolver>; @@ -2219,6 +2347,45 @@ export type SetFeeResponseResolvers; }; +export type TenantResolvers = { + createdAt?: Resolver; + endpoints?: Resolver, ParentType, ContextType>; + id?: Resolver; + kratosIdentityId?: Resolver; + updatedAt?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type TenantEdgeResolvers = { + cursor?: Resolver; + node?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type TenantEndpointResolvers = { + type?: Resolver; + value?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type TenantEndpointEdgeResolvers = { + cursor?: Resolver; + node?: Resolver, ParentType, ContextType>; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type TenantEndpointsConnectionResolvers = { + edges?: Resolver, ParentType, ContextType>; + pageInfo?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type TenantsConnectionResolvers = { + edges?: Resolver, ParentType, ContextType>; + pageInfo?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type TriggerWalletAddressEventsMutationResponseResolvers = { count?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; @@ -2338,6 +2505,7 @@ export type Resolvers = { CreateOrUpdatePeerByUrlMutationResponse?: CreateOrUpdatePeerByUrlMutationResponseResolvers; CreatePeerMutationResponse?: CreatePeerMutationResponseResolvers; CreateReceiverResponse?: CreateReceiverResponseResolvers; + CreateTenantMutationResponse?: CreateTenantMutationResponseResolvers; CreateWalletAddressKeyMutationResponse?: CreateWalletAddressKeyMutationResponseResolvers; CreateWalletAddressMutationResponse?: CreateWalletAddressMutationResponseResolvers; DeleteAssetMutationResponse?: DeleteAssetMutationResponseResolvers; @@ -2375,6 +2543,12 @@ export type Resolvers = { Receiver?: ReceiverResolvers; RevokeWalletAddressKeyMutationResponse?: RevokeWalletAddressKeyMutationResponseResolvers; SetFeeResponse?: SetFeeResponseResolvers; + Tenant?: TenantResolvers; + TenantEdge?: TenantEdgeResolvers; + TenantEndpoint?: TenantEndpointResolvers; + TenantEndpointEdge?: TenantEndpointEdgeResolvers; + TenantEndpointsConnection?: TenantEndpointsConnectionResolvers; + TenantsConnection?: TenantsConnectionResolvers; TriggerWalletAddressEventsMutationResponse?: TriggerWalletAddressEventsMutationResponseResolvers; UInt8?: GraphQLScalarType; UInt64?: GraphQLScalarType;