diff --git a/localenv/cloud-nine-wallet/docker-compose.yml b/localenv/cloud-nine-wallet/docker-compose.yml index cb2eb8b04b..d94136df73 100644 --- a/localenv/cloud-nine-wallet/docker-compose.yml +++ b/localenv/cloud-nine-wallet/docker-compose.yml @@ -77,6 +77,8 @@ services: ENABLE_TELEMETRY: true KEY_ID: 7097F83B-CB84-469E-96C6-2141C72E22C0 AUTH_ADMIN_API_SECRET: rPoZpe9tVyBNCigm05QDco7WLcYa0xMao7lO5KG1XG4= + OPERATOR_IDP_CONSENT_URL: http://localhost:3030/mock-idp/ + OPERATOR_IDP_SECRET: 2pEcn2kkCclbOHQiGNEwhJ0rucATZhrA807HTm2rNXE= depends_on: - shared-database - shared-redis diff --git a/localenv/cloud-nine-wallet/seed.yml b/localenv/cloud-nine-wallet/seed.yml index 81d058e509..5477a47416 100644 --- a/localenv/cloud-nine-wallet/seed.yml +++ b/localenv/cloud-nine-wallet/seed.yml @@ -1,5 +1,5 @@ tenants: - - name: PrimaryTenant + - name: c9PrimaryTenant idpConsentUrl: https://interledger.org/consent idpSecret: myVerySecureSecret endpoints: [ diff --git a/localenv/happy-life-bank/docker-compose.yml b/localenv/happy-life-bank/docker-compose.yml index 489657021e..16794e479b 100644 --- a/localenv/happy-life-bank/docker-compose.yml +++ b/localenv/happy-life-bank/docker-compose.yml @@ -67,7 +67,10 @@ services: WALLET_ADDRESS_URL: ${HAPPY_LIFE_BANK_WALLET_ADDRESS_URL:-https://happy-life-bank-backend/.well-known/pay} ENABLE_TELEMETRY: true KEY_ID: 53f2d913-e98a-40b9-b270-372d0547f23d - AUTH_ADMIN_URL: http://happy-life-bank-auth:4003 + AUTH_ADMIN_URL: http://happy-life-bank-auth:3003/graphql + AUTH_ADMIN_API_SECRET: rPoZpe9tVyBNCigm05QDco7WLcYa0xMao7lO5KG1XG4= + OPERATOR_IDP_CONSENT_URL: http://localhost:3031/mock-idp/ + OPERATOR_IDP_SECRET: 2pEcn2kkCclbOHQiGNEwhJ0rucATZhrA807HTm2rNXE= depends_on: - cloud-nine-backend happy-life-auth: diff --git a/localenv/happy-life-bank/seed.yml b/localenv/happy-life-bank/seed.yml index 1a8e57cff7..daccbfb3bf 100644 --- a/localenv/happy-life-bank/seed.yml +++ b/localenv/happy-life-bank/seed.yml @@ -1,3 +1,17 @@ +tenants: + - name: hlbPrimaryTenant + idpConsentUrl: https://interledger.org/consent + idpSecret: myVerySecureSecret + endpoints: [ + { + "type": "RatesUrl", + "value": "https://interledger.org/rates" + }, + { + "type": "WebhookBaseUrl", + "value": "https://interledger.org/webhooks" + } + ] assets: - code: USD scale: 2 diff --git a/localenv/mock-account-servicing-entity/generated/graphql.ts b/localenv/mock-account-servicing-entity/generated/graphql.ts index 7eeceba0a4..ffb15ca10a 100644 --- a/localenv/mock-account-servicing-entity/generated/graphql.ts +++ b/localenv/mock-account-servicing-entity/generated/graphql.ts @@ -345,10 +345,12 @@ export type CreateTenantEndpointsInput = { }; export type CreateTenantInput = { + /** Email of the tenant */ + email: Scalars['String']['input']; /** List of endpoints types for the tenant */ endpoints: Array; /** IDP Endpoint */ - idpConsentEndpoint: Scalars['String']['input']; + idpConsentUrl: Scalars['String']['input']; /** IDP Secret */ idpSecret: Scalars['String']['input']; }; @@ -1175,7 +1177,7 @@ export type QueryReceiverArgs = { export type QueryTenantArgs = { - id: Scalars['ID']['input']; + id: Scalars['String']['input']; }; @@ -1309,6 +1311,8 @@ export type Tenant = { __typename?: 'Tenant'; /** Date-time of creation */ createdAt: Scalars['String']['output']; + /** Tenant Email for Kratos identity & recovery */ + email: Scalars['String']['output']; /** Tenant ID that is used in subsequent resources */ id: Scalars['ID']['output']; /** Kratos identity ID */ @@ -2323,6 +2327,7 @@ export type SetFeeResponseResolvers = { createdAt?: Resolver; + email?: Resolver; id?: Resolver; kratosIdentityId?: Resolver; updatedAt?: Resolver; diff --git a/packages/backend/migrations/20240827115808_create_tenants_tables.js b/packages/backend/migrations/20240827115808_create_tenants_tables.js index cfa2582e1d..38a2929641 100644 --- a/packages/backend/migrations/20240827115808_create_tenants_tables.js +++ b/packages/backend/migrations/20240827115808_create_tenants_tables.js @@ -6,6 +6,7 @@ exports.up = function (knex) { return knex.schema.createTable('tenants', function (table) { table.uuid('id').primary() table.string('kratosIdentityId').notNullable() + table.string('email').notNullable() table.timestamp('createdAt').defaultTo(knex.fn.now()) table.timestamp('updatedAt').defaultTo(knex.fn.now()) table.timestamp('deletedAt').nullable().defaultTo(null) diff --git a/packages/backend/src/app.ts b/packages/backend/src/app.ts index 587630ee6c..2dc795e8c2 100644 --- a/packages/backend/src/app.ts +++ b/packages/backend/src/app.ts @@ -105,6 +105,7 @@ import { LoggingPlugin } from './graphql/plugin' import { GrantService } from './open_payments/grant/service' import { AuthServerService } from './open_payments/authServer/service' import { TenantService } from './tenant/service' +import { EndpointType } from './tenant/model' export interface AppContextData { logger: Logger container: AppContainer @@ -308,14 +309,15 @@ export class App { } } + // TODO: Move into Kratos service public async createOperatorIdentity(): Promise { const { kratosAdminEmail, kratosAdminUrl } = await this.container.use('config') const logger = await this.container.use('logger') - // TODO: error out since kratos is essentially required if (!kratosAdminUrl || !kratosAdminEmail) { throw new Error('Missing admin configuration') } + let identityId try { const identityQueryResponse = await axios.get( `${kratosAdminUrl}/identities?credentials_identifier=${kratosAdminEmail}` @@ -331,6 +333,7 @@ export class App { logger.debug( `Identity with email ${kratosAdminEmail} exists on the system with the ID: ${identityQueryResponse.data[0].id}` ) + identityId = identityQueryResponse.data.id return } else if (isExistingIdentity && !operatorRole) { // Identity already exists but does not have operator role @@ -345,6 +348,7 @@ export class App { logger.debug( `Successfully created user ${kratosAdminEmail} with ID ${identityResponse.data.id}` ) + identityId = identityResponse.data.id } else { // Identity does not exist logger.debug( @@ -371,17 +375,35 @@ export class App { logger.debug( `Successfully created user ${kratosAdminEmail} with ID ${identityResponse.data.id}` ) + identityId = identityResponse.data.id } const recoveryCodeResponse = await axios.post( `${kratosAdminUrl}/recovery/link`, { - identity_id: identityResponse.data.id + identity_id: identityId } ) logger.info( `Recovery link for ${kratosAdminEmail} at ${recoveryCodeResponse.data.recovery_link}` ) + + // Create tenant if it does not exist + // TODO: check if tenant exists by querying psql for tenant email, then querying kratos for stored kratosId if found + const tenantService = await this.container.use('tenantService') + const operatorTenant = await tenantService.getByIdentity(identityId) + if (!operatorTenant) { + const config = await this.container.use('config') + await tenantService.create({ + idpSecret: config.operatorIdpSecret, + idpConsentEndpoint: config.operatorIdpConsentUrl, + endpoints: [ + { value: config.webhookUrl, type: EndpointType.WebhookBaseUrl } + ], + email: config.kratosAdminEmail, + isOperator: true + }) + } } catch (error) { if (axios.isAxiosError(error)) { logger.error( @@ -492,10 +514,11 @@ export class App { } // Determine Kratos Identity - koa.use(async (ctx: TenantedAppContext, next: Koa.Next): Promise => { - await getTenantIdFromRequestHeaders(ctx, this.config) - return next() - }) + // TODO: Comment out for now until seed script has a good way to acquire a kratos session + // koa.use(async (ctx: TenantedAppContext, next: Koa.Next): Promise => { + // await getTenantIdFromRequestHeaders(ctx, this.config) + // return next() + // }) koa.use( koaMiddleware(this.apolloServer, { diff --git a/packages/backend/src/config/app.ts b/packages/backend/src/config/app.ts index 3f9cc99c33..012b0aa4d1 100644 --- a/packages/backend/src/config/app.ts +++ b/packages/backend/src/config/app.ts @@ -193,7 +193,9 @@ export const Config = { enableSpspPaymentPointers: envBool('ENABLE_SPSP_PAYMENT_POINTERS', true), kratosPublicUrl: envString('KRATOS_PUBLIC_URL'), kratosAdminUrl: envString('KRATOS_ADMIN_URL'), - kratosAdminEmail: envString('KRATOS_ADMIN_EMAIL') + kratosAdminEmail: envString('KRATOS_ADMIN_EMAIL'), + operatorIdpSecret: envString('OPERATOR_IDP_SECRET'), + operatorIdpConsentUrl: envString('OPERATOR_IDP_CONSENT_URL') } function parseRedisTlsConfig( diff --git a/packages/backend/src/graphql/generated/graphql.schema.json b/packages/backend/src/graphql/generated/graphql.schema.json index 4073287bb8..13664b78fa 100644 --- a/packages/backend/src/graphql/generated/graphql.schema.json +++ b/packages/backend/src/graphql/generated/graphql.schema.json @@ -2174,6 +2174,22 @@ "description": null, "fields": null, "inputFields": [ + { + "name": "email", + "description": "Email of the tenant", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "endpoints", "description": "List of endpoints types for the tenant", @@ -2199,7 +2215,7 @@ "deprecationReason": null }, { - "name": "idpConsentEndpoint", + "name": "idpConsentUrl", "description": "IDP Endpoint", "type": { "kind": "NON_NULL", @@ -6905,7 +6921,7 @@ "name": null, "ofType": { "kind": "SCALAR", - "name": "ID", + "name": "String", "ofType": null } }, @@ -7805,6 +7821,22 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "email", + "description": "Tenant Email for Kratos identity & recovery", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "id", "description": "Tenant ID that is used in subsequent resources", diff --git a/packages/backend/src/graphql/generated/graphql.ts b/packages/backend/src/graphql/generated/graphql.ts index 7eeceba0a4..ffb15ca10a 100644 --- a/packages/backend/src/graphql/generated/graphql.ts +++ b/packages/backend/src/graphql/generated/graphql.ts @@ -345,10 +345,12 @@ export type CreateTenantEndpointsInput = { }; export type CreateTenantInput = { + /** Email of the tenant */ + email: Scalars['String']['input']; /** List of endpoints types for the tenant */ endpoints: Array; /** IDP Endpoint */ - idpConsentEndpoint: Scalars['String']['input']; + idpConsentUrl: Scalars['String']['input']; /** IDP Secret */ idpSecret: Scalars['String']['input']; }; @@ -1175,7 +1177,7 @@ export type QueryReceiverArgs = { export type QueryTenantArgs = { - id: Scalars['ID']['input']; + id: Scalars['String']['input']; }; @@ -1309,6 +1311,8 @@ export type Tenant = { __typename?: 'Tenant'; /** Date-time of creation */ createdAt: Scalars['String']['output']; + /** Tenant Email for Kratos identity & recovery */ + email: Scalars['String']['output']; /** Tenant ID that is used in subsequent resources */ id: Scalars['ID']['output']; /** Kratos identity ID */ @@ -2323,6 +2327,7 @@ export type SetFeeResponseResolvers = { createdAt?: Resolver; + email?: Resolver; id?: Resolver; kratosIdentityId?: Resolver; updatedAt?: Resolver; diff --git a/packages/backend/src/graphql/resolvers/tenant.ts b/packages/backend/src/graphql/resolvers/tenant.ts index ffc42877e4..15d9ec180e 100644 --- a/packages/backend/src/graphql/resolvers/tenant.ts +++ b/packages/backend/src/graphql/resolvers/tenant.ts @@ -32,7 +32,7 @@ export const getTenants: QueryResolvers['tenants'] = async ( const order = sortOrder === 'ASC' ? SortOrder.Asc : SortOrder.Desc const tenants = await tenantService.getPage(pagination, order) const pageInfo = await getPageInfo({ - getPage: (pagination: Pagination, sortOrder?: SortOrder) => + getPage: (pagination: Pagination, sortOrder?: SortOrder) => tenantService.getPage(pagination, sortOrder), page: tenants, sortOrder: order @@ -76,14 +76,15 @@ export const createTenant: MutationResolvers['createTenant'] = const tenantService = await ctx.container.use('tenantService') const tenantOrError = await tenantService.create({ - idpConsentEndpoint: args.input.idpConsentEndpoint, + idpConsentEndpoint: args.input.idpConsentUrl, idpSecret: args.input.idpSecret, endpoints: args.input.endpoints.map((endpoint) => { return { value: endpoint.value, type: mapTenantEndpointTypeToModelEndpointType[endpoint.type] } - }) + }), + email: args.input.email }) if (isTenantError(tenantOrError)) { @@ -102,8 +103,9 @@ export const createTenant: MutationResolvers['createTenant'] = export function tenantToGraphql(tenant: Tenant): SchemaTenant { return { id: tenant.id, + email: tenant.email, kratosIdentityId: tenant.kratosIdentityId, - createdAt: new Date(tenant.createdAt).toISOString(), - updatedAt: new Date(tenant.updatedAt).toISOString() + createdAt: tenant.createdAt.toISOString(), + updatedAt: tenant.updatedAt.toISOString() } } diff --git a/packages/backend/src/graphql/schema.graphql b/packages/backend/src/graphql/schema.graphql index 9b8b451562..a71e894626 100644 --- a/packages/backend/src/graphql/schema.graphql +++ b/packages/backend/src/graphql/schema.graphql @@ -1,6 +1,6 @@ type Query { "Fetch a tenant" - tenant(id: ID!): Tenant + tenant(id: String!): Tenant "Fetch a page of tenants" tenants( @@ -1148,11 +1148,14 @@ input CreateTenantEndpointsInput { } input CreateTenantInput { + "Email of the tenant" + email: String! + "List of endpoints types for the tenant" endpoints: [CreateTenantEndpointsInput!]! "IDP Endpoint" - idpConsentEndpoint: String! + idpConsentUrl: String! "IDP Secret" idpSecret: String! @@ -1374,6 +1377,8 @@ type CreateTenantMutationResponse { type Tenant { "Tenant ID that is used in subsequent resources" id: ID! + "Tenant Email for Kratos identity & recovery" + email: String! "Kratos identity ID" kratosIdentityId: String! "Date-time of creation" diff --git a/packages/backend/src/shared/utils.ts b/packages/backend/src/shared/utils.ts index de6b56656a..07610853a1 100644 --- a/packages/backend/src/shared/utils.ts +++ b/packages/backend/src/shared/utils.ts @@ -207,7 +207,6 @@ export async function getTenantIdFromRequestHeaders( ctx: TenantedAppContext, config: IAppConfig ): Promise { - const logger = await ctx.container.use('logger') const cookie = ctx.request.headers['cookie'] const session = await axios.get(`${config.kratosPublicUrl}/sessions/whoami`, { headers: { @@ -222,7 +221,7 @@ export async function getTenantIdFromRequestHeaders( const identityId = session.data?.identity.id const tenantService = await ctx.container.use('tenantService') - const tenant = await tenantService.getByKratosId(identityId) + const tenant = await tenantService.getByIdentity(identityId) if (!tenant) { ctx.throw(401, 'Unauthorized') } diff --git a/packages/backend/src/tenant/model.ts b/packages/backend/src/tenant/model.ts index 4c3a76c756..ee160e9ccd 100644 --- a/packages/backend/src/tenant/model.ts +++ b/packages/backend/src/tenant/model.ts @@ -11,6 +11,7 @@ export class Tenant extends BaseModel { return 'tenants' } + public email!: string public kratosIdentityId!: string public deletedAt?: Date } diff --git a/packages/backend/src/tenant/service.ts b/packages/backend/src/tenant/service.ts index 099be87e33..0bbcfecd9f 100644 --- a/packages/backend/src/tenant/service.ts +++ b/packages/backend/src/tenant/service.ts @@ -1,3 +1,4 @@ +import axios from 'axios' import { TransactionOrKnex } from 'objection' import { BaseService } from '../shared/baseService' import { TenantError } from './errors' @@ -8,7 +9,6 @@ import { Tenant as AuthTenant, CreateTenantInput as CreateAuthTenantInput } from '../generated/graphql' -import { v4 as uuidv4 } from 'uuid' import { Pagination, SortOrder } from '../shared/baseModel' export interface EndpointOptions { @@ -20,10 +20,13 @@ export interface CreateTenantOptions { idpConsentEndpoint: string idpSecret: string endpoints: EndpointOptions[] + email: string + isOperator?: boolean } export interface TenantService { get(id: string): Promise + getByIdentity(id: string): Promise getPage(pagination?: Pagination, sortOrder?: SortOrder): Promise create(CreateOptions: CreateTenantOptions): Promise } @@ -48,6 +51,7 @@ export async function createTenantService( return { get: (id: string) => getTenant(deps, id), + getByIdentity: (id: string) => getByIdentity(deps, id), getPage: (pagination?, sortOrder?) => getTenantsPage(deps, pagination, sortOrder), create: (options: CreateTenantOptions) => createTenant(deps, options) @@ -66,8 +70,17 @@ async function getTenant( deps: ServiceDependencies, id: string ): Promise { - return Tenant.query(deps.knex) - .findById(id) + return Tenant.query(deps.knex).findById(id) +} + +// TODO: tests for this service function +async function getByIdentity( + deps: ServiceDependencies, + id: string +): Promise { + return Tenant.query(deps.knex).findOne({ + kratosIdentityId: id + }) } async function createTenant( @@ -81,54 +94,91 @@ async function createTenant( * 3.1 if success, commit DB trx and return tenant data * 3.2 if error, rollback DB trx and return error */ - return deps.knex.transaction(async (trx) => { - let tenant: Tenant - try { - // create tenant on backend - tenant = await Tenant.query(trx).insert({ - kratosIdentityId: uuidv4() - }) - - const tenantEndpointsData = options.endpoints.map((endpoint) => ({ - type: endpoint.type, - value: endpoint.value, - tenantId: tenant.id - })) - - await TenantEndpoints.query(trx) - .insert(tenantEndpointsData) - .returning(['type', 'value']) - - // call auth admin api - const mutation = gql` - mutation CreateAuthTenant($input: CreateTenantInput!) { - createTenant(input: $input) { - tenant { - id - } + const trx = await Tenant.startTransaction() + try { + // create tenant on backend + const { kratosAdminUrl } = deps.config + let identityId + const getIdentityResponse = await axios.get( + `${kratosAdminUrl}/identities?credentials_identifier=${options.email}` + ) + if (getIdentityResponse.data.length > 0 && getIdentityResponse.data[0].id) { + identityId = getIdentityResponse.data[0].id + } else { + const createIdentityResponse = await axios.post( + `${kratosAdminUrl}/identities`, + { + schema_id: 'default', + traits: { + email: options.email + }, + metadata_public: { + operator: options.isOperator + } + }, + { + headers: { + 'Content-Type': 'application/json' } } - ` - const variables = { - input: { - tenantId: tenant.id, - idpSecret: options.idpSecret, - idpConsentEndpoint: options.idpConsentEndpoint - } + ) + + identityId = createIdentityResponse.data.id + } + + const recoveryRequest = await axios.post( + `${kratosAdminUrl}/recovery/link`, + { + identity_id: identityId } + ) + deps.logger.info( + `Recovery link for ${options.email} at ${recoveryRequest.data.recovery_link}` + ) + const tenant = await Tenant.query(trx).insertAndFetch({ + email: options.email, + kratosIdentityId: identityId + }) + + const tenantEndpointsData = options.endpoints.map((endpoint) => ({ + type: endpoint.type, + value: endpoint.value, + tenantId: tenant.id + })) + + await TenantEndpoints.query(trx) + .insert(tenantEndpointsData) + .returning(['type', 'value']) - await deps.apolloClient.mutate< - AuthTenant, - { input: CreateAuthTenantInput } - >({ - mutation, - variables - }) - } catch (err) { - await trx.rollback() - throw err + // call auth admin api + const mutation = gql` + mutation CreateAuthTenant($input: CreateTenantInput!) { + createTenant(input: $input) { + tenant { + id + } + } + } + ` + const variables = { + input: { + tenantId: tenant.id, + idpSecret: options.idpSecret, + idpConsentEndpoint: options.idpConsentEndpoint + } } + await deps.apolloClient.mutate< + AuthTenant, + { input: CreateAuthTenantInput } + >({ + mutation, + variables + }) + await trx.commit() return tenant - }) + } catch (err) { + await trx.rollback() + throw err + } } diff --git a/packages/frontend/app/generated/graphql.ts b/packages/frontend/app/generated/graphql.ts index e7ab79670a..80ba584cdd 100644 --- a/packages/frontend/app/generated/graphql.ts +++ b/packages/frontend/app/generated/graphql.ts @@ -345,10 +345,12 @@ export type CreateTenantEndpointsInput = { }; export type CreateTenantInput = { + /** Email of the tenant */ + email: Scalars['String']['input']; /** List of endpoints types for the tenant */ endpoints: Array; /** IDP Endpoint */ - idpConsentEndpoint: Scalars['String']['input']; + idpConsentUrl: Scalars['String']['input']; /** IDP Secret */ idpSecret: Scalars['String']['input']; }; @@ -1175,7 +1177,7 @@ export type QueryReceiverArgs = { export type QueryTenantArgs = { - id: Scalars['ID']['input']; + id: Scalars['String']['input']; }; @@ -1309,6 +1311,8 @@ export type Tenant = { __typename?: 'Tenant'; /** Date-time of creation */ createdAt: Scalars['String']['output']; + /** Tenant Email for Kratos identity & recovery */ + email: Scalars['String']['output']; /** Tenant ID that is used in subsequent resources */ id: Scalars['ID']['output']; /** Kratos identity ID */ @@ -2323,6 +2327,7 @@ export type SetFeeResponseResolvers = { createdAt?: Resolver; + email?: Resolver; id?: Resolver; kratosIdentityId?: Resolver; updatedAt?: Resolver; @@ -2688,6 +2693,30 @@ export type WithdrawPeerLiquidityVariables = Exact<{ export type WithdrawPeerLiquidity = { __typename?: 'Mutation', createPeerLiquidityWithdrawal?: { __typename?: 'LiquidityMutationResponse', success: boolean } | null }; +export type GetTenantQueryVariables = Exact<{ + id: Scalars['String']['input']; +}>; + + +export type GetTenantQuery = { __typename?: 'Query', tenant?: { __typename?: 'Tenant', id: string } | 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 } }>, 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?: 'CreateTenantMutationResponse', tenant: { __typename?: 'Tenant', id: string } } }; + 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 aab7484e34..57a8e25643 100644 --- a/packages/frontend/app/lib/api/tenant.server.ts +++ b/packages/frontend/app/lib/api/tenant.server.ts @@ -6,11 +6,8 @@ import type { CreateTenantInput, CreateTenantMutation, CreateTenantMutationVariables, - DeleteTenantInput, - DeleteTenantMutation, - DeleteTenantMutationVariables, ListTenantsQuery, - ListTenantsVariables, + ListTenantsQueryVariables, QueryTenantsArgs } from '~/generated/graphql' import { apolloClient } from '../apollo.server' @@ -24,8 +21,6 @@ export const getTenant = async (args: QueryTenantArgs, cookie?: string) => { query GetTenantQuery($id: String!) { tenant(id: $id) { id - email - idpConsentUrl } } `, @@ -39,7 +34,7 @@ export const getTenant = async (args: QueryTenantArgs, cookie?: string) => { export const listTenants = async (args: QueryTenantsArgs, cookie?: string) => { const response = await apolloClient.query< ListTenantsQuery, - ListTenantsVariables + ListTenantsQueryVariables >({ query: gql` query ListTenantsQuery( @@ -52,9 +47,6 @@ export const listTenants = async (args: QueryTenantsArgs, cookie?: string) => { edges { node { id - email - idpConsentUrl - createdAt } } pageInfo { @@ -86,8 +78,6 @@ export const createTenant = async ( createTenant(input: $input) { tenant { id - email - idpConsentUrl } } } @@ -100,31 +90,3 @@ export const createTenant = async ( return response.data?.createTenant } - -export const deleteTenant = async ( - args: DeleteTenantInput, - cookie?: string -) => { - const response = await apolloClient.mutate< - DeleteTenantMutation, - DeleteTenantMutationVariables - >({ - mutation: gql` - mutation DeleteTenantMutation($input: DeleteTenantInput!) { - deleteTenant(input: $input) { - tenant { - id - email - idpConsentUrl - } - } - } - `, - variables: { - input: args - }, - context: { headers: { cookie } } - }) - - return response.data?.deleteTenant -} diff --git a/packages/frontend/app/routes/tenants.$tenantId.tsx b/packages/frontend/app/routes/tenants.$tenantId.tsx index 1f28db77f9..383e943790 100644 --- a/packages/frontend/app/routes/tenants.$tenantId.tsx +++ b/packages/frontend/app/routes/tenants.$tenantId.tsx @@ -1,12 +1,7 @@ -import { - json, - type ActionFunctionArgs, - type LoaderFunctionArgs -} from '@remix-run/node' +import { json, type LoaderFunctionArgs } from '@remix-run/node' import { Form, Outlet, - useActionData, useFormAction, useLoaderData, useNavigation, @@ -20,9 +15,7 @@ import { ConfirmationDialog, type ConfirmationDialogRef } from '~/components/ConfirmationDialog' -import { messageStorage, setMessageAndRedirect } from '~/lib/message.server' -import { uuidSchema } from '~/lib/validate.server' -import { getTenant, deleteTenant } from '~/lib/api/tenant.server' +import { getTenant } from '~/lib/api/tenant.server' import { checkAuthAndRedirect } from '../lib/kratos_checks.server' export async function loader({ request, params }: LoaderFunctionArgs) { @@ -80,34 +73,18 @@ export default function ViewTenantPage() {

