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:
150
frontend/src/stores/authStore.ts
Normal file
150
frontend/src/stores/authStore.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
|
||||
export interface User {
|
||||
accountId: string;
|
||||
displayName: string;
|
||||
emailAddress?: string;
|
||||
avatarUrl?: string;
|
||||
}
|
||||
|
||||
interface AuthConfig {
|
||||
oauthEnabled: boolean;
|
||||
serviceAccountEnabled: boolean;
|
||||
jiraHost: string;
|
||||
}
|
||||
|
||||
interface AuthState {
|
||||
// State
|
||||
user: User | null;
|
||||
isAuthenticated: boolean;
|
||||
authMethod: 'oauth' | 'service-account' | null;
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
config: AuthConfig | null;
|
||||
|
||||
// Actions
|
||||
setUser: (user: User | null, method: 'oauth' | 'service-account' | null) => void;
|
||||
setLoading: (loading: boolean) => void;
|
||||
setError: (error: string | null) => void;
|
||||
setConfig: (config: AuthConfig) => void;
|
||||
logout: () => Promise<void>;
|
||||
checkAuth: () => Promise<void>;
|
||||
fetchConfig: () => Promise<void>;
|
||||
}
|
||||
|
||||
const API_BASE = import.meta.env.VITE_API_URL || 'http://localhost:3001';
|
||||
|
||||
export const useAuthStore = create<AuthState>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
// Initial state
|
||||
user: null,
|
||||
isAuthenticated: false,
|
||||
authMethod: null,
|
||||
isLoading: true,
|
||||
error: null,
|
||||
config: null,
|
||||
|
||||
// Actions
|
||||
setUser: (user, method) => set({
|
||||
user,
|
||||
isAuthenticated: !!user,
|
||||
authMethod: method,
|
||||
error: null,
|
||||
}),
|
||||
|
||||
setLoading: (loading) => set({ isLoading: loading }),
|
||||
|
||||
setError: (error) => set({ error, isLoading: false }),
|
||||
|
||||
setConfig: (config) => set({ config }),
|
||||
|
||||
logout: async () => {
|
||||
try {
|
||||
await fetch(`${API_BASE}/api/auth/logout`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Logout error:', error);
|
||||
}
|
||||
set({
|
||||
user: null,
|
||||
isAuthenticated: false,
|
||||
authMethod: null,
|
||||
error: null,
|
||||
});
|
||||
},
|
||||
|
||||
checkAuth: async () => {
|
||||
set({ isLoading: true });
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/auth/me`, {
|
||||
credentials: 'include',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Auth check failed');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.authenticated) {
|
||||
set({
|
||||
user: data.user,
|
||||
isAuthenticated: true,
|
||||
authMethod: data.authMethod,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
});
|
||||
} else {
|
||||
set({
|
||||
user: null,
|
||||
isAuthenticated: false,
|
||||
authMethod: null,
|
||||
isLoading: false,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Auth check error:', error);
|
||||
set({
|
||||
user: null,
|
||||
isAuthenticated: false,
|
||||
authMethod: null,
|
||||
isLoading: false,
|
||||
error: error instanceof Error ? error.message : 'Authentication check failed',
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
fetchConfig: async () => {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/auth/config`, {
|
||||
credentials: 'include',
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const config = await response.json();
|
||||
set({ config });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch auth config:', error);
|
||||
}
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: 'auth-storage',
|
||||
partialize: (state) => ({
|
||||
// Only persist non-sensitive data
|
||||
authMethod: state.authMethod,
|
||||
}),
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// Helper to get login URL
|
||||
export function getLoginUrl(): string {
|
||||
return `${API_BASE}/api/auth/login`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user