Add OAuth 2.0 authentication support for Jira Data Center

- Add OAuth 2.0 configuration options in backend env.ts
- Create authService.ts for OAuth flow, token management, and sessions
- Create auth.ts routes for login, callback, logout, and user info
- Update JiraAssets service to use user tokens when OAuth is enabled
- Add cookie-parser for session handling
- Create Login.tsx component with Jira OAuth login button
- Add authStore.ts (Zustand) for frontend auth state management
- Update App.tsx to show login page when OAuth is enabled
- Add user menu with logout functionality
- Document OAuth setup in CLAUDE.md

Supports two modes:
1. Service Account: Uses JIRA_PAT for all requests (default)
2. OAuth 2.0: Each user authenticates with their Jira credentials
This commit is contained in:
2026-01-06 15:40:52 +01:00
parent 0b27adc2fb
commit ea1c84262c
11 changed files with 1016 additions and 10 deletions

View File

@@ -1,13 +1,87 @@
import { useEffect, useState } from 'react';
import { Routes, Route, Link, useLocation } from 'react-router-dom';
import { clsx } from 'clsx';
import Dashboard from './components/Dashboard';
import ApplicationList from './components/ApplicationList';
import ApplicationDetail from './components/ApplicationDetail';
import TeamDashboard from './components/TeamDashboard';
import Configuration from './components/Configuration';
import ConfigurationV25 from './components/ConfigurationV25';
import Login from './components/Login';
import { useAuthStore } from './stores/authStore';
function App() {
function UserMenu() {
const { user, authMethod, logout } = useAuthStore();
const [isOpen, setIsOpen] = useState(false);
if (!user) return null;
const initials = user.displayName
.split(' ')
.map(n => n[0])
.join('')
.toUpperCase()
.slice(0, 2);
return (
<div className="relative">
<button
onClick={() => setIsOpen(!isOpen)}
className="flex items-center gap-2 px-3 py-1.5 rounded-lg hover:bg-gray-100 transition-colors"
>
{user.avatarUrl ? (
<img
src={user.avatarUrl}
alt={user.displayName}
className="w-8 h-8 rounded-full"
/>
) : (
<div className="w-8 h-8 rounded-full bg-blue-600 flex items-center justify-center">
<span className="text-white text-sm font-medium">{initials}</span>
</div>
)}
<span className="text-sm text-gray-700 hidden sm:block">{user.displayName}</span>
<svg className="w-4 h-4 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
{isOpen && (
<>
<div
className="fixed inset-0 z-10"
onClick={() => setIsOpen(false)}
/>
<div className="absolute right-0 mt-2 w-56 bg-white rounded-lg shadow-lg border border-gray-200 z-20">
<div className="px-4 py-3 border-b border-gray-100">
<p className="text-sm font-medium text-gray-900">{user.displayName}</p>
{user.emailAddress && (
<p className="text-xs text-gray-500 truncate">{user.emailAddress}</p>
)}
<p className="text-xs text-gray-400 mt-1">
{authMethod === 'oauth' ? 'Jira OAuth' : 'Service Account'}
</p>
</div>
<div className="py-1">
{authMethod === 'oauth' && (
<button
onClick={() => {
setIsOpen(false);
logout();
}}
className="w-full px-4 py-2 text-left text-sm text-red-600 hover:bg-red-50 transition-colors"
>
Uitloggen
</button>
)}
</div>
</div>
</>
)}
</div>
);
}
function AppContent() {
const location = useLocation();
const navItems = [
@@ -60,9 +134,7 @@ function App() {
</nav>
</div>
<div className="flex items-center space-x-4">
<span className="text-sm text-gray-500">ICMT</span>
</div>
<UserMenu />
</div>
</div>
</header>
@@ -81,4 +153,42 @@ function App() {
);
}
function App() {
const { isAuthenticated, isLoading, checkAuth, fetchConfig, config } = useAuthStore();
useEffect(() => {
// Fetch auth config first, then check auth status
const init = async () => {
await fetchConfig();
await checkAuth();
};
init();
}, [fetchConfig, checkAuth]);
// Show loading state
if (isLoading) {
return (
<div className="min-h-screen bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 flex items-center justify-center">
<div className="text-center">
<div className="w-12 h-12 border-4 border-cyan-500 border-t-transparent rounded-full animate-spin mx-auto mb-4"></div>
<p className="text-slate-400">Laden...</p>
</div>
</div>
);
}
// Show login if OAuth is enabled and not authenticated
if (config?.oauthEnabled && !isAuthenticated) {
return <Login />;
}
// Show login if nothing is configured
if (!config?.oauthEnabled && !config?.serviceAccountEnabled) {
return <Login />;
}
// Show main app
return <AppContent />;
}
export default App;