mirror of
https://github.com/OpenBankProject/API-Explorer-II.git
synced 2026-02-06 10:47:04 +00:00
add logout error handling
This commit is contained in:
parent
ba0fe64f02
commit
e95a172235
2
components.d.ts
vendored
2
components.d.ts
vendored
@ -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']
|
||||
|
||||
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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
|
||||
}
|
||||
}
|
||||
@ -46,6 +46,10 @@ export class OpeyController {
|
||||
@Res() response: Response,
|
||||
): Promise<Response> {
|
||||
|
||||
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) {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -67,18 +67,21 @@ export default {
|
||||
<div class="tool-message-container" v-bind:class="expanded? 'expanded':''">
|
||||
|
||||
<div class="tool-message-header">
|
||||
<div class="status" v-bind:class="status">
|
||||
<div v-if="status === 'pending'">
|
||||
<el-icon class="is-loading" color="#20cbeb"><RefreshRight /></el-icon>
|
||||
<div class="tool-name">Tool Call: {{ name }}</div>
|
||||
<div class="right-aligned">
|
||||
<div class="status" v-bind:class="status">
|
||||
<div v-if="status === 'pending'">
|
||||
<el-icon class="is-loading" color="#20cbeb"><RefreshRight /></el-icon>
|
||||
</div>
|
||||
<div v-else-if="status === 'success'">
|
||||
<el-icon color="#00ff18"><Check /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="status === 'success'">
|
||||
<el-icon color="#00ff18"><Check /></el-icon>
|
||||
<div class="expand-icon" @click="toggleExpanded">
|
||||
<el-icon><ArrowDown v-if="!expanded" /><ArrowUp v-else /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tool-name">{{ name }}</div>
|
||||
<div class="expand-icon" @click="toggleExpanded">
|
||||
<el-icon><ArrowDown v-if="!expanded" /><ArrowUp v-else /></el-icon>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
@ -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%;
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user