From 99d698201b5937c84aee4dfc8074a632116dc31b Mon Sep 17 00:00:00 2001 From: Nathan Lie Date: Tue, 21 Jan 2025 14:06:45 -0800 Subject: [PATCH 01/22] feat: backend tenant graphql resolvers --- .../generated/graphql.ts | 193 +++- .../src/graphql/generated/graphql.schema.json | 828 ++++++++++++++++-- .../backend/src/graphql/generated/graphql.ts | 193 +++- .../backend/src/graphql/resolvers/index.ts | 18 +- .../src/graphql/resolvers/tenant.test.ts | 508 +++++++++++ .../backend/src/graphql/resolvers/tenant.ts | 179 ++++ packages/backend/src/graphql/schema.graphql | 103 +++ packages/backend/src/tests/app.ts | 1 + packages/backend/src/tests/tenant.ts | 10 + packages/frontend/app/generated/graphql.ts | 193 +++- .../src/generated/graphql.ts | 193 +++- test/integration/lib/generated/graphql.ts | 193 +++- 12 files changed, 2550 insertions(+), 62 deletions(-) create mode 100644 packages/backend/src/graphql/resolvers/tenant.test.ts create mode 100644 packages/backend/src/graphql/resolvers/tenant.ts diff --git a/localenv/mock-account-servicing-entity/generated/graphql.ts b/localenv/mock-account-servicing-entity/generated/graphql.ts index 46dad2dedb..0d37f19a34 100644 --- a/localenv/mock-account-servicing-entity/generated/graphql.ts +++ b/localenv/mock-account-servicing-entity/generated/graphql.ts @@ -364,6 +364,19 @@ export type CreateReceiverResponse = { receiver?: Maybe; }; +export type CreateTenantInput = { + /** Secret used to secure requests made for this tenant. */ + apiSecret: Scalars['String']['input']; + /** Contact email of the tenant owner. */ + email: Scalars['String']['input']; + /** URL of the tenant's identity provider's consent screen. */ + idpConsentUrl: Scalars['String']['input']; + /** Secret used to secure requests from the tenant's identity provider. */ + idpSecret: Scalars['String']['input']; + /** Public name for the tenant. */ + publicName?: InputMaybe; +}; + export type CreateWalletAddressInput = { /** Additional properties associated with the wallet address. */ additionalProperties?: InputMaybe>; @@ -440,6 +453,11 @@ export type DeletePeerMutationResponse = { success: Scalars['Boolean']['output']; }; +export type DeleteTenantMutationResponse = { + __typename?: 'DeleteTenantMutationResponse'; + success: Scalars['Boolean']['output']; +}; + export type DepositAssetLiquidityInput = { /** Amount of liquidity to deposit. */ amount: Scalars['UInt64']['input']; @@ -721,6 +739,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 a tenant. */ + createTenant: TenantMutationResponse; /** Create a new wallet address. */ createWalletAddress: CreateWalletAddressMutationResponse; /** Add a public key to a wallet address that is used to verify Open Payments requests. */ @@ -731,6 +751,8 @@ export type Mutation = { deleteAsset: DeleteAssetMutationResponse; /** Delete a peer. */ deletePeer: DeletePeerMutationResponse; + /** Delete a tenant. */ + deleteTenant: DeleteTenantMutationResponse; /** Deposit asset liquidity. */ depositAssetLiquidity?: Maybe; /** @@ -756,6 +778,8 @@ export type Mutation = { updateIncomingPayment: IncomingPaymentResponse; /** Update an existing peer. */ updatePeer: UpdatePeerMutationResponse; + /** Update a tenant. */ + updateTenant: TenantMutationResponse; /** Update an existing wallet address. */ updateWalletAddress: UpdateWalletAddressMutationResponse; /** Void liquidity withdrawal. Withdrawals are two-phase commits and are rolled back via this mutation. */ @@ -843,6 +867,11 @@ export type MutationCreateReceiverArgs = { }; +export type MutationCreateTenantArgs = { + input: CreateTenantInput; +}; + + export type MutationCreateWalletAddressArgs = { input: CreateWalletAddressInput; }; @@ -868,6 +897,11 @@ export type MutationDeletePeerArgs = { }; +export type MutationDeleteTenantArgs = { + id: Scalars['String']['input']; +}; + + export type MutationDepositAssetLiquidityArgs = { input: DepositAssetLiquidityInput; }; @@ -923,6 +957,11 @@ export type MutationUpdatePeerArgs = { }; +export type MutationUpdateTenantArgs = { + input: UpdateTenantInput; +}; + + export type MutationUpdateWalletAddressArgs = { input: UpdateWalletAddressInput; }; @@ -1150,6 +1189,10 @@ export type Query = { quote?: Maybe; /** Retrieve an Open Payments incoming payment by receiver ID. The receiver's wallet address can be hosted on this server or a remote Open Payments resource server. */ receiver?: Maybe; + /** Retrieve a tenant of the instance. */ + tenant?: Maybe; + /** Fetch a paginated list of tenants on the instance. */ + tenants: TenantsConnection; /** Fetch a wallet address by its ID. */ walletAddress?: Maybe; /** Get a wallet address by its url if it exists */ @@ -1158,6 +1201,8 @@ export type Query = { walletAddresses: WalletAddressesConnection; /** Fetch a paginated list of webhook events. */ webhookEvents: WebhookEventsConnection; + /** Determine if the requester has operator permissions */ + whoami: WhoamiResponse; }; @@ -1247,6 +1292,20 @@ export type QueryReceiverArgs = { }; +export type QueryTenantArgs = { + id: Scalars['String']['input']; +}; + + +export type QueryTenantsArgs = { + after?: InputMaybe; + before?: InputMaybe; + first?: InputMaybe; + last?: InputMaybe; + sortOrder?: InputMaybe; +}; + + export type QueryWalletAddressArgs = { id: Scalars['String']['input']; }; @@ -1376,6 +1435,47 @@ export enum SortOrder { Desc = 'DESC' } +export type Tenant = Model & { + __typename?: 'Tenant'; + /** Secret used to secure requests made for this tenant. */ + apiSecret: Scalars['String']['output']; + /** The date and time that this tenant was created. */ + createdAt: Scalars['String']['output']; + /** The date and time that this tenant was deleted. */ + deletedAt?: Maybe; + /** Contact email of the tenant owner. */ + email: Scalars['String']['output']; + /** Unique identifier of the tenant. */ + id: Scalars['ID']['output']; + /** URL of the tenant's identity provider's consent screen. */ + idpConsentUrl: Scalars['String']['output']; + /** Secret used to secure requests from the tenant's identity provider. */ + idpSecret: Scalars['String']['output']; + /** Public name for the tenant. */ + publicName?: Maybe; +}; + +export type TenantEdge = { + __typename?: 'TenantEdge'; + /** A cursor for paginating through the tenants. */ + cursor: Scalars['String']['output']; + /** A tenant node in the list. */ + node: Tenant; +}; + +export type TenantMutationResponse = { + __typename?: 'TenantMutationResponse'; + tenant: Tenant; +}; + +export type TenantsConnection = { + __typename?: 'TenantsConnection'; + /** A list of edges representing tenants and cursors for pagination. */ + edges: Array; + /** Information to aid in pagination. */ + pageInfo: PageInfo; +}; + export enum TransferState { /** The accounting transfer is pending */ Pending = 'PENDING', @@ -1448,6 +1548,21 @@ export type UpdatePeerMutationResponse = { peer?: Maybe; }; +export type UpdateTenantInput = { + /** Secret used to secure requests made for this tenant. */ + apiSecret?: InputMaybe; + /** Contact email of the tenant owner. */ + email?: InputMaybe; + /** Unique identifier of the tenant. */ + id: Scalars['ID']['input']; + /** URL of the tenant's identity provider's consent screen. */ + idpConsentUrl?: InputMaybe; + /** Secret used to secure requests from the tenant's identity provider. */ + idpSecret?: InputMaybe; + /** Public name for the tenant. */ + publicName?: InputMaybe; +}; + export type UpdateWalletAddressInput = { /** Additional properties associated with this wallet address. */ additionalProperties?: InputMaybe>; @@ -1640,6 +1755,12 @@ export type WebhookEventsEdge = { node: WebhookEvent; }; +export type WhoamiResponse = { + __typename?: 'WhoamiResponse'; + id: Scalars['String']['output']; + isOperator: Scalars['Boolean']['output']; +}; + export type WithdrawEventLiquidityInput = { /** Unique identifier of the event to withdraw liquidity from. */ eventId: Scalars['String']['input']; @@ -1718,7 +1839,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 */ @@ -1756,6 +1877,7 @@ export type ResolversTypes = { CreateQuoteInput: ResolverTypeWrapper>; CreateReceiverInput: ResolverTypeWrapper>; CreateReceiverResponse: ResolverTypeWrapper>; + CreateTenantInput: ResolverTypeWrapper>; CreateWalletAddressInput: ResolverTypeWrapper>; CreateWalletAddressKeyInput: ResolverTypeWrapper>; CreateWalletAddressKeyMutationResponse: ResolverTypeWrapper>; @@ -1766,6 +1888,7 @@ export type ResolversTypes = { DeleteAssetMutationResponse: ResolverTypeWrapper>; DeletePeerInput: ResolverTypeWrapper>; DeletePeerMutationResponse: ResolverTypeWrapper>; + DeleteTenantMutationResponse: ResolverTypeWrapper>; DepositAssetLiquidityInput: ResolverTypeWrapper>; DepositEventLiquidityInput: ResolverTypeWrapper>; DepositOutgoingPaymentLiquidityInput: ResolverTypeWrapper>; @@ -1825,6 +1948,10 @@ export type ResolversTypes = { SetFeeResponse: ResolverTypeWrapper>; SortOrder: ResolverTypeWrapper>; String: ResolverTypeWrapper>; + Tenant: ResolverTypeWrapper>; + TenantEdge: ResolverTypeWrapper>; + TenantMutationResponse: ResolverTypeWrapper>; + TenantsConnection: ResolverTypeWrapper>; TransferState: ResolverTypeWrapper>; TransferType: ResolverTypeWrapper>; TriggerWalletAddressEventsInput: ResolverTypeWrapper>; @@ -1835,6 +1962,7 @@ export type ResolversTypes = { UpdateIncomingPaymentInput: ResolverTypeWrapper>; UpdatePeerInput: ResolverTypeWrapper>; UpdatePeerMutationResponse: ResolverTypeWrapper>; + UpdateTenantInput: ResolverTypeWrapper>; UpdateWalletAddressInput: ResolverTypeWrapper>; UpdateWalletAddressMutationResponse: ResolverTypeWrapper>; VoidLiquidityWithdrawalInput: ResolverTypeWrapper>; @@ -1851,6 +1979,7 @@ export type ResolversTypes = { WebhookEventFilter: ResolverTypeWrapper>; WebhookEventsConnection: ResolverTypeWrapper>; WebhookEventsEdge: ResolverTypeWrapper>; + WhoamiResponse: ResolverTypeWrapper>; WithdrawEventLiquidityInput: ResolverTypeWrapper>; }; @@ -1888,6 +2017,7 @@ export type ResolversParentTypes = { CreateQuoteInput: Partial; CreateReceiverInput: Partial; CreateReceiverResponse: Partial; + CreateTenantInput: Partial; CreateWalletAddressInput: Partial; CreateWalletAddressKeyInput: Partial; CreateWalletAddressKeyMutationResponse: Partial; @@ -1897,6 +2027,7 @@ export type ResolversParentTypes = { DeleteAssetMutationResponse: Partial; DeletePeerInput: Partial; DeletePeerMutationResponse: Partial; + DeleteTenantMutationResponse: Partial; DepositAssetLiquidityInput: Partial; DepositEventLiquidityInput: Partial; DepositOutgoingPaymentLiquidityInput: Partial; @@ -1949,6 +2080,10 @@ export type ResolversParentTypes = { SetFeeInput: Partial; SetFeeResponse: Partial; String: Partial; + Tenant: Partial; + TenantEdge: Partial; + TenantMutationResponse: Partial; + TenantsConnection: Partial; TriggerWalletAddressEventsInput: Partial; TriggerWalletAddressEventsMutationResponse: Partial; UInt8: Partial; @@ -1957,6 +2092,7 @@ export type ResolversParentTypes = { UpdateIncomingPaymentInput: Partial; UpdatePeerInput: Partial; UpdatePeerMutationResponse: Partial; + UpdateTenantInput: Partial; UpdateWalletAddressInput: Partial; UpdateWalletAddressMutationResponse: Partial; VoidLiquidityWithdrawalInput: Partial; @@ -1972,6 +2108,7 @@ export type ResolversParentTypes = { WebhookEventFilter: Partial; WebhookEventsConnection: Partial; WebhookEventsEdge: Partial; + WhoamiResponse: Partial; WithdrawEventLiquidityInput: Partial; }; @@ -2093,6 +2230,11 @@ export type DeletePeerMutationResponseResolvers; }; +export type DeleteTenantMutationResponseResolvers = { + success?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type FeeResolvers = { assetId?: Resolver; basisPoints?: Resolver; @@ -2176,7 +2318,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; }; @@ -2197,11 +2339,13 @@ export type MutationResolvers, ParentType, ContextType, RequireFields>; createQuote?: Resolver>; createReceiver?: Resolver>; + createTenant?: Resolver>; createWalletAddress?: Resolver>; createWalletAddressKey?: Resolver, ParentType, ContextType, RequireFields>; createWalletAddressWithdrawal?: Resolver, ParentType, ContextType, RequireFields>; deleteAsset?: Resolver>; deletePeer?: Resolver>; + deleteTenant?: Resolver>; depositAssetLiquidity?: Resolver, ParentType, ContextType, RequireFields>; depositEventLiquidity?: Resolver, ParentType, ContextType, RequireFields>; depositOutgoingPaymentLiquidity?: Resolver, ParentType, ContextType, RequireFields>; @@ -2213,6 +2357,7 @@ export type MutationResolvers>; updateIncomingPayment?: Resolver>; updatePeer?: Resolver>; + updateTenant?: Resolver>; updateWalletAddress?: Resolver>; voidLiquidityWithdrawal?: Resolver, ParentType, ContextType, RequireFields>; withdrawEventLiquidity?: Resolver, ParentType, ContextType, RequireFields>; @@ -2325,10 +2470,13 @@ 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>; walletAddressByUrl?: Resolver, ParentType, ContextType, RequireFields>; walletAddresses?: Resolver>; webhookEvents?: Resolver>; + whoami?: Resolver; }; export type QuoteResolvers = { @@ -2383,6 +2531,35 @@ export type SetFeeResponseResolvers; }; +export type TenantResolvers = { + apiSecret?: Resolver; + createdAt?: Resolver; + deletedAt?: Resolver, ParentType, ContextType>; + email?: Resolver; + id?: Resolver; + idpConsentUrl?: Resolver; + idpSecret?: Resolver; + publicName?: Resolver, ParentType, ContextType>; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type TenantEdgeResolvers = { + cursor?: Resolver; + node?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type TenantMutationResponseResolvers = { + tenant?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type TenantsConnectionResolvers = { + edges?: Resolver, ParentType, ContextType>; + pageInfo?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type TriggerWalletAddressEventsMutationResponseResolvers = { count?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; @@ -2487,6 +2664,12 @@ export type WebhookEventsEdgeResolvers; }; +export type WhoamiResponseResolvers = { + id?: Resolver; + isOperator?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type Resolvers = { AccountingTransfer?: AccountingTransferResolvers; AccountingTransferConnection?: AccountingTransferConnectionResolvers; @@ -2506,6 +2689,7 @@ export type Resolvers = { CreateWalletAddressMutationResponse?: CreateWalletAddressMutationResponseResolvers; DeleteAssetMutationResponse?: DeleteAssetMutationResponseResolvers; DeletePeerMutationResponse?: DeletePeerMutationResponseResolvers; + DeleteTenantMutationResponse?: DeleteTenantMutationResponseResolvers; Fee?: FeeResolvers; FeeEdge?: FeeEdgeResolvers; FeesConnection?: FeesConnectionResolvers; @@ -2539,6 +2723,10 @@ export type Resolvers = { Receiver?: ReceiverResolvers; RevokeWalletAddressKeyMutationResponse?: RevokeWalletAddressKeyMutationResponseResolvers; SetFeeResponse?: SetFeeResponseResolvers; + Tenant?: TenantResolvers; + TenantEdge?: TenantEdgeResolvers; + TenantMutationResponse?: TenantMutationResponseResolvers; + TenantsConnection?: TenantsConnectionResolvers; TriggerWalletAddressEventsMutationResponse?: TriggerWalletAddressEventsMutationResponseResolvers; UInt8?: GraphQLScalarType; UInt64?: GraphQLScalarType; @@ -2555,5 +2743,6 @@ export type Resolvers = { WebhookEvent?: WebhookEventResolvers; WebhookEventsConnection?: WebhookEventsConnectionResolvers; WebhookEventsEdge?: WebhookEventsEdgeResolvers; + WhoamiResponse?: WhoamiResponseResolvers; }; diff --git a/packages/backend/src/graphql/generated/graphql.schema.json b/packages/backend/src/graphql/generated/graphql.schema.json index 1745966aa2..c745f24e15 100644 --- a/packages/backend/src/graphql/generated/graphql.schema.json +++ b/packages/backend/src/graphql/generated/graphql.schema.json @@ -2105,6 +2105,93 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "INPUT_OBJECT", + "name": "CreateTenantInput", + "description": null, + "fields": null, + "inputFields": [ + { + "name": "apiSecret", + "description": "Secret used to secure requests made for this tenant.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "email", + "description": "Contact email of the tenant owner.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "idpConsentUrl", + "description": "URL of the tenant's identity provider's consent screen.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "idpSecret", + "description": "Secret used to secure requests from the tenant's identity provider.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "publicName", + "description": "Public name for the tenant.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, { "kind": "INPUT_OBJECT", "name": "CreateWalletAddressInput", @@ -2513,6 +2600,33 @@ "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": "DepositAssetLiquidityInput", @@ -3988,6 +4102,11 @@ "name": "Peer", "ofType": null }, + { + "kind": "OBJECT", + "name": "Tenant", + "ofType": null + }, { "kind": "OBJECT", "name": "WalletAddress", @@ -4489,6 +4608,39 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "createTenant", + "description": "Create a 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": "TenantMutationResponse", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "createWalletAddress", "description": "Create a new wallet address.", @@ -4646,6 +4798,39 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "deleteTenant", + "description": "Delete a tenant.", + "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": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "DeleteTenantMutationResponse", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "depositAssetLiquidity", "description": "Deposit asset liquidity.", @@ -4985,6 +5170,39 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "updateTenant", + "description": "Update a tenant.", + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "UpdateTenantInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "TenantMutationResponse", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "updateWalletAddress", "description": "Update an existing wallet address.", @@ -6811,41 +7029,12 @@ "deprecationReason": null }, { - "name": "walletAddress", - "description": "Fetch a wallet address by its ID.", + "name": "tenant", + "description": "Retrieve a tenant of the instance.", "args": [ { "name": "id", - "description": "Unique identifier of the wallet address.", - "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": "walletAddressByUrl", - "description": "Get a wallet address by its url if it exists", - "args": [ - { - "name": "url", - "description": "Wallet Address URL.", + "description": "Unique identifier of the tenant.", "type": { "kind": "NON_NULL", "name": null, @@ -6862,19 +7051,19 @@ ], "type": { "kind": "OBJECT", - "name": "WalletAddress", + "name": "Tenant", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "walletAddresses", - "description": "Fetch a paginated list of wallet addresses.", + "name": "tenants", + "description": "Fetch a paginated list of tenants on the instance.", "args": [ { "name": "after", - "description": "Forward pagination: Cursor (wallet address ID) to start retrieving wallet addresses after this point.", + "description": "Forward pagination: Cursor (tenant ID) to start retrieving tenants after this point.", "type": { "kind": "SCALAR", "name": "String", @@ -6886,7 +7075,7 @@ }, { "name": "before", - "description": "Backward pagination: Cursor (wallet address ID) to start retrieving wallet addresses before this point.", + "description": "Backward pagination: Cursor (tenant ID) to start retrieving tenants before this point.", "type": { "kind": "SCALAR", "name": "String", @@ -6898,7 +7087,7 @@ }, { "name": "first", - "description": "Forward pagination: Limit the result to the first **n** wallet addresses after the `after` cursor.", + "description": "Forward pagination: Limit the result to the first **n** tenants after the `after` cursor.", "type": { "kind": "SCALAR", "name": "Int", @@ -6910,7 +7099,7 @@ }, { "name": "last", - "description": "Backward pagination: Limit the result to the last **n** wallet addresses before the `before` cursor.", + "description": "Backward pagination: Limit the result to the last **n** tenants before the `before` cursor.", "type": { "kind": "SCALAR", "name": "Int", @@ -6922,7 +7111,7 @@ }, { "name": "sortOrder", - "description": "Specify the sort order of wallet addresses based on their creation date, either ascending or descending.", + "description": "Specify the sort order of tenants based on their creation date, either ascending or descending.", "type": { "kind": "ENUM", "name": "SortOrder", @@ -6938,7 +7127,7 @@ "name": null, "ofType": { "kind": "OBJECT", - "name": "WalletAddressesConnection", + "name": "TenantsConnection", "ofType": null } }, @@ -6946,22 +7135,157 @@ "deprecationReason": null }, { - "name": "webhookEvents", - "description": "Fetch a paginated list of webhook events.", + "name": "walletAddress", + "description": "Fetch a wallet address by its ID.", "args": [ { - "name": "after", - "description": "Forward pagination: Cursor (webhook event ID) to start retrieving webhook events after this point.", + "name": "id", + "description": "Unique identifier of the wallet address.", "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null + "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": "walletAddressByUrl", + "description": "Get a wallet address by its url if it exists", + "args": [ + { + "name": "url", + "description": "Wallet Address URL.", + "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 paginated list of wallet addresses.", + "args": [ + { + "name": "after", + "description": "Forward pagination: Cursor (wallet address ID) to start retrieving wallet addresses after this point.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Backward pagination: Cursor (wallet address ID) to start retrieving wallet addresses before this point.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Forward pagination: Limit the result to the first **n** wallet addresses after the `after` cursor.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Backward pagination: Limit the result to the last **n** wallet addresses before the `before` cursor.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "sortOrder", + "description": "Specify the sort order of wallet addresses based on their creation date, either ascending or descending.", + "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 paginated list of webhook events.", + "args": [ + { + "name": "after", + "description": "Forward pagination: Cursor (webhook event ID) to start retrieving webhook events after this point.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { "name": "before", "description": "Backward pagination: Cursor (webhook event ID) to start retrieving webhook events before this point.", "type": { @@ -7033,6 +7357,22 @@ }, "isDeprecated": false, "deprecationReason": null + }, + { + "name": "whoami", + "description": "Determine if the requester has operator permissions", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "WhoamiResponse", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null } ], "inputFields": null, @@ -7624,6 +7964,264 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "OBJECT", + "name": "Tenant", + "description": null, + "fields": [ + { + "name": "apiSecret", + "description": "Secret used to secure requests made for this tenant.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createdAt", + "description": "The date and time that this tenant was created.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deletedAt", + "description": "The date and time that this tenant was deleted.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "email", + "description": "Contact email of the tenant owner.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "id", + "description": "Unique identifier of the tenant.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "idpConsentUrl", + "description": "URL of the tenant's identity provider's consent screen.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "idpSecret", + "description": "Secret used to secure requests from the tenant's identity provider.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "publicName", + "description": "Public name for the tenant.", + "args": [], + "type": { + "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": "A cursor for paginating through the tenants.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "node", + "description": "A tenant node in the list.", + "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": "TenantMutationResponse", + "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": "OBJECT", + "name": "TenantsConnection", + "description": null, + "fields": [ + { + "name": "edges", + "description": "A list of edges representing tenants and cursors for pagination.", + "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": "Information to aid in pagination.", + "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": "TransferState", @@ -7992,6 +8590,93 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "INPUT_OBJECT", + "name": "UpdateTenantInput", + "description": null, + "fields": null, + "inputFields": [ + { + "name": "apiSecret", + "description": "Secret used to secure requests made for this tenant.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "email", + "description": "Contact email of the tenant owner.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "id", + "description": "Unique identifier of the tenant.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "idpConsentUrl", + "description": "URL of the tenant's identity provider's consent screen.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "idpSecret", + "description": "Secret used to secure requests from the tenant's identity provider.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "publicName", + "description": "Public name for the tenant.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, { "kind": "INPUT_OBJECT", "name": "UpdateWalletAddressInput", @@ -9158,6 +9843,49 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "OBJECT", + "name": "WhoamiResponse", + "description": null, + "fields": [ + { + "name": "id", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isOperator", + "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": "WithdrawEventLiquidityInput", diff --git a/packages/backend/src/graphql/generated/graphql.ts b/packages/backend/src/graphql/generated/graphql.ts index 46dad2dedb..0d37f19a34 100644 --- a/packages/backend/src/graphql/generated/graphql.ts +++ b/packages/backend/src/graphql/generated/graphql.ts @@ -364,6 +364,19 @@ export type CreateReceiverResponse = { receiver?: Maybe; }; +export type CreateTenantInput = { + /** Secret used to secure requests made for this tenant. */ + apiSecret: Scalars['String']['input']; + /** Contact email of the tenant owner. */ + email: Scalars['String']['input']; + /** URL of the tenant's identity provider's consent screen. */ + idpConsentUrl: Scalars['String']['input']; + /** Secret used to secure requests from the tenant's identity provider. */ + idpSecret: Scalars['String']['input']; + /** Public name for the tenant. */ + publicName?: InputMaybe; +}; + export type CreateWalletAddressInput = { /** Additional properties associated with the wallet address. */ additionalProperties?: InputMaybe>; @@ -440,6 +453,11 @@ export type DeletePeerMutationResponse = { success: Scalars['Boolean']['output']; }; +export type DeleteTenantMutationResponse = { + __typename?: 'DeleteTenantMutationResponse'; + success: Scalars['Boolean']['output']; +}; + export type DepositAssetLiquidityInput = { /** Amount of liquidity to deposit. */ amount: Scalars['UInt64']['input']; @@ -721,6 +739,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 a tenant. */ + createTenant: TenantMutationResponse; /** Create a new wallet address. */ createWalletAddress: CreateWalletAddressMutationResponse; /** Add a public key to a wallet address that is used to verify Open Payments requests. */ @@ -731,6 +751,8 @@ export type Mutation = { deleteAsset: DeleteAssetMutationResponse; /** Delete a peer. */ deletePeer: DeletePeerMutationResponse; + /** Delete a tenant. */ + deleteTenant: DeleteTenantMutationResponse; /** Deposit asset liquidity. */ depositAssetLiquidity?: Maybe; /** @@ -756,6 +778,8 @@ export type Mutation = { updateIncomingPayment: IncomingPaymentResponse; /** Update an existing peer. */ updatePeer: UpdatePeerMutationResponse; + /** Update a tenant. */ + updateTenant: TenantMutationResponse; /** Update an existing wallet address. */ updateWalletAddress: UpdateWalletAddressMutationResponse; /** Void liquidity withdrawal. Withdrawals are two-phase commits and are rolled back via this mutation. */ @@ -843,6 +867,11 @@ export type MutationCreateReceiverArgs = { }; +export type MutationCreateTenantArgs = { + input: CreateTenantInput; +}; + + export type MutationCreateWalletAddressArgs = { input: CreateWalletAddressInput; }; @@ -868,6 +897,11 @@ export type MutationDeletePeerArgs = { }; +export type MutationDeleteTenantArgs = { + id: Scalars['String']['input']; +}; + + export type MutationDepositAssetLiquidityArgs = { input: DepositAssetLiquidityInput; }; @@ -923,6 +957,11 @@ export type MutationUpdatePeerArgs = { }; +export type MutationUpdateTenantArgs = { + input: UpdateTenantInput; +}; + + export type MutationUpdateWalletAddressArgs = { input: UpdateWalletAddressInput; }; @@ -1150,6 +1189,10 @@ export type Query = { quote?: Maybe; /** Retrieve an Open Payments incoming payment by receiver ID. The receiver's wallet address can be hosted on this server or a remote Open Payments resource server. */ receiver?: Maybe; + /** Retrieve a tenant of the instance. */ + tenant?: Maybe; + /** Fetch a paginated list of tenants on the instance. */ + tenants: TenantsConnection; /** Fetch a wallet address by its ID. */ walletAddress?: Maybe; /** Get a wallet address by its url if it exists */ @@ -1158,6 +1201,8 @@ export type Query = { walletAddresses: WalletAddressesConnection; /** Fetch a paginated list of webhook events. */ webhookEvents: WebhookEventsConnection; + /** Determine if the requester has operator permissions */ + whoami: WhoamiResponse; }; @@ -1247,6 +1292,20 @@ export type QueryReceiverArgs = { }; +export type QueryTenantArgs = { + id: Scalars['String']['input']; +}; + + +export type QueryTenantsArgs = { + after?: InputMaybe; + before?: InputMaybe; + first?: InputMaybe; + last?: InputMaybe; + sortOrder?: InputMaybe; +}; + + export type QueryWalletAddressArgs = { id: Scalars['String']['input']; }; @@ -1376,6 +1435,47 @@ export enum SortOrder { Desc = 'DESC' } +export type Tenant = Model & { + __typename?: 'Tenant'; + /** Secret used to secure requests made for this tenant. */ + apiSecret: Scalars['String']['output']; + /** The date and time that this tenant was created. */ + createdAt: Scalars['String']['output']; + /** The date and time that this tenant was deleted. */ + deletedAt?: Maybe; + /** Contact email of the tenant owner. */ + email: Scalars['String']['output']; + /** Unique identifier of the tenant. */ + id: Scalars['ID']['output']; + /** URL of the tenant's identity provider's consent screen. */ + idpConsentUrl: Scalars['String']['output']; + /** Secret used to secure requests from the tenant's identity provider. */ + idpSecret: Scalars['String']['output']; + /** Public name for the tenant. */ + publicName?: Maybe; +}; + +export type TenantEdge = { + __typename?: 'TenantEdge'; + /** A cursor for paginating through the tenants. */ + cursor: Scalars['String']['output']; + /** A tenant node in the list. */ + node: Tenant; +}; + +export type TenantMutationResponse = { + __typename?: 'TenantMutationResponse'; + tenant: Tenant; +}; + +export type TenantsConnection = { + __typename?: 'TenantsConnection'; + /** A list of edges representing tenants and cursors for pagination. */ + edges: Array; + /** Information to aid in pagination. */ + pageInfo: PageInfo; +}; + export enum TransferState { /** The accounting transfer is pending */ Pending = 'PENDING', @@ -1448,6 +1548,21 @@ export type UpdatePeerMutationResponse = { peer?: Maybe; }; +export type UpdateTenantInput = { + /** Secret used to secure requests made for this tenant. */ + apiSecret?: InputMaybe; + /** Contact email of the tenant owner. */ + email?: InputMaybe; + /** Unique identifier of the tenant. */ + id: Scalars['ID']['input']; + /** URL of the tenant's identity provider's consent screen. */ + idpConsentUrl?: InputMaybe; + /** Secret used to secure requests from the tenant's identity provider. */ + idpSecret?: InputMaybe; + /** Public name for the tenant. */ + publicName?: InputMaybe; +}; + export type UpdateWalletAddressInput = { /** Additional properties associated with this wallet address. */ additionalProperties?: InputMaybe>; @@ -1640,6 +1755,12 @@ export type WebhookEventsEdge = { node: WebhookEvent; }; +export type WhoamiResponse = { + __typename?: 'WhoamiResponse'; + id: Scalars['String']['output']; + isOperator: Scalars['Boolean']['output']; +}; + export type WithdrawEventLiquidityInput = { /** Unique identifier of the event to withdraw liquidity from. */ eventId: Scalars['String']['input']; @@ -1718,7 +1839,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 */ @@ -1756,6 +1877,7 @@ export type ResolversTypes = { CreateQuoteInput: ResolverTypeWrapper>; CreateReceiverInput: ResolverTypeWrapper>; CreateReceiverResponse: ResolverTypeWrapper>; + CreateTenantInput: ResolverTypeWrapper>; CreateWalletAddressInput: ResolverTypeWrapper>; CreateWalletAddressKeyInput: ResolverTypeWrapper>; CreateWalletAddressKeyMutationResponse: ResolverTypeWrapper>; @@ -1766,6 +1888,7 @@ export type ResolversTypes = { DeleteAssetMutationResponse: ResolverTypeWrapper>; DeletePeerInput: ResolverTypeWrapper>; DeletePeerMutationResponse: ResolverTypeWrapper>; + DeleteTenantMutationResponse: ResolverTypeWrapper>; DepositAssetLiquidityInput: ResolverTypeWrapper>; DepositEventLiquidityInput: ResolverTypeWrapper>; DepositOutgoingPaymentLiquidityInput: ResolverTypeWrapper>; @@ -1825,6 +1948,10 @@ export type ResolversTypes = { SetFeeResponse: ResolverTypeWrapper>; SortOrder: ResolverTypeWrapper>; String: ResolverTypeWrapper>; + Tenant: ResolverTypeWrapper>; + TenantEdge: ResolverTypeWrapper>; + TenantMutationResponse: ResolverTypeWrapper>; + TenantsConnection: ResolverTypeWrapper>; TransferState: ResolverTypeWrapper>; TransferType: ResolverTypeWrapper>; TriggerWalletAddressEventsInput: ResolverTypeWrapper>; @@ -1835,6 +1962,7 @@ export type ResolversTypes = { UpdateIncomingPaymentInput: ResolverTypeWrapper>; UpdatePeerInput: ResolverTypeWrapper>; UpdatePeerMutationResponse: ResolverTypeWrapper>; + UpdateTenantInput: ResolverTypeWrapper>; UpdateWalletAddressInput: ResolverTypeWrapper>; UpdateWalletAddressMutationResponse: ResolverTypeWrapper>; VoidLiquidityWithdrawalInput: ResolverTypeWrapper>; @@ -1851,6 +1979,7 @@ export type ResolversTypes = { WebhookEventFilter: ResolverTypeWrapper>; WebhookEventsConnection: ResolverTypeWrapper>; WebhookEventsEdge: ResolverTypeWrapper>; + WhoamiResponse: ResolverTypeWrapper>; WithdrawEventLiquidityInput: ResolverTypeWrapper>; }; @@ -1888,6 +2017,7 @@ export type ResolversParentTypes = { CreateQuoteInput: Partial; CreateReceiverInput: Partial; CreateReceiverResponse: Partial; + CreateTenantInput: Partial; CreateWalletAddressInput: Partial; CreateWalletAddressKeyInput: Partial; CreateWalletAddressKeyMutationResponse: Partial; @@ -1897,6 +2027,7 @@ export type ResolversParentTypes = { DeleteAssetMutationResponse: Partial; DeletePeerInput: Partial; DeletePeerMutationResponse: Partial; + DeleteTenantMutationResponse: Partial; DepositAssetLiquidityInput: Partial; DepositEventLiquidityInput: Partial; DepositOutgoingPaymentLiquidityInput: Partial; @@ -1949,6 +2080,10 @@ export type ResolversParentTypes = { SetFeeInput: Partial; SetFeeResponse: Partial; String: Partial; + Tenant: Partial; + TenantEdge: Partial; + TenantMutationResponse: Partial; + TenantsConnection: Partial; TriggerWalletAddressEventsInput: Partial; TriggerWalletAddressEventsMutationResponse: Partial; UInt8: Partial; @@ -1957,6 +2092,7 @@ export type ResolversParentTypes = { UpdateIncomingPaymentInput: Partial; UpdatePeerInput: Partial; UpdatePeerMutationResponse: Partial; + UpdateTenantInput: Partial; UpdateWalletAddressInput: Partial; UpdateWalletAddressMutationResponse: Partial; VoidLiquidityWithdrawalInput: Partial; @@ -1972,6 +2108,7 @@ export type ResolversParentTypes = { WebhookEventFilter: Partial; WebhookEventsConnection: Partial; WebhookEventsEdge: Partial; + WhoamiResponse: Partial; WithdrawEventLiquidityInput: Partial; }; @@ -2093,6 +2230,11 @@ export type DeletePeerMutationResponseResolvers; }; +export type DeleteTenantMutationResponseResolvers = { + success?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type FeeResolvers = { assetId?: Resolver; basisPoints?: Resolver; @@ -2176,7 +2318,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; }; @@ -2197,11 +2339,13 @@ export type MutationResolvers, ParentType, ContextType, RequireFields>; createQuote?: Resolver>; createReceiver?: Resolver>; + createTenant?: Resolver>; createWalletAddress?: Resolver>; createWalletAddressKey?: Resolver, ParentType, ContextType, RequireFields>; createWalletAddressWithdrawal?: Resolver, ParentType, ContextType, RequireFields>; deleteAsset?: Resolver>; deletePeer?: Resolver>; + deleteTenant?: Resolver>; depositAssetLiquidity?: Resolver, ParentType, ContextType, RequireFields>; depositEventLiquidity?: Resolver, ParentType, ContextType, RequireFields>; depositOutgoingPaymentLiquidity?: Resolver, ParentType, ContextType, RequireFields>; @@ -2213,6 +2357,7 @@ export type MutationResolvers>; updateIncomingPayment?: Resolver>; updatePeer?: Resolver>; + updateTenant?: Resolver>; updateWalletAddress?: Resolver>; voidLiquidityWithdrawal?: Resolver, ParentType, ContextType, RequireFields>; withdrawEventLiquidity?: Resolver, ParentType, ContextType, RequireFields>; @@ -2325,10 +2470,13 @@ 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>; walletAddressByUrl?: Resolver, ParentType, ContextType, RequireFields>; walletAddresses?: Resolver>; webhookEvents?: Resolver>; + whoami?: Resolver; }; export type QuoteResolvers = { @@ -2383,6 +2531,35 @@ export type SetFeeResponseResolvers; }; +export type TenantResolvers = { + apiSecret?: Resolver; + createdAt?: Resolver; + deletedAt?: Resolver, ParentType, ContextType>; + email?: Resolver; + id?: Resolver; + idpConsentUrl?: Resolver; + idpSecret?: Resolver; + publicName?: Resolver, ParentType, ContextType>; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type TenantEdgeResolvers = { + cursor?: Resolver; + node?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type TenantMutationResponseResolvers = { + tenant?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type TenantsConnectionResolvers = { + edges?: Resolver, ParentType, ContextType>; + pageInfo?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type TriggerWalletAddressEventsMutationResponseResolvers = { count?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; @@ -2487,6 +2664,12 @@ export type WebhookEventsEdgeResolvers; }; +export type WhoamiResponseResolvers = { + id?: Resolver; + isOperator?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type Resolvers = { AccountingTransfer?: AccountingTransferResolvers; AccountingTransferConnection?: AccountingTransferConnectionResolvers; @@ -2506,6 +2689,7 @@ export type Resolvers = { CreateWalletAddressMutationResponse?: CreateWalletAddressMutationResponseResolvers; DeleteAssetMutationResponse?: DeleteAssetMutationResponseResolvers; DeletePeerMutationResponse?: DeletePeerMutationResponseResolvers; + DeleteTenantMutationResponse?: DeleteTenantMutationResponseResolvers; Fee?: FeeResolvers; FeeEdge?: FeeEdgeResolvers; FeesConnection?: FeesConnectionResolvers; @@ -2539,6 +2723,10 @@ export type Resolvers = { Receiver?: ReceiverResolvers; RevokeWalletAddressKeyMutationResponse?: RevokeWalletAddressKeyMutationResponseResolvers; SetFeeResponse?: SetFeeResponseResolvers; + Tenant?: TenantResolvers; + TenantEdge?: TenantEdgeResolvers; + TenantMutationResponse?: TenantMutationResponseResolvers; + TenantsConnection?: TenantsConnectionResolvers; TriggerWalletAddressEventsMutationResponse?: TriggerWalletAddressEventsMutationResponseResolvers; UInt8?: GraphQLScalarType; UInt64?: GraphQLScalarType; @@ -2555,5 +2743,6 @@ export type Resolvers = { WebhookEvent?: WebhookEventResolvers; WebhookEventsConnection?: WebhookEventsConnectionResolvers; WebhookEventsEdge?: WebhookEventsEdgeResolvers; + WhoamiResponse?: WhoamiResponseResolvers; }; diff --git a/packages/backend/src/graphql/resolvers/index.ts b/packages/backend/src/graphql/resolvers/index.ts index 2c191b30e8..343c07a475 100644 --- a/packages/backend/src/graphql/resolvers/index.ts +++ b/packages/backend/src/graphql/resolvers/index.ts @@ -77,6 +77,14 @@ import { GraphQLJSONObject } from 'graphql-scalars' import { getCombinedPayments } from './combined_payments' import { createOrUpdatePeerByUrl } from './auto-peering' import { getAccountingTransfers } from './accounting_transfer' +import { + whoami, + createTenant, + updateTenant, + deleteTenant, + getTenant, + getTenants +} from './tenant' export const resolvers: Resolvers = { UInt8: GraphQLUInt8, @@ -92,6 +100,7 @@ export const resolvers: Resolvers = { liquidity: getPeerLiquidity }, Query: { + whoami, walletAddress: getWalletAddress, walletAddressByUrl: getWalletAddressByUrl, walletAddresses: getWalletAddresses, @@ -108,7 +117,9 @@ export const resolvers: Resolvers = { webhookEvents: getWebhookEvents, payments: getCombinedPayments, accountingTransfers: getAccountingTransfers, - receiver: getReceiver + receiver: getReceiver, + tenant: getTenant, + tenants: getTenants }, WalletAddress: { liquidity: getWalletAddressLiquidity, @@ -161,6 +172,9 @@ export const resolvers: Resolvers = { createIncomingPaymentWithdrawal, createOutgoingPaymentWithdrawal, setFee, - updateIncomingPayment + updateIncomingPayment, + createTenant, + updateTenant, + deleteTenant } } diff --git a/packages/backend/src/graphql/resolvers/tenant.test.ts b/packages/backend/src/graphql/resolvers/tenant.test.ts new file mode 100644 index 0000000000..4882d8b7bd --- /dev/null +++ b/packages/backend/src/graphql/resolvers/tenant.test.ts @@ -0,0 +1,508 @@ +import { IocContract } from '@adonisjs/fold' +import { AppServices } from '../../app' +import { createTestApp, TestContainer } from '../../tests/app' +import { + DeleteTenantMutationResponse, + Tenant, + TenantMutationResponse, + TenantsConnection, + WhoamiResponse +} from '../generated/graphql' +import { initIocContainer } from '../..' +import { Config, IAppConfig } from '../../config/app' +import { createTenant, generateTenantInput } from '../../tests/tenant' +import { ApolloError, gql, NormalizedCacheObject } from '@apollo/client' +import { getPageTests } from './page.test' +import { truncateTables } from '../../tests/tableManager' +import nock from 'nock' +import { + createHttpLink, + ApolloLink, + ApolloClient, + InMemoryCache +} from '@apollo/client' +import { setContext } from '@apollo/client/link/context' +import { GraphQLErrorCode } from '../errors' +import { Tenant as TenantModel } from '../../tenants/model' + +function createTenantedApolloClient( + appContainer: TestContainer, + tenantId: string +): ApolloClient { + const httpLink = createHttpLink({ + uri: `http://localhost:${appContainer.app.getAdminPort()}/graphql`, + fetch + }) + const authLink = setContext((_, { headers }) => { + return { + headers: { + ...headers, + 'tenant-id': tenantId + } + } + }) + + const link = ApolloLink.from([authLink, httpLink]) + + return new ApolloClient({ + cache: new InMemoryCache({}), + link: link, + defaultOptions: { + query: { + fetchPolicy: 'no-cache' + }, + mutate: { + fetchPolicy: 'no-cache' + }, + watchQuery: { + fetchPolicy: 'no-cache' + } + } + }) +} + +describe('Tenant Resolvers', (): void => { + let deps: IocContract + let appContainer: TestContainer + let config: IAppConfig + + beforeAll(async (): Promise => { + deps = await initIocContainer(Config) + appContainer = await createTestApp(deps) + config = await deps.use('config') + }) + + afterEach(async (): Promise => { + await truncateTables(appContainer.knex) + }) + afterAll(async (): Promise => { + await appContainer.apolloClient.stop() + await appContainer.shutdown() + }) + + describe('whoami', (): void => { + test.each` + isOperator | description + ${true} | ${'operator'} + ${false} | ${'tenant'} + `('whoami query as $description', async ({ isOperator }): Promise => { + const tenant = await createTenant(deps) + const client = isOperator + ? appContainer.apolloClient + : createTenantedApolloClient(appContainer, tenant.id) + + const result = await client + .query({ + query: gql` + query Whoami { + whoami { + id + isOperator + } + } + ` + }) + .then((query): WhoamiResponse => query.data?.whoami) + + expect(result).toEqual({ + id: isOperator ? config.operatorTenantId : tenant.id, + isOperator, + __typename: 'WhoamiResponse' + }) + }) + }) + + describe('Query.tenant', (): void => { + describe('page tests', (): void => { + getPageTests({ + getClient: () => appContainer.apolloClient, + createModel: () => createTenant(deps), + pagedQuery: 'tenants' + }) + + test('Cannot get page as non-operator', async (): Promise => { + const tenant = await createTenant(deps) + const apolloClient = createTenantedApolloClient(appContainer, tenant.id) + try { + await apolloClient + .query({ + query: gql` + query GetTenants { + tenants { + edges { + node { + id + } + } + } + } + ` + }) + .then((query): TenantsConnection => query.data?.tenants) + } catch (error) { + expect(error).toBeInstanceOf(ApolloError) + expect((error as ApolloError).graphQLErrors).toContainEqual( + expect.objectContaining({ + message: 'cannot get tenants page', + extensions: expect.objectContaining({ + code: GraphQLErrorCode.Forbidden + }) + }) + ) + } + }) + }) + + test('can get tenant as operator', async (): Promise => { + const tenant = await createTenant(deps) + + const query = await appContainer.apolloClient + .query({ + query: gql` + query Tenant($id: String!) { + tenant(id: $id) { + id + email + } + } + `, + variables: { + id: tenant.id + } + }) + .then((query): Tenant => query.data?.tenant) + + expect(query).toEqual({ + id: tenant.id, + email: tenant.email, + __typename: 'Tenant' + }) + }) + + test('can get own tenant', async (): Promise => { + const tenant = await createTenant(deps) + const apolloClient = createTenantedApolloClient(appContainer, tenant.id) + + const query = await apolloClient + .query({ + query: gql` + query Tenant($id: String!) { + tenant(id: $id) { + id + email + } + } + `, + variables: { + id: tenant.id + } + }) + .then((query): Tenant => query.data?.tenant) + + expect(query).toEqual({ + id: tenant.id, + email: tenant.email, + __typename: 'Tenant' + }) + }) + + test('cannot get other tenant as non-operator', async (): Promise => { + const firstTenant = await createTenant(deps) + const secondTenant = await createTenant(deps) + + const apolloClient = createTenantedApolloClient( + appContainer, + firstTenant.id + ) + + try { + await apolloClient + .query({ + query: gql` + query Tenant($id: String!) { + tenant(id: $id) { + id + email + } + } + `, + variables: { + id: secondTenant.id + } + }) + .then((query): Tenant => query.data?.tenant) + } catch (error) { + expect(error).toBeInstanceOf(ApolloError) + expect((error as ApolloError).graphQLErrors).toContainEqual( + expect.objectContaining({ + message: 'tenant does not exist', + extensions: expect.objectContaining({ + code: GraphQLErrorCode.NotFound + }) + }) + ) + } + }) + }) + + describe('Mutations', (): void => { + describe('Create', (): void => { + test('can create a tenant', async (): Promise => { + const input = generateTenantInput() + const scope = nock(config.authAdminApiUrl) + .post('') + .reply(200, { data: { createTenant: { id: 1234 } } }) + + const mutation = await appContainer.apolloClient + .mutate({ + mutation: gql` + mutation CreateTenant($input: CreateTenantInput!) { + createTenant(input: $input) { + tenant { + id + email + apiSecret + idpConsentUrl + idpSecret + publicName + } + } + } + `, + variables: { + input + } + }) + .then((query): TenantMutationResponse => query.data?.createTenant) + + expect(mutation.tenant).toEqual({ + ...input, + id: expect.any(String), + __typename: 'Tenant' + }) + scope.done() + }) + + test('cannot create tenant as non-operator', async (): Promise => { + const input = generateTenantInput() + const tenant = await createTenant(deps) + const apolloClient = createTenantedApolloClient(appContainer, tenant.id) + + try { + await apolloClient + .mutate({ + mutation: gql` + mutation CreateTenant($input: CreateTenantInput!) { + createTenant(input: $input) { + tenant { + id + email + apiSecret + idpConsentUrl + idpSecret + publicName + } + } + } + `, + variables: { + input + } + }) + .then((query): TenantMutationResponse => query.data?.createTenant) + } catch (error) { + expect(error).toBeInstanceOf(ApolloError) + expect((error as ApolloError).graphQLErrors).toContainEqual( + expect.objectContaining({ + message: 'permission denied', + extensions: expect.objectContaining({ + code: GraphQLErrorCode.Forbidden + }) + }) + ) + } + }) + }) + describe('Update', (): void => { + let tenantedApolloClient: ApolloClient + let tenant: TenantModel + beforeEach(async (): Promise => { + tenant = await createTenant(deps) + tenantedApolloClient = createTenantedApolloClient( + appContainer, + tenant.id + ) + }) + + afterEach(async (): Promise => { + await truncateTables(appContainer.knex) + }) + + test.each` + isOperator | description + ${true} | ${'operator'} + ${false} | ${'tenant'} + `( + 'can update a tenant as $description', + async ({ isOperator }): Promise => { + const client = isOperator + ? appContainer.apolloClient + : tenantedApolloClient + const updateInput = { + ...generateTenantInput(), + id: tenant.id + } + + const scope = nock(config.authAdminApiUrl) + .post('') + .reply(200, { data: { updateTenant: { id: tenant.id } } }) + const mutation = await client + .mutate({ + mutation: gql` + mutation UpdateTenant($input: UpdateTenantInput!) { + updateTenant(input: $input) { + tenant { + id + email + apiSecret + idpConsentUrl + idpSecret + publicName + } + } + } + `, + variables: { + input: updateInput + } + }) + .then((query): TenantMutationResponse => query.data?.updateTenant) + + scope.done() + expect(mutation.tenant).toEqual({ + ...updateInput, + __typename: 'Tenant' + }) + } + ) + test('Cannot update other tenant as non-operator', async (): Promise => { + const firstTenant = await createTenant(deps) + const secondTenant = await createTenant(deps) + + const updateInput = { + ...generateTenantInput(), + id: secondTenant.id + } + const client = createTenantedApolloClient(appContainer, firstTenant.id) + try { + await client + .mutate({ + mutation: gql` + mutation UpdateTenant($input: UpdateTenantInput!) { + updateTenant(input: $input) { + tenant { + id + email + apiSecret + idpConsentUrl + idpSecret + publicName + } + } + } + `, + variables: { + input: updateInput + } + }) + .then((query): TenantMutationResponse => query.data?.updateTenant) + } catch (error) { + expect(error).toBeInstanceOf(ApolloError) + expect((error as ApolloError).graphQLErrors).toContainEqual( + expect.objectContaining({ + message: 'tenant does not exist', + extensions: expect.objectContaining({ + code: GraphQLErrorCode.NotFound + }) + }) + ) + } + }) + }) + + describe('Delete', (): void => { + test.each` + isOperator | description + ${true} | ${'operator'} + ${false} | ${'tenant'} + `( + 'Can delete a tenant as $description', + async ({ isOperator }): Promise => { + const tenant = await createTenant(deps) + + const client = isOperator + ? appContainer.apolloClient + : createTenantedApolloClient(appContainer, tenant.id) + const scope = nock(config.authAdminApiUrl) + .post('') + .reply(200, { data: { deleteTenant: { id: tenant.id } } }) + const mutation = await client + .mutate({ + mutation: gql` + mutation DeleteTenant($id: String!) { + deleteTenant(id: $id) { + success + } + } + `, + variables: { + id: tenant.id + } + }) + .then( + (query): DeleteTenantMutationResponse => query.data?.deleteTenant + ) + + scope.done() + expect(mutation.success).toBe(true) + } + ) + + test('Cannot delete other tenant as non-operator', async (): Promise => { + const firstTenant = await createTenant(deps) + const secondTenant = await createTenant(deps) + + const client = createTenantedApolloClient(appContainer, secondTenant.id) + + try { + await client + .mutate({ + mutation: gql` + mutation DeleteTenant($id: String!) { + deleteTenant(id: $id) { + success + } + } + `, + variables: { + id: firstTenant.id + } + }) + .then( + (query): DeleteTenantMutationResponse => query.data?.deleteTenant + ) + } catch (error) { + expect(error).toBeInstanceOf(ApolloError) + expect((error as ApolloError).graphQLErrors).toContainEqual( + expect.objectContaining({ + message: 'tenant does not exist', + extensions: expect.objectContaining({ + code: GraphQLErrorCode.NotFound + }) + }) + ) + } + }) + }) + }) +}) diff --git a/packages/backend/src/graphql/resolvers/tenant.ts b/packages/backend/src/graphql/resolvers/tenant.ts new file mode 100644 index 0000000000..6d27940a4e --- /dev/null +++ b/packages/backend/src/graphql/resolvers/tenant.ts @@ -0,0 +1,179 @@ +import { GraphQLError } from 'graphql' +import { TenantedApolloContext } from '../../app' +import { + MutationResolvers, + QueryResolvers, + ResolversTypes, + Tenant as SchemaTenant +} from '../generated/graphql' +import { GraphQLErrorCode } from '../errors' +import { Tenant } from '../../tenants/model' +import { Pagination, SortOrder } from '../../shared/baseModel' +import { getPageInfo } from '../../shared/pagination' + +export const whoami: QueryResolvers['whoami'] = async ( + parent, + args, + ctx +): Promise => { + const { tenant, isOperator } = ctx + + return { + id: tenant.id, + isOperator + } +} + +export const getTenant: QueryResolvers['tenant'] = + async (parent, args, ctx): Promise => { + const { tenant: contextTenant, isOperator } = ctx + + // TODO: make this a util + // If the tenant that was authorized in the request is not the tenant being requested, + // or the requester is not the operator, return not found + if (args.id !== contextTenant.id && !isOperator) { + throw new GraphQLError('tenant does not exist', { + extensions: { + code: GraphQLErrorCode.NotFound + } + }) + } + + const tenantService = await ctx.container.use('tenantService') + const tenant = await tenantService.get(args.id) + if (!tenant) { + throw new GraphQLError('tenant does not exist', { + extensions: { + code: GraphQLErrorCode.NotFound + } + }) + } + + return tenantToGraphQl(tenant) + } + +export const getTenants: QueryResolvers['tenants'] = + async (parent, args, ctx): Promise => { + const { isOperator } = ctx + if (!isOperator) { + throw new GraphQLError('cannot get tenants page', { + extensions: { + code: GraphQLErrorCode.Forbidden + } + }) + } + + const { sortOrder, ...pagination } = args + const order = sortOrder === 'ASC' ? SortOrder.Asc : SortOrder.Desc + const tenantService = await ctx.container.use('tenantService') + + 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 createTenant: MutationResolvers['createTenant'] = + async ( + parent, + args, + ctx + ): Promise => { + // createTenant is an operator-only resolver + const { isOperator } = ctx + if (!isOperator) { + throw new GraphQLError('permission denied', { + extensions: { + code: GraphQLErrorCode.Forbidden + } + }) + } + + const tenantService = await ctx.container.use('tenantService') + const tenant = await tenantService.create(args.input) + + return { tenant: tenantToGraphQl(tenant) } + } + +export const updateTenant: MutationResolvers['updateTenant'] = + async ( + parent, + args, + ctx + ): Promise => { + const { tenant: contextTenant, isOperator } = ctx + // TODO: make this a util + if (args.input.id !== contextTenant.id && !isOperator) { + throw new GraphQLError('tenant does not exist', { + extensions: { + code: GraphQLErrorCode.NotFound + } + }) + } + + const tenantService = await ctx.container.use('tenantService') + try { + const updatedTenant = await tenantService.update(args.input) + return { tenant: tenantToGraphQl(updatedTenant) } + } catch (err) { + throw new GraphQLError('failed to update tenant', { + extensions: { + code: GraphQLErrorCode.NotFound + } + }) + } + } + +export const deleteTenant: MutationResolvers['deleteTenant'] = + async ( + parent, + args, + ctx + ): Promise => { + const { tenant: contextTenant, isOperator } = ctx + if (args.id !== contextTenant.id && !isOperator) { + throw new GraphQLError('tenant does not exist', { + extensions: { + code: GraphQLErrorCode.NotFound + } + }) + } + + const tenantService = await ctx.container.use('tenantService') + try { + await tenantService.delete(args.id) + return { success: true } + } catch (err) { + throw new GraphQLError('failed to delete tenant', { + extensions: { + code: GraphQLErrorCode.NotFound + } + }) + } + } + +export function tenantToGraphQl(tenant: Tenant): SchemaTenant { + return { + id: tenant.id, + email: tenant.email, + apiSecret: tenant.apiSecret, + idpConsentUrl: tenant.idpConsentUrl, + idpSecret: tenant.idpSecret, + publicName: tenant.publicName, + createdAt: new Date(+tenant.createdAt).toISOString(), + deletedAt: tenant.deletedAt + ? new Date(+tenant.deletedAt).toISOString() + : null + } +} diff --git a/packages/backend/src/graphql/schema.graphql b/packages/backend/src/graphql/schema.graphql index f8286e14d4..958c9e9626 100644 --- a/packages/backend/src/graphql/schema.graphql +++ b/packages/backend/src/graphql/schema.graphql @@ -148,6 +148,26 @@ type Query { "Unique identifier of the receiver (incoming payment URL)." id: String! ): Receiver + + "Retrieve a tenant of the instance." + tenant("Unique identifier of the tenant." id: String!): Tenant + + "Fetch a paginated list of tenants on the instance." + tenants( + "Forward pagination: Cursor (tenant ID) to start retrieving tenants after this point." + after: String + "Backward pagination: Cursor (tenant ID) to start retrieving tenants before this point." + before: String + "Forward pagination: Limit the result to the first **n** tenants after the `after` cursor." + first: Int + "Backward pagination: Limit the result to the last **n** tenants before the `before` cursor." + last: Int + "Specify the sort order of tenants based on their creation date, either ascending or descending." + sortOrder: SortOrder + ): TenantsConnection! + + "Determine if the requester has operator permissions" + whoami: WhoamiResponse! } type Mutation { @@ -306,6 +326,15 @@ type Mutation { cancelIncomingPayment( input: CancelIncomingPaymentInput! ): CancelIncomingPaymentResponse! + + "Create a tenant." + createTenant(input: CreateTenantInput!): TenantMutationResponse! + + "Update a tenant." + updateTenant(input: UpdateTenantInput!): TenantMutationResponse! + + "Delete a tenant." + deleteTenant(id: String!): DeleteTenantMutationResponse! } type PageInfo { @@ -319,6 +348,11 @@ type PageInfo { startCursor: String } +type WhoamiResponse { + id: String! + isOperator: Boolean! +} + type AssetsConnection { "Information to aid in pagination." pageInfo: PageInfo! @@ -1493,6 +1527,75 @@ type CancelIncomingPaymentResponse { payment: IncomingPayment } +type Tenant implements Model { + "Unique identifier of the tenant." + id: ID! + "Contact email of the tenant owner." + email: String! + "Secret used to secure requests made for this tenant." + apiSecret: String! + "URL of the tenant's identity provider's consent screen." + idpConsentUrl: String! + "Secret used to secure requests from the tenant's identity provider." + idpSecret: String! + "Public name for the tenant." + publicName: String + "The date and time that this tenant was created." + createdAt: String! + "The date and time that this tenant was deleted." + deletedAt: String +} + +type TenantsConnection { + "Information to aid in pagination." + pageInfo: PageInfo! + "A list of edges representing tenants and cursors for pagination." + edges: [TenantEdge!]! +} + +type TenantEdge { + "A tenant node in the list." + node: Tenant! + "A cursor for paginating through the tenants." + cursor: String! +} + +input CreateTenantInput { + "Contact email of the tenant owner." + email: String! + "Secret used to secure requests made for this tenant." + apiSecret: String! + "URL of the tenant's identity provider's consent screen." + idpConsentUrl: String! + "Secret used to secure requests from the tenant's identity provider." + idpSecret: String! + "Public name for the tenant." + publicName: String +} + +input UpdateTenantInput { + "Unique identifier of the tenant." + id: ID! + "Contact email of the tenant owner." + email: String + "Secret used to secure requests made for this tenant." + apiSecret: String + "URL of the tenant's identity provider's consent screen." + idpConsentUrl: String + "Secret used to secure requests from the tenant's identity provider." + idpSecret: String + "Public name for the tenant." + publicName: String +} + +type TenantMutationResponse { + tenant: Tenant! +} + +type DeleteTenantMutationResponse { + success: Boolean! +} + """ The `UInt8` scalar type represents unsigned 8-bit whole numeric values, ranging from 0 to 255. """ diff --git a/packages/backend/src/tests/app.ts b/packages/backend/src/tests/app.ts index 3642de6c1a..4f3d6bab52 100644 --- a/packages/backend/src/tests/app.ts +++ b/packages/backend/src/tests/app.ts @@ -14,6 +14,7 @@ import { start, gracefulShutdown } from '..' import { onError } from '@apollo/client/link/error' import { App, AppServices } from '../app' +import { Config } from '../config/app' export const testAccessToken = 'test-app-access' diff --git a/packages/backend/src/tests/tenant.ts b/packages/backend/src/tests/tenant.ts index 579735b73d..d8874b4573 100644 --- a/packages/backend/src/tests/tenant.ts +++ b/packages/backend/src/tests/tenant.ts @@ -11,6 +11,16 @@ interface CreateOptions { idpSecret: string } +export function generateTenantInput() { + return { + email: faker.internet.email(), + apiSecret: faker.string.alphanumeric(8), + idpConsentUrl: faker.internet.url(), + idpSecret: faker.string.alphanumeric(8), + publicName: faker.company.name() + } +} + export async function createTenant( deps: IocContract, options?: CreateOptions diff --git a/packages/frontend/app/generated/graphql.ts b/packages/frontend/app/generated/graphql.ts index 1116595860..d9a9050ed1 100644 --- a/packages/frontend/app/generated/graphql.ts +++ b/packages/frontend/app/generated/graphql.ts @@ -364,6 +364,19 @@ export type CreateReceiverResponse = { receiver?: Maybe; }; +export type CreateTenantInput = { + /** Secret used to secure requests made for this tenant. */ + apiSecret: Scalars['String']['input']; + /** Contact email of the tenant owner. */ + email: Scalars['String']['input']; + /** URL of the tenant's identity provider's consent screen. */ + idpConsentUrl: Scalars['String']['input']; + /** Secret used to secure requests from the tenant's identity provider. */ + idpSecret: Scalars['String']['input']; + /** Public name for the tenant. */ + publicName?: InputMaybe; +}; + export type CreateWalletAddressInput = { /** Additional properties associated with the wallet address. */ additionalProperties?: InputMaybe>; @@ -440,6 +453,11 @@ export type DeletePeerMutationResponse = { success: Scalars['Boolean']['output']; }; +export type DeleteTenantMutationResponse = { + __typename?: 'DeleteTenantMutationResponse'; + success: Scalars['Boolean']['output']; +}; + export type DepositAssetLiquidityInput = { /** Amount of liquidity to deposit. */ amount: Scalars['UInt64']['input']; @@ -721,6 +739,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 a tenant. */ + createTenant: TenantMutationResponse; /** Create a new wallet address. */ createWalletAddress: CreateWalletAddressMutationResponse; /** Add a public key to a wallet address that is used to verify Open Payments requests. */ @@ -731,6 +751,8 @@ export type Mutation = { deleteAsset: DeleteAssetMutationResponse; /** Delete a peer. */ deletePeer: DeletePeerMutationResponse; + /** Delete a tenant. */ + deleteTenant: DeleteTenantMutationResponse; /** Deposit asset liquidity. */ depositAssetLiquidity?: Maybe; /** @@ -756,6 +778,8 @@ export type Mutation = { updateIncomingPayment: IncomingPaymentResponse; /** Update an existing peer. */ updatePeer: UpdatePeerMutationResponse; + /** Update a tenant. */ + updateTenant: TenantMutationResponse; /** Update an existing wallet address. */ updateWalletAddress: UpdateWalletAddressMutationResponse; /** Void liquidity withdrawal. Withdrawals are two-phase commits and are rolled back via this mutation. */ @@ -843,6 +867,11 @@ export type MutationCreateReceiverArgs = { }; +export type MutationCreateTenantArgs = { + input: CreateTenantInput; +}; + + export type MutationCreateWalletAddressArgs = { input: CreateWalletAddressInput; }; @@ -868,6 +897,11 @@ export type MutationDeletePeerArgs = { }; +export type MutationDeleteTenantArgs = { + id: Scalars['String']['input']; +}; + + export type MutationDepositAssetLiquidityArgs = { input: DepositAssetLiquidityInput; }; @@ -923,6 +957,11 @@ export type MutationUpdatePeerArgs = { }; +export type MutationUpdateTenantArgs = { + input: UpdateTenantInput; +}; + + export type MutationUpdateWalletAddressArgs = { input: UpdateWalletAddressInput; }; @@ -1150,6 +1189,10 @@ export type Query = { quote?: Maybe; /** Retrieve an Open Payments incoming payment by receiver ID. The receiver's wallet address can be hosted on this server or a remote Open Payments resource server. */ receiver?: Maybe; + /** Retrieve a tenant of the instance. */ + tenant?: Maybe; + /** Fetch a paginated list of tenants on the instance. */ + tenants: TenantsConnection; /** Fetch a wallet address by its ID. */ walletAddress?: Maybe; /** Get a wallet address by its url if it exists */ @@ -1158,6 +1201,8 @@ export type Query = { walletAddresses: WalletAddressesConnection; /** Fetch a paginated list of webhook events. */ webhookEvents: WebhookEventsConnection; + /** Determine if the requester has operator permissions */ + whoami: WhoamiResponse; }; @@ -1247,6 +1292,20 @@ export type QueryReceiverArgs = { }; +export type QueryTenantArgs = { + id: Scalars['String']['input']; +}; + + +export type QueryTenantsArgs = { + after?: InputMaybe; + before?: InputMaybe; + first?: InputMaybe; + last?: InputMaybe; + sortOrder?: InputMaybe; +}; + + export type QueryWalletAddressArgs = { id: Scalars['String']['input']; }; @@ -1376,6 +1435,47 @@ export enum SortOrder { Desc = 'DESC' } +export type Tenant = Model & { + __typename?: 'Tenant'; + /** Secret used to secure requests made for this tenant. */ + apiSecret: Scalars['String']['output']; + /** The date and time that this tenant was created. */ + createdAt: Scalars['String']['output']; + /** The date and time that this tenant was deleted. */ + deletedAt?: Maybe; + /** Contact email of the tenant owner. */ + email: Scalars['String']['output']; + /** Unique identifier of the tenant. */ + id: Scalars['ID']['output']; + /** URL of the tenant's identity provider's consent screen. */ + idpConsentUrl: Scalars['String']['output']; + /** Secret used to secure requests from the tenant's identity provider. */ + idpSecret: Scalars['String']['output']; + /** Public name for the tenant. */ + publicName?: Maybe; +}; + +export type TenantEdge = { + __typename?: 'TenantEdge'; + /** A cursor for paginating through the tenants. */ + cursor: Scalars['String']['output']; + /** A tenant node in the list. */ + node: Tenant; +}; + +export type TenantMutationResponse = { + __typename?: 'TenantMutationResponse'; + tenant: Tenant; +}; + +export type TenantsConnection = { + __typename?: 'TenantsConnection'; + /** A list of edges representing tenants and cursors for pagination. */ + edges: Array; + /** Information to aid in pagination. */ + pageInfo: PageInfo; +}; + export enum TransferState { /** The accounting transfer is pending */ Pending = 'PENDING', @@ -1448,6 +1548,21 @@ export type UpdatePeerMutationResponse = { peer?: Maybe; }; +export type UpdateTenantInput = { + /** Secret used to secure requests made for this tenant. */ + apiSecret?: InputMaybe; + /** Contact email of the tenant owner. */ + email?: InputMaybe; + /** Unique identifier of the tenant. */ + id: Scalars['ID']['input']; + /** URL of the tenant's identity provider's consent screen. */ + idpConsentUrl?: InputMaybe; + /** Secret used to secure requests from the tenant's identity provider. */ + idpSecret?: InputMaybe; + /** Public name for the tenant. */ + publicName?: InputMaybe; +}; + export type UpdateWalletAddressInput = { /** Additional properties associated with this wallet address. */ additionalProperties?: InputMaybe>; @@ -1640,6 +1755,12 @@ export type WebhookEventsEdge = { node: WebhookEvent; }; +export type WhoamiResponse = { + __typename?: 'WhoamiResponse'; + id: Scalars['String']['output']; + isOperator: Scalars['Boolean']['output']; +}; + export type WithdrawEventLiquidityInput = { /** Unique identifier of the event to withdraw liquidity from. */ eventId: Scalars['String']['input']; @@ -1718,7 +1839,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 */ @@ -1756,6 +1877,7 @@ export type ResolversTypes = { CreateQuoteInput: ResolverTypeWrapper>; CreateReceiverInput: ResolverTypeWrapper>; CreateReceiverResponse: ResolverTypeWrapper>; + CreateTenantInput: ResolverTypeWrapper>; CreateWalletAddressInput: ResolverTypeWrapper>; CreateWalletAddressKeyInput: ResolverTypeWrapper>; CreateWalletAddressKeyMutationResponse: ResolverTypeWrapper>; @@ -1766,6 +1888,7 @@ export type ResolversTypes = { DeleteAssetMutationResponse: ResolverTypeWrapper>; DeletePeerInput: ResolverTypeWrapper>; DeletePeerMutationResponse: ResolverTypeWrapper>; + DeleteTenantMutationResponse: ResolverTypeWrapper>; DepositAssetLiquidityInput: ResolverTypeWrapper>; DepositEventLiquidityInput: ResolverTypeWrapper>; DepositOutgoingPaymentLiquidityInput: ResolverTypeWrapper>; @@ -1825,6 +1948,10 @@ export type ResolversTypes = { SetFeeResponse: ResolverTypeWrapper>; SortOrder: ResolverTypeWrapper>; String: ResolverTypeWrapper>; + Tenant: ResolverTypeWrapper>; + TenantEdge: ResolverTypeWrapper>; + TenantMutationResponse: ResolverTypeWrapper>; + TenantsConnection: ResolverTypeWrapper>; TransferState: ResolverTypeWrapper>; TransferType: ResolverTypeWrapper>; TriggerWalletAddressEventsInput: ResolverTypeWrapper>; @@ -1835,6 +1962,7 @@ export type ResolversTypes = { UpdateIncomingPaymentInput: ResolverTypeWrapper>; UpdatePeerInput: ResolverTypeWrapper>; UpdatePeerMutationResponse: ResolverTypeWrapper>; + UpdateTenantInput: ResolverTypeWrapper>; UpdateWalletAddressInput: ResolverTypeWrapper>; UpdateWalletAddressMutationResponse: ResolverTypeWrapper>; VoidLiquidityWithdrawalInput: ResolverTypeWrapper>; @@ -1851,6 +1979,7 @@ export type ResolversTypes = { WebhookEventFilter: ResolverTypeWrapper>; WebhookEventsConnection: ResolverTypeWrapper>; WebhookEventsEdge: ResolverTypeWrapper>; + WhoamiResponse: ResolverTypeWrapper>; WithdrawEventLiquidityInput: ResolverTypeWrapper>; }; @@ -1888,6 +2017,7 @@ export type ResolversParentTypes = { CreateQuoteInput: Partial; CreateReceiverInput: Partial; CreateReceiverResponse: Partial; + CreateTenantInput: Partial; CreateWalletAddressInput: Partial; CreateWalletAddressKeyInput: Partial; CreateWalletAddressKeyMutationResponse: Partial; @@ -1897,6 +2027,7 @@ export type ResolversParentTypes = { DeleteAssetMutationResponse: Partial; DeletePeerInput: Partial; DeletePeerMutationResponse: Partial; + DeleteTenantMutationResponse: Partial; DepositAssetLiquidityInput: Partial; DepositEventLiquidityInput: Partial; DepositOutgoingPaymentLiquidityInput: Partial; @@ -1949,6 +2080,10 @@ export type ResolversParentTypes = { SetFeeInput: Partial; SetFeeResponse: Partial; String: Partial; + Tenant: Partial; + TenantEdge: Partial; + TenantMutationResponse: Partial; + TenantsConnection: Partial; TriggerWalletAddressEventsInput: Partial; TriggerWalletAddressEventsMutationResponse: Partial; UInt8: Partial; @@ -1957,6 +2092,7 @@ export type ResolversParentTypes = { UpdateIncomingPaymentInput: Partial; UpdatePeerInput: Partial; UpdatePeerMutationResponse: Partial; + UpdateTenantInput: Partial; UpdateWalletAddressInput: Partial; UpdateWalletAddressMutationResponse: Partial; VoidLiquidityWithdrawalInput: Partial; @@ -1972,6 +2108,7 @@ export type ResolversParentTypes = { WebhookEventFilter: Partial; WebhookEventsConnection: Partial; WebhookEventsEdge: Partial; + WhoamiResponse: Partial; WithdrawEventLiquidityInput: Partial; }; @@ -2093,6 +2230,11 @@ export type DeletePeerMutationResponseResolvers; }; +export type DeleteTenantMutationResponseResolvers = { + success?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type FeeResolvers = { assetId?: Resolver; basisPoints?: Resolver; @@ -2176,7 +2318,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; }; @@ -2197,11 +2339,13 @@ export type MutationResolvers, ParentType, ContextType, RequireFields>; createQuote?: Resolver>; createReceiver?: Resolver>; + createTenant?: Resolver>; createWalletAddress?: Resolver>; createWalletAddressKey?: Resolver, ParentType, ContextType, RequireFields>; createWalletAddressWithdrawal?: Resolver, ParentType, ContextType, RequireFields>; deleteAsset?: Resolver>; deletePeer?: Resolver>; + deleteTenant?: Resolver>; depositAssetLiquidity?: Resolver, ParentType, ContextType, RequireFields>; depositEventLiquidity?: Resolver, ParentType, ContextType, RequireFields>; depositOutgoingPaymentLiquidity?: Resolver, ParentType, ContextType, RequireFields>; @@ -2213,6 +2357,7 @@ export type MutationResolvers>; updateIncomingPayment?: Resolver>; updatePeer?: Resolver>; + updateTenant?: Resolver>; updateWalletAddress?: Resolver>; voidLiquidityWithdrawal?: Resolver, ParentType, ContextType, RequireFields>; withdrawEventLiquidity?: Resolver, ParentType, ContextType, RequireFields>; @@ -2325,10 +2470,13 @@ 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>; walletAddressByUrl?: Resolver, ParentType, ContextType, RequireFields>; walletAddresses?: Resolver>; webhookEvents?: Resolver>; + whoami?: Resolver; }; export type QuoteResolvers = { @@ -2383,6 +2531,35 @@ export type SetFeeResponseResolvers; }; +export type TenantResolvers = { + apiSecret?: Resolver; + createdAt?: Resolver; + deletedAt?: Resolver, ParentType, ContextType>; + email?: Resolver; + id?: Resolver; + idpConsentUrl?: Resolver; + idpSecret?: Resolver; + publicName?: Resolver, ParentType, ContextType>; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type TenantEdgeResolvers = { + cursor?: Resolver; + node?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type TenantMutationResponseResolvers = { + tenant?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type TenantsConnectionResolvers = { + edges?: Resolver, ParentType, ContextType>; + pageInfo?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type TriggerWalletAddressEventsMutationResponseResolvers = { count?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; @@ -2487,6 +2664,12 @@ export type WebhookEventsEdgeResolvers; }; +export type WhoamiResponseResolvers = { + id?: Resolver; + isOperator?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type Resolvers = { AccountingTransfer?: AccountingTransferResolvers; AccountingTransferConnection?: AccountingTransferConnectionResolvers; @@ -2506,6 +2689,7 @@ export type Resolvers = { CreateWalletAddressMutationResponse?: CreateWalletAddressMutationResponseResolvers; DeleteAssetMutationResponse?: DeleteAssetMutationResponseResolvers; DeletePeerMutationResponse?: DeletePeerMutationResponseResolvers; + DeleteTenantMutationResponse?: DeleteTenantMutationResponseResolvers; Fee?: FeeResolvers; FeeEdge?: FeeEdgeResolvers; FeesConnection?: FeesConnectionResolvers; @@ -2539,6 +2723,10 @@ export type Resolvers = { Receiver?: ReceiverResolvers; RevokeWalletAddressKeyMutationResponse?: RevokeWalletAddressKeyMutationResponseResolvers; SetFeeResponse?: SetFeeResponseResolvers; + Tenant?: TenantResolvers; + TenantEdge?: TenantEdgeResolvers; + TenantMutationResponse?: TenantMutationResponseResolvers; + TenantsConnection?: TenantsConnectionResolvers; TriggerWalletAddressEventsMutationResponse?: TriggerWalletAddressEventsMutationResponseResolvers; UInt8?: GraphQLScalarType; UInt64?: GraphQLScalarType; @@ -2555,6 +2743,7 @@ export type Resolvers = { WebhookEvent?: WebhookEventResolvers; WebhookEventsConnection?: WebhookEventsConnectionResolvers; WebhookEventsEdge?: WebhookEventsEdgeResolvers; + WhoamiResponse?: WhoamiResponseResolvers; }; diff --git a/packages/mock-account-service-lib/src/generated/graphql.ts b/packages/mock-account-service-lib/src/generated/graphql.ts index 46dad2dedb..0d37f19a34 100644 --- a/packages/mock-account-service-lib/src/generated/graphql.ts +++ b/packages/mock-account-service-lib/src/generated/graphql.ts @@ -364,6 +364,19 @@ export type CreateReceiverResponse = { receiver?: Maybe; }; +export type CreateTenantInput = { + /** Secret used to secure requests made for this tenant. */ + apiSecret: Scalars['String']['input']; + /** Contact email of the tenant owner. */ + email: Scalars['String']['input']; + /** URL of the tenant's identity provider's consent screen. */ + idpConsentUrl: Scalars['String']['input']; + /** Secret used to secure requests from the tenant's identity provider. */ + idpSecret: Scalars['String']['input']; + /** Public name for the tenant. */ + publicName?: InputMaybe; +}; + export type CreateWalletAddressInput = { /** Additional properties associated with the wallet address. */ additionalProperties?: InputMaybe>; @@ -440,6 +453,11 @@ export type DeletePeerMutationResponse = { success: Scalars['Boolean']['output']; }; +export type DeleteTenantMutationResponse = { + __typename?: 'DeleteTenantMutationResponse'; + success: Scalars['Boolean']['output']; +}; + export type DepositAssetLiquidityInput = { /** Amount of liquidity to deposit. */ amount: Scalars['UInt64']['input']; @@ -721,6 +739,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 a tenant. */ + createTenant: TenantMutationResponse; /** Create a new wallet address. */ createWalletAddress: CreateWalletAddressMutationResponse; /** Add a public key to a wallet address that is used to verify Open Payments requests. */ @@ -731,6 +751,8 @@ export type Mutation = { deleteAsset: DeleteAssetMutationResponse; /** Delete a peer. */ deletePeer: DeletePeerMutationResponse; + /** Delete a tenant. */ + deleteTenant: DeleteTenantMutationResponse; /** Deposit asset liquidity. */ depositAssetLiquidity?: Maybe; /** @@ -756,6 +778,8 @@ export type Mutation = { updateIncomingPayment: IncomingPaymentResponse; /** Update an existing peer. */ updatePeer: UpdatePeerMutationResponse; + /** Update a tenant. */ + updateTenant: TenantMutationResponse; /** Update an existing wallet address. */ updateWalletAddress: UpdateWalletAddressMutationResponse; /** Void liquidity withdrawal. Withdrawals are two-phase commits and are rolled back via this mutation. */ @@ -843,6 +867,11 @@ export type MutationCreateReceiverArgs = { }; +export type MutationCreateTenantArgs = { + input: CreateTenantInput; +}; + + export type MutationCreateWalletAddressArgs = { input: CreateWalletAddressInput; }; @@ -868,6 +897,11 @@ export type MutationDeletePeerArgs = { }; +export type MutationDeleteTenantArgs = { + id: Scalars['String']['input']; +}; + + export type MutationDepositAssetLiquidityArgs = { input: DepositAssetLiquidityInput; }; @@ -923,6 +957,11 @@ export type MutationUpdatePeerArgs = { }; +export type MutationUpdateTenantArgs = { + input: UpdateTenantInput; +}; + + export type MutationUpdateWalletAddressArgs = { input: UpdateWalletAddressInput; }; @@ -1150,6 +1189,10 @@ export type Query = { quote?: Maybe; /** Retrieve an Open Payments incoming payment by receiver ID. The receiver's wallet address can be hosted on this server or a remote Open Payments resource server. */ receiver?: Maybe; + /** Retrieve a tenant of the instance. */ + tenant?: Maybe; + /** Fetch a paginated list of tenants on the instance. */ + tenants: TenantsConnection; /** Fetch a wallet address by its ID. */ walletAddress?: Maybe; /** Get a wallet address by its url if it exists */ @@ -1158,6 +1201,8 @@ export type Query = { walletAddresses: WalletAddressesConnection; /** Fetch a paginated list of webhook events. */ webhookEvents: WebhookEventsConnection; + /** Determine if the requester has operator permissions */ + whoami: WhoamiResponse; }; @@ -1247,6 +1292,20 @@ export type QueryReceiverArgs = { }; +export type QueryTenantArgs = { + id: Scalars['String']['input']; +}; + + +export type QueryTenantsArgs = { + after?: InputMaybe; + before?: InputMaybe; + first?: InputMaybe; + last?: InputMaybe; + sortOrder?: InputMaybe; +}; + + export type QueryWalletAddressArgs = { id: Scalars['String']['input']; }; @@ -1376,6 +1435,47 @@ export enum SortOrder { Desc = 'DESC' } +export type Tenant = Model & { + __typename?: 'Tenant'; + /** Secret used to secure requests made for this tenant. */ + apiSecret: Scalars['String']['output']; + /** The date and time that this tenant was created. */ + createdAt: Scalars['String']['output']; + /** The date and time that this tenant was deleted. */ + deletedAt?: Maybe; + /** Contact email of the tenant owner. */ + email: Scalars['String']['output']; + /** Unique identifier of the tenant. */ + id: Scalars['ID']['output']; + /** URL of the tenant's identity provider's consent screen. */ + idpConsentUrl: Scalars['String']['output']; + /** Secret used to secure requests from the tenant's identity provider. */ + idpSecret: Scalars['String']['output']; + /** Public name for the tenant. */ + publicName?: Maybe; +}; + +export type TenantEdge = { + __typename?: 'TenantEdge'; + /** A cursor for paginating through the tenants. */ + cursor: Scalars['String']['output']; + /** A tenant node in the list. */ + node: Tenant; +}; + +export type TenantMutationResponse = { + __typename?: 'TenantMutationResponse'; + tenant: Tenant; +}; + +export type TenantsConnection = { + __typename?: 'TenantsConnection'; + /** A list of edges representing tenants and cursors for pagination. */ + edges: Array; + /** Information to aid in pagination. */ + pageInfo: PageInfo; +}; + export enum TransferState { /** The accounting transfer is pending */ Pending = 'PENDING', @@ -1448,6 +1548,21 @@ export type UpdatePeerMutationResponse = { peer?: Maybe; }; +export type UpdateTenantInput = { + /** Secret used to secure requests made for this tenant. */ + apiSecret?: InputMaybe; + /** Contact email of the tenant owner. */ + email?: InputMaybe; + /** Unique identifier of the tenant. */ + id: Scalars['ID']['input']; + /** URL of the tenant's identity provider's consent screen. */ + idpConsentUrl?: InputMaybe; + /** Secret used to secure requests from the tenant's identity provider. */ + idpSecret?: InputMaybe; + /** Public name for the tenant. */ + publicName?: InputMaybe; +}; + export type UpdateWalletAddressInput = { /** Additional properties associated with this wallet address. */ additionalProperties?: InputMaybe>; @@ -1640,6 +1755,12 @@ export type WebhookEventsEdge = { node: WebhookEvent; }; +export type WhoamiResponse = { + __typename?: 'WhoamiResponse'; + id: Scalars['String']['output']; + isOperator: Scalars['Boolean']['output']; +}; + export type WithdrawEventLiquidityInput = { /** Unique identifier of the event to withdraw liquidity from. */ eventId: Scalars['String']['input']; @@ -1718,7 +1839,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 */ @@ -1756,6 +1877,7 @@ export type ResolversTypes = { CreateQuoteInput: ResolverTypeWrapper>; CreateReceiverInput: ResolverTypeWrapper>; CreateReceiverResponse: ResolverTypeWrapper>; + CreateTenantInput: ResolverTypeWrapper>; CreateWalletAddressInput: ResolverTypeWrapper>; CreateWalletAddressKeyInput: ResolverTypeWrapper>; CreateWalletAddressKeyMutationResponse: ResolverTypeWrapper>; @@ -1766,6 +1888,7 @@ export type ResolversTypes = { DeleteAssetMutationResponse: ResolverTypeWrapper>; DeletePeerInput: ResolverTypeWrapper>; DeletePeerMutationResponse: ResolverTypeWrapper>; + DeleteTenantMutationResponse: ResolverTypeWrapper>; DepositAssetLiquidityInput: ResolverTypeWrapper>; DepositEventLiquidityInput: ResolverTypeWrapper>; DepositOutgoingPaymentLiquidityInput: ResolverTypeWrapper>; @@ -1825,6 +1948,10 @@ export type ResolversTypes = { SetFeeResponse: ResolverTypeWrapper>; SortOrder: ResolverTypeWrapper>; String: ResolverTypeWrapper>; + Tenant: ResolverTypeWrapper>; + TenantEdge: ResolverTypeWrapper>; + TenantMutationResponse: ResolverTypeWrapper>; + TenantsConnection: ResolverTypeWrapper>; TransferState: ResolverTypeWrapper>; TransferType: ResolverTypeWrapper>; TriggerWalletAddressEventsInput: ResolverTypeWrapper>; @@ -1835,6 +1962,7 @@ export type ResolversTypes = { UpdateIncomingPaymentInput: ResolverTypeWrapper>; UpdatePeerInput: ResolverTypeWrapper>; UpdatePeerMutationResponse: ResolverTypeWrapper>; + UpdateTenantInput: ResolverTypeWrapper>; UpdateWalletAddressInput: ResolverTypeWrapper>; UpdateWalletAddressMutationResponse: ResolverTypeWrapper>; VoidLiquidityWithdrawalInput: ResolverTypeWrapper>; @@ -1851,6 +1979,7 @@ export type ResolversTypes = { WebhookEventFilter: ResolverTypeWrapper>; WebhookEventsConnection: ResolverTypeWrapper>; WebhookEventsEdge: ResolverTypeWrapper>; + WhoamiResponse: ResolverTypeWrapper>; WithdrawEventLiquidityInput: ResolverTypeWrapper>; }; @@ -1888,6 +2017,7 @@ export type ResolversParentTypes = { CreateQuoteInput: Partial; CreateReceiverInput: Partial; CreateReceiverResponse: Partial; + CreateTenantInput: Partial; CreateWalletAddressInput: Partial; CreateWalletAddressKeyInput: Partial; CreateWalletAddressKeyMutationResponse: Partial; @@ -1897,6 +2027,7 @@ export type ResolversParentTypes = { DeleteAssetMutationResponse: Partial; DeletePeerInput: Partial; DeletePeerMutationResponse: Partial; + DeleteTenantMutationResponse: Partial; DepositAssetLiquidityInput: Partial; DepositEventLiquidityInput: Partial; DepositOutgoingPaymentLiquidityInput: Partial; @@ -1949,6 +2080,10 @@ export type ResolversParentTypes = { SetFeeInput: Partial; SetFeeResponse: Partial; String: Partial; + Tenant: Partial; + TenantEdge: Partial; + TenantMutationResponse: Partial; + TenantsConnection: Partial; TriggerWalletAddressEventsInput: Partial; TriggerWalletAddressEventsMutationResponse: Partial; UInt8: Partial; @@ -1957,6 +2092,7 @@ export type ResolversParentTypes = { UpdateIncomingPaymentInput: Partial; UpdatePeerInput: Partial; UpdatePeerMutationResponse: Partial; + UpdateTenantInput: Partial; UpdateWalletAddressInput: Partial; UpdateWalletAddressMutationResponse: Partial; VoidLiquidityWithdrawalInput: Partial; @@ -1972,6 +2108,7 @@ export type ResolversParentTypes = { WebhookEventFilter: Partial; WebhookEventsConnection: Partial; WebhookEventsEdge: Partial; + WhoamiResponse: Partial; WithdrawEventLiquidityInput: Partial; }; @@ -2093,6 +2230,11 @@ export type DeletePeerMutationResponseResolvers; }; +export type DeleteTenantMutationResponseResolvers = { + success?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type FeeResolvers = { assetId?: Resolver; basisPoints?: Resolver; @@ -2176,7 +2318,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; }; @@ -2197,11 +2339,13 @@ export type MutationResolvers, ParentType, ContextType, RequireFields>; createQuote?: Resolver>; createReceiver?: Resolver>; + createTenant?: Resolver>; createWalletAddress?: Resolver>; createWalletAddressKey?: Resolver, ParentType, ContextType, RequireFields>; createWalletAddressWithdrawal?: Resolver, ParentType, ContextType, RequireFields>; deleteAsset?: Resolver>; deletePeer?: Resolver>; + deleteTenant?: Resolver>; depositAssetLiquidity?: Resolver, ParentType, ContextType, RequireFields>; depositEventLiquidity?: Resolver, ParentType, ContextType, RequireFields>; depositOutgoingPaymentLiquidity?: Resolver, ParentType, ContextType, RequireFields>; @@ -2213,6 +2357,7 @@ export type MutationResolvers>; updateIncomingPayment?: Resolver>; updatePeer?: Resolver>; + updateTenant?: Resolver>; updateWalletAddress?: Resolver>; voidLiquidityWithdrawal?: Resolver, ParentType, ContextType, RequireFields>; withdrawEventLiquidity?: Resolver, ParentType, ContextType, RequireFields>; @@ -2325,10 +2470,13 @@ 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>; walletAddressByUrl?: Resolver, ParentType, ContextType, RequireFields>; walletAddresses?: Resolver>; webhookEvents?: Resolver>; + whoami?: Resolver; }; export type QuoteResolvers = { @@ -2383,6 +2531,35 @@ export type SetFeeResponseResolvers; }; +export type TenantResolvers = { + apiSecret?: Resolver; + createdAt?: Resolver; + deletedAt?: Resolver, ParentType, ContextType>; + email?: Resolver; + id?: Resolver; + idpConsentUrl?: Resolver; + idpSecret?: Resolver; + publicName?: Resolver, ParentType, ContextType>; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type TenantEdgeResolvers = { + cursor?: Resolver; + node?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type TenantMutationResponseResolvers = { + tenant?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type TenantsConnectionResolvers = { + edges?: Resolver, ParentType, ContextType>; + pageInfo?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type TriggerWalletAddressEventsMutationResponseResolvers = { count?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; @@ -2487,6 +2664,12 @@ export type WebhookEventsEdgeResolvers; }; +export type WhoamiResponseResolvers = { + id?: Resolver; + isOperator?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type Resolvers = { AccountingTransfer?: AccountingTransferResolvers; AccountingTransferConnection?: AccountingTransferConnectionResolvers; @@ -2506,6 +2689,7 @@ export type Resolvers = { CreateWalletAddressMutationResponse?: CreateWalletAddressMutationResponseResolvers; DeleteAssetMutationResponse?: DeleteAssetMutationResponseResolvers; DeletePeerMutationResponse?: DeletePeerMutationResponseResolvers; + DeleteTenantMutationResponse?: DeleteTenantMutationResponseResolvers; Fee?: FeeResolvers; FeeEdge?: FeeEdgeResolvers; FeesConnection?: FeesConnectionResolvers; @@ -2539,6 +2723,10 @@ export type Resolvers = { Receiver?: ReceiverResolvers; RevokeWalletAddressKeyMutationResponse?: RevokeWalletAddressKeyMutationResponseResolvers; SetFeeResponse?: SetFeeResponseResolvers; + Tenant?: TenantResolvers; + TenantEdge?: TenantEdgeResolvers; + TenantMutationResponse?: TenantMutationResponseResolvers; + TenantsConnection?: TenantsConnectionResolvers; TriggerWalletAddressEventsMutationResponse?: TriggerWalletAddressEventsMutationResponseResolvers; UInt8?: GraphQLScalarType; UInt64?: GraphQLScalarType; @@ -2555,5 +2743,6 @@ export type Resolvers = { WebhookEvent?: WebhookEventResolvers; WebhookEventsConnection?: WebhookEventsConnectionResolvers; WebhookEventsEdge?: WebhookEventsEdgeResolvers; + WhoamiResponse?: WhoamiResponseResolvers; }; diff --git a/test/integration/lib/generated/graphql.ts b/test/integration/lib/generated/graphql.ts index 46dad2dedb..0d37f19a34 100644 --- a/test/integration/lib/generated/graphql.ts +++ b/test/integration/lib/generated/graphql.ts @@ -364,6 +364,19 @@ export type CreateReceiverResponse = { receiver?: Maybe; }; +export type CreateTenantInput = { + /** Secret used to secure requests made for this tenant. */ + apiSecret: Scalars['String']['input']; + /** Contact email of the tenant owner. */ + email: Scalars['String']['input']; + /** URL of the tenant's identity provider's consent screen. */ + idpConsentUrl: Scalars['String']['input']; + /** Secret used to secure requests from the tenant's identity provider. */ + idpSecret: Scalars['String']['input']; + /** Public name for the tenant. */ + publicName?: InputMaybe; +}; + export type CreateWalletAddressInput = { /** Additional properties associated with the wallet address. */ additionalProperties?: InputMaybe>; @@ -440,6 +453,11 @@ export type DeletePeerMutationResponse = { success: Scalars['Boolean']['output']; }; +export type DeleteTenantMutationResponse = { + __typename?: 'DeleteTenantMutationResponse'; + success: Scalars['Boolean']['output']; +}; + export type DepositAssetLiquidityInput = { /** Amount of liquidity to deposit. */ amount: Scalars['UInt64']['input']; @@ -721,6 +739,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 a tenant. */ + createTenant: TenantMutationResponse; /** Create a new wallet address. */ createWalletAddress: CreateWalletAddressMutationResponse; /** Add a public key to a wallet address that is used to verify Open Payments requests. */ @@ -731,6 +751,8 @@ export type Mutation = { deleteAsset: DeleteAssetMutationResponse; /** Delete a peer. */ deletePeer: DeletePeerMutationResponse; + /** Delete a tenant. */ + deleteTenant: DeleteTenantMutationResponse; /** Deposit asset liquidity. */ depositAssetLiquidity?: Maybe; /** @@ -756,6 +778,8 @@ export type Mutation = { updateIncomingPayment: IncomingPaymentResponse; /** Update an existing peer. */ updatePeer: UpdatePeerMutationResponse; + /** Update a tenant. */ + updateTenant: TenantMutationResponse; /** Update an existing wallet address. */ updateWalletAddress: UpdateWalletAddressMutationResponse; /** Void liquidity withdrawal. Withdrawals are two-phase commits and are rolled back via this mutation. */ @@ -843,6 +867,11 @@ export type MutationCreateReceiverArgs = { }; +export type MutationCreateTenantArgs = { + input: CreateTenantInput; +}; + + export type MutationCreateWalletAddressArgs = { input: CreateWalletAddressInput; }; @@ -868,6 +897,11 @@ export type MutationDeletePeerArgs = { }; +export type MutationDeleteTenantArgs = { + id: Scalars['String']['input']; +}; + + export type MutationDepositAssetLiquidityArgs = { input: DepositAssetLiquidityInput; }; @@ -923,6 +957,11 @@ export type MutationUpdatePeerArgs = { }; +export type MutationUpdateTenantArgs = { + input: UpdateTenantInput; +}; + + export type MutationUpdateWalletAddressArgs = { input: UpdateWalletAddressInput; }; @@ -1150,6 +1189,10 @@ export type Query = { quote?: Maybe; /** Retrieve an Open Payments incoming payment by receiver ID. The receiver's wallet address can be hosted on this server or a remote Open Payments resource server. */ receiver?: Maybe; + /** Retrieve a tenant of the instance. */ + tenant?: Maybe; + /** Fetch a paginated list of tenants on the instance. */ + tenants: TenantsConnection; /** Fetch a wallet address by its ID. */ walletAddress?: Maybe; /** Get a wallet address by its url if it exists */ @@ -1158,6 +1201,8 @@ export type Query = { walletAddresses: WalletAddressesConnection; /** Fetch a paginated list of webhook events. */ webhookEvents: WebhookEventsConnection; + /** Determine if the requester has operator permissions */ + whoami: WhoamiResponse; }; @@ -1247,6 +1292,20 @@ export type QueryReceiverArgs = { }; +export type QueryTenantArgs = { + id: Scalars['String']['input']; +}; + + +export type QueryTenantsArgs = { + after?: InputMaybe; + before?: InputMaybe; + first?: InputMaybe; + last?: InputMaybe; + sortOrder?: InputMaybe; +}; + + export type QueryWalletAddressArgs = { id: Scalars['String']['input']; }; @@ -1376,6 +1435,47 @@ export enum SortOrder { Desc = 'DESC' } +export type Tenant = Model & { + __typename?: 'Tenant'; + /** Secret used to secure requests made for this tenant. */ + apiSecret: Scalars['String']['output']; + /** The date and time that this tenant was created. */ + createdAt: Scalars['String']['output']; + /** The date and time that this tenant was deleted. */ + deletedAt?: Maybe; + /** Contact email of the tenant owner. */ + email: Scalars['String']['output']; + /** Unique identifier of the tenant. */ + id: Scalars['ID']['output']; + /** URL of the tenant's identity provider's consent screen. */ + idpConsentUrl: Scalars['String']['output']; + /** Secret used to secure requests from the tenant's identity provider. */ + idpSecret: Scalars['String']['output']; + /** Public name for the tenant. */ + publicName?: Maybe; +}; + +export type TenantEdge = { + __typename?: 'TenantEdge'; + /** A cursor for paginating through the tenants. */ + cursor: Scalars['String']['output']; + /** A tenant node in the list. */ + node: Tenant; +}; + +export type TenantMutationResponse = { + __typename?: 'TenantMutationResponse'; + tenant: Tenant; +}; + +export type TenantsConnection = { + __typename?: 'TenantsConnection'; + /** A list of edges representing tenants and cursors for pagination. */ + edges: Array; + /** Information to aid in pagination. */ + pageInfo: PageInfo; +}; + export enum TransferState { /** The accounting transfer is pending */ Pending = 'PENDING', @@ -1448,6 +1548,21 @@ export type UpdatePeerMutationResponse = { peer?: Maybe; }; +export type UpdateTenantInput = { + /** Secret used to secure requests made for this tenant. */ + apiSecret?: InputMaybe; + /** Contact email of the tenant owner. */ + email?: InputMaybe; + /** Unique identifier of the tenant. */ + id: Scalars['ID']['input']; + /** URL of the tenant's identity provider's consent screen. */ + idpConsentUrl?: InputMaybe; + /** Secret used to secure requests from the tenant's identity provider. */ + idpSecret?: InputMaybe; + /** Public name for the tenant. */ + publicName?: InputMaybe; +}; + export type UpdateWalletAddressInput = { /** Additional properties associated with this wallet address. */ additionalProperties?: InputMaybe>; @@ -1640,6 +1755,12 @@ export type WebhookEventsEdge = { node: WebhookEvent; }; +export type WhoamiResponse = { + __typename?: 'WhoamiResponse'; + id: Scalars['String']['output']; + isOperator: Scalars['Boolean']['output']; +}; + export type WithdrawEventLiquidityInput = { /** Unique identifier of the event to withdraw liquidity from. */ eventId: Scalars['String']['input']; @@ -1718,7 +1839,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 */ @@ -1756,6 +1877,7 @@ export type ResolversTypes = { CreateQuoteInput: ResolverTypeWrapper>; CreateReceiverInput: ResolverTypeWrapper>; CreateReceiverResponse: ResolverTypeWrapper>; + CreateTenantInput: ResolverTypeWrapper>; CreateWalletAddressInput: ResolverTypeWrapper>; CreateWalletAddressKeyInput: ResolverTypeWrapper>; CreateWalletAddressKeyMutationResponse: ResolverTypeWrapper>; @@ -1766,6 +1888,7 @@ export type ResolversTypes = { DeleteAssetMutationResponse: ResolverTypeWrapper>; DeletePeerInput: ResolverTypeWrapper>; DeletePeerMutationResponse: ResolverTypeWrapper>; + DeleteTenantMutationResponse: ResolverTypeWrapper>; DepositAssetLiquidityInput: ResolverTypeWrapper>; DepositEventLiquidityInput: ResolverTypeWrapper>; DepositOutgoingPaymentLiquidityInput: ResolverTypeWrapper>; @@ -1825,6 +1948,10 @@ export type ResolversTypes = { SetFeeResponse: ResolverTypeWrapper>; SortOrder: ResolverTypeWrapper>; String: ResolverTypeWrapper>; + Tenant: ResolverTypeWrapper>; + TenantEdge: ResolverTypeWrapper>; + TenantMutationResponse: ResolverTypeWrapper>; + TenantsConnection: ResolverTypeWrapper>; TransferState: ResolverTypeWrapper>; TransferType: ResolverTypeWrapper>; TriggerWalletAddressEventsInput: ResolverTypeWrapper>; @@ -1835,6 +1962,7 @@ export type ResolversTypes = { UpdateIncomingPaymentInput: ResolverTypeWrapper>; UpdatePeerInput: ResolverTypeWrapper>; UpdatePeerMutationResponse: ResolverTypeWrapper>; + UpdateTenantInput: ResolverTypeWrapper>; UpdateWalletAddressInput: ResolverTypeWrapper>; UpdateWalletAddressMutationResponse: ResolverTypeWrapper>; VoidLiquidityWithdrawalInput: ResolverTypeWrapper>; @@ -1851,6 +1979,7 @@ export type ResolversTypes = { WebhookEventFilter: ResolverTypeWrapper>; WebhookEventsConnection: ResolverTypeWrapper>; WebhookEventsEdge: ResolverTypeWrapper>; + WhoamiResponse: ResolverTypeWrapper>; WithdrawEventLiquidityInput: ResolverTypeWrapper>; }; @@ -1888,6 +2017,7 @@ export type ResolversParentTypes = { CreateQuoteInput: Partial; CreateReceiverInput: Partial; CreateReceiverResponse: Partial; + CreateTenantInput: Partial; CreateWalletAddressInput: Partial; CreateWalletAddressKeyInput: Partial; CreateWalletAddressKeyMutationResponse: Partial; @@ -1897,6 +2027,7 @@ export type ResolversParentTypes = { DeleteAssetMutationResponse: Partial; DeletePeerInput: Partial; DeletePeerMutationResponse: Partial; + DeleteTenantMutationResponse: Partial; DepositAssetLiquidityInput: Partial; DepositEventLiquidityInput: Partial; DepositOutgoingPaymentLiquidityInput: Partial; @@ -1949,6 +2080,10 @@ export type ResolversParentTypes = { SetFeeInput: Partial; SetFeeResponse: Partial; String: Partial; + Tenant: Partial; + TenantEdge: Partial; + TenantMutationResponse: Partial; + TenantsConnection: Partial; TriggerWalletAddressEventsInput: Partial; TriggerWalletAddressEventsMutationResponse: Partial; UInt8: Partial; @@ -1957,6 +2092,7 @@ export type ResolversParentTypes = { UpdateIncomingPaymentInput: Partial; UpdatePeerInput: Partial; UpdatePeerMutationResponse: Partial; + UpdateTenantInput: Partial; UpdateWalletAddressInput: Partial; UpdateWalletAddressMutationResponse: Partial; VoidLiquidityWithdrawalInput: Partial; @@ -1972,6 +2108,7 @@ export type ResolversParentTypes = { WebhookEventFilter: Partial; WebhookEventsConnection: Partial; WebhookEventsEdge: Partial; + WhoamiResponse: Partial; WithdrawEventLiquidityInput: Partial; }; @@ -2093,6 +2230,11 @@ export type DeletePeerMutationResponseResolvers; }; +export type DeleteTenantMutationResponseResolvers = { + success?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type FeeResolvers = { assetId?: Resolver; basisPoints?: Resolver; @@ -2176,7 +2318,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; }; @@ -2197,11 +2339,13 @@ export type MutationResolvers, ParentType, ContextType, RequireFields>; createQuote?: Resolver>; createReceiver?: Resolver>; + createTenant?: Resolver>; createWalletAddress?: Resolver>; createWalletAddressKey?: Resolver, ParentType, ContextType, RequireFields>; createWalletAddressWithdrawal?: Resolver, ParentType, ContextType, RequireFields>; deleteAsset?: Resolver>; deletePeer?: Resolver>; + deleteTenant?: Resolver>; depositAssetLiquidity?: Resolver, ParentType, ContextType, RequireFields>; depositEventLiquidity?: Resolver, ParentType, ContextType, RequireFields>; depositOutgoingPaymentLiquidity?: Resolver, ParentType, ContextType, RequireFields>; @@ -2213,6 +2357,7 @@ export type MutationResolvers>; updateIncomingPayment?: Resolver>; updatePeer?: Resolver>; + updateTenant?: Resolver>; updateWalletAddress?: Resolver>; voidLiquidityWithdrawal?: Resolver, ParentType, ContextType, RequireFields>; withdrawEventLiquidity?: Resolver, ParentType, ContextType, RequireFields>; @@ -2325,10 +2470,13 @@ 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>; walletAddressByUrl?: Resolver, ParentType, ContextType, RequireFields>; walletAddresses?: Resolver>; webhookEvents?: Resolver>; + whoami?: Resolver; }; export type QuoteResolvers = { @@ -2383,6 +2531,35 @@ export type SetFeeResponseResolvers; }; +export type TenantResolvers = { + apiSecret?: Resolver; + createdAt?: Resolver; + deletedAt?: Resolver, ParentType, ContextType>; + email?: Resolver; + id?: Resolver; + idpConsentUrl?: Resolver; + idpSecret?: Resolver; + publicName?: Resolver, ParentType, ContextType>; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type TenantEdgeResolvers = { + cursor?: Resolver; + node?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type TenantMutationResponseResolvers = { + tenant?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type TenantsConnectionResolvers = { + edges?: Resolver, ParentType, ContextType>; + pageInfo?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type TriggerWalletAddressEventsMutationResponseResolvers = { count?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; @@ -2487,6 +2664,12 @@ export type WebhookEventsEdgeResolvers; }; +export type WhoamiResponseResolvers = { + id?: Resolver; + isOperator?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type Resolvers = { AccountingTransfer?: AccountingTransferResolvers; AccountingTransferConnection?: AccountingTransferConnectionResolvers; @@ -2506,6 +2689,7 @@ export type Resolvers = { CreateWalletAddressMutationResponse?: CreateWalletAddressMutationResponseResolvers; DeleteAssetMutationResponse?: DeleteAssetMutationResponseResolvers; DeletePeerMutationResponse?: DeletePeerMutationResponseResolvers; + DeleteTenantMutationResponse?: DeleteTenantMutationResponseResolvers; Fee?: FeeResolvers; FeeEdge?: FeeEdgeResolvers; FeesConnection?: FeesConnectionResolvers; @@ -2539,6 +2723,10 @@ export type Resolvers = { Receiver?: ReceiverResolvers; RevokeWalletAddressKeyMutationResponse?: RevokeWalletAddressKeyMutationResponseResolvers; SetFeeResponse?: SetFeeResponseResolvers; + Tenant?: TenantResolvers; + TenantEdge?: TenantEdgeResolvers; + TenantMutationResponse?: TenantMutationResponseResolvers; + TenantsConnection?: TenantsConnectionResolvers; TriggerWalletAddressEventsMutationResponse?: TriggerWalletAddressEventsMutationResponseResolvers; UInt8?: GraphQLScalarType; UInt64?: GraphQLScalarType; @@ -2555,5 +2743,6 @@ export type Resolvers = { WebhookEvent?: WebhookEventResolvers; WebhookEventsConnection?: WebhookEventsConnectionResolvers; WebhookEventsEdge?: WebhookEventsEdgeResolvers; + WhoamiResponse?: WhoamiResponseResolvers; }; From 344e21af432e26ce3db3fe83f8a687e105d14dab Mon Sep 17 00:00:00 2001 From: Nathan Lie Date: Tue, 21 Jan 2025 14:20:24 -0800 Subject: [PATCH 02/22] chore: formatting --- packages/backend/src/tests/app.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/backend/src/tests/app.ts b/packages/backend/src/tests/app.ts index 4f3d6bab52..3642de6c1a 100644 --- a/packages/backend/src/tests/app.ts +++ b/packages/backend/src/tests/app.ts @@ -14,7 +14,6 @@ import { start, gracefulShutdown } from '..' import { onError } from '@apollo/client/link/error' import { App, AppServices } from '../app' -import { Config } from '../config/app' export const testAccessToken = 'test-app-access' From 657780fa5c5d5600e4d9d4eba98e6bba52641197 Mon Sep 17 00:00:00 2001 From: Nathan Lie Date: Wed, 22 Jan 2025 14:12:55 -0800 Subject: [PATCH 03/22] fix: extra testing db for tenants --- packages/backend/jest.setup.ts | 31 +++++++++++++++++++ packages/backend/jest.teardown.js | 5 +++ packages/backend/knexfile.js | 16 ++++++++++ packages/backend/scripts/init.sh | 2 ++ .../src/graphql/resolvers/tenant.test.ts | 30 +++++++++--------- 5 files changed, 69 insertions(+), 15 deletions(-) diff --git a/packages/backend/jest.setup.ts b/packages/backend/jest.setup.ts index ef4340581d..a12bfa1585 100644 --- a/packages/backend/jest.setup.ts +++ b/packages/backend/jest.setup.ts @@ -34,6 +34,10 @@ const setup = async (globalConfig): Promise => { POSTGRES_PORT )}/testing` + process.env.TENANT_TEST_DATABASE_URL = `postgresql://postgres:password@localhost:${postgresContainer.getMappedPort( + POSTGRES_PORT + )}/tenant_testing` + global.__BACKEND_POSTGRES__ = postgresContainer } @@ -49,6 +53,18 @@ const setup = async (globalConfig): Promise => { } }) + const tenantDb = knex({ + client: 'postgresql', + connection: process.env.TENANT_TEST_DATABASE_URL, + pool: { + min: 2, + max: 10 + }, + migrations: { + tableName: 'knex_migrations' + } + }) + // node pg defaults to returning bigint as string. This ensures it parses to bigint db.client.driver.types.setTypeParser( db.client.driver.types.builtins.INT8, @@ -59,14 +75,29 @@ const setup = async (globalConfig): Promise => { directory: __dirname + '/migrations' }) + tenantDb.client.driver.types.setTypeParser( + tenantDb.client.driver.types.builtins.INT8, + 'text', + BigInt + ) + await tenantDb.migrate.latest({ + directory: __dirname + '/migrations' + }) + for (let i = 1; i <= workers; i++) { const workerDatabaseName = `testing_${i}` + const tenantWorkerDatabaseName = `tenant_testing_${i}` await db.raw(`DROP DATABASE IF EXISTS ${workerDatabaseName}`) + await tenantDb.raw(`DROP DATABASE IF EXISTS ${tenantWorkerDatabaseName}`) await db.raw(`CREATE DATABASE ${workerDatabaseName} TEMPLATE testing`) + await tenantDb.raw( + `CREATE DATABASE ${tenantWorkerDatabaseName} TEMPLATE tenant_testing` + ) } global.__BACKEND_KNEX__ = db + global.__BACKEND_TENANT_KNEX__ = tenantDb } const setupRedis = async () => { diff --git a/packages/backend/jest.teardown.js b/packages/backend/jest.teardown.js index 00020f301f..adc1aab423 100644 --- a/packages/backend/jest.teardown.js +++ b/packages/backend/jest.teardown.js @@ -3,7 +3,12 @@ module.exports = async () => { { directory: __dirname + '/migrations' }, true ) + await global.__BACKEND_TENANT_KNEX__.migrate.rollback( + { directory: __dirname + '/migrations' }, + true + ) await global.__BACKEND_KNEX__.destroy() + await global.__BACKEND_TENANT_KNEX__.destroy() if (global.__BACKEND_POSTGRES__) { await global.__BACKEND_POSTGRES__.stop() } diff --git a/packages/backend/knexfile.js b/packages/backend/knexfile.js index 6f8e2606ed..e036ca74c1 100644 --- a/packages/backend/knexfile.js +++ b/packages/backend/knexfile.js @@ -33,6 +33,22 @@ module.exports = { } }, + tenant_testing: { + client: 'postgresql', + connection: { + database: 'tenant_testing', + user: 'postgres', + password: 'password' + }, + pool: { + min: 2, + max: 10 + }, + migrations: { + tableName: 'knex_migrations' + } + }, + production: { client: 'postgresql', connection: process.env.DATABASE_URL, diff --git a/packages/backend/scripts/init.sh b/packages/backend/scripts/init.sh index a1fa255c7d..6f3b607aa9 100755 --- a/packages/backend/scripts/init.sh +++ b/packages/backend/scripts/init.sh @@ -3,6 +3,8 @@ set -e psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL DROP DATABASE IF EXISTS TESTING; + DROP DATABASE IF EXISTS TENANT_TESTING; CREATE DATABASE testing; + CREATE DATABASE tenant_testing; CREATE DATABASE development; EOSQL diff --git a/packages/backend/src/graphql/resolvers/tenant.test.ts b/packages/backend/src/graphql/resolvers/tenant.test.ts index 4882d8b7bd..50c54eaf89 100644 --- a/packages/backend/src/graphql/resolvers/tenant.test.ts +++ b/packages/backend/src/graphql/resolvers/tenant.test.ts @@ -14,7 +14,6 @@ import { createTenant, generateTenantInput } from '../../tests/tenant' import { ApolloError, gql, NormalizedCacheObject } from '@apollo/client' import { getPageTests } from './page.test' import { truncateTables } from '../../tests/tableManager' -import nock from 'nock' import { createHttpLink, ApolloLink, @@ -67,13 +66,26 @@ describe('Tenant Resolvers', (): void => { let config: IAppConfig beforeAll(async (): Promise => { - deps = await initIocContainer(Config) + deps = await initIocContainer({ + ...Config, + databaseUrl: process.env.TENANT_TEST_DATABASE_URL as string + }) appContainer = await createTestApp(deps) config = await deps.use('config') + const authServiceClient = await deps.use('authServiceClient') + jest + .spyOn(authServiceClient.tenant, 'create') + .mockImplementation(async () => undefined) + jest + .spyOn(authServiceClient.tenant, 'update') + .mockImplementation(async () => undefined) + jest + .spyOn(authServiceClient.tenant, 'delete') + .mockImplementation(async () => undefined) }) afterEach(async (): Promise => { - await truncateTables(appContainer.knex) + await truncateTables(appContainer.knex, true) }) afterAll(async (): Promise => { await appContainer.apolloClient.stop() @@ -249,9 +261,6 @@ describe('Tenant Resolvers', (): void => { describe('Create', (): void => { test('can create a tenant', async (): Promise => { const input = generateTenantInput() - const scope = nock(config.authAdminApiUrl) - .post('') - .reply(200, { data: { createTenant: { id: 1234 } } }) const mutation = await appContainer.apolloClient .mutate({ @@ -280,7 +289,6 @@ describe('Tenant Resolvers', (): void => { id: expect.any(String), __typename: 'Tenant' }) - scope.done() }) test('cannot create tenant as non-operator', async (): Promise => { @@ -353,9 +361,6 @@ describe('Tenant Resolvers', (): void => { id: tenant.id } - const scope = nock(config.authAdminApiUrl) - .post('') - .reply(200, { data: { updateTenant: { id: tenant.id } } }) const mutation = await client .mutate({ mutation: gql` @@ -378,7 +383,6 @@ describe('Tenant Resolvers', (): void => { }) .then((query): TenantMutationResponse => query.data?.updateTenant) - scope.done() expect(mutation.tenant).toEqual({ ...updateInput, __typename: 'Tenant' @@ -443,9 +447,6 @@ describe('Tenant Resolvers', (): void => { const client = isOperator ? appContainer.apolloClient : createTenantedApolloClient(appContainer, tenant.id) - const scope = nock(config.authAdminApiUrl) - .post('') - .reply(200, { data: { deleteTenant: { id: tenant.id } } }) const mutation = await client .mutate({ mutation: gql` @@ -463,7 +464,6 @@ describe('Tenant Resolvers', (): void => { (query): DeleteTenantMutationResponse => query.data?.deleteTenant ) - scope.done() expect(mutation.success).toBe(true) } ) From 3ce91282c3027747478c5183bbdf05f1ac7c635b Mon Sep 17 00:00:00 2001 From: Nathan Lie Date: Thu, 23 Jan 2025 14:21:55 -0800 Subject: [PATCH 04/22] feat: bruno collection --- .../Rafiki Admin APIs/Create Tenant.bru | 50 +++++++++++++++++++ .../Rafiki Admin APIs/Delete Tenant.bru | 31 ++++++++++++ .../Rafiki Admin APIs/Update Tenant.bru | 43 ++++++++++++++++ .../Rafiki/environments/Local Playground.bru | 1 + bruno/collections/Rafiki/scripts.js | 1 + 5 files changed, 126 insertions(+) create mode 100644 bruno/collections/Rafiki/Rafiki Admin APIs/Create Tenant.bru create mode 100644 bruno/collections/Rafiki/Rafiki Admin APIs/Delete Tenant.bru create mode 100644 bruno/collections/Rafiki/Rafiki Admin APIs/Update Tenant.bru 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..c078d15371 --- /dev/null +++ b/bruno/collections/Rafiki/Rafiki Admin APIs/Create Tenant.bru @@ -0,0 +1,50 @@ +meta { + name: Create Tenant + type: graphql + seq: 54 +} + +post { + url: {{RafikiGraphqlHost}}/graphql + body: graphql + auth: none +} + +body:graphql { + mutation CreateTenant($input: CreateTenantInput!) { + createTenant(input:$input) { + tenant { + id + email + apiSecret + idpConsentUrl + idpSecret + } + } + } +} + +body:graphql:vars { + { + "input": { + "email": "example@example.com", + "apiSecret": "test-secret", + "idpConsentUrl": "https://example.com/consent", + "idpSecret": "test-idp-secret" + } + } +} + +script:pre-request { + const scripts = require('./scripts'); + + scripts.addApiSignatureHeader(); +} + +script:post-response { + const body = res.getBody(); + + if (body?.data) { + bru.setEnvVar("tenantId", body.data.createTenant.tenant?.id); + } +} diff --git a/bruno/collections/Rafiki/Rafiki Admin APIs/Delete Tenant.bru b/bruno/collections/Rafiki/Rafiki Admin APIs/Delete Tenant.bru new file mode 100644 index 0000000000..6c664049f7 --- /dev/null +++ b/bruno/collections/Rafiki/Rafiki Admin APIs/Delete Tenant.bru @@ -0,0 +1,31 @@ +meta { + name: Delete Tenant + type: graphql + seq: 56 +} + +post { + url: {{RafikiGraphqlHost}}/graphql + body: graphql + auth: none +} + +body:graphql { + mutation DeleteTenant($id: String!) { + deleteTenant(id:$id) { + success + } + } +} + +body:graphql:vars { + { + "id": "{{tenantId}}" + } +} + +script:pre-request { + const scripts = require('./scripts'); + + scripts.addApiSignatureHeader(); +} diff --git a/bruno/collections/Rafiki/Rafiki Admin APIs/Update Tenant.bru b/bruno/collections/Rafiki/Rafiki Admin APIs/Update Tenant.bru new file mode 100644 index 0000000000..b3f71689d8 --- /dev/null +++ b/bruno/collections/Rafiki/Rafiki Admin APIs/Update Tenant.bru @@ -0,0 +1,43 @@ +meta { + name: Update Tenant + type: graphql + seq: 55 +} + +post { + url: {{RafikiGraphqlHost}}/graphql + body: graphql + auth: none +} + +body:graphql { + mutation UpdateTenant($input: UpdateTenantInput!) { + updateTenant(input:$input) { + tenant { + id + email + apiSecret + idpConsentUrl + idpSecret + } + } + } +} + +body:graphql:vars { + { + "input": { + "id": "{{tenantId}}", + "email": "updated@example.com", + "apiSecret": "updated-test-secret", + "idpConsentUrl": "https://example.com/consent-updated", + "idpSecret": "updated-test-idp-secret" + } + } +} + +script:pre-request { + const scripts = require('./scripts'); + + scripts.addApiSignatureHeader(); +} diff --git a/bruno/collections/Rafiki/environments/Local Playground.bru b/bruno/collections/Rafiki/environments/Local Playground.bru index 6cb1033ff6..24924232e4 100644 --- a/bruno/collections/Rafiki/environments/Local Playground.bru +++ b/bruno/collections/Rafiki/environments/Local Playground.bru @@ -30,4 +30,5 @@ vars { assetIdTigerBeetle: 1 assetCode: USD assetScale: 2 + senderTenantId: 438fa74a-fa7d-4317-9ced-dde32ece1787 } diff --git a/bruno/collections/Rafiki/scripts.js b/bruno/collections/Rafiki/scripts.js index 8ea6b7a614..ef03f9ab1d 100644 --- a/bruno/collections/Rafiki/scripts.js +++ b/bruno/collections/Rafiki/scripts.js @@ -127,6 +127,7 @@ const scripts = { signature = this.generateBackendApiSignature(formattedBody) } req.setHeader('signature', signature) + req.setHeader('tenant-id', bru.getEnvVar('senderTenantId')) }, addHostHeader: function (hostVarName) { From 15cb3876ffa49dbf7601d88e5d3c002c76652ea9 Mon Sep 17 00:00:00 2001 From: Nathan Lie Date: Fri, 24 Jan 2025 13:26:49 -0800 Subject: [PATCH 05/22] feat: update graphql schema comments --- localenv/mock-account-servicing-entity/generated/graphql.ts | 4 ++-- packages/backend/src/graphql/generated/graphql.schema.json | 4 ++-- packages/backend/src/graphql/generated/graphql.ts | 4 ++-- packages/backend/src/graphql/schema.graphql | 4 ++-- packages/frontend/app/generated/graphql.ts | 4 ++-- packages/mock-account-service-lib/src/generated/graphql.ts | 4 ++-- test/integration/lib/generated/graphql.ts | 4 ++-- 7 files changed, 14 insertions(+), 14 deletions(-) diff --git a/localenv/mock-account-servicing-entity/generated/graphql.ts b/localenv/mock-account-servicing-entity/generated/graphql.ts index 0d37f19a34..8d6aac067a 100644 --- a/localenv/mock-account-servicing-entity/generated/graphql.ts +++ b/localenv/mock-account-servicing-entity/generated/graphql.ts @@ -739,7 +739,7 @@ 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 a tenant. */ + /** As an operator, create a tenant. */ createTenant: TenantMutationResponse; /** Create a new wallet address. */ createWalletAddress: CreateWalletAddressMutationResponse; @@ -1191,7 +1191,7 @@ export type Query = { receiver?: Maybe; /** Retrieve a tenant of the instance. */ tenant?: Maybe; - /** Fetch a paginated list of tenants on the instance. */ + /** As an operator, fetch a paginated list of tenants on the instance. */ tenants: TenantsConnection; /** Fetch a wallet address by its ID. */ walletAddress?: Maybe; diff --git a/packages/backend/src/graphql/generated/graphql.schema.json b/packages/backend/src/graphql/generated/graphql.schema.json index c745f24e15..da5a837a0e 100644 --- a/packages/backend/src/graphql/generated/graphql.schema.json +++ b/packages/backend/src/graphql/generated/graphql.schema.json @@ -4610,7 +4610,7 @@ }, { "name": "createTenant", - "description": "Create a tenant.", + "description": "As an operator, create a tenant.", "args": [ { "name": "input", @@ -7059,7 +7059,7 @@ }, { "name": "tenants", - "description": "Fetch a paginated list of tenants on the instance.", + "description": "As an operator, fetch a paginated list of tenants on the instance.", "args": [ { "name": "after", diff --git a/packages/backend/src/graphql/generated/graphql.ts b/packages/backend/src/graphql/generated/graphql.ts index 0d37f19a34..8d6aac067a 100644 --- a/packages/backend/src/graphql/generated/graphql.ts +++ b/packages/backend/src/graphql/generated/graphql.ts @@ -739,7 +739,7 @@ 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 a tenant. */ + /** As an operator, create a tenant. */ createTenant: TenantMutationResponse; /** Create a new wallet address. */ createWalletAddress: CreateWalletAddressMutationResponse; @@ -1191,7 +1191,7 @@ export type Query = { receiver?: Maybe; /** Retrieve a tenant of the instance. */ tenant?: Maybe; - /** Fetch a paginated list of tenants on the instance. */ + /** As an operator, fetch a paginated list of tenants on the instance. */ tenants: TenantsConnection; /** Fetch a wallet address by its ID. */ walletAddress?: Maybe; diff --git a/packages/backend/src/graphql/schema.graphql b/packages/backend/src/graphql/schema.graphql index 958c9e9626..2e55d937c5 100644 --- a/packages/backend/src/graphql/schema.graphql +++ b/packages/backend/src/graphql/schema.graphql @@ -152,7 +152,7 @@ type Query { "Retrieve a tenant of the instance." tenant("Unique identifier of the tenant." id: String!): Tenant - "Fetch a paginated list of tenants on the instance." + "As an operator, fetch a paginated list of tenants on the instance." tenants( "Forward pagination: Cursor (tenant ID) to start retrieving tenants after this point." after: String @@ -327,7 +327,7 @@ type Mutation { input: CancelIncomingPaymentInput! ): CancelIncomingPaymentResponse! - "Create a tenant." + "As an operator, create a tenant." createTenant(input: CreateTenantInput!): TenantMutationResponse! "Update a tenant." diff --git a/packages/frontend/app/generated/graphql.ts b/packages/frontend/app/generated/graphql.ts index d9a9050ed1..4475460428 100644 --- a/packages/frontend/app/generated/graphql.ts +++ b/packages/frontend/app/generated/graphql.ts @@ -739,7 +739,7 @@ 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 a tenant. */ + /** As an operator, create a tenant. */ createTenant: TenantMutationResponse; /** Create a new wallet address. */ createWalletAddress: CreateWalletAddressMutationResponse; @@ -1191,7 +1191,7 @@ export type Query = { receiver?: Maybe; /** Retrieve a tenant of the instance. */ tenant?: Maybe; - /** Fetch a paginated list of tenants on the instance. */ + /** As an operator, fetch a paginated list of tenants on the instance. */ tenants: TenantsConnection; /** Fetch a wallet address by its ID. */ walletAddress?: Maybe; diff --git a/packages/mock-account-service-lib/src/generated/graphql.ts b/packages/mock-account-service-lib/src/generated/graphql.ts index 0d37f19a34..8d6aac067a 100644 --- a/packages/mock-account-service-lib/src/generated/graphql.ts +++ b/packages/mock-account-service-lib/src/generated/graphql.ts @@ -739,7 +739,7 @@ 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 a tenant. */ + /** As an operator, create a tenant. */ createTenant: TenantMutationResponse; /** Create a new wallet address. */ createWalletAddress: CreateWalletAddressMutationResponse; @@ -1191,7 +1191,7 @@ export type Query = { receiver?: Maybe; /** Retrieve a tenant of the instance. */ tenant?: Maybe; - /** Fetch a paginated list of tenants on the instance. */ + /** As an operator, fetch a paginated list of tenants on the instance. */ tenants: TenantsConnection; /** Fetch a wallet address by its ID. */ walletAddress?: Maybe; diff --git a/test/integration/lib/generated/graphql.ts b/test/integration/lib/generated/graphql.ts index 0d37f19a34..8d6aac067a 100644 --- a/test/integration/lib/generated/graphql.ts +++ b/test/integration/lib/generated/graphql.ts @@ -739,7 +739,7 @@ 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 a tenant. */ + /** As an operator, create a tenant. */ createTenant: TenantMutationResponse; /** Create a new wallet address. */ createWalletAddress: CreateWalletAddressMutationResponse; @@ -1191,7 +1191,7 @@ export type Query = { receiver?: Maybe; /** Retrieve a tenant of the instance. */ tenant?: Maybe; - /** Fetch a paginated list of tenants on the instance. */ + /** As an operator, fetch a paginated list of tenants on the instance. */ tenants: TenantsConnection; /** Fetch a wallet address by its ID. */ walletAddress?: Maybe; From 4aafebb9e3cc00a402416e94b1c43b518ba23ec0 Mon Sep 17 00:00:00 2001 From: Nathan Lie Date: Mon, 27 Jan 2025 11:25:04 -0800 Subject: [PATCH 06/22] fix: review comments --- .../generated/graphql.ts | 18 +++--- packages/backend/jest.setup.ts | 31 ---------- packages/backend/jest.teardown.js | 5 -- packages/backend/knexfile.js | 16 ----- .../src/graphql/generated/graphql.schema.json | 60 ++++++------------- .../backend/src/graphql/generated/graphql.ts | 18 +++--- .../src/graphql/resolvers/tenant.test.ts | 7 ++- packages/backend/src/graphql/schema.graphql | 12 ++-- packages/frontend/app/generated/graphql.ts | 18 +++--- .../src/generated/graphql.ts | 18 +++--- test/integration/lib/generated/graphql.ts | 18 +++--- 11 files changed, 75 insertions(+), 146 deletions(-) diff --git a/localenv/mock-account-servicing-entity/generated/graphql.ts b/localenv/mock-account-servicing-entity/generated/graphql.ts index 8d6aac067a..2fe726420e 100644 --- a/localenv/mock-account-servicing-entity/generated/graphql.ts +++ b/localenv/mock-account-servicing-entity/generated/graphql.ts @@ -368,11 +368,11 @@ export type CreateTenantInput = { /** Secret used to secure requests made for this tenant. */ apiSecret: Scalars['String']['input']; /** Contact email of the tenant owner. */ - email: Scalars['String']['input']; + email?: InputMaybe; /** URL of the tenant's identity provider's consent screen. */ - idpConsentUrl: Scalars['String']['input']; + idpConsentUrl?: InputMaybe; /** Secret used to secure requests from the tenant's identity provider. */ - idpSecret: Scalars['String']['input']; + idpSecret?: InputMaybe; /** Public name for the tenant. */ publicName?: InputMaybe; }; @@ -1444,13 +1444,13 @@ export type Tenant = Model & { /** The date and time that this tenant was deleted. */ deletedAt?: Maybe; /** Contact email of the tenant owner. */ - email: Scalars['String']['output']; + email?: Maybe; /** Unique identifier of the tenant. */ id: Scalars['ID']['output']; /** URL of the tenant's identity provider's consent screen. */ - idpConsentUrl: Scalars['String']['output']; + idpConsentUrl?: Maybe; /** Secret used to secure requests from the tenant's identity provider. */ - idpSecret: Scalars['String']['output']; + idpSecret?: Maybe; /** Public name for the tenant. */ publicName?: Maybe; }; @@ -2535,10 +2535,10 @@ export type TenantResolvers; createdAt?: Resolver; deletedAt?: Resolver, ParentType, ContextType>; - email?: Resolver; + email?: Resolver, ParentType, ContextType>; id?: Resolver; - idpConsentUrl?: Resolver; - idpSecret?: Resolver; + idpConsentUrl?: Resolver, ParentType, ContextType>; + idpSecret?: Resolver, ParentType, ContextType>; publicName?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; }; diff --git a/packages/backend/jest.setup.ts b/packages/backend/jest.setup.ts index a12bfa1585..ef4340581d 100644 --- a/packages/backend/jest.setup.ts +++ b/packages/backend/jest.setup.ts @@ -34,10 +34,6 @@ const setup = async (globalConfig): Promise => { POSTGRES_PORT )}/testing` - process.env.TENANT_TEST_DATABASE_URL = `postgresql://postgres:password@localhost:${postgresContainer.getMappedPort( - POSTGRES_PORT - )}/tenant_testing` - global.__BACKEND_POSTGRES__ = postgresContainer } @@ -53,18 +49,6 @@ const setup = async (globalConfig): Promise => { } }) - const tenantDb = knex({ - client: 'postgresql', - connection: process.env.TENANT_TEST_DATABASE_URL, - pool: { - min: 2, - max: 10 - }, - migrations: { - tableName: 'knex_migrations' - } - }) - // node pg defaults to returning bigint as string. This ensures it parses to bigint db.client.driver.types.setTypeParser( db.client.driver.types.builtins.INT8, @@ -75,29 +59,14 @@ const setup = async (globalConfig): Promise => { directory: __dirname + '/migrations' }) - tenantDb.client.driver.types.setTypeParser( - tenantDb.client.driver.types.builtins.INT8, - 'text', - BigInt - ) - await tenantDb.migrate.latest({ - directory: __dirname + '/migrations' - }) - for (let i = 1; i <= workers; i++) { const workerDatabaseName = `testing_${i}` - const tenantWorkerDatabaseName = `tenant_testing_${i}` await db.raw(`DROP DATABASE IF EXISTS ${workerDatabaseName}`) - await tenantDb.raw(`DROP DATABASE IF EXISTS ${tenantWorkerDatabaseName}`) await db.raw(`CREATE DATABASE ${workerDatabaseName} TEMPLATE testing`) - await tenantDb.raw( - `CREATE DATABASE ${tenantWorkerDatabaseName} TEMPLATE tenant_testing` - ) } global.__BACKEND_KNEX__ = db - global.__BACKEND_TENANT_KNEX__ = tenantDb } const setupRedis = async () => { diff --git a/packages/backend/jest.teardown.js b/packages/backend/jest.teardown.js index adc1aab423..00020f301f 100644 --- a/packages/backend/jest.teardown.js +++ b/packages/backend/jest.teardown.js @@ -3,12 +3,7 @@ module.exports = async () => { { directory: __dirname + '/migrations' }, true ) - await global.__BACKEND_TENANT_KNEX__.migrate.rollback( - { directory: __dirname + '/migrations' }, - true - ) await global.__BACKEND_KNEX__.destroy() - await global.__BACKEND_TENANT_KNEX__.destroy() if (global.__BACKEND_POSTGRES__) { await global.__BACKEND_POSTGRES__.stop() } diff --git a/packages/backend/knexfile.js b/packages/backend/knexfile.js index e036ca74c1..6f8e2606ed 100644 --- a/packages/backend/knexfile.js +++ b/packages/backend/knexfile.js @@ -33,22 +33,6 @@ module.exports = { } }, - tenant_testing: { - client: 'postgresql', - connection: { - database: 'tenant_testing', - user: 'postgres', - password: 'password' - }, - pool: { - min: 2, - max: 10 - }, - migrations: { - tableName: 'knex_migrations' - } - }, - production: { client: 'postgresql', connection: process.env.DATABASE_URL, diff --git a/packages/backend/src/graphql/generated/graphql.schema.json b/packages/backend/src/graphql/generated/graphql.schema.json index da5a837a0e..a843adf06c 100644 --- a/packages/backend/src/graphql/generated/graphql.schema.json +++ b/packages/backend/src/graphql/generated/graphql.schema.json @@ -2131,13 +2131,9 @@ "name": "email", "description": "Contact email of the tenant owner.", "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } + "kind": "SCALAR", + "name": "String", + "ofType": null }, "defaultValue": null, "isDeprecated": false, @@ -2147,13 +2143,9 @@ "name": "idpConsentUrl", "description": "URL of the tenant's identity provider's consent screen.", "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } + "kind": "SCALAR", + "name": "String", + "ofType": null }, "defaultValue": null, "isDeprecated": false, @@ -2163,13 +2155,9 @@ "name": "idpSecret", "description": "Secret used to secure requests from the tenant's identity provider.", "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } + "kind": "SCALAR", + "name": "String", + "ofType": null }, "defaultValue": null, "isDeprecated": false, @@ -8018,13 +8006,9 @@ "description": "Contact email of the tenant owner.", "args": [], "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } + "kind": "SCALAR", + "name": "String", + "ofType": null }, "isDeprecated": false, "deprecationReason": null @@ -8050,13 +8034,9 @@ "description": "URL of the tenant's identity provider's consent screen.", "args": [], "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } + "kind": "SCALAR", + "name": "String", + "ofType": null }, "isDeprecated": false, "deprecationReason": null @@ -8066,13 +8046,9 @@ "description": "Secret used to secure requests from the tenant's identity provider.", "args": [], "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } + "kind": "SCALAR", + "name": "String", + "ofType": null }, "isDeprecated": false, "deprecationReason": null diff --git a/packages/backend/src/graphql/generated/graphql.ts b/packages/backend/src/graphql/generated/graphql.ts index 8d6aac067a..2fe726420e 100644 --- a/packages/backend/src/graphql/generated/graphql.ts +++ b/packages/backend/src/graphql/generated/graphql.ts @@ -368,11 +368,11 @@ export type CreateTenantInput = { /** Secret used to secure requests made for this tenant. */ apiSecret: Scalars['String']['input']; /** Contact email of the tenant owner. */ - email: Scalars['String']['input']; + email?: InputMaybe; /** URL of the tenant's identity provider's consent screen. */ - idpConsentUrl: Scalars['String']['input']; + idpConsentUrl?: InputMaybe; /** Secret used to secure requests from the tenant's identity provider. */ - idpSecret: Scalars['String']['input']; + idpSecret?: InputMaybe; /** Public name for the tenant. */ publicName?: InputMaybe; }; @@ -1444,13 +1444,13 @@ export type Tenant = Model & { /** The date and time that this tenant was deleted. */ deletedAt?: Maybe; /** Contact email of the tenant owner. */ - email: Scalars['String']['output']; + email?: Maybe; /** Unique identifier of the tenant. */ id: Scalars['ID']['output']; /** URL of the tenant's identity provider's consent screen. */ - idpConsentUrl: Scalars['String']['output']; + idpConsentUrl?: Maybe; /** Secret used to secure requests from the tenant's identity provider. */ - idpSecret: Scalars['String']['output']; + idpSecret?: Maybe; /** Public name for the tenant. */ publicName?: Maybe; }; @@ -2535,10 +2535,10 @@ export type TenantResolvers; createdAt?: Resolver; deletedAt?: Resolver, ParentType, ContextType>; - email?: Resolver; + email?: Resolver, ParentType, ContextType>; id?: Resolver; - idpConsentUrl?: Resolver; - idpSecret?: Resolver; + idpConsentUrl?: Resolver, ParentType, ContextType>; + idpSecret?: Resolver, ParentType, ContextType>; publicName?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; }; diff --git a/packages/backend/src/graphql/resolvers/tenant.test.ts b/packages/backend/src/graphql/resolvers/tenant.test.ts index 50c54eaf89..bb706dc650 100644 --- a/packages/backend/src/graphql/resolvers/tenant.test.ts +++ b/packages/backend/src/graphql/resolvers/tenant.test.ts @@ -68,7 +68,7 @@ describe('Tenant Resolvers', (): void => { beforeAll(async (): Promise => { deps = await initIocContainer({ ...Config, - databaseUrl: process.env.TENANT_TEST_DATABASE_URL as string + dbSchema: 'tenant_service_test_schema' }) appContainer = await createTestApp(deps) config = await deps.use('config') @@ -136,6 +136,7 @@ describe('Tenant Resolvers', (): void => { const tenant = await createTenant(deps) const apolloClient = createTenantedApolloClient(appContainer, tenant.id) try { + expect.assertions(2) await apolloClient .query({ query: gql` @@ -228,6 +229,7 @@ describe('Tenant Resolvers', (): void => { ) try { + expect.assertions(2) await apolloClient .query({ query: gql` @@ -297,6 +299,7 @@ describe('Tenant Resolvers', (): void => { const apolloClient = createTenantedApolloClient(appContainer, tenant.id) try { + expect.assertions(2) await apolloClient .mutate({ mutation: gql` @@ -399,6 +402,7 @@ describe('Tenant Resolvers', (): void => { } const client = createTenantedApolloClient(appContainer, firstTenant.id) try { + expect.assertions(2) await client .mutate({ mutation: gql` @@ -475,6 +479,7 @@ describe('Tenant Resolvers', (): void => { const client = createTenantedApolloClient(appContainer, secondTenant.id) try { + expect.assertions(2) await client .mutate({ mutation: gql` diff --git a/packages/backend/src/graphql/schema.graphql b/packages/backend/src/graphql/schema.graphql index 2e55d937c5..a80b8906a2 100644 --- a/packages/backend/src/graphql/schema.graphql +++ b/packages/backend/src/graphql/schema.graphql @@ -1531,13 +1531,13 @@ type Tenant implements Model { "Unique identifier of the tenant." id: ID! "Contact email of the tenant owner." - email: String! + email: String "Secret used to secure requests made for this tenant." apiSecret: String! "URL of the tenant's identity provider's consent screen." - idpConsentUrl: String! + idpConsentUrl: String "Secret used to secure requests from the tenant's identity provider." - idpSecret: String! + idpSecret: String "Public name for the tenant." publicName: String "The date and time that this tenant was created." @@ -1562,13 +1562,13 @@ type TenantEdge { input CreateTenantInput { "Contact email of the tenant owner." - email: String! + email: String "Secret used to secure requests made for this tenant." apiSecret: String! "URL of the tenant's identity provider's consent screen." - idpConsentUrl: String! + idpConsentUrl: String "Secret used to secure requests from the tenant's identity provider." - idpSecret: String! + idpSecret: String "Public name for the tenant." publicName: String } diff --git a/packages/frontend/app/generated/graphql.ts b/packages/frontend/app/generated/graphql.ts index 4475460428..d7019b0127 100644 --- a/packages/frontend/app/generated/graphql.ts +++ b/packages/frontend/app/generated/graphql.ts @@ -368,11 +368,11 @@ export type CreateTenantInput = { /** Secret used to secure requests made for this tenant. */ apiSecret: Scalars['String']['input']; /** Contact email of the tenant owner. */ - email: Scalars['String']['input']; + email?: InputMaybe; /** URL of the tenant's identity provider's consent screen. */ - idpConsentUrl: Scalars['String']['input']; + idpConsentUrl?: InputMaybe; /** Secret used to secure requests from the tenant's identity provider. */ - idpSecret: Scalars['String']['input']; + idpSecret?: InputMaybe; /** Public name for the tenant. */ publicName?: InputMaybe; }; @@ -1444,13 +1444,13 @@ export type Tenant = Model & { /** The date and time that this tenant was deleted. */ deletedAt?: Maybe; /** Contact email of the tenant owner. */ - email: Scalars['String']['output']; + email?: Maybe; /** Unique identifier of the tenant. */ id: Scalars['ID']['output']; /** URL of the tenant's identity provider's consent screen. */ - idpConsentUrl: Scalars['String']['output']; + idpConsentUrl?: Maybe; /** Secret used to secure requests from the tenant's identity provider. */ - idpSecret: Scalars['String']['output']; + idpSecret?: Maybe; /** Public name for the tenant. */ publicName?: Maybe; }; @@ -2535,10 +2535,10 @@ export type TenantResolvers; createdAt?: Resolver; deletedAt?: Resolver, ParentType, ContextType>; - email?: Resolver; + email?: Resolver, ParentType, ContextType>; id?: Resolver; - idpConsentUrl?: Resolver; - idpSecret?: Resolver; + idpConsentUrl?: Resolver, ParentType, ContextType>; + idpSecret?: Resolver, ParentType, ContextType>; publicName?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; }; diff --git a/packages/mock-account-service-lib/src/generated/graphql.ts b/packages/mock-account-service-lib/src/generated/graphql.ts index 8d6aac067a..2fe726420e 100644 --- a/packages/mock-account-service-lib/src/generated/graphql.ts +++ b/packages/mock-account-service-lib/src/generated/graphql.ts @@ -368,11 +368,11 @@ export type CreateTenantInput = { /** Secret used to secure requests made for this tenant. */ apiSecret: Scalars['String']['input']; /** Contact email of the tenant owner. */ - email: Scalars['String']['input']; + email?: InputMaybe; /** URL of the tenant's identity provider's consent screen. */ - idpConsentUrl: Scalars['String']['input']; + idpConsentUrl?: InputMaybe; /** Secret used to secure requests from the tenant's identity provider. */ - idpSecret: Scalars['String']['input']; + idpSecret?: InputMaybe; /** Public name for the tenant. */ publicName?: InputMaybe; }; @@ -1444,13 +1444,13 @@ export type Tenant = Model & { /** The date and time that this tenant was deleted. */ deletedAt?: Maybe; /** Contact email of the tenant owner. */ - email: Scalars['String']['output']; + email?: Maybe; /** Unique identifier of the tenant. */ id: Scalars['ID']['output']; /** URL of the tenant's identity provider's consent screen. */ - idpConsentUrl: Scalars['String']['output']; + idpConsentUrl?: Maybe; /** Secret used to secure requests from the tenant's identity provider. */ - idpSecret: Scalars['String']['output']; + idpSecret?: Maybe; /** Public name for the tenant. */ publicName?: Maybe; }; @@ -2535,10 +2535,10 @@ export type TenantResolvers; createdAt?: Resolver; deletedAt?: Resolver, ParentType, ContextType>; - email?: Resolver; + email?: Resolver, ParentType, ContextType>; id?: Resolver; - idpConsentUrl?: Resolver; - idpSecret?: Resolver; + idpConsentUrl?: Resolver, ParentType, ContextType>; + idpSecret?: Resolver, ParentType, ContextType>; publicName?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; }; diff --git a/test/integration/lib/generated/graphql.ts b/test/integration/lib/generated/graphql.ts index 8d6aac067a..2fe726420e 100644 --- a/test/integration/lib/generated/graphql.ts +++ b/test/integration/lib/generated/graphql.ts @@ -368,11 +368,11 @@ export type CreateTenantInput = { /** Secret used to secure requests made for this tenant. */ apiSecret: Scalars['String']['input']; /** Contact email of the tenant owner. */ - email: Scalars['String']['input']; + email?: InputMaybe; /** URL of the tenant's identity provider's consent screen. */ - idpConsentUrl: Scalars['String']['input']; + idpConsentUrl?: InputMaybe; /** Secret used to secure requests from the tenant's identity provider. */ - idpSecret: Scalars['String']['input']; + idpSecret?: InputMaybe; /** Public name for the tenant. */ publicName?: InputMaybe; }; @@ -1444,13 +1444,13 @@ export type Tenant = Model & { /** The date and time that this tenant was deleted. */ deletedAt?: Maybe; /** Contact email of the tenant owner. */ - email: Scalars['String']['output']; + email?: Maybe; /** Unique identifier of the tenant. */ id: Scalars['ID']['output']; /** URL of the tenant's identity provider's consent screen. */ - idpConsentUrl: Scalars['String']['output']; + idpConsentUrl?: Maybe; /** Secret used to secure requests from the tenant's identity provider. */ - idpSecret: Scalars['String']['output']; + idpSecret?: Maybe; /** Public name for the tenant. */ publicName?: Maybe; }; @@ -2535,10 +2535,10 @@ export type TenantResolvers; createdAt?: Resolver; deletedAt?: Resolver, ParentType, ContextType>; - email?: Resolver; + email?: Resolver, ParentType, ContextType>; id?: Resolver; - idpConsentUrl?: Resolver; - idpSecret?: Resolver; + idpConsentUrl?: Resolver, ParentType, ContextType>; + idpSecret?: Resolver, ParentType, ContextType>; publicName?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; }; From 9072e020ad3459aef7dc4c42db0d03816a03357a Mon Sep 17 00:00:00 2001 From: Nathan Lie Date: Tue, 28 Jan 2025 10:29:56 -0800 Subject: [PATCH 07/22] feat: optional idp secret & consent url --- .../migrations/20241125233415_create_tenants_table.js | 4 ++-- packages/auth/src/tenant/model.ts | 4 ++-- packages/auth/src/tenant/routes.ts | 8 ++++---- packages/auth/src/tenant/service.ts | 4 ++-- packages/backend/jest.env.js | 2 +- packages/backend/src/auth-service-client/client.ts | 4 ++-- packages/backend/src/graphql/resolvers/tenant.test.ts | 1 + packages/backend/src/tenants/service.ts | 6 +++--- 8 files changed, 17 insertions(+), 16 deletions(-) diff --git a/packages/auth/migrations/20241125233415_create_tenants_table.js b/packages/auth/migrations/20241125233415_create_tenants_table.js index 9112108977..4846a07ce9 100644 --- a/packages/auth/migrations/20241125233415_create_tenants_table.js +++ b/packages/auth/migrations/20241125233415_create_tenants_table.js @@ -5,8 +5,8 @@ exports.up = function (knex) { return knex.schema.createTable('tenants', function (table) { table.uuid('id').notNullable().primary() - table.string('idpConsentUrl').notNullable() - table.string('idpSecret').notNullable() + table.string('idpConsentUrl') + table.string('idpSecret') table.timestamp('createdAt').defaultTo(knex.fn.now()) table.timestamp('updatedAt').defaultTo(knex.fn.now()) diff --git a/packages/auth/src/tenant/model.ts b/packages/auth/src/tenant/model.ts index b135541c34..412422e217 100644 --- a/packages/auth/src/tenant/model.ts +++ b/packages/auth/src/tenant/model.ts @@ -5,8 +5,8 @@ export class Tenant extends BaseModel { return 'tenants' } - public idpConsentUrl!: string - public idpSecret!: string + public idpConsentUrl?: string + public idpSecret?: string public deletedAt?: Date } diff --git a/packages/auth/src/tenant/routes.ts b/packages/auth/src/tenant/routes.ts index 0ffe7c5940..895e07cf2c 100644 --- a/packages/auth/src/tenant/routes.ts +++ b/packages/auth/src/tenant/routes.ts @@ -22,8 +22,8 @@ type TenantContext = Exclude< interface CreateTenantBody { id: string - idpConsentUrl: string - idpSecret: string + idpConsentUrl?: string + idpSecret?: string } type UpdateTenantBody = Partial> @@ -34,8 +34,8 @@ interface TenantParams { interface TenantResponse { id: string - idpConsentUrl: string - idpSecret: string + idpConsentUrl?: string + idpSecret?: string } export type GetContext = TenantContext diff --git a/packages/auth/src/tenant/service.ts b/packages/auth/src/tenant/service.ts index d4f3cc336a..27e562e397 100644 --- a/packages/auth/src/tenant/service.ts +++ b/packages/auth/src/tenant/service.ts @@ -4,8 +4,8 @@ import { Tenant } from './model' export interface CreateOptions { id: string - idpConsentUrl: string - idpSecret: string + idpConsentUrl?: string + idpSecret?: string } export interface TenantService { diff --git a/packages/backend/jest.env.js b/packages/backend/jest.env.js index 4a8435dd72..8b804444ca 100644 --- a/packages/backend/jest.env.js +++ b/packages/backend/jest.env.js @@ -1,4 +1,4 @@ -process.env.LOG_LEVEL = 'silent' +process.env.LOG_LEVEL = 'info' process.env.INSTANCE_NAME = 'Rafiki' process.env.KEY_ID = 'myKey' process.env.OPEN_PAYMENTS_URL = 'http://127.0.0.1:3000' diff --git a/packages/backend/src/auth-service-client/client.ts b/packages/backend/src/auth-service-client/client.ts index da2e0a9a72..402435446f 100644 --- a/packages/backend/src/auth-service-client/client.ts +++ b/packages/backend/src/auth-service-client/client.ts @@ -1,7 +1,7 @@ interface Tenant { id: string - idpConsentUrl: string - idpSecret: string + idpConsentUrl?: string + idpSecret?: string } export class AuthServiceClientError extends Error { diff --git a/packages/backend/src/graphql/resolvers/tenant.test.ts b/packages/backend/src/graphql/resolvers/tenant.test.ts index bb706dc650..2e1f9ad889 100644 --- a/packages/backend/src/graphql/resolvers/tenant.test.ts +++ b/packages/backend/src/graphql/resolvers/tenant.test.ts @@ -99,6 +99,7 @@ describe('Tenant Resolvers', (): void => { ${false} | ${'tenant'} `('whoami query as $description', async ({ isOperator }): Promise => { const tenant = await createTenant(deps) + console.log('created tenant') const client = isOperator ? appContainer.apolloClient : createTenantedApolloClient(appContainer, tenant.id) diff --git a/packages/backend/src/tenants/service.ts b/packages/backend/src/tenants/service.ts index 947881619f..11eedc73ca 100644 --- a/packages/backend/src/tenants/service.ts +++ b/packages/backend/src/tenants/service.ts @@ -60,10 +60,10 @@ async function getTenantPage( } interface CreateTenantOptions { - email: string + email?: string apiSecret: string - idpSecret: string - idpConsentUrl: string + idpSecret?: string + idpConsentUrl?: string publicName?: string } From e4a6e6ca5d47b949dd2c5e30faf50671d4b44bcc Mon Sep 17 00:00:00 2001 From: Nathan Lie Date: Tue, 28 Jan 2025 11:09:59 -0800 Subject: [PATCH 08/22] feat: tenant response requirement --- .../mock-account-servicing-entity/generated/graphql.ts | 4 ++-- .../backend/src/graphql/generated/graphql.schema.json | 10 +++++++--- packages/backend/src/graphql/generated/graphql.ts | 4 ++-- packages/backend/src/graphql/schema.graphql | 2 +- packages/frontend/app/generated/graphql.ts | 4 ++-- .../mock-account-service-lib/src/generated/graphql.ts | 4 ++-- test/integration/lib/generated/graphql.ts | 4 ++-- 7 files changed, 18 insertions(+), 14 deletions(-) diff --git a/localenv/mock-account-servicing-entity/generated/graphql.ts b/localenv/mock-account-servicing-entity/generated/graphql.ts index 2fe726420e..a9a4b15bc0 100644 --- a/localenv/mock-account-servicing-entity/generated/graphql.ts +++ b/localenv/mock-account-servicing-entity/generated/graphql.ts @@ -1190,7 +1190,7 @@ export type Query = { /** Retrieve an Open Payments incoming payment by receiver ID. The receiver's wallet address can be hosted on this server or a remote Open Payments resource server. */ receiver?: Maybe; /** Retrieve a tenant of the instance. */ - tenant?: Maybe; + tenant: Tenant; /** As an operator, fetch a paginated list of tenants on the instance. */ tenants: TenantsConnection; /** Fetch a wallet address by its ID. */ @@ -2470,7 +2470,7 @@ export type QueryResolvers>; quote?: Resolver, ParentType, ContextType, RequireFields>; receiver?: Resolver, ParentType, ContextType, RequireFields>; - tenant?: Resolver, ParentType, ContextType, RequireFields>; + tenant?: Resolver>; tenants?: Resolver>; walletAddress?: Resolver, ParentType, ContextType, RequireFields>; walletAddressByUrl?: Resolver, ParentType, ContextType, RequireFields>; diff --git a/packages/backend/src/graphql/generated/graphql.schema.json b/packages/backend/src/graphql/generated/graphql.schema.json index a843adf06c..ed648e8445 100644 --- a/packages/backend/src/graphql/generated/graphql.schema.json +++ b/packages/backend/src/graphql/generated/graphql.schema.json @@ -7038,9 +7038,13 @@ } ], "type": { - "kind": "OBJECT", - "name": "Tenant", - "ofType": null + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Tenant", + "ofType": null + } }, "isDeprecated": false, "deprecationReason": null diff --git a/packages/backend/src/graphql/generated/graphql.ts b/packages/backend/src/graphql/generated/graphql.ts index 2fe726420e..a9a4b15bc0 100644 --- a/packages/backend/src/graphql/generated/graphql.ts +++ b/packages/backend/src/graphql/generated/graphql.ts @@ -1190,7 +1190,7 @@ export type Query = { /** Retrieve an Open Payments incoming payment by receiver ID. The receiver's wallet address can be hosted on this server or a remote Open Payments resource server. */ receiver?: Maybe; /** Retrieve a tenant of the instance. */ - tenant?: Maybe; + tenant: Tenant; /** As an operator, fetch a paginated list of tenants on the instance. */ tenants: TenantsConnection; /** Fetch a wallet address by its ID. */ @@ -2470,7 +2470,7 @@ export type QueryResolvers>; quote?: Resolver, ParentType, ContextType, RequireFields>; receiver?: Resolver, ParentType, ContextType, RequireFields>; - tenant?: Resolver, ParentType, ContextType, RequireFields>; + tenant?: Resolver>; tenants?: Resolver>; walletAddress?: Resolver, ParentType, ContextType, RequireFields>; walletAddressByUrl?: Resolver, ParentType, ContextType, RequireFields>; diff --git a/packages/backend/src/graphql/schema.graphql b/packages/backend/src/graphql/schema.graphql index a80b8906a2..fe1e4354c5 100644 --- a/packages/backend/src/graphql/schema.graphql +++ b/packages/backend/src/graphql/schema.graphql @@ -150,7 +150,7 @@ type Query { ): Receiver "Retrieve a tenant of the instance." - tenant("Unique identifier of the tenant." id: String!): Tenant + tenant("Unique identifier of the tenant." id: String!): Tenant! "As an operator, fetch a paginated list of tenants on the instance." tenants( diff --git a/packages/frontend/app/generated/graphql.ts b/packages/frontend/app/generated/graphql.ts index d7019b0127..b8fd92871d 100644 --- a/packages/frontend/app/generated/graphql.ts +++ b/packages/frontend/app/generated/graphql.ts @@ -1190,7 +1190,7 @@ export type Query = { /** Retrieve an Open Payments incoming payment by receiver ID. The receiver's wallet address can be hosted on this server or a remote Open Payments resource server. */ receiver?: Maybe; /** Retrieve a tenant of the instance. */ - tenant?: Maybe; + tenant: Tenant; /** As an operator, fetch a paginated list of tenants on the instance. */ tenants: TenantsConnection; /** Fetch a wallet address by its ID. */ @@ -2470,7 +2470,7 @@ export type QueryResolvers>; quote?: Resolver, ParentType, ContextType, RequireFields>; receiver?: Resolver, ParentType, ContextType, RequireFields>; - tenant?: Resolver, ParentType, ContextType, RequireFields>; + tenant?: Resolver>; tenants?: Resolver>; walletAddress?: Resolver, ParentType, ContextType, RequireFields>; walletAddressByUrl?: Resolver, ParentType, ContextType, RequireFields>; diff --git a/packages/mock-account-service-lib/src/generated/graphql.ts b/packages/mock-account-service-lib/src/generated/graphql.ts index 2fe726420e..a9a4b15bc0 100644 --- a/packages/mock-account-service-lib/src/generated/graphql.ts +++ b/packages/mock-account-service-lib/src/generated/graphql.ts @@ -1190,7 +1190,7 @@ export type Query = { /** Retrieve an Open Payments incoming payment by receiver ID. The receiver's wallet address can be hosted on this server or a remote Open Payments resource server. */ receiver?: Maybe; /** Retrieve a tenant of the instance. */ - tenant?: Maybe; + tenant: Tenant; /** As an operator, fetch a paginated list of tenants on the instance. */ tenants: TenantsConnection; /** Fetch a wallet address by its ID. */ @@ -2470,7 +2470,7 @@ export type QueryResolvers>; quote?: Resolver, ParentType, ContextType, RequireFields>; receiver?: Resolver, ParentType, ContextType, RequireFields>; - tenant?: Resolver, ParentType, ContextType, RequireFields>; + tenant?: Resolver>; tenants?: Resolver>; walletAddress?: Resolver, ParentType, ContextType, RequireFields>; walletAddressByUrl?: Resolver, ParentType, ContextType, RequireFields>; diff --git a/test/integration/lib/generated/graphql.ts b/test/integration/lib/generated/graphql.ts index 2fe726420e..a9a4b15bc0 100644 --- a/test/integration/lib/generated/graphql.ts +++ b/test/integration/lib/generated/graphql.ts @@ -1190,7 +1190,7 @@ export type Query = { /** Retrieve an Open Payments incoming payment by receiver ID. The receiver's wallet address can be hosted on this server or a remote Open Payments resource server. */ receiver?: Maybe; /** Retrieve a tenant of the instance. */ - tenant?: Maybe; + tenant: Tenant; /** As an operator, fetch a paginated list of tenants on the instance. */ tenants: TenantsConnection; /** Fetch a wallet address by its ID. */ @@ -2470,7 +2470,7 @@ export type QueryResolvers>; quote?: Resolver, ParentType, ContextType, RequireFields>; receiver?: Resolver, ParentType, ContextType, RequireFields>; - tenant?: Resolver, ParentType, ContextType, RequireFields>; + tenant?: Resolver>; tenants?: Resolver>; walletAddress?: Resolver, ParentType, ContextType, RequireFields>; walletAddressByUrl?: Resolver, ParentType, ContextType, RequireFields>; From 45dcb5b4ea8c573236d635c70389f083cf26745c Mon Sep 17 00:00:00 2001 From: Nathan Lie Date: Tue, 28 Jan 2025 11:39:38 -0800 Subject: [PATCH 09/22] feat: make delete operator-only --- .../src/graphql/resolvers/tenant.test.ts | 52 ++++++++----------- .../backend/src/graphql/resolvers/tenant.ts | 8 +-- 2 files changed, 25 insertions(+), 35 deletions(-) diff --git a/packages/backend/src/graphql/resolvers/tenant.test.ts b/packages/backend/src/graphql/resolvers/tenant.test.ts index 2e1f9ad889..628be01be6 100644 --- a/packages/backend/src/graphql/resolvers/tenant.test.ts +++ b/packages/backend/src/graphql/resolvers/tenant.test.ts @@ -440,40 +440,30 @@ describe('Tenant Resolvers', (): void => { }) describe('Delete', (): void => { - test.each` - isOperator | description - ${true} | ${'operator'} - ${false} | ${'tenant'} - `( - 'Can delete a tenant as $description', - async ({ isOperator }): Promise => { - const tenant = await createTenant(deps) + test('Can delete a tenant as operator', async (): Promise => { + const tenant = await createTenant(deps) - const client = isOperator - ? appContainer.apolloClient - : createTenantedApolloClient(appContainer, tenant.id) - const mutation = await client - .mutate({ - mutation: gql` - mutation DeleteTenant($id: String!) { - deleteTenant(id: $id) { - success - } + const mutation = await appContainer.apolloClient + .mutate({ + mutation: gql` + mutation DeleteTenant($id: String!) { + deleteTenant(id: $id) { + success } - `, - variables: { - id: tenant.id } - }) - .then( - (query): DeleteTenantMutationResponse => query.data?.deleteTenant - ) + `, + variables: { + id: tenant.id + } + }) + .then( + (query): DeleteTenantMutationResponse => query.data?.deleteTenant + ) - expect(mutation.success).toBe(true) - } - ) + expect(mutation.success).toBe(true) + }) - test('Cannot delete other tenant as non-operator', async (): Promise => { + test('Cannot delete tenant as non-operator', async (): Promise => { const firstTenant = await createTenant(deps) const secondTenant = await createTenant(deps) @@ -501,9 +491,9 @@ describe('Tenant Resolvers', (): void => { expect(error).toBeInstanceOf(ApolloError) expect((error as ApolloError).graphQLErrors).toContainEqual( expect.objectContaining({ - message: 'tenant does not exist', + message: 'permission denied', extensions: expect.objectContaining({ - code: GraphQLErrorCode.NotFound + code: GraphQLErrorCode.Forbidden }) }) ) diff --git a/packages/backend/src/graphql/resolvers/tenant.ts b/packages/backend/src/graphql/resolvers/tenant.ts index 6d27940a4e..19e86bd077 100644 --- a/packages/backend/src/graphql/resolvers/tenant.ts +++ b/packages/backend/src/graphql/resolvers/tenant.ts @@ -141,11 +141,11 @@ export const deleteTenant: MutationResolvers['deleteTenan args, ctx ): Promise => { - const { tenant: contextTenant, isOperator } = ctx - if (args.id !== contextTenant.id && !isOperator) { - throw new GraphQLError('tenant does not exist', { + const { isOperator } = ctx + if (!isOperator) { + throw new GraphQLError('permission denied', { extensions: { - code: GraphQLErrorCode.NotFound + code: GraphQLErrorCode.Forbidden } }) } From 993e4c465514ea6bd476d08ee00ee4c8565bd973 Mon Sep 17 00:00:00 2001 From: koekiebox Date: Wed, 29 Jan 2025 14:48:55 +0100 Subject: [PATCH 10/22] feat(2915): admin front-end for tenant support --- .../generated/graphql.ts | 3 + .../src/graphql/generated/graphql.schema.json | 16 + .../backend/src/graphql/generated/graphql.ts | 3 + .../backend/src/graphql/resolvers/tenant.ts | 4 +- packages/backend/src/graphql/schema.graphql | 2 + packages/frontend/app/components/Sidebar.tsx | 4 + packages/frontend/app/generated/graphql.ts | 41 +++ .../frontend/app/lib/api/tenant.server.ts | 162 ++++++++++ packages/frontend/app/lib/validate.server.ts | 25 +- .../frontend/app/routes/tenants.$tenantId.tsx | 294 ++++++++++++++++++ .../frontend/app/routes/tenants._index.tsx | 133 ++++++++ .../frontend/app/routes/tenants.create.tsx | 151 +++++++++ .../src/generated/graphql.ts | 3 + test/integration/lib/generated/graphql.ts | 3 + 14 files changed, 842 insertions(+), 2 deletions(-) create mode 100644 packages/frontend/app/lib/api/tenant.server.ts create mode 100644 packages/frontend/app/routes/tenants.$tenantId.tsx create mode 100644 packages/frontend/app/routes/tenants._index.tsx create mode 100644 packages/frontend/app/routes/tenants.create.tsx diff --git a/localenv/mock-account-servicing-entity/generated/graphql.ts b/localenv/mock-account-servicing-entity/generated/graphql.ts index 5296c25729..31b6c2cff5 100644 --- a/localenv/mock-account-servicing-entity/generated/graphql.ts +++ b/localenv/mock-account-servicing-entity/generated/graphql.ts @@ -1454,6 +1454,8 @@ export type Tenant = Model & { idpConsentUrl?: Maybe; /** Secret used to secure requests from the tenant's identity provider. */ idpSecret?: Maybe; + /** Is the tenant an Operator tenant. */ + isOperator: Scalars['Boolean']['output']; /** Public name for the tenant. */ publicName?: Maybe; }; @@ -2544,6 +2546,7 @@ export type TenantResolvers; idpConsentUrl?: Resolver, ParentType, ContextType>; idpSecret?: Resolver, ParentType, ContextType>; + isOperator?: Resolver; publicName?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; }; diff --git a/packages/backend/src/graphql/generated/graphql.schema.json b/packages/backend/src/graphql/generated/graphql.schema.json index f441f81038..efcaca00e0 100644 --- a/packages/backend/src/graphql/generated/graphql.schema.json +++ b/packages/backend/src/graphql/generated/graphql.schema.json @@ -8081,6 +8081,22 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "isOperator", + "description": "Is the tenant an Operator tenant.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "publicName", "description": "Public name for the tenant.", diff --git a/packages/backend/src/graphql/generated/graphql.ts b/packages/backend/src/graphql/generated/graphql.ts index 5296c25729..31b6c2cff5 100644 --- a/packages/backend/src/graphql/generated/graphql.ts +++ b/packages/backend/src/graphql/generated/graphql.ts @@ -1454,6 +1454,8 @@ export type Tenant = Model & { idpConsentUrl?: Maybe; /** Secret used to secure requests from the tenant's identity provider. */ idpSecret?: Maybe; + /** Is the tenant an Operator tenant. */ + isOperator: Scalars['Boolean']['output']; /** Public name for the tenant. */ publicName?: Maybe; }; @@ -2544,6 +2546,7 @@ export type TenantResolvers; idpConsentUrl?: Resolver, ParentType, ContextType>; idpSecret?: Resolver, ParentType, ContextType>; + isOperator?: Resolver; publicName?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; }; diff --git a/packages/backend/src/graphql/resolvers/tenant.ts b/packages/backend/src/graphql/resolvers/tenant.ts index 19e86bd077..fafd95ffab 100644 --- a/packages/backend/src/graphql/resolvers/tenant.ts +++ b/packages/backend/src/graphql/resolvers/tenant.ts @@ -10,6 +10,7 @@ import { GraphQLErrorCode } from '../errors' import { Tenant } from '../../tenants/model' import { Pagination, SortOrder } from '../../shared/baseModel' import { getPageInfo } from '../../shared/pagination' +import { Config } from '../../config/app' export const whoami: QueryResolvers['whoami'] = async ( parent, @@ -174,6 +175,7 @@ export function tenantToGraphQl(tenant: Tenant): SchemaTenant { createdAt: new Date(+tenant.createdAt).toISOString(), deletedAt: tenant.deletedAt ? new Date(+tenant.deletedAt).toISOString() - : null + : null, + isOperator: tenant.apiSecret === Config.adminApiSecret } } diff --git a/packages/backend/src/graphql/schema.graphql b/packages/backend/src/graphql/schema.graphql index 78218b01a1..b73e960170 100644 --- a/packages/backend/src/graphql/schema.graphql +++ b/packages/backend/src/graphql/schema.graphql @@ -1551,6 +1551,8 @@ type Tenant implements Model { createdAt: String! "The date and time that this tenant was deleted." deletedAt: String + "Is the tenant an Operator tenant." + isOperator: Boolean! } type TenantsConnection { diff --git a/packages/frontend/app/components/Sidebar.tsx b/packages/frontend/app/components/Sidebar.tsx index 8555bae1ab..5e37ef29d5 100644 --- a/packages/frontend/app/components/Sidebar.tsx +++ b/packages/frontend/app/components/Sidebar.tsx @@ -17,6 +17,10 @@ const navigation = [ name: 'Home', href: '/' }, + { + name: 'Tenants', + href: '/tenants' + }, { name: 'Assets', href: '/assets' diff --git a/packages/frontend/app/generated/graphql.ts b/packages/frontend/app/generated/graphql.ts index e7c149d3d2..b522e55f38 100644 --- a/packages/frontend/app/generated/graphql.ts +++ b/packages/frontend/app/generated/graphql.ts @@ -1454,6 +1454,8 @@ export type Tenant = Model & { idpConsentUrl?: Maybe; /** Secret used to secure requests from the tenant's identity provider. */ idpSecret?: Maybe; + /** Is the tenant an Operator tenant. */ + isOperator: Scalars['Boolean']['output']; /** Public name for the tenant. */ publicName?: Maybe; }; @@ -2544,6 +2546,7 @@ export type TenantResolvers; idpConsentUrl?: Resolver, ParentType, ContextType>; idpSecret?: Resolver, ParentType, ContextType>; + isOperator?: Resolver; publicName?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; }; @@ -2921,6 +2924,44 @@ export type WithdrawPeerLiquidityVariables = Exact<{ export type WithdrawPeerLiquidity = { __typename?: 'Mutation', createPeerLiquidityWithdrawal?: { __typename?: 'LiquidityMutationResponse', success: boolean } | null }; +export type ListTenantsQueryVariables = Exact<{ + after?: InputMaybe; + before?: InputMaybe; + first?: InputMaybe; + last?: InputMaybe; +}>; + + +export type ListTenantsQuery = { __typename?: 'Query', tenants: { __typename?: 'TenantsConnection', edges: Array<{ __typename?: 'TenantEdge', node: { __typename?: 'Tenant', id: string, email?: string | null, apiSecret: string, idpConsentUrl?: string | null, idpSecret?: string | null, publicName?: string | null, createdAt: string, deletedAt?: string | null, isOperator: boolean } }>, pageInfo: { __typename?: 'PageInfo', startCursor?: string | null, endCursor?: string | null, hasNextPage: boolean, hasPreviousPage: boolean } } }; + +export type CreateTenantMutationVariables = Exact<{ + input: CreateTenantInput; +}>; + + +export type CreateTenantMutation = { __typename?: 'Mutation', createTenant: { __typename?: 'TenantMutationResponse', tenant: { __typename?: 'Tenant', publicName?: string | null, email?: string | null, apiSecret: string, idpConsentUrl?: string | null, idpSecret?: string | null } } }; + +export type UpdateTenantMutationVariables = Exact<{ + input: UpdateTenantInput; +}>; + + +export type UpdateTenantMutation = { __typename?: 'Mutation', updateTenant: { __typename?: 'TenantMutationResponse', tenant: { __typename?: 'Tenant', id: string, email?: string | null, apiSecret: string, idpConsentUrl?: string | null, idpSecret?: string | null, publicName?: string | null } } }; + +export type DeleteTenantMutationVariables = Exact<{ + id: Scalars['String']['input']; +}>; + + +export type DeleteTenantMutation = { __typename?: 'Mutation', deleteTenant: { __typename?: 'DeleteTenantMutationResponse', success: boolean } }; + +export type GetTenantQueryVariables = Exact<{ + id: Scalars['String']['input']; +}>; + + +export type GetTenantQuery = { __typename?: 'Query', tenant: { __typename?: 'Tenant', id: string, email?: string | null, apiSecret: string, idpConsentUrl?: string | null, idpSecret?: string | null, publicName?: string | null, createdAt: string, deletedAt?: string | null, isOperator: boolean } }; + export type GetWalletAddressQueryVariables = Exact<{ id: Scalars['String']['input']; }>; diff --git a/packages/frontend/app/lib/api/tenant.server.ts b/packages/frontend/app/lib/api/tenant.server.ts new file mode 100644 index 0000000000..188a81f850 --- /dev/null +++ b/packages/frontend/app/lib/api/tenant.server.ts @@ -0,0 +1,162 @@ +import { gql } from '@apollo/client' +import type { + CreateTenantInput, + CreateTenantMutation, + CreateTenantMutationVariables, + QueryTenantsArgs, + ListTenantsQuery, + ListTenantsQueryVariables +} from '~/generated/graphql' +import { getApolloClient } from '../apollo.server' + +export const listTenants = async (request: Request, args: QueryTenantsArgs) => { + const apolloClient = await getApolloClient(request) + const response = await apolloClient.query< + ListTenantsQuery, + ListTenantsQueryVariables + >({ + query: gql` + query ListTenantsQuery( + $after: String + $before: String + $first: Int + $last: Int + ) { + tenants(after: $after, before: $before, first: $first, last: $last) { + edges { + node { + id + email + apiSecret + idpConsentUrl + idpSecret + publicName + createdAt + deletedAt + isOperator + } + } + pageInfo { + startCursor + endCursor + hasNextPage + hasPreviousPage + } + } + } + `, + variables: args + }) + return response.data.tenants +} + +export const createTenant = async ( + request: Request, + args: CreateTenantInput +) => { + const apolloClient = await getApolloClient(request) + const response = await apolloClient.mutate< + CreateTenantMutation, + CreateTenantMutationVariables + >({ + mutation: gql` + mutation CreateTenantMutation($input: CreateTenantInput!) { + createTenant(input: $input) { + tenant { + publicName + email + apiSecret + idpConsentUrl + idpSecret + } + } + } + `, + variables: { + input: args + } + }) + + return response.data?.createTenant +} + +export const updateTenant = async ( + request: Request, + args: UpdateTenantInput +) => { + const apolloClient = await getApolloClient(request) + const response = await apolloClient.mutate< + UpdateTenantMutation, + UpdateTenantMutationVariables + >({ + mutation: gql` + mutation UpdateTenantMutation($input: UpdateTenantInput!) { + updateTenant(input: $input) { + tenant { + id + email + apiSecret + idpConsentUrl + idpSecret + publicName + } + } + } + `, + variables: { + input: args + } + }) + + return response.data?.updateTenant +} + +export const deleteTenant = async (request: Request, args: string) => { + const apolloClient = await getApolloClient(request) + const response = await apolloClient.mutate< + DeleteTenantMutation, + DeleteTenantMutationVariables + >({ + mutation: gql` + mutation DeleteTenantMutation($id: String!) { + deleteTenant(id: $id) { + success + } + } + `, + variables: { + id: args + } + }) + + return response.data?.deleteTenant +} + +export const getTenantInfo = async ( + request: Request, + args: QueryTenantArgs +) => { + const apolloClient = await getApolloClient(request) + const response = await apolloClient.query< + GetTenantQuery, + GetTenantQueryVariables + >({ + query: gql` + query GetTenantQuery($id: String!) { + tenant(id: $id) { + id + email + apiSecret + idpConsentUrl + idpSecret + publicName + createdAt + deletedAt + isOperator + } + } + `, + variables: args + }) + return response.data.tenant +} diff --git a/packages/frontend/app/lib/validate.server.ts b/packages/frontend/app/lib/validate.server.ts index ca74197fe3..211ebd566a 100644 --- a/packages/frontend/app/lib/validate.server.ts +++ b/packages/frontend/app/lib/validate.server.ts @@ -94,7 +94,7 @@ export const createAssetSchema = z .object({ code: z .string() - .min(3, { message: 'Code should be atleast 3 characters long' }) + .min(3, { message: 'Code should be at least 3 characters long' }) .max(6, { message: 'Maximum length of Code is 6 characters' }) .regex(/^[a-zA-Z]+$/, { message: 'Code should only contain letters.' }) .transform((code) => code.toUpperCase()), @@ -127,3 +127,26 @@ export const updateWalletAddressSchema = z status: z.enum([WalletAddressStatus.Active, WalletAddressStatus.Inactive]) }) .merge(uuidSchema) + +export const updateTenantSchema = z + .object({ + publicName: z.string().optional(), + email: z.string().optional(), + idpConsentUrl: z.string().optional(), + idpSecret: z.string().optional() + }) + .merge(uuidSchema) + +export const createTenantSchema = z + .object({ + apiSecret: z + .string() + .min(3, { message: 'API Secret should be at least 3 characters long' }) + .max(6, { message: 'Maximum length of API Secret is 255 characters' }) + .regex( + /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/, + { message: 'API Secret should be Base64 encoded.' } + ) + }) + .merge(updateTenantSchema) + .omit({ id: true }) diff --git a/packages/frontend/app/routes/tenants.$tenantId.tsx b/packages/frontend/app/routes/tenants.$tenantId.tsx new file mode 100644 index 0000000000..5ad7b952d6 --- /dev/null +++ b/packages/frontend/app/routes/tenants.$tenantId.tsx @@ -0,0 +1,294 @@ +import { + json, + type ActionFunctionArgs, + type LoaderFunctionArgs +} from '@remix-run/node' +import { + Form, + Outlet, + useActionData, + useFormAction, + useLoaderData, + useNavigation, + useSubmit +} from '@remix-run/react' +import { type FormEvent, useState, useRef } from 'react' +import { z } from 'zod' +import { DangerZone, PageHeader } from '~/components' +import { Button, ErrorPanel, Input } from '~/components/ui' +import { + ConfirmationDialog, + type ConfirmationDialogRef +} from '~/components/ConfirmationDialog' +import { updateTenant, deleteTenant } from '~/lib/api/tenant.server' +import { messageStorage, setMessageAndRedirect } from '~/lib/message.server' +import { updateTenantSchema, uuidSchema } from '~/lib/validate.server' +import type { ZodFieldErrors } from '~/shared/types' +import { checkAuthAndRedirect } from '../lib/kratos_checks.server' +import { getTenantInfo } from '~/lib/api/tenant.server' + +export async function loader({ request, params }: LoaderFunctionArgs) { + const cookies = request.headers.get('cookie') + await checkAuthAndRedirect(request.url, cookies) + + const tenantId = params.tenantId + + const result = z.string().uuid().safeParse(tenantId) + if (!result.success) { + throw json(null, { status: 400, statusText: 'Invalid tenant ID.' }) + } + + const tenant = await getTenantInfo(request, { id: result.data }) + + if (!tenant) { + throw json(null, { status: 404, statusText: 'Tenant not found.' }) + } + + return json({ tenant }) +} + +export default function ViewTenantPage() { + const { tenant } = useLoaderData() + const response = useActionData() + const navigation = useNavigation() + const formAction = useFormAction() + const submit = useSubmit() + const dialogRef = useRef(null) + + const isSubmitting = navigation.state === 'submitting' + const currentPageAction = isSubmitting && navigation.formAction === formAction + + const [formData, setFormData] = useState() + + const submitHandler = (event: FormEvent) => { + event.preventDefault() + setFormData(new FormData(event.currentTarget)) + dialogRef.current?.display() + } + + const onConfirm = () => { + if (formData) { + submit(formData, { method: 'post' }) + } + } + + return ( +
+
+ + + +
+
+

General Information

+

+ Created at {new Date(tenant.createdAt).toLocaleString()} +

+ +
+
+
+
+
+ + + + +
+
+ +
+
+
+
+
+ {/* Sensitive Info */} +
+
+

Sensitive Information

+ +
+
+
+
+
+ + +
+
+ +
+
+
+
+
+ {/* Sensitive - END */} + {/* Identity Provider Information */} +
+
+

+ Identity Provider Information +

+ +
+
+
+
+
+ + + +
+
+ +
+
+
+
+
+ {/* Identity Provider Information - END */} + + {/* DELETE TENANT - Danger zone */} + +
+ + + +
+
+
+ + +
+ ) +} + +export async function action({ request }: ActionFunctionArgs) { + const actionResponse: { + errors: { + general: { + fieldErrors: ZodFieldErrors + message: string[] + } + } + } = { + errors: { + general: { + fieldErrors: {}, + message: [] + } + } + } + + const session = await messageStorage.getSession(request.headers.get('cookie')) + const formData = await request.formData() + const intent = formData.get('intent') + formData.delete('intent') + + switch (intent) { + case 'general': { + const result = updateTenantSchema.safeParse(Object.fromEntries(formData)) + if (!result.success) { + actionResponse.errors.general.fieldErrors = + result.error.flatten().fieldErrors + return json({ ...actionResponse }, { status: 400 }) + } + + const response = await updateTenant(request, { + ...result.data, + ...(result.data.withdrawalThreshold + ? { withdrawalThreshold: result.data.withdrawalThreshold } + : { withdrawalThreshold: undefined }) + }) + + if (!response?.asset) { + actionResponse.errors.general.message = [ + 'Could not update tenant. Please try again!' + ] + return json({ ...actionResponse }, { status: 400 }) + } + break + } + case 'delete': { + const result = uuidSchema.safeParse(Object.fromEntries(formData)) + if (!result.success) { + return setMessageAndRedirect({ + session, + message: { + content: 'Invalid tenant ID.', + type: 'error' + }, + location: '.' + }) + } + + const response = await deleteTenant(request, { id: result.data.id }) + if (!response?.tenant) { + return setMessageAndRedirect({ + session, + message: { + content: 'Could not delete Tenant.', + type: 'error' + }, + location: '.' + }) + } + + return setMessageAndRedirect({ + session, + message: { + content: 'Tenant was deleted.', + type: 'success' + }, + location: '/tenants' + }) + } + default: + throw json(null, { status: 400, statusText: 'Invalid intent.' }) + } + + return setMessageAndRedirect({ + session, + message: { + content: 'Tenant information was updated', + type: 'success' + }, + location: '.' + }) +} diff --git a/packages/frontend/app/routes/tenants._index.tsx b/packages/frontend/app/routes/tenants._index.tsx new file mode 100644 index 0000000000..ab7949036a --- /dev/null +++ b/packages/frontend/app/routes/tenants._index.tsx @@ -0,0 +1,133 @@ +import { json, type LoaderFunctionArgs } from '@remix-run/node' +import { useLoaderData, useNavigate } from '@remix-run/react' +import { Badge, BadgeColor, PageHeader } from '~/components' +import { Button, Table } from '~/components/ui' +import { listTenants } from '~/lib/api/tenant.server' +import { paginationSchema } from '~/lib/validate.server' +import { checkAuthAndRedirect } from '../lib/kratos_checks.server' + +export const loader = async ({ request }: LoaderFunctionArgs) => { + const cookies = request.headers.get('cookie') + await checkAuthAndRedirect(request.url, cookies) + + const url = new URL(request.url) + const pagination = paginationSchema.safeParse( + Object.fromEntries(url.searchParams.entries()) + ) + + if (!pagination.success) { + throw json(null, { status: 400, statusText: 'Invalid pagination.' }) + } + + const tenants = await listTenants(request, { + ...pagination.data + }) + + let previousPageUrl = '', + nextPageUrl = '' + if (tenants.pageInfo.hasPreviousPage) + previousPageUrl = `/tenants?before=${tenants.pageInfo.startCursor}` + if (tenants.pageInfo.hasNextPage) + nextPageUrl = `/tenants?after=${tenants.pageInfo.endCursor}` + + return json({ tenants, previousPageUrl, nextPageUrl }) +} + +export default function TenantsPage() { + const { tenants, previousPageUrl, nextPageUrl } = + useLoaderData() + const navigate = useNavigate() + + return ( +
+
+ +
+

Tenants

+
+
+ +
+
+ + + + {tenants.edges.length ? ( + tenants.edges.map((tenant) => ( + navigate(`/tenants/${tenant.node.id}`)} + > + {tenant.node.id} + +
+ {tenant.node.publicName ? ( + + {tenant.node.publicName} + + ) : ( + No public name + )} +
+
+ + {tenant.node.email ? ( + {tenant.node.email} + ) : ( + No email + )} + + + {tenant.node.deletedAt ? ( + Inactive + ) : ( + Active + )} + + + {tenant.node.isOperator ? ( + Yes + ) : ( + No + )} + +
+ )) + ) : ( + + + No tenants found. + + + )} +
+
+
+ + +
+
+
+ ) +} diff --git a/packages/frontend/app/routes/tenants.create.tsx b/packages/frontend/app/routes/tenants.create.tsx new file mode 100644 index 0000000000..9220357801 --- /dev/null +++ b/packages/frontend/app/routes/tenants.create.tsx @@ -0,0 +1,151 @@ +import { json, type ActionFunctionArgs } from '@remix-run/node' +import { Form, useActionData, useNavigation } from '@remix-run/react' +import { PageHeader } from '~/components' +import { Button, ErrorPanel, Input } from '~/components/ui' +import { createTenant } from '~/lib/api/tenant.server' +import { messageStorage, setMessageAndRedirect } from '~/lib/message.server' +import { createTenantSchema } from '~/lib/validate.server' +import type { ZodFieldErrors } from '~/shared/types' +import { checkAuthAndRedirect } from '../lib/kratos_checks.server' +import { type LoaderFunctionArgs } from '@remix-run/node' + +export const loader = async ({ request }: LoaderFunctionArgs) => { + const cookies = request.headers.get('cookie') + await checkAuthAndRedirect(request.url, cookies) + return null +} + +export default function CreateTenantPage() { + const response = useActionData() + const { state } = useNavigation() + const isSubmitting = state === 'submitting' + + return ( +
+
+ +

Create Tenant

+ +
+ {/* Create Tenant form */} +
+
+ +
+ +
+ {/* Tenant General Info */} +
+
+

General Information

+
+
+
+ + +
+
+
+ {/* Tenant General Info - END */} + {/* Tenant Sensitive Info */} +
+
+

Sensitive Information

+
+
+
+ +
+
+
+ {/* Tenant Sensitive Info - END */} + {/* Tenant Identity Provider */} +
+
+

+ Identity Provider Information +

+
+
+
+ + +
+
+
+ {/* Tenant Identity Provider - End */} +
+ +
+
+
+ {/* Create Tenant form - END */} +
+
+ ) +} + +export async function action({ request }: ActionFunctionArgs) { + const errors: { + fieldErrors: ZodFieldErrors + message: string[] + } = { + fieldErrors: {}, + message: [] + } + + const formData = Object.fromEntries(await request.formData()) + const result = createTenantSchema.safeParse(formData) + + if (!result.success) { + errors.fieldErrors = result.error.flatten().fieldErrors + return json({ errors }, { status: 400 }) + } + + const response = await createTenant(request, { ...result.data }) + + if (!response?.tenant) { + errors.message = ['Could not create tenant. Please try again!'] + return json({ errors }, { status: 400 }) + } + + const session = await messageStorage.getSession(request.headers.get('cookie')) + + return setMessageAndRedirect({ + session, + message: { + content: 'Tenant created.', + type: 'success' + }, + location: `/tenants/${response.tenant?.id}` + }) +} diff --git a/packages/mock-account-service-lib/src/generated/graphql.ts b/packages/mock-account-service-lib/src/generated/graphql.ts index 5296c25729..31b6c2cff5 100644 --- a/packages/mock-account-service-lib/src/generated/graphql.ts +++ b/packages/mock-account-service-lib/src/generated/graphql.ts @@ -1454,6 +1454,8 @@ export type Tenant = Model & { idpConsentUrl?: Maybe; /** Secret used to secure requests from the tenant's identity provider. */ idpSecret?: Maybe; + /** Is the tenant an Operator tenant. */ + isOperator: Scalars['Boolean']['output']; /** Public name for the tenant. */ publicName?: Maybe; }; @@ -2544,6 +2546,7 @@ export type TenantResolvers; idpConsentUrl?: Resolver, ParentType, ContextType>; idpSecret?: Resolver, ParentType, ContextType>; + isOperator?: Resolver; publicName?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; }; diff --git a/test/integration/lib/generated/graphql.ts b/test/integration/lib/generated/graphql.ts index 5296c25729..31b6c2cff5 100644 --- a/test/integration/lib/generated/graphql.ts +++ b/test/integration/lib/generated/graphql.ts @@ -1454,6 +1454,8 @@ export type Tenant = Model & { idpConsentUrl?: Maybe; /** Secret used to secure requests from the tenant's identity provider. */ idpSecret?: Maybe; + /** Is the tenant an Operator tenant. */ + isOperator: Scalars['Boolean']['output']; /** Public name for the tenant. */ publicName?: Maybe; }; @@ -2544,6 +2546,7 @@ export type TenantResolvers; idpConsentUrl?: Resolver, ParentType, ContextType>; idpSecret?: Resolver, ParentType, ContextType>; + isOperator?: Resolver; publicName?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; }; From ac04fa67a7096433e5a2bbc094eafcc33c08fc50 Mon Sep 17 00:00:00 2001 From: koekiebox Date: Thu, 30 Jan 2025 13:29:00 +0100 Subject: [PATCH 11/22] feat(2915): apply permissions for tenant screens --- .../frontend/app/lib/api/tenant.server.ts | 1 + packages/frontend/app/lib/validate.server.ts | 4 +- .../frontend/app/routes/tenants.$tenantId.tsx | 9 ++--- .../frontend/app/routes/tenants._index.tsx | 40 ++++++++++++++----- 4 files changed, 37 insertions(+), 17 deletions(-) diff --git a/packages/frontend/app/lib/api/tenant.server.ts b/packages/frontend/app/lib/api/tenant.server.ts index 188a81f850..f86115a2f8 100644 --- a/packages/frontend/app/lib/api/tenant.server.ts +++ b/packages/frontend/app/lib/api/tenant.server.ts @@ -63,6 +63,7 @@ export const createTenant = async ( mutation CreateTenantMutation($input: CreateTenantInput!) { createTenant(input: $input) { tenant { + id publicName email apiSecret diff --git a/packages/frontend/app/lib/validate.server.ts b/packages/frontend/app/lib/validate.server.ts index 211ebd566a..4a5c467cfc 100644 --- a/packages/frontend/app/lib/validate.server.ts +++ b/packages/frontend/app/lib/validate.server.ts @@ -141,8 +141,8 @@ export const createTenantSchema = z .object({ apiSecret: z .string() - .min(3, { message: 'API Secret should be at least 3 characters long' }) - .max(6, { message: 'Maximum length of API Secret is 255 characters' }) + .min(10, { message: 'API Secret should be at least 3 characters long' }) + .max(255, { message: 'Maximum length of API Secret is 255 characters' }) .regex( /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/, { message: 'API Secret should be Base64 encoded.' } diff --git a/packages/frontend/app/routes/tenants.$tenantId.tsx b/packages/frontend/app/routes/tenants.$tenantId.tsx index 5ad7b952d6..cf9245c12f 100644 --- a/packages/frontend/app/routes/tenants.$tenantId.tsx +++ b/packages/frontend/app/routes/tenants.$tenantId.tsx @@ -231,10 +231,7 @@ export async function action({ request }: ActionFunctionArgs) { } const response = await updateTenant(request, { - ...result.data, - ...(result.data.withdrawalThreshold - ? { withdrawalThreshold: result.data.withdrawalThreshold } - : { withdrawalThreshold: undefined }) + ...result.data }) if (!response?.asset) { @@ -258,8 +255,8 @@ export async function action({ request }: ActionFunctionArgs) { }) } - const response = await deleteTenant(request, { id: result.data.id }) - if (!response?.tenant) { + const response = await deleteTenant(request, result.data.id) + if (!response) { return setMessageAndRedirect({ session, message: { diff --git a/packages/frontend/app/routes/tenants._index.tsx b/packages/frontend/app/routes/tenants._index.tsx index ab7949036a..2fab521585 100644 --- a/packages/frontend/app/routes/tenants._index.tsx +++ b/packages/frontend/app/routes/tenants._index.tsx @@ -5,6 +5,7 @@ import { Button, Table } from '~/components/ui' import { listTenants } from '~/lib/api/tenant.server' import { paginationSchema } from '~/lib/validate.server' import { checkAuthAndRedirect } from '../lib/kratos_checks.server' +import { getSession } from '~/lib/session.server' export const loader = async ({ request }: LoaderFunctionArgs) => { const cookies = request.headers.get('cookie') @@ -19,6 +20,7 @@ export const loader = async ({ request }: LoaderFunctionArgs) => { throw json(null, { status: 400, statusText: 'Invalid pagination.' }) } + let isOperator = false const tenants = await listTenants(request, { ...pagination.data }) @@ -30,11 +32,29 @@ export const loader = async ({ request }: LoaderFunctionArgs) => { if (tenants.pageInfo.hasNextPage) nextPageUrl = `/tenants?after=${tenants.pageInfo.endCursor}` - return json({ tenants, previousPageUrl, nextPageUrl }) + let tenantEdges = tenants.edges + const tenantPageInfo = tenants.pageInfo + if (tenantEdges.length) { + const session = await getSession(cookies) + const sessionApiSecret = session.get('apiSecret') + if (sessionApiSecret && sessionApiSecret.length > 0) { + for (const edge of tenantEdges) { + const edgeNode = edge.node + if (edgeNode && sessionApiSecret === edgeNode.apiSecret) { + isOperator = edgeNode.isOperator + break + } + } + } + tenantEdges = isOperator ? tenants.edges : + tenantEdges.filter( + ({ node }) => node.apiSecret === sessionApiSecret) + } + return json({ tenantEdges, tenantPageInfo, previousPageUrl, nextPageUrl, isOperator }) } export default function TenantsPage() { - const { tenants, previousPageUrl, nextPageUrl } = + const { tenantEdges, tenantPageInfo, previousPageUrl, nextPageUrl, isOperator } = useLoaderData() const navigate = useNavigate() @@ -46,9 +66,11 @@ export default function TenantsPage() {

Tenants

- + {isOperator && ( + + )}
@@ -56,8 +78,8 @@ export default function TenantsPage() { columns={['ID', 'Public name', 'Email', 'Status', 'Operator']} /> - {tenants.edges.length ? ( - tenants.edges.map((tenant) => ( + {tenantEdges.length ? ( + tenantEdges.map((tenant) => ( @@ -165,7 +165,7 @@ export default function ViewTenantPage() { aria-label='save ip information' type='submit' name='intent' - value='general' + value='ip' > {currentPageAction ? 'Saving ...' : 'Save'} @@ -222,7 +222,9 @@ export async function action({ request }: ActionFunctionArgs) { formData.delete('intent') switch (intent) { - case 'general': { + case 'general': + case 'ip': + case 'sensitive': { const result = updateTenantSchema.safeParse(Object.fromEntries(formData)) if (!result.success) { actionResponse.errors.general.fieldErrors = diff --git a/packages/frontend/app/routes/tenants._index.tsx b/packages/frontend/app/routes/tenants._index.tsx index 2fab521585..6470872a60 100644 --- a/packages/frontend/app/routes/tenants._index.tsx +++ b/packages/frontend/app/routes/tenants._index.tsx @@ -46,16 +46,27 @@ export const loader = async ({ request }: LoaderFunctionArgs) => { } } } - tenantEdges = isOperator ? tenants.edges : - tenantEdges.filter( - ({ node }) => node.apiSecret === sessionApiSecret) + tenantEdges = isOperator + ? tenants.edges + : tenantEdges.filter(({ node }) => node.apiSecret === sessionApiSecret) } - return json({ tenantEdges, tenantPageInfo, previousPageUrl, nextPageUrl, isOperator }) + return json({ + tenantEdges, + tenantPageInfo, + previousPageUrl, + nextPageUrl, + isOperator + }) } export default function TenantsPage() { - const { tenantEdges, tenantPageInfo, previousPageUrl, nextPageUrl, isOperator } = - useLoaderData() + const { + tenantEdges, + tenantPageInfo, + previousPageUrl, + nextPageUrl, + isOperator + } = useLoaderData() const navigate = useNavigate() return ( @@ -82,8 +93,12 @@ export default function TenantsPage() { tenantEdges.map((tenant) => ( navigate(`/tenants/${tenant.node.id}`)} + className={tenant.node.deletedAt ? '' : 'cursor-pointer'} + onClick={() => + tenant.node.deletedAt + ? 'return' + : navigate(`/tenants/${tenant.node.id}`) + } > {tenant.node.id} From 8bc7d7f53b923751cdba38d0b53d6a3e2521a01e Mon Sep 17 00:00:00 2001 From: Nathan Lie Date: Thu, 30 Jan 2025 16:19:55 -0800 Subject: [PATCH 13/22] chore: cleanup --- packages/backend/jest.env.js | 2 +- packages/backend/scripts/init.sh | 2 -- packages/backend/src/graphql/resolvers/tenant.test.ts | 1 - 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/backend/jest.env.js b/packages/backend/jest.env.js index 8b804444ca..4a8435dd72 100644 --- a/packages/backend/jest.env.js +++ b/packages/backend/jest.env.js @@ -1,4 +1,4 @@ -process.env.LOG_LEVEL = 'info' +process.env.LOG_LEVEL = 'silent' process.env.INSTANCE_NAME = 'Rafiki' process.env.KEY_ID = 'myKey' process.env.OPEN_PAYMENTS_URL = 'http://127.0.0.1:3000' diff --git a/packages/backend/scripts/init.sh b/packages/backend/scripts/init.sh index 6f3b607aa9..a1fa255c7d 100755 --- a/packages/backend/scripts/init.sh +++ b/packages/backend/scripts/init.sh @@ -3,8 +3,6 @@ set -e psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL DROP DATABASE IF EXISTS TESTING; - DROP DATABASE IF EXISTS TENANT_TESTING; CREATE DATABASE testing; - CREATE DATABASE tenant_testing; CREATE DATABASE development; EOSQL diff --git a/packages/backend/src/graphql/resolvers/tenant.test.ts b/packages/backend/src/graphql/resolvers/tenant.test.ts index 628be01be6..177ed85015 100644 --- a/packages/backend/src/graphql/resolvers/tenant.test.ts +++ b/packages/backend/src/graphql/resolvers/tenant.test.ts @@ -99,7 +99,6 @@ describe('Tenant Resolvers', (): void => { ${false} | ${'tenant'} `('whoami query as $description', async ({ isOperator }): Promise => { const tenant = await createTenant(deps) - console.log('created tenant') const client = isOperator ? appContainer.apolloClient : createTenantedApolloClient(appContainer, tenant.id) From 4de527ddfde99e2bab669c0dd3afda83a7d109a7 Mon Sep 17 00:00:00 2001 From: koekiebox Date: Tue, 4 Feb 2025 09:57:52 +0100 Subject: [PATCH 14/22] feat(2915): merged with tenant branch. updates to update screen. --- .../generated/graphql.ts | 3 ++ .../src/graphql/generated/graphql.schema.json | 16 ++++++++ .../backend/src/graphql/generated/graphql.ts | 3 ++ packages/frontend/app/generated/graphql.ts | 41 +++++++++++++++++++ .../frontend/app/routes/tenants.$tenantId.tsx | 36 ++++++++++++---- .../frontend/app/routes/tenants.create.tsx | 1 + .../wallet-addresses.$walletAddressId.tsx | 2 +- .../src/generated/graphql.ts | 3 ++ test/integration/lib/generated/graphql.ts | 3 ++ 9 files changed, 100 insertions(+), 8 deletions(-) diff --git a/localenv/mock-account-servicing-entity/generated/graphql.ts b/localenv/mock-account-servicing-entity/generated/graphql.ts index 5296c25729..31b6c2cff5 100644 --- a/localenv/mock-account-servicing-entity/generated/graphql.ts +++ b/localenv/mock-account-servicing-entity/generated/graphql.ts @@ -1454,6 +1454,8 @@ export type Tenant = Model & { idpConsentUrl?: Maybe; /** Secret used to secure requests from the tenant's identity provider. */ idpSecret?: Maybe; + /** Is the tenant an Operator tenant. */ + isOperator: Scalars['Boolean']['output']; /** Public name for the tenant. */ publicName?: Maybe; }; @@ -2544,6 +2546,7 @@ export type TenantResolvers; idpConsentUrl?: Resolver, ParentType, ContextType>; idpSecret?: Resolver, ParentType, ContextType>; + isOperator?: Resolver; publicName?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; }; diff --git a/packages/backend/src/graphql/generated/graphql.schema.json b/packages/backend/src/graphql/generated/graphql.schema.json index f441f81038..efcaca00e0 100644 --- a/packages/backend/src/graphql/generated/graphql.schema.json +++ b/packages/backend/src/graphql/generated/graphql.schema.json @@ -8081,6 +8081,22 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "isOperator", + "description": "Is the tenant an Operator tenant.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "publicName", "description": "Public name for the tenant.", diff --git a/packages/backend/src/graphql/generated/graphql.ts b/packages/backend/src/graphql/generated/graphql.ts index 5296c25729..31b6c2cff5 100644 --- a/packages/backend/src/graphql/generated/graphql.ts +++ b/packages/backend/src/graphql/generated/graphql.ts @@ -1454,6 +1454,8 @@ export type Tenant = Model & { idpConsentUrl?: Maybe; /** Secret used to secure requests from the tenant's identity provider. */ idpSecret?: Maybe; + /** Is the tenant an Operator tenant. */ + isOperator: Scalars['Boolean']['output']; /** Public name for the tenant. */ publicName?: Maybe; }; @@ -2544,6 +2546,7 @@ export type TenantResolvers; idpConsentUrl?: Resolver, ParentType, ContextType>; idpSecret?: Resolver, ParentType, ContextType>; + isOperator?: Resolver; publicName?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; }; diff --git a/packages/frontend/app/generated/graphql.ts b/packages/frontend/app/generated/graphql.ts index e7c149d3d2..380e51377c 100644 --- a/packages/frontend/app/generated/graphql.ts +++ b/packages/frontend/app/generated/graphql.ts @@ -1454,6 +1454,8 @@ export type Tenant = Model & { idpConsentUrl?: Maybe; /** Secret used to secure requests from the tenant's identity provider. */ idpSecret?: Maybe; + /** Is the tenant an Operator tenant. */ + isOperator: Scalars['Boolean']['output']; /** Public name for the tenant. */ publicName?: Maybe; }; @@ -2544,6 +2546,7 @@ export type TenantResolvers; idpConsentUrl?: Resolver, ParentType, ContextType>; idpSecret?: Resolver, ParentType, ContextType>; + isOperator?: Resolver; publicName?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; }; @@ -2921,6 +2924,44 @@ export type WithdrawPeerLiquidityVariables = Exact<{ export type WithdrawPeerLiquidity = { __typename?: 'Mutation', createPeerLiquidityWithdrawal?: { __typename?: 'LiquidityMutationResponse', success: boolean } | null }; +export type ListTenantsQueryVariables = Exact<{ + after?: InputMaybe; + before?: InputMaybe; + first?: InputMaybe; + last?: InputMaybe; +}>; + + +export type ListTenantsQuery = { __typename?: 'Query', tenants: { __typename?: 'TenantsConnection', edges: Array<{ __typename?: 'TenantEdge', node: { __typename?: 'Tenant', id: string, email?: string | null, apiSecret: string, idpConsentUrl?: string | null, idpSecret?: string | null, publicName?: string | null, createdAt: string, deletedAt?: string | null, isOperator: boolean } }>, pageInfo: { __typename?: 'PageInfo', startCursor?: string | null, endCursor?: string | null, hasNextPage: boolean, hasPreviousPage: boolean } } }; + +export type CreateTenantMutationVariables = Exact<{ + input: CreateTenantInput; +}>; + + +export type CreateTenantMutation = { __typename?: 'Mutation', createTenant: { __typename?: 'TenantMutationResponse', tenant: { __typename?: 'Tenant', id: string, publicName?: string | null, email?: string | null, apiSecret: string, idpConsentUrl?: string | null, idpSecret?: string | null } } }; + +export type UpdateTenantMutationVariables = Exact<{ + input: UpdateTenantInput; +}>; + + +export type UpdateTenantMutation = { __typename?: 'Mutation', updateTenant: { __typename?: 'TenantMutationResponse', tenant: { __typename?: 'Tenant', id: string, email?: string | null, apiSecret: string, idpConsentUrl?: string | null, idpSecret?: string | null, publicName?: string | null } } }; + +export type DeleteTenantMutationVariables = Exact<{ + id: Scalars['String']['input']; +}>; + + +export type DeleteTenantMutation = { __typename?: 'Mutation', deleteTenant: { __typename?: 'DeleteTenantMutationResponse', success: boolean } }; + +export type GetTenantQueryVariables = Exact<{ + id: Scalars['String']['input']; +}>; + + +export type GetTenantQuery = { __typename?: 'Query', tenant: { __typename?: 'Tenant', id: string, email?: string | null, apiSecret: string, idpConsentUrl?: string | null, idpSecret?: string | null, publicName?: string | null, createdAt: string, deletedAt?: string | null, isOperator: boolean } }; + export type GetWalletAddressQueryVariables = Exact<{ id: Scalars['String']['input']; }>; diff --git a/packages/frontend/app/routes/tenants.$tenantId.tsx b/packages/frontend/app/routes/tenants.$tenantId.tsx index 2f1fb88ff7..79fe76b38d 100644 --- a/packages/frontend/app/routes/tenants.$tenantId.tsx +++ b/packages/frontend/app/routes/tenants.$tenantId.tsx @@ -76,7 +76,7 @@ export default function ViewTenantPage() {
- @@ -99,8 +99,16 @@ export default function ViewTenantPage() { disabled readOnly /> - - + +
diff --git a/packages/frontend/app/routes/wallet-addresses.$walletAddressId.tsx b/packages/frontend/app/routes/wallet-addresses.$walletAddressId.tsx index b2d69907f8..06e45ed102 100644 --- a/packages/frontend/app/routes/wallet-addresses.$walletAddressId.tsx +++ b/packages/frontend/app/routes/wallet-addresses.$walletAddressId.tsx @@ -142,7 +142,7 @@ export default function ViewAssetPage() {

Withdrawal threshold

{walletAddress.asset.withdrawalThreshold ?? - 'No withdrawal threshhold'} + 'No withdrawal threshold'}

diff --git a/packages/mock-account-service-lib/src/generated/graphql.ts b/packages/mock-account-service-lib/src/generated/graphql.ts index 5296c25729..31b6c2cff5 100644 --- a/packages/mock-account-service-lib/src/generated/graphql.ts +++ b/packages/mock-account-service-lib/src/generated/graphql.ts @@ -1454,6 +1454,8 @@ export type Tenant = Model & { idpConsentUrl?: Maybe; /** Secret used to secure requests from the tenant's identity provider. */ idpSecret?: Maybe; + /** Is the tenant an Operator tenant. */ + isOperator: Scalars['Boolean']['output']; /** Public name for the tenant. */ publicName?: Maybe; }; @@ -2544,6 +2546,7 @@ export type TenantResolvers; idpConsentUrl?: Resolver, ParentType, ContextType>; idpSecret?: Resolver, ParentType, ContextType>; + isOperator?: Resolver; publicName?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; }; diff --git a/test/integration/lib/generated/graphql.ts b/test/integration/lib/generated/graphql.ts index 5296c25729..31b6c2cff5 100644 --- a/test/integration/lib/generated/graphql.ts +++ b/test/integration/lib/generated/graphql.ts @@ -1454,6 +1454,8 @@ export type Tenant = Model & { idpConsentUrl?: Maybe; /** Secret used to secure requests from the tenant's identity provider. */ idpSecret?: Maybe; + /** Is the tenant an Operator tenant. */ + isOperator: Scalars['Boolean']['output']; /** Public name for the tenant. */ publicName?: Maybe; }; @@ -2544,6 +2546,7 @@ export type TenantResolvers; idpConsentUrl?: Resolver, ParentType, ContextType>; idpSecret?: Resolver, ParentType, ContextType>; + isOperator?: Resolver; publicName?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; }; From 32587670bdc7dd80dd3197aa5fdc28261ca87bed Mon Sep 17 00:00:00 2001 From: koekiebox Date: Thu, 6 Feb 2025 12:33:18 +0100 Subject: [PATCH 15/22] feat(2915): update fixes. --- .../frontend/app/lib/api/tenant.server.ts | 10 ++++- .../frontend/app/routes/tenants.$tenantId.tsx | 41 +++++++++---------- .../frontend/app/routes/tenants._index.tsx | 33 +++++++++------ .../frontend/app/routes/tenants.create.tsx | 10 ++--- .../wallet-addresses.$walletAddressId.tsx | 2 +- 5 files changed, 54 insertions(+), 42 deletions(-) diff --git a/packages/frontend/app/lib/api/tenant.server.ts b/packages/frontend/app/lib/api/tenant.server.ts index f86115a2f8..d25736e0aa 100644 --- a/packages/frontend/app/lib/api/tenant.server.ts +++ b/packages/frontend/app/lib/api/tenant.server.ts @@ -2,10 +2,18 @@ import { gql } from '@apollo/client' import type { CreateTenantInput, CreateTenantMutation, + UpdateTenantMutationVariables, + UpdateTenantInput, + UpdateTenantMutation, CreateTenantMutationVariables, QueryTenantsArgs, ListTenantsQuery, - ListTenantsQueryVariables + ListTenantsQueryVariables, + DeleteTenantMutationVariables, + DeleteTenantMutation, + QueryTenantArgs, + GetTenantQuery, + GetTenantQueryVariables } from '~/generated/graphql' import { getApolloClient } from '../apollo.server' diff --git a/packages/frontend/app/routes/tenants.$tenantId.tsx b/packages/frontend/app/routes/tenants.$tenantId.tsx index 79fe76b38d..96de7b5219 100644 --- a/packages/frontend/app/routes/tenants.$tenantId.tsx +++ b/packages/frontend/app/routes/tenants.$tenantId.tsx @@ -7,7 +7,6 @@ import { Form, Outlet, useActionData, - useFormAction, useLoaderData, useNavigation, useSubmit @@ -15,7 +14,7 @@ import { import { type FormEvent, useState, useRef } from 'react' import { z } from 'zod' import { DangerZone, PageHeader } from '~/components' -import { Button, ErrorPanel, Input } from '~/components/ui' +import { Button, ErrorPanel, Input, PasswordInput } from '~/components/ui' import { ConfirmationDialog, type ConfirmationDialogRef @@ -39,10 +38,8 @@ export async function loader({ request, params }: LoaderFunctionArgs) { } const tenant = await getTenantInfo(request, { id: result.data }) - - if (!tenant) { + if (!tenant) throw json(null, { status: 404, statusText: 'Tenant not found.' }) - } return json({ tenant }) } @@ -51,14 +48,12 @@ export default function ViewTenantPage() { const { tenant } = useLoaderData() const response = useActionData() const navigation = useNavigation() - const formAction = useFormAction() + const [formData, setFormData] = useState() + const submit = useSubmit() const dialogRef = useRef(null) const isSubmitting = navigation.state === 'submitting' - const currentPageAction = isSubmitting && navigation.formAction === formAction - - const [formData, setFormData] = useState() const submitHandler = (event: FormEvent) => { event.preventDefault() @@ -90,22 +85,25 @@ export default function ViewTenantPage() {
-
+
@@ -117,7 +115,7 @@ export default function ViewTenantPage() { name='intent' value='general' > - {currentPageAction ? 'Saving ...' : 'Save'} + {isSubmitting ? 'Saving ...' : 'Save'}
@@ -132,11 +130,12 @@ export default function ViewTenantPage() {
-
+
- @@ -148,7 +147,7 @@ export default function ViewTenantPage() { name='intent' value='sensitive' > - {currentPageAction ? 'Saving ...' : 'Save'} + {isSubmitting ? 'Saving ...' : 'Save'}
@@ -166,14 +165,16 @@ export default function ViewTenantPage() {
-
+
- @@ -185,7 +186,7 @@ export default function ViewTenantPage() { name='intent' value='ip' > - {currentPageAction ? 'Saving ...' : 'Save'} + {isSubmitting ? 'Saving ...' : 'Save'}
@@ -250,14 +251,10 @@ export async function action({ request }: ActionFunctionArgs) { return json({ ...actionResponse }, { status: 400 }) } - console.log('JASON::form-data', formData) - console.log('JASON::request', result.data) const response = await updateTenant(request, { ...result.data }) - console.log('JASON::response', response) - if (!response?.tenant) { actionResponse.errors.general.message = [ 'Could not update tenant. Please try again!' diff --git a/packages/frontend/app/routes/tenants._index.tsx b/packages/frontend/app/routes/tenants._index.tsx index 6470872a60..cc4f63f804 100644 --- a/packages/frontend/app/routes/tenants._index.tsx +++ b/packages/frontend/app/routes/tenants._index.tsx @@ -85,9 +85,7 @@ export default function TenantsPage() {
- + {tenantEdges.length ? ( tenantEdges.map((tenant) => ( @@ -100,16 +98,32 @@ export default function TenantsPage() { : navigate(`/tenants/${tenant.node.id}`) } > - {tenant.node.id}
{tenant.node.publicName ? ( - {tenant.node.publicName} + {tenant.node.publicName}{' '} + {tenant.node.isOperator && ( + (Operator) + )} ) : ( - No public name + + No public name{' '} + {tenant.node.isOperator && ( + + {' '} + (Operator) + + )} + )} +
+ (ID: {tenant.node.id}) +
@@ -126,13 +140,6 @@ export default function TenantsPage() { Active )} - - {tenant.node.isOperator ? ( - Yes - ) : ( - No - )} - )) ) : ( diff --git a/packages/frontend/app/routes/tenants.create.tsx b/packages/frontend/app/routes/tenants.create.tsx index 07ca17e23f..e579be7ccf 100644 --- a/packages/frontend/app/routes/tenants.create.tsx +++ b/packages/frontend/app/routes/tenants.create.tsx @@ -1,7 +1,7 @@ import { json, type ActionFunctionArgs } from '@remix-run/node' import { Form, useActionData, useNavigation } from '@remix-run/react' import { PageHeader } from '~/components' -import { Button, ErrorPanel, Input } from '~/components/ui' +import { Button, ErrorPanel, Input, PasswordInput } from '~/components/ui' import { createTenant } from '~/lib/api/tenant.server' import { messageStorage, setMessageAndRedirect } from '~/lib/message.server' import { createTenantSchema } from '~/lib/validate.server' @@ -45,7 +45,7 @@ export default function CreateTenantPage() {
@@ -66,9 +66,9 @@ export default function CreateTenantPage() {
- - () const response = useActionData() const navigation = useNavigation() From 8f7884995ed79a7b4dd2fec384c295fc731f4793 Mon Sep 17 00:00:00 2001 From: koekiebox Date: Fri, 7 Feb 2025 10:31:24 +0100 Subject: [PATCH 16/22] feat(3180): bug fixes and testing with non operator tenant. --- packages/frontend/app/generated/graphql.ts | 5 ++ .../frontend/app/lib/api/tenant.server.ts | 15 ++++ .../frontend/app/routes/tenants.$tenantId.tsx | 22 ++++- .../frontend/app/routes/tenants._index.tsx | 81 ++++++++----------- 4 files changed, 73 insertions(+), 50 deletions(-) diff --git a/packages/frontend/app/generated/graphql.ts b/packages/frontend/app/generated/graphql.ts index 380e51377c..2c4d6669c4 100644 --- a/packages/frontend/app/generated/graphql.ts +++ b/packages/frontend/app/generated/graphql.ts @@ -2962,6 +2962,11 @@ export type GetTenantQueryVariables = Exact<{ export type GetTenantQuery = { __typename?: 'Query', tenant: { __typename?: 'Tenant', id: string, email?: string | null, apiSecret: string, idpConsentUrl?: string | null, idpSecret?: string | null, publicName?: string | null, createdAt: string, deletedAt?: string | null, isOperator: boolean } }; +export type WhoAmIQueryVariables = Exact<{ [key: string]: never; }>; + + +export type WhoAmIQuery = { __typename?: 'Query', whoami: { __typename?: 'WhoamiResponse', id: string, isOperator: boolean } }; + export type GetWalletAddressQueryVariables = Exact<{ id: Scalars['String']['input']; }>; diff --git a/packages/frontend/app/lib/api/tenant.server.ts b/packages/frontend/app/lib/api/tenant.server.ts index d25736e0aa..b4139b4b1f 100644 --- a/packages/frontend/app/lib/api/tenant.server.ts +++ b/packages/frontend/app/lib/api/tenant.server.ts @@ -169,3 +169,18 @@ export const getTenantInfo = async ( }) return response.data.tenant } + +export const whoAmI = async (request: Request) => { + const apolloClient = await getApolloClient(request) + const response = await apolloClient.query({ + query: gql` + query WhoAmIQuery { + whoami { + id + isOperator + } + } + ` + }) + return response.data.whoami +} diff --git a/packages/frontend/app/routes/tenants.$tenantId.tsx b/packages/frontend/app/routes/tenants.$tenantId.tsx index 96de7b5219..92cd5d9e8c 100644 --- a/packages/frontend/app/routes/tenants.$tenantId.tsx +++ b/packages/frontend/app/routes/tenants.$tenantId.tsx @@ -19,7 +19,7 @@ import { ConfirmationDialog, type ConfirmationDialogRef } from '~/components/ConfirmationDialog' -import { updateTenant, deleteTenant } from '~/lib/api/tenant.server' +import { updateTenant, deleteTenant, whoAmI } from '~/lib/api/tenant.server' import { messageStorage, setMessageAndRedirect } from '~/lib/message.server' import { updateTenantSchema, uuidSchema } from '~/lib/validate.server' import type { ZodFieldErrors } from '~/shared/types' @@ -41,11 +41,14 @@ export async function loader({ request, params }: LoaderFunctionArgs) { if (!tenant) throw json(null, { status: 404, statusText: 'Tenant not found.' }) - return json({ tenant }) + const me = await whoAmI(request) + const isOperator = me.isOperator + + return json({ tenant, isOperator }) } export default function ViewTenantPage() { - const { tenant } = useLoaderData() + const { tenant, isOperator } = useLoaderData() const response = useActionData() const navigation = useNavigation() const [formData, setFormData] = useState() @@ -95,6 +98,12 @@ export default function ViewTenantPage() { disabled readOnly /> + - diff --git a/packages/frontend/app/routes/tenants._index.tsx b/packages/frontend/app/routes/tenants._index.tsx index cc4f63f804..46397efc10 100644 --- a/packages/frontend/app/routes/tenants._index.tsx +++ b/packages/frontend/app/routes/tenants._index.tsx @@ -2,10 +2,9 @@ import { json, type LoaderFunctionArgs } from '@remix-run/node' import { useLoaderData, useNavigate } from '@remix-run/react' import { Badge, BadgeColor, PageHeader } from '~/components' import { Button, Table } from '~/components/ui' -import { listTenants } from '~/lib/api/tenant.server' +import { getTenantInfo, listTenants, whoAmI } from '~/lib/api/tenant.server' import { paginationSchema } from '~/lib/validate.server' import { checkAuthAndRedirect } from '../lib/kratos_checks.server' -import { getSession } from '~/lib/session.server' export const loader = async ({ request }: LoaderFunctionArgs) => { const cookies = request.headers.get('cookie') @@ -20,36 +19,31 @@ export const loader = async ({ request }: LoaderFunctionArgs) => { throw json(null, { status: 400, statusText: 'Invalid pagination.' }) } - let isOperator = false - const tenants = await listTenants(request, { - ...pagination.data - }) + const me = await whoAmI(request) + const isOperator = me.isOperator + const tenants = isOperator + ? await listTenants(request, { + ...pagination.data + }) + : undefined let previousPageUrl = '', nextPageUrl = '' - if (tenants.pageInfo.hasPreviousPage) - previousPageUrl = `/tenants?before=${tenants.pageInfo.startCursor}` - if (tenants.pageInfo.hasNextPage) - nextPageUrl = `/tenants?after=${tenants.pageInfo.endCursor}` - - let tenantEdges = tenants.edges - const tenantPageInfo = tenants.pageInfo - if (tenantEdges.length) { - const session = await getSession(cookies) - const sessionApiSecret = session.get('apiSecret') - if (sessionApiSecret && sessionApiSecret.length > 0) { - for (const edge of tenantEdges) { - const edgeNode = edge.node - if (edgeNode && sessionApiSecret === edgeNode.apiSecret) { - isOperator = edgeNode.isOperator - break - } - } - } - tenantEdges = isOperator - ? tenants.edges - : tenantEdges.filter(({ node }) => node.apiSecret === sessionApiSecret) + let tenantPageInfo + let tenantEdges + if (tenants) { + if (tenants.pageInfo.hasPreviousPage) + previousPageUrl = `/tenants?before=${tenants.pageInfo.startCursor}` + if (tenants.pageInfo.hasNextPage) + nextPageUrl = `/tenants?after=${tenants.pageInfo.endCursor}` + tenantPageInfo = tenants.pageInfo + tenantEdges = tenants.edges + } else { + const tenantInfo = await getTenantInfo(request, { id: me.id }) + tenantPageInfo = { hasNextPage: false, hasPreviousPage: false } + tenantEdges = [{ node: tenantInfo }] } + return json({ tenantEdges, tenantPageInfo, @@ -100,27 +94,22 @@ export default function TenantsPage() { >
- {tenant.node.publicName ? ( - - {tenant.node.publicName}{' '} - {tenant.node.isOperator && ( - (Operator) - )} - - ) : ( - - No public name{' '} - {tenant.node.isOperator && ( - - {' '} - (Operator) +
+ + {tenant.node.publicName ? ( + + {tenant.node.publicName} + + ) : ( + + No public name )} - )} + {tenant.node.isOperator && ( + Operator + )} +
(ID: {tenant.node.id})
From ae7692531e043fa080810102665c2f616fcd1b6b Mon Sep 17 00:00:00 2001 From: koekiebox Date: Fri, 7 Feb 2025 11:32:00 +0100 Subject: [PATCH 17/22] feat(3180): bug fixes and testing with non operator tenant. --- packages/frontend/app/lib/validate.server.ts | 19 +++++++++---------- .../frontend/app/routes/tenants.$tenantId.tsx | 11 +++++++++-- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/packages/frontend/app/lib/validate.server.ts b/packages/frontend/app/lib/validate.server.ts index c815c0a38e..d15668419f 100644 --- a/packages/frontend/app/lib/validate.server.ts +++ b/packages/frontend/app/lib/validate.server.ts @@ -130,6 +130,14 @@ export const updateWalletAddressSchema = z export const updateTenantSchema = z .object({ + apiSecret: z + .string() + .min(10, { message: 'API Secret should be at least 3 characters long' }) + .max(255, { message: 'Maximum length of API Secret is 255 characters' }) + .regex( + /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/, + { message: 'API Secret should be Base64 encoded.' } + ), publicName: z.string().optional(), email: z .string() @@ -143,15 +151,6 @@ export const updateTenantSchema = z .merge(uuidSchema) export const createTenantSchema = z - .object({ - apiSecret: z - .string() - .min(10, { message: 'API Secret should be at least 3 characters long' }) - .max(255, { message: 'Maximum length of API Secret is 255 characters' }) - .regex( - /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/, - { message: 'API Secret should be Base64 encoded.' } - ) - }) + .object({}) .merge(updateTenantSchema) .omit({ id: true }) diff --git a/packages/frontend/app/routes/tenants.$tenantId.tsx b/packages/frontend/app/routes/tenants.$tenantId.tsx index 92cd5d9e8c..cb96a5efb6 100644 --- a/packages/frontend/app/routes/tenants.$tenantId.tsx +++ b/packages/frontend/app/routes/tenants.$tenantId.tsx @@ -143,8 +143,8 @@ export default function ViewTenantPage() {
@@ -258,7 +258,8 @@ export async function action({ request }: ActionFunctionArgs) { case 'general': case 'ip': case 'sensitive': { - const result = updateTenantSchema.safeParse(Object.fromEntries(formData)) + const formEntries = Object.fromEntries(formData) + const result = updateTenantSchema.safeParse(formEntries) if (!result.success) { actionResponse.errors.general.fieldErrors = result.error.flatten().fieldErrors @@ -275,6 +276,12 @@ export async function action({ request }: ActionFunctionArgs) { ] return json({ ...actionResponse }, { status: 400 }) } + + const me = await whoAmI(request) + // We update the apiSecret of the session in case it changed. + if (formEntries.apiSecret && me.id === formEntries.id) { + session.set('apiSecret', formEntries.apiSecret) + } break } case 'delete': { From e718b64e2768ce17ba347c210a18b2b83ea9fac2 Mon Sep 17 00:00:00 2001 From: koekiebox Date: Wed, 12 Feb 2025 14:53:46 +0100 Subject: [PATCH 18/22] feat(2915): review feedback --- .../generated/graphql.ts | 3 -- .../src/graphql/generated/graphql.schema.json | 16 -------- .../backend/src/graphql/generated/graphql.ts | 3 -- .../backend/src/graphql/resolvers/tenant.ts | 4 +- packages/backend/src/graphql/schema.graphql | 2 - packages/frontend/app/generated/graphql.ts | 7 +--- .../frontend/app/lib/api/tenant.server.ts | 2 - packages/frontend/app/lib/validate.server.ts | 15 ++----- .../frontend/app/routes/tenants.$tenantId.tsx | 39 +++++++------------ .../frontend/app/routes/tenants._index.tsx | 15 +++---- .../frontend/app/routes/tenants.create.tsx | 28 ++++++++++--- .../src/generated/graphql.ts | 3 -- test/integration/lib/generated/graphql.ts | 3 -- 13 files changed, 47 insertions(+), 93 deletions(-) diff --git a/localenv/mock-account-servicing-entity/generated/graphql.ts b/localenv/mock-account-servicing-entity/generated/graphql.ts index 31b6c2cff5..5296c25729 100644 --- a/localenv/mock-account-servicing-entity/generated/graphql.ts +++ b/localenv/mock-account-servicing-entity/generated/graphql.ts @@ -1454,8 +1454,6 @@ export type Tenant = Model & { idpConsentUrl?: Maybe; /** Secret used to secure requests from the tenant's identity provider. */ idpSecret?: Maybe; - /** Is the tenant an Operator tenant. */ - isOperator: Scalars['Boolean']['output']; /** Public name for the tenant. */ publicName?: Maybe; }; @@ -2546,7 +2544,6 @@ export type TenantResolvers; idpConsentUrl?: Resolver, ParentType, ContextType>; idpSecret?: Resolver, ParentType, ContextType>; - isOperator?: Resolver; publicName?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; }; diff --git a/packages/backend/src/graphql/generated/graphql.schema.json b/packages/backend/src/graphql/generated/graphql.schema.json index efcaca00e0..f441f81038 100644 --- a/packages/backend/src/graphql/generated/graphql.schema.json +++ b/packages/backend/src/graphql/generated/graphql.schema.json @@ -8081,22 +8081,6 @@ "isDeprecated": false, "deprecationReason": null }, - { - "name": "isOperator", - "description": "Is the tenant an Operator tenant.", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, { "name": "publicName", "description": "Public name for the tenant.", diff --git a/packages/backend/src/graphql/generated/graphql.ts b/packages/backend/src/graphql/generated/graphql.ts index 31b6c2cff5..5296c25729 100644 --- a/packages/backend/src/graphql/generated/graphql.ts +++ b/packages/backend/src/graphql/generated/graphql.ts @@ -1454,8 +1454,6 @@ export type Tenant = Model & { idpConsentUrl?: Maybe; /** Secret used to secure requests from the tenant's identity provider. */ idpSecret?: Maybe; - /** Is the tenant an Operator tenant. */ - isOperator: Scalars['Boolean']['output']; /** Public name for the tenant. */ publicName?: Maybe; }; @@ -2546,7 +2544,6 @@ export type TenantResolvers; idpConsentUrl?: Resolver, ParentType, ContextType>; idpSecret?: Resolver, ParentType, ContextType>; - isOperator?: Resolver; publicName?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; }; diff --git a/packages/backend/src/graphql/resolvers/tenant.ts b/packages/backend/src/graphql/resolvers/tenant.ts index fafd95ffab..19e86bd077 100644 --- a/packages/backend/src/graphql/resolvers/tenant.ts +++ b/packages/backend/src/graphql/resolvers/tenant.ts @@ -10,7 +10,6 @@ import { GraphQLErrorCode } from '../errors' import { Tenant } from '../../tenants/model' import { Pagination, SortOrder } from '../../shared/baseModel' import { getPageInfo } from '../../shared/pagination' -import { Config } from '../../config/app' export const whoami: QueryResolvers['whoami'] = async ( parent, @@ -175,7 +174,6 @@ export function tenantToGraphQl(tenant: Tenant): SchemaTenant { createdAt: new Date(+tenant.createdAt).toISOString(), deletedAt: tenant.deletedAt ? new Date(+tenant.deletedAt).toISOString() - : null, - isOperator: tenant.apiSecret === Config.adminApiSecret + : null } } diff --git a/packages/backend/src/graphql/schema.graphql b/packages/backend/src/graphql/schema.graphql index b73e960170..78218b01a1 100644 --- a/packages/backend/src/graphql/schema.graphql +++ b/packages/backend/src/graphql/schema.graphql @@ -1551,8 +1551,6 @@ type Tenant implements Model { createdAt: String! "The date and time that this tenant was deleted." deletedAt: String - "Is the tenant an Operator tenant." - isOperator: Boolean! } type TenantsConnection { diff --git a/packages/frontend/app/generated/graphql.ts b/packages/frontend/app/generated/graphql.ts index 2c4d6669c4..4668c59a95 100644 --- a/packages/frontend/app/generated/graphql.ts +++ b/packages/frontend/app/generated/graphql.ts @@ -1454,8 +1454,6 @@ export type Tenant = Model & { idpConsentUrl?: Maybe; /** Secret used to secure requests from the tenant's identity provider. */ idpSecret?: Maybe; - /** Is the tenant an Operator tenant. */ - isOperator: Scalars['Boolean']['output']; /** Public name for the tenant. */ publicName?: Maybe; }; @@ -2546,7 +2544,6 @@ export type TenantResolvers; idpConsentUrl?: Resolver, ParentType, ContextType>; idpSecret?: Resolver, ParentType, ContextType>; - isOperator?: Resolver; publicName?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; }; @@ -2932,7 +2929,7 @@ export type ListTenantsQueryVariables = Exact<{ }>; -export type ListTenantsQuery = { __typename?: 'Query', tenants: { __typename?: 'TenantsConnection', edges: Array<{ __typename?: 'TenantEdge', node: { __typename?: 'Tenant', id: string, email?: string | null, apiSecret: string, idpConsentUrl?: string | null, idpSecret?: string | null, publicName?: string | null, createdAt: string, deletedAt?: string | null, isOperator: boolean } }>, pageInfo: { __typename?: 'PageInfo', startCursor?: string | null, endCursor?: string | null, hasNextPage: boolean, hasPreviousPage: boolean } } }; +export type ListTenantsQuery = { __typename?: 'Query', tenants: { __typename?: 'TenantsConnection', edges: Array<{ __typename?: 'TenantEdge', node: { __typename?: 'Tenant', id: string, email?: string | null, apiSecret: string, idpConsentUrl?: string | null, idpSecret?: string | null, publicName?: string | null, createdAt: string, deletedAt?: string | null } }>, pageInfo: { __typename?: 'PageInfo', startCursor?: string | null, endCursor?: string | null, hasNextPage: boolean, hasPreviousPage: boolean } } }; export type CreateTenantMutationVariables = Exact<{ input: CreateTenantInput; @@ -2960,7 +2957,7 @@ export type GetTenantQueryVariables = Exact<{ }>; -export type GetTenantQuery = { __typename?: 'Query', tenant: { __typename?: 'Tenant', id: string, email?: string | null, apiSecret: string, idpConsentUrl?: string | null, idpSecret?: string | null, publicName?: string | null, createdAt: string, deletedAt?: string | null, isOperator: boolean } }; +export type GetTenantQuery = { __typename?: 'Query', tenant: { __typename?: 'Tenant', id: string, email?: string | null, apiSecret: string, idpConsentUrl?: string | null, idpSecret?: string | null, publicName?: string | null, createdAt: string, deletedAt?: string | null } }; export type WhoAmIQueryVariables = Exact<{ [key: string]: never; }>; diff --git a/packages/frontend/app/lib/api/tenant.server.ts b/packages/frontend/app/lib/api/tenant.server.ts index b4139b4b1f..9e01d9d8b2 100644 --- a/packages/frontend/app/lib/api/tenant.server.ts +++ b/packages/frontend/app/lib/api/tenant.server.ts @@ -41,7 +41,6 @@ export const listTenants = async (request: Request, args: QueryTenantsArgs) => { publicName createdAt deletedAt - isOperator } } pageInfo { @@ -161,7 +160,6 @@ export const getTenantInfo = async ( publicName createdAt deletedAt - isOperator } } `, diff --git a/packages/frontend/app/lib/validate.server.ts b/packages/frontend/app/lib/validate.server.ts index d15668419f..973d8bcc5b 100644 --- a/packages/frontend/app/lib/validate.server.ts +++ b/packages/frontend/app/lib/validate.server.ts @@ -132,19 +132,10 @@ export const updateTenantSchema = z .object({ apiSecret: z .string() - .min(10, { message: 'API Secret should be at least 3 characters long' }) - .max(255, { message: 'Maximum length of API Secret is 255 characters' }) - .regex( - /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/, - { message: 'API Secret should be Base64 encoded.' } - ), + .min(10, { message: 'API Secret should be at least 10 characters long' }) + .max(255, { message: 'Maximum length of API Secret is 255 characters' }), publicName: z.string().optional(), - email: z - .string() - .regex(/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/, { - message: 'Invalid email address.' - }) - .optional(), + email: z.string().optional(), idpConsentUrl: z.string().optional(), idpSecret: z.string().optional() }) diff --git a/packages/frontend/app/routes/tenants.$tenantId.tsx b/packages/frontend/app/routes/tenants.$tenantId.tsx index cb96a5efb6..240503849b 100644 --- a/packages/frontend/app/routes/tenants.$tenantId.tsx +++ b/packages/frontend/app/routes/tenants.$tenantId.tsx @@ -31,7 +31,6 @@ export async function loader({ request, params }: LoaderFunctionArgs) { await checkAuthAndRedirect(request.url, cookies) const tenantId = params.tenantId - const result = z.string().uuid().safeParse(tenantId) if (!result.success) { throw json(null, { status: 400, statusText: 'Invalid tenant ID.' }) @@ -42,13 +41,11 @@ export async function loader({ request, params }: LoaderFunctionArgs) { throw json(null, { status: 404, statusText: 'Tenant not found.' }) const me = await whoAmI(request) - const isOperator = me.isOperator - - return json({ tenant, isOperator }) + return json({ tenant, me }) } export default function ViewTenantPage() { - const { tenant, isOperator } = useLoaderData() + const { tenant, me } = useLoaderData() const response = useActionData() const navigation = useNavigation() const [formData, setFormData] = useState() @@ -98,12 +95,6 @@ export default function ViewTenantPage() { disabled readOnly /> -
@@ -205,20 +197,17 @@ export default function ViewTenantPage() { {/* Identity Provider Information - END */} {/* DELETE TENANT - Danger zone */} - -
- - - - -
+ {me.isOperator && me.id !== tenant.id && ( + +
+ + + + +
+ )}
{ tenantPageInfo, previousPageUrl, nextPageUrl, - isOperator + me }) } export default function TenantsPage() { - const { - tenantEdges, - tenantPageInfo, - previousPageUrl, - nextPageUrl, - isOperator - } = useLoaderData() + const { tenantEdges, tenantPageInfo, previousPageUrl, nextPageUrl, me } = + useLoaderData() const navigate = useNavigate() return ( @@ -71,7 +66,7 @@ export default function TenantsPage() {

Tenants

- {isOperator && ( + {me.isOperator && ( @@ -106,7 +101,7 @@ export default function TenantsPage() { )} - {tenant.node.isOperator && ( + {me.isOperator && me.id == tenant.node.id && ( Operator )}
diff --git a/packages/frontend/app/routes/tenants.create.tsx b/packages/frontend/app/routes/tenants.create.tsx index e579be7ccf..fb8a1c1732 100644 --- a/packages/frontend/app/routes/tenants.create.tsx +++ b/packages/frontend/app/routes/tenants.create.tsx @@ -1,8 +1,13 @@ -import { json, type ActionFunctionArgs } from '@remix-run/node' -import { Form, useActionData, useNavigation } from '@remix-run/react' +import { json, type ActionFunctionArgs, redirect } from '@remix-run/node' +import { + Form, + useActionData, + useLoaderData, + useNavigation +} from '@remix-run/react' import { PageHeader } from '~/components' import { Button, ErrorPanel, Input, PasswordInput } from '~/components/ui' -import { createTenant } from '~/lib/api/tenant.server' +import { createTenant, whoAmI } from '~/lib/api/tenant.server' import { messageStorage, setMessageAndRedirect } from '~/lib/message.server' import { createTenantSchema } from '~/lib/validate.server' import type { ZodFieldErrors } from '~/shared/types' @@ -12,13 +17,16 @@ import { type LoaderFunctionArgs } from '@remix-run/node' export const loader = async ({ request }: LoaderFunctionArgs) => { const cookies = request.headers.get('cookie') await checkAuthAndRedirect(request.url, cookies) - return null + const me = await whoAmI(request) + return json({ me }) } export default function CreateTenantPage() { const response = useActionData() const { state } = useNavigation() const isSubmitting = state === 'submitting' + const { me } = useLoaderData() + if (!me || !me.isOperator) throw redirect('tenants') return (
@@ -126,14 +134,22 @@ export async function action({ request }: ActionFunctionArgs) { const formData = Object.fromEntries(await request.formData()) const result = createTenantSchema.safeParse(formData) - if (!result.success) { errors.fieldErrors = result.error.flatten().fieldErrors return json({ errors }, { status: 400 }) } + if ( + result.data.email && + result.data.email.trim().length > 0 && + !new RegExp(/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/).test( + result.data.email + ) + ) { + errors.message = ['Email is invalid.'] + return json({ errors }, { status: 400 }) + } const response = await createTenant(request, { ...result.data }) - if (!response?.tenant) { errors.message = ['Could not create tenant. Please try again!'] return json({ errors }, { status: 400 }) diff --git a/packages/mock-account-service-lib/src/generated/graphql.ts b/packages/mock-account-service-lib/src/generated/graphql.ts index 31b6c2cff5..5296c25729 100644 --- a/packages/mock-account-service-lib/src/generated/graphql.ts +++ b/packages/mock-account-service-lib/src/generated/graphql.ts @@ -1454,8 +1454,6 @@ export type Tenant = Model & { idpConsentUrl?: Maybe; /** Secret used to secure requests from the tenant's identity provider. */ idpSecret?: Maybe; - /** Is the tenant an Operator tenant. */ - isOperator: Scalars['Boolean']['output']; /** Public name for the tenant. */ publicName?: Maybe; }; @@ -2546,7 +2544,6 @@ export type TenantResolvers; idpConsentUrl?: Resolver, ParentType, ContextType>; idpSecret?: Resolver, ParentType, ContextType>; - isOperator?: Resolver; publicName?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; }; diff --git a/test/integration/lib/generated/graphql.ts b/test/integration/lib/generated/graphql.ts index 31b6c2cff5..5296c25729 100644 --- a/test/integration/lib/generated/graphql.ts +++ b/test/integration/lib/generated/graphql.ts @@ -1454,8 +1454,6 @@ export type Tenant = Model & { idpConsentUrl?: Maybe; /** Secret used to secure requests from the tenant's identity provider. */ idpSecret?: Maybe; - /** Is the tenant an Operator tenant. */ - isOperator: Scalars['Boolean']['output']; /** Public name for the tenant. */ publicName?: Maybe; }; @@ -2546,7 +2544,6 @@ export type TenantResolvers; idpConsentUrl?: Resolver, ParentType, ContextType>; idpSecret?: Resolver, ParentType, ContextType>; - isOperator?: Resolver; publicName?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; }; From f90efd064107fb63ef85fa9068598a09a24fd5be Mon Sep 17 00:00:00 2001 From: koekiebox Date: Wed, 12 Feb 2025 16:20:09 +0100 Subject: [PATCH 19/22] feat(2915): fix update. field validation for email. --- .../frontend/app/routes/peers.$peerId.tsx | 1 - .../frontend/app/routes/tenants.$tenantId.tsx | 128 ++++++++---------- .../frontend/app/routes/tenants.create.tsx | 2 +- 3 files changed, 61 insertions(+), 70 deletions(-) diff --git a/packages/frontend/app/routes/peers.$peerId.tsx b/packages/frontend/app/routes/peers.$peerId.tsx index dd2a9428a9..8cadf5e4c7 100644 --- a/packages/frontend/app/routes/peers.$peerId.tsx +++ b/packages/frontend/app/routes/peers.$peerId.tsx @@ -43,7 +43,6 @@ export async function loader({ request, params }: LoaderFunctionArgs) { } const peer = await getPeer(request, { id: result.data }) - if (!peer) { throw json(null, { status: 400, statusText: 'Peer not found.' }) } diff --git a/packages/frontend/app/routes/tenants.$tenantId.tsx b/packages/frontend/app/routes/tenants.$tenantId.tsx index 240503849b..4157b47594 100644 --- a/packages/frontend/app/routes/tenants.$tenantId.tsx +++ b/packages/frontend/app/routes/tenants.$tenantId.tsx @@ -21,7 +21,7 @@ import { } from '~/components/ConfirmationDialog' import { updateTenant, deleteTenant, whoAmI } from '~/lib/api/tenant.server' import { messageStorage, setMessageAndRedirect } from '~/lib/message.server' -import { updateTenantSchema, uuidSchema } from '~/lib/validate.server' +import { createTenantSchema, updateTenantSchema, uuidSchema } from '~/lib/validate.server' import type { ZodFieldErrors } from '~/shared/types' import { checkAuthAndRedirect } from '../lib/kratos_checks.server' import { getTenantInfo } from '~/lib/api/tenant.server' @@ -62,9 +62,7 @@ export default function ViewTenantPage() { } const onConfirm = () => { - if (formData) { - submit(formData, { method: 'post' }) - } + if (formData) submit(formData, { method: 'post' }) } return ( @@ -81,13 +79,14 @@ export default function ViewTenantPage() {

Created at {new Date(tenant.createdAt).toLocaleString()}

- +
+
@@ -122,62 +121,31 @@ export default function ViewTenantPage() {
- {/* Sensitive Info */} -
-
-

Sensitive Information

- -
-
-
-
-
- - -
-
- -
-
- -
-
- {/* Sensitive - END */} {/* Identity Provider Information */}

Identity Provider Information

- +
+
@@ -195,7 +163,30 @@ export default function ViewTenantPage() {
{/* Identity Provider Information - END */} - + {/* Sensitive Info */} +
+
+

Sensitive Information

+ +
+
+ +
+
+ + +
+
+ +
+
+ {/* Sensitive - END */} {/* DELETE TENANT - Danger zone */} {me.isOperator && me.id !== tenant.id && ( @@ -208,34 +199,26 @@ export default function ViewTenantPage() { )} +
-
) } export async function action({ request }: ActionFunctionArgs) { - const actionResponse: { - errors: { - general: { - fieldErrors: ZodFieldErrors - message: string[] - } - } + const errors: { + fieldErrors: ZodFieldErrors + message: string[] } = { - errors: { - general: { - fieldErrors: {}, - message: [] - } - } + fieldErrors: {}, + message: [] } const session = await messageStorage.getSession(request.headers.get('cookie')) @@ -250,9 +233,18 @@ export async function action({ request }: ActionFunctionArgs) { const formEntries = Object.fromEntries(formData) const result = updateTenantSchema.safeParse(formEntries) if (!result.success) { - actionResponse.errors.general.fieldErrors = - result.error.flatten().fieldErrors - return json({ ...actionResponse }, { status: 400 }) + errors.fieldErrors = result.error.flatten().fieldErrors + return json({ ...errors }, { status: 400 }) + } + if ( + result.data.email && + result.data.email.trim().length > 0 && + !new RegExp(/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/).test( + result.data.email + ) + ) { + errors.fieldErrors.email = ['Email is invalid.'] + return json({ errors }, { status: 400 }) } const response = await updateTenant(request, { @@ -260,10 +252,10 @@ export async function action({ request }: ActionFunctionArgs) { }) if (!response?.tenant) { - actionResponse.errors.general.message = [ + errors.message = [ 'Could not update tenant. Please try again!' ] - return json({ ...actionResponse }, { status: 400 }) + return json({ ...errors }, { status: 400 }) } const me = await whoAmI(request) diff --git a/packages/frontend/app/routes/tenants.create.tsx b/packages/frontend/app/routes/tenants.create.tsx index fb8a1c1732..7cebd4b6fb 100644 --- a/packages/frontend/app/routes/tenants.create.tsx +++ b/packages/frontend/app/routes/tenants.create.tsx @@ -145,7 +145,7 @@ export async function action({ request }: ActionFunctionArgs) { result.data.email ) ) { - errors.message = ['Email is invalid.'] + errors.fieldErrors.email = ['Email is invalid.'] return json({ errors }, { status: 400 }) } From 267ef6c44da0fabcbe18dacff77fe91cc37eb347 Mon Sep 17 00:00:00 2001 From: koekiebox Date: Wed, 12 Feb 2025 16:27:50 +0100 Subject: [PATCH 20/22] feat(2915): formatting. --- .../frontend/app/routes/tenants.$tenantId.tsx | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/packages/frontend/app/routes/tenants.$tenantId.tsx b/packages/frontend/app/routes/tenants.$tenantId.tsx index 4157b47594..3aa8d133c5 100644 --- a/packages/frontend/app/routes/tenants.$tenantId.tsx +++ b/packages/frontend/app/routes/tenants.$tenantId.tsx @@ -21,7 +21,8 @@ import { } from '~/components/ConfirmationDialog' import { updateTenant, deleteTenant, whoAmI } from '~/lib/api/tenant.server' import { messageStorage, setMessageAndRedirect } from '~/lib/message.server' -import { createTenantSchema, updateTenantSchema, uuidSchema } from '~/lib/validate.server' +import type { createTenantSchema } from '~/lib/validate.server' +import { updateTenantSchema, uuidSchema } from '~/lib/validate.server' import type { ZodFieldErrors } from '~/shared/types' import { checkAuthAndRedirect } from '../lib/kratos_checks.server' import { getTenantInfo } from '~/lib/api/tenant.server' @@ -86,7 +87,11 @@ export default function ViewTenantPage() {
- +
- + Date: Wed, 12 Feb 2025 16:53:48 +0100 Subject: [PATCH 21/22] feat(2915): fix. --- packages/frontend/app/routes/tenants.$tenantId.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/frontend/app/routes/tenants.$tenantId.tsx b/packages/frontend/app/routes/tenants.$tenantId.tsx index 3aa8d133c5..c974181a6d 100644 --- a/packages/frontend/app/routes/tenants.$tenantId.tsx +++ b/packages/frontend/app/routes/tenants.$tenantId.tsx @@ -80,7 +80,7 @@ export default function ViewTenantPage() {

Created at {new Date(tenant.createdAt).toLocaleString()}

- +
@@ -243,7 +243,7 @@ export async function action({ request }: ActionFunctionArgs) { const result = updateTenantSchema.safeParse(formEntries) if (!result.success) { errors.fieldErrors = result.error.flatten().fieldErrors - return json({ ...errors }, { status: 400 }) + return json({ errors }, { status: 400 }) } if ( result.data.email && @@ -262,7 +262,7 @@ export async function action({ request }: ActionFunctionArgs) { if (!response?.tenant) { errors.message = ['Could not update tenant. Please try again!'] - return json({ ...errors }, { status: 400 }) + return json({ errors }, { status: 400 }) } const me = await whoAmI(request) From 970fcbf6d65385ec29de3d7a4195d7100b66857b Mon Sep 17 00:00:00 2001 From: koekiebox Date: Wed, 12 Feb 2025 17:59:18 +0100 Subject: [PATCH 22/22] feat(2915): review fixes. --- packages/frontend/app/lib/validate.server.ts | 2 +- packages/frontend/app/routes/tenants.$tenantId.tsx | 10 ---------- packages/frontend/app/routes/tenants.create.tsx | 10 ---------- 3 files changed, 1 insertion(+), 21 deletions(-) diff --git a/packages/frontend/app/lib/validate.server.ts b/packages/frontend/app/lib/validate.server.ts index 973d8bcc5b..5cd62b6380 100644 --- a/packages/frontend/app/lib/validate.server.ts +++ b/packages/frontend/app/lib/validate.server.ts @@ -135,7 +135,7 @@ export const updateTenantSchema = z .min(10, { message: 'API Secret should be at least 10 characters long' }) .max(255, { message: 'Maximum length of API Secret is 255 characters' }), publicName: z.string().optional(), - email: z.string().optional(), + email: z.string().email().or(z.literal('')), idpConsentUrl: z.string().optional(), idpSecret: z.string().optional() }) diff --git a/packages/frontend/app/routes/tenants.$tenantId.tsx b/packages/frontend/app/routes/tenants.$tenantId.tsx index c974181a6d..8ba5579c8a 100644 --- a/packages/frontend/app/routes/tenants.$tenantId.tsx +++ b/packages/frontend/app/routes/tenants.$tenantId.tsx @@ -245,16 +245,6 @@ export async function action({ request }: ActionFunctionArgs) { errors.fieldErrors = result.error.flatten().fieldErrors return json({ errors }, { status: 400 }) } - if ( - result.data.email && - result.data.email.trim().length > 0 && - !new RegExp(/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/).test( - result.data.email - ) - ) { - errors.fieldErrors.email = ['Email is invalid.'] - return json({ errors }, { status: 400 }) - } const response = await updateTenant(request, { ...result.data diff --git a/packages/frontend/app/routes/tenants.create.tsx b/packages/frontend/app/routes/tenants.create.tsx index 7cebd4b6fb..a68c0a53fa 100644 --- a/packages/frontend/app/routes/tenants.create.tsx +++ b/packages/frontend/app/routes/tenants.create.tsx @@ -138,16 +138,6 @@ export async function action({ request }: ActionFunctionArgs) { errors.fieldErrors = result.error.flatten().fieldErrors return json({ errors }, { status: 400 }) } - if ( - result.data.email && - result.data.email.trim().length > 0 && - !new RegExp(/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/).test( - result.data.email - ) - ) { - errors.fieldErrors.email = ['Email is invalid.'] - return json({ errors }, { status: 400 }) - } const response = await createTenant(request, { ...result.data }) if (!response?.tenant) {