2024-05-14 11:41:52 +00:00
|
|
|
/*
|
|
|
|
|
* 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/)
|
|
|
|
|
*
|
|
|
|
|
*/
|
|
|
|
|
|
2023-05-03 17:17:01 +00:00
|
|
|
import { Service } from 'typedi'
|
2025-12-11 19:44:07 +00:00
|
|
|
import { DEFAULT_OBP_API_VERSION } from '../../src/shared-constants.js'
|
2025-12-01 10:28:40 +00:00
|
|
|
|
2025-12-02 01:08:16 +00:00
|
|
|
// Custom error class to preserve HTTP status codes
|
|
|
|
|
class OBPAPIError extends Error {
|
|
|
|
|
status: number
|
|
|
|
|
constructor(status: number, message: string) {
|
|
|
|
|
super(message)
|
|
|
|
|
this.status = status
|
|
|
|
|
this.name = 'OBPAPIError'
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-01 10:28:40 +00:00
|
|
|
// OAuth2 Bearer token configuration
|
|
|
|
|
interface OAuth2Config {
|
|
|
|
|
accessToken: string
|
|
|
|
|
tokenType: string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// API Client configuration for OAuth2
|
|
|
|
|
interface APIClientConfig {
|
|
|
|
|
baseUri: string
|
|
|
|
|
version: string
|
|
|
|
|
oauth2?: OAuth2Config
|
|
|
|
|
}
|
2023-05-03 17:17:01 +00:00
|
|
|
|
|
|
|
|
@Service()
|
2025-03-12 12:10:30 +00:00
|
|
|
/**
|
|
|
|
|
* OBPClientService provides methods for interacting with the Open Bank Project API.
|
2025-12-01 10:28:40 +00:00
|
|
|
*
|
|
|
|
|
* This service handles API communication with OBP using OAuth2 Bearer token authentication,
|
|
|
|
|
* making HTTP requests (GET, POST, PUT, DELETE).
|
|
|
|
|
*
|
2025-03-12 12:10:30 +00:00
|
|
|
* @class OBPClientService
|
2025-12-01 10:28:40 +00:00
|
|
|
*
|
2025-03-12 12:10:30 +00:00
|
|
|
* @property {APIClientConfig} clientConfig - API client configuration
|
2025-12-01 10:28:40 +00:00
|
|
|
*
|
2025-03-12 12:10:30 +00:00
|
|
|
* @example
|
|
|
|
|
* const obpService = new OBPClientService();
|
2025-12-01 10:28:40 +00:00
|
|
|
* const response = await obpService.get('/obp/v5.1.0/banks', sessionConfig);
|
2025-03-12 12:10:30 +00:00
|
|
|
*/
|
2023-05-03 17:17:01 +00:00
|
|
|
export default class OBPClientService {
|
|
|
|
|
private clientConfig: APIClientConfig
|
2025-12-01 10:28:40 +00:00
|
|
|
|
2023-05-03 17:17:01 +00:00
|
|
|
constructor() {
|
2025-11-07 06:15:10 +00:00
|
|
|
if (!process.env.VITE_OBP_API_HOST) throw new Error('VITE_OBP_API_HOST is not set')
|
2025-12-01 10:28:40 +00:00
|
|
|
|
2025-12-08 15:21:10 +00:00
|
|
|
// Always use v5.1.0 for application infrastructure - stable and debuggable
|
2023-05-03 17:17:01 +00:00
|
|
|
this.clientConfig = {
|
2025-11-07 06:15:10 +00:00
|
|
|
baseUri: process.env.VITE_OBP_API_HOST!,
|
2025-12-08 15:21:10 +00:00
|
|
|
version: DEFAULT_OBP_API_VERSION
|
2023-05-03 17:17:01 +00:00
|
|
|
}
|
|
|
|
|
}
|
2023-06-05 15:32:25 +00:00
|
|
|
async get(path: string, clientConfig: any): Promise<any> {
|
2023-06-05 15:39:20 +00:00
|
|
|
const config = this.getSessionConfig(clientConfig)
|
2025-12-01 10:28:40 +00:00
|
|
|
|
2025-12-02 13:26:19 +00:00
|
|
|
// If no config or no access token, make unauthenticated request
|
|
|
|
|
if (!config || !config.oauth2?.accessToken) {
|
|
|
|
|
return await this.getWithoutAuth(path)
|
2025-12-01 10:28:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return await this.getWithBearer(path, config.oauth2.accessToken)
|
2023-05-03 17:17:01 +00:00
|
|
|
}
|
2025-12-01 10:28:40 +00:00
|
|
|
|
2023-06-05 15:32:25 +00:00
|
|
|
async create(path: string, body: any, clientConfig: any): Promise<any> {
|
2023-06-05 15:39:20 +00:00
|
|
|
const config = this.getSessionConfig(clientConfig)
|
2025-12-01 10:28:40 +00:00
|
|
|
|
2025-12-02 13:26:19 +00:00
|
|
|
if (!config || !config.oauth2?.accessToken) {
|
|
|
|
|
throw new Error('Authentication required for creating resources.')
|
2025-12-01 10:28:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return await this.createWithBearer(path, body, config.oauth2.accessToken)
|
2023-05-03 17:17:01 +00:00
|
|
|
}
|
2025-12-01 10:28:40 +00:00
|
|
|
|
2023-06-05 15:32:25 +00:00
|
|
|
async update(path: string, body: any, clientConfig: any): Promise<any> {
|
2023-06-05 15:39:20 +00:00
|
|
|
const config = this.getSessionConfig(clientConfig)
|
2025-12-01 10:28:40 +00:00
|
|
|
|
2025-12-02 13:26:19 +00:00
|
|
|
if (!config || !config.oauth2?.accessToken) {
|
|
|
|
|
throw new Error('Authentication required for updating resources.')
|
2025-12-01 10:28:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return await this.updateWithBearer(path, body, config.oauth2.accessToken)
|
2023-05-03 17:17:01 +00:00
|
|
|
}
|
2025-12-01 10:28:40 +00:00
|
|
|
|
2023-06-05 15:32:25 +00:00
|
|
|
async discard(path: string, clientConfig: any): Promise<any> {
|
2023-06-05 15:39:20 +00:00
|
|
|
const config = this.getSessionConfig(clientConfig)
|
2025-12-01 10:28:40 +00:00
|
|
|
|
2025-12-02 13:26:19 +00:00
|
|
|
if (!config || !config.oauth2?.accessToken) {
|
|
|
|
|
throw new Error('Authentication required for deleting resources.')
|
2025-12-01 10:28:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return await this.discardWithBearer(path, config.oauth2.accessToken)
|
2023-06-05 15:39:20 +00:00
|
|
|
}
|
2025-03-12 12:10:30 +00:00
|
|
|
private getSessionConfig(clientConfig: APIClientConfig): APIClientConfig {
|
2023-06-05 15:39:20 +00:00
|
|
|
return clientConfig || this.clientConfig
|
2023-05-03 17:17:01 +00:00
|
|
|
}
|
2023-05-24 21:57:08 +00:00
|
|
|
|
|
|
|
|
getOBPVersion(): string {
|
|
|
|
|
return this.clientConfig.version
|
|
|
|
|
}
|
2023-06-05 15:32:25 +00:00
|
|
|
|
2025-03-07 16:21:10 +00:00
|
|
|
getOBPClientConfig(): APIClientConfig {
|
2023-06-05 15:32:25 +00:00
|
|
|
return this.clientConfig
|
|
|
|
|
}
|
2025-03-07 11:14:28 +00:00
|
|
|
|
2025-12-02 13:26:19 +00:00
|
|
|
/**
|
|
|
|
|
* Make a GET request without authentication (for public endpoints)
|
|
|
|
|
*
|
|
|
|
|
* @param path - The API endpoint path (e.g., /obp/v5.1.0/api/versions)
|
|
|
|
|
* @returns Response data from the API
|
|
|
|
|
*/
|
|
|
|
|
private async getWithoutAuth(path: string): Promise<any> {
|
|
|
|
|
// Ensure proper slash handling between base URI and path
|
|
|
|
|
const normalizedPath = path.startsWith('/') ? path : `/${path}`
|
|
|
|
|
const url = `${this.clientConfig.baseUri}${normalizedPath}`
|
|
|
|
|
console.log('OBPClientService: GET request without authentication to:', url)
|
|
|
|
|
|
|
|
|
|
const response = await fetch(url, {
|
|
|
|
|
method: 'GET',
|
|
|
|
|
headers: {
|
|
|
|
|
'Content-Type': 'application/json'
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
const errorText = await response.text()
|
|
|
|
|
// 401 errors are expected when user is not authenticated
|
|
|
|
|
if (response.status === 401) {
|
|
|
|
|
console.log(`[OBPClientService] 401 Unauthorized: ${url} (authentication required)`)
|
|
|
|
|
} else {
|
|
|
|
|
console.error('[OBPClientService] GET request failed:', response.status, errorText)
|
|
|
|
|
}
|
|
|
|
|
throw new OBPAPIError(response.status, errorText)
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-11 18:22:59 +00:00
|
|
|
const responseData = await response.json()
|
|
|
|
|
// Log count instead of full data to reduce log noise
|
|
|
|
|
if (
|
|
|
|
|
responseData &&
|
|
|
|
|
responseData.scanned_api_versions &&
|
|
|
|
|
Array.isArray(responseData.scanned_api_versions)
|
|
|
|
|
) {
|
|
|
|
|
console.log(
|
|
|
|
|
`OBPClientService: Response data: ${responseData.scanned_api_versions.length} scanned_api_versions`
|
|
|
|
|
)
|
|
|
|
|
} else {
|
|
|
|
|
console.log('OBPClientService: Response data received:', typeof responseData)
|
|
|
|
|
}
|
|
|
|
|
return responseData
|
2025-12-02 13:26:19 +00:00
|
|
|
}
|
|
|
|
|
|
2025-03-12 12:10:30 +00:00
|
|
|
/**
|
2025-12-01 10:28:40 +00:00
|
|
|
* Make a GET request with OAuth2 Bearer token authentication
|
|
|
|
|
*
|
|
|
|
|
* @param path - The API endpoint path (e.g., /obp/v5.1.0/banks)
|
|
|
|
|
* @param accessToken - OAuth2 access token
|
|
|
|
|
* @returns Response data from the API
|
2025-03-12 12:10:30 +00:00
|
|
|
*/
|
2025-12-01 10:28:40 +00:00
|
|
|
private async getWithBearer(path: string, accessToken: string): Promise<any> {
|
2025-12-02 13:26:19 +00:00
|
|
|
// Ensure proper slash handling between base URI and path
|
|
|
|
|
const normalizedPath = path.startsWith('/') ? path : `/${path}`
|
|
|
|
|
const url = `${this.clientConfig.baseUri}${normalizedPath}`
|
2025-12-01 10:28:40 +00:00
|
|
|
console.log('OBPClientService: GET request with Bearer token to:', url)
|
|
|
|
|
|
|
|
|
|
const response = await fetch(url, {
|
|
|
|
|
method: 'GET',
|
|
|
|
|
headers: {
|
|
|
|
|
Authorization: `Bearer ${accessToken}`,
|
|
|
|
|
'Content-Type': 'application/json'
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
const errorText = await response.text()
|
2025-12-02 13:26:19 +00:00
|
|
|
// 401 errors indicate token expiration or invalid token
|
|
|
|
|
if (response.status === 401) {
|
|
|
|
|
console.warn(
|
|
|
|
|
`[OBPClientService] 401 Unauthorized with Bearer token: ${url} (token may be expired)`
|
|
|
|
|
)
|
|
|
|
|
} else {
|
|
|
|
|
console.error(
|
|
|
|
|
'[OBPClientService] GET request with Bearer failed:',
|
|
|
|
|
response.status,
|
|
|
|
|
errorText
|
|
|
|
|
)
|
|
|
|
|
}
|
2025-12-02 01:08:16 +00:00
|
|
|
throw new OBPAPIError(response.status, errorText)
|
2025-03-12 12:10:30 +00:00
|
|
|
}
|
|
|
|
|
|
2025-12-01 10:28:40 +00:00
|
|
|
return await response.json()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Make a POST request with OAuth2 Bearer token authentication
|
|
|
|
|
*
|
|
|
|
|
* @param path - The API endpoint path
|
|
|
|
|
* @param body - Request body data
|
|
|
|
|
* @param accessToken - OAuth2 access token
|
|
|
|
|
* @returns Response data from the API
|
|
|
|
|
*/
|
|
|
|
|
private async createWithBearer(path: string, body: any, accessToken: string): Promise<any> {
|
2025-12-02 13:26:19 +00:00
|
|
|
// Ensure proper slash handling between base URI and path
|
|
|
|
|
const normalizedPath = path.startsWith('/') ? path : `/${path}`
|
|
|
|
|
const url = `${this.clientConfig.baseUri}${normalizedPath}`
|
2025-12-01 10:28:40 +00:00
|
|
|
console.log('OBPClientService: POST request with Bearer token to:', url)
|
|
|
|
|
|
|
|
|
|
const response = await fetch(url, {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: {
|
|
|
|
|
Authorization: `Bearer ${accessToken}`,
|
|
|
|
|
'Content-Type': 'application/json'
|
|
|
|
|
},
|
|
|
|
|
body: JSON.stringify(body)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
const errorText = await response.text()
|
2025-12-02 13:26:19 +00:00
|
|
|
if (response.status === 401) {
|
|
|
|
|
console.warn(`[OBPClientService] 401 Unauthorized on POST: ${url} (token may be expired)`)
|
|
|
|
|
} else {
|
|
|
|
|
console.error('[OBPClientService] POST request failed:', response.status, errorText)
|
|
|
|
|
}
|
2025-12-02 01:08:16 +00:00
|
|
|
throw new OBPAPIError(response.status, errorText)
|
2025-12-01 10:28:40 +00:00
|
|
|
}
|
2025-03-07 16:21:10 +00:00
|
|
|
|
2025-12-01 10:28:40 +00:00
|
|
|
return await response.json()
|
2025-03-07 16:21:10 +00:00
|
|
|
}
|
|
|
|
|
|
2025-12-01 10:28:40 +00:00
|
|
|
/**
|
|
|
|
|
* Make a PUT request with OAuth2 Bearer token authentication
|
|
|
|
|
*
|
|
|
|
|
* @param path - The API endpoint path
|
|
|
|
|
* @param body - Request body data
|
|
|
|
|
* @param accessToken - OAuth2 access token
|
|
|
|
|
* @returns Response data from the API
|
|
|
|
|
*/
|
|
|
|
|
private async updateWithBearer(path: string, body: any, accessToken: string): Promise<any> {
|
2025-12-02 13:26:19 +00:00
|
|
|
// Ensure proper slash handling between base URI and path
|
|
|
|
|
const normalizedPath = path.startsWith('/') ? path : `/${path}`
|
|
|
|
|
const url = `${this.clientConfig.baseUri}${normalizedPath}`
|
2025-12-01 10:28:40 +00:00
|
|
|
console.log('OBPClientService: PUT request with Bearer token to:', url)
|
2025-03-07 11:14:28 +00:00
|
|
|
|
2025-12-01 10:28:40 +00:00
|
|
|
const response = await fetch(url, {
|
|
|
|
|
method: 'PUT',
|
|
|
|
|
headers: {
|
|
|
|
|
Authorization: `Bearer ${accessToken}`,
|
|
|
|
|
'Content-Type': 'application/json'
|
|
|
|
|
},
|
|
|
|
|
body: JSON.stringify(body)
|
|
|
|
|
})
|
2025-03-07 11:14:28 +00:00
|
|
|
|
2025-12-01 10:28:40 +00:00
|
|
|
if (!response.ok) {
|
|
|
|
|
const errorText = await response.text()
|
2025-12-02 13:26:19 +00:00
|
|
|
if (response.status === 401) {
|
|
|
|
|
console.warn(`[OBPClientService] 401 Unauthorized on PUT: ${url} (token may be expired)`)
|
|
|
|
|
} else {
|
|
|
|
|
console.error('[OBPClientService] PUT request failed:', response.status, errorText)
|
|
|
|
|
}
|
2025-12-02 01:08:16 +00:00
|
|
|
throw new OBPAPIError(response.status, errorText)
|
2025-12-01 10:28:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return await response.json()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Make a DELETE request with OAuth2 Bearer token authentication
|
|
|
|
|
*
|
|
|
|
|
* @param path - The API endpoint path
|
|
|
|
|
* @param accessToken - OAuth2 access token
|
|
|
|
|
* @returns Response data from the API
|
|
|
|
|
*/
|
|
|
|
|
private async discardWithBearer(path: string, accessToken: string): Promise<any> {
|
2025-12-02 13:26:19 +00:00
|
|
|
// Ensure proper slash handling between base URI and path
|
|
|
|
|
const normalizedPath = path.startsWith('/') ? path : `/${path}`
|
|
|
|
|
const url = `${this.clientConfig.baseUri}${normalizedPath}`
|
2025-12-01 10:28:40 +00:00
|
|
|
console.log('OBPClientService: DELETE request with Bearer token to:', url)
|
|
|
|
|
|
|
|
|
|
const response = await fetch(url, {
|
|
|
|
|
method: 'DELETE',
|
2025-03-07 11:14:28 +00:00
|
|
|
headers: {
|
2025-12-01 10:28:40 +00:00
|
|
|
Authorization: `Bearer ${accessToken}`,
|
|
|
|
|
'Content-Type': 'application/json'
|
2025-03-07 11:14:28 +00:00
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
2025-12-01 10:28:40 +00:00
|
|
|
if (!response.ok) {
|
|
|
|
|
const errorText = await response.text()
|
2025-12-02 13:26:19 +00:00
|
|
|
if (response.status === 401) {
|
|
|
|
|
console.warn(`[OBPClientService] 401 Unauthorized on DELETE: ${url} (token may be expired)`)
|
|
|
|
|
} else {
|
|
|
|
|
console.error('[OBPClientService] DELETE request failed:', response.status, errorText)
|
|
|
|
|
}
|
2025-12-02 01:08:16 +00:00
|
|
|
throw new OBPAPIError(response.status, errorText)
|
2025-12-01 10:28:40 +00:00
|
|
|
}
|
2025-03-07 11:14:28 +00:00
|
|
|
|
2025-12-01 10:28:40 +00:00
|
|
|
return await response.json()
|
2025-03-07 11:14:28 +00:00
|
|
|
}
|
2023-05-03 17:17:01 +00:00
|
|
|
}
|