From 1e97e190c54c735983b4fe9e7266f8e45bd73885 Mon Sep 17 00:00:00 2001 From: FragginWagon Date: Wed, 28 Jan 2026 22:46:32 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=92=20Add=20authentication=20composabl?= =?UTF-8?q?e=20for=20managing=20auth=20state,=20token=20handling,=20and=20?= =?UTF-8?q?user=20info?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pokedex.online/src/composables/useAuth.js | 189 ++++++++++++++++++ 1 file changed, 189 insertions(+) diff --git a/code/websites/pokedex.online/src/composables/useAuth.js b/code/websites/pokedex.online/src/composables/useAuth.js index e69de29..82d0984 100644 --- a/code/websites/pokedex.online/src/composables/useAuth.js +++ b/code/websites/pokedex.online/src/composables/useAuth.js @@ -0,0 +1,189 @@ +/** + * useAuth Composable + * + * Manages authentication state including login, logout, token storage, and user info + */ + +import { ref, computed, watch } from 'vue'; +import { apiClient } from '../utilities/api-client.js'; + +// Shared state +const token = ref(localStorage.getItem('auth_token') || null); +const user = ref(null); +const isLoading = ref(false); +const error = ref(null); + +// Compute derived states +const isAuthenticated = computed(() => !!token.value && !!user.value); +const isAdmin = computed(() => user.value?.isAdmin || false); + +/** + * Load and verify stored token on app startup + */ +async function initializeAuth() { + const storedToken = localStorage.getItem('auth_token'); + + if (!storedToken) { + token.value = null; + user.value = null; + return; + } + + try { + const response = await apiClient.post('/api/auth/verify', { token: storedToken }); + token.value = storedToken; + user.value = response.user; + } catch (err) { + // Token is invalid, clear it + localStorage.removeItem('auth_token'); + token.value = null; + user.value = null; + } +} + +/** + * Login with password + * @param {string} password - Admin password + * @returns {Promise} Response with token and user info + * @throws {Error} If login fails + */ +async function login(password) { + isLoading.value = true; + error.value = null; + + try { + if (!password) { + throw new Error('Password is required'); + } + + const response = await apiClient.post('/api/auth/login', { password }); + + // Store token + token.value = response.token; + user.value = response.user; + localStorage.setItem('auth_token', response.token); + + return response; + } catch (err) { + error.value = err.message; + token.value = null; + user.value = null; + throw err; + } finally { + isLoading.value = false; + } +} + +/** + * Logout and clear authentication + */ +async function logout() { + isLoading.value = true; + + try { + await apiClient.post('/api/auth/logout', {}); + } catch (err) { + console.error('Logout error:', err); + } finally { + // Clear token regardless of API response + token.value = null; + user.value = null; + localStorage.removeItem('auth_token'); + isLoading.value = false; + } +} + +/** + * Refresh the current token + * @returns {Promise} Response with new token + * @throws {Error} If refresh fails + */ +async function refreshToken() { + if (!token.value) { + throw new Error('No token to refresh'); + } + + try { + const response = await apiClient.post('/api/auth/refresh', { token: token.value }); + token.value = response.token; + localStorage.setItem('auth_token', response.token); + return response; + } catch (err) { + // If refresh fails, logout + await logout(); + throw err; + } +} + +/** + * Get current user info + * @returns {Promise} User info + */ +async function getUserInfo() { + try { + const response = await apiClient.get('/api/auth/user', { + headers: { + Authorization: `Bearer ${token.value}` + } + }); + user.value = response.user; + return response.user; + } catch (err) { + // If getting user info fails, logout + await logout(); + throw err; + } +} + +/** + * Check if user has a specific permission + * @param {string|string[]} permissions - Permission or array of permissions + * @returns {boolean} True if user has permission + */ +function hasPermission(permissions) { + if (!user.value) return false; + + const perms = Array.isArray(permissions) ? permissions : [permissions]; + return perms.some(perm => user.value.permissions?.includes(perm)); +} + +/** + * Add auth token to API client headers + */ +export function setupAuthInterceptor() { + if (token.value) { + apiClient.setDefaultHeader('Authorization', `Bearer ${token.value}`); + } + + // Watch for token changes and update header + watch(token, (newToken) => { + if (newToken) { + apiClient.setDefaultHeader('Authorization', `Bearer ${newToken}`); + } else { + apiClient.removeDefaultHeader('Authorization'); + } + }); +} + +export function useAuth() { + return { + // State + token: computed(() => token.value), + user: computed(() => user.value), + isLoading: computed(() => isLoading.value), + error: computed(() => error.value), + + // Computed + isAuthenticated, + isAdmin, + + // Methods + initializeAuth, + login, + logout, + refreshToken, + getUserInfo, + hasPermission, + setupAuthInterceptor + }; +}