Files
timmal/app/composables/useAuth.ts
Lucien Cartier-Tilet fe2bc5fc87 fix(auth): resolve reactivity bug and improve error handling
- Fix Vue reactivity bug in isAuthenticated computed property by
  reordering condition to ensure dependency tracking (!!user.value
  before pb.authStore.isValid)
- Fix cross-tab sync onChange listener to handle logout by using
  nullish coalescing for undefined model
- Add user-friendly error message mapping in login catch block
- Export initAuth method from useAuth composable
- Add auth.client.ts plugin for client-side auth initialization
- Remove debug console.log statements that masked the Heisenbug
- Simplify auth.client plugin tests to structural checks due to
  Nuxt's test environment auto-importing defineNuxtPlugin
- Update test expectations for new error message behaviour
2026-02-03 22:11:28 +01:00

105 lines
2.9 KiB
TypeScript

import type { AuthProviderInfo, RecordModel } from 'pocketbase';
import { usePocketbase } from './usePocketbase';
export interface LoggedInUser extends RecordModel {
id: string;
email: string;
emailVisibility: boolean;
verified: boolean;
name: string;
avatar?: string;
created: Date;
updated: Date;
}
const user = ref<LoggedInUser | null>(null);
const loading = ref<boolean>(false);
const error = ref<Error | null>(null);
let isInitialized = false;
export const useAuth = () => {
const pb = usePocketbase();
const router = useRouter();
const userCollection = 'users';
const initAuth = async () => {
user.value = pb.authStore.record as LoggedInUser;
pb.authStore.onChange((_token, model) => (user.value = (model as LoggedInUser) ?? null));
};
if (!isInitialized) {
initAuth();
isInitialized = true;
}
const isAuthenticated = computed<boolean>(() => {
return !!user.value && pb.authStore.isValid;
});
const authProviders = async (): Promise<AuthProviderInfo[]> => {
const authMethods = await pb.collection(userCollection).listAuthMethods();
return authMethods.oauth2.enabled ? authMethods.oauth2.providers : [];
};
const login = async (provider: string) => {
loading.value = true;
error.value = null;
try {
const providers = await authProviders();
const providerData = providers.find((p) => p.name === provider);
if (!providerData) {
throw new Error(`${provider} OAuth is not configured`);
}
const response = await pb.collection(userCollection).authWithOAuth2({ provider });
user.value = response.record as LoggedInUser;
} catch (pbError) {
const err = pbError as Error;
console.error('[useAuth] Login failed:', err);
const message = err?.message?.toLowerCase() || '';
if (message.includes('not configured')) {
error.value = new Error('This login provider is not available. Contact admin.');
} else if (message.includes('denied') || message.includes('cancel')) {
error.value = new Error('Login was cancelled. Please try again.');
} else if (message.includes('network') || message.includes('fetch')) {
error.value = new Error('Connection failed. Check your internet and try again.');
} else {
error.value = new Error('Login failed. Please try again later.');
}
} finally {
loading.value = false;
}
};
const refreshAuth = async () => await pb.collection(userCollection).authRefresh();
const handleOAuthCallback = async () => {
user.value = pb.authStore.record as LoggedInUser;
if (isAuthenticated.value) {
await router.push('/dashboard');
} else {
await router.push('/');
}
};
const logout = () => {
user.value = null;
error.value = null;
pb.authStore.clear();
};
return {
user,
loading,
error,
isAuthenticated,
initAuth,
login,
logout,
refreshAuth,
handleOAuthCallback,
authProviders,
};
};