mirror of
https://github.com/OpenBankProject/API-Explorer-II.git
synced 2026-02-06 10:47:04 +00:00
use plain express 5 with new files
This commit is contained in:
parent
5cb5cfc229
commit
c755b47e80
244
server/routes/obp.ts
Normal file
244
server/routes/obp.ts
Normal file
@ -0,0 +1,244 @@
|
||||
/*
|
||||
* Open Bank Project - API Explorer II
|
||||
* Copyright (C) 2023-2025, 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 { Router } from 'express'
|
||||
import type { Request, Response } from 'express'
|
||||
import { Container } from 'typedi'
|
||||
import OBPClientService from '../services/OBPClientService.js'
|
||||
import { OAuth2Service } from '../services/OAuth2Service.js'
|
||||
|
||||
const router = Router()
|
||||
|
||||
// Get services from container
|
||||
const obpClientService = Container.get(OBPClientService)
|
||||
const oauth2Service = Container.get(OAuth2Service)
|
||||
|
||||
/**
|
||||
* Check if access token is expired and refresh it if needed
|
||||
* This ensures API calls always use a valid token
|
||||
*/
|
||||
async function ensureValidToken(session: any): Promise<boolean> {
|
||||
const accessToken = session.oauth2_access_token
|
||||
const refreshToken = session.oauth2_refresh_token
|
||||
|
||||
// If no access token, user is not authenticated
|
||||
if (!accessToken) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if token is expired
|
||||
if (oauth2Service.isTokenExpired(accessToken)) {
|
||||
console.log('OBP: Access token expired, attempting refresh')
|
||||
|
||||
if (!refreshToken) {
|
||||
console.log('OBP: No refresh token available')
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
const newTokens = await oauth2Service.refreshAccessToken(refreshToken)
|
||||
|
||||
// Update session with new tokens
|
||||
session.oauth2_access_token = newTokens.accessToken
|
||||
session.oauth2_refresh_token = newTokens.refreshToken || refreshToken
|
||||
session.oauth2_id_token = newTokens.idToken
|
||||
session.oauth2_token_timestamp = Date.now()
|
||||
session.oauth2_expires_in = newTokens.expiresIn
|
||||
|
||||
// Update clientConfig with new access token
|
||||
if (session.clientConfig && session.clientConfig.oauth2) {
|
||||
session.clientConfig.oauth2.accessToken = newTokens.accessToken
|
||||
console.log('OBP: Updated clientConfig with refreshed token')
|
||||
}
|
||||
|
||||
console.log('OBP: Token refresh successful')
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('OBP: Token refresh failed:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Token is still valid
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /get
|
||||
* Proxy GET requests to OBP API
|
||||
* Query params:
|
||||
* - path: OBP API path to call (e.g., /obp/v5.1.0/banks)
|
||||
*/
|
||||
router.get('/get', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const path = req.query.path as string
|
||||
const session = req.session as any
|
||||
|
||||
// Ensure token is valid before making the request
|
||||
const tokenValid = await ensureValidToken(session)
|
||||
if (!tokenValid && session.oauth2_user) {
|
||||
console.log('OBP: Token expired and refresh failed')
|
||||
return res.status(401).json({
|
||||
code: 401,
|
||||
message: 'Session expired. Please log in again.'
|
||||
})
|
||||
}
|
||||
|
||||
const oauthConfig = session.clientConfig
|
||||
|
||||
const result = await obpClientService.get(path, oauthConfig)
|
||||
res.json(result)
|
||||
} catch (error: any) {
|
||||
// 401 errors are expected when user is not authenticated - log as info, not error
|
||||
if (error.status === 401) {
|
||||
console.log(`OBP: 401 Unauthorized for path: ${req.query.path} (user not authenticated)`)
|
||||
} else {
|
||||
console.error('OBP: GET request error:', error)
|
||||
}
|
||||
res.status(error.status || 500).json({
|
||||
code: error.status || 500,
|
||||
message: error.message || 'Internal server error'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* POST /create
|
||||
* Proxy POST requests to OBP API
|
||||
* Query params:
|
||||
* - path: OBP API path to call
|
||||
* Body: JSON data to send to OBP API
|
||||
*/
|
||||
router.post('/create', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const path = req.query.path as string
|
||||
const data = req.body
|
||||
const session = req.session as any
|
||||
|
||||
// Ensure token is valid before making the request
|
||||
const tokenValid = await ensureValidToken(session)
|
||||
if (!tokenValid && session.oauth2_user) {
|
||||
console.log('OBP: Token expired and refresh failed')
|
||||
return res.status(401).json({
|
||||
code: 401,
|
||||
message: 'Session expired. Please log in again.'
|
||||
})
|
||||
}
|
||||
|
||||
const oauthConfig = session.clientConfig
|
||||
|
||||
// Debug logging to diagnose authentication issues
|
||||
console.log('OBP.create - Debug Info:')
|
||||
console.log(' Path:', path)
|
||||
console.log(' Session exists:', !!session)
|
||||
console.log(' clientConfig exists:', !!oauthConfig)
|
||||
console.log(' oauth2 exists:', oauthConfig?.oauth2 ? 'YES' : 'NO')
|
||||
console.log(' accessToken exists:', oauthConfig?.oauth2?.accessToken ? 'YES' : 'NO')
|
||||
console.log(' oauth2_user exists:', session?.oauth2_user ? 'YES' : 'NO')
|
||||
|
||||
const result = await obpClientService.create(path, data, oauthConfig)
|
||||
res.json(result)
|
||||
} catch (error: any) {
|
||||
console.error('OBP.create error:', error)
|
||||
res.status(error.status || 500).json({
|
||||
code: error.status || 500,
|
||||
message: error.message || 'Internal server error'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* PUT /update
|
||||
* Proxy PUT requests to OBP API
|
||||
* Query params:
|
||||
* - path: OBP API path to call
|
||||
* Body: JSON data to send to OBP API
|
||||
*/
|
||||
router.put('/update', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const path = req.query.path as string
|
||||
const data = req.body
|
||||
const session = req.session as any
|
||||
|
||||
// Ensure token is valid before making the request
|
||||
const tokenValid = await ensureValidToken(session)
|
||||
if (!tokenValid && session.oauth2_user) {
|
||||
console.log('OBP: Token expired and refresh failed')
|
||||
return res.status(401).json({
|
||||
code: 401,
|
||||
message: 'Session expired. Please log in again.'
|
||||
})
|
||||
}
|
||||
|
||||
const oauthConfig = session.clientConfig
|
||||
|
||||
const result = await obpClientService.update(path, data, oauthConfig)
|
||||
res.json(result)
|
||||
} catch (error: any) {
|
||||
console.error('OBP.update error:', error)
|
||||
res.status(error.status || 500).json({
|
||||
code: error.status || 500,
|
||||
message: error.message || 'Internal server error'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* DELETE /delete
|
||||
* Proxy DELETE requests to OBP API
|
||||
* Query params:
|
||||
* - path: OBP API path to call
|
||||
*/
|
||||
router.delete('/delete', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const path = req.query.path as string
|
||||
const session = req.session as any
|
||||
|
||||
// Ensure token is valid before making the request
|
||||
const tokenValid = await ensureValidToken(session)
|
||||
if (!tokenValid && session.oauth2_user) {
|
||||
console.log('OBP: Token expired and refresh failed')
|
||||
return res.status(401).json({
|
||||
code: 401,
|
||||
message: 'Session expired. Please log in again.'
|
||||
})
|
||||
}
|
||||
|
||||
const oauthConfig = session.clientConfig
|
||||
|
||||
const result = await obpClientService.discard(path, oauthConfig)
|
||||
res.json(result)
|
||||
} catch (error: any) {
|
||||
console.error('OBP.delete error:', error)
|
||||
res.status(error.status || 500).json({
|
||||
code: error.status || 500,
|
||||
message: error.message || 'Internal server error'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
280
server/routes/opey.ts
Normal file
280
server/routes/opey.ts
Normal file
@ -0,0 +1,280 @@
|
||||
/*
|
||||
* Open Bank Project - API Explorer II
|
||||
* Copyright (C) 2023-2025, 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 { Router } from 'express'
|
||||
import type { Request, Response } from 'express'
|
||||
import { Readable } from 'node:stream'
|
||||
import { ReadableStream as WebReadableStream } from 'stream/web'
|
||||
import { Container } from 'typedi'
|
||||
import OBPClientService from '../services/OBPClientService.js'
|
||||
import OpeyClientService from '../services/OpeyClientService.js'
|
||||
import OBPConsentsService from '../services/OBPConsentsService.js'
|
||||
import { UserInput } from '../schema/OpeySchema.js'
|
||||
|
||||
const router = Router()
|
||||
|
||||
// Get services from container
|
||||
const obpClientService = Container.get(OBPClientService)
|
||||
const opeyClientService = Container.get(OpeyClientService)
|
||||
const obpConsentsService = Container.get(OBPConsentsService)
|
||||
|
||||
/**
|
||||
* Helper function to convert web stream to Node.js stream
|
||||
*/
|
||||
function safeFromWeb(webStream: WebReadableStream<any>): Readable {
|
||||
if (typeof Readable.fromWeb === 'function') {
|
||||
return Readable.fromWeb(webStream)
|
||||
} else {
|
||||
console.warn('Readable.fromWeb is not available, using a polyfill')
|
||||
|
||||
// Create a Node.js Readable stream
|
||||
const nodeReadable = new Readable({
|
||||
read() {}
|
||||
})
|
||||
|
||||
// Pump data from webreadable to node readable stream
|
||||
const reader = webStream.getReader()
|
||||
|
||||
;(async () => {
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
|
||||
if (done) {
|
||||
nodeReadable.push(null) // end stream
|
||||
break
|
||||
}
|
||||
|
||||
nodeReadable.push(value)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error reading from web stream:', error)
|
||||
nodeReadable.destroy(error instanceof Error ? error : new Error(String(error)))
|
||||
}
|
||||
})()
|
||||
|
||||
return nodeReadable
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /opey
|
||||
* Check Opey chatbot status
|
||||
*/
|
||||
router.get('/opey', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const opeyStatus = await opeyClientService.getOpeyStatus()
|
||||
console.log('Opey status: ', opeyStatus)
|
||||
res.status(200).json({ status: 'Opey is running' })
|
||||
} catch (error) {
|
||||
console.error('Error in /opey endpoint: ', error)
|
||||
res.status(500).json({ error: 'Internal Server Error' })
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* POST /opey/stream
|
||||
* Stream chatbot responses
|
||||
* Body: { message, thread_id, is_tool_call_approval }
|
||||
*/
|
||||
router.post('/opey/stream', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const session = req.session as any
|
||||
|
||||
if (!session) {
|
||||
console.error('Session not found')
|
||||
return res.status(401).json({ error: 'Session Time Out' })
|
||||
}
|
||||
|
||||
// Check if the consent is in the session
|
||||
const opeyConfig = session.opeyConfig
|
||||
if (!opeyConfig) {
|
||||
console.error('Opey config not found in session')
|
||||
return res.status(500).json({ error: 'Internal Server Error' })
|
||||
}
|
||||
|
||||
// Read user input from request body
|
||||
let user_input: UserInput
|
||||
try {
|
||||
console.log('Request body: ', req.body)
|
||||
user_input = {
|
||||
message: req.body.message,
|
||||
thread_id: req.body.thread_id,
|
||||
is_tool_call_approval: req.body.is_tool_call_approval
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error in stream endpoint, could not parse into UserInput: ', error)
|
||||
return res.status(500).json({ error: 'Internal Server Error' })
|
||||
}
|
||||
|
||||
// Transform to decode and log the stream
|
||||
const frontendTransformer = new TransformStream({
|
||||
transform(chunk, controller) {
|
||||
// Decode the chunk to a string
|
||||
const decodedChunk = new TextDecoder().decode(chunk)
|
||||
|
||||
console.log('Sending chunk', decodedChunk)
|
||||
controller.enqueue(decodedChunk)
|
||||
},
|
||||
flush(controller) {
|
||||
console.log('[flush]')
|
||||
// Close ReadableStream when done
|
||||
controller.terminate()
|
||||
}
|
||||
})
|
||||
|
||||
let stream: ReadableStream | null = null
|
||||
|
||||
try {
|
||||
// Read web stream from OpeyClientService
|
||||
console.log('Calling OpeyClientService.stream')
|
||||
stream = await opeyClientService.stream(user_input, opeyConfig)
|
||||
} catch (error) {
|
||||
console.error('Error reading stream: ', error)
|
||||
return res.status(500).json({ error: 'Internal Server Error' })
|
||||
}
|
||||
|
||||
if (!stream) {
|
||||
console.error('Stream is not received or not readable')
|
||||
return res.status(500).json({ error: 'Internal Server Error' })
|
||||
}
|
||||
|
||||
// Transform our stream
|
||||
const frontendStream: ReadableStream = stream.pipeThrough(frontendTransformer)
|
||||
|
||||
const nodeStream = safeFromWeb(frontendStream as WebReadableStream<any>)
|
||||
|
||||
res.setHeader('Content-Type', 'text/event-stream')
|
||||
res.setHeader('Cache-Control', 'no-cache')
|
||||
res.setHeader('Connection', 'keep-alive')
|
||||
|
||||
nodeStream.pipe(res)
|
||||
|
||||
// Handle stream completion
|
||||
nodeStream.on('end', () => {
|
||||
console.log('Stream ended successfully')
|
||||
})
|
||||
|
||||
nodeStream.on('error', (error) => {
|
||||
console.error('Stream error:', error)
|
||||
})
|
||||
|
||||
// Add a timeout to prevent hanging
|
||||
const timeout = setTimeout(() => {
|
||||
console.warn('Stream timeout reached')
|
||||
nodeStream.destroy()
|
||||
}, 30000)
|
||||
|
||||
// Clear the timeout when stream ends
|
||||
nodeStream.on('end', () => clearTimeout(timeout))
|
||||
nodeStream.on('error', () => clearTimeout(timeout))
|
||||
} catch (error) {
|
||||
console.error('Error in /opey/stream:', error)
|
||||
if (!res.headersSent) {
|
||||
res.status(500).json({ error: 'Internal Server Error' })
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* POST /opey/invoke
|
||||
* Invoke chatbot without streaming
|
||||
* Body: { message, thread_id, is_tool_call_approval }
|
||||
*/
|
||||
router.post('/opey/invoke', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const session = req.session as any
|
||||
|
||||
// Check if the consent is in the session
|
||||
const opeyConfig = session.opeyConfig
|
||||
if (!opeyConfig) {
|
||||
console.error('Opey config not found in session')
|
||||
return res.status(500).json({ error: 'Internal Server Error' })
|
||||
}
|
||||
|
||||
let user_input: UserInput
|
||||
try {
|
||||
user_input = {
|
||||
message: req.body.message,
|
||||
thread_id: req.body.thread_id,
|
||||
is_tool_call_approval: req.body.is_tool_call_approval
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error in invoke endpoint, could not parse into UserInput: ', error)
|
||||
return res.status(500).json({ error: 'Internal Server Error' })
|
||||
}
|
||||
|
||||
const opey_response = await opeyClientService.invoke(user_input, opeyConfig)
|
||||
res.status(200).json(opey_response)
|
||||
} catch (error) {
|
||||
console.error('Error in /opey/invoke:', error)
|
||||
res.status(500).json({ error: 'Internal Server Error' })
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* POST /opey/consent
|
||||
* Retrieve or create a consent for Opey to access OBP on user's behalf
|
||||
*/
|
||||
router.post('/opey/consent', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const session = req.session as any
|
||||
|
||||
// Create consent as logged in user
|
||||
const opeyConfig = await opeyClientService.getOpeyConfig()
|
||||
session.opeyConfig = opeyConfig
|
||||
|
||||
// Check if user already has a consent for opey
|
||||
const consentId = await obpConsentsService.getExistingOpeyConsentId(session)
|
||||
|
||||
if (consentId) {
|
||||
console.log('Existing consent ID: ', consentId)
|
||||
// If we have a consent id, we can get the consent from OBP
|
||||
const consent = await obpConsentsService.getConsentByConsentId(session, consentId)
|
||||
|
||||
return res.status(200).json({ consent_id: consent.consent_id, jwt: consent.jwt })
|
||||
} else {
|
||||
console.log('No existing consent ID found')
|
||||
}
|
||||
|
||||
await obpConsentsService.createConsent(session)
|
||||
|
||||
console.log('Consent at controller: ', session.opeyConfig)
|
||||
|
||||
const authConfig = session.opeyConfig?.authConfig
|
||||
|
||||
res.status(200).json({
|
||||
consent_id: authConfig?.obpConsent.consent_id,
|
||||
jwt: authConfig?.obpConsent.jwt
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error in /opey/consent endpoint: ', error)
|
||||
res.status(500).json({ error: 'Internal Server Error' })
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
228
server/routes/status.ts
Normal file
228
server/routes/status.ts
Normal file
@ -0,0 +1,228 @@
|
||||
/*
|
||||
* Open Bank Project - API Explorer II
|
||||
* Copyright (C) 2023-2025, 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 { Router } from 'express'
|
||||
import type { Request, Response } from 'express'
|
||||
import { Container } from 'typedi'
|
||||
import OBPClientService from '../services/OBPClientService.js'
|
||||
import { OAuth2Service } from '../services/OAuth2Service.js'
|
||||
import { commitId } from '../app.js'
|
||||
import {
|
||||
RESOURCE_DOCS_API_VERSION,
|
||||
MESSAGE_DOCS_API_VERSION,
|
||||
API_VERSIONS_LIST_API_VERSION
|
||||
} from '../../src/shared-constants.js'
|
||||
|
||||
const router = Router()
|
||||
|
||||
// Get services from container
|
||||
const obpClientService = Container.get(OBPClientService)
|
||||
const oauth2Service = Container.get(OAuth2Service)
|
||||
|
||||
const connectors = ['akka_vDec2018', 'rest_vMar2019', 'stored_procedure_vDec2019', 'rabbitmq_vOct2024']
|
||||
|
||||
/**
|
||||
* Helper function to check if response contains an error
|
||||
*/
|
||||
function isCodeError(response: any, path: string): boolean {
|
||||
console.log(`Validating ${path} response...`)
|
||||
if (!response || Object.keys(response).length === 0) return true
|
||||
if (Object.keys(response).includes('code')) {
|
||||
const code = response['code']
|
||||
if (code >= 400) {
|
||||
console.log(response) // Log error response
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if resource docs are accessible
|
||||
*/
|
||||
async function checkResourceDocs(oauthConfig: any, version: string): Promise<boolean> {
|
||||
try {
|
||||
const path = `/obp/${RESOURCE_DOCS_API_VERSION}/resource-docs/${version}/obp`
|
||||
const resourceDocs = await obpClientService.get(path, oauthConfig)
|
||||
return !isCodeError(resourceDocs, path)
|
||||
} catch (error) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if message docs are accessible
|
||||
*/
|
||||
async function checkMessageDocs(oauthConfig: any, version: string): Promise<boolean> {
|
||||
try {
|
||||
const messageDocsCodeResult = await Promise.all(
|
||||
connectors.map(async (connector) => {
|
||||
const path = `/obp/${MESSAGE_DOCS_API_VERSION}/message-docs/${connector}`
|
||||
return !isCodeError(await obpClientService.get(path, oauthConfig), path)
|
||||
})
|
||||
)
|
||||
return messageDocsCodeResult.every((isCodeError: boolean) => isCodeError)
|
||||
} catch (error) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if API versions are accessible
|
||||
*/
|
||||
async function checkApiVersions(oauthConfig: any, version: string): Promise<boolean> {
|
||||
try {
|
||||
const path = `/obp/${API_VERSIONS_LIST_API_VERSION}/api/versions`
|
||||
const versions = await obpClientService.get(path, oauthConfig)
|
||||
return !isCodeError(versions, path)
|
||||
} catch (error) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /status
|
||||
* Get application status and health checks
|
||||
*/
|
||||
router.get('/status', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const session = req.session as any
|
||||
const oauthConfig = session.clientConfig
|
||||
const version = obpClientService.getOBPVersion()
|
||||
|
||||
// Check if user is authenticated
|
||||
const isAuthenticated = oauthConfig && oauthConfig.oauth2?.accessToken
|
||||
|
||||
let currentUser = null
|
||||
let apiVersions = false
|
||||
let messageDocs = false
|
||||
let resourceDocs = false
|
||||
|
||||
if (isAuthenticated) {
|
||||
try {
|
||||
currentUser = await obpClientService.get(`/obp/${version}/users/current`, oauthConfig)
|
||||
apiVersions = await checkApiVersions(oauthConfig, version)
|
||||
messageDocs = await checkMessageDocs(oauthConfig, version)
|
||||
resourceDocs = await checkResourceDocs(oauthConfig, version)
|
||||
} catch (error) {
|
||||
console.error('Status: Error fetching authenticated data:', error)
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
status: apiVersions && messageDocs && resourceDocs,
|
||||
apiVersions,
|
||||
messageDocs,
|
||||
resourceDocs,
|
||||
currentUser,
|
||||
isAuthenticated,
|
||||
commitId
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Status: Error getting status:', error)
|
||||
res.status(500).json({
|
||||
status: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* GET /status/oauth2
|
||||
* Get OAuth2/OIDC status
|
||||
*/
|
||||
router.get('/status/oauth2', (req: Request, res: Response) => {
|
||||
try {
|
||||
const isInitialized = oauth2Service.isInitialized()
|
||||
const oidcConfig = oauth2Service.getOIDCConfiguration()
|
||||
const healthCheckActive = oauth2Service.isHealthCheckActive()
|
||||
const healthCheckAttempts = oauth2Service.getHealthCheckAttempts()
|
||||
|
||||
res.json({
|
||||
available: isInitialized,
|
||||
message: isInitialized
|
||||
? 'OAuth2/OIDC is ready for authentication'
|
||||
: 'OAuth2/OIDC is not available',
|
||||
issuer: oidcConfig?.issuer || null,
|
||||
authorizationEndpoint: oidcConfig?.authorization_endpoint || null,
|
||||
wellKnownUrl: process.env.VITE_OBP_OAUTH2_WELL_KNOWN_URL || null,
|
||||
healthCheck: {
|
||||
active: healthCheckActive,
|
||||
attempts: healthCheckAttempts
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
available: false,
|
||||
message: 'Error checking OAuth2 status',
|
||||
error: error instanceof Error ? error.message : 'Unknown error'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* GET /status/oauth2/reconnect
|
||||
* Attempt to reconnect OAuth2/OIDC
|
||||
*/
|
||||
router.get('/status/oauth2/reconnect', async (req: Request, res: Response) => {
|
||||
try {
|
||||
if (oauth2Service.isInitialized()) {
|
||||
return res.json({
|
||||
success: true,
|
||||
message: 'OAuth2 is already connected',
|
||||
alreadyConnected: true
|
||||
})
|
||||
}
|
||||
|
||||
const wellKnownUrl = process.env.VITE_OBP_OAUTH2_WELL_KNOWN_URL
|
||||
if (!wellKnownUrl) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'VITE_OBP_OAUTH2_WELL_KNOWN_URL not configured'
|
||||
})
|
||||
}
|
||||
|
||||
console.log('Manual OAuth2 reconnection attempt triggered...')
|
||||
await oauth2Service.initializeFromWellKnown(wellKnownUrl)
|
||||
|
||||
console.log('Manual OAuth2 reconnection successful!')
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'OAuth2 reconnection successful',
|
||||
issuer: oauth2Service.getOIDCConfiguration()?.issuer || null
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Manual OAuth2 reconnection failed:', error)
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'OAuth2 reconnection failed',
|
||||
error: error instanceof Error ? error.message : 'Unknown error'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
173
server/routes/user.ts
Normal file
173
server/routes/user.ts
Normal file
@ -0,0 +1,173 @@
|
||||
/*
|
||||
* Open Bank Project - API Explorer II
|
||||
* Copyright (C) 2023-2025, 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 { Router } from 'express'
|
||||
import type { Request, Response } from 'express'
|
||||
import { Container } from 'typedi'
|
||||
import OBPClientService from '../services/OBPClientService.js'
|
||||
import { OAuth2Service } from '../services/OAuth2Service.js'
|
||||
import { DEFAULT_OBP_API_VERSION } from '../../src/shared-constants.js'
|
||||
|
||||
const router = Router()
|
||||
|
||||
// Get services from container
|
||||
const obpClientService = Container.get(OBPClientService)
|
||||
const oauth2Service = Container.get(OAuth2Service)
|
||||
|
||||
const obpExplorerHome = process.env.VITE_OBP_API_EXPLORER_HOST
|
||||
|
||||
/**
|
||||
* GET /user/current
|
||||
* Get current logged in user information
|
||||
*/
|
||||
router.get('/user/current', async (req: Request, res: Response) => {
|
||||
try {
|
||||
console.log('User: Getting current user')
|
||||
const session = req.session as any
|
||||
|
||||
// Check OAuth2 session
|
||||
if (!session.oauth2_user) {
|
||||
console.log('User: No authentication session found')
|
||||
return res.json({})
|
||||
}
|
||||
|
||||
console.log('User: Returning OAuth2 user info')
|
||||
const oauth2User = session.oauth2_user
|
||||
const accessToken = session.oauth2_access_token
|
||||
const refreshToken = session.oauth2_refresh_token
|
||||
|
||||
// Check if access token is expired and needs refresh
|
||||
if (accessToken && oauth2Service.isTokenExpired(accessToken)) {
|
||||
console.log('User: Access token expired')
|
||||
|
||||
if (refreshToken) {
|
||||
console.log('User: Attempting token refresh')
|
||||
try {
|
||||
const newTokens = await oauth2Service.refreshAccessToken(refreshToken)
|
||||
|
||||
// Update session with new tokens
|
||||
session.oauth2_access_token = newTokens.accessToken
|
||||
session.oauth2_refresh_token = newTokens.refreshToken || refreshToken
|
||||
session.oauth2_id_token = newTokens.idToken
|
||||
session.oauth2_token_timestamp = Date.now()
|
||||
session.oauth2_expires_in = newTokens.expiresIn
|
||||
|
||||
// Update clientConfig with new access token
|
||||
if (session.clientConfig && session.clientConfig.oauth2) {
|
||||
session.clientConfig.oauth2.accessToken = newTokens.accessToken
|
||||
console.log('User: Updated clientConfig with new access token')
|
||||
}
|
||||
|
||||
console.log('User: Token refresh successful')
|
||||
} catch (error) {
|
||||
console.error('User: Token refresh failed:', error)
|
||||
return res.json({})
|
||||
}
|
||||
} else {
|
||||
console.log('User: No refresh token available, user needs to re-authenticate')
|
||||
return res.json({})
|
||||
}
|
||||
}
|
||||
|
||||
// Get actual user ID from OBP-API
|
||||
let obpUserId = oauth2User.sub // Default to sub if OBP call fails
|
||||
const clientConfig = session.clientConfig
|
||||
|
||||
if (clientConfig && clientConfig.oauth2?.accessToken) {
|
||||
try {
|
||||
const version = DEFAULT_OBP_API_VERSION
|
||||
console.log('User: Fetching OBP user from /obp/' + version + '/users/current')
|
||||
const obpUser = await obpClientService.get(`/obp/${version}/users/current`, clientConfig)
|
||||
if (obpUser && obpUser.user_id) {
|
||||
obpUserId = obpUser.user_id
|
||||
console.log('User: Got OBP user ID:', obpUserId, '(was:', oauth2User.sub, ')')
|
||||
} else {
|
||||
console.warn('User: OBP user response has no user_id:', obpUser)
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.warn('User: Could not fetch OBP user ID, using token sub:', oauth2User.sub)
|
||||
console.warn('User: Error details:', error.message)
|
||||
}
|
||||
} else {
|
||||
console.warn('User: No valid clientConfig or access token, using token sub:', oauth2User.sub)
|
||||
}
|
||||
|
||||
// Return user info in format compatible with frontend
|
||||
res.json({
|
||||
user_id: obpUserId,
|
||||
username: oauth2User.username,
|
||||
email: oauth2User.email,
|
||||
email_verified: oauth2User.email_verified,
|
||||
name: oauth2User.name,
|
||||
given_name: oauth2User.given_name,
|
||||
family_name: oauth2User.family_name,
|
||||
provider: oauth2User.provider || 'oauth2'
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('User: Error getting current user:', error)
|
||||
res.json({})
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* GET /user/logoff
|
||||
* Logout user and clear session
|
||||
* Query params:
|
||||
* - redirect: URL to redirect to after logout (optional)
|
||||
*/
|
||||
router.get('/user/logoff', (req: Request, res: Response) => {
|
||||
console.log('User: Logging out user')
|
||||
const session = req.session as any
|
||||
|
||||
// Clear OAuth2 session data
|
||||
delete session.oauth2_access_token
|
||||
delete session.oauth2_refresh_token
|
||||
delete session.oauth2_id_token
|
||||
delete session.oauth2_token_type
|
||||
delete session.oauth2_expires_in
|
||||
delete session.oauth2_token_timestamp
|
||||
delete session.oauth2_user_info
|
||||
delete session.oauth2_user
|
||||
delete session.oauth2_provider
|
||||
delete session.clientConfig
|
||||
delete session.opeyConfig
|
||||
|
||||
// Destroy the session completely
|
||||
session.destroy((err: any) => {
|
||||
if (err) {
|
||||
console.error('User: Error destroying session:', err)
|
||||
} else {
|
||||
console.log('User: Session destroyed successfully')
|
||||
}
|
||||
|
||||
const redirectPage = (req.query.redirect as string) || obpExplorerHome || '/'
|
||||
console.log('User: Redirecting to:', redirectPage)
|
||||
res.redirect(redirectPage)
|
||||
})
|
||||
})
|
||||
|
||||
export default router
|
||||
@ -48,6 +48,7 @@ import type { OIDCConfiguration, TokenResponse } from '../types/oauth2.js'
|
||||
export class OAuth2ClientWithConfig extends OAuth2Client {
|
||||
public OIDCConfig?: OIDCConfiguration
|
||||
public provider: string
|
||||
public wellKnownUri?: string
|
||||
private _clientSecret: string
|
||||
private _redirectUri: string
|
||||
|
||||
@ -67,14 +68,16 @@ export class OAuth2ClientWithConfig extends OAuth2Client {
|
||||
* @example
|
||||
* await client.initOIDCConfig('http://localhost:9000/obp-oidc/.well-known/openid-configuration')
|
||||
*/
|
||||
async initOIDCConfig(oidcConfigUrl: string): Promise<void> {
|
||||
async initOIDCConfig(wellKnownUrl: string): Promise<void> {
|
||||
console.log(
|
||||
`OAuth2ClientWithConfig: Fetching OIDC config for ${this.provider} from:`,
|
||||
oidcConfigUrl
|
||||
`OAuth2ClientWithConfig: Fetching OIDC config for ${this.provider} from: ${wellKnownUrl}`
|
||||
)
|
||||
|
||||
// Store the well-known URL for health checks
|
||||
this.wellKnownUri = wellKnownUrl
|
||||
|
||||
try {
|
||||
const response = await fetch(oidcConfigUrl)
|
||||
const response = await fetch(wellKnownUrl)
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
|
||||
Loading…
Reference in New Issue
Block a user