monacousa-portal/composables/useAuth.ts

231 lines
6.8 KiB
TypeScript

import type { User } from '~/utils/types';
export const useAuth = () => {
const user = ref<User | null>(null);
const isAuthenticated = computed(() => !!user.value);
const loading = ref(false);
const error = ref<string | null>(null);
// Tier-based computed properties
const userTier = computed(() => user.value?.tier || 'user');
const isUser = computed(() => user.value?.tier === 'user');
const isBoard = computed(() => user.value?.tier === 'board');
const isAdmin = computed(() => user.value?.tier === 'admin');
const firstName = computed(() => {
if (user.value?.firstName) return user.value.firstName;
if (user.value?.name) return user.value.name.split(' ')[0];
return 'User';
});
// Helper methods
const hasTier = (requiredTier: 'user' | 'board' | 'admin') => {
return user.value?.tier === requiredTier;
};
const hasGroup = (groupName: string) => {
return user.value?.groups?.includes(groupName) || false;
};
// Legacy compatibility
const hasRole = (role: string) => {
return hasGroup(role);
};
// 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(`🔄 Forced session check attempt ${attempts}/${maxAttempts}`);
sessionSuccess = await checkAuth(true); // Force bypass throttling for login
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;
}
};
// Session check throttling to prevent iOS Safari loops
const lastSessionCheck = ref(0);
const SESSION_CHECK_THROTTLE = 5000; // 5 seconds minimum between checks
// Check authentication status with smart throttling
const checkAuth = async (force = false) => {
const now = Date.now();
// Allow forced checks to bypass throttling for critical operations
if (!force && now - lastSessionCheck.value < SESSION_CHECK_THROTTLE) {
console.log('🚫 Session check throttled, using cached result');
return !!user.value;
}
try {
const logType = force ? 'forced' : 'throttled';
console.log(`🔄 Performing ${logType} session check...`);
lastSessionCheck.value = now;
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, // Legacy compatibility
// Actions
login,
loginOAuth,
logout,
requestPasswordReset,
checkAuth,
};
};