diff --git a/packages/core/integrations/integration-router.js b/packages/core/integrations/integration-router.js index c69b0dc7a..7df4e7072 100644 --- a/packages/core/integrations/integration-router.js +++ b/packages/core/integrations/integration-router.js @@ -531,13 +531,33 @@ function setEntityRoutes(router, authenticateUser, useCases) { 'data', ]); - const entityDetails = await processAuthorizationCallback.execute( - userId, - params.entityType, - params.data + const dataKeys = + params.data && typeof params.data === 'object' + ? Object.keys(params.data) + : []; + console.log( + `[Frigg] POST /api/authorize userId=${userId} entityType=${params.entityType} dataKeys=${JSON.stringify(dataKeys)}` ); - res.json(entityDetails); + try { + const entityDetails = + await processAuthorizationCallback.execute( + userId, + params.entityType, + params.data + ); + + console.log( + `[Frigg] POST /api/authorize success userId=${userId} entityType=${params.entityType} credentialId=${entityDetails?.credential_id} entityId=${entityDetails?.entity_id}` + ); + + res.json(entityDetails); + } catch (err) { + console.error( + `[Frigg] POST /api/authorize failed userId=${userId} entityType=${params.entityType} error=${err?.message || err}` + ); + throw err; + } }) ); diff --git a/packages/core/modules/use-cases/__tests__/process-authorization-callback.test.js b/packages/core/modules/use-cases/__tests__/process-authorization-callback.test.js index 3a4f9794f..1aa022905 100644 --- a/packages/core/modules/use-cases/__tests__/process-authorization-callback.test.js +++ b/packages/core/modules/use-cases/__tests__/process-authorization-callback.test.js @@ -38,6 +38,15 @@ describe('ProcessAuthorizationCallback — re-auth status restoration', () => { externalId: 'ext-1', credential: 'cred-1', }), + findEntitiesByUserIdAndModuleName: jest.fn().mockResolvedValue([ + { + id: 'entity-1', + userId: 'user-1', + moduleName: 'testmodule', + externalId: 'ext-1', + credential: { id: 'cred-1', authIsValid: false }, + }, + ]), createEntity: jest.fn(), }; @@ -216,4 +225,38 @@ describe('ProcessAuthorizationCallback — re-auth status restoration', () => { success: true, }); }); + + it('persists credentials explicitly on OAuth2 re-auth (belt-and-suspenders)', async () => { + // Force the OAuth2 path + const { Module } = require('../../module'); + Module.mockImplementation(({ userId, definition, entity }) => ({ + userId, + entity, + credential: undefined, + definition, + apiClass: { requesterType: 'oauth2' }, + api: { delegate: null }, + testAuth: jest.fn().mockResolvedValue(true), + apiParamsFromCredential: jest.fn().mockReturnValue({}), + apiParamsFromEntity: jest.fn().mockReturnValue({}), + getName: jest.fn().mockReturnValue('testmodule'), + })); + + moduleDefinitions[0].requiredAuthMethods.getToken = jest + .fn() + .mockResolvedValue({ + access_token: 'fresh-access', + refresh_token: 'fresh-refresh', + }); + + await useCase.execute('user-1', 'testmodule', { code: 'oauth-code' }); + + // upsertCredential must be called once even on the OAuth2 path — + // we no longer rely solely on the DLGT_TOKEN_UPDATE notification. + expect(credentialRepository.upsertCredential).toHaveBeenCalledTimes(1); + expect( + credentialRepository.upsertCredential.mock.calls[0][0].details + .authIsValid + ).toBe(true); + }); }); diff --git a/packages/core/modules/use-cases/process-authorization-callback.js b/packages/core/modules/use-cases/process-authorization-callback.js index 493a31dcb..305bfe2f6 100644 --- a/packages/core/modules/use-cases/process-authorization-callback.js +++ b/packages/core/modules/use-cases/process-authorization-callback.js @@ -28,6 +28,11 @@ class ProcessAuthorizationCallback { } async execute(userId, entityType, params) { + const hasCode = Boolean(params && params.code); + console.log( + `[Frigg] processAuthorizationCallback start userId=${userId} entityType=${entityType} hasCode=${hasCode}` + ); + const moduleDefinition = this.moduleDefinitions.find((def) => { return entityType === def.moduleName; }); @@ -38,12 +43,29 @@ class ProcessAuthorizationCallback { ); } - // todo: check if we need to pass entity to Module, right now it's null - let entity = null; + // Bootstrap the Module with the existing entity (if any) so the API + // requester is preloaded with prior tokens. This enables a refresh + // fallback when callers lack a fresh OAuth code, and lets us match a + // re-auth back to the existing credential record by id. + const existingEntities = + await this.moduleRepository.findEntitiesByUserIdAndModuleName( + userId, + entityType + ); + const existingEntity = + existingEntities && existingEntities.length > 0 + ? existingEntities[0] + : null; + + if (existingEntity) { + console.log( + `[Frigg] processAuthorizationCallback found existing entity id=${existingEntity.id} credentialId=${existingEntity.credential?.id}` + ); + } const module = new Module({ userId, - entity, + entity: existingEntity, definition: moduleDefinition, }); @@ -53,6 +75,16 @@ class ProcessAuthorizationCallback { module.api, params ); + console.log( + `[Frigg] processAuthorizationCallback OAuth getToken complete userId=${userId} entityType=${entityType}` + ); + // Belt-and-suspenders: persist tokens explicitly here rather than + // relying solely on the DLGT_TOKEN_UPDATE notification chain + // inside setTokens. The notification path remains in place but + // has been observed to no-op silently in some prod paths, + // leaving newly-issued tokens unsaved while the user-visible + // OAuth flow appears to succeed. + await this.onTokenUpdate(module, moduleDefinition, userId); } else { tokenResponse = await moduleDefinition.requiredAuthMethods.setAuthParams( @@ -62,6 +94,10 @@ class ProcessAuthorizationCallback { await this.onTokenUpdate(module, moduleDefinition, userId); } + console.log( + `[Frigg] processAuthorizationCallback credential persisted credentialId=${module.credential?.id} authIsValid=${module.credential?.authIsValid}` + ); + const authRes = await module.testAuth(); if (!authRes) { throw new Error('Authorization failed'); @@ -90,7 +126,12 @@ class ProcessAuthorizationCallback { // credential + entity are already persisted. Operators can recover // stuck integrations manually. try { - await this.restoreIntegrationsForEntity(persistedEntity.id); + const restoredCount = await this.restoreIntegrationsForEntity( + persistedEntity.id + ); + console.log( + `[Frigg] processAuthorizationCallback restored ${restoredCount} integration(s) for entityId=${persistedEntity.id}` + ); } catch (err) { console.error( `[Frigg] Failed to restore integrations for entity ${persistedEntity.id} after successful re-auth — manual intervention may be needed`, @@ -106,11 +147,12 @@ class ProcessAuthorizationCallback { } async restoreIntegrationsForEntity(entityId) { - if (!this.integrationRepository) return; + if (!this.integrationRepository) return 0; const integrations = await this.integrationRepository.findIntegrationsByEntityId( entityId ); + let restored = 0; for (const integration of integrations) { if (STATUSES_RESET_ON_REAUTH.includes(integration.status)) { console.log( @@ -120,8 +162,10 @@ class ProcessAuthorizationCallback { integration.id, 'ENABLED' ); + restored++; } } + return restored; } async onTokenUpdate(module, moduleDefinition, userId) {