From e95a1722351024cbb2a540adf86ae350812006f8 Mon Sep 17 00:00:00 2001 From: Nemo Godebski-Pedersen Date: Wed, 9 Apr 2025 15:47:43 +0100 Subject: [PATCH] add logout error handling --- components.d.ts | 2 + server/controllers/OpeyController.ts | 212 ------------------------- server/controllers/OpeyIIController.ts | 4 + src/components/ChatMessage.vue | 7 + src/components/ToolCall.vue | 33 ++-- src/stores/chat.ts | 11 +- 6 files changed, 46 insertions(+), 223 deletions(-) delete mode 100644 server/controllers/OpeyController.ts diff --git a/components.d.ts b/components.d.ts index f222a8f..9ca678f 100644 --- a/components.d.ts +++ b/components.d.ts @@ -39,6 +39,8 @@ declare module 'vue' { ElMenuItem: typeof import('element-plus/es')['ElMenuItem'] ElRow: typeof import('element-plus/es')['ElRow'] ElScrollbar: typeof import('element-plus/es')['ElScrollbar'] + ElSkeleton: typeof import('element-plus/es')['ElSkeleton'] + ElSkeletonItem: typeof import('element-plus/es')['ElSkeletonItem'] ElText: typeof import('element-plus/es')['ElText'] ElTooltip: typeof import('element-plus/es')['ElTooltip'] GlossarySearchNav: typeof import('./src/components/GlossarySearchNav.vue')['default'] diff --git a/server/controllers/OpeyController.ts b/server/controllers/OpeyController.ts deleted file mode 100644 index ea8fd1d..0000000 --- a/server/controllers/OpeyController.ts +++ /dev/null @@ -1,212 +0,0 @@ -/* - * Open Bank Project - API Explorer II - * Copyright (C) 2023-2024, TESOBE GmbH - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * - * Email: contact@tesobe.com - * TESOBE GmbH - * Osloerstrasse 16/17 - * Berlin 13359, Germany - * - * This product includes software developed at - * TESOBE (http://www.tesobe.com/) - * - */ - -import { Controller, Session, Req, Res, Post } from 'routing-controllers' -import { Request, Response } from 'express' -import OBPClientService from '../services/OBPClientService' -import { Service } from 'typedi' -import * as fs from 'fs' -import * as jwt from 'jsonwebtoken' - -@Service() -@Controller('/opey-old') -/** - * Controller class for handling Opey related operations. - * This used to hold the /chat endpoint, but that endpoint has become obsolete since using websockets. - * Now it serves to get tokens to authenticate the user at websocket handshake. - * This is called from the frontend when ChatWidget.vue is mounted. (It is done at the backend to keep the private key secret) - */ -export class OpeyController { - constructor( - private obpClientService: OBPClientService, - ) {} - - @Post('/consent') - /** - * Retrieves a consent from OBP for the current user - */ - async getConsent( - @Session() session: any, - @Req() request: Request, - @Res() response: Response - ): Response { - try { - console.log("Getting consent from OBP") - // Check if consent is already in session - if (session['obpConsent']) { - console.log("Consent found in session, returning cached consent ID") - const obpConsent = session['obpConsent'] - // NOTE: Arguably we should not return the consent to the frontend as it could be hijacked, - // we can keep everything in the backend and only return the JWT token - return response.status(200).json({consent_id: obpConsent.consent_id}); - } - - const oauthConfig = session['clientConfig'] - const version = this.obpClientService.getOBPVersion() - // Obbiously this should not be hard-coded, especially the consumer_id, but for now it is - const consentRequestBody = { - "everything": false, - "views": [], - "entitlements": [], - "consumer_id": "33e0a1bd-9f1d-4128-911b-8936110f802f" - } - - // Get current user, only proceed if user is logged in - const currentUser = await this.obpClientService.get(`/obp/${version}/users/current`, oauthConfig) - const currentResponseKeys = Object.keys(currentUser) - if (!currentResponseKeys.includes('user_id')) { - return response.status(400).json({ message: 'User not logged in, Authentication required' }); - } - - // url needs to be changed once we get the 'bankless' consent endpoint - // this creates a consent for the current logged in user, and starts SCA flow i.e. sends SMS or email OTP to user - const consent = await this.obpClientService.create(`/obp/${version}/banks/gh.29.uk/my/consents/IMPLICIT`, consentRequestBody, oauthConfig) - console.log("Consent: ", consent) - - // store consent in session, return consent 200 OK - session['obpConsent'] = consent - return response.status(200).json({consent_id: consent.consent_id}); - } catch (error) { - console.error("Error in consent endpoint: ", error); - return response.status(500).json({ error: 'Internal Server Error '}); - } - } - - @Post('/consent/answer-challenge') - /** - * Endpoint to answer the consent challenge with code i.e. SMS or email OTP for SCA - * If successful, returns a Consent-JWT for use by Opey to access endpoints/ roles that the consenting user has - * This completes (i.e. is the final step in) the consent flow - */ - async answerConsentChallenge( - @Session() session: any, - @Req() request: Request, - @Res() response: Response - ): Response { - try { - const oauthConfig = session['clientConfig'] - const version = this.obpClientService.getOBPVersion() - - const obpConsent = session['obpConsent'] - if (!obpConsent) { - return response.status(400).json({ message: 'Consent not found in session' }); - } else if (obpConsent.status === 'ACCEPTED') { - return response.status(400).json({ message: 'Consent already accepted' }); - } - const answerBody = request.body - - const consentJWT = await this.obpClientService.create(`/obp/${version}/banks/gh.29.uk/consents/${obpConsent.consent_id}/challenge`, answerBody, oauthConfig) - console.log("Consent JWT: ", consentJWT) - // store consent JWT in session, return consent JWT 200 OK - session['obpConsentJWT'] = consentJWT - return response.status(200).json(true); - - } catch (error) { - console.error("Error in consent/answer-challenge endpoint: ", error); - return response.status(500).json({ error: 'Internal Server Error' }); - } - - } - - - @Post('/token') - /** - * Retrieves a JWT token for the current user. - * This only works if the user is logged in. (i.e. the user has a valid session) - * Request for the token is made to POST /api/opey/token - * - * @param session - The session object. - * @param request - The request object. - * @param response - The response object. - * @returns The response containing the JWT token or an error message. - * - */ - async getToken( - @Session() session: any, - @Req() request: Request, - @Res() response: Response - ): Response { - try { - // Get current user - const oauthConfig = session['clientConfig'] - const version = this.obpClientService.getOBPVersion() - const currentUser = await this.obpClientService.get(`/obp/${version}/users/current`, oauthConfig) - const currentResponseKeys = Object.keys(currentUser) - // If current user is logged in, issue JWT signed with private key - if (currentResponseKeys.includes('user_id')) { - // sign - const jwtToken = this.generateJWT(currentUser.user_id, currentUser.username, session) - return response.status(200).json({ token: jwtToken }); - } else { - return response.status(400).json({ message: 'User not logged in, Authentication required' }); - } - } catch (error) { - console.error("Error in token endpoint: ", error); - return response.status(500).json({ error: 'Internal Server Error' }); - } - } - - /** - * Generates a JSON Web Token (JWT) for the given Open Bank Project (OBP) user. - * @param obpUserId - The ID of the OBP user. - * @param obpUsername - The username of the OBP user. - * @param session - The session object. - * @returns The generated JWT. - */ - generateJWT(obpUserId: string, obpUsername: string, session: typeof Session): string { - - // Retrieve secret key - let privateKey: string; - if (session['opeyToken']) { - console.log("Returning cached token"); - return session['opeyToken']; - } - - // Read private key from file - // Private key must be in the server/cert directory, this is pretty janky at the moment and should be improved - // Opey must also have a copy of the public key to verify the JWT - try { - privateKey = fs.readFileSync('./server/cert/private_key.pem', {encoding: 'utf-8'}); - } catch (error) { - console.error("Error reading private key: ", error); - return ''; - } - - // Allows some user data to be passed in the JWT (this could be the obp consent in the future) - const payload = { - user_id: obpUserId, - username: obpUsername, - exp: Math.floor(Date.now() / 1000) + (60 * 60), - }; - - console.log("Generating new token for Opey"); - const token = jwt.sign(payload, privateKey, { algorithm: 'RS256' }); - session['opeyToken'] = token; - - return token - } -} diff --git a/server/controllers/OpeyIIController.ts b/server/controllers/OpeyIIController.ts index d796c3c..7196be7 100644 --- a/server/controllers/OpeyIIController.ts +++ b/server/controllers/OpeyIIController.ts @@ -46,6 +46,10 @@ export class OpeyController { @Res() response: Response, ): Promise { + if (!session) { + console.error("Session not found") + return response.status(401).json({ error: 'Session Time Out' }) + } // Check if the consent is in the session, and can be added to the headers const opeyConfig = session['opeyConfig'] if (!opeyConfig) { diff --git a/src/components/ChatMessage.vue b/src/components/ChatMessage.vue index 540435b..4928a70 100644 --- a/src/components/ChatMessage.vue +++ b/src/components/ChatMessage.vue @@ -158,6 +158,13 @@ export default { background-color: #3e4e70; } +.assistant .error { + color: red; + font-weight: bold; + align-self: flex-start; + font-size: smaller; +} + .content { margin-top: -10px; margin-bottom: -10px; diff --git a/src/components/ToolCall.vue b/src/components/ToolCall.vue index 97a525a..1d40fbd 100644 --- a/src/components/ToolCall.vue +++ b/src/components/ToolCall.vue @@ -67,18 +67,21 @@ export default {
-
-
- +
Tool Call: {{ name }}
+
+
+
+ +
+
+ +
-
- +
+
-
{{ name }}
-
- -
+
@@ -179,16 +182,26 @@ export default { overflow: auto; } - +.status { + margin-left: auto; + margin-right: 20px; +} .expand-icon { margin-left: auto; cursor: pointer; } +.right-aligned { + display: flex; + flex-direction: row; + align-items: center; +} + .tool-message-header { display: flex; flex-direction: row; + justify-content: space-between; align-items: center; width: 100%; } diff --git a/src/stores/chat.ts b/src/stores/chat.ts index 4f15f52..1f23ba3 100644 --- a/src/stores/chat.ts +++ b/src/stores/chat.ts @@ -202,6 +202,10 @@ export const useChat = defineStore('chat', { } if (response.status !== 200) { + switch (response.status) { + case 401: + throw new Error('Unauthorized. Please log in again.'); + } throw new Error(`Error sending Opey message: ${response.statusText}`); } @@ -209,7 +213,12 @@ export const useChat = defineStore('chat', { } catch (error) { console.error('Error sending Opey message:', error); - const errorMessage = "Hmmm, Looks like smething went wrong. Please try again later."; + let errorMessage = "Hmmm, Looks like smething went wrong. Please try again later."; + + switch (error) { + case 'Unauthorized. Please log in again.': + errorMessage = 'You are not logged in. Please log in to continue.'; + } // Apply error state to the assistant message await this.applyErrorToMessage(this.currentAssistantMessage.id, errorMessage);