diff --git a/server/routes/obp.ts b/server/routes/obp.ts
new file mode 100644
index 0000000..bf55513
--- /dev/null
+++ b/server/routes/obp.ts
@@ -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 .
+ *
+ * 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 {
+ 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
diff --git a/server/routes/opey.ts b/server/routes/opey.ts
new file mode 100644
index 0000000..58bca65
--- /dev/null
+++ b/server/routes/opey.ts
@@ -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 .
+ *
+ * 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): 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)
+
+ 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
diff --git a/server/routes/status.ts b/server/routes/status.ts
new file mode 100644
index 0000000..9c8c7b2
--- /dev/null
+++ b/server/routes/status.ts
@@ -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 .
+ *
+ * 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 {
+ 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 {
+ 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 {
+ 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
diff --git a/server/routes/user.ts b/server/routes/user.ts
new file mode 100644
index 0000000..fd445d1
--- /dev/null
+++ b/server/routes/user.ts
@@ -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 .
+ *
+ * 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
diff --git a/server/services/OAuth2ClientWithConfig.ts b/server/services/OAuth2ClientWithConfig.ts
index 670bc08..5e19f91 100644
--- a/server/services/OAuth2ClientWithConfig.ts
+++ b/server/services/OAuth2ClientWithConfig.ts
@@ -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 {
+ async initOIDCConfig(wellKnownUrl: string): Promise {
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(