323 lines
9.5 KiB
TypeScript
323 lines
9.5 KiB
TypeScript
import type { User } from '~/utils/types';
|
|
|
|
export const useAuth = () => {
|
|
// Use useState for SSR compatibility - prevents hydration mismatches
|
|
const user = useState<User | null>('auth.user', () => null);
|
|
const isAuthenticated = computed(() => !!user.value);
|
|
const loading = ref(false);
|
|
const error = ref<string | null>(null);
|
|
|
|
// Enhanced role checking method - supports both realm roles and legacy groups
|
|
const hasRole = (roleName: string): boolean => {
|
|
if (!user.value) return false;
|
|
|
|
// Get roles from user token (Keycloak format)
|
|
const userToken = user.value as any; // Cast for accessing token properties
|
|
|
|
// Check realm roles first (new system)
|
|
const realmRoles = userToken.realm_access?.roles || [];
|
|
if (realmRoles.includes(roleName)) {
|
|
return true;
|
|
}
|
|
|
|
// Check client roles (new system)
|
|
const clientRoles = userToken.resource_access || {};
|
|
for (const clientId in clientRoles) {
|
|
const roles = clientRoles[clientId]?.roles || [];
|
|
if (roles.includes(roleName)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// Fallback to legacy group system
|
|
const groups = user.value.groups || [];
|
|
return groups.includes(roleName) || groups.includes(`/${roleName}`);
|
|
};
|
|
|
|
// Enhanced tier-based computed properties with role support
|
|
const isUser = computed(() => {
|
|
// Check new realm roles first
|
|
if (hasRole('monaco-user')) return true;
|
|
// Fallback to legacy tier system
|
|
return user.value?.tier === 'user';
|
|
});
|
|
|
|
const isBoard = computed(() => {
|
|
// Check new realm roles first
|
|
if (hasRole('monaco-board')) return true;
|
|
// Fallback to legacy tier system
|
|
return user.value?.tier === 'board';
|
|
});
|
|
|
|
const isAdmin = computed(() => {
|
|
// Check new realm roles first
|
|
if (hasRole('monaco-admin')) return true;
|
|
// Fallback to legacy tier system
|
|
return user.value?.tier === 'admin';
|
|
});
|
|
|
|
// Enhanced tier computation with role priority
|
|
const userTier = computed(() => {
|
|
if (hasRole('monaco-admin')) return 'admin';
|
|
if (hasRole('monaco-board')) return 'board';
|
|
if (hasRole('monaco-user')) return 'user';
|
|
// Fallback to legacy tier system
|
|
return user.value?.tier || 'user';
|
|
});
|
|
|
|
const firstName = computed(() => {
|
|
if (user.value?.firstName) return user.value.firstName;
|
|
if (user.value?.name) return user.value.name.split(' ')[0];
|
|
return 'User';
|
|
});
|
|
|
|
// Enhanced helper methods
|
|
const hasTier = (requiredTier: 'user' | 'board' | 'admin') => {
|
|
// Use computed userTier which handles both new and legacy systems
|
|
return userTier.value === requiredTier;
|
|
};
|
|
|
|
const hasGroup = (groupName: string) => {
|
|
return user.value?.groups?.includes(groupName) || false;
|
|
};
|
|
|
|
// New helper methods for realm roles
|
|
const hasRealmRole = (roleName: string): boolean => {
|
|
if (!user.value) return false;
|
|
const userToken = user.value as any;
|
|
const realmRoles = userToken.realm_access?.roles || [];
|
|
return realmRoles.includes(roleName);
|
|
};
|
|
|
|
const hasClientRole = (roleName: string, clientId?: string): boolean => {
|
|
if (!user.value) return false;
|
|
const userToken = user.value as any;
|
|
const clientRoles = userToken.resource_access || {};
|
|
|
|
if (clientId) {
|
|
// Check specific client
|
|
const roles = clientRoles[clientId]?.roles || [];
|
|
return roles.includes(roleName);
|
|
} else {
|
|
// Check all clients
|
|
for (const cId in clientRoles) {
|
|
const roles = clientRoles[cId]?.roles || [];
|
|
if (roles.includes(roleName)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
};
|
|
|
|
// Get all user roles (combines realm and client roles)
|
|
const getAllRoles = (): string[] => {
|
|
if (!user.value) return [];
|
|
const userToken = user.value as any;
|
|
const roles: string[] = [];
|
|
|
|
// Add realm roles
|
|
const realmRoles = userToken.realm_access?.roles || [];
|
|
roles.push(...realmRoles);
|
|
|
|
// Add client roles
|
|
const clientRoles = userToken.resource_access || {};
|
|
for (const clientId in clientRoles) {
|
|
const clientRolesList = clientRoles[clientId]?.roles || [];
|
|
roles.push(...clientRolesList);
|
|
}
|
|
|
|
// Add legacy groups for compatibility
|
|
const groups = user.value.groups || [];
|
|
roles.push(...groups);
|
|
|
|
return [...new Set(roles)]; // Remove duplicates
|
|
};
|
|
|
|
// Direct login method
|
|
const login = async (credentials: { username: string; password: string; rememberMe?: boolean }) => {
|
|
loading.value = true;
|
|
error.value = null;
|
|
|
|
try {
|
|
console.log('🔄 Starting login request...');
|
|
|
|
const response = await $fetch<{
|
|
success: boolean;
|
|
redirectTo?: string;
|
|
user?: User;
|
|
}>('/api/auth/direct-login', {
|
|
method: 'POST',
|
|
body: credentials,
|
|
timeout: 30000 // 30 second timeout
|
|
});
|
|
|
|
console.log('✅ Login response received:', response);
|
|
|
|
if (response.success) {
|
|
// Add a small delay to ensure cookie is set before checking session
|
|
console.log('⏳ Waiting for cookie to be set...');
|
|
await new Promise(resolve => setTimeout(resolve, 200));
|
|
|
|
// After successful login, get the user data from the session
|
|
console.log('🔄 Getting user data from session...');
|
|
|
|
// Try multiple times in case of timing issues
|
|
let sessionSuccess = false;
|
|
let attempts = 0;
|
|
const maxAttempts = 3;
|
|
|
|
while (!sessionSuccess && attempts < maxAttempts) {
|
|
attempts++;
|
|
console.log(`🔄 Session check attempt ${attempts}/${maxAttempts}`);
|
|
|
|
sessionSuccess = await checkAuth();
|
|
|
|
if (!sessionSuccess && attempts < maxAttempts) {
|
|
console.log('⏳ Session not ready, waiting 500ms...');
|
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
}
|
|
}
|
|
|
|
if (sessionSuccess) {
|
|
console.log('👤 User data retrieved from session:', user.value);
|
|
|
|
// Return redirect URL for the component to handle
|
|
console.log('✅ Login successful, returning redirect URL:', response.redirectTo || '/dashboard');
|
|
return {
|
|
success: true,
|
|
redirectTo: response.redirectTo || '/dashboard'
|
|
};
|
|
} else {
|
|
console.warn('❌ Failed to get user data from session after login');
|
|
// Still return success with redirect since login was successful on server
|
|
return {
|
|
success: true,
|
|
redirectTo: '/dashboard'
|
|
};
|
|
}
|
|
}
|
|
|
|
console.warn('❌ Login response indicates failure:', response);
|
|
return { success: false, error: 'Login failed' };
|
|
} catch (err: any) {
|
|
console.error('❌ Login error caught:', err);
|
|
|
|
// Handle different types of errors
|
|
let errorMessage = 'Login failed';
|
|
|
|
if (err.status === 502) {
|
|
errorMessage = 'Server temporarily unavailable. Please try again.';
|
|
} else if (err.status === 401) {
|
|
errorMessage = 'Invalid username or password';
|
|
} else if (err.status === 429) {
|
|
errorMessage = 'Too many login attempts. Please try again later.';
|
|
} else if (err.data?.message) {
|
|
errorMessage = err.data.message;
|
|
} else if (err.message) {
|
|
errorMessage = err.message;
|
|
}
|
|
|
|
error.value = errorMessage;
|
|
return { success: false, error: errorMessage };
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
};
|
|
|
|
// OAuth login method (fallback)
|
|
const loginOAuth = () => {
|
|
return navigateTo('/api/auth/login');
|
|
};
|
|
|
|
// Password reset method
|
|
const requestPasswordReset = async (email: string) => {
|
|
loading.value = true;
|
|
error.value = null;
|
|
|
|
try {
|
|
const response = await $fetch<{
|
|
success: boolean;
|
|
message: string;
|
|
}>('/api/auth/forgot-password', {
|
|
method: 'POST',
|
|
body: { email }
|
|
});
|
|
|
|
return { success: true, message: response.message };
|
|
} catch (err: any) {
|
|
error.value = err.data?.message || 'Password reset failed';
|
|
return { success: false, error: error.value };
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
};
|
|
|
|
// Check authentication status - simple and reliable
|
|
const checkAuth = async () => {
|
|
try {
|
|
console.log('🔄 Performing session check...');
|
|
|
|
const response = await $fetch<{
|
|
authenticated: boolean;
|
|
user: User | null;
|
|
}>('/api/auth/session');
|
|
|
|
if (response.authenticated && response.user) {
|
|
user.value = response.user;
|
|
return true;
|
|
} else {
|
|
user.value = null;
|
|
return false;
|
|
}
|
|
} catch (err) {
|
|
console.error('Auth check error:', err);
|
|
user.value = null;
|
|
return false;
|
|
}
|
|
};
|
|
|
|
// Logout method
|
|
const logout = async () => {
|
|
try {
|
|
await $fetch('/api/auth/logout', { method: 'POST' });
|
|
user.value = null;
|
|
await navigateTo('/login');
|
|
} catch (err) {
|
|
console.error('Logout error:', err);
|
|
user.value = null;
|
|
await navigateTo('/login');
|
|
}
|
|
};
|
|
|
|
return {
|
|
// State
|
|
user: readonly(user),
|
|
isAuthenticated,
|
|
loading: readonly(loading),
|
|
error: readonly(error),
|
|
|
|
// Tier-based properties
|
|
userTier,
|
|
isUser,
|
|
isBoard,
|
|
isAdmin,
|
|
firstName,
|
|
|
|
// Helper methods
|
|
hasTier,
|
|
hasGroup,
|
|
hasRole, // Enhanced with realm role support
|
|
hasRealmRole,
|
|
hasClientRole,
|
|
getAllRoles,
|
|
|
|
// Actions
|
|
login,
|
|
loginOAuth,
|
|
logout,
|
|
requestPasswordReset,
|
|
checkAuth,
|
|
};
|
|
};
|