diff --git a/server/controllers/OpeyIIController.ts b/server/controllers/OpeyIIController.ts index 8b90f28..0baa6db 100644 --- a/server/controllers/OpeyIIController.ts +++ b/server/controllers/OpeyIIController.ts @@ -1,5 +1,5 @@ import { Controller, Session, Req, Res, Post, Get } from 'routing-controllers' -import { Request, Response } from 'express' +import { Request, Response} from 'express' import { Readable } from "node:stream" import { ReadableStream as WebReadableStream } from "stream/web" import { Service } from 'typedi' @@ -7,7 +7,7 @@ import OBPClientService from '../services/OBPClientService' import OpeyClientService from '../services/OpeyClientService' import OBPConsentsService from '../services/OBPConsentsService' -import { UserInput } from '../schema/OpeySchema' +import { UserInput, OpeyConfig} from '../schema/OpeySchema' import { APIApi, Configuration, ConsentApi, ConsumerConsentrequestsBody, InlineResponse20151 } from 'obp-api-typescript' @Service() @@ -46,6 +46,13 @@ export class OpeyController { @Res() response: Response, ): Promise { + // Check if the consent is in the session, and can be added to the headers + const opeyConfig = session['opeyConfig'] + if (!opeyConfig) { + console.error("Opey config not found in session") + return response.status(500).json({ error: 'Internal Server Error' }) + } + // Read user input from request body let user_input: UserInput try { @@ -83,7 +90,7 @@ export class OpeyController { try { // Read web stream from OpeyClientService console.log("Calling OpeyClientService.stream") - stream = await this.opeyClientService.stream(user_input) + stream = await this.opeyClientService.stream(user_input, opeyConfig) } catch (error) { console.error("Error reading stream: ", error) @@ -184,6 +191,14 @@ export class OpeyController { @Res() response: Response ): Promise { + + // Check if the consent is in the session, and can be added to the headers + const opeyConfig = session['opeyConfig'] + if (!opeyConfig) { + console.error("Opey config not found in session") + return response.status(500).json({ error: 'Internal Server Error' }) + } + let user_input: UserInput try { user_input = { @@ -197,7 +212,7 @@ export class OpeyController { } try { - const opey_response = await this.opeyClientService.invoke(user_input) + const opey_response = await this.opeyClientService.invoke(user_input, opeyConfig) //console.log("Opey response: ", opey_response) return response.status(200).json(opey_response) @@ -279,12 +294,17 @@ export class OpeyController { ): Promise { try { // create consent as logged in user - const obpConsent = await this.obpConsentsService.createConsent(session) + const opeyConfig = await this.opeyClientService.getOpeyConfig() + session['opeyConfig'] = opeyConfig - console.log("Consent: ", obpConsent) + // Either here or in this method, we should check if there is already a consent stored in the session + await this.obpConsentsService.createConsent(session) - session['obpConsent'] = obpConsent - return response.status(200).json({consent_id: obpConsent?.consent_id}); + console.log("Consent at controller: ", session['opeyConfig']) + + const authConfig = session['opeyConfig']['authConfig'] + + return response.status(200).json({consent_id: authConfig?.obpConsent.consent_id}); } catch (error) { console.error("Error in consent endpoint: ", error); diff --git a/server/schema/OpeySchema.ts b/server/schema/OpeySchema.ts index f49849f..7572e85 100644 --- a/server/schema/OpeySchema.ts +++ b/server/schema/OpeySchema.ts @@ -21,7 +21,8 @@ export interface OBPConsent { status: string; } export interface AuthConfig { - opeyConsent: OBPConsent; + obpConsent: OBPConsent; + // Add more auth config fields here if needed } export interface OpeyConfig { diff --git a/server/services/OBPConsentsService.ts b/server/services/OBPConsentsService.ts index 09e814e..5113753 100644 --- a/server/services/OBPConsentsService.ts +++ b/server/services/OBPConsentsService.ts @@ -85,6 +85,13 @@ export default class OBPConsentsService { try { const consentResponse = await client.oBPv510CreateConsentImplicit(body, {headers: {'Content-Type': 'application/json',}}) + // Save the consent in the session + session['opeyConfig'] = { + authConfig: { + obpConsent: consentResponse.data + } + } + return consentResponse.data } catch (error: any) { diff --git a/server/services/OpeyClientService.ts b/server/services/OpeyClientService.ts index 4ce4e09..438c6b0 100644 --- a/server/services/OpeyClientService.ts +++ b/server/services/OpeyClientService.ts @@ -1,5 +1,5 @@ import { Service } from 'typedi' -import { UserInput, StreamInput, OpeyConfig, AuthConfig, ConsentRequestResponse } from '../schema/OpeySchema' +import { UserInput, StreamInput, OpeyConfig, ConsentRequestResponse } from '../schema/OpeySchema' import OBPClientService from './OBPClientService' @Service() @@ -55,11 +55,11 @@ export default class OpeyClientService { ...partialConfig.authConfig }; - // If opeyConsent is provided, merge it too - if (partialConfig.authConfig.opeyConsent && mergedConfig.authConfig.opeyConsent) { - mergedConfig.authConfig.opeyConsent = { - ...mergedConfig.authConfig.opeyConsent, - ...partialConfig.authConfig.opeyConsent + // If obpConsent is provided, merge it too + if (partialConfig.authConfig.obpConsent && mergedConfig.authConfig.obpConsent) { + mergedConfig.authConfig.obpConsent = { + ...mergedConfig.authConfig.obpConsent, + ...partialConfig.authConfig.obpConsent }; } } @@ -116,15 +116,20 @@ export default class OpeyClientService { * @throws Error if there's any issue streaming from Opey */ async stream(user_input: UserInput, opeyConfig?: Partial): Promise { + console.log("OpeyConfig: ", opeyConfig) //DEBUG + const config = await this.getOpeyConfig(opeyConfig) + console.log("OpeyConfig after getting: ", config) //DEBUG + // Check if we have the consent for Opey const auth = await this.checkAuthConfig(config) if (!auth.valid) { throw new Error(`AuthConfig not valid: ${auth.reason}`) } - // Should check here if the consent status is 'ACCEPTED' before streaming + // Get auth headers + const authHeaders = await this.getConsentAuthHeaders(config) try { @@ -138,10 +143,7 @@ export default class OpeyClientService { const response = await fetch(url, { method: 'POST', - headers: { - "Authorization": `Bearer ${config.authConfig.opeyConsent.jwt}`, // Should not be undefined as we already checked authConfig - "Content-Type": "application/json" - }, + headers: authHeaders, body: JSON.stringify(stream_input) }) if (!response.body) { @@ -181,6 +183,9 @@ export default class OpeyClientService { throw new Error(`AuthConfig not valid: ${auth.reason}`) } + // Get auth headers + const authHeaders = await this.getConsentAuthHeaders(config) + const url = `${config.baseUri}${config.paths.invoke}` console.log(`Posting to Opey, STREAMING OFF: ${JSON.stringify(user_input)}\n URL: ${url}`) //DEBUG @@ -188,10 +193,7 @@ export default class OpeyClientService { try { const response = await fetch(url, { method: 'POST', - headers: { - "Authorization": `Bearer ${config.authConfig.opeyConsent.jwt}`, // not undefined as we checked authConfig - "Content-Type": "application/json" - }, + headers: authHeaders, body: JSON.stringify(user_input) }) if (response.status === 200) { @@ -210,7 +212,7 @@ export default class OpeyClientService { * Checks if the authentication configuration in the OpeyConfig is valid. * * This method validates that: - * - authConfig exists and contains opeyConsent + * - authConfig exists and contains obpConsent * - the OBP consent object has a status of 'ACCEPTED' * * @param opeyConfig - The configuration object to validate @@ -220,29 +222,29 @@ export default class OpeyClientService { */ async checkAuthConfig(opeyConfig: OpeyConfig): Promise<{ valid: boolean; reason: string }> { - if (!opeyConfig.authConfig || !opeyConfig.authConfig.opeyConsent) { + console.log("Checking auth config: ", opeyConfig) //DEBUG + + if (!opeyConfig.authConfig || !opeyConfig.authConfig.obpConsent) { return { valid: false, reason: 'No authConfig set in opeyConfig, authentication required' } - } else if (!opeyConfig.authConfig.opeyConsent) { + } else if (!opeyConfig.authConfig.obpConsent) { return { valid: false, reason: 'Opey consent missing in opeyConfig.authConfig' } } - if (!(opeyConfig.authConfig.opeyConsent.status === 'ACCEPTED')) { + if (!(opeyConfig.authConfig.obpConsent.status === 'ACCEPTED')) { return { valid: false, reason: 'Opey consent status is not ACCEPTED' } } return { valid: true, reason: 'AuthConfig is valid' } } - // async createConsentRequest(): Promise { - // // Create a consent request for the current user - - - // const oauthConfig = session['clientConfig'] - - // try { - // this.obpClientService.create('/obp/v5.0.0/consumer/consent-requests', ) - // } catch (error) { - // throw new Error(`Error creating consent request: ${error}`) - // } - // } + async getConsentAuthHeaders(opeyConfig: OpeyConfig): Promise<{ [key: string]: string } | undefined> { + + if (!opeyConfig.authConfig || !opeyConfig.authConfig.obpConsent) { + throw new Error('AuthConfig not found or obpConsent missing') + } + return { + 'Consent-JWT': opeyConfig.authConfig.obpConsent.jwt, + 'Content-Type': 'application/json' + } + } } \ No newline at end of file diff --git a/server/test/OBPConsentsService.test.ts b/server/test/OBPConsentsService.test.ts index 2398987..ae49f78 100644 --- a/server/test/OBPConsentsService.test.ts +++ b/server/test/OBPConsentsService.test.ts @@ -129,4 +129,30 @@ describe('OBPConsentsService.createConsent', () => { expect(consentRequest).toHaveProperty('status', 'INITIATED'); expect(mockOBPv310CreateConsentImplicit).toHaveBeenCalled(); }) + + it('should update the session with a valid OpeyConfig with auth', async () => { + // Create mock response function for consent IMPLICIT + mockOBPv310CreateConsentImplicit = vi.fn().mockResolvedValue({ + data: { + consent_id: '12345678', + jwt: "asdjfawieofaowbfaowhh2084h02pefhh0.20fh02h0h29eyf09q3h09h.2-hf4-8h284hf0h0h0284r0", + status: 'INITIATED', + }, + } as AxiosResponse); + + mockConsentApi = { + oBPv510CreateConsentImplicit: mockOBPv310CreateConsentImplicit, + } as unknown as ConsentApi; + + // Mock the createConsentClient method + vi.spyOn(obpConsentsService, 'createUserConsentsClient').mockResolvedValue(mockConsentApi); + + await obpConsentsService.createConsent(mockSession); + + expect(mockSession).toHaveProperty('opeyConfig'); + expect(mockSession.opeyConfig).toHaveProperty('authConfig'); + expect(mockSession.opeyConfig.authConfig).toHaveProperty('obpConsent'); + expect(mockSession.opeyConfig.authConfig.obpConsent).toHaveProperty('status', 'INITIATED'); + expect(mockSession.opeyConfig.authConfig.obpConsent).toHaveProperty('jwt', 'asdjfawieofaowbfaowhh2084h02pefhh0.20fh02h0h29eyf09q3h09h.2-hf4-8h284hf0h0h0284r0'); + }); }) \ No newline at end of file diff --git a/server/test/OpeyClientService.test.ts b/server/test/OpeyClientService.test.ts index 6241f91..bffe40b 100644 --- a/server/test/OpeyClientService.test.ts +++ b/server/test/OpeyClientService.test.ts @@ -73,7 +73,7 @@ describe('stream', async () => { opeyConfig = { authConfig: { - opeyConsent: { + obpConsent: { consent_id: 'test-consent-id', status: 'ACCEPTED', jwt: 'test-jwt-token', @@ -83,7 +83,7 @@ describe('stream', async () => { }) - it('should add the opeyConsent jwt to the Authorization header', async () => { + it('should add the obpConsent jwt to the Authorization header', async () => { const user_input: UserInput = { message: 'test message', @@ -95,7 +95,7 @@ describe('stream', async () => { expect(global.fetch).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({ method: 'POST', headers: expect.objectContaining({ - "Authorization": `Bearer ${opeyConfig.authConfig?.opeyConsent.jwt}`, + "Consent-JWT": `${opeyConfig.authConfig?.obpConsent.jwt}`, }), })) @@ -197,7 +197,7 @@ describe('getOpeyConfig', async () => { it('should merge authConfig when provided', async () => { const partialConfig: Partial = { authConfig: { - opeyConsent: { + obpConsent: { consent_id: 'test-consent-id', status: 'ACCEPTED', jwt: 'test-jwt-token', @@ -209,7 +209,7 @@ describe('getOpeyConfig', async () => { // Verify authConfig was added with correct values expect(resultConfig.authConfig).toBeDefined(); - expect(resultConfig.authConfig!.opeyConsent).toEqual({ + expect(resultConfig.authConfig!.obpConsent).toEqual({ consent_id: 'test-consent-id', status: 'ACCEPTED', jwt: 'test-jwt-token', @@ -272,14 +272,14 @@ describe('checkAuthConfig', async () => { expect(result.reason).toBe('No authConfig set in opeyConfig, authentication required'); }); - it('should return invalid when opeyConsent is missing', async () => { + it('should return invalid when obpConsent is missing', async () => { const opeyConfig: OpeyConfig = { baseUri: 'http://localhost:5000', paths: { status: '/status', }, authConfig: { - // opeyConsent intentionally missing + // obpConsent intentionally missing } }; @@ -296,7 +296,7 @@ describe('checkAuthConfig', async () => { status: '/status', }, authConfig: { - opeyConsent: { + obpConsent: { status: 'INITIATED', jwt: 'test-token', consent_id: '12345', @@ -318,7 +318,7 @@ describe('checkAuthConfig', async () => { status: '/status', }, authConfig: { - opeyConsent: { + obpConsent: { status: 'ACCEPTED', jwt: 'test-token', consent_id: '12345', @@ -339,7 +339,7 @@ describe('checkAuthConfig', async () => { status: '/status', }, authConfig: { - opeyConsent: { + obpConsent: { status: 'ACCEPTED', jwt: 'test-token', consent_id: '12345', diff --git a/server/test/opey-controller.test.ts b/server/test/opey-controller.test.ts index 880d58e..3bd60e1 100644 --- a/server/test/opey-controller.test.ts +++ b/server/test/opey-controller.test.ts @@ -3,11 +3,13 @@ import { OpeyController } from "../controllers/OpeyIIController"; import OpeyClientService from '../services/OpeyClientService'; import OBPClientService from '../services/OBPClientService'; import OBPConsentsService from '../services/OBPConsentsService'; +import { OpeyConfig } from '../schema/OpeySchema'; import Stream, { Readable } from 'stream'; import { Request, Response } from 'express'; import httpMocks from 'node-mocks-http' import { EventEmitter } from 'events'; import { InlineResponse2017 } from 'obp-api-typescript'; +import { c } from 'vitest/dist/reporters-5f784f42.js'; vi.mock("../../server/services/OpeyClientService", () => { return { @@ -169,7 +171,16 @@ describe('OpeyController consents', () => { const MockOpeyClientService = { authConfig: {}, - opeyConfig: {}, + opeyConfig: { + baseUri: 'http://localhost:8080', + paths: { + invoke: '/invoke', + status: '/status', + stream: '/stream', + approve_tool: '/approve_tool/{thread_id}', + feedback: '/feedback', + } + }, getOpeyStatus: vi.fn(async () => { return {status: 'running'} }), @@ -189,17 +200,36 @@ describe('OpeyController consents', () => { return { content: 'Hi this is Opey', } + }), + getOpeyConfig: vi.fn(async (partialConfig?) => { + return { + baseUri: 'http://localhost:8080', + paths: { + invoke: '/invoke', + status: '/status', + stream: '/stream', + approve_tool: '/approve_tool/{thread_id}', + feedback: '/feedback', + } + } }) } as unknown as OpeyClientService const MockOBPConsentsService = { - createConsent: vi.fn(async () => { - return { + createConsent: vi.fn(async (session) => { + + const mockConsentResponse = { "consent_id": "8ca8a7e4-6d02-40e3-a129-0b2bf89de9f0", "jwt": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Ik9CUCBDb25zZW50IFRva2VuIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE2MTYyMzkwMjJ9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c", "status": "INITIATED", } as InlineResponse2017 - }) + + session['opeyConfig'] = { + authConfig: { obpConsent: mockConsentResponse } + } + + return mockConsentResponse + }), } as unknown as OBPConsentsService // Instantiate OpeyController with the mocked OpeyClientService @@ -235,9 +265,14 @@ describe('OpeyController consents', () => { }) // Expect that the consent object was saved in the session - expect(session).toHaveProperty('obpConsent') - expect(session['obpConsent']).toHaveProperty('consent_id', "8ca8a7e4-6d02-40e3-a129-0b2bf89de9f0") - expect(session['obpConsent']).toHaveProperty('jwt', "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Ik9CUCBDb25zZW50IFRva2VuIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE2MTYyMzkwMjJ9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c") - expect(session['obpConsent']).toHaveProperty('status', "INITIATED") + expect(session).toHaveProperty('opeyConfig') + const opeyConfig = session['opeyConfig'] + console.log(opeyConfig) + expect(opeyConfig).toHaveProperty('authConfig') + expect(session['opeyConfig']).toHaveProperty('authConfig') + expect(session['opeyConfig']['authConfig']).toHaveProperty('obpConsent') + expect(session['opeyConfig']['authConfig']['obpConsent']).toHaveProperty('consent_id', "8ca8a7e4-6d02-40e3-a129-0b2bf89de9f0") + expect(session['opeyConfig']['authConfig']['obpConsent']).toHaveProperty('jwt', "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Ik9CUCBDb25zZW50IFRva2VuIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE2MTYyMzkwMjJ9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c") + expect(session['opeyConfig']['authConfig']['obpConsent']).toHaveProperty('status', "INITIATED") }) }) \ No newline at end of file diff --git a/src/components/ChatWidget.vue b/src/components/ChatWidget.vue index 24b3f9f..652c431 100644 --- a/src/components/ChatWidget.vue +++ b/src/components/ChatWidget.vue @@ -8,7 +8,7 @@ import { Close, Top as ElTop } from '@element-plus/icons-vue' import { ElMessage } from 'element-plus' import ChatMessage from './ChatMessage.vue'; import { v4 as uuidv4 } from 'uuid'; -import { OpeyStreamContext, OpeyMessage, UserMessage, sendOpeyMessage, getOpeyConsent } from '@/obp/opey-functions'; +import { OpeyStreamContext, OpeyMessage, UserMessage, sendOpeyMessage, getobpConsent } from '@/obp/opey-functions'; import { getCurrentUser } from '@/obp'; export default { @@ -64,7 +64,7 @@ export default { }, async initiateConsentFlow() { // get consent for Opey from user - const consentResponse = await getOpeyConsent() + const consentResponse = await getobpConsent() if (consentResponse) { const consentId = consentResponse.consent_id diff --git a/src/components/ChatWidgetOld.vue b/src/components/ChatWidgetOld.vue index e1639f4..1f9c401 100644 --- a/src/components/ChatWidgetOld.vue +++ b/src/components/ChatWidgetOld.vue @@ -34,7 +34,7 @@ import { inject } from 'vue'; import { obpApiHostKey } from '@/obp/keys'; import { getCurrentUser } from '../obp'; - import { getOpeyJWT, getOpeyConsent, answerOpeyConsentChallenge } from '@/obp/common-functions' + import { getOpeyJWT, getobpConsent, answerobpConsentChallenge } from '@/obp/common-functions' import { storeToRefs } from "pinia"; import { socket } from '@/socket'; import { useConnectionStore } from '@/stores/connection'; @@ -126,7 +126,7 @@ // Check if the user already has a token in the cookies try { - const consentResponse = await getOpeyConsent() + const consentResponse = await getobpConsent() console.log('Consent response: ', consentResponse) if (consentResponse.status === 200 && consentResponse.data.consent_id) { this.consentId = consentResponse.data.consent_id diff --git a/src/obp/common-functions.ts b/src/obp/common-functions.ts index 2c82ef9..3b8f0ba 100644 --- a/src/obp/common-functions.ts +++ b/src/obp/common-functions.ts @@ -85,12 +85,12 @@ export async function getOpeyJWT() { return token } -export async function getOpeyConsent() { +export async function getobpConsent() { await axios.post('/api/opey/consent').catch((error) => { if (error.response) { - throw new Error(`getOpeyConsent returned an error: ${error.toJSON()}`); + throw new Error(`getobpConsent returned an error: ${error.toJSON()}`); } else { - throw new Error(`getOpeyConsent returned an error: ${error.message}`); + throw new Error(`getobpConsent returned an error: ${error.message}`); } }).then((response) => { console.log(response) @@ -98,12 +98,12 @@ export async function getOpeyConsent() { }); } -export async function answerOpeyConsentChallenge(answerBody: any) { +export async function answerobpConsentChallenge(answerBody: any) { const response = await axios.post('/api/opey/consent/answer-challenge', answerBody).catch((error) => { if (error.response) { - throw new Error(`answerOpeyConsentChallenge returned an error: ${error.toJSON()}`); + throw new Error(`answerobpConsentChallenge returned an error: ${error.toJSON()}`); } else { - throw new Error(`answerOpeyConsentChallenge returned an error: ${error.message}`); + throw new Error(`answerobpConsentChallenge returned an error: ${error.message}`); } }); return response diff --git a/src/obp/opey-functions.ts b/src/obp/opey-functions.ts index 3cc5d51..8f6bebc 100644 --- a/src/obp/opey-functions.ts +++ b/src/obp/opey-functions.ts @@ -19,7 +19,7 @@ export interface OpeyStreamContext { status: string; } -export interface OpeyConsentObject { +export interface obpConsentObject { consent_id: string; } @@ -135,7 +135,7 @@ export async function sendOpeyMessage( } -export async function getOpeyConsent(): Promise { +export async function getobpConsent(): Promise { // Get consent from the Opey API try { const consentResponse = await fetch('/api/opey/consent', { diff --git a/src/test/opey-functions.test.ts b/src/test/opey-functions.test.ts index 1dbebaf..81f49b1 100644 --- a/src/test/opey-functions.test.ts +++ b/src/test/opey-functions.test.ts @@ -244,7 +244,7 @@ describe('sendOpeyMessage', () => { }) }) -describe('getOpeyConsent', () => { +describe('getobpConsent', () => { beforeEach(() => { global.fetch = vi.fn(() => @@ -256,12 +256,12 @@ describe('getOpeyConsent', () => { }) it('should call fetch', async () => { - await OpeyModule.getOpeyConsent() + await OpeyModule.getobpConsent() expect(global.fetch).toHaveBeenCalled() }) it('should return a consent id', async () => { - const consentId = await OpeyModule.getOpeyConsent() + const consentId = await OpeyModule.getobpConsent() expect(consentId).toStrictEqual({consent_id: 1234}) }) }) \ No newline at end of file