From f48bc8f9d54ca8a7c169186cf1095532d284830e Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Tue, 3 Dec 2024 12:08:32 -0500 Subject: [PATCH 01/22] feat(auth): add service api with /healtz endpoint --- packages/auth/src/app.ts | 25 +++++++++++++++++++ packages/auth/src/config/app.ts | 1 + packages/auth/src/index.ts | 3 +++ packages/auth/src/tests/app.ts | 1 + .../docs/integration/prod/docker-compose.mdx | 1 + .../src/partials/auth-variables.mdx | 1 + 6 files changed, 32 insertions(+) diff --git a/packages/auth/src/app.ts b/packages/auth/src/app.ts index dba8efebda..352b43a7db 100644 --- a/packages/auth/src/app.ts +++ b/packages/auth/src/app.ts @@ -113,6 +113,7 @@ export class App { private interactionServer!: Server private introspectionServer!: Server private adminServer!: Server + private serviceAPIServer!: Server private logger!: Logger private config!: IAppConfig private databaseCleanupRules!: { @@ -455,6 +456,23 @@ export class App { this.interactionServer = koa.listen(port) } + public async startServiceAPIServer(port: number | string): Promise { + const koa = await this.createKoaServer() + + const router = new Router() + router.use(bodyParser()) + + router.get('/healthz', (ctx: AppContext): void => { + ctx.status = 200 + }) + + koa.use(cors()) + koa.use(router.middleware()) + koa.use(router.routes()) + + this.serviceAPIServer = koa.listen(port) + } + private async createKoaServer(): Promise> { const koa = new Koa({ proxy: this.config.trustProxy @@ -500,6 +518,9 @@ export class App { if (this.introspectionServer) { await this.stopServer(this.introspectionServer) } + if (this.serviceAPIServer) { + await this.stopServer(this.serviceAPIServer) + } } private async stopServer(server: Server): Promise { @@ -530,6 +551,10 @@ export class App { return this.getPort(this.introspectionServer) } + public getServiceAPIPort(): number { + return this.getPort(this.serviceAPIServer) + } + private getPort(server: Server): number { const address = server?.address() if (address && !(typeof address == 'string')) { diff --git a/packages/auth/src/config/app.ts b/packages/auth/src/config/app.ts index 549d71cb9a..7ac09896f1 100644 --- a/packages/auth/src/config/app.ts +++ b/packages/auth/src/config/app.ts @@ -43,6 +43,7 @@ export const Config = { authPort: envInt('AUTH_PORT', 3006), interactionPort: envInt('INTERACTION_PORT', 3009), introspectionPort: envInt('INTROSPECTION_PORT', 3007), + serviceAPIPort: envInt('SERVICE_API_PORT', 3010), env: envString('NODE_ENV', 'development'), trustProxy: envBool('TRUST_PROXY', false), enableManualMigrations: envBool('ENABLE_MANUAL_MIGRATIONS', false), diff --git a/packages/auth/src/index.ts b/packages/auth/src/index.ts index 356b321cf1..39af694070 100644 --- a/packages/auth/src/index.ts +++ b/packages/auth/src/index.ts @@ -315,6 +315,9 @@ export const start = async ( await app.startIntrospectionServer(config.introspectionPort) logger.info(`Introspection server listening on ${app.getIntrospectionPort()}`) + + await app.startServiceAPIServer(config.serviceAPIPort) + logger.info(`Service API server listening on ${app.getServiceAPIPort()}`) } // If this script is run directly, start the server diff --git a/packages/auth/src/tests/app.ts b/packages/auth/src/tests/app.ts index 87338ba17c..aedf90fad3 100644 --- a/packages/auth/src/tests/app.ts +++ b/packages/auth/src/tests/app.ts @@ -34,6 +34,7 @@ export const createTestApp = async ( config.introspectionPort = 0 config.adminPort = 0 config.interactionPort = 0 + config.serviceAPIPort = 0 const logger = createLogger({ transport: { diff --git a/packages/documentation/src/content/docs/integration/prod/docker-compose.mdx b/packages/documentation/src/content/docs/integration/prod/docker-compose.mdx index b214273aae..bb28379611 100644 --- a/packages/documentation/src/content/docs/integration/prod/docker-compose.mdx +++ b/packages/documentation/src/content/docs/integration/prod/docker-compose.mdx @@ -108,6 +108,7 @@ services: AUTH_PORT: 3006 INTROSPECTION_PORT: 3007 INTERACTION_PORT: 3009 + SERVICE_API_PORT: 3010 COOKIE_KEY: {...} IDENTITY_SERVER_SECRET: {...} IDENTITY_SERVER_URL: {https://idp.mysystem.com} diff --git a/packages/documentation/src/partials/auth-variables.mdx b/packages/documentation/src/partials/auth-variables.mdx index 663ee7a31d..42930d56a5 100644 --- a/packages/documentation/src/partials/auth-variables.mdx +++ b/packages/documentation/src/partials/auth-variables.mdx @@ -33,6 +33,7 @@ import { LinkOut } from '@interledger/docs-design-system' | `INTERACTION_EXPIRY_SECONDS` | `auth.interactionExpirySeconds` | `600` (10 minutes) | The time, in seconds, for which a user can interact with a grant request before the request expires. | | `INTERACTION_PORT` | `auth.port.interaction` | `3009` | The port number of your Open Payments interaction-related APIs. | | `INTROSPECTION_PORT` | `auth.port.introspection` | `3007` | The port of your Open Payments access token introspection server. | +| `SERVICE_API_PORT` | `auth.port.serviceAPIPort` | `3010` | The port to expose the internal service api. | `LIST_ALL_ACCESS_INTERACTION` | `auth.interaction.listAll` | `true` | When `true`, grant requests that include a `list-all` action will require interaction. In these requests, the client asks to list resources that it did not create. | | `LOG_LEVEL` | `auth.logLevel` | `info` | Pino log level | | `NODE_ENV` | `auth.nodeEnv` | `development` | The type of node environment: `development`, `test`, or `production`. | From e593b2a8b87dc0136d44e11ae1b5ad9d363a21fd Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Tue, 3 Dec 2024 12:39:31 -0500 Subject: [PATCH 02/22] feat(auth): tenant routes --- packages/auth/src/app.ts | 7 + packages/auth/src/index.ts | 11 ++ packages/auth/src/tenant/routes.test.ts | 176 ++++++++++++++++++++++++ packages/auth/src/tenant/routes.ts | 123 +++++++++++++++++ 4 files changed, 317 insertions(+) create mode 100644 packages/auth/src/tenant/routes.test.ts create mode 100644 packages/auth/src/tenant/routes.ts diff --git a/packages/auth/src/app.ts b/packages/auth/src/app.ts index 352b43a7db..16fac76afe 100644 --- a/packages/auth/src/app.ts +++ b/packages/auth/src/app.ts @@ -466,6 +466,13 @@ export class App { ctx.status = 200 }) + const tenantRoutes = await this.container.use('tenantRoutes') + + router.get('/tenant/:id', tenantRoutes.get) + router.post('/tenant', tenantRoutes.create) + router.patch('/tenant/:id', tenantRoutes.update) + router.delete('/tenant/:id', tenantRoutes.delete) + koa.use(cors()) koa.use(router.middleware()) koa.use(router.routes()) diff --git a/packages/auth/src/index.ts b/packages/auth/src/index.ts index 39af694070..b997a29f6a 100644 --- a/packages/auth/src/index.ts +++ b/packages/auth/src/index.ts @@ -22,6 +22,7 @@ import { createInteractionService } from './interaction/service' import { getTokenIntrospectionOpenAPI } from 'token-introspection' import { Redis } from 'ioredis' import { createTenantService } from './tenant/service' +import { createTenantRoutes } from './tenant/routes' const container = initIocContainer(Config) const app = new App(container) @@ -163,6 +164,16 @@ export function initIocContainer( } ) + container.singleton( + 'tenantRoutes', + async (deps: IocContract) => { + return createTenantRoutes({ + tenantService: await deps.use('tenantService'), + logger: await deps.use('logger') + }) + } + ) + container.singleton('openApi', async () => { const authServerSpec = await getAuthServerOpenAPI() const idpSpec = await createOpenAPI( diff --git a/packages/auth/src/tenant/routes.test.ts b/packages/auth/src/tenant/routes.test.ts new file mode 100644 index 0000000000..930b963e9f --- /dev/null +++ b/packages/auth/src/tenant/routes.test.ts @@ -0,0 +1,176 @@ +import { IocContract } from '@adonisjs/fold' +import { v4 } from 'uuid' + +import { createContext } from '../tests/context' +import { createTestApp, TestContainer } from '../tests/app' +import { Config } from '../config/app' +import { initIocContainer } from '..' +import { AppServices } from '../app' +import { truncateTables } from '../tests/tableManager' +import { + CreateContext, + UpdateContext, + DeleteContext, + TenantRoutes, + createTenantRoutes +} from './routes' +import { TenantService } from './service' +import { Tenant } from './model' + +describe('Tenant Routes', (): void => { + let deps: IocContract + let appContainer: TestContainer + let tenantRoutes: TenantRoutes + let tenantService: TenantService + + beforeAll(async (): Promise => { + deps = initIocContainer(Config) + appContainer = await createTestApp(deps) + tenantService = await deps.use('tenantService') + const logger = await deps.use('logger') + + tenantRoutes = createTenantRoutes({ + tenantService, + logger + }) + }) + + afterEach(async (): Promise => { + await truncateTables(appContainer.knex) + }) + + afterAll(async (): Promise => { + await appContainer.shutdown() + }) + + describe('create', (): void => { + test('Creates a tenant', async (): Promise => { + const tenantData = { + id: v4(), + idpConsentUrl: 'https://example.com/consent', + idpSecret: 'secret123' + } + + const ctx = createContext( + { + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json' + } + }, + {} + ) + ctx.request.body = tenantData + + await expect(tenantRoutes.create(ctx)).resolves.toBeUndefined() + expect(ctx.status).toBe(201) + expect(ctx.body).toBe(undefined) + + const tenant = await Tenant.query().findById(tenantData.id) + expect(tenant).toBeDefined() + expect(tenant?.idpConsentUrl).toBe(tenantData.idpConsentUrl) + expect(tenant?.idpSecret).toBe(tenantData.idpSecret) + }) + }) + + describe('update', (): void => { + test('Updates a tenant', async (): Promise => { + const tenant = await Tenant.query().insert({ + id: v4(), + idpConsentUrl: 'https://example.com/consent', + idpSecret: 'secret123' + }) + + const updateData = { + idpConsentUrl: 'https://example.com/new-consent', + idpSecret: 'newSecret123' + } + + const ctx = createContext( + { + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json' + } + }, + { + id: tenant.id + } + ) + ctx.request.body = updateData + + await expect(tenantRoutes.update(ctx)).resolves.toBeUndefined() + expect(ctx.status).toBe(200) + expect(ctx.body).toBe(undefined) + + const updatedTenant = await Tenant.query().findById(tenant.id) + expect(updatedTenant?.idpConsentUrl).toBe(updateData.idpConsentUrl) + expect(updatedTenant?.idpSecret).toBe(updateData.idpSecret) + }) + + test('Returns 404 when updating non-existent tenant', async (): Promise => { + const ctx = createContext( + { + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json' + } + }, + { + id: v4() + } + ) + ctx.request.body = { + idpConsentUrl: 'https://example.com/new-consent' + } + + await expect(tenantRoutes.update(ctx)).resolves.toBeUndefined() + expect(ctx.status).toBe(404) + }) + }) + + describe('delete', (): void => { + test('Deletes a tenant', async (): Promise => { + const tenant = await Tenant.query().insert({ + id: v4(), + idpConsentUrl: 'https://example.com/consent', + idpSecret: 'secret123' + }) + + const ctx = createContext( + { + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json' + } + }, + { + id: tenant.id + } + ) + + await expect(tenantRoutes.delete(ctx)).resolves.toBeUndefined() + expect(ctx.status).toBe(204) + + const deletedTenant = await Tenant.query().findById(tenant.id) + expect(deletedTenant?.deletedAt).not.toBeNull() + }) + + test('Returns 404 when deleting non-existent tenant', async (): Promise => { + const ctx = createContext( + { + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json' + } + }, + { + id: v4() + } + ) + + await expect(tenantRoutes.delete(ctx)).resolves.toBeUndefined() + expect(ctx.status).toBe(404) + }) + }) +}) diff --git a/packages/auth/src/tenant/routes.ts b/packages/auth/src/tenant/routes.ts new file mode 100644 index 0000000000..24cecd9e90 --- /dev/null +++ b/packages/auth/src/tenant/routes.ts @@ -0,0 +1,123 @@ +import { ParsedUrlQuery } from 'querystring' +import { AppContext } from '../app' +import { TenantService } from './service' +import { BaseService } from '../shared/baseService' + +type TenantRequest = Exclude< + AppContext['request'], + 'body' +> & { + body: BodyT + query: ParsedUrlQuery & QueryT +} + +type TenantContext = Exclude< + AppContext, + 'request' +> & { + request: TenantRequest +} + +interface CreateTenantBody { + id: string + idpConsentUrl: string + idpSecret: string +} + +type UpdateTenantBody = Partial> + +interface TenantParams { + id: string +} + +export type GetContext = TenantContext +export type CreateContext = TenantContext +export type UpdateContext = TenantContext +export type DeleteContext = TenantContext + +export interface TenantRoutes { + get(ctx: GetContext): Promise + create(ctx: CreateContext): Promise + update(ctx: UpdateContext): Promise + delete(ctx: DeleteContext): Promise +} + +interface ServiceDependencies extends BaseService { + tenantService: TenantService +} + +export function createTenantRoutes({ + tenantService, + logger +}: ServiceDependencies): TenantRoutes { + const log = logger.child({ + service: 'TenantRoutes' + }) + + const deps = { tenantService, logger: log } + + return { + get: (ctx: GetContext) => getTenant(deps, ctx), + create: (ctx: CreateContext) => createTenant(deps, ctx), + update: (ctx: UpdateContext) => updateTenant(deps, ctx), + delete: (ctx: DeleteContext) => deleteTenant(deps, ctx) + } +} + +// TODO: error handling? + +async function createTenant( + deps: ServiceDependencies, + ctx: CreateContext +): Promise { + const { body } = ctx.request + const tenant = await deps.tenantService.create(body) + ctx.status = 201 +} + +async function updateTenant( + deps: ServiceDependencies, + ctx: UpdateContext +): Promise { + const { id } = ctx.params + const { body } = ctx.request + const tenant = await deps.tenantService.update(id, body) + + if (!tenant) { + ctx.status = 404 + return + } + + ctx.status = 200 +} + +async function deleteTenant( + deps: ServiceDependencies, + ctx: DeleteContext +): Promise { + const { id } = ctx.params + const deleted = await deps.tenantService.delete(id) + + if (!deleted) { + ctx.status = 404 + return + } + + ctx.status = 204 +} + +async function getTenant( + deps: ServiceDependencies, + ctx: GetContext +): Promise { + const { id } = ctx.params + const tenant = await deps.tenantService.get(id) + + if (!tenant) { + ctx.status = 404 + return + } + + ctx.status = 200 + ctx.body = tenant +} From cab3e2c8c6064e9b76f8ed35bd58e831ba582ba9 Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Tue, 3 Dec 2024 13:37:47 -0500 Subject: [PATCH 03/22] feat(auth): service api error handling --- packages/auth/src/app.ts | 24 ++++++++++++++++++++++++ packages/auth/src/tenant/routes.ts | 11 ++++------- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/packages/auth/src/app.ts b/packages/auth/src/app.ts index 16fac76afe..32abf9fbfc 100644 --- a/packages/auth/src/app.ts +++ b/packages/auth/src/app.ts @@ -462,6 +462,30 @@ export class App { const router = new Router() router.use(bodyParser()) + const errorHandler = async (ctx: Koa.Context, next: Koa.Next) => { + try { + await next() + } catch (err) { + const logger = await ctx.container.use('logger') + + let message + if (err instanceof Error) { + message = err.message + } + + logger.info({ + method: ctx.method, + route: ctx.path, + headers: ctx.headers, + params: ctx.params, + requestBody: ctx.request.body + }) + logger.error({ err }) + } + } + + koa.use(errorHandler) + router.get('/healthz', (ctx: AppContext): void => { ctx.status = 200 }) diff --git a/packages/auth/src/tenant/routes.ts b/packages/auth/src/tenant/routes.ts index 24cecd9e90..3adc5bd1a7 100644 --- a/packages/auth/src/tenant/routes.ts +++ b/packages/auth/src/tenant/routes.ts @@ -71,7 +71,7 @@ async function createTenant( ctx: CreateContext ): Promise { const { body } = ctx.request - const tenant = await deps.tenantService.create(body) + await deps.tenantService.create(body) ctx.status = 201 } @@ -84,8 +84,7 @@ async function updateTenant( const tenant = await deps.tenantService.update(id, body) if (!tenant) { - ctx.status = 404 - return + ctx.throw(404) } ctx.status = 200 @@ -99,8 +98,7 @@ async function deleteTenant( const deleted = await deps.tenantService.delete(id) if (!deleted) { - ctx.status = 404 - return + ctx.throw(404) } ctx.status = 204 @@ -114,8 +112,7 @@ async function getTenant( const tenant = await deps.tenantService.get(id) if (!tenant) { - ctx.status = 404 - return + ctx.throw(404) } ctx.status = 200 From 0ded2df0aea773d04f03cceae6187bfaaa63b3bb Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Tue, 3 Dec 2024 13:51:37 -0500 Subject: [PATCH 04/22] chore(auth): rm old todo --- packages/auth/src/tenant/routes.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/auth/src/tenant/routes.ts b/packages/auth/src/tenant/routes.ts index 3adc5bd1a7..27e71291a7 100644 --- a/packages/auth/src/tenant/routes.ts +++ b/packages/auth/src/tenant/routes.ts @@ -64,8 +64,6 @@ export function createTenantRoutes({ } } -// TODO: error handling? - async function createTenant( deps: ServiceDependencies, ctx: CreateContext From 7cc448d3bcbab6cd23a9a9a074b1bc55fe51308f Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Tue, 3 Dec 2024 15:02:24 -0500 Subject: [PATCH 05/22] fix(auth): how errors are set --- packages/auth/src/app.ts | 25 +++++++++++-------------- packages/auth/src/tenant/routes.ts | 9 ++++++--- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/packages/auth/src/app.ts b/packages/auth/src/app.ts index 32abf9fbfc..40895cc000 100644 --- a/packages/auth/src/app.ts +++ b/packages/auth/src/app.ts @@ -467,20 +467,17 @@ export class App { await next() } catch (err) { const logger = await ctx.container.use('logger') - - let message - if (err instanceof Error) { - message = err.message - } - - logger.info({ - method: ctx.method, - route: ctx.path, - headers: ctx.headers, - params: ctx.params, - requestBody: ctx.request.body - }) - logger.error({ err }) + logger.info( + { + method: ctx.method, + route: ctx.path, + headers: ctx.headers, + params: ctx.params, + requestBody: ctx.request.body, + err + }, + 'Service API Error' + ) } } diff --git a/packages/auth/src/tenant/routes.ts b/packages/auth/src/tenant/routes.ts index 27e71291a7..d0209e80f8 100644 --- a/packages/auth/src/tenant/routes.ts +++ b/packages/auth/src/tenant/routes.ts @@ -82,7 +82,8 @@ async function updateTenant( const tenant = await deps.tenantService.update(id, body) if (!tenant) { - ctx.throw(404) + ctx.status = 404 + return } ctx.status = 200 @@ -96,7 +97,8 @@ async function deleteTenant( const deleted = await deps.tenantService.delete(id) if (!deleted) { - ctx.throw(404) + ctx.status = 404 + return } ctx.status = 204 @@ -110,7 +112,8 @@ async function getTenant( const tenant = await deps.tenantService.get(id) if (!tenant) { - ctx.throw(404) + ctx.status = 404 + return } ctx.status = 200 From d2b641170f38a2c711e6eba551e62e5e8c777930 Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Tue, 3 Dec 2024 15:57:10 -0500 Subject: [PATCH 06/22] fix(auth): improve tenant tests, cleanup tenant get response, --- packages/auth/src/tenant/routes.test.ts | 51 +++++++++++++++++++++++- packages/auth/src/tenant/routes.ts | 17 +++++++- packages/auth/src/tenant/service.test.ts | 31 ++++++++++---- 3 files changed, 89 insertions(+), 10 deletions(-) diff --git a/packages/auth/src/tenant/routes.test.ts b/packages/auth/src/tenant/routes.test.ts index 930b963e9f..de8ab36154 100644 --- a/packages/auth/src/tenant/routes.test.ts +++ b/packages/auth/src/tenant/routes.test.ts @@ -12,7 +12,8 @@ import { UpdateContext, DeleteContext, TenantRoutes, - createTenantRoutes + createTenantRoutes, + GetContext } from './routes' import { TenantService } from './service' import { Tenant } from './model' @@ -43,6 +44,54 @@ describe('Tenant Routes', (): void => { await appContainer.shutdown() }) + describe('get', (): void => { + test('Gets a tenant', async (): Promise => { + const tenant = await Tenant.query().insert({ + id: v4(), + idpConsentUrl: 'https://example.com/consent', + idpSecret: 'secret123' + }) + + const ctx = createContext( + { + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json' + } + }, + { + id: tenant.id + } + ) + + await expect(tenantRoutes.get(ctx)).resolves.toBeUndefined() + expect(ctx.status).toBe(200) + expect(ctx.body).toEqual({ + id: tenant.id, + idpConsentUrl: tenant.idpConsentUrl, + idpSecret: tenant.idpSecret + }) + }) + + test('Returns 404 when getting non-existent tenant', async (): Promise => { + const ctx = createContext( + { + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json' + } + }, + { + id: v4() + } + ) + + await expect(tenantRoutes.get(ctx)).resolves.toBeUndefined() + expect(ctx.status).toBe(404) + expect(ctx.body).toBeUndefined() + }) + }) + describe('create', (): void => { test('Creates a tenant', async (): Promise => { const tenantData = { diff --git a/packages/auth/src/tenant/routes.ts b/packages/auth/src/tenant/routes.ts index d0209e80f8..e51bd65e12 100644 --- a/packages/auth/src/tenant/routes.ts +++ b/packages/auth/src/tenant/routes.ts @@ -2,6 +2,7 @@ import { ParsedUrlQuery } from 'querystring' import { AppContext } from '../app' import { TenantService } from './service' import { BaseService } from '../shared/baseService' +import { Tenant } from './model' type TenantRequest = Exclude< AppContext['request'], @@ -30,6 +31,12 @@ interface TenantParams { id: string } +interface TenantResponse { + id: string + idpConsentUrl: string + idpSecret: string +} + export type GetContext = TenantContext export type CreateContext = TenantContext export type UpdateContext = TenantContext @@ -117,5 +124,13 @@ async function getTenant( } ctx.status = 200 - ctx.body = tenant + ctx.body = toTenantResponse(tenant) +} + +function toTenantResponse(tenant: Tenant): TenantResponse { + return { + id: tenant.id, + idpConsentUrl: tenant.idpConsentUrl, + idpSecret: tenant.idpSecret + } } diff --git a/packages/auth/src/tenant/service.test.ts b/packages/auth/src/tenant/service.test.ts index fa553b7b18..4428e1f7ab 100644 --- a/packages/auth/src/tenant/service.test.ts +++ b/packages/auth/src/tenant/service.test.ts @@ -39,12 +39,14 @@ describe('Tenant Service', (): void => { const tenantData = createTenantData() const tenant = await tenantService.create(tenantData) - expect(tenant).toMatchObject({ + expect(tenant).toEqual({ id: tenantData.id, idpConsentUrl: tenantData.idpConsentUrl, - idpSecret: tenantData.idpSecret + idpSecret: tenantData.idpSecret, + createdAt: expect.any(Date), + updatedAt: expect.any(Date), + deletedAt: undefined }) - expect(tenant.deletedAt).toBe(undefined) }) test('fails to create tenant with duplicate id', async (): Promise => { @@ -61,7 +63,14 @@ describe('Tenant Service', (): void => { const created = await tenantService.create(tenantData) const tenant = await tenantService.get(created.id) - expect(tenant).toMatchObject(tenantData) + expect(tenant).toEqual({ + id: tenantData.id, + idpConsentUrl: tenantData.idpConsentUrl, + idpSecret: tenantData.idpSecret, + createdAt: expect.any(Date), + updatedAt: expect.any(Date), + deletedAt: null + }) }) test('returns undefined for non-existent tenant', async (): Promise => { @@ -90,9 +99,12 @@ describe('Tenant Service', (): void => { } const updated = await tenantService.update(created.id, updateData) - expect(updated).toMatchObject({ + expect(updated).toEqual({ id: created.id, - ...updateData + ...updateData, + createdAt: expect.any(Date), + updatedAt: expect.any(Date), + deletedAt: null }) }) @@ -105,10 +117,13 @@ describe('Tenant Service', (): void => { } const updated = await tenantService.update(created.id, updateData) - expect(updated).toMatchObject({ + expect(updated).toEqual({ id: created.id, idpConsentUrl: updateData.idpConsentUrl, - idpSecret: created.idpSecret + idpSecret: created.idpSecret, + createdAt: expect.any(Date), + updatedAt: expect.any(Date), + deletedAt: null }) }) From b3eda457d5f6c120082ff286a68fb6575101774f Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Wed, 4 Dec 2024 21:25:33 -0500 Subject: [PATCH 07/22] feat(backend): auth service api client --- .../src/auth-service-client/client.test.ts | 119 ++++++++++++++++++ .../backend/src/auth-service-client/client.ts | 83 ++++++++++++ 2 files changed, 202 insertions(+) create mode 100644 packages/backend/src/auth-service-client/client.test.ts create mode 100644 packages/backend/src/auth-service-client/client.ts diff --git a/packages/backend/src/auth-service-client/client.test.ts b/packages/backend/src/auth-service-client/client.test.ts new file mode 100644 index 0000000000..72425a6e9a --- /dev/null +++ b/packages/backend/src/auth-service-client/client.test.ts @@ -0,0 +1,119 @@ +import { faker } from '@faker-js/faker' +import nock from 'nock' +import { AuthServiceClient, AuthServiceClientError } from './client' + +describe('AuthServiceClient', () => { + const baseUrl = 'http://auth-service.biz' + let client: AuthServiceClient + + beforeEach(() => { + client = new AuthServiceClient(baseUrl) + nock.cleanAll() + }) + + afterEach(() => { + expect(nock.isDone()).toBeTruthy() + }) + + const createTenantData = () => ({ + id: faker.string.uuid(), + idpConsentUrl: faker.internet.url(), + idpSecret: faker.string.alphanumeric(32) + }) + + describe('tenant', () => { + describe('get', () => { + test('retrieves a tenant', async () => { + const tenantData = createTenantData() + + nock(baseUrl).get(`/tenant/${tenantData.id}`).reply(200, tenantData) + + const tenant = await client.tenant.get(tenantData.id) + expect(tenant).toEqual(tenantData) + }) + + test('throws on bad request', async () => { + const id = faker.string.uuid() + + nock(baseUrl).get(`/tenant/${id}`).reply(404) + + await expect(client.tenant.get(id)).rejects.toThrow( + AuthServiceClientError + ) + }) + }) + + describe('create', () => { + test('creates a new tenant', async () => { + const tenantData = createTenantData() + + nock(baseUrl).post('/tenant', tenantData).reply(201) + + await expect(client.tenant.create(tenantData)).resolves.toBe('') + }) + + test('throws on bad request', async () => { + const tenantData = createTenantData() + + nock(baseUrl) + .post('/tenant', tenantData) + .reply(409, { message: 'Tenant already exists' }) + + await expect(client.tenant.create(tenantData)).rejects.toThrow( + AuthServiceClientError + ) + }) + }) + + describe('update', () => { + test('updates an existing tenant', async () => { + const id = faker.string.uuid() + const updateData = { + idpConsentUrl: faker.internet.url(), + idpSecret: faker.string.alphanumeric(32) + } + + nock(baseUrl).patch(`/tenant/${id}`, updateData).reply(200) + + await expect(client.tenant.update(id, updateData)).resolves.toBe('') + }) + + test('throws on bad request', async () => { + const id = faker.string.uuid() + const updateData = { + idpConsentUrl: faker.internet.url() + } + + nock(baseUrl) + .patch(`/tenant/${id}`, updateData) + .reply(404, { message: 'Tenant not found' }) + + await expect(client.tenant.update(id, updateData)).rejects.toThrow( + AuthServiceClientError + ) + }) + }) + + describe('delete', () => { + test('deletes an existing tenant', async () => { + const id = faker.string.uuid() + + nock(baseUrl).delete(`/tenant/${id}`).reply(204) + + await expect(client.tenant.delete(id)).resolves.toBeUndefined() + }) + + test('throws on bad request', async () => { + const id = faker.string.uuid() + + nock(baseUrl) + .delete(`/tenant/${id}`) + .reply(404, { message: 'Tenant not found' }) + + await expect(client.tenant.delete(id)).rejects.toThrow( + AuthServiceClientError + ) + }) + }) + }) +}) diff --git a/packages/backend/src/auth-service-client/client.ts b/packages/backend/src/auth-service-client/client.ts new file mode 100644 index 0000000000..4ce39bac7f --- /dev/null +++ b/packages/backend/src/auth-service-client/client.ts @@ -0,0 +1,83 @@ +interface Tenant { + id: string + idpConsentUrl: string + idpSecret: string +} + +export class AuthServiceClientError extends Error { + constructor( + message: string, + public status: number, + public details?: any + ) { + super(message) + this.status = status + this.details = details + } +} + +export class AuthServiceClient { + private baseUrl: string + + constructor(baseUrl: string) { + this.baseUrl = baseUrl + } + + private async request(path: string, options: RequestInit): Promise { + const response = await fetch(`${this.baseUrl}${path}`, options) + + if (!response.ok) { + let errorDetails + try { + errorDetails = await response.json() + } catch { + errorDetails = { message: response.statusText } + } + + throw new AuthServiceClientError( + `Auth Service Client Error: ${response.status} ${response.statusText}`, + response.status, + errorDetails + ) + } + + if ( + response.status === 204 || + response.headers.get('Content-Length') === '0' + ) { + return undefined as T // TODO: not this. handle the type correctly + } + + const contentType = response.headers.get('Content-Type') + if (contentType && contentType.includes('application/json')) { + try { + return (await response.json()) as T + } catch (error) { + throw new AuthServiceClientError( + `Failed to parse JSON response from ${path}`, + response.status + ) + } + } + + return (await response.text()) as T + } + + public tenant = { + get: (id: string) => + this.request(`/tenant/${id}`, { method: 'GET' }), + create: (data: Omit) => + this.request('/tenant', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data) + }), + update: (id: string, data: Partial>) => + this.request(`/tenant/${id}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data) + }), + delete: (id: string) => this.request(`/tenant/${id}`, { method: 'DELETE' }) + } +} From ecae9f2f037c0959be24984f31e4d18259f2a3d5 Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Wed, 4 Dec 2024 21:33:00 -0500 Subject: [PATCH 08/22] fix(auth): change status codes to 204 where no body --- packages/auth/src/tenant/routes.test.ts | 4 ++-- packages/auth/src/tenant/routes.ts | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/auth/src/tenant/routes.test.ts b/packages/auth/src/tenant/routes.test.ts index de8ab36154..5950452945 100644 --- a/packages/auth/src/tenant/routes.test.ts +++ b/packages/auth/src/tenant/routes.test.ts @@ -112,7 +112,7 @@ describe('Tenant Routes', (): void => { ctx.request.body = tenantData await expect(tenantRoutes.create(ctx)).resolves.toBeUndefined() - expect(ctx.status).toBe(201) + expect(ctx.status).toBe(204) expect(ctx.body).toBe(undefined) const tenant = await Tenant.query().findById(tenantData.id) @@ -149,7 +149,7 @@ describe('Tenant Routes', (): void => { ctx.request.body = updateData await expect(tenantRoutes.update(ctx)).resolves.toBeUndefined() - expect(ctx.status).toBe(200) + expect(ctx.status).toBe(204) expect(ctx.body).toBe(undefined) const updatedTenant = await Tenant.query().findById(tenant.id) diff --git a/packages/auth/src/tenant/routes.ts b/packages/auth/src/tenant/routes.ts index e51bd65e12..6ccbf1f856 100644 --- a/packages/auth/src/tenant/routes.ts +++ b/packages/auth/src/tenant/routes.ts @@ -76,8 +76,10 @@ async function createTenant( ctx: CreateContext ): Promise { const { body } = ctx.request + await deps.tenantService.create(body) - ctx.status = 201 + + ctx.status = 204 } async function updateTenant( @@ -93,7 +95,7 @@ async function updateTenant( return } - ctx.status = 200 + ctx.status = 204 } async function deleteTenant( From 8e51dffc513a54491543a67318223c3ac654d7f4 Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Wed, 4 Dec 2024 21:47:29 -0500 Subject: [PATCH 09/22] fix(backend): format --- packages/backend/src/auth-service-client/client.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/auth-service-client/client.ts b/packages/backend/src/auth-service-client/client.ts index 4ce39bac7f..7362164cd4 100644 --- a/packages/backend/src/auth-service-client/client.ts +++ b/packages/backend/src/auth-service-client/client.ts @@ -8,6 +8,7 @@ export class AuthServiceClientError extends Error { constructor( message: string, public status: number, + // eslint-disable-next-line @typescript-eslint/no-explicit-any public details?: any ) { super(message) @@ -23,7 +24,11 @@ export class AuthServiceClient { this.baseUrl = baseUrl } - private async request(path: string, options: RequestInit): Promise { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private async request( + path: string, + options: RequestInit + ): Promise { const response = await fetch(`${this.baseUrl}${path}`, options) if (!response.ok) { @@ -45,7 +50,7 @@ export class AuthServiceClient { response.status === 204 || response.headers.get('Content-Length') === '0' ) { - return undefined as T // TODO: not this. handle the type correctly + return undefined as T } const contentType = response.headers.get('Content-Type') From 0bab0c4051d012d6472c90ae3a5c8bb91404adca Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Thu, 5 Dec 2024 11:04:09 -0500 Subject: [PATCH 10/22] feat(auth): add required deletedAt to DELETE /tenant body --- packages/auth/src/shared/utils.test.ts | 17 ++++++++++++++++- packages/auth/src/shared/utils.ts | 5 +++++ packages/auth/src/tenant/routes.test.ts | 2 ++ packages/auth/src/tenant/routes.ts | 10 +++++++++- 4 files changed, 32 insertions(+), 2 deletions(-) diff --git a/packages/auth/src/shared/utils.test.ts b/packages/auth/src/shared/utils.test.ts index 8a5b2630ee..5bfbab445a 100644 --- a/packages/auth/src/shared/utils.test.ts +++ b/packages/auth/src/shared/utils.test.ts @@ -6,7 +6,7 @@ import { Config } from '../config/app' import { createContext } from '../tests/context' import { generateApiSignature } from '../tests/apiSignature' import { initIocContainer } from '..' -import { verifyApiSignature } from './utils' +import { verifyApiSignature, isValidDateString } from './utils' import { TestContainer, createTestApp } from '../tests/app' describe('utils', (): void => { @@ -145,4 +145,19 @@ describe('utils', (): void => { expect(verified).toBe(false) }) }) + + describe('isValidDateString', () => { + test.each([ + ['2024-12-05T15:10:09.545Z', true], + ['2024-12-05', true], + ['invalid-date', false], // Invalid date string + ['2024-12-05T25:10:09.545Z', false], // Invalid date string (invalid hour) + ['"2024-12-05T15:10:09.545Z"', false], // Improperly formatted string + ['', false], // Empty string + [null, false], // Null value + [undefined, false] // Undefined value + ])('should return %p for input %p', (input, expected) => { + expect(isValidDateString(input!)).toBe(expected) + }) + }) }) diff --git a/packages/auth/src/shared/utils.ts b/packages/auth/src/shared/utils.ts index c3b6ea5e26..f3b9bcbd84 100644 --- a/packages/auth/src/shared/utils.ts +++ b/packages/auth/src/shared/utils.ts @@ -104,3 +104,8 @@ export async function verifyApiSignature( return verifyApiSignatureDigest(signature as string, ctx.request, config) } + +// Intended for Date strings like "2024-12-05T15:10:09.545Z" (e.g., from new Date().toISOString()) +export function isValidDateString(date: string): boolean { + return !isNaN(Date.parse(date)) +} diff --git a/packages/auth/src/tenant/routes.test.ts b/packages/auth/src/tenant/routes.test.ts index 5950452945..2e3226d4e4 100644 --- a/packages/auth/src/tenant/routes.test.ts +++ b/packages/auth/src/tenant/routes.test.ts @@ -197,6 +197,7 @@ describe('Tenant Routes', (): void => { id: tenant.id } ) + ctx.request.body = { deletedAt: new Date().toISOString() } await expect(tenantRoutes.delete(ctx)).resolves.toBeUndefined() expect(ctx.status).toBe(204) @@ -217,6 +218,7 @@ describe('Tenant Routes', (): void => { id: v4() } ) + ctx.request.body = { deletedAt: new Date().toISOString() } await expect(tenantRoutes.delete(ctx)).resolves.toBeUndefined() expect(ctx.status).toBe(404) diff --git a/packages/auth/src/tenant/routes.ts b/packages/auth/src/tenant/routes.ts index 6ccbf1f856..e4b349d0ee 100644 --- a/packages/auth/src/tenant/routes.ts +++ b/packages/auth/src/tenant/routes.ts @@ -3,6 +3,7 @@ import { AppContext } from '../app' import { TenantService } from './service' import { BaseService } from '../shared/baseService' import { Tenant } from './model' +import { isValidDateString } from '../shared/utils' type TenantRequest = Exclude< AppContext['request'], @@ -40,7 +41,7 @@ interface TenantResponse { export type GetContext = TenantContext export type CreateContext = TenantContext export type UpdateContext = TenantContext -export type DeleteContext = TenantContext +export type DeleteContext = TenantContext<{ deletedAt: string }, TenantParams> export interface TenantRoutes { get(ctx: GetContext): Promise @@ -103,6 +104,13 @@ async function deleteTenant( ctx: DeleteContext ): Promise { const { id } = ctx.params + const { deletedAt: deletedAtString } = ctx.request.body + + if (!isValidDateString(deletedAtString)) { + ctx.status = 400 + return + } + const deleted = await deps.tenantService.delete(id) if (!deleted) { From 5fe1061a5962daae9fbb4d0c1890e4d468e86ca9 Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Wed, 18 Dec 2024 10:38:02 -0500 Subject: [PATCH 11/22] feat(backend): AUTH_SERVICE_API_URL env var --- localenv/cloud-nine-wallet/docker-compose.yml | 2 ++ packages/backend/src/config/app.ts | 1 + 2 files changed, 3 insertions(+) diff --git a/localenv/cloud-nine-wallet/docker-compose.yml b/localenv/cloud-nine-wallet/docker-compose.yml index 6f22a11e27..777d6dd76a 100644 --- a/localenv/cloud-nine-wallet/docker-compose.yml +++ b/localenv/cloud-nine-wallet/docker-compose.yml @@ -67,6 +67,7 @@ services: AUTH_SERVER_INTROSPECTION_URL: http://cloud-nine-wallet-auth:3007 AUTH_ADMIN_API_URL: 'http://cloud-nine-wallet-auth:3003/graphql' AUTH_ADMIN_API_SECRET: 'rPoZpe9tVyBNCigm05QDco7WLcYa0xMao7lO5KG1XG4=' + AUTH_SERVICE_API_URL: 'http://cloud-nine-wallet-auth:3010/' ILP_ADDRESS: ${ILP_ADDRESS:-test.cloud-nine-wallet} STREAM_SECRET: BjPXtnd00G2mRQwP/8ZpwyZASOch5sUXT5o0iR5b5wU= API_SECRET: iyIgCprjb9uL8wFckR+pLEkJWMB7FJhgkvqhTQR/964= @@ -108,6 +109,7 @@ services: - '3006:3006' - "9230:9229" - '3009:3009' + - '3010:3010' environment: NODE_ENV: ${NODE_ENV:-development} TRUST_PROXY: ${TRUST_PROXY} diff --git a/packages/backend/src/config/app.ts b/packages/backend/src/config/app.ts index ec70dbb608..0d51cc501f 100644 --- a/packages/backend/src/config/app.ts +++ b/packages/backend/src/config/app.ts @@ -129,6 +129,7 @@ export const Config = { authAdminApiUrl: envString('AUTH_ADMIN_API_URL'), authAdminApiSecret: envString('AUTH_ADMIN_API_SECRET'), authAdminApiSignatureVersion: envInt('AUTH_ADMIN_API_SIGNATURE_VERSION', 1), + authServiceApiUrl: envString('AUTH_SERVICE_API_URL'), outgoingPaymentWorkers: envInt('OUTGOING_PAYMENT_WORKERS', 1), outgoingPaymentWorkerIdle: envInt('OUTGOING_PAYMENT_WORKER_IDLE', 10), // milliseconds From dba2cd4fe5316148131a88604542bf2a79a474d4 Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Wed, 18 Dec 2024 10:49:44 -0500 Subject: [PATCH 12/22] fix(backend): auth service client tests to mock codes correctly --- .../backend/src/auth-service-client/client.test.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/backend/src/auth-service-client/client.test.ts b/packages/backend/src/auth-service-client/client.test.ts index 72425a6e9a..27b9706f10 100644 --- a/packages/backend/src/auth-service-client/client.test.ts +++ b/packages/backend/src/auth-service-client/client.test.ts @@ -47,9 +47,9 @@ describe('AuthServiceClient', () => { test('creates a new tenant', async () => { const tenantData = createTenantData() - nock(baseUrl).post('/tenant', tenantData).reply(201) + nock(baseUrl).post('/tenant', tenantData).reply(204) - await expect(client.tenant.create(tenantData)).resolves.toBe('') + await expect(client.tenant.create(tenantData)).resolves.toBeUndefined() }) test('throws on bad request', async () => { @@ -73,9 +73,11 @@ describe('AuthServiceClient', () => { idpSecret: faker.string.alphanumeric(32) } - nock(baseUrl).patch(`/tenant/${id}`, updateData).reply(200) + nock(baseUrl).patch(`/tenant/${id}`, updateData).reply(204) - await expect(client.tenant.update(id, updateData)).resolves.toBe('') + await expect( + client.tenant.update(id, updateData) + ).resolves.toBeUndefined() }) test('throws on bad request', async () => { From 02cc1d50d4abe5463ba68d6185badd832ec5f7ce Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Wed, 18 Dec 2024 12:28:40 -0500 Subject: [PATCH 13/22] feat(backend): add AuthServiceClient dep --- packages/backend/jest.env.js | 1 + packages/backend/src/index.ts | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/packages/backend/jest.env.js b/packages/backend/jest.env.js index 5509b4f583..eb93347565 100644 --- a/packages/backend/jest.env.js +++ b/packages/backend/jest.env.js @@ -6,6 +6,7 @@ process.env.ILP_CONNECTOR_URL = 'http://127.0.0.1:3002' process.env.ILP_ADDRESS = 'test.rafiki' process.env.AUTH_SERVER_GRANT_URL = 'http://127.0.0.1:3006' process.env.AUTH_SERVER_INTROSPECTION_URL = 'http://127.0.0.1:3007/' +process.env.AUTH_SERVICE_API_URL = 'http://127.0.0.1:3010' process.env.WEBHOOK_URL = 'http://127.0.0.1:4001/webhook' process.env.STREAM_SECRET = '2/PxuRFV9PAp0yJlnAifJ+1OxujjjI16lN+DBnLNRLA=' process.env.USE_TIGERBEETLE = false diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index b167410756..35a273d0bf 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -74,6 +74,7 @@ import { import { createWebhookService } from './webhook/service' import { createInMemoryDataStore } from './middleware/cache/data-stores/in-memory' import { createTenantService } from './tenants/service' +import { AuthServiceClient } from './auth-service-client/client' BigInt.prototype.toJSON = function () { return this.toString() @@ -220,6 +221,10 @@ export function initIocContainer( return createInMemoryDataStore(config.localCacheDuration) }) + container.singleton('authServiceClient', async () => { + return new AuthServiceClient(config.authServiceApiUrl) + }) + container.singleton('tenantService', async (deps) => { return createTenantService({ logger: await deps.use('logger'), From 563563193ef9a576670fc9595cd4a99c56ae93c6 Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Wed, 18 Dec 2024 14:28:31 -0500 Subject: [PATCH 14/22] feat(backend): use auth service client in tenant service --- .../backend/src/auth-service-client/client.ts | 10 +- packages/backend/src/index.ts | 6 +- packages/backend/src/tenants/service.test.ts | 204 +++++++----------- packages/backend/src/tenants/service.ts | 64 ++---- packages/backend/src/tests/tenant.ts | 10 +- 5 files changed, 100 insertions(+), 194 deletions(-) diff --git a/packages/backend/src/auth-service-client/client.ts b/packages/backend/src/auth-service-client/client.ts index 7362164cd4..5dcfac2c91 100644 --- a/packages/backend/src/auth-service-client/client.ts +++ b/packages/backend/src/auth-service-client/client.ts @@ -25,10 +25,7 @@ export class AuthServiceClient { } // eslint-disable-next-line @typescript-eslint/no-explicit-any - private async request( - path: string, - options: RequestInit - ): Promise { + private async request(path: string, options: RequestInit): Promise { const response = await fetch(`${this.baseUrl}${path}`, options) if (!response.ok) { @@ -71,7 +68,7 @@ export class AuthServiceClient { public tenant = { get: (id: string) => this.request(`/tenant/${id}`, { method: 'GET' }), - create: (data: Omit) => + create: (data: Tenant) => this.request('/tenant', { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -83,6 +80,7 @@ export class AuthServiceClient { headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }), - delete: (id: string) => this.request(`/tenant/${id}`, { method: 'DELETE' }) + delete: (id: string, deletedAt?: Date) => + this.request(`/tenant/${id}`, { method: 'DELETE' }) } } diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 35a273d0bf..c0be42bb28 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -221,7 +221,7 @@ export function initIocContainer( return createInMemoryDataStore(config.localCacheDuration) }) - container.singleton('authServiceClient', async () => { + container.singleton('authServiceClient', () => { return new AuthServiceClient(config.authServiceApiUrl) }) @@ -229,8 +229,8 @@ export function initIocContainer( return createTenantService({ logger: await deps.use('logger'), knex: await deps.use('knex'), - apolloClient: await deps.use('apolloClient'), - tenantCache: await deps.use('tenantCache') + tenantCache: await deps.use('tenantCache'), + authServiceClient: deps.use('authServiceClient') }) }) diff --git a/packages/backend/src/tenants/service.test.ts b/packages/backend/src/tenants/service.test.ts index da6d3b7009..86c96e3c10 100644 --- a/packages/backend/src/tenants/service.test.ts +++ b/packages/backend/src/tenants/service.test.ts @@ -9,47 +9,28 @@ import { createTestApp, TestContainer } from '../tests/app' import { TenantService } from './service' import { Config, IAppConfig } from '../config/app' import { truncateTables } from '../tests/tableManager' -import { ApolloClient, NormalizedCacheObject } from '@apollo/client' import { Tenant } from './model' import { getPageTests } from '../shared/baseModel.test' import { Pagination, SortOrder } from '../shared/baseModel' import { createTenant } from '../tests/tenant' import { CacheDataStore } from '../middleware/cache/data-stores' - -const generateMutateGqlError = (path: string = 'createTenant') => ({ - errors: [ - { - message: 'invalid input syntax', - locations: [ - { - line: 1, - column: 1 - } - ], - path: [path], - extensions: { - code: 'INTERNAl_SERVER_ERROR' - } - } - ], - data: null -}) +import { AuthServiceClient } from '../auth-service-client/client' describe('Tenant Service', (): void => { let deps: IocContract let appContainer: TestContainer let tenantService: TenantService let config: IAppConfig - let apolloClient: ApolloClient let knex: Knex + let authServiceClient: AuthServiceClient beforeAll(async (): Promise => { deps = initIocContainer(Config) appContainer = await createTestApp(deps) tenantService = await deps.use('tenantService') config = await deps.use('config') - apolloClient = await deps.use('apolloClient') knex = await deps.use('knex') + authServiceClient = await deps.use('authServiceClient') }) afterEach(async (): Promise => { @@ -126,28 +107,21 @@ describe('Tenant Service', (): void => { idpSecret: 'test-idp-secret' } - const scope = nock(config.authAdminApiUrl) - .post('') - .reply(200, { data: { createTenant: { id: 1234 } } }) + const spy = jest + .spyOn(authServiceClient.tenant, 'create') + .mockImplementationOnce(async () => undefined) - const apolloSpy = jest.spyOn(apolloClient, 'mutate') const tenant = await tenantService.create(createOptions) expect(tenant).toEqual(expect.objectContaining(createOptions)) - expect(apolloSpy).toHaveBeenCalledWith( + expect(spy).toHaveBeenCalledWith( expect.objectContaining({ - variables: { - input: { - id: tenant.id, - idpSecret: createOptions.idpSecret, - idpConsentUrl: createOptions.idpConsentUrl - } - } + id: tenant.id, + idpSecret: createOptions.idpSecret, + idpConsentUrl: createOptions.idpConsentUrl }) ) - - scope.done() }) test('tenant creation rolls back if auth tenant create fails', async (): Promise => { @@ -159,11 +133,13 @@ describe('Tenant Service', (): void => { idpSecret: 'test-idp-secret' } - const scope = nock(config.authAdminApiUrl) - .post('') - .reply(200, generateMutateGqlError('createTenant')) + const spy = jest + .spyOn(authServiceClient.tenant, 'create') + .mockImplementationOnce(() => { + throw new Error() + }) - const apolloSpy = jest.spyOn(apolloClient, 'mutate') + expect.assertions(3) let tenant try { tenant = await tenantService.create(createOptions) @@ -173,19 +149,14 @@ describe('Tenant Service', (): void => { const tenants = await Tenant.query() expect(tenants.length).toEqual(0) - expect(apolloSpy).toHaveBeenCalledWith( + expect(spy).toHaveBeenCalledWith( expect.objectContaining({ - variables: { - input: { - id: expect.any(String), - idpConsentUrl: createOptions.idpConsentUrl, - idpSecret: createOptions.idpSecret - } - } + id: expect.any(String), + idpConsentUrl: createOptions.idpConsentUrl, + idpSecret: createOptions.idpSecret }) ) } - scope.done() }) }) @@ -199,10 +170,10 @@ describe('Tenant Service', (): void => { idpSecret: 'test-idp-secret' } - const scope = nock(config.authAdminApiUrl) - .post('') - .reply(200, { data: { createTenant: { id: 1234 } } }) - .persist() + jest + .spyOn(authServiceClient.tenant, 'create') + .mockImplementationOnce(async () => undefined) + const tenant = await tenantService.create(originalTenantInfo) const updatedTenantInfo = { @@ -214,22 +185,16 @@ describe('Tenant Service', (): void => { idpSecret: 'test-idp-secret-two' } - const apolloSpy = jest.spyOn(apolloClient, 'mutate') + const spy = jest + .spyOn(authServiceClient.tenant, 'update') + .mockImplementationOnce(async () => undefined) const updatedTenant = await tenantService.update(updatedTenantInfo) expect(updatedTenant).toEqual(expect.objectContaining(updatedTenantInfo)) - expect(apolloSpy).toHaveBeenCalledWith( - expect.objectContaining({ - variables: { - input: { - id: tenant.id, - idpConsentUrl: updatedTenantInfo.idpConsentUrl, - idpSecret: updatedTenantInfo.idpSecret - } - } - }) - ) - scope.done() + expect(spy).toHaveBeenCalledWith(tenant.id, { + idpConsentUrl: updatedTenantInfo.idpConsentUrl, + idpSecret: updatedTenantInfo.idpSecret + }) }) test('rolls back tenant if auth tenant update fails', async (): Promise => { @@ -241,9 +206,10 @@ describe('Tenant Service', (): void => { idpSecret: 'test-idp-secret' } - nock(config.authAdminApiUrl) - .post('') - .reply(200, { data: { createTenant: { id: 1234 } } }) + jest + .spyOn(authServiceClient.tenant, 'create') + .mockImplementationOnce(async () => undefined) + const tenant = await tenantService.create(originalTenantInfo) const updatedTenantInfo = { id: tenant.id, @@ -254,13 +220,14 @@ describe('Tenant Service', (): void => { idpSecret: 'test-idp-secret-two' } - nock.cleanAll() + const spy = jest + .spyOn(authServiceClient.tenant, 'update') + .mockImplementationOnce(async () => { + throw new Error() + }) - nock(config.authAdminApiUrl) - .post('') - .reply(200, generateMutateGqlError('updateTenant')) - const apolloSpy = jest.spyOn(apolloClient, 'mutate') let updatedTenant + expect.assertions(3) try { updatedTenant = await tenantService.update(updatedTenantInfo) } catch (err) { @@ -268,20 +235,14 @@ describe('Tenant Service', (): void => { const dbTenant = await Tenant.query().findById(tenant.id) assert.ok(dbTenant) expect(dbTenant).toEqual(expect.objectContaining(originalTenantInfo)) - expect(apolloSpy).toHaveBeenCalledWith( + expect(spy).toHaveBeenCalledWith( + tenant.id, expect.objectContaining({ - variables: { - input: { - id: tenant.id, - idpConsentUrl: updatedTenantInfo.idpConsentUrl, - idpSecret: updatedTenantInfo.idpSecret - } - } + idpConsentUrl: updatedTenantInfo.idpConsentUrl, + idpSecret: updatedTenantInfo.idpSecret }) ) } - - nock.cleanAll() }) test('Cannot update deleted tenant', async (): Promise => { @@ -294,7 +255,7 @@ describe('Tenant Service', (): void => { deletedAt: new Date() }) - const apolloSpy = jest.spyOn(apolloClient, 'mutate') + const spy = jest.spyOn(authServiceClient.tenant, 'update') try { await tenantService.update({ id: dbTenant.id, @@ -307,7 +268,7 @@ describe('Tenant Service', (): void => { assert.ok(dbTenantAfterUpdate) expect(dbTenantAfterUpdate.apiSecret).toEqual(originalSecret) - expect(apolloSpy).toHaveBeenCalledTimes(0) + expect(spy).toHaveBeenCalledTimes(0) } }) }) @@ -322,28 +283,22 @@ describe('Tenant Service', (): void => { idpSecret: 'test-idp-secret' } - const scope = nock(config.authAdminApiUrl) - .post('') - .reply(200, { data: { createTenant: { id: 1234 } } }) - .persist() + jest + .spyOn(authServiceClient.tenant, 'create') + .mockImplementationOnce(async () => undefined) const tenant = await tenantService.create(createOptions) - const apolloSpy = jest.spyOn(apolloClient, 'mutate') + const spy = jest + .spyOn(authServiceClient.tenant, 'delete') + .mockImplementationOnce(async () => undefined) await tenantService.delete(tenant.id) const dbTenant = await Tenant.query().findById(tenant.id) - expect(dbTenant?.deletedAt?.getTime()).toBeLessThanOrEqual( + assert.ok(dbTenant?.deletedAt) + expect(dbTenant.deletedAt.getTime()).toBeLessThanOrEqual( new Date(Date.now()).getTime() ) - expect(apolloSpy).toHaveBeenCalledWith( - expect.objectContaining({ - variables: { - input: { id: tenant.id, deletedAt: dbTenant?.deletedAt } - } - }) - ) - - scope.done() + expect(spy).toHaveBeenCalledWith(tenant.id, dbTenant.deletedAt) }) test('Reverts deletion if auth tenant delete fails', async (): Promise => { @@ -355,17 +310,18 @@ describe('Tenant Service', (): void => { idpSecret: 'test-idp-secret' } - nock(config.authAdminApiUrl) - .post('') - .reply(200, { data: { createTenant: { id: 1234 } } }) + jest + .spyOn(authServiceClient.tenant, 'create') + .mockImplementationOnce(async () => undefined) const tenant = await tenantService.create(createOptions) - nock.cleanAll() + const spy = jest + .spyOn(authServiceClient.tenant, 'delete') + .mockImplementationOnce(async () => { + throw new Error() + }) - const apolloSpy = jest.spyOn(apolloClient, 'mutate') - const deleteScope = nock(config.authAdminApiUrl) - .post('') - .reply(200, generateMutateGqlError('deleteTenant')) + expect.assertions(3) try { await tenantService.delete(tenant.id) } catch (err) { @@ -373,19 +329,8 @@ describe('Tenant Service', (): void => { assert.ok(dbTenant) expect(dbTenant.id).toEqual(tenant.id) expect(dbTenant.deletedAt).toBeNull() - expect(apolloSpy).toHaveBeenCalledWith( - expect.objectContaining({ - variables: { - input: { - id: tenant.id, - deletedAt: expect.any(Date) - } - } - }) - ) + expect(spy).toHaveBeenCalledWith(tenant.id, expect.any(Date)) } - - deleteScope.done() }) }) @@ -395,6 +340,7 @@ describe('Tenant Service', (): void => { let config: IAppConfig let tenantService: TenantService let tenantCache: CacheDataStore + let authServiceClient: AuthServiceClient beforeAll(async (): Promise => { deps = initIocContainer({ @@ -405,6 +351,7 @@ describe('Tenant Service', (): void => { config = await deps.use('config') tenantService = await deps.use('tenantService') tenantCache = await deps.use('tenantCache') + authServiceClient = await deps.use('authServiceClient') }) afterEach(async (): Promise => { @@ -425,10 +372,9 @@ describe('Tenant Service', (): void => { idpSecret: 'test-idp-secret' } - const scope = nock(config.authAdminApiUrl) - .post('') - .reply(200, { data: { createTenant: { tenant: { id: 1234 } } } }) - .persist() + jest + .spyOn(authServiceClient.tenant, 'create') + .mockImplementation(async () => undefined) const spyCacheSet = jest.spyOn(tenantCache, 'set') const tenant = await tenantService.create(createOptions) @@ -447,6 +393,9 @@ describe('Tenant Service', (): void => { expect(spyCacheGet).toHaveBeenCalledWith(tenant.id) const spyCacheUpdateSet = jest.spyOn(tenantCache, 'set') + jest + .spyOn(authServiceClient.tenant, 'update') + .mockImplementation(async () => undefined) const updatedTenant = await tenantService.update({ id: tenant.id, apiSecret: 'test-api-secret-2' @@ -461,6 +410,9 @@ describe('Tenant Service', (): void => { expect(spyCacheUpdateSet).toHaveBeenCalledWith(tenant.id, updatedTenant) const spyCacheDelete = jest.spyOn(tenantCache, 'delete') + jest + .spyOn(authServiceClient.tenant, 'delete') + .mockImplementation(async () => undefined) await tenantService.delete(tenant.id) await expect(tenantService.get(tenant.id)).resolves.toBeUndefined() @@ -468,8 +420,6 @@ describe('Tenant Service', (): void => { // Ensure that cache was set for deletion expect(spyCacheDelete).toHaveBeenCalledTimes(1) expect(spyCacheDelete).toHaveBeenCalledWith(tenant.id) - - scope.done() }) }) }) diff --git a/packages/backend/src/tenants/service.ts b/packages/backend/src/tenants/service.ts index d1973471eb..947881619f 100644 --- a/packages/backend/src/tenants/service.ts +++ b/packages/backend/src/tenants/service.ts @@ -1,10 +1,9 @@ import { Tenant } from './model' import { BaseService } from '../shared/baseService' -import { gql, NormalizedCacheObject } from '@apollo/client' -import { ApolloClient } from '@apollo/client' import { TransactionOrKnex } from 'objection' import { Pagination, SortOrder } from '../shared/baseModel' import { CacheDataStore } from '../middleware/cache/data-stores' +import type { AuthServiceClient } from '../auth-service-client/client' export interface TenantService { get: (id: string) => Promise @@ -16,8 +15,8 @@ export interface TenantService { export interface ServiceDependencies extends BaseService { knex: TransactionOrKnex - apolloClient: ApolloClient tenantCache: CacheDataStore + authServiceClient: AuthServiceClient } export async function createTenantService( @@ -83,26 +82,12 @@ async function createTenant( idpConsentUrl }) - const mutation = gql` - mutation CreateAuthTenant($input: CreateTenantInput!) { - createTenant(input: $input) { - tenant { - id - } - } - } - ` - - const variables = { - input: { - id: tenant.id, - idpSecret, - idpConsentUrl - } - } + await deps.authServiceClient.tenant.create({ + id: tenant.id, + idpSecret, + idpConsentUrl + }) - // TODO: add type to this in https://github.com/interledger/rafiki/issues/3125 - await deps.apolloClient.mutate({ mutation, variables }) await trx.commit() await deps.tenantCache.set(tenant.id, tenant) @@ -143,26 +128,10 @@ async function updateTenant( .throwIfNotFound() if (idpConsentUrl || idpSecret) { - const mutation = gql` - mutation UpdateAuthTenant($input: UpdateTenantInput!) { - updateTenant(input: $input) { - tenant { - id - } - } - } - ` - - const variables = { - input: { - id, - idpConsentUrl, - idpSecret - } - } - - // TODO: add types to this in https://github.com/interledger/rafiki/issues/3125 - await deps.apolloClient.mutate({ mutation, variables }) + await deps.authServiceClient.tenant.update(id, { + idpConsentUrl, + idpSecret + }) } await trx.commit() @@ -186,16 +155,7 @@ async function deleteTenant( await Tenant.query(trx).patchAndFetchById(id, { deletedAt }) - const mutation = gql` - mutation DeleteAuthTenantMutation($input: DeleteTenantInput!) { - deleteTenant(input: $input) { - sucess - } - } - ` - const variables = { input: { id, deletedAt } } - // TODO: add types to this in https://github.com/interledger/rafiki/issues/3125 - await deps.apolloClient.mutate({ mutation, variables }) + await deps.authServiceClient.tenant.delete(id, deletedAt) await trx.commit() } catch (err) { await trx.rollback() diff --git a/packages/backend/src/tests/tenant.ts b/packages/backend/src/tests/tenant.ts index f174a58f2f..579735b73d 100644 --- a/packages/backend/src/tests/tenant.ts +++ b/packages/backend/src/tests/tenant.ts @@ -1,4 +1,3 @@ -import nock from 'nock' import { IocContract } from '@adonisjs/fold' import { faker } from '@faker-js/faker' import { AppServices } from '../app' @@ -17,10 +16,10 @@ export async function createTenant( options?: CreateOptions ): Promise { const tenantService = await deps.use('tenantService') - const config = await deps.use('config') - const scope = nock(config.authAdminApiUrl) - .post('') - .reply(200, { data: { createTenant: { id: 1234 } } }) + const authServiceClient = await deps.use('authServiceClient') + jest + .spyOn(authServiceClient.tenant, 'create') + .mockImplementationOnce(async () => undefined) const tenant = await tenantService.create( options || { email: faker.internet.email(), @@ -30,7 +29,6 @@ export async function createTenant( idpSecret: 'test-idp-secret' } ) - scope.done() if (!tenant) { throw Error('Failed to create test tenant') From 2742f1b55e19e83a199efde2b4561f550d45313a Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Fri, 20 Dec 2024 11:42:09 -0500 Subject: [PATCH 15/22] chore(auth): format --- packages/documentation/src/partials/auth-variables.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/documentation/src/partials/auth-variables.mdx b/packages/documentation/src/partials/auth-variables.mdx index 5cfd3d386f..b0585874f2 100644 --- a/packages/documentation/src/partials/auth-variables.mdx +++ b/packages/documentation/src/partials/auth-variables.mdx @@ -33,7 +33,7 @@ import { LinkOut } from '@interledger/docs-design-system' | `INTERACTION_EXPIRY_SECONDS` | `auth.interactionExpirySeconds` | `600` (10 minutes) | The time, in seconds, for which a user can interact with a grant request before the request expires. | | `INTERACTION_PORT` | `auth.port.interaction` | `3009` | The port number of your Open Payments interaction-related APIs. | | `INTROSPECTION_PORT` | `auth.port.introspection` | `3007` | The port of your Open Payments access token introspection server. | -| `SERVICE_API_PORT` | `auth.port.serviceAPIPort` | `3010` | The port to expose the internal service api. +| `SERVICE_API_PORT` | `auth.port.serviceAPIPort` | `3010` | The port to expose the internal service api. | | `LIST_ALL_ACCESS_INTERACTION` | `auth.interaction.listAll` | `true` | When `true`, grant requests that include a `list-all` action will require interaction. In these requests, the client asks to list resources that it did not create. | | `LOG_LEVEL` | `auth.logLevel` | `info` | Pino log level | | `NODE_ENV` | `auth.nodeEnv` | `development` | The type of node environment: `development`, `test`, or `production`. | From a499775d40bc3c707d2f1e559a9de23c5594c5d9 Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Fri, 20 Dec 2024 12:00:12 -0500 Subject: [PATCH 16/22] chore(auth): format --- packages/backend/src/auth-service-client/client.ts | 2 +- packages/backend/src/tenants/service.test.ts | 6 +----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/backend/src/auth-service-client/client.ts b/packages/backend/src/auth-service-client/client.ts index 5dcfac2c91..afacf9b0ff 100644 --- a/packages/backend/src/auth-service-client/client.ts +++ b/packages/backend/src/auth-service-client/client.ts @@ -80,7 +80,7 @@ export class AuthServiceClient { headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }), - delete: (id: string, deletedAt?: Date) => + delete: (id: string, _deletedAt?: Date) => this.request(`/tenant/${id}`, { method: 'DELETE' }) } } diff --git a/packages/backend/src/tenants/service.test.ts b/packages/backend/src/tenants/service.test.ts index 86c96e3c10..3a7234b0de 100644 --- a/packages/backend/src/tenants/service.test.ts +++ b/packages/backend/src/tenants/service.test.ts @@ -7,7 +7,7 @@ import { AppServices } from '../app' import { initIocContainer } from '..' import { createTestApp, TestContainer } from '../tests/app' import { TenantService } from './service' -import { Config, IAppConfig } from '../config/app' +import { Config } from '../config/app' import { truncateTables } from '../tests/tableManager' import { Tenant } from './model' import { getPageTests } from '../shared/baseModel.test' @@ -20,7 +20,6 @@ describe('Tenant Service', (): void => { let deps: IocContract let appContainer: TestContainer let tenantService: TenantService - let config: IAppConfig let knex: Knex let authServiceClient: AuthServiceClient @@ -28,7 +27,6 @@ describe('Tenant Service', (): void => { deps = initIocContainer(Config) appContainer = await createTestApp(deps) tenantService = await deps.use('tenantService') - config = await deps.use('config') knex = await deps.use('knex') authServiceClient = await deps.use('authServiceClient') }) @@ -337,7 +335,6 @@ describe('Tenant Service', (): void => { describe('Tenant Service using cache', (): void => { let deps: IocContract let appContainer: TestContainer - let config: IAppConfig let tenantService: TenantService let tenantCache: CacheDataStore let authServiceClient: AuthServiceClient @@ -348,7 +345,6 @@ describe('Tenant Service', (): void => { localCacheDuration: 5_000 // 5-second default. }) appContainer = await createTestApp(deps) - config = await deps.use('config') tenantService = await deps.use('tenantService') tenantCache = await deps.use('tenantCache') authServiceClient = await deps.use('authServiceClient') From 2f60a862acdbb09ef3857db79ff8d82325b8a819 Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Fri, 20 Dec 2024 12:46:56 -0500 Subject: [PATCH 17/22] fix(integration,localenv): auth service api config --- localenv/cloud-nine-wallet/docker-compose.yml | 5 +++-- localenv/happy-life-bank/docker-compose.yml | 3 +++ packages/auth/src/config/app.ts | 2 +- .../integration/testenv/cloud-nine-wallet/docker-compose.yml | 3 +++ test/integration/testenv/happy-life-bank/docker-compose.yml | 3 +++ 5 files changed, 13 insertions(+), 3 deletions(-) diff --git a/localenv/cloud-nine-wallet/docker-compose.yml b/localenv/cloud-nine-wallet/docker-compose.yml index 777d6dd76a..5f1a6177bb 100644 --- a/localenv/cloud-nine-wallet/docker-compose.yml +++ b/localenv/cloud-nine-wallet/docker-compose.yml @@ -67,7 +67,7 @@ services: AUTH_SERVER_INTROSPECTION_URL: http://cloud-nine-wallet-auth:3007 AUTH_ADMIN_API_URL: 'http://cloud-nine-wallet-auth:3003/graphql' AUTH_ADMIN_API_SECRET: 'rPoZpe9tVyBNCigm05QDco7WLcYa0xMao7lO5KG1XG4=' - AUTH_SERVICE_API_URL: 'http://cloud-nine-wallet-auth:3010/' + AUTH_SERVICE_API_URL: 'http://cloud-nine-wallet-auth:3011' ILP_ADDRESS: ${ILP_ADDRESS:-test.cloud-nine-wallet} STREAM_SECRET: BjPXtnd00G2mRQwP/8ZpwyZASOch5sUXT5o0iR5b5wU= API_SECRET: iyIgCprjb9uL8wFckR+pLEkJWMB7FJhgkvqhTQR/964= @@ -109,7 +109,7 @@ services: - '3006:3006' - "9230:9229" - '3009:3009' - - '3010:3010' + - '3011:3011' environment: NODE_ENV: ${NODE_ENV:-development} TRUST_PROXY: ${TRUST_PROXY} @@ -121,6 +121,7 @@ services: COOKIE_KEY: 42397d1f371dd4b8b7d0308a689a57c882effd4ea909d792302542af47e2cd37 ADMIN_API_SECRET: rPoZpe9tVyBNCigm05QDco7WLcYa0xMao7lO5KG1XG4= OPERATOR_TENANT_ID: 438fa74a-fa7d-4317-9ced-dde32ece1787 + SERVICE_API_PORT: 3011 depends_on: - shared-database - shared-redis diff --git a/localenv/happy-life-bank/docker-compose.yml b/localenv/happy-life-bank/docker-compose.yml index 15d41cce4a..e1c17aec0e 100644 --- a/localenv/happy-life-bank/docker-compose.yml +++ b/localenv/happy-life-bank/docker-compose.yml @@ -60,6 +60,7 @@ services: AUTH_SERVER_INTROSPECTION_URL: http://happy-life-bank-auth:3007 AUTH_ADMIN_API_URL: 'http://happy-life-bank-auth:4003/graphql' AUTH_ADMIN_API_SECRET: 'rPoZpe9tVyBNCigm05QDco7WLcYa0xMao7lO5KG1XG4=' + AUTH_SERVICE_API_URL: 'http://happy-life-bank-auth:4011' ILP_ADDRESS: test.happy-life-bank ILP_CONNECTOR_URL: http://happy-life-bank-backend:4002 STREAM_SECRET: BjPXtnd00G2mRQwP/8ZpwyZASOch5sUXT5o0iR5b5wU= @@ -124,6 +125,7 @@ services: - rafiki ports: - '4010:4010' + - '4011:4011' environment: PORT: 4010 LOG_LEVEL: debug @@ -134,6 +136,7 @@ services: AUTH_ENABLED: false SIGNATURE_VERSION: 1 SIGNATURE_SECRET: iyIgCprjb9uL8wFckR+pLEkJWMB7FJhgkvqhTQR/964= + SERVICE_API_PORT: 4011 depends_on: - cloud-nine-admin - happy-life-backend diff --git a/packages/auth/src/config/app.ts b/packages/auth/src/config/app.ts index d0524399ab..ef4daeffc0 100644 --- a/packages/auth/src/config/app.ts +++ b/packages/auth/src/config/app.ts @@ -43,7 +43,7 @@ export const Config = { authPort: envInt('AUTH_PORT', 3006), interactionPort: envInt('INTERACTION_PORT', 3009), introspectionPort: envInt('INTROSPECTION_PORT', 3007), - serviceAPIPort: envInt('SERVICE_API_PORT', 3010), + serviceAPIPort: envInt('SERVICE_API_PORT', 3011), env: envString('NODE_ENV', 'development'), trustProxy: envBool('TRUST_PROXY', false), enableManualMigrations: envBool('ENABLE_MANUAL_MIGRATIONS', false), diff --git a/test/integration/testenv/cloud-nine-wallet/docker-compose.yml b/test/integration/testenv/cloud-nine-wallet/docker-compose.yml index 18630e78eb..d0a45c6bd9 100644 --- a/test/integration/testenv/cloud-nine-wallet/docker-compose.yml +++ b/test/integration/testenv/cloud-nine-wallet/docker-compose.yml @@ -33,6 +33,7 @@ services: AUTH_SERVER_GRANT_URL: http://cloud-nine-wallet-test-auth:3106 AUTH_ADMIN_API_URL: 'http://cloud-nine-wallet-test-auth:3003/graphql' AUTH_ADMIN_API_SECRET: 'test-secret' + AUTH_SERVICE_API_URL: 'http://cloud-nine-wallet-auth:3111/' ILP_ADDRESS: test.cloud-nine-wallet-test ILP_CONNECTOR_URL: http://cloud-nine-wallet-test-backend:3102 STREAM_SECRET: BjPXtnd00G2mRQwP/8ZpwyZASOch5sUXT5o0iR5b5wU= @@ -62,6 +63,7 @@ services: - '3106:3106' - '3107:3107' - '3109:3109' + - '3111:3111' environment: NODE_ENV: ${NODE_ENV:-development} AUTH_SERVER_URL: http://cloud-nine-wallet-test-auth:3106 @@ -71,6 +73,7 @@ services: INTERACTION_PORT: 3109 AUTH_PORT: 3106 ADMIN_PORT: 3103 + SERVICE_API_PORT: 3111 REDIS_URL: redis://shared-redis:6379/1 IDENTITY_SERVER_URL: http://localhost:3030/mock-idp/ IDENTITY_SERVER_SECRET: 2pEcn2kkCclbOHQiGNEwhJ0rucATZhrA807HTm2rNXE= diff --git a/test/integration/testenv/happy-life-bank/docker-compose.yml b/test/integration/testenv/happy-life-bank/docker-compose.yml index a0a42586ba..951a769ad3 100644 --- a/test/integration/testenv/happy-life-bank/docker-compose.yml +++ b/test/integration/testenv/happy-life-bank/docker-compose.yml @@ -28,6 +28,7 @@ services: AUTH_SERVER_INTROSPECTION_URL: http://happy-life-bank-test-auth:4107 AUTH_ADMIN_API_URL: 'http://happy-life-bank-test-auth:4003/graphql' AUTH_ADMIN_API_SECRET: 'test-secret' + AUTH_SERVICE_API_URL: 'http://happy-life-bank-test-auth:4111/' # matches pfry key id KEY_ID: keyid-97a3a431-8ee1-48fc-ac85-70e2f5eba8e5 PRIVATE_KEY_FILE: /workspace/private-key.pem @@ -62,6 +63,7 @@ services: - '4106:4106' - '4107:4107' - '4109:4109' + - '4111:4111' environment: NODE_ENV: development AUTH_DATABASE_URL: postgresql://happy_life_bank_test_auth:happy_life_bank_test_auth@shared-database/happy_life_bank_test_auth @@ -70,6 +72,7 @@ services: INTERACTION_PORT: 4109 INTROSPECTION_PORT: 4107 ADMIN_PORT: 4103 + SERVICE_API_PORT: 4111 AUTH_PORT: 4106 REDIS_URL: redis://shared-redis:6379/3 IDENTITY_SERVER_URL: http://localhost:3030/mock-idp/ From 0188f2fef8cc8d74e57680af0181c51b87836448 Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Fri, 20 Dec 2024 13:25:45 -0500 Subject: [PATCH 18/22] fix(backend,auth): update tenant api to support deletedAt --- packages/auth/src/tenant/routes.ts | 3 ++- packages/auth/src/tenant/service.test.ts | 12 ++++++------ packages/auth/src/tenant/service.ts | 9 +++++---- .../backend/src/auth-service-client/client.test.ts | 6 ++++-- packages/backend/src/auth-service-client/client.ts | 8 ++++++-- 5 files changed, 23 insertions(+), 15 deletions(-) diff --git a/packages/auth/src/tenant/routes.ts b/packages/auth/src/tenant/routes.ts index e4b349d0ee..0ffe7c5940 100644 --- a/packages/auth/src/tenant/routes.ts +++ b/packages/auth/src/tenant/routes.ts @@ -110,8 +110,9 @@ async function deleteTenant( ctx.status = 400 return } + const deletedAt = new Date(deletedAtString) - const deleted = await deps.tenantService.delete(id) + const deleted = await deps.tenantService.delete(id, deletedAt) if (!deleted) { ctx.status = 404 diff --git a/packages/auth/src/tenant/service.test.ts b/packages/auth/src/tenant/service.test.ts index 4428e1f7ab..d5b68ffc15 100644 --- a/packages/auth/src/tenant/service.test.ts +++ b/packages/auth/src/tenant/service.test.ts @@ -81,7 +81,7 @@ describe('Tenant Service', (): void => { test('returns undefined for soft deleted tenant', async (): Promise => { const tenantData = createTenantData() const created = await tenantService.create(tenantData) - await tenantService.delete(created.id) + await tenantService.delete(created.id, new Date()) const tenant = await tenantService.get(created.id) expect(tenant).toBeUndefined() @@ -137,7 +137,7 @@ describe('Tenant Service', (): void => { test('returns undefined for soft-deleted tenant', async (): Promise => { const tenantData = createTenantData() const created = await tenantService.create(tenantData) - await tenantService.delete(created.id) + await tenantService.delete(created.id, new Date()) const updated = await tenantService.update(created.id, { idpConsentUrl: faker.internet.url() @@ -151,7 +151,7 @@ describe('Tenant Service', (): void => { const tenantData = createTenantData() const created = await tenantService.create(tenantData) - const result = await tenantService.delete(created.id) + const result = await tenantService.delete(created.id, new Date()) expect(result).toBe(true) const tenant = await tenantService.get(created.id) @@ -165,7 +165,7 @@ describe('Tenant Service', (): void => { }) test('returns false for non-existent tenant', async (): Promise => { - const result = await tenantService.delete(faker.string.uuid()) + const result = await tenantService.delete(faker.string.uuid(), new Date()) expect(result).toBe(false) }) @@ -173,8 +173,8 @@ describe('Tenant Service', (): void => { const tenantData = createTenantData() const created = await tenantService.create(tenantData) - await tenantService.delete(created.id) - const secondDelete = await tenantService.delete(created.id) + await tenantService.delete(created.id, new Date()) + const secondDelete = await tenantService.delete(created.id, new Date()) expect(secondDelete).toBe(false) }) }) diff --git a/packages/auth/src/tenant/service.ts b/packages/auth/src/tenant/service.ts index d8d9f2a24c..d4f3cc336a 100644 --- a/packages/auth/src/tenant/service.ts +++ b/packages/auth/src/tenant/service.ts @@ -15,7 +15,7 @@ export interface TenantService { id: string, input: Partial> ): Promise - delete(id: string): Promise + delete(id: string, deletedAt: Date): Promise } interface ServiceDependencies extends BaseService { @@ -39,7 +39,7 @@ export async function createTenantService({ get: (id: string) => getTenant(deps, id), update: (id: string, input: Partial>) => updateTenant(deps, id, input), - delete: (id: string) => deleteTenant(deps, id) + delete: (id: string, deletedAt: Date) => deleteTenant(deps, id, deletedAt) } } @@ -72,10 +72,11 @@ async function updateTenant( async function deleteTenant( deps: ServiceDependencies, - id: string + id: string, + deletedAt: Date ): Promise { const deleted = await Tenant.query(deps.knex) - .patch({ deletedAt: new Date() }) + .patch({ deletedAt }) .whereNull('deletedAt') .where('id', id) return deleted > 0 diff --git a/packages/backend/src/auth-service-client/client.test.ts b/packages/backend/src/auth-service-client/client.test.ts index 27b9706f10..5a4af980c0 100644 --- a/packages/backend/src/auth-service-client/client.test.ts +++ b/packages/backend/src/auth-service-client/client.test.ts @@ -102,7 +102,9 @@ describe('AuthServiceClient', () => { nock(baseUrl).delete(`/tenant/${id}`).reply(204) - await expect(client.tenant.delete(id)).resolves.toBeUndefined() + await expect( + client.tenant.delete(id, new Date()) + ).resolves.toBeUndefined() }) test('throws on bad request', async () => { @@ -112,7 +114,7 @@ describe('AuthServiceClient', () => { .delete(`/tenant/${id}`) .reply(404, { message: 'Tenant not found' }) - await expect(client.tenant.delete(id)).rejects.toThrow( + await expect(client.tenant.delete(id, new Date())).rejects.toThrow( AuthServiceClientError ) }) diff --git a/packages/backend/src/auth-service-client/client.ts b/packages/backend/src/auth-service-client/client.ts index afacf9b0ff..230cca7ea7 100644 --- a/packages/backend/src/auth-service-client/client.ts +++ b/packages/backend/src/auth-service-client/client.ts @@ -80,7 +80,11 @@ export class AuthServiceClient { headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }), - delete: (id: string, _deletedAt?: Date) => - this.request(`/tenant/${id}`, { method: 'DELETE' }) + delete: (id: string, deletedAt: Date) => + this.request(`/tenant/${id}`, { + method: 'DELETE', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ deletedAt }) + }) } } From eebb862b9a36934a21956f71087cb15b85f19fc6 Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Fri, 20 Dec 2024 13:48:14 -0500 Subject: [PATCH 19/22] docs: update with env vars --- packages/backend/jest.env.js | 2 +- .../src/content/docs/integration/prod/docker-compose.mdx | 4 +++- packages/documentation/src/partials/auth-variables.mdx | 2 +- packages/documentation/src/partials/backend-variables.mdx | 1 + 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/backend/jest.env.js b/packages/backend/jest.env.js index eb93347565..4a8435dd72 100644 --- a/packages/backend/jest.env.js +++ b/packages/backend/jest.env.js @@ -6,7 +6,7 @@ process.env.ILP_CONNECTOR_URL = 'http://127.0.0.1:3002' process.env.ILP_ADDRESS = 'test.rafiki' process.env.AUTH_SERVER_GRANT_URL = 'http://127.0.0.1:3006' process.env.AUTH_SERVER_INTROSPECTION_URL = 'http://127.0.0.1:3007/' -process.env.AUTH_SERVICE_API_URL = 'http://127.0.0.1:3010' +process.env.AUTH_SERVICE_API_URL = 'http://127.0.0.1:3011' process.env.WEBHOOK_URL = 'http://127.0.0.1:4001/webhook' process.env.STREAM_SECRET = '2/PxuRFV9PAp0yJlnAifJ+1OxujjjI16lN+DBnLNRLA=' process.env.USE_TIGERBEETLE = false diff --git a/packages/documentation/src/content/docs/integration/prod/docker-compose.mdx b/packages/documentation/src/content/docs/integration/prod/docker-compose.mdx index 188429ca49..6cda45440a 100644 --- a/packages/documentation/src/content/docs/integration/prod/docker-compose.mdx +++ b/packages/documentation/src/content/docs/integration/prod/docker-compose.mdx @@ -112,7 +112,7 @@ services: AUTH_PORT: 3006 INTROSPECTION_PORT: 3007 INTERACTION_PORT: 3009 - SERVICE_API_PORT: 3010 + SERVICE_API_PORT: 3011 COOKIE_KEY: {...} IDENTITY_SERVER_SECRET: {...} IDENTITY_SERVER_URL: {https://idp.mysystem.com} @@ -127,6 +127,7 @@ services: - '3006:3006' - '3007:3007' - '3009:3009' + - '3011:3011' restart: always rafiki-backend: @@ -138,6 +139,7 @@ services: environment: AUTH_SERVER_GRANT_URL: {https://auth.myrafiki.com} AUTH_SERVER_INTROSPECTION_URL: {https://auth.myrafiki.com/3007} + AUTH_SERVICE_API_URL: {https://auth.myrafiki.com/3011} DATABASE_URL: {postgresql://...} ILP_ADDRESS: {test.myrafiki} ADMIN_PORT: 3001 diff --git a/packages/documentation/src/partials/auth-variables.mdx b/packages/documentation/src/partials/auth-variables.mdx index b0585874f2..b970386202 100644 --- a/packages/documentation/src/partials/auth-variables.mdx +++ b/packages/documentation/src/partials/auth-variables.mdx @@ -33,7 +33,7 @@ import { LinkOut } from '@interledger/docs-design-system' | `INTERACTION_EXPIRY_SECONDS` | `auth.interactionExpirySeconds` | `600` (10 minutes) | The time, in seconds, for which a user can interact with a grant request before the request expires. | | `INTERACTION_PORT` | `auth.port.interaction` | `3009` | The port number of your Open Payments interaction-related APIs. | | `INTROSPECTION_PORT` | `auth.port.introspection` | `3007` | The port of your Open Payments access token introspection server. | -| `SERVICE_API_PORT` | `auth.port.serviceAPIPort` | `3010` | The port to expose the internal service api. | +| `SERVICE_API_PORT` | `auth.port.serviceAPIPort` | `3011` | The port to expose the internal service api. | | `LIST_ALL_ACCESS_INTERACTION` | `auth.interaction.listAll` | `true` | When `true`, grant requests that include a `list-all` action will require interaction. In these requests, the client asks to list resources that it did not create. | | `LOG_LEVEL` | `auth.logLevel` | `info` | Pino log level | | `NODE_ENV` | `auth.nodeEnv` | `development` | The type of node environment: `development`, `test`, or `production`. | diff --git a/packages/documentation/src/partials/backend-variables.mdx b/packages/documentation/src/partials/backend-variables.mdx index 68fdb527b0..ce6f576811 100644 --- a/packages/documentation/src/partials/backend-variables.mdx +++ b/packages/documentation/src/partials/backend-variables.mdx @@ -17,6 +17,7 @@ import { LinkOut } from '@interledger/docs-design-system' | `REDIS_URL` | `backend.redis.host`,
`backend.redis.port` | `redis://127.0.0.1:6379` | The Redis URL of the database handling ILP packet data. For Helm, these components are provided individually. | | `USE_TIGERBEETLE` | `backend.use.tigerbeetle` | `true` | When `true`, a TigerBeetle database is used for accounting. When `false`, a Postgres database is used. | | `WEBHOOK_URL` | `backend.serviceUrls.WEBHOOK_URL` | _undefined_ | Your endpoint that consumes webhook events. | +| `AUTH_SERVICE_API_URL` | `backend.serviceUrls.AUTH_SERVICE_API_URL` | _undefined_ | The service-to-service api endpoint on your Open Payments authorization server. | From 2212f8890ec901746f90ca5e508e28f208a324a5 Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Fri, 20 Dec 2024 14:26:52 -0500 Subject: [PATCH 20/22] fix(backend): dep container type --- packages/backend/src/app.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/backend/src/app.ts b/packages/backend/src/app.ts index 03fbbcf1b6..db0e174012 100644 --- a/packages/backend/src/app.ts +++ b/packages/backend/src/app.ts @@ -105,6 +105,7 @@ import { getTenantFromApiSignature, TenantApiSignatureResult } from './shared/utils' +import { AuthServiceClient } from './auth-service-client/client' export interface AppContextData { logger: Logger container: AppContainer @@ -265,6 +266,7 @@ export interface AppServices { paymentMethodHandlerService: Promise ilpPaymentService: Promise localPaymentService: Promise + authServiceClient: AuthServiceClient } export type AppContainer = IocContract From aa03813a81bf15834fe36ade768f48132013d97f Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Fri, 20 Dec 2024 14:27:06 -0500 Subject: [PATCH 21/22] fix(localenv): docker compose config --- localenv/happy-life-bank/docker-compose.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/localenv/happy-life-bank/docker-compose.yml b/localenv/happy-life-bank/docker-compose.yml index e1c17aec0e..ba4acb74d5 100644 --- a/localenv/happy-life-bank/docker-compose.yml +++ b/localenv/happy-life-bank/docker-compose.yml @@ -99,6 +99,7 @@ services: - '4006:3006' - '9232:9229' - '4009:3009' + - '4011:4011' environment: NODE_ENV: development AUTH_DATABASE_URL: postgresql://happy_life_bank_auth:happy_life_bank_auth@shared-database/happy_life_bank_auth @@ -109,6 +110,7 @@ services: COOKIE_KEY: 42397d1f371dd4b8b7d0308a689a57c882effd4ea909d792302542af47e2cd37 ADMIN_API_SECRET: rPoZpe9tVyBNCigm05QDco7WLcYa0xMao7lO5KG1XG4= OPERATOR_TENANT_ID: cf5fd7d3-1eb1-4041-8e43-ba45747e9e5d + SERVICE_API_PORT: 4011 depends_on: - cloud-nine-auth happy-life-admin: @@ -125,7 +127,6 @@ services: - rafiki ports: - '4010:4010' - - '4011:4011' environment: PORT: 4010 LOG_LEVEL: debug @@ -136,7 +137,6 @@ services: AUTH_ENABLED: false SIGNATURE_VERSION: 1 SIGNATURE_SECRET: iyIgCprjb9uL8wFckR+pLEkJWMB7FJhgkvqhTQR/964= - SERVICE_API_PORT: 4011 depends_on: - cloud-nine-admin - happy-life-backend From 8835a8c8910a20ba5d3b0b328908a06de1514bb5 Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Tue, 14 Jan 2025 13:27:28 -0500 Subject: [PATCH 22/22] fix(backend): add default header to api client --- packages/backend/src/auth-service-client/client.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/backend/src/auth-service-client/client.ts b/packages/backend/src/auth-service-client/client.ts index 230cca7ea7..da2e0a9a72 100644 --- a/packages/backend/src/auth-service-client/client.ts +++ b/packages/backend/src/auth-service-client/client.ts @@ -26,6 +26,8 @@ export class AuthServiceClient { // eslint-disable-next-line @typescript-eslint/no-explicit-any private async request(path: string, options: RequestInit): Promise { + options.headers = { 'Content-Type': 'application/json', ...options.headers } + const response = await fetch(`${this.baseUrl}${path}`, options) if (!response.ok) {