mirror of
https://github.com/OpenBankProject/API-Explorer-II.git
synced 2026-02-06 10:47:04 +00:00
OAuth2/OIDC
This commit is contained in:
parent
b97f39b4e1
commit
056171388f
1
.gitignore
vendored
1
.gitignore
vendored
@ -61,3 +61,4 @@ src/test/integration/playwright/.auth/
|
||||
test-results/
|
||||
playwright-report/
|
||||
playwright-coverage/
|
||||
shared-constants.js
|
||||
|
||||
38
.zed/settings.json
Normal file
38
.zed/settings.json
Normal file
@ -0,0 +1,38 @@
|
||||
{
|
||||
"theme": {
|
||||
"mode": "dark",
|
||||
"dark": "API-Explorer-II-Green",
|
||||
"light": "API-Explorer-II-Green"
|
||||
},
|
||||
"ui_font_size": 14,
|
||||
"buffer_font_size": 14,
|
||||
"terminal": {
|
||||
"font_size": 13,
|
||||
"line_height": "comfortable"
|
||||
},
|
||||
"project_panel": {
|
||||
"button": true,
|
||||
"default_width": 300,
|
||||
"dock": "left"
|
||||
},
|
||||
"tab_bar": {
|
||||
"show": true
|
||||
},
|
||||
"status_bar": {
|
||||
"show": true
|
||||
},
|
||||
"toolbar": {
|
||||
"breadcrumbs": true,
|
||||
"quick_actions": true
|
||||
},
|
||||
"workspace": {
|
||||
"save_on_focus_change": true
|
||||
},
|
||||
"git": {
|
||||
"enabled": true,
|
||||
"autoFetch": true
|
||||
},
|
||||
"format_on_save": "on",
|
||||
"show_whitespaces": "selection",
|
||||
"soft_wrap": "prefer_line"
|
||||
}
|
||||
8
.zed/tasks.json
Normal file
8
.zed/tasks.json
Normal file
@ -0,0 +1,8 @@
|
||||
[
|
||||
{
|
||||
"label": "🟢 Open Terminal Here",
|
||||
"command": "gnome-terminal",
|
||||
"args": ["--title=\"🟢 API-Explorer\"", "--working-directory=${workspaceFolder}"],
|
||||
"reveal": "never"
|
||||
}
|
||||
]
|
||||
28
.zed/theme.json
Normal file
28
.zed/theme.json
Normal file
@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "API-Explorer-II-Green",
|
||||
"author": "Portal/Opey Development Setup",
|
||||
"themes": [
|
||||
{
|
||||
"name": "API-Explorer-II-Green",
|
||||
"appearance": "dark",
|
||||
"style": {
|
||||
"background": "#1e1e1e",
|
||||
"foreground": "#d4d4d4",
|
||||
"cursor": "#28A745",
|
||||
"selection": "#28A74533",
|
||||
"selected_line_background": "#28A74511",
|
||||
"line_number": "#858585",
|
||||
"active_line_number": "#28A745",
|
||||
"find_highlight": "#28A745",
|
||||
"border": "#28A74566",
|
||||
"panel_background": "#252526",
|
||||
"panel_focused_border": "#28A745",
|
||||
"tab_bar_background": "#2d2d30",
|
||||
"tab_active_background": "#28A74588",
|
||||
"status_bar_background": "#28A74566",
|
||||
"title_bar_background": "#28A74544",
|
||||
"toolbar_background": "#37373d"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
988
CONVERT_TO_SVELTE.md
Normal file
988
CONVERT_TO_SVELTE.md
Normal file
@ -0,0 +1,988 @@
|
||||
# Converting API Explorer II to Svelte
|
||||
|
||||
**Document Version:** 1.0
|
||||
**Date:** December 2024
|
||||
**Author:** Development Team
|
||||
**Status:** Exploration / Proposal
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
This document explores the feasibility, benefits, challenges, and strategy for converting API Explorer II from Vue 3 to Svelte, aligning with the OBP-Portal architecture. The analysis covers technical considerations, migration strategies, effort estimation, and recommendations.
|
||||
|
||||
**Key Findings:**
|
||||
- Svelte offers significant performance and bundle size improvements
|
||||
- Migration is feasible but requires substantial effort (estimated 4-8 weeks)
|
||||
- Alignment with OBP-Portal would improve maintainability across projects
|
||||
- Backend (Express + TypeScript) remains unchanged
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Current Architecture](#current-architecture)
|
||||
2. [Why Consider Svelte?](#why-consider-svelte)
|
||||
3. [Svelte vs Vue 3 Comparison](#svelte-vs-vue-3-comparison)
|
||||
4. [OBP-Portal Architecture Reference](#obp-portal-architecture-reference)
|
||||
5. [Migration Strategy](#migration-strategy)
|
||||
6. [Technical Considerations](#technical-considerations)
|
||||
7. [Effort Estimation](#effort-estimation)
|
||||
8. [Risks and Challenges](#risks-and-challenges)
|
||||
9. [Benefits Analysis](#benefits-analysis)
|
||||
10. [Recommendations](#recommendations)
|
||||
11. [Appendix](#appendix)
|
||||
|
||||
---
|
||||
|
||||
## Current Architecture
|
||||
|
||||
### Frontend Stack (Vue 3)
|
||||
|
||||
```
|
||||
API Explorer II (Current)
|
||||
├── Vue 3.4+ (Composition API)
|
||||
├── TypeScript 5+
|
||||
├── Vite 5+ (Build tool)
|
||||
├── Element Plus (UI Components)
|
||||
├── Vue Router
|
||||
├── Pinia (State Management)
|
||||
├── Highlight.js (Code highlighting)
|
||||
└── i18n (Internationalization)
|
||||
```
|
||||
|
||||
### Backend Stack (Unchanged)
|
||||
|
||||
```
|
||||
Express + TypeScript
|
||||
├── routing-controllers
|
||||
├── TypeDI (Dependency Injection)
|
||||
├── Express Session
|
||||
├── OAuth2 (Arctic library)
|
||||
├── Redis (Session storage)
|
||||
└── Custom OBP API Client
|
||||
```
|
||||
|
||||
### Current File Structure
|
||||
|
||||
```
|
||||
src/
|
||||
├── components/ # Vue SFCs
|
||||
│ ├── Content.vue
|
||||
│ ├── Menu.vue
|
||||
│ ├── Preview.vue
|
||||
│ └── SearchNav.vue
|
||||
├── obp/ # API logic
|
||||
│ ├── index.ts
|
||||
│ ├── resource-docs.ts
|
||||
│ ├── message-docs.ts
|
||||
│ └── api-version.ts
|
||||
├── views/ # Route views
|
||||
├── router/ # Vue Router config
|
||||
├── stores/ # Pinia stores
|
||||
└── main.ts # App entry
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Why Consider Svelte?
|
||||
|
||||
### 1. **Alignment with OBP-Portal**
|
||||
|
||||
The OBP-Portal project already uses Svelte, creating an opportunity for:
|
||||
- **Shared knowledge**: Developers can work across both projects seamlessly
|
||||
- **Reusable components**: Common UI components can be shared
|
||||
- **Consistent patterns**: Unified architecture across OBP ecosystem
|
||||
- **Reduced cognitive load**: One framework to master instead of two
|
||||
|
||||
### 2. **Performance Benefits**
|
||||
|
||||
Svelte compiles to vanilla JavaScript, resulting in:
|
||||
- **Smaller bundle sizes**: No runtime framework overhead
|
||||
- **Faster execution**: No virtual DOM diffing
|
||||
- **Better TTI** (Time to Interactive): Faster initial load
|
||||
- **Reduced memory footprint**: Less JavaScript to parse and execute
|
||||
|
||||
### 3. **Developer Experience**
|
||||
|
||||
Svelte offers:
|
||||
- **Less boilerplate**: No `ref()`, `reactive()`, or `computed()`
|
||||
- **Intuitive reactivity**: Variables are reactive by default
|
||||
- **Cleaner syntax**: Less ceremony, more readable code
|
||||
- **Built-in animations**: No additional libraries needed
|
||||
- **Scoped styles**: CSS scoped by default without configuration
|
||||
|
||||
### 4. **Modern Tooling**
|
||||
|
||||
- **SvelteKit**: Full-stack framework with SSR/SSG capabilities
|
||||
- **Vite native support**: Already using Vite, smooth transition
|
||||
- **TypeScript support**: First-class TypeScript integration
|
||||
- **Testing**: Vitest works seamlessly with Svelte
|
||||
|
||||
---
|
||||
|
||||
## Svelte vs Vue 3 Comparison
|
||||
|
||||
### Code Comparison
|
||||
|
||||
#### Vue 3 Component (Current)
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div class="api-endpoint">
|
||||
<h3>{{ title }}</h3>
|
||||
<button @click="fetchData">{{ loading ? 'Loading...' : 'Fetch' }}</button>
|
||||
<div v-if="error" class="error">{{ error }}</div>
|
||||
<ul v-else>
|
||||
<li v-for="item in items" :key="item.id">{{ item.name }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { get } from '../obp'
|
||||
|
||||
const props = defineProps<{ endpoint: string }>()
|
||||
const emit = defineEmits<{ success: [data: any] }>()
|
||||
|
||||
const items = ref<any[]>([])
|
||||
const loading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
const title = computed(() => props.endpoint.toUpperCase())
|
||||
|
||||
async function fetchData() {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const response = await get(props.endpoint)
|
||||
items.value = response.data
|
||||
emit('success', response)
|
||||
} catch (err: any) {
|
||||
error.value = err.message
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.api-endpoint { padding: 1rem; }
|
||||
.error { color: red; }
|
||||
</style>
|
||||
```
|
||||
|
||||
#### Svelte Component (Converted)
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte'
|
||||
import { get } from '../obp'
|
||||
|
||||
export let endpoint: string
|
||||
|
||||
let items: any[] = []
|
||||
let loading = false
|
||||
let error: string | null = null
|
||||
|
||||
// Reactive statement - automatically computed
|
||||
$: title = endpoint.toUpperCase()
|
||||
|
||||
async function fetchData() {
|
||||
loading = true
|
||||
error = null
|
||||
try {
|
||||
const response = await get(endpoint)
|
||||
items = response.data
|
||||
dispatch('success', response)
|
||||
} catch (err: any) {
|
||||
error = err.message
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
fetchData()
|
||||
})
|
||||
|
||||
// Event dispatcher
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
const dispatch = createEventDispatcher()
|
||||
</script>
|
||||
|
||||
<div class="api-endpoint">
|
||||
<h3>{title}</h3>
|
||||
<button on:click={fetchData}>
|
||||
{loading ? 'Loading...' : 'Fetch'}
|
||||
</button>
|
||||
|
||||
{#if error}
|
||||
<div class="error">{error}</div>
|
||||
{:else}
|
||||
<ul>
|
||||
{#each items as item (item.id)}
|
||||
<li>{item.name}</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.api-endpoint { padding: 1rem; }
|
||||
.error { color: red; }
|
||||
</style>
|
||||
```
|
||||
|
||||
### Key Differences
|
||||
|
||||
| Aspect | Vue 3 | Svelte |
|
||||
|--------|-------|--------|
|
||||
| **Reactivity** | `ref()`, `reactive()` | Variables are reactive |
|
||||
| **Computed** | `computed()` function | `$:` reactive statements |
|
||||
| **Props** | `defineProps<T>()` | `export let prop` |
|
||||
| **Events** | `defineEmits<T>()` | `createEventDispatcher()` |
|
||||
| **Lifecycle** | `onMounted()`, `onUnmounted()` | `onMount()`, `onDestroy()` |
|
||||
| **Conditionals** | `v-if`, `v-else` | `{#if}`, `{:else}`, `{/if}` |
|
||||
| **Loops** | `v-for` | `{#each}`, `{/each}` |
|
||||
| **Bundle Size** | ~34 KB (runtime) | ~2 KB (compiler output) |
|
||||
| **Performance** | Virtual DOM | Direct DOM manipulation |
|
||||
|
||||
---
|
||||
|
||||
## OBP-Portal Architecture Reference
|
||||
|
||||
### OBP-Portal Stack
|
||||
|
||||
```
|
||||
OBP-Portal
|
||||
├── SvelteKit 2.x
|
||||
├── TypeScript
|
||||
├── Vite
|
||||
├── Tailwind CSS
|
||||
├── Svelte Stores (State)
|
||||
├── Svelte Router (built-in)
|
||||
└── OAuth2 integration
|
||||
```
|
||||
|
||||
### Lessons from OBP-Portal
|
||||
|
||||
**What Works Well:**
|
||||
1. **SvelteKit's file-based routing** - Intuitive and maintainable
|
||||
2. **Svelte stores** - Simple, effective state management
|
||||
3. **TypeScript integration** - Smooth developer experience
|
||||
4. **Build performance** - Fast development and production builds
|
||||
5. **Component reusability** - Easy to create shared components
|
||||
|
||||
**Challenges Encountered:**
|
||||
1. **SSR complexity** - Server-side rendering requires careful handling
|
||||
2. **Third-party libraries** - Some Vue/React libraries need Svelte alternatives
|
||||
3. **Learning curve** - Team needs time to adapt to Svelte paradigms
|
||||
4. **Ecosystem** - Smaller ecosystem compared to Vue/React
|
||||
|
||||
### Reusable Patterns from OBP-Portal
|
||||
|
||||
```typescript
|
||||
// Shared authentication store pattern
|
||||
// stores/auth.ts
|
||||
import { writable } from 'svelte/store'
|
||||
|
||||
export const user = writable<User | null>(null)
|
||||
export const isAuthenticated = writable(false)
|
||||
|
||||
// Shared API client pattern
|
||||
// lib/api.ts
|
||||
export async function obpGet(path: string) {
|
||||
const response = await fetch(`/api/get?path=${encodeURIComponent(path)}`)
|
||||
return response.json()
|
||||
}
|
||||
|
||||
// Shared component pattern
|
||||
// components/OBPButton.svelte
|
||||
<script lang="ts">
|
||||
export let variant: 'primary' | 'secondary' = 'primary'
|
||||
export let disabled = false
|
||||
</script>
|
||||
|
||||
<button class="obp-btn {variant}" {disabled} on:click>
|
||||
<slot />
|
||||
</button>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Migration Strategy
|
||||
|
||||
### Approach: Incremental Migration
|
||||
|
||||
**Phase 1: Preparation (1 week)**
|
||||
- [ ] Set up parallel Svelte build configuration
|
||||
- [ ] Create proof-of-concept with 1-2 simple components
|
||||
- [ ] Establish component migration patterns
|
||||
- [ ] Set up testing infrastructure for Svelte
|
||||
- [ ] Document migration guidelines
|
||||
|
||||
**Phase 2: Core Infrastructure (2 weeks)**
|
||||
- [ ] Migrate routing (Vue Router → SvelteKit routing)
|
||||
- [ ] Migrate state management (Pinia → Svelte stores)
|
||||
- [ ] Migrate API client layer (minimal changes)
|
||||
- [ ] Set up i18n for Svelte
|
||||
- [ ] Create Svelte equivalents of utility functions
|
||||
|
||||
**Phase 3: Component Migration (3-4 weeks)**
|
||||
- [ ] Migrate simple components first (buttons, cards, etc.)
|
||||
- [ ] Migrate medium complexity components (forms, tables)
|
||||
- [ ] Migrate complex components (SearchNav, Content, Preview)
|
||||
- [ ] Replace Element Plus with Svelte alternatives
|
||||
- Option A: Carbon Components Svelte
|
||||
- Option B: Svelte Material UI
|
||||
- Option C: Build custom components
|
||||
|
||||
**Phase 4: Integration & Testing (1-2 weeks)**
|
||||
- [ ] End-to-end testing
|
||||
- [ ] Performance benchmarking
|
||||
- [ ] Accessibility audit
|
||||
- [ ] Browser compatibility testing
|
||||
- [ ] Documentation updates
|
||||
|
||||
**Phase 5: Deployment**
|
||||
- [ ] Staged rollout
|
||||
- [ ] Monitor for issues
|
||||
- [ ] Gather user feedback
|
||||
- [ ] Performance monitoring
|
||||
|
||||
### Alternative Approach: Big Bang Rewrite
|
||||
|
||||
**Pros:**
|
||||
- Clean slate, no legacy code
|
||||
- Faster if done right
|
||||
- No Vue/Svelte coexistence issues
|
||||
|
||||
**Cons:**
|
||||
- Higher risk
|
||||
- Longer time before visible progress
|
||||
- More difficult to test incrementally
|
||||
- Team blocked from feature development
|
||||
|
||||
**Recommendation:** **Incremental migration** is safer and allows parallel development.
|
||||
|
||||
---
|
||||
|
||||
## Technical Considerations
|
||||
|
||||
### 1. UI Component Library Replacement
|
||||
|
||||
**Current:** Element Plus (Vue)
|
||||
**Options:**
|
||||
|
||||
| Library | Pros | Cons |
|
||||
|---------|------|------|
|
||||
| **Carbon Components Svelte** | Enterprise-grade, accessible | Large bundle, IBM-specific design |
|
||||
| **Svelte Material UI** | Material Design, comprehensive | Material design may not fit brand |
|
||||
| **Attraction** | Lightweight, modern | Less mature, smaller community |
|
||||
| **Custom Components** | Full control, aligned with brand | Most effort, maintenance burden |
|
||||
|
||||
**Recommendation:** Start with **Svelte Material UI** or **Carbon Components**, create custom components where needed.
|
||||
|
||||
### 2. State Management Migration
|
||||
|
||||
**Current:** Pinia
|
||||
**Target:** Svelte Stores
|
||||
|
||||
```typescript
|
||||
// Before (Pinia)
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useUserStore = defineStore('user', {
|
||||
state: () => ({ user: null, authenticated: false }),
|
||||
actions: {
|
||||
setUser(user) { this.user = user }
|
||||
}
|
||||
})
|
||||
|
||||
// After (Svelte Store)
|
||||
import { writable } from 'svelte/store'
|
||||
|
||||
function createUserStore() {
|
||||
const { subscribe, set, update } = writable({ user: null, authenticated: false })
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
setUser: (user) => update(state => ({ ...state, user })),
|
||||
reset: () => set({ user: null, authenticated: false })
|
||||
}
|
||||
}
|
||||
|
||||
export const userStore = createUserStore()
|
||||
```
|
||||
|
||||
### 3. Routing Migration
|
||||
|
||||
**Current:** Vue Router
|
||||
**Target:** SvelteKit file-based routing
|
||||
|
||||
```
|
||||
# Before (Vue Router)
|
||||
src/router/index.ts → defines routes manually
|
||||
|
||||
# After (SvelteKit)
|
||||
src/routes/
|
||||
├── +page.svelte # /
|
||||
├── +layout.svelte # Root layout
|
||||
├── api/
|
||||
│ └── [operation]/
|
||||
│ └── +page.svelte # /api/:operation
|
||||
└── glossary/
|
||||
└── +page.svelte # /glossary
|
||||
```
|
||||
|
||||
### 4. API Client Layer
|
||||
|
||||
**Good News:** Minimal changes required!
|
||||
|
||||
```typescript
|
||||
// src/obp/index.ts - mostly unchanged
|
||||
export async function get(path: string): Promise<any> {
|
||||
const response = await fetch(`/api/get?path=${encodeURIComponent(path)}`)
|
||||
if (!response.ok) throw new Error('Request failed')
|
||||
return response.json()
|
||||
}
|
||||
|
||||
// Usage in Svelte component
|
||||
<script lang="ts">
|
||||
import { get } from '$lib/obp'
|
||||
|
||||
let data = $state(null)
|
||||
|
||||
async function loadData() {
|
||||
data = await get('/obp/v5.1.0/banks')
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
### 5. Backend Compatibility
|
||||
|
||||
**Zero changes required!** The Express backend remains identical:
|
||||
|
||||
```
|
||||
✅ OAuth2 authentication - unchanged
|
||||
✅ Session management - unchanged
|
||||
✅ API proxy endpoints - unchanged
|
||||
✅ Request controllers - unchanged
|
||||
✅ TypeDI services - unchanged
|
||||
```
|
||||
|
||||
### 6. Internationalization (i18n)
|
||||
|
||||
**Current:** vue-i18n
|
||||
**Target:** svelte-i18n
|
||||
|
||||
```typescript
|
||||
// Before (vue-i18n)
|
||||
import { useI18n } from 'vue-i18n'
|
||||
const { t } = useI18n()
|
||||
|
||||
// After (svelte-i18n)
|
||||
import { _, locale } from 'svelte-i18n'
|
||||
|
||||
<p>{$_('welcome.message')}</p>
|
||||
<button on:click={() => $locale = 'es'}>Español</button>
|
||||
```
|
||||
|
||||
### 7. Code Highlighting
|
||||
|
||||
**Current:** Highlight.js with Vue plugin
|
||||
**Target:** Highlight.js with Svelte action
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
import hljs from 'highlight.js'
|
||||
|
||||
function highlight(node: HTMLElement) {
|
||||
hljs.highlightElement(node)
|
||||
return {
|
||||
destroy() { /* cleanup if needed */ }
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<pre><code use:highlight class="language-json">{code}</code></pre>
|
||||
```
|
||||
|
||||
### 8. Build Configuration
|
||||
|
||||
**Current:** Vite + Vue plugin
|
||||
**Target:** Vite + Svelte plugin
|
||||
|
||||
```javascript
|
||||
// vite.config.js
|
||||
import { defineConfig } from 'vite'
|
||||
import { svelte } from '@sveltejs/vite-plugin-svelte'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [svelte()],
|
||||
// Most config remains the same!
|
||||
server: {
|
||||
proxy: {
|
||||
'/api': 'http://localhost:3000'
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Effort Estimation
|
||||
|
||||
### Time Estimates (Conservative)
|
||||
|
||||
| Phase | Duration | Team Size | Total Effort |
|
||||
|-------|----------|-----------|--------------|
|
||||
| Preparation | 1 week | 1 dev | 1 week |
|
||||
| Core Infrastructure | 2 weeks | 2 devs | 4 weeks |
|
||||
| Component Migration | 4 weeks | 2-3 devs | 8-12 weeks |
|
||||
| Testing & Integration | 2 weeks | 2 devs | 4 weeks |
|
||||
| **Total** | **9 weeks** | **2-3 devs** | **17-21 weeks** |
|
||||
|
||||
### Complexity Breakdown
|
||||
|
||||
**Low Complexity (1-2 days each):**
|
||||
- Simple display components
|
||||
- Buttons, cards, badges
|
||||
- Layout components
|
||||
|
||||
**Medium Complexity (3-5 days each):**
|
||||
- Form components
|
||||
- Tables with sorting/filtering
|
||||
- Modal dialogs
|
||||
- Navigation components
|
||||
|
||||
**High Complexity (1-2 weeks each):**
|
||||
- SearchNav component (API version selector, filters)
|
||||
- Content component (API endpoint details, dynamic forms)
|
||||
- Preview component (Try it out functionality, response display)
|
||||
- Resource docs caching system
|
||||
|
||||
### Risk Buffer
|
||||
|
||||
Add **25% contingency** for:
|
||||
- Unexpected technical challenges
|
||||
- Learning curve
|
||||
- Testing and bug fixes
|
||||
- Documentation
|
||||
|
||||
**Adjusted Total: 12-14 weeks** (3-3.5 months)
|
||||
|
||||
---
|
||||
|
||||
## Risks and Challenges
|
||||
|
||||
### Technical Risks
|
||||
|
||||
1. **Third-Party Library Compatibility**
|
||||
- **Risk:** Some libraries may not have Svelte equivalents
|
||||
- **Mitigation:** Research alternatives early, build custom if needed
|
||||
- **Impact:** Medium
|
||||
|
||||
2. **Element Plus Replacement**
|
||||
- **Risk:** Feature parity with Element Plus components
|
||||
- **Mitigation:** Incremental replacement, custom components for gaps
|
||||
- **Impact:** High
|
||||
|
||||
3. **SSR/Hydration Issues**
|
||||
- **Risk:** SvelteKit SSR may cause issues with OAuth flow
|
||||
- **Mitigation:** Use client-side only mode initially, add SSR later
|
||||
- **Impact:** Low
|
||||
|
||||
4. **Performance Regressions**
|
||||
- **Risk:** Improper migration could reduce performance
|
||||
- **Mitigation:** Continuous benchmarking, performance testing
|
||||
- **Impact:** Low
|
||||
|
||||
### Organizational Risks
|
||||
|
||||
1. **Team Learning Curve**
|
||||
- **Risk:** Developers unfamiliar with Svelte
|
||||
- **Mitigation:** Training sessions, pair programming, good documentation
|
||||
- **Impact:** Medium
|
||||
|
||||
2. **Feature Development Freeze**
|
||||
- **Risk:** No new features during migration
|
||||
- **Mitigation:** Incremental migration allows parallel development
|
||||
- **Impact:** Medium
|
||||
|
||||
3. **User-Facing Bugs**
|
||||
- **Risk:** Migration introduces regressions
|
||||
- **Mitigation:** Comprehensive testing, staged rollout, quick rollback plan
|
||||
- **Impact:** High
|
||||
|
||||
4. **Maintenance Burden**
|
||||
- **Risk:** Supporting two frameworks during migration
|
||||
- **Mitigation:** Clear migration plan, time-boxed phases
|
||||
- **Impact:** Medium
|
||||
|
||||
### Mitigation Strategies
|
||||
|
||||
```markdown
|
||||
## Risk Mitigation Plan
|
||||
|
||||
### Before Migration
|
||||
- [ ] Complete prototype with 3-5 key components
|
||||
- [ ] Performance benchmark current Vue version
|
||||
- [ ] Document all critical user flows
|
||||
- [ ] Create comprehensive test suite
|
||||
- [ ] Establish rollback procedures
|
||||
|
||||
### During Migration
|
||||
- [ ] Daily progress tracking
|
||||
- [ ] Weekly performance testing
|
||||
- [ ] Continuous integration testing
|
||||
- [ ] Stakeholder communication
|
||||
- [ ] Feature freeze communication
|
||||
|
||||
### After Migration
|
||||
- [ ] Monitor error rates
|
||||
- [ ] Track performance metrics
|
||||
- [ ] Gather user feedback
|
||||
- [ ] Quick patch releases for bugs
|
||||
- [ ] Post-mortem analysis
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Benefits Analysis
|
||||
|
||||
### Quantitative Benefits
|
||||
|
||||
| Metric | Vue 3 (Current) | Svelte (Expected) | Improvement |
|
||||
|--------|-----------------|-------------------|-------------|
|
||||
| **Bundle Size** | ~450 KB | ~200 KB | 55% smaller |
|
||||
| **Initial Load** | 1.2s | 0.7s | 42% faster |
|
||||
| **TTI** | 2.5s | 1.5s | 40% faster |
|
||||
| **Memory Usage** | 45 MB | 28 MB | 38% less |
|
||||
| **Build Time** | 8s | 5s | 37% faster |
|
||||
|
||||
*Note: These are estimated improvements based on typical Vue-to-Svelte migrations.*
|
||||
|
||||
### Qualitative Benefits
|
||||
|
||||
**For Developers:**
|
||||
- ✅ Simpler, more readable code
|
||||
- ✅ Less boilerplate
|
||||
- ✅ Faster development iteration
|
||||
- ✅ Unified stack with OBP-Portal
|
||||
- ✅ Easier onboarding for new developers
|
||||
|
||||
**For Users:**
|
||||
- ✅ Faster page loads
|
||||
- ✅ Smoother interactions
|
||||
- ✅ Better mobile performance
|
||||
- ✅ Reduced data usage
|
||||
- ✅ More responsive UI
|
||||
|
||||
**For Organization:**
|
||||
- ✅ Consistent technology stack
|
||||
- ✅ Shared components across projects
|
||||
- ✅ Easier knowledge transfer
|
||||
- ✅ Reduced maintenance overhead
|
||||
- ✅ Modern, future-proof architecture
|
||||
|
||||
### Cost-Benefit Analysis
|
||||
|
||||
**Costs:**
|
||||
- 3-4 months of development effort
|
||||
- Learning curve for team
|
||||
- Temporary feature freeze
|
||||
- Testing and QA effort
|
||||
|
||||
**Benefits:**
|
||||
- Long-term maintainability
|
||||
- Performance improvements
|
||||
- Stack alignment
|
||||
- Developer productivity gains
|
||||
- User experience improvements
|
||||
|
||||
**Break-Even Point:** Estimated 6-9 months after migration completion
|
||||
|
||||
---
|
||||
|
||||
## Recommendations
|
||||
|
||||
### Option 1: Full Migration (Recommended for Long-Term)
|
||||
|
||||
**When:**
|
||||
- Planning major feature updates
|
||||
- Have 3-4 months available
|
||||
- Want full alignment with OBP-Portal
|
||||
|
||||
**Pros:**
|
||||
- Complete modernization
|
||||
- Maximum long-term benefits
|
||||
- Clean, maintainable codebase
|
||||
|
||||
**Cons:**
|
||||
- Significant upfront effort
|
||||
- Temporary feature freeze
|
||||
- Higher risk
|
||||
|
||||
### Option 2: Hybrid Approach
|
||||
|
||||
**When:**
|
||||
- Need to maintain feature velocity
|
||||
- Limited developer resources
|
||||
- Risk-averse environment
|
||||
|
||||
**Strategy:**
|
||||
- Keep Vue for existing features
|
||||
- Build new features in Svelte
|
||||
- Gradual replacement over time
|
||||
|
||||
**Pros:**
|
||||
- Lower risk
|
||||
- Continuous feature development
|
||||
- Flexible timeline
|
||||
|
||||
**Cons:**
|
||||
- Maintenance complexity
|
||||
- Longer transition period
|
||||
- Two frameworks to support
|
||||
|
||||
### Option 3: Stay with Vue 3
|
||||
|
||||
**When:**
|
||||
- No strong business case for change
|
||||
- Limited resources
|
||||
- Vue 3 performance is sufficient
|
||||
|
||||
**Pros:**
|
||||
- No migration effort
|
||||
- Stable, known technology
|
||||
- Focus on features
|
||||
|
||||
**Cons:**
|
||||
- Stack fragmentation with OBP-Portal
|
||||
- Miss performance benefits
|
||||
- No shared components
|
||||
|
||||
### Final Recommendation
|
||||
|
||||
**Proceed with Option 1 (Full Migration) if:**
|
||||
1. ✅ OBP-Portal is successful and stable with Svelte
|
||||
2. ✅ Team has bandwidth for 3-4 month project
|
||||
3. ✅ Performance improvements are valuable
|
||||
4. ✅ Stack alignment is a priority
|
||||
|
||||
**Timeline Recommendation:**
|
||||
- **Q1 2025:** Planning and preparation
|
||||
- **Q2 2025:** Core infrastructure and component migration
|
||||
- **Q3 2025:** Testing, deployment, stabilization
|
||||
|
||||
---
|
||||
|
||||
## Appendix
|
||||
|
||||
### A. Component Inventory
|
||||
|
||||
Current Vue components requiring migration:
|
||||
|
||||
```
|
||||
src/components/
|
||||
├── Content.vue [HIGH COMPLEXITY] - 500+ lines, dynamic forms
|
||||
├── Menu.vue [MEDIUM] - Navigation, state management
|
||||
├── Preview.vue [HIGH COMPLEXITY] - API testing, response handling
|
||||
├── SearchNav.vue [HIGH COMPLEXITY] - Search, filters, collections
|
||||
├── Footer.vue [LOW] - Simple layout
|
||||
├── Header.vue [MEDIUM] - Auth status, user menu
|
||||
└── ... (15 more components)
|
||||
```
|
||||
|
||||
### B. Svelte Learning Resources
|
||||
|
||||
**Official Documentation:**
|
||||
- Svelte Tutorial: https://svelte.dev/tutorial
|
||||
- SvelteKit Docs: https://kit.svelte.dev/docs
|
||||
- Svelte Society: https://sveltesociety.dev/
|
||||
|
||||
**Migration Guides:**
|
||||
- Vue to Svelte: https://svelte.dev/blog/frameworks-without-the-framework
|
||||
- Component Patterns: https://svelte.dev/repl
|
||||
|
||||
**Video Tutorials:**
|
||||
- Svelte Crash Course (YouTube)
|
||||
- SvelteKit Full Tutorial
|
||||
- Svelte State Management
|
||||
|
||||
### C. Component Library Comparison
|
||||
|
||||
| Feature | Element Plus | Carbon Svelte | Svelte Material UI |
|
||||
|---------|--------------|---------------|-------------------|
|
||||
| Buttons | ✅ | ✅ | ✅ |
|
||||
| Forms | ✅ | ✅ | ✅ |
|
||||
| Tables | ✅ | ✅ | ✅ |
|
||||
| Modals | ✅ | ✅ | ✅ |
|
||||
| Notifications | ✅ | ✅ | ✅ |
|
||||
| Date Pickers | ✅ | ✅ | ✅ |
|
||||
| Trees | ✅ | ✅ | ❌ |
|
||||
| Bundle Size | Large | Large | Medium |
|
||||
| TypeScript | ✅ | ✅ | ✅ |
|
||||
| Accessibility | Good | Excellent | Good |
|
||||
|
||||
### D. Performance Benchmarks
|
||||
|
||||
```bash
|
||||
# Test setup
|
||||
npm run build
|
||||
npm run lighthouse
|
||||
|
||||
# Results (estimated)
|
||||
Vue 3 (Current):
|
||||
- First Contentful Paint: 1.2s
|
||||
- Largest Contentful Paint: 2.5s
|
||||
- Total Blocking Time: 180ms
|
||||
- Cumulative Layout Shift: 0.05
|
||||
- Speed Index: 2.3s
|
||||
|
||||
Svelte (Expected):
|
||||
- First Contentful Paint: 0.7s
|
||||
- Largest Contentful Paint: 1.5s
|
||||
- Total Blocking Time: 80ms
|
||||
- Cumulative Layout Shift: 0.02
|
||||
- Speed Index: 1.4s
|
||||
```
|
||||
|
||||
### E. Migration Checklist Template
|
||||
|
||||
```markdown
|
||||
## Component Migration Checklist
|
||||
|
||||
### Pre-Migration
|
||||
- [ ] Document component API (props, events, slots)
|
||||
- [ ] Identify dependencies
|
||||
- [ ] Write unit tests (if missing)
|
||||
- [ ] Screenshot current behavior
|
||||
- [ ] Note any quirks or edge cases
|
||||
|
||||
### Migration
|
||||
- [ ] Create new Svelte component file
|
||||
- [ ] Convert template syntax
|
||||
- [ ] Convert script logic
|
||||
- [ ] Convert styles
|
||||
- [ ] Update imports/exports
|
||||
- [ ] Test in isolation
|
||||
|
||||
### Post-Migration
|
||||
- [ ] Verify visual appearance matches
|
||||
- [ ] Test all interactions
|
||||
- [ ] Check accessibility
|
||||
- [ ] Performance test
|
||||
- [ ] Update documentation
|
||||
- [ ] Code review
|
||||
|
||||
### Integration
|
||||
- [ ] Update parent component imports
|
||||
- [ ] Test in full application
|
||||
- [ ] Cross-browser testing
|
||||
- [ ] Deploy to staging
|
||||
- [ ] User acceptance testing
|
||||
```
|
||||
|
||||
### F. Code Conversion Patterns
|
||||
|
||||
#### Pattern 1: Reactive State
|
||||
|
||||
```javascript
|
||||
// Vue 3
|
||||
const count = ref(0)
|
||||
const doubled = computed(() => count.value * 2)
|
||||
|
||||
// Svelte
|
||||
let count = 0
|
||||
$: doubled = count * 2
|
||||
```
|
||||
|
||||
#### Pattern 2: Props and Events
|
||||
|
||||
```javascript
|
||||
// Vue 3
|
||||
const props = defineProps<{ title: string }>()
|
||||
const emit = defineEmits<{ close: [] }>()
|
||||
|
||||
// Svelte
|
||||
export let title: string
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
const dispatch = createEventDispatcher()
|
||||
dispatch('close')
|
||||
```
|
||||
|
||||
#### Pattern 3: Lifecycle
|
||||
|
||||
```javascript
|
||||
// Vue 3
|
||||
onMounted(() => console.log('mounted'))
|
||||
onUnmounted(() => console.log('cleanup'))
|
||||
|
||||
// Svelte
|
||||
onMount(() => {
|
||||
console.log('mounted')
|
||||
return () => console.log('cleanup')
|
||||
})
|
||||
```
|
||||
|
||||
#### Pattern 4: Conditional Rendering
|
||||
|
||||
```html
|
||||
<!-- Vue 3 -->
|
||||
<div v-if="condition">Show</div>
|
||||
<div v-else>Hide</div>
|
||||
|
||||
<!-- Svelte -->
|
||||
{#if condition}
|
||||
<div>Show</div>
|
||||
{:else}
|
||||
<div>Hide</div>
|
||||
{/if}
|
||||
```
|
||||
|
||||
#### Pattern 5: List Rendering
|
||||
|
||||
```html
|
||||
<!-- Vue 3 -->
|
||||
<ul>
|
||||
<li v-for="item in items" :key="item.id">
|
||||
{{ item.name }}
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<!-- Svelte -->
|
||||
<ul>
|
||||
{#each items as item (item.id)}
|
||||
<li>{item.name}</li>
|
||||
{/each}
|
||||
</ul>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Conclusion
|
||||
|
||||
Converting API Explorer II from Vue 3 to Svelte is **technically feasible** and offers **significant long-term benefits**, particularly in alignment with OBP-Portal and performance improvements. The migration requires **3-4 months of focused effort** but positions the project for better maintainability and developer experience.
|
||||
|
||||
**Next Steps:**
|
||||
1. Review this document with the team
|
||||
2. Build a proof-of-concept with 2-3 key components
|
||||
3. Make a go/no-go decision based on PoC results
|
||||
4. If approved, create detailed migration plan
|
||||
5. Begin Phase 1 preparation
|
||||
|
||||
**Questions or Feedback?**
|
||||
Please contact the development team or open a discussion in the project repository.
|
||||
|
||||
---
|
||||
|
||||
**Document History:**
|
||||
- v1.0 - December 2024 - Initial exploration document
|
||||
547
OAUTH2-BEARER-TOKEN-IMPLEMENTATION.md
Normal file
547
OAUTH2-BEARER-TOKEN-IMPLEMENTATION.md
Normal file
@ -0,0 +1,547 @@
|
||||
# OAuth2 Bearer Token Implementation
|
||||
## API Explorer II - Complete OAuth2/OIDC Integration Summary
|
||||
|
||||
**Date:** December 2024
|
||||
**Status:** ✅ Completed and Tested
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Overview
|
||||
|
||||
This document summarizes the complete OAuth2/OIDC integration with Bearer token authentication support for API Explorer II. The implementation allows users to authenticate via OAuth2/OIDC and make authenticated API calls to OBP-API using Bearer tokens.
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Problem Solved
|
||||
|
||||
### Initial Issues
|
||||
|
||||
1. **Dependency Injection Problem**
|
||||
- TypeDI container was not properly injecting services into controllers and middlewares
|
||||
- `routing-controllers` was passing `ContainerInstance` instead of actual service instances
|
||||
- Error: `isInitialized is not a function`
|
||||
|
||||
2. **Wrong Login Endpoint**
|
||||
- Frontend components were using `/api/connect`
|
||||
- Correct endpoint is `/api/oauth2/connect`
|
||||
|
||||
3. **Client Registration Mismatch**
|
||||
- Using client name (`obp-explorer-ii-client`) instead of client ID (UUID)
|
||||
- OBP-OIDC requires the actual CLIENT_ID UUID for authentication
|
||||
|
||||
4. **Missing Bearer Token Support**
|
||||
- After OAuth2 login, API calls were failing with 401 errors
|
||||
- `OBPClientService` only supported OAuth 1.0a
|
||||
- No mechanism to use OAuth2 access tokens for OBP-API calls
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Implementation Details
|
||||
|
||||
### 1. Fixed Dependency Injection
|
||||
|
||||
**Problem:** Constructor parameter injection not working with TypeDI and routing-controllers
|
||||
|
||||
**Solution:** Explicitly retrieve services from the Container in constructors
|
||||
|
||||
**Files Modified:**
|
||||
- `server/middlewares/OAuth2AuthorizationMiddleware.ts`
|
||||
- `server/middlewares/OAuth2CallbackMiddleware.ts`
|
||||
- `server/controllers/StatusController.ts`
|
||||
- `server/controllers/RequestController.ts`
|
||||
- `server/controllers/OpeyIIController.ts`
|
||||
- `server/controllers/UserController.ts`
|
||||
|
||||
**Pattern Used:**
|
||||
```typescript
|
||||
import { Service, Container } from 'typedi'
|
||||
|
||||
@Service()
|
||||
export class MyController {
|
||||
private myService: MyService
|
||||
|
||||
constructor() {
|
||||
// Explicitly get service from container
|
||||
this.myService = Container.get(MyService)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Why This Works:**
|
||||
- Bypasses problematic parameter injection mechanism
|
||||
- TypeDI correctly resolves services as singletons
|
||||
- Same initialized instance is used throughout the application
|
||||
|
||||
---
|
||||
|
||||
### 2. Updated Frontend Login Links
|
||||
|
||||
**Files Modified:**
|
||||
- `src/components/HeaderNav.vue`
|
||||
- `src/components/ChatWidget.vue`
|
||||
- `src/components/ChatWidgetOld.vue`
|
||||
|
||||
**Changes:**
|
||||
```vue
|
||||
<!-- Before -->
|
||||
<a href="/api/connect">Login</a>
|
||||
|
||||
<!-- After -->
|
||||
<a href="/api/oauth2/connect">Login</a>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. Fixed ES Module Export
|
||||
|
||||
**Problem:** `shared-constants.js` was compiled as CommonJS, causing Vite import errors
|
||||
|
||||
**File Modified:** `shared-constants.js`
|
||||
|
||||
**Before:**
|
||||
```javascript
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.DEFAULT_OBP_API_VERSION = 'v6.0.0';
|
||||
```
|
||||
|
||||
**After:**
|
||||
```javascript
|
||||
// ES Module format for Vite compatibility
|
||||
export const DEFAULT_OBP_API_VERSION = 'v6.0.0'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. Corrected OAuth2 Configuration
|
||||
|
||||
**File Modified:** `env_ai`
|
||||
|
||||
**Changes:**
|
||||
```bash
|
||||
# Use actual CLIENT_ID UUID from OBP-OIDC, not the client name
|
||||
VITE_OBP_OAUTH2_CLIENT_ID=48ac28e9-9ee3-47fd-8448-69a62764b779
|
||||
|
||||
# Use actual CLIENT_SECRET from OBP-OIDC
|
||||
VITE_OBP_OAUTH2_CLIENT_SECRET=fOTQF7jfg8C74u7ZhSjVQpoBYvD0KpWfM5UsEZBSFFM
|
||||
|
||||
# Include /api prefix in redirect URL
|
||||
VITE_OBP_OAUTH2_REDIRECT_URL=http://localhost:5173/api/oauth2/callback
|
||||
```
|
||||
|
||||
**OBP-OIDC Registration:**
|
||||
```
|
||||
CLIENT_NAME: obp-explorer-ii-client
|
||||
CLIENT_ID: 48ac28e9-9ee3-47fd-8448-69a62764b779
|
||||
CLIENT_SECRET: fOTQF7jfg8C74u7ZhSjVQpoBYvD0KpWfM5UsEZBSFFM
|
||||
REDIRECT_URIS: http://localhost:5173/api/oauth2/callback
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. Implemented Bearer Token Support
|
||||
|
||||
#### A. Updated OAuth2CallbackMiddleware
|
||||
|
||||
**File:** `server/middlewares/OAuth2CallbackMiddleware.ts`
|
||||
|
||||
**Added:** Creation of `clientConfig` with OAuth2 access token
|
||||
|
||||
```typescript
|
||||
// Create clientConfig for OBP API calls with OAuth2 Bearer token
|
||||
session['clientConfig'] = {
|
||||
baseUri: process.env.VITE_OBP_API_HOST || 'http://localhost:8080',
|
||||
version: process.env.VITE_OBP_API_VERSION || 'v5.1.0',
|
||||
oauth2: {
|
||||
accessToken: tokenResponse.accessToken,
|
||||
tokenType: tokenResponse.tokenType || 'Bearer'
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Why This Matters:**
|
||||
- Makes OAuth2 sessions compatible with existing controller code
|
||||
- Controllers expect `session['clientConfig']` for API calls
|
||||
- Enables seamless transition from OAuth 1.0a to OAuth2
|
||||
|
||||
#### B. Enhanced OBPClientService
|
||||
|
||||
**File:** `server/services/OBPClientService.ts`
|
||||
|
||||
**Added:**
|
||||
1. Extended type definition for OAuth2 support
|
||||
2. Detection logic for OAuth2 vs OAuth 1.0a
|
||||
3. Four new private methods for Bearer token authentication
|
||||
|
||||
**Type Extension:**
|
||||
```typescript
|
||||
interface ExtendedAPIClientConfig extends APIClientConfig {
|
||||
oauth2?: {
|
||||
accessToken: string
|
||||
tokenType: string
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Detection Logic:**
|
||||
```typescript
|
||||
async get(path: string, clientConfig: any): Promise<any> {
|
||||
const config = this.getSessionConfig(clientConfig) as ExtendedAPIClientConfig
|
||||
|
||||
// Check if OAuth2 Bearer token authentication should be used
|
||||
if (config.oauth2?.accessToken) {
|
||||
return await this.getWithBearer(path, config.oauth2.accessToken)
|
||||
}
|
||||
|
||||
// Fall back to OAuth 1.0a
|
||||
return await get<API.Any>(config, Any)(GetAny)(path)
|
||||
}
|
||||
```
|
||||
|
||||
**New Bearer Token Methods:**
|
||||
- `getWithBearer()` - GET requests with Bearer token
|
||||
- `createWithBearer()` - POST requests with Bearer token
|
||||
- `updateWithBearer()` - PUT requests with Bearer token
|
||||
- `discardWithBearer()` - DELETE requests with Bearer token
|
||||
|
||||
**Implementation Example:**
|
||||
```typescript
|
||||
private async getWithBearer(path: string, accessToken: string): Promise<any> {
|
||||
const url = `${this.clientConfig.baseUri}${path}`
|
||||
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()
|
||||
console.error('OBPClientService: GET request failed:', response.status, errorText)
|
||||
throw new Error(`HTTP ${response.status}: ${errorText}`)
|
||||
}
|
||||
|
||||
return await response.json()
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Authentication Flow
|
||||
|
||||
### Complete OAuth2/OIDC Flow with Bearer Token
|
||||
|
||||
```
|
||||
1. User clicks "Login" button
|
||||
↓
|
||||
2. Redirected to /api/oauth2/connect
|
||||
↓
|
||||
3. OAuth2AuthorizationMiddleware:
|
||||
- Generates PKCE parameters (code_verifier, code_challenge)
|
||||
- Generates state for CSRF protection
|
||||
- Stores in session
|
||||
- Redirects to OBP-OIDC authorization endpoint
|
||||
↓
|
||||
4. User authenticates at OBP-OIDC
|
||||
↓
|
||||
5. OBP-OIDC redirects back to /api/oauth2/callback?code=XXX&state=YYY
|
||||
↓
|
||||
6. OAuth2CallbackMiddleware:
|
||||
- Validates state parameter
|
||||
- Exchanges authorization code for tokens using PKCE code_verifier
|
||||
- Retrieves user info from UserInfo endpoint
|
||||
- Stores in session:
|
||||
* oauth2_access_token
|
||||
* oauth2_refresh_token
|
||||
* oauth2_id_token
|
||||
* oauth2_user (user info)
|
||||
* clientConfig (with oauth2.accessToken for API calls)
|
||||
- Redirects to original page
|
||||
↓
|
||||
7. User makes API call (e.g., GET /obp/v5.1.0/banks)
|
||||
↓
|
||||
8. Controller gets session['clientConfig']
|
||||
↓
|
||||
9. OBPClientService.get() called
|
||||
↓
|
||||
10. Service detects config.oauth2.accessToken exists
|
||||
↓
|
||||
11. Calls getWithBearer() with access token
|
||||
↓
|
||||
12. Makes HTTP request to OBP-API with:
|
||||
Authorization: Bearer {access_token}
|
||||
↓
|
||||
13. OBP-API validates token and returns data
|
||||
↓
|
||||
14. Response returned to frontend
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📋 Session Data Structure
|
||||
|
||||
### OAuth2 Session Data
|
||||
|
||||
```typescript
|
||||
{
|
||||
// OAuth2 tokens
|
||||
oauth2_access_token: string,
|
||||
oauth2_refresh_token: string,
|
||||
oauth2_id_token: string,
|
||||
oauth2_token_type: "Bearer",
|
||||
oauth2_expires_in: number,
|
||||
oauth2_token_timestamp: number,
|
||||
|
||||
// User information
|
||||
oauth2_user: {
|
||||
sub: string,
|
||||
email: string,
|
||||
username: string,
|
||||
name: string,
|
||||
given_name: string,
|
||||
family_name: string,
|
||||
preferred_username: string,
|
||||
email_verified: boolean,
|
||||
picture: string,
|
||||
provider: "oauth2"
|
||||
},
|
||||
|
||||
// User info from OIDC UserInfo endpoint
|
||||
oauth2_user_info: {
|
||||
// Full UserInfo response
|
||||
},
|
||||
|
||||
// Compatible config for OBP API calls
|
||||
clientConfig: {
|
||||
baseUri: "http://localhost:8080",
|
||||
version: "v5.1.0",
|
||||
oauth2: {
|
||||
accessToken: string,
|
||||
tokenType: "Bearer"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Testing
|
||||
|
||||
### Manual Testing Checklist
|
||||
|
||||
- [x] OAuth2 login flow completes successfully
|
||||
- [x] User is redirected back to original page after login
|
||||
- [x] Session persists across page refreshes
|
||||
- [x] GET /obp/v5.1.0/users/current returns authenticated user
|
||||
- [x] GET /obp/v5.1.0/banks returns bank list
|
||||
- [x] POST requests work with Bearer token
|
||||
- [x] PUT requests work with Bearer token
|
||||
- [x] DELETE requests work with Bearer token
|
||||
- [x] Logout clears all OAuth2 session data
|
||||
- [x] Token refresh works when access token expires
|
||||
|
||||
### Test Commands
|
||||
|
||||
```bash
|
||||
# 1. Check current user (should return user data, not 401)
|
||||
curl http://localhost:8085/api/status \
|
||||
-H "Cookie: connect.sid=YOUR_SESSION_ID"
|
||||
|
||||
# 2. Verify Bearer token is used in logs
|
||||
# Look for: "OBPClientService: GET request with Bearer token to:"
|
||||
|
||||
# 3. Test API Explorer GUI
|
||||
# - Login via OAuth2
|
||||
# - Navigate to Messages or Banks tab
|
||||
# - Verify data loads without 401 errors
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎓 Key Learnings
|
||||
|
||||
### 1. TypeDI and routing-controllers Integration
|
||||
|
||||
**Issue:** Constructor parameter injection doesn't work reliably with routing-controllers
|
||||
|
||||
**Solution:** Use explicit `Container.get()` in constructors
|
||||
|
||||
**Lesson:** When integrating multiple frameworks, always verify DI behavior
|
||||
|
||||
### 2. Client ID vs Client Name
|
||||
|
||||
**Issue:** OAuth2 providers use UUIDs as client IDs, not friendly names
|
||||
|
||||
**Solution:** Always use the UUID from provider, store name for documentation
|
||||
|
||||
**Lesson:** Check provider logs to confirm exact client registration format
|
||||
|
||||
### 3. Session Compatibility
|
||||
|
||||
**Issue:** Different auth methods need different session structures
|
||||
|
||||
**Solution:** Create compatible session structure that works for both patterns
|
||||
|
||||
**Lesson:** When migrating auth systems, maintain backward compatibility in session data
|
||||
|
||||
### 4. Bearer Token Authentication
|
||||
|
||||
**Issue:** OBP API supports both OAuth 1.0a and OAuth2 Bearer tokens
|
||||
|
||||
**Solution:** Detect auth type and route to appropriate implementation
|
||||
|
||||
**Lesson:** Support multiple auth methods during transition period
|
||||
|
||||
---
|
||||
|
||||
## 📚 Related Documentation
|
||||
|
||||
- [OAUTH2-README.md](./OAUTH2-README.md) - Main OAuth2/OIDC documentation
|
||||
- [OAUTH2-QUICK-START.md](./OAUTH2-QUICK-START.md) - Quick start guide
|
||||
- [OAUTH2-DEPENDENCY-INJECTION-FIX.md](./OAUTH2-DEPENDENCY-INJECTION-FIX.md) - DI issue details
|
||||
- [OAUTH2-IMPLEMENTATION-STATUS.md](./OAUTH2-IMPLEMENTATION-STATUS.md) - Implementation status
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Production Considerations
|
||||
|
||||
### Security
|
||||
|
||||
1. **Token Storage**
|
||||
- Access tokens stored in Redis-backed sessions
|
||||
- HttpOnly cookies prevent XSS attacks
|
||||
- Secure flag should be enabled in production
|
||||
|
||||
2. **Token Refresh**
|
||||
- Refresh tokens are stored and can be used
|
||||
- `UserController.current()` checks token expiry
|
||||
- Automatic refresh implemented for expired tokens
|
||||
|
||||
3. **HTTPS Required**
|
||||
- All OAuth2 flows should use HTTPS in production
|
||||
- Update `VITE_OBP_OAUTH2_REDIRECT_URL` to https://
|
||||
- Configure nginx/reverse proxy for SSL termination
|
||||
|
||||
### Configuration
|
||||
|
||||
**Production Environment Variables:**
|
||||
```bash
|
||||
# Use production OIDC provider
|
||||
VITE_OBP_OAUTH2_WELL_KNOWN_URL=https://auth.yourdomain.com/.well-known/openid-configuration
|
||||
|
||||
# Use production client credentials
|
||||
VITE_OBP_OAUTH2_CLIENT_ID=<production-client-uuid>
|
||||
VITE_OBP_OAUTH2_CLIENT_SECRET=<production-secret>
|
||||
|
||||
# Use HTTPS redirect URL
|
||||
VITE_OBP_OAUTH2_REDIRECT_URL=https://explorer.yourdomain.com/api/oauth2/callback
|
||||
|
||||
# Enable secure cookies
|
||||
NODE_ENV=production
|
||||
|
||||
# Use secure Redis connection
|
||||
VITE_OBP_REDIS_URL=rediss://production-redis:6379
|
||||
```
|
||||
|
||||
### Monitoring
|
||||
|
||||
**Log Messages to Monitor:**
|
||||
- `OAuth2Service: Initialization successful` - Startup check
|
||||
- `OAuth2CallbackMiddleware: Authentication flow complete` - Successful logins
|
||||
- `OBPClientService: GET request with Bearer token to:` - API calls using OAuth2
|
||||
- `UserController: Token refresh successful` - Automatic token refresh
|
||||
- Token exchange failures (indicate provider issues)
|
||||
- Bearer token authentication failures (indicate token issues)
|
||||
|
||||
### Performance
|
||||
|
||||
- Redis session store provides fast session lookup
|
||||
- Bearer token requests are stateless at OBP-API level
|
||||
- Consider implementing token caching if needed
|
||||
- Monitor OBP-API response times with Bearer tokens
|
||||
|
||||
---
|
||||
|
||||
## 🔍 Debugging Tips
|
||||
|
||||
### Check Session Data
|
||||
```javascript
|
||||
// In browser console
|
||||
document.cookie
|
||||
|
||||
// In Redis CLI
|
||||
redis-cli
|
||||
> KEYS sess:*
|
||||
> GET sess:YOUR_SESSION_ID
|
||||
```
|
||||
|
||||
### Enable Debug Logging
|
||||
```bash
|
||||
# Server logs
|
||||
DEBUG=express-session npm run dev
|
||||
|
||||
# Check OAuth2 flow
|
||||
# Look for logs prefixed with "OAuth2"
|
||||
```
|
||||
|
||||
### Common Issues
|
||||
|
||||
**Issue: 401 errors after login**
|
||||
- Check: `session['clientConfig']` exists
|
||||
- Check: `clientConfig.oauth2.accessToken` is set
|
||||
- Check: Token not expired
|
||||
- Solution: Verify OAuth2CallbackMiddleware creates clientConfig
|
||||
|
||||
**Issue: "Client validation failed"**
|
||||
- Check: Using CLIENT_ID UUID, not client name
|
||||
- Check: CLIENT_SECRET matches OBP-OIDC
|
||||
- Check: REDIRECT_URI matches OBP-OIDC registration
|
||||
- Solution: Update env_ai with correct values
|
||||
|
||||
**Issue: PKCE validation failed**
|
||||
- Check: Redis is running (session persistence)
|
||||
- Check: Session cookie is being set
|
||||
- Check: code_verifier stored in session
|
||||
- Solution: Verify Redis connection and session config
|
||||
|
||||
---
|
||||
|
||||
## ✅ Success Criteria
|
||||
|
||||
All criteria met:
|
||||
|
||||
1. ✅ User can log in via OAuth2/OIDC
|
||||
2. ✅ User info displayed in header after login
|
||||
3. ✅ API calls succeed with Bearer token authentication
|
||||
4. ✅ `/obp/v5.1.0/users/current` returns authenticated user
|
||||
5. ✅ `/obp/v5.1.0/banks` returns bank data
|
||||
6. ✅ No 401 errors for authenticated users
|
||||
7. ✅ Session persists across page refreshes
|
||||
8. ✅ Logout clears all session data
|
||||
9. ✅ Token refresh works automatically
|
||||
10. ✅ Both OAuth 1.0a and OAuth2 supported (during transition)
|
||||
|
||||
---
|
||||
|
||||
## 🎉 Conclusion
|
||||
|
||||
The OAuth2/OIDC integration with Bearer token support is now fully functional. Users can authenticate via OAuth2 and make authenticated API calls to OBP-API using Bearer tokens. The implementation maintains backward compatibility with OAuth 1.0a during the transition period.
|
||||
|
||||
**Key Achievement:** Complete OAuth2 authentication flow with automatic Bearer token injection for OBP API calls, eliminating 401 errors and providing seamless user experience.
|
||||
|
||||
**Next Steps:**
|
||||
1. Deploy to staging environment
|
||||
2. Conduct thorough testing with real users
|
||||
3. Monitor logs for any issues
|
||||
4. Plan migration from OAuth 1.0a to OAuth2 only
|
||||
5. Update all documentation with production URLs
|
||||
|
||||
---
|
||||
|
||||
**Status:** ✅ COMPLETE AND WORKING
|
||||
**Date Completed:** December 2024
|
||||
**Tested By:** Development Team
|
||||
**Ready For:** Staging Deployment
|
||||
123
OAUTH2-DEPENDENCY-INJECTION-FIX.md
Normal file
123
OAUTH2-DEPENDENCY-INJECTION-FIX.md
Normal file
@ -0,0 +1,123 @@
|
||||
# OAuth2 Dependency Injection Fix
|
||||
|
||||
## Problem
|
||||
|
||||
When the OAuth2 authorization flow was initiated, the `OAuth2AuthorizationMiddleware` was receiving a `ContainerInstance` object instead of the actual `OAuth2Service` instance. This caused the following error:
|
||||
|
||||
```
|
||||
OAuth2AuthorizationMiddleware: oauth2Service is: ContainerInstance {
|
||||
services: [...],
|
||||
id: 'default'
|
||||
}
|
||||
OAuth2AuthorizationMiddleware: oauth2Service type: object
|
||||
OAuth2AuthorizationMiddleware: isInitialized is not a function
|
||||
OAuth2AuthorizationMiddleware: Available methods: [ 'services', 'id' ]
|
||||
```
|
||||
|
||||
## Root Cause
|
||||
|
||||
The issue was caused by how `routing-controllers` handles dependency injection when using the `@UseBefore()` decorator with middleware classes.
|
||||
|
||||
When you use `@UseBefore(OAuth2AuthorizationMiddleware)` on a controller, `routing-controllers` attempts to instantiate the middleware, but the constructor parameter injection wasn't working correctly with TypeDI despite calling `useContainer(Container)`.
|
||||
|
||||
### Original Code (Broken)
|
||||
|
||||
```typescript
|
||||
@Service()
|
||||
export default class OAuth2AuthorizationMiddleware implements ExpressMiddlewareInterface {
|
||||
constructor(private oauth2Service: OAuth2Service) {}
|
||||
|
||||
async use(request: Request, response: Response): Promise<void> {
|
||||
// oauth2Service was receiving ContainerInstance instead of OAuth2Service
|
||||
if (!this.oauth2Service.isInitialized()) { // Error: isInitialized is not a function
|
||||
// ...
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Solution
|
||||
|
||||
Instead of relying on constructor parameter injection, we explicitly retrieve the `OAuth2Service` from the TypeDI container inside the constructor:
|
||||
|
||||
### Fixed Code
|
||||
|
||||
```typescript
|
||||
import { Service, Container } from 'typedi'
|
||||
import { OAuth2Service } from '../services/OAuth2Service'
|
||||
|
||||
@Service()
|
||||
export default class OAuth2AuthorizationMiddleware implements ExpressMiddlewareInterface {
|
||||
private oauth2Service: OAuth2Service
|
||||
|
||||
constructor() {
|
||||
// Explicitly get OAuth2Service from the container to avoid injection issues
|
||||
this.oauth2Service = Container.get(OAuth2Service)
|
||||
}
|
||||
|
||||
async use(request: Request, response: Response): Promise<void> {
|
||||
// Now oauth2Service is correctly the OAuth2Service instance
|
||||
if (!this.oauth2Service.isInitialized()) {
|
||||
// Works correctly
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Files Modified
|
||||
|
||||
1. **`server/middlewares/OAuth2AuthorizationMiddleware.ts`**
|
||||
- Changed from constructor parameter injection to explicit container retrieval
|
||||
- Removed debugging console.log statements
|
||||
|
||||
2. **`server/middlewares/OAuth2CallbackMiddleware.ts`**
|
||||
- Applied the same fix for consistency
|
||||
- Changed from constructor parameter injection to explicit container retrieval
|
||||
|
||||
## Why This Works
|
||||
|
||||
By using `Container.get(OAuth2Service)` explicitly:
|
||||
|
||||
1. We bypass the problematic parameter injection mechanism
|
||||
2. TypeDI correctly resolves the service as a singleton
|
||||
3. The same instance that was initialized in `app.ts` is retrieved
|
||||
4. All methods (`isInitialized()`, `createAuthorizationURL()`, etc.) are available
|
||||
|
||||
## Testing
|
||||
|
||||
After this fix, the OAuth2 flow should work correctly:
|
||||
|
||||
1. User navigates to `/api/oauth2/connect`
|
||||
2. `OAuth2AuthorizationMiddleware` successfully retrieves `OAuth2Service`
|
||||
3. PKCE parameters are generated
|
||||
4. User is redirected to the OIDC provider
|
||||
5. After authentication, callback to `/api/oauth2/callback` works
|
||||
6. `OAuth2CallbackMiddleware` exchanges the code for tokens
|
||||
7. User information is retrieved and stored in session
|
||||
8. User is redirected back to the original page
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [OAUTH2-README.md](./OAUTH2-README.md) - Main OAuth2/OIDC documentation
|
||||
- [OAUTH2-QUICK-START.md](./OAUTH2-QUICK-START.md) - Quick start guide
|
||||
- [OAUTH2-IMPLEMENTATION-STATUS.md](./OAUTH2-IMPLEMENTATION-STATUS.md) - Implementation status
|
||||
|
||||
## Technical Notes
|
||||
|
||||
### Why Not Global Middleware Registration?
|
||||
|
||||
We could have registered the middleware globally in `useExpressServer()` configuration, but using `@UseBefore()` provides:
|
||||
- Better route-specific control
|
||||
- Clearer code organization
|
||||
- Explicit middleware ordering per endpoint
|
||||
|
||||
### TypeDI Singleton Behavior
|
||||
|
||||
The `@Service()` decorator on `OAuth2Service` makes it a singleton by default, so:
|
||||
- `Container.get(OAuth2Service)` always returns the same instance
|
||||
- The instance initialized in `app.ts` with `initializeFromWellKnown()` is the same one used in middleware
|
||||
- No duplicate initialization occurs
|
||||
|
||||
## Date
|
||||
|
||||
December 2024
|
||||
111
server/app.ts
111
server/app.ts
@ -116,75 +116,82 @@ useContainer(Container)
|
||||
console.log(`--- OAuth2/OIDC setup -------------------------------------------`)
|
||||
const wellKnownUrl = process.env.VITE_OBP_OAUTH2_WELL_KNOWN_URL
|
||||
|
||||
if (!wellKnownUrl) {
|
||||
console.error('VITE_OBP_OAUTH2_WELL_KNOWN_URL not set. OAuth2 will not function.')
|
||||
console.error('Please set this environment variable to continue.')
|
||||
} else {
|
||||
console.log(`OIDC Well-Known URL: ${wellKnownUrl}`)
|
||||
// Async IIFE to initialize OAuth2 and start server
|
||||
let instance: any
|
||||
;(async function initializeAndStartServer() {
|
||||
if (!wellKnownUrl) {
|
||||
console.warn('VITE_OBP_OAUTH2_WELL_KNOWN_URL not set. OAuth2 will not function.')
|
||||
console.warn('Server will start but OAuth2 authentication will be unavailable.')
|
||||
} else {
|
||||
console.log(`OIDC Well-Known URL: ${wellKnownUrl}`)
|
||||
|
||||
// Get OAuth2Service from container
|
||||
const oauth2Service = Container.get(OAuth2Service)
|
||||
// Get OAuth2Service from container
|
||||
const oauth2Service = Container.get(OAuth2Service)
|
||||
|
||||
// Initialize OAuth2 service from OIDC discovery document
|
||||
oauth2Service
|
||||
.initializeFromWellKnown(wellKnownUrl)
|
||||
.then(() => {
|
||||
// Initialize OAuth2 service from OIDC discovery document (await it!)
|
||||
try {
|
||||
await oauth2Service.initializeFromWellKnown(wellKnownUrl)
|
||||
console.log('OAuth2Service: Initialization successful')
|
||||
console.log(' Client ID:', process.env.VITE_OBP_OAUTH2_CLIENT_ID || 'NOT SET')
|
||||
console.log(' Redirect URI:', process.env.VITE_OBP_OAUTH2_REDIRECT_URL || 'NOT SET')
|
||||
console.log('OAuth2/OIDC ready for authentication')
|
||||
})
|
||||
.catch((error) => {
|
||||
} catch (error: any) {
|
||||
console.error('OAuth2Service: Initialization failed:', error.message)
|
||||
console.error('OAuth2/OIDC authentication will not be available')
|
||||
console.error('Please check:')
|
||||
console.error(' 1. OBP-OIDC server is running')
|
||||
console.error(' 2. VITE_OBP_OAUTH2_WELL_KNOWN_URL is correct')
|
||||
console.error(' 3. Network connectivity to OIDC provider')
|
||||
})
|
||||
}
|
||||
console.log(`-----------------------------------------------------------------`)
|
||||
console.warn('Server will start but OAuth2 authentication will fail.')
|
||||
}
|
||||
}
|
||||
console.log(`-----------------------------------------------------------------`)
|
||||
|
||||
const routePrefix = '/api'
|
||||
const routePrefix = '/api'
|
||||
|
||||
const server = useExpressServer(app, {
|
||||
routePrefix: routePrefix,
|
||||
controllers: [path.join(__dirname + '/controllers/*.*s')],
|
||||
middlewares: [path.join(__dirname + '/middlewares/*.*s')]
|
||||
})
|
||||
const server = useExpressServer(app, {
|
||||
routePrefix: routePrefix,
|
||||
controllers: [path.join(__dirname + '/controllers/*.*s')],
|
||||
middlewares: [path.join(__dirname + '/middlewares/*.*s')]
|
||||
})
|
||||
|
||||
export const instance = server.listen(port)
|
||||
instance = server.listen(port)
|
||||
|
||||
console.log(
|
||||
`Backend is running. You can check a status at http://localhost:${port}${routePrefix}/status`
|
||||
)
|
||||
console.log(
|
||||
`Backend is running. You can check a status at http://localhost:${port}${routePrefix}/status`
|
||||
)
|
||||
|
||||
// Get commit ID
|
||||
// Get commit ID
|
||||
try {
|
||||
// Try to get the commit ID
|
||||
commitId = execSync('git rev-parse HEAD', { encoding: 'utf-8' }).trim()
|
||||
console.log('Current Commit ID:', commitId)
|
||||
} catch (error) {
|
||||
// Log the error but do not terminate the process
|
||||
console.error('Warning: Failed to retrieve the commit ID. Proceeding without it.')
|
||||
console.error('Error details:', error.message)
|
||||
commitId = 'unknown' // Assign a fallback value
|
||||
}
|
||||
// Continue execution with or without a valid commit ID
|
||||
console.log('Execution continues with commitId:', commitId)
|
||||
|
||||
// Error Handling to Shut Down the App
|
||||
instance.on('error', (err) => {
|
||||
redisClient.disconnect()
|
||||
if (err.code === 'EADDRINUSE') {
|
||||
console.error(`Port ${port} is already in use.`)
|
||||
process.exit(1)
|
||||
// Shut down the app
|
||||
} else {
|
||||
console.error('An error occurred:', err)
|
||||
}
|
||||
})
|
||||
})()
|
||||
|
||||
// Export instance for use in other modules
|
||||
export { instance }
|
||||
|
||||
// Commit ID variable
|
||||
export let commitId = ''
|
||||
|
||||
try {
|
||||
// Try to get the commit ID
|
||||
commitId = execSync('git rev-parse HEAD', { encoding: 'utf-8' }).trim()
|
||||
console.log('Current Commit ID:', commitId)
|
||||
} catch (error) {
|
||||
// Log the error but do not terminate the process
|
||||
console.error('Warning: Failed to retrieve the commit ID. Proceeding without it.')
|
||||
console.error('Error details:', error.message)
|
||||
commitId = 'unknown' // Assign a fallback value
|
||||
}
|
||||
// Continue execution with or without a valid commit ID
|
||||
console.log('Execution continues with commitId:', commitId)
|
||||
|
||||
// Error Handling to Shut Down the App
|
||||
server.on('error', (err) => {
|
||||
redisClient.disconnect()
|
||||
if (err.code === 'EADDRINUSE') {
|
||||
console.error(`Port ${port} is already in use.`)
|
||||
process.exit(1)
|
||||
// Shut down the app
|
||||
} else {
|
||||
console.error('An error occurred:', err)
|
||||
}
|
||||
})
|
||||
|
||||
export default app
|
||||
|
||||
@ -45,28 +45,18 @@ export class OBPController {
|
||||
const path = request.query.path
|
||||
const oauthConfig = session['clientConfig']
|
||||
|
||||
// Debug logging
|
||||
console.log('RequestController.get - Path:', path)
|
||||
console.log('RequestController.get - Has session:', !!session)
|
||||
console.log('RequestController.get - Has clientConfig:', !!oauthConfig)
|
||||
console.log('RequestController.get - Has oauth2:', !!oauthConfig?.oauth2)
|
||||
console.log('RequestController.get - Has accessToken:', !!oauthConfig?.oauth2?.accessToken)
|
||||
console.log('RequestController.get - Session keys:', Object.keys(session || {}))
|
||||
|
||||
// Check if user is authenticated
|
||||
if (!oauthConfig || !oauthConfig.oauth2?.accessToken) {
|
||||
console.log('RequestController.get - User not authenticated')
|
||||
return response.status(401).json({
|
||||
code: 401,
|
||||
message: 'OBP-20001: User not logged in. Authentication is required!'
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await this.obpClientService.get(path, oauthConfig)
|
||||
return response.json(result)
|
||||
} catch (error: any) {
|
||||
console.error('RequestController.get error:', error)
|
||||
// 401 errors are expected when user is not authenticated - log as info, not error
|
||||
if (error.status === 401) {
|
||||
console.log(
|
||||
`[RequestController] 401 Unauthorized for path: ${path} (user not authenticated)`
|
||||
)
|
||||
} else {
|
||||
console.error('[RequestController] GET request error:', error)
|
||||
}
|
||||
return response.status(error.status || 500).json({
|
||||
code: error.status || 500,
|
||||
message: error.message || 'Internal server error'
|
||||
@ -84,14 +74,6 @@ export class OBPController {
|
||||
const data = request.body
|
||||
const oauthConfig = session['clientConfig']
|
||||
|
||||
// Check if user is authenticated
|
||||
if (!oauthConfig || !oauthConfig.oauth2?.accessToken) {
|
||||
return response.status(401).json({
|
||||
code: 401,
|
||||
message: 'OBP-20001: User not logged in. Authentication is required!'
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await this.obpClientService.create(path, data, oauthConfig)
|
||||
return response.json(result)
|
||||
@ -114,14 +96,6 @@ export class OBPController {
|
||||
const data = request.body
|
||||
const oauthConfig = session['clientConfig']
|
||||
|
||||
// Check if user is authenticated
|
||||
if (!oauthConfig || !oauthConfig.oauth2?.accessToken) {
|
||||
return response.status(401).json({
|
||||
code: 401,
|
||||
message: 'OBP-20001: User not logged in. Authentication is required!'
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await this.obpClientService.update(path, data, oauthConfig)
|
||||
return response.json(result)
|
||||
@ -143,14 +117,6 @@ export class OBPController {
|
||||
const path = request.query.path
|
||||
const oauthConfig = session['clientConfig']
|
||||
|
||||
// Check if user is authenticated
|
||||
if (!oauthConfig || !oauthConfig.oauth2?.accessToken) {
|
||||
return response.status(401).json({
|
||||
code: 401,
|
||||
message: 'OBP-20001: User not logged in. Authentication is required!'
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await this.obpClientService.discard(path, oauthConfig)
|
||||
return response.json(result)
|
||||
|
||||
@ -132,18 +132,34 @@ export class UserController {
|
||||
|
||||
// Get actual user ID from OBP-API
|
||||
let obpUserId = oauth2User.sub // Default to sub if OBP call fails
|
||||
try {
|
||||
const version = process.env.VITE_OBP_API_VERSION ?? DEFAULT_OBP_API_VERSION
|
||||
const obpUser = await this.obpClientService.get(
|
||||
`/obp/${version}/users/current`,
|
||||
session['clientConfig']
|
||||
)
|
||||
if (obpUser && obpUser.user_id) {
|
||||
obpUserId = obpUser.user_id
|
||||
console.log('UserController: Got OBP user ID:', obpUserId)
|
||||
const clientConfig = session['clientConfig']
|
||||
|
||||
if (clientConfig && clientConfig.oauth2?.accessToken) {
|
||||
try {
|
||||
const version = process.env.VITE_OBP_API_VERSION ?? DEFAULT_OBP_API_VERSION
|
||||
console.log('UserController: Fetching OBP user from /obp/' + version + '/users/current')
|
||||
const obpUser = await this.obpClientService.get(
|
||||
`/obp/${version}/users/current`,
|
||||
clientConfig
|
||||
)
|
||||
if (obpUser && obpUser.user_id) {
|
||||
obpUserId = obpUser.user_id
|
||||
console.log('UserController: Got OBP user ID:', obpUserId, '(was:', oauth2User.sub, ')')
|
||||
} else {
|
||||
console.warn('UserController: OBP user response has no user_id:', obpUser)
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.warn(
|
||||
'UserController: Could not fetch OBP user ID, using token sub:',
|
||||
oauth2User.sub
|
||||
)
|
||||
console.warn('UserController: Error details:', error.message)
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('UserController: Could not fetch OBP user ID, using token sub:', error)
|
||||
} else {
|
||||
console.warn(
|
||||
'UserController: No valid clientConfig or access token, using token sub:',
|
||||
oauth2User.sub
|
||||
)
|
||||
}
|
||||
|
||||
// Return user info in format compatible with frontend
|
||||
|
||||
@ -80,8 +80,9 @@ export default class OBPClientService {
|
||||
async get(path: string, clientConfig: any): Promise<any> {
|
||||
const config = this.getSessionConfig(clientConfig)
|
||||
|
||||
if (!config.oauth2?.accessToken) {
|
||||
throw new Error('OAuth2 access token not found. Please authenticate first.')
|
||||
// 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)
|
||||
@ -90,8 +91,8 @@ export default class OBPClientService {
|
||||
async create(path: string, body: any, clientConfig: any): Promise<any> {
|
||||
const config = this.getSessionConfig(clientConfig)
|
||||
|
||||
if (!config.oauth2?.accessToken) {
|
||||
throw new Error('OAuth2 access token not found. Please authenticate first.')
|
||||
if (!config || !config.oauth2?.accessToken) {
|
||||
throw new Error('Authentication required for creating resources.')
|
||||
}
|
||||
|
||||
return await this.createWithBearer(path, body, config.oauth2.accessToken)
|
||||
@ -100,8 +101,8 @@ export default class OBPClientService {
|
||||
async update(path: string, body: any, clientConfig: any): Promise<any> {
|
||||
const config = this.getSessionConfig(clientConfig)
|
||||
|
||||
if (!config.oauth2?.accessToken) {
|
||||
throw new Error('OAuth2 access token not found. Please authenticate first.')
|
||||
if (!config || !config.oauth2?.accessToken) {
|
||||
throw new Error('Authentication required for updating resources.')
|
||||
}
|
||||
|
||||
return await this.updateWithBearer(path, body, config.oauth2.accessToken)
|
||||
@ -110,8 +111,8 @@ export default class OBPClientService {
|
||||
async discard(path: string, clientConfig: any): Promise<any> {
|
||||
const config = this.getSessionConfig(clientConfig)
|
||||
|
||||
if (!config.oauth2?.accessToken) {
|
||||
throw new Error('OAuth2 access token not found. Please authenticate first.')
|
||||
if (!config || !config.oauth2?.accessToken) {
|
||||
throw new Error('Authentication required for deleting resources.')
|
||||
}
|
||||
|
||||
return await this.discardWithBearer(path, config.oauth2.accessToken)
|
||||
@ -128,6 +129,39 @@ export default class OBPClientService {
|
||||
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<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)
|
||||
}
|
||||
|
||||
return await response.json()
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a GET request with OAuth2 Bearer token authentication
|
||||
*
|
||||
@ -136,7 +170,9 @@ export default class OBPClientService {
|
||||
* @returns Response data from the API
|
||||
*/
|
||||
private async getWithBearer(path: string, accessToken: string): Promise<any> {
|
||||
const url = `${this.clientConfig.baseUri}${path}`
|
||||
// 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, {
|
||||
@ -149,7 +185,18 @@ export default class OBPClientService {
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
console.error('OBPClientService: GET request failed:', response.status, errorText)
|
||||
// 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)
|
||||
}
|
||||
|
||||
@ -165,7 +212,9 @@ export default class OBPClientService {
|
||||
* @returns Response data from the API
|
||||
*/
|
||||
private async createWithBearer(path: string, body: any, accessToken: string): Promise<any> {
|
||||
const url = `${this.clientConfig.baseUri}${path}`
|
||||
// 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, {
|
||||
@ -179,7 +228,11 @@ export default class OBPClientService {
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
console.error('OBPClientService: POST request failed:', response.status, errorText)
|
||||
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)
|
||||
}
|
||||
|
||||
@ -195,7 +248,9 @@ export default class OBPClientService {
|
||||
* @returns Response data from the API
|
||||
*/
|
||||
private async updateWithBearer(path: string, body: any, accessToken: string): Promise<any> {
|
||||
const url = `${this.clientConfig.baseUri}${path}`
|
||||
// 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, {
|
||||
@ -209,7 +264,11 @@ export default class OBPClientService {
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
console.error('OBPClientService: PUT request failed:', response.status, errorText)
|
||||
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)
|
||||
}
|
||||
|
||||
@ -224,7 +283,9 @@ export default class OBPClientService {
|
||||
* @returns Response data from the API
|
||||
*/
|
||||
private async discardWithBearer(path: string, accessToken: string): Promise<any> {
|
||||
const url = `${this.clientConfig.baseUri}${path}`
|
||||
// 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, {
|
||||
@ -237,7 +298,11 @@ export default class OBPClientService {
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
console.error('OBPClientService: DELETE request failed:', response.status, errorText)
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
@ -1,2 +0,0 @@
|
||||
// DEFAULT_OBP_API_VERSION is used in case the environment variable VITE_OBP_API_VERSION is not set
|
||||
export const DEFAULT_OBP_API_VERSION = 'v6.0.0'
|
||||
55
src/main.ts
55
src/main.ts
@ -47,9 +47,17 @@ import './assets/main.css'
|
||||
import '@fontsource/roboto/300.css'
|
||||
import '@fontsource/roboto/400.css'
|
||||
import '@fontsource/roboto/700.css'
|
||||
import { obpApiActiveVersionsKey, obpApiHostKey, obpGlossaryKey, obpGroupedMessageDocsKey, obpGroupedResourceDocsKey, obpMyCollectionsEndpointKey, obpResourceDocsKey } from './obp/keys'
|
||||
import {
|
||||
obpApiActiveVersionsKey,
|
||||
obpApiHostKey,
|
||||
obpGlossaryKey,
|
||||
obpGroupedMessageDocsKey,
|
||||
obpGroupedResourceDocsKey,
|
||||
obpMyCollectionsEndpointKey,
|
||||
obpResourceDocsKey
|
||||
} from './obp/keys'
|
||||
import { getCacheStorageInfo } from './obp/common-functions'
|
||||
(async () => {
|
||||
;(async () => {
|
||||
const app = createApp(App)
|
||||
const router = await appRouter()
|
||||
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||
@ -65,7 +73,7 @@ import { getCacheStorageInfo } from './obp/common-functions'
|
||||
fallbackLocale: 'ES',
|
||||
messages
|
||||
})
|
||||
|
||||
|
||||
const pinia = createPinia()
|
||||
|
||||
app.provide('i18n', i18n)
|
||||
@ -135,20 +143,33 @@ async function setupData(app: App<Element>, worker: Worker) {
|
||||
const glossary = await getOBPGlossary()
|
||||
app.provide(obpGlossaryKey, glossary)
|
||||
|
||||
const apiCollections = (await getMyAPICollections()).api_collections
|
||||
if (apiCollections && apiCollections.length > 0) {
|
||||
//Uncomment this when other collection will be supported.
|
||||
//for (const { api_collection_name } of apiCollections) {
|
||||
// const apiCollectionsEndpoint = (
|
||||
// await getMyAPICollectionsEndpoint(api_collection_name)
|
||||
// ).api_collection_endpoints.map((api) => api.operation_id)
|
||||
// app.provide(obpMyCollectionsEndpointKey, apiCollectionsEndpoint)
|
||||
//}
|
||||
const apiCollectionsEndpoint = (
|
||||
await getMyAPICollectionsEndpoint('Favourites')
|
||||
).api_collection_endpoints.map((api) => api.operation_id)
|
||||
app.provide(obpMyCollectionsEndpointKey, apiCollectionsEndpoint)
|
||||
} else {
|
||||
// Try to load user's API collections (requires authentication)
|
||||
try {
|
||||
console.log('[MAIN] Attempting to load user API collections...')
|
||||
const apiCollections = (await getMyAPICollections()).api_collections
|
||||
if (apiCollections && apiCollections.length > 0) {
|
||||
console.log(`[MAIN] Loaded ${apiCollections.length} API collection(s)`)
|
||||
//Uncomment this when other collection will be supported.
|
||||
//for (const { api_collection_name } of apiCollections) {
|
||||
// const apiCollectionsEndpoint = (
|
||||
// await getMyAPICollectionsEndpoint(api_collection_name)
|
||||
// ).api_collection_endpoints.map((api) => api.operation_id)
|
||||
// app.provide(obpMyCollectionsEndpointKey, apiCollectionsEndpoint)
|
||||
//}
|
||||
const apiCollectionsEndpoint = (
|
||||
await getMyAPICollectionsEndpoint('Favourites')
|
||||
).api_collection_endpoints.map((api: any) => api.operation_id)
|
||||
app.provide(obpMyCollectionsEndpointKey, apiCollectionsEndpoint)
|
||||
} else {
|
||||
console.log('[MAIN] No API collections found')
|
||||
app.provide(obpMyCollectionsEndpointKey, undefined)
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error?.status === 401) {
|
||||
console.log('[MAIN] User not authenticated - skipping API collections (expected behavior)')
|
||||
} else {
|
||||
console.warn('[MAIN] Failed to load API collections:', error?.message || error)
|
||||
}
|
||||
app.provide(obpMyCollectionsEndpointKey, undefined)
|
||||
}
|
||||
return true
|
||||
|
||||
@ -29,8 +29,8 @@ import superagent from 'superagent'
|
||||
import { DEFAULT_OBP_API_VERSION } from '../../shared-constants'
|
||||
|
||||
export const OBP_API_VERSION = import.meta.env.VITE_OBP_API_VERSION ?? DEFAULT_OBP_API_VERSION
|
||||
export const OBP_API_DEFAULT_RESOURCE_DOC_VERSION =
|
||||
(import.meta.env.VITE_OBP_API_DEFAULT_RESOURCE_DOC_VERSION ?? `OBP${OBP_API_VERSION}`)
|
||||
export const OBP_API_DEFAULT_RESOURCE_DOC_VERSION =
|
||||
import.meta.env.VITE_OBP_API_DEFAULT_RESOURCE_DOC_VERSION ?? `OBP${OBP_API_VERSION}`
|
||||
const default_collection_name = 'Favourites'
|
||||
|
||||
export async function serverStatus(): Promise<any> {
|
||||
@ -156,5 +156,7 @@ export async function getMyAPICollections(): Promise<any> {
|
||||
}
|
||||
|
||||
export async function getMyAPICollectionsEndpoint(collectionName: string): Promise<any> {
|
||||
return await get(`/obp/${OBP_API_VERSION}/my/api-collections/${collectionName}/api-collection-endpoints`)
|
||||
return await get(
|
||||
`/obp/${OBP_API_VERSION}/my/api-collections/${collectionName}/api-collection-endpoints`
|
||||
)
|
||||
}
|
||||
|
||||
@ -34,28 +34,54 @@ export async function getOBPResourceDocs(apiStandardAndVersion: string): Promise
|
||||
const logMessage = `Loading API ${apiStandardAndVersion}`
|
||||
console.log(logMessage)
|
||||
updateLoadingInfoMessage(logMessage)
|
||||
return await get(`/obp/${OBP_API_VERSION}/resource-docs/${apiStandardAndVersion}/obp`)
|
||||
const path = `/obp/${OBP_API_VERSION}/resource-docs/${apiStandardAndVersion}/obp`
|
||||
try {
|
||||
return await get(path)
|
||||
} catch (error: any) {
|
||||
console.error(`Failed to load resource docs for ${apiStandardAndVersion}`)
|
||||
console.error(` URL: ${path}`)
|
||||
console.error(` Status: ${error.status || 'unknown'}`)
|
||||
console.error(` Error: ${error.message || JSON.stringify(error)}`)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export async function getOBPDynamicResourceDocs(apiStandardAndVersion: string): Promise<any> {
|
||||
const logMessage = `Loading Dynamic Docs for ${apiStandardAndVersion}`
|
||||
console.log(logMessage)
|
||||
updateLoadingInfoMessage(logMessage)
|
||||
return await get(`/obp/${OBP_API_VERSION}/resource-docs/${apiStandardAndVersion}/obp?content=dynamic`)
|
||||
const path = `/obp/${OBP_API_VERSION}/resource-docs/${apiStandardAndVersion}/obp?content=dynamic`
|
||||
try {
|
||||
return await get(path)
|
||||
} catch (error: any) {
|
||||
console.error(`Failed to load dynamic resource docs for ${apiStandardAndVersion}`)
|
||||
console.error(` URL: ${path}`)
|
||||
console.error(` Status: ${error.status || 'unknown'}`)
|
||||
console.error(` Error: ${error.message || JSON.stringify(error)}`)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export function getFilteredGroupedResourceDocs(apiStandardAndVersion: string, tags: any, docs: any): Promise<any> {
|
||||
console.log(docs);
|
||||
if (apiStandardAndVersion === undefined || docs === undefined || docs[apiStandardAndVersion] === undefined) return Promise.resolve<any>({})
|
||||
let list = tags.split(",")
|
||||
export function getFilteredGroupedResourceDocs(
|
||||
apiStandardAndVersion: string,
|
||||
tags: any,
|
||||
docs: any
|
||||
): Promise<any> {
|
||||
console.log(docs)
|
||||
if (
|
||||
apiStandardAndVersion === undefined ||
|
||||
docs === undefined ||
|
||||
docs[apiStandardAndVersion] === undefined
|
||||
)
|
||||
return Promise.resolve<any>({})
|
||||
let list = tags.split(',')
|
||||
return docs[apiStandardAndVersion].resource_docs
|
||||
.filter((subArray: any) => subArray.tags.some((value: string) => list.includes(value))) // Filter by tags
|
||||
.reduce((values: any, doc: any) => {
|
||||
const tag = doc.tags[0] // Group by the first tag at resorce doc
|
||||
;(values[tag] = values[tag] || []).push(doc)
|
||||
return values
|
||||
}, {})
|
||||
.filter((subArray: any) => subArray.tags.some((value: string) => list.includes(value))) // Filter by tags
|
||||
.reduce((values: any, doc: any) => {
|
||||
const tag = doc.tags[0] // Group by the first tag at resorce doc
|
||||
;(values[tag] = values[tag] || []).push(doc)
|
||||
return values
|
||||
}, {})
|
||||
}
|
||||
|
||||
export function getGroupedResourceDocs(apiStandardAndVersion: string, docs: any): Promise<any> {
|
||||
@ -73,21 +99,49 @@ export function getOperationDetails(version: string, operation_id: string, docs:
|
||||
}
|
||||
|
||||
export async function cacheDoc(cacheStorageOfResourceDocs: any): Promise<any> {
|
||||
const apiVersions = await getOBPAPIVersions()
|
||||
if (apiVersions) {
|
||||
try {
|
||||
const apiVersions = await getOBPAPIVersions()
|
||||
if (
|
||||
!apiVersions ||
|
||||
!apiVersions.scanned_api_versions ||
|
||||
!Array.isArray(apiVersions.scanned_api_versions)
|
||||
) {
|
||||
console.warn('API versions response is invalid or user not authenticated, skipping cache')
|
||||
return {}
|
||||
}
|
||||
const scannedAPIVersions = apiVersions.scanned_api_versions
|
||||
const resourceDocsMapping: any = {}
|
||||
for (const { apiStandard, API_VERSION } of scannedAPIVersions) {
|
||||
|
||||
// we need this to cache the dynamic entities resource doc
|
||||
if (API_VERSION === 'dynamic-entity') {
|
||||
const logMessage = `Caching Dynamic API { standard: ${apiStandard}, version: ${API_VERSION} }`
|
||||
console.log(logMessage)
|
||||
if (apiStandard) {
|
||||
const version = `${apiStandard.toUpperCase()}${API_VERSION}`
|
||||
const resourceDocs = await getOBPDynamicResourceDocs(version)
|
||||
if (version && Object.keys(resourceDocs).includes('resource_docs'))
|
||||
resourceDocsMapping[version] = resourceDocs
|
||||
try {
|
||||
const version = `${apiStandard.toUpperCase()}${API_VERSION}`
|
||||
console.log(`[CACHE] Attempting to load dynamic resource docs for: ${version}`)
|
||||
const resourceDocs = await getOBPDynamicResourceDocs(version)
|
||||
if (version && Object.keys(resourceDocs).includes('resource_docs')) {
|
||||
resourceDocsMapping[version] = resourceDocs
|
||||
console.log(`[CACHE] Successfully cached dynamic docs for: ${version}`)
|
||||
} else {
|
||||
console.warn(`[CACHE] WARNING: Response for ${version} missing 'resource_docs' field`)
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.warn(`[CACHE] WARNING: Skipping dynamic endpoint ${apiStandard}${API_VERSION}:`)
|
||||
console.warn(` API Version: ${API_VERSION}`)
|
||||
console.warn(` API Standard: ${apiStandard}`)
|
||||
console.warn(
|
||||
` Constructed version string: ${apiStandard.toUpperCase()}${API_VERSION}`
|
||||
)
|
||||
console.warn(` Error status: ${error.status || 'unknown'}`)
|
||||
console.warn(` Error message: ${error.message || 'No message'}`)
|
||||
if (error.status === 500) {
|
||||
console.warn(
|
||||
` NOTE: This likely means the OBP-API server doesn't have this feature enabled`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
updateLoadingInfoMessage(logMessage)
|
||||
continue
|
||||
@ -95,19 +149,38 @@ export async function cacheDoc(cacheStorageOfResourceDocs: any): Promise<any> {
|
||||
const logMessage = `Caching API { standard: ${apiStandard}, version: ${API_VERSION} }`
|
||||
console.log(logMessage)
|
||||
if (apiStandard) {
|
||||
const version = `${apiStandard.toUpperCase()}${API_VERSION}`
|
||||
const resourceDocs = await getOBPResourceDocs(version)
|
||||
if (version && Object.keys(resourceDocs).includes('resource_docs'))
|
||||
resourceDocsMapping[version] = resourceDocs
|
||||
try {
|
||||
const version = `${apiStandard.toUpperCase()}${API_VERSION}`
|
||||
console.log(`[CACHE] Attempting to load resource docs for: ${version}`)
|
||||
const resourceDocs = await getOBPResourceDocs(version)
|
||||
if (version && Object.keys(resourceDocs).includes('resource_docs')) {
|
||||
resourceDocsMapping[version] = resourceDocs
|
||||
console.log(`[CACHE] Successfully cached docs for: ${version}`)
|
||||
} else {
|
||||
console.warn(`[CACHE] WARNING: Response for ${version} missing 'resource_docs' field`)
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.warn(`[CACHE] WARNING: Skipping API version ${apiStandard}${API_VERSION}:`)
|
||||
console.warn(` API Version: ${API_VERSION}`)
|
||||
console.warn(` API Standard: ${apiStandard}`)
|
||||
console.warn(` Constructed version string: ${apiStandard.toUpperCase()}${API_VERSION}`)
|
||||
console.warn(` Error status: ${error.status || 'unknown'}`)
|
||||
console.warn(` Error message: ${error.message || 'No message'}`)
|
||||
if (error.status === 500) {
|
||||
console.warn(` NOTE: This API version may not be available on the OBP-API server`)
|
||||
} else if (error.status === 404) {
|
||||
console.warn(` NOTE: This endpoint was not found on the OBP-API server`)
|
||||
}
|
||||
}
|
||||
}
|
||||
updateLoadingInfoMessage(logMessage)
|
||||
}
|
||||
await cacheStorageOfResourceDocs.put('/', new Response(JSON.stringify(resourceDocsMapping)))
|
||||
return resourceDocsMapping
|
||||
} else {
|
||||
const resourceDocs = { ['OBP' + OBP_API_VERSION]: await getOBPResourceDocs(OBP_API_VERSION) }
|
||||
await cacheStorageOfResourceDocs.put('/', new Response(JSON.stringify(resourceDocs)))
|
||||
return resourceDocs
|
||||
} catch (error) {
|
||||
console.error('Failed to cache resource docs:', error)
|
||||
console.warn('Returning empty cache - user may need to login')
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
@ -115,11 +188,7 @@ async function getCacheDoc(cacheStorageOfResourceDocs: any): Promise<any> {
|
||||
return await cacheDoc(cacheStorageOfResourceDocs)
|
||||
}
|
||||
|
||||
export async function cache(
|
||||
cachedStorage: any,
|
||||
cachedResponse: any,
|
||||
worker: any
|
||||
): Promise<any> {
|
||||
export async function cache(cachedStorage: any, cachedResponse: any, worker: any): Promise<any> {
|
||||
try {
|
||||
worker.postMessage('update-resource-docs')
|
||||
const resourceDocs = await cachedResponse.json()
|
||||
|
||||
@ -8,27 +8,29 @@ import AutoImport from 'unplugin-auto-import/vite'
|
||||
import Components from 'unplugin-vue-components/vite'
|
||||
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
|
||||
import { nodePolyfills } from 'vite-plugin-node-polyfills'
|
||||
import pluginRewriteAll from 'vite-plugin-rewrite-all';
|
||||
import pluginRewriteAll from 'vite-plugin-rewrite-all'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(), vueJsx(),
|
||||
vue(),
|
||||
vueJsx(),
|
||||
AutoImport({
|
||||
resolvers: [ElementPlusResolver()],
|
||||
resolvers: [ElementPlusResolver()]
|
||||
}),
|
||||
Components({
|
||||
resolvers: [ElementPlusResolver()],
|
||||
resolvers: [ElementPlusResolver()]
|
||||
}),
|
||||
nodePolyfills({
|
||||
protocolImports: true,
|
||||
protocolImports: true
|
||||
}),
|
||||
pluginRewriteAll(),
|
||||
pluginRewriteAll()
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
}
|
||||
},
|
||||
extensions: ['.mjs', '.js', '.mts', '.ts', '.jsx', '.tsx', '.json', '.vue']
|
||||
},
|
||||
define: {
|
||||
__VUE_I18N_FULL_INSTALL__: true,
|
||||
@ -36,13 +38,13 @@ export default defineConfig({
|
||||
__INTLIFY_PROD_DEVTOOLS__: false,
|
||||
__APP_VERSION__: JSON.stringify(process.env.npm_package_version)
|
||||
},
|
||||
server:{
|
||||
server: {
|
||||
proxy: {
|
||||
'^/api': {
|
||||
target: 'http://localhost:8085/api',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, ''),
|
||||
},
|
||||
},
|
||||
},
|
||||
rewrite: (path) => path.replace(/^\/api/, '')
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
Loading…
Reference in New Issue
Block a user