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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user