Add authentication, user management, and database migration features
- Implement OAuth 2.0 and PAT authentication methods - Add user management, roles, and profile functionality - Add database migrations and admin user scripts - Update services for authentication and user settings - Add protected routes and permission hooks - Update documentation for authentication and database access
This commit is contained in:
@@ -2,18 +2,28 @@ import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
|
||||
export interface User {
|
||||
accountId: string;
|
||||
id?: number;
|
||||
accountId?: string;
|
||||
email?: string;
|
||||
username?: string;
|
||||
displayName: string;
|
||||
emailAddress?: string;
|
||||
avatarUrl?: string;
|
||||
roles?: string[];
|
||||
permissions?: string[];
|
||||
}
|
||||
|
||||
interface AuthConfig {
|
||||
// Application branding
|
||||
appName: string;
|
||||
appTagline: string;
|
||||
appCopyright: string;
|
||||
// The configured authentication method
|
||||
authMethod: 'pat' | 'oauth' | 'none';
|
||||
authMethod: 'pat' | 'oauth' | 'local' | 'none';
|
||||
// Legacy fields (for backward compatibility)
|
||||
oauthEnabled: boolean;
|
||||
serviceAccountEnabled: boolean;
|
||||
localAuthEnabled: boolean;
|
||||
jiraHost: string;
|
||||
}
|
||||
|
||||
@@ -21,26 +31,31 @@ interface AuthState {
|
||||
// State
|
||||
user: User | null;
|
||||
isAuthenticated: boolean;
|
||||
authMethod: 'oauth' | 'service-account' | null;
|
||||
authMethod: 'oauth' | 'local' | 'service-account' | null;
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
config: AuthConfig | null;
|
||||
isInitialized: boolean; // Track if initialization has completed
|
||||
|
||||
// Actions
|
||||
setUser: (user: User | null, method: 'oauth' | 'service-account' | null) => void;
|
||||
setUser: (user: User | null, method: 'oauth' | 'local' | 'service-account' | null) => void;
|
||||
setLoading: (loading: boolean) => void;
|
||||
setError: (error: string | null) => void;
|
||||
setConfig: (config: AuthConfig) => void;
|
||||
setInitialized: (initialized: boolean) => void;
|
||||
logout: () => Promise<void>;
|
||||
checkAuth: () => Promise<void>;
|
||||
fetchConfig: () => Promise<void>;
|
||||
localLogin: (email: string, password: string) => Promise<void>;
|
||||
hasPermission: (permission: string) => boolean;
|
||||
hasRole: (role: string) => boolean;
|
||||
}
|
||||
|
||||
const API_BASE = import.meta.env.VITE_API_URL || 'http://localhost:3001';
|
||||
|
||||
export const useAuthStore = create<AuthState>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
(set, get) => ({
|
||||
// Initial state
|
||||
user: null,
|
||||
isAuthenticated: false,
|
||||
@@ -48,6 +63,7 @@ export const useAuthStore = create<AuthState>()(
|
||||
isLoading: true,
|
||||
error: null,
|
||||
config: null,
|
||||
isInitialized: false,
|
||||
|
||||
// Actions
|
||||
setUser: (user, method) => set({
|
||||
@@ -63,6 +79,8 @@ export const useAuthStore = create<AuthState>()(
|
||||
|
||||
setConfig: (config) => set({ config }),
|
||||
|
||||
setInitialized: (initialized) => set({ isInitialized: initialized }),
|
||||
|
||||
logout: async () => {
|
||||
try {
|
||||
await fetch(`${API_BASE}/api/auth/logout`, {
|
||||
@@ -81,19 +99,49 @@ export const useAuthStore = create<AuthState>()(
|
||||
},
|
||||
|
||||
checkAuth: async () => {
|
||||
// Use a simple flag to prevent concurrent calls
|
||||
const currentState = get();
|
||||
if (currentState.isLoading) {
|
||||
// Wait for the existing call to complete (max 1 second)
|
||||
let waitCount = 0;
|
||||
while (get().isLoading && waitCount < 10) {
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
waitCount++;
|
||||
}
|
||||
const stateAfterWait = get();
|
||||
// If previous call completed and we have auth state, we're done
|
||||
if (!stateAfterWait.isLoading && (stateAfterWait.isAuthenticated || stateAfterWait.user)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
set({ isLoading: true });
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/auth/me`, {
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Auth check failed');
|
||||
// Handle rate limiting (429) gracefully
|
||||
if (response.status === 429) {
|
||||
set({
|
||||
user: null,
|
||||
isAuthenticated: false,
|
||||
authMethod: null,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
});
|
||||
return;
|
||||
}
|
||||
throw new Error(`Auth check failed: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.authenticated) {
|
||||
if (data.authenticated && data.user) {
|
||||
set({
|
||||
user: data.user,
|
||||
isAuthenticated: true,
|
||||
@@ -110,7 +158,7 @@ export const useAuthStore = create<AuthState>()(
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Auth check error:', error);
|
||||
console.error('[checkAuth] Auth check error:', error);
|
||||
set({
|
||||
user: null,
|
||||
isAuthenticated: false,
|
||||
@@ -122,18 +170,98 @@ export const useAuthStore = create<AuthState>()(
|
||||
},
|
||||
|
||||
fetchConfig: async () => {
|
||||
// Check if config is already loaded to prevent duplicate calls
|
||||
const currentState = get();
|
||||
if (currentState.config) {
|
||||
return; // Config already loaded, skip API call
|
||||
}
|
||||
|
||||
const defaultConfig = {
|
||||
appName: 'CMDB Insight',
|
||||
appTagline: 'Management console for Jira Assets',
|
||||
appCopyright: `© ${new Date().getFullYear()} Zuyderland Medisch Centrum`,
|
||||
authMethod: 'local' as const,
|
||||
oauthEnabled: false,
|
||||
serviceAccountEnabled: false,
|
||||
localAuthEnabled: true,
|
||||
jiraHost: '',
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/auth/config`, {
|
||||
credentials: 'include',
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const config = await response.json();
|
||||
set({ config });
|
||||
const configData = await response.json();
|
||||
set({ config: configData });
|
||||
} else {
|
||||
// Any non-OK response - set default config
|
||||
set({ config: defaultConfig });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch auth config:', error);
|
||||
console.error('[fetchConfig] Failed to fetch auth config:', error);
|
||||
// Set default config to allow app to proceed
|
||||
set({ config: defaultConfig });
|
||||
}
|
||||
|
||||
// Final verification - ensure config is set
|
||||
const finalState = get();
|
||||
if (!finalState.config) {
|
||||
set({ config: defaultConfig });
|
||||
}
|
||||
},
|
||||
|
||||
localLogin: async (email: string, password: string) => {
|
||||
set({ isLoading: true, error: null });
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ email, password }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.error || 'Login failed');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
set({
|
||||
user: data.user,
|
||||
isAuthenticated: true,
|
||||
authMethod: 'local',
|
||||
isLoading: false,
|
||||
error: null,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Local login error:', error);
|
||||
set({
|
||||
isLoading: false,
|
||||
error: error instanceof Error ? error.message : 'Login failed',
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
hasPermission: (permission: string) => {
|
||||
const { user } = get();
|
||||
if (!user || !user.permissions) {
|
||||
return false;
|
||||
}
|
||||
return user.permissions.includes(permission);
|
||||
},
|
||||
|
||||
hasRole: (role: string) => {
|
||||
const { user } = get();
|
||||
if (!user || !user.roles) {
|
||||
return false;
|
||||
}
|
||||
return user.roles.includes(role);
|
||||
},
|
||||
}),
|
||||
{
|
||||
@@ -150,4 +278,3 @@ export const useAuthStore = create<AuthState>()(
|
||||
export function getLoginUrl(): string {
|
||||
return `${API_BASE}/api/auth/login`;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user