diff --git a/packages/backend/src/app.ts b/packages/backend/src/app.ts index 2dc795e8c2..3469e34bbb 100644 --- a/packages/backend/src/app.ts +++ b/packages/backend/src/app.ts @@ -1,7 +1,6 @@ import { join } from 'path' import http, { Server } from 'http' import { ParsedUrlQuery } from 'querystring' -import axios from 'axios' import { Client as TigerbeetleClient } from 'tigerbeetle-node' import { IocContract } from '@adonisjs/fold' @@ -87,7 +86,7 @@ import { TelemetryService } from './telemetry/service' import { ApolloArmor } from '@escape.tech/graphql-armor' import { openPaymentsServerErrorMiddleware } from './open_payments/route-errors' import { - getTenantIdFromRequestHeaders, + // getTenantIdFromRequestHeaders, verifyApiSignature } from './shared/utils' import { WalletAddress } from './open_payments/wallet_address/model' @@ -313,110 +312,24 @@ export class App { public async createOperatorIdentity(): Promise { const { kratosAdminEmail, kratosAdminUrl } = await this.container.use('config') - const logger = await this.container.use('logger') if (!kratosAdminUrl || !kratosAdminEmail) { throw new Error('Missing admin configuration') } - let identityId - try { - const identityQueryResponse = await axios.get( - `${kratosAdminUrl}/identities?credentials_identifier=${kratosAdminEmail}` - ) - const isExistingIdentity = - identityQueryResponse.data.length > 0 && - identityQueryResponse.data[0].id - const operatorRole = - identityQueryResponse.data[0]?.metadata_public.operator - let identityResponse - if (isExistingIdentity && operatorRole) { - // Identity already exists with operator role - 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 - identityResponse = await axios.put( - `${kratosAdminUrl}/admin/identities/${identityQueryResponse.data[0].id}`, - { - metadata_public: { - operator: true - } - } - ) - logger.debug( - `Successfully created user ${kratosAdminEmail} with ID ${identityResponse.data.id}` - ) - identityId = identityResponse.data.id - } else { - // Identity does not exist - logger.debug( - `No identity with email ${kratosAdminEmail} exists on the system` - ) - - identityResponse = await axios.post( - `${kratosAdminUrl}/identities`, - { - schema_id: 'default', - traits: { - email: kratosAdminEmail - }, - metadata_public: { - operator: true - } - }, - { - headers: { - 'Content-Type': 'application/json' - } - } - ) - 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: 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( - `Error retrieving identity ${kratosAdminEmail}:`, - error.response?.status, - error.response?.data - ) - } else { - logger.error( - `An unexpected error occurred while trying to retrieve the identity for ${kratosAdminEmail}:`, - error - ) - } + // 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.getByEmail(kratosAdminEmail) + 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 + }) } } diff --git a/packages/backend/src/tenant/endpoints/service.ts b/packages/backend/src/tenant/endpoints/service.ts index f3970bbcba..ac4a65f041 100644 --- a/packages/backend/src/tenant/endpoints/service.ts +++ b/packages/backend/src/tenant/endpoints/service.ts @@ -1,6 +1,5 @@ import { TransactionOrKnex } from 'objection' import { BaseService } from '../../shared/baseService' -import { TenantEndpointError } from './errors' import { EndpointType, TenantEndpoint } from './model' import { Pagination, SortOrder } from '../../shared/baseModel' @@ -16,9 +15,7 @@ export interface CreateOptions { } export interface TenantEndpointService { - create( - createOptions: CreateOptions - ): Promise + create(createOptions: CreateOptions): Promise getForTenant(tenantId: string): Promise getPage( pagination?: Pagination, @@ -69,7 +66,7 @@ async function getTenantEndpointsPage( async function createTenantEndpoint( deps: ServiceDependencies, createOptions: CreateOptions -): Promise { +): Promise { const tenantEndpointsData = createOptions.endpoints.map((endpoint) => ({ type: endpoint.type, value: endpoint.value, diff --git a/packages/backend/src/tenant/model.ts b/packages/backend/src/tenant/model.ts index 98a5580115..d0e81f972e 100644 --- a/packages/backend/src/tenant/model.ts +++ b/packages/backend/src/tenant/model.ts @@ -26,6 +26,10 @@ export class Tenant extends BaseModel { public endpoints!: TenantEndpoint[] } +export interface TenantWithEndpoints extends Tenant { + endpoints: NonNullable +} + export enum EndpointType { WebhookBaseUrl = 'WebhookBaseUrl', RatesUrl = 'RatesUrl' diff --git a/packages/backend/src/tenant/service.ts b/packages/backend/src/tenant/service.ts index 1252bfd555..8c54ad2dc6 100644 --- a/packages/backend/src/tenant/service.ts +++ b/packages/backend/src/tenant/service.ts @@ -2,7 +2,7 @@ import axios from 'axios' import { TransactionOrKnex } from 'objection' import { BaseService } from '../shared/baseService' import { TenantError } from './errors' -import { Tenant } from './model' +import { Tenant, TenantWithEndpoints } from './model' import { IAppConfig } from '../config/app' import { ApolloClient, gql, NormalizedCacheObject } from '@apollo/client' import { @@ -22,6 +22,7 @@ export interface CreateTenantOptions { export interface TenantService { get(id: string): Promise + getByEmail(email: string): Promise getByIdentity(id: string): Promise getPage(pagination?: Pagination, sortOrder?: SortOrder): Promise create(createOptions: CreateTenantOptions): Promise @@ -49,6 +50,7 @@ export async function createTenantService( return { get: (id: string) => getTenant(deps, id), + getByEmail: (email: string) => getByEmail(deps, email), getByIdentity: (id: string) => getByIdentity(deps, id), getPage: (pagination?, sortOrder?) => getTenantsPage(deps, pagination, sortOrder), @@ -61,7 +63,9 @@ async function getTenantsPage( pagination?: Pagination, sortOrder?: SortOrder ): Promise { - return await Tenant.query(deps.knex).getPage(pagination, sortOrder) + return await Tenant.query(deps.knex) + .withGraphFetched('endpoints') + .getPage(pagination, sortOrder) } async function getTenant( @@ -71,6 +75,16 @@ async function getTenant( return Tenant.query(deps.knex).withGraphFetched('endpoints').findById(id) } +// TODO: Tests for this +async function getByEmail( + deps: ServiceDependencies, + email: string +): Promise { + return Tenant.query(deps.knex).findOne({ + email + }) +} + // TODO: tests for this service function async function getByIdentity( deps: ServiceDependencies, @@ -84,7 +98,7 @@ async function getByIdentity( async function createTenant( deps: ServiceDependencies, options: CreateTenantOptions -): Promise { +): Promise { /** * 1. Open DB transaction * 2. Insert tenant data into DB @@ -92,90 +106,104 @@ 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 { - const { kratosAdminUrl } = deps.config - let identityId - const getIdentityResponse = await axios.get( - `${kratosAdminUrl}/identities?credentials_identifier=${options.email}` + let tenant: Tenant + const trx = await Tenant.startTransaction() + try { + // TODO: move into kratos service + const { kratosAdminUrl } = deps.config + let identityId + const getIdentityResponse = await axios.get( + `${kratosAdminUrl}/identities?credentials_identifier=${options.email}` + ) + const operatorRole = getIdentityResponse.data[0]?.metadata_public.operator + const isExistingIdentity = + getIdentityResponse.data.length > 0 && getIdentityResponse.data[0].id + if (isExistingIdentity && !options.isOperator) { + identityId = getIdentityResponse.data[0].id + } else if (isExistingIdentity && !operatorRole && options.isOperator) { + // Identity already exists but does not have operator role + const updateIdentityResponse = await axios.put( + `${kratosAdminUrl}/admin/identities/${getIdentityResponse.data[0].id}`, + { + metadata_public: { + operator: true + } + } ) - 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 - } + identityId = updateIdentityResponse.data.id + } else { + const createIdentityResponse = await axios.post( + `${kratosAdminUrl}/identities`, + { + schema_id: 'default', + traits: { + email: options.email }, - { - headers: { - 'Content-Type': 'application/json' - } + metadata_public: { + operator: options.isOperator } - ) - - identityId = createIdentityResponse.data.id - } - - const recoveryRequest = await axios.post( - `${kratosAdminUrl}/recovery/link`, + }, { - identity_id: identityId + headers: { + 'Content-Type': 'application/json' + } } ) - deps.logger.info( - `Recovery link for ${options.email} at ${recoveryRequest.data.recovery_link}` - ) - // create tenant on backend - tenant = await Tenant.query(trx).insert({ - email: options.email, - kratosIdentityId: identityId - }) - await deps.tenantEndpointService.create({ - endpoints: options.endpoints, - tenantId: tenant.id, - trx - }) + identityId = createIdentityResponse.data.id + } - // 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 - } + const recoveryRequest = await axios.post( + `${kratosAdminUrl}/recovery/link`, + { + identity_id: identityId } + ) + deps.logger.info( + `Recovery link for ${options.email} at ${recoveryRequest.data.recovery_link}` + ) + // create tenant on backend + tenant = await Tenant.query(trx).insertAndFetch({ + email: options.email, + kratosIdentityId: identityId + }) + + const endpoints = await deps.tenantEndpointService.create({ + endpoints: options.endpoints, + tenantId: tenant.id, + trx + }) - await deps.apolloClient.mutate< - AuthTenant, - { input: CreateAuthTenantInput } - >({ - mutation, - variables - }) - await trx.commit() - return tenant - } 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() + tenant.endpoints = endpoints + return tenant + } catch (err) { + await trx.rollback() + throw err + } }