General Information

-

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

- - -
@@ -135,53 +112,3 @@ export default function ViewTenantPage() {
) } - -export async function action({ request }: ActionFunctionArgs) { - const cookies = request.headers.get('cookie') - const session = await messageStorage.getSession(cookies) - const formData = await request.formData() - const intent = formData.get('intent') - formData.delete('intent') - - switch (intent) { - 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( - { id: result.data.id }, - cookies as string - ) - 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: '/tenant' - }) - } - default: - throw json(null, { status: 400, statusText: 'Invalid intent.' }) - } -} diff --git a/packages/frontend/app/routes/tenants._index.tsx b/packages/frontend/app/routes/tenants._index.tsx index e861be5bfa..ac9dbc9b8b 100644 --- a/packages/frontend/app/routes/tenants._index.tsx +++ b/packages/frontend/app/routes/tenants._index.tsx @@ -70,8 +70,6 @@ export default function TenantsPage() { onClick={() => navigate(`/tenants/${tenant.node.id}`)} > {tenant.node.id} - {tenant.node.email} - {tenant.node.webhooks} )) ) : ( diff --git a/packages/frontend/app/routes/tenants.create.tsx b/packages/frontend/app/routes/tenants.create.tsx index cad854e136..6b78cd6a7d 100644 --- a/packages/frontend/app/routes/tenants.create.tsx +++ b/packages/frontend/app/routes/tenants.create.tsx @@ -9,6 +9,7 @@ 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' +import { TenantEndpointType } from '~/generated/graphql' export const loader = async ({ request }: LoaderFunctionArgs) => { const cookies = request.headers.get('cookie') @@ -106,10 +107,14 @@ export async function action({ request }: ActionFunctionArgs) { return json({ errors }, { status: 400 }) } + const { webhookUrl, ...restOfData } = result.data const cookies = request.headers.get('cookie') const response = await createTenant( { - ...result.data + ...restOfData, + endpoints: [ + { type: TenantEndpointType.WebhookBaseUrl, value: webhookUrl } + ] }, cookies as string ) diff --git a/packages/mock-account-service-lib/src/generated/graphql.ts b/packages/mock-account-service-lib/src/generated/graphql.ts index 7eeceba0a4..ffb15ca10a 100644 --- a/packages/mock-account-service-lib/src/generated/graphql.ts +++ b/packages/mock-account-service-lib/src/generated/graphql.ts @@ -345,10 +345,12 @@ export type CreateTenantEndpointsInput = { }; export type CreateTenantInput = { + /** Email of the tenant */ + email: Scalars['String']['input']; /** List of endpoints types for the tenant */ endpoints: Array; /** IDP Endpoint */ - idpConsentEndpoint: Scalars['String']['input']; + idpConsentUrl: Scalars['String']['input']; /** IDP Secret */ idpSecret: Scalars['String']['input']; }; @@ -1175,7 +1177,7 @@ export type QueryReceiverArgs = { export type QueryTenantArgs = { - id: Scalars['ID']['input']; + id: Scalars['String']['input']; }; @@ -1309,6 +1311,8 @@ export type Tenant = { __typename?: 'Tenant'; /** Date-time of creation */ createdAt: Scalars['String']['output']; + /** Tenant Email for Kratos identity & recovery */ + email: Scalars['String']['output']; /** Tenant ID that is used in subsequent resources */ id: Scalars['ID']['output']; /** Kratos identity ID */ @@ -2323,6 +2327,7 @@ export type SetFeeResponseResolvers = { createdAt?: Resolver; + email?: Resolver; id?: Resolver; kratosIdentityId?: Resolver; updatedAt?: Resolver; diff --git a/packages/mock-account-service-lib/src/requesters.ts b/packages/mock-account-service-lib/src/requesters.ts index 308ea7788b..98b18484d5 100644 --- a/packages/mock-account-service-lib/src/requesters.ts +++ b/packages/mock-account-service-lib/src/requesters.ts @@ -35,7 +35,8 @@ export function createRequesters( createTenant: ( idpConsentUrl: string, idpSecret: string, - endpoints: EndpointType[] + endpoints: EndpointType[], + email: string ) => Promise createPeer: ( staticIlpAddress: string, @@ -82,8 +83,15 @@ export function createRequesters( return { createAsset: (code, scale, liquidityThreshold) => createAsset(apolloClient, code, scale, liquidityThreshold), - createTenant: (idpConsentUrl, idpSecret, endpoints) => - createTenant(apolloClient, logger, idpConsentUrl, idpSecret, endpoints), + createTenant: (idpConsentUrl, idpSecret, endpoints, email) => + createTenant( + apolloClient, + logger, + idpConsentUrl, + idpSecret, + endpoints, + email + ), createPeer: ( staticIlpAddress, outgoingEndpoint, @@ -167,7 +175,8 @@ export async function createTenant( logger: Logger, idpConsentUrl: string, idpSecret: string, - endpoints: EndpointType[] + endpoints: EndpointType[], + email: string ): Promise { const createTenantMutation = gql` mutation CreateTenant($input: CreateTenantInput!) { @@ -183,7 +192,8 @@ export async function createTenant( input: { idpConsentUrl, idpSecret, - endpoints + endpoints, + email } } diff --git a/packages/mock-account-service-lib/src/seed.ts b/packages/mock-account-service-lib/src/seed.ts index bf016a556a..7125889127 100644 --- a/packages/mock-account-service-lib/src/seed.ts +++ b/packages/mock-account-service-lib/src/seed.ts @@ -44,7 +44,12 @@ export async function setupFromSeed( const tenants: Record = {} for (const { name, idpConsentUrl, idpSecret, endpoints } of config.seed .tenants) { - const { tenant } = await createTenant(idpConsentUrl, idpSecret, endpoints) + const { tenant } = await createTenant( + idpConsentUrl, + idpSecret, + endpoints, + `${name}@example.com` + ) if (!tenant) { throw new Error('error creating tenant') } @@ -142,7 +147,7 @@ export async function setupFromSeed( account.name, `${config.publicHost}/${account.path}`, accountAsset.id, - tenants['PrimaryTenant'] + tenants[config.seed.tenants[0].name] ) await mockAccounts.setWalletAddress( diff --git a/test/integration/lib/generated/graphql.ts b/test/integration/lib/generated/graphql.ts index 7eeceba0a4..ffb15ca10a 100644 --- a/test/integration/lib/generated/graphql.ts +++ b/test/integration/lib/generated/graphql.ts @@ -345,10 +345,12 @@ export type CreateTenantEndpointsInput = { }; export type CreateTenantInput = { + /** Email of the tenant */ + email: Scalars['String']['input']; /** List of endpoints types for the tenant */ endpoints: Array; /** IDP Endpoint */ - idpConsentEndpoint: Scalars['String']['input']; + idpConsentUrl: Scalars['String']['input']; /** IDP Secret */ idpSecret: Scalars['String']['input']; }; @@ -1175,7 +1177,7 @@ export type QueryReceiverArgs = { export type QueryTenantArgs = { - id: Scalars['ID']['input']; + id: Scalars['String']['input']; }; @@ -1309,6 +1311,8 @@ export type Tenant = { __typename?: 'Tenant'; /** Date-time of creation */ createdAt: Scalars['String']['output']; + /** Tenant Email for Kratos identity & recovery */ + email: Scalars['String']['output']; /** Tenant ID that is used in subsequent resources */ id: Scalars['ID']['output']; /** Kratos identity ID */ @@ -2323,6 +2327,7 @@ export type SetFeeResponseResolvers = { createdAt?: Resolver; + email?: Resolver; id?: Resolver; kratosIdentityId?: Resolver; updatedAt?: Resolver;