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:
2026-01-15 03:20:50 +01:00
parent f3637b85e1
commit 1fa424efb9
70 changed files with 15597 additions and 2098 deletions

View File

@@ -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`;
}