diff --git a/app/composables/useAuth.ts b/app/composables/useAuth.ts index a994027..23a9e5e 100644 --- a/app/composables/useAuth.ts +++ b/app/composables/useAuth.ts @@ -42,6 +42,31 @@ export const useAuth = () => { return authMethods.oauth2.enabled ? authMethods.oauth2.providers : []; }; + /** + * Initiates OAuth login flow with the specified provider. + * + * Handles various error scenarios with user-friendly messages: + * - **Unconfigured Provider**: "not configured" in error → Provider not set up in Pocketbase + * - **Denied Authorization**: "denied" or "cancel" in error → User cancelled OAuth popup + * - **Network Errors**: "network" or "fetch" in error → Connection issues + * - **Generic Errors**: All other errors → Fallback message for unexpected failures + * + * All errors are logged to console with `[useAuth]` prefix for debugging. + * + * @param provider - The OAuth provider name (e.g., 'google', 'microsoft') + * @throws Sets `error.value` with user-friendly message on failure + * + * @example + * ```typescript + * const { login, error } = useAuth() + * + * await login('google') + * if (error.value) { + * // Display error.value.message to user + * console.log(error.value.message) // "Login was cancelled. Please try again." + * } + * ``` + */ const login = async (provider: string) => { loading.value = true; error.value = null; @@ -57,6 +82,7 @@ export const useAuth = () => { const err = pbError as Error; console.error('[useAuth] Login failed:', err); + // Error categorization for user-friendly messages const message = err?.message?.toLowerCase() || ''; if (message.includes('not configured')) { error.value = new Error('This login provider is not available. Contact admin.'); diff --git a/app/plugins/auth.client.ts b/app/plugins/auth.client.ts index 018ce7e..850213e 100644 --- a/app/plugins/auth.client.ts +++ b/app/plugins/auth.client.ts @@ -1,5 +1,23 @@ import { useAuth } from '../composables/useAuth'; +/** + * Authentication plugin that initializes auth state on app mount (client-side only). + * + * This plugin automatically: + * - Restores the user session from Pocketbase's authStore on page load + * - Sets up cross-tab synchronization via Pocketbase's onChange listener + * - Enables session persistence across page refreshes + * + * **Lifecycle**: Runs once on app mount, before any pages are rendered. + * + * **Cross-Tab Sync**: When a user logs in or out in one browser tab, all other tabs + * automatically update their auth state within ~2 seconds (handled by Pocketbase SDK). + * + * **Session Restoration**: On page refresh, the plugin checks Pocketbase's authStore + * and restores the user object if a valid session exists. + * + * @see {@link useAuth} for the auth composable API + */ export default defineNuxtPlugin(() => { const { initAuth } = useAuth(); initAuth(); diff --git a/app/utils/validateRedirect.ts b/app/utils/validateRedirect.ts index 10b6479..67a211b 100644 --- a/app/utils/validateRedirect.ts +++ b/app/utils/validateRedirect.ts @@ -1,3 +1,31 @@ +/** + * Validates a redirect URL to prevent open redirect vulnerabilities. + * + * Only allows same-origin redirects (paths starting with `/` but not `//`). + * External URLs, protocol-relative URLs, and invalid input are rejected. + * + * @param redirect - The redirect URL to validate (typically from query parameters) + * @param fallback - The fallback path to use if validation fails (default: '/dashboard') + * @returns A validated same-origin path or the fallback path + * + * @example + * ```typescript + * // Valid same-origin paths + * validateRedirect('/dashboard') // returns '/dashboard' + * validateRedirect('/projects/123') // returns '/projects/123' + * + * // Rejected external URLs (returns fallback) + * validateRedirect('https://evil.com') // returns '/dashboard' + * validateRedirect('//evil.com') // returns '/dashboard' + * + * // Invalid input (returns fallback) + * validateRedirect(null) // returns '/dashboard' + * validateRedirect(undefined) // returns '/dashboard' + * + * // Custom fallback + * validateRedirect('https://evil.com', '/login') // returns '/login' + * ``` + */ export const validateRedirect = (redirect: string | unknown, fallback = '/dashboard'): string => { if (typeof redirect !== 'string') { return fallback;