/* * Open Bank Project - API Explorer II * Copyright (C) 2023-2024, TESOBE GmbH * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * * Email: contact@tesobe.com * TESOBE GmbH * Osloerstrasse 16/17 * Berlin 13359, Germany * * This product includes software developed at * TESOBE (http://www.tesobe.com/) * */ import { Service } from 'typedi' import { DEFAULT_OBP_API_VERSION } from '../../src/shared-constants.js' // 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' } } // OAuth2 Bearer token configuration interface OAuth2Config { accessToken: string tokenType: string } // API Client configuration for OAuth2 interface APIClientConfig { baseUri: string version: string oauth2?: OAuth2Config } @Service() /** * OBPClientService provides methods for interacting with the Open Bank Project API. * * This service handles API communication with OBP using OAuth2 Bearer token authentication, * making HTTP requests (GET, POST, PUT, DELETE). * * @class OBPClientService * * @property {APIClientConfig} clientConfig - API client configuration * * @example * const obpService = new OBPClientService(); * const response = await obpService.get('/obp/v5.1.0/banks', sessionConfig); */ export default class OBPClientService { private clientConfig: APIClientConfig constructor() { if (!process.env.VITE_OBP_API_HOST) throw new Error('VITE_OBP_API_HOST is not set') // Always use v5.1.0 for application infrastructure - stable and debuggable this.clientConfig = { baseUri: process.env.VITE_OBP_API_HOST!, version: DEFAULT_OBP_API_VERSION } } async get(path: string, clientConfig: any): Promise { const config = this.getSessionConfig(clientConfig) // If no config or no access token, make unauthenticated request if (!config || !config.oauth2?.accessToken) { return await this.getWithoutAuth(path) } return await this.getWithBearer(path, config.oauth2.accessToken) } async create(path: string, body: any, clientConfig: any): Promise { const config = this.getSessionConfig(clientConfig) if (!config || !config.oauth2?.accessToken) { throw new Error('Authentication required for creating resources.') } return await this.createWithBearer(path, body, config.oauth2.accessToken) } async update(path: string, body: any, clientConfig: any): Promise { const config = this.getSessionConfig(clientConfig) if (!config || !config.oauth2?.accessToken) { throw new Error('Authentication required for updating resources.') } return await this.updateWithBearer(path, body, config.oauth2.accessToken) } async discard(path: string, clientConfig: any): Promise { const config = this.getSessionConfig(clientConfig) if (!config || !config.oauth2?.accessToken) { throw new Error('Authentication required for deleting resources.') } return await this.discardWithBearer(path, config.oauth2.accessToken) } private getSessionConfig(clientConfig: APIClientConfig): APIClientConfig { return clientConfig || this.clientConfig } getOBPVersion(): string { return this.clientConfig.version } getOBPClientConfig(): APIClientConfig { return this.clientConfig } /** * 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 { // 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) } 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 } /** * 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 */ private async getWithBearer(path: string, accessToken: string): Promise { // 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 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() // 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 ) } throw new OBPAPIError(response.status, errorText) } 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 { // Ensure proper slash handling between base URI and path const normalizedPath = path.startsWith('/') ? path : `/${path}` const url = `${this.clientConfig.baseUri}${normalizedPath}` 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() 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) } throw new OBPAPIError(response.status, errorText) } return await response.json() } /** * 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 { // Ensure proper slash handling between base URI and path const normalizedPath = path.startsWith('/') ? path : `/${path}` const url = `${this.clientConfig.baseUri}${normalizedPath}` console.log('OBPClientService: PUT request with Bearer token to:', url) const response = await fetch(url, { method: 'PUT', headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify(body) }) if (!response.ok) { const errorText = await response.text() 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) } throw new OBPAPIError(response.status, errorText) } 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 { // Ensure proper slash handling between base URI and path const normalizedPath = path.startsWith('/') ? path : `/${path}` const url = `${this.clientConfig.baseUri}${normalizedPath}` console.log('OBPClientService: DELETE request with Bearer token to:', url) const response = await fetch(url, { method: 'DELETE', headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json' } }) if (!response.ok) { const errorText = await response.text() 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) } throw new OBPAPIError(response.status, errorText) } return await response.json() } }