/** * Authentication Service Module * Handles login, logout, token management, and user session state */ (function (window) { 'use strict'; const AUTH_STORAGE_KEYS = { ACCESS_TOKEN: 'gb-access-token', REFRESH_TOKEN: 'gb-refresh-token', TOKEN_EXPIRES: 'gb-token-expires', USER_DATA: 'gb-user-data', REMEMBER_ME: 'gb-remember-me' }; const AUTH_ENDPOINTS = { LOGIN: '/api/auth/login', LOGOUT: '/api/auth/logout', REFRESH: '/api/auth/refresh', CURRENT_USER: '/api/auth/me', VERIFY_2FA: '/api/auth/2fa/verify', RESEND_2FA: '/api/auth/2fa/resend' }; const USER_ENDPOINTS = { LIST: '/api/directory/users/list', CREATE: '/api/directory/users/create', UPDATE: '/api/directory/users/:user_id/update', DELETE: '/api/directory/users/:user_id/delete', PROFILE: '/api/directory/users/:user_id/profile', ROLES: '/api/directory/users/:user_id/roles' }; const GROUP_ENDPOINTS = { LIST: '/api/directory/groups/list', CREATE: '/api/directory/groups/create', UPDATE: '/api/directory/groups/:group_id/update', DELETE: '/api/directory/groups/:group_id/delete', MEMBERS: '/api/directory/groups/:group_id/members', ADD_MEMBER: '/api/directory/groups/:group_id/members/add', REMOVE_MEMBER: '/api/directory/groups/:group_id/members/remove' }; class AuthService { constructor() { this.currentUser = null; this.tokenRefreshTimer = null; this.eventListeners = {}; this.init(); } init() { this.loadStoredUser(); this.setupTokenRefresh(); this.setupInterceptors(); } loadStoredUser() { try { const userData = localStorage.getItem(AUTH_STORAGE_KEYS.USER_DATA); if (userData) { this.currentUser = JSON.parse(userData); } } catch (e) { console.warn('Failed to load stored user data:', e); this.clearAuth(); } } setupTokenRefresh() { const expiresAt = localStorage.getItem(AUTH_STORAGE_KEYS.TOKEN_EXPIRES); if (expiresAt) { const expiresMs = parseInt(expiresAt, 10) - Date.now(); const refreshMs = expiresMs - (5 * 60 * 1000); if (refreshMs > 0) { this.tokenRefreshTimer = setTimeout(() => { this.refreshToken(); }, refreshMs); } else if (expiresMs > 0) { this.refreshToken(); } else { this.handleTokenExpired(); } } } setupInterceptors() { const originalFetch = window.fetch; const self = this; window.fetch = async function (url, options) { options = options || {}; options.headers = options.headers || {}; const token = self.getAccessToken(); if (token && !options.headers['Authorization']) { options.headers['Authorization'] = 'Bearer ' + token; } try { const response = await originalFetch(url, options); if (response.status === 401) { const refreshed = await self.refreshToken(); if (refreshed) { options.headers['Authorization'] = 'Bearer ' + self.getAccessToken(); return originalFetch(url, options); } else { self.handleTokenExpired(); } } return response; } catch (error) { throw error; } }; } async login(email, password, remember) { try { const response = await fetch(AUTH_ENDPOINTS.LOGIN, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: email, password: password, remember: remember || false }) }); const data = await response.json(); if (!response.ok) { throw new Error(data.error || 'Login failed'); } if (data.requires_2fa) { return { success: false, requires_2fa: true, session_token: data.session_token }; } if (data.success && data.access_token) { this.storeTokens(data, remember); await this.fetchCurrentUser(); this.emit('login', this.currentUser); return { success: true, redirect: data.redirect || '/' }; } throw new Error(data.message || 'Login failed'); } catch (error) { console.error('Login error:', error); throw error; } } async verify2FA(sessionToken, code, trustDevice) { try { const response = await fetch(AUTH_ENDPOINTS.VERIFY_2FA, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ session_token: sessionToken, code: code, trust_device: trustDevice || false }) }); const data = await response.json(); if (!response.ok) { throw new Error(data.error || '2FA verification failed'); } if (data.success && data.access_token) { this.storeTokens(data, false); await this.fetchCurrentUser(); this.emit('login', this.currentUser); return { success: true, redirect: data.redirect || '/' }; } throw new Error(data.message || '2FA verification failed'); } catch (error) { console.error('2FA verification error:', error); throw error; } } async resend2FA(sessionToken) { try { const response = await fetch(AUTH_ENDPOINTS.RESEND_2FA, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ session_token: sessionToken }) }); const data = await response.json(); if (!response.ok) { throw new Error(data.error || 'Failed to resend 2FA code'); } return data; } catch (error) { console.error('Resend 2FA error:', error); throw error; } } async logout() { try { const token = this.getAccessToken(); if (token) { await fetch(AUTH_ENDPOINTS.LOGOUT, { method: 'POST', headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' } }); } } catch (error) { console.warn('Logout API call failed:', error); } finally { this.clearAuth(); this.emit('logout'); window.location.href = '/auth/login'; } } async fetchCurrentUser() { try { const token = this.getAccessToken(); if (!token) { return null; } const response = await fetch(AUTH_ENDPOINTS.CURRENT_USER, { headers: { 'Authorization': 'Bearer ' + token } }); if (!response.ok) { if (response.status === 401) { this.handleTokenExpired(); } return null; } const userData = await response.json(); this.currentUser = userData; localStorage.setItem(AUTH_STORAGE_KEYS.USER_DATA, JSON.stringify(userData)); this.emit('userUpdated', userData); return userData; } catch (error) { console.error('Failed to fetch current user:', error); return null; } } async refreshToken() { const refreshToken = localStorage.getItem(AUTH_STORAGE_KEYS.REFRESH_TOKEN); if (!refreshToken) { return false; } try { const response = await fetch(AUTH_ENDPOINTS.REFRESH, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ refresh_token: refreshToken }) }); if (!response.ok) { this.handleTokenExpired(); return false; } const data = await response.json(); if (data.access_token) { this.storeTokens(data, !!localStorage.getItem(AUTH_STORAGE_KEYS.REMEMBER_ME)); return true; } return false; } catch (error) { console.error('Token refresh failed:', error); this.handleTokenExpired(); return false; } } storeTokens(data, remember) { const storage = remember ? localStorage : sessionStorage; if (data.access_token) { localStorage.setItem(AUTH_STORAGE_KEYS.ACCESS_TOKEN, data.access_token); } if (data.refresh_token) { localStorage.setItem(AUTH_STORAGE_KEYS.REFRESH_TOKEN, data.refresh_token); } if (data.expires_in) { const expiresAt = Date.now() + (data.expires_in * 1000); localStorage.setItem(AUTH_STORAGE_KEYS.TOKEN_EXPIRES, expiresAt.toString()); } if (remember) { localStorage.setItem(AUTH_STORAGE_KEYS.REMEMBER_ME, 'true'); } this.setupTokenRefresh(); } clearAuth() { if (this.tokenRefreshTimer) { clearTimeout(this.tokenRefreshTimer); this.tokenRefreshTimer = null; } Object.values(AUTH_STORAGE_KEYS).forEach(key => { localStorage.removeItem(key); sessionStorage.removeItem(key); }); this.currentUser = null; } handleTokenExpired() { this.clearAuth(); this.emit('tokenExpired'); const currentPath = window.location.pathname; if (!currentPath.startsWith('/auth/')) { window.location.href = '/auth/login?expired=1&redirect=' + encodeURIComponent(currentPath); } } getAccessToken() { return localStorage.getItem(AUTH_STORAGE_KEYS.ACCESS_TOKEN) || sessionStorage.getItem(AUTH_STORAGE_KEYS.ACCESS_TOKEN); } isAuthenticated() { const token = this.getAccessToken(); const expiresAt = localStorage.getItem(AUTH_STORAGE_KEYS.TOKEN_EXPIRES); if (!token) { return false; } if (expiresAt && Date.now() > parseInt(expiresAt, 10)) { return false; } return true; } getCurrentUser() { return this.currentUser; } hasRole(role) { if (!this.currentUser || !this.currentUser.roles) { return false; } return this.currentUser.roles.includes(role); } hasAnyRole(roles) { if (!this.currentUser || !this.currentUser.roles) { return false; } return roles.some(role => this.currentUser.roles.includes(role)); } isAdmin() { return this.hasAnyRole(['admin', 'super_admin', 'superadmin']); } on(event, callback) { if (!this.eventListeners[event]) { this.eventListeners[event] = []; } this.eventListeners[event].push(callback); } off(event, callback) { if (!this.eventListeners[event]) { return; } this.eventListeners[event] = this.eventListeners[event].filter(cb => cb !== callback); } emit(event, data) { if (!this.eventListeners[event]) { return; } this.eventListeners[event].forEach(callback => { try { callback(data); } catch (e) { console.error('Event listener error:', e); } }); } } class UserService { constructor(authService) { this.authService = authService; } async listUsers(page, perPage, search) { const params = new URLSearchParams(); if (page) params.append('page', page); if (perPage) params.append('per_page', perPage); if (search) params.append('search', search); const url = USER_ENDPOINTS.LIST + (params.toString() ? '?' + params.toString() : ''); const response = await fetch(url); if (!response.ok) { const error = await response.json(); throw new Error(error.error || 'Failed to list users'); } return response.json(); } async createUser(userData) { const response = await fetch(USER_ENDPOINTS.CREATE, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(userData) }); if (!response.ok) { const error = await response.json(); throw new Error(error.error || 'Failed to create user'); } return response.json(); } async updateUser(userId, userData) { const url = USER_ENDPOINTS.UPDATE.replace(':user_id', userId); const response = await fetch(url, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(userData) }); if (!response.ok) { const error = await response.json(); throw new Error(error.error || 'Failed to update user'); } return response.json(); } async deleteUser(userId) { const url = USER_ENDPOINTS.DELETE.replace(':user_id', userId); const response = await fetch(url, { method: 'DELETE' }); if (!response.ok) { const error = await response.json(); throw new Error(error.error || 'Failed to delete user'); } return response.json(); } async getUserProfile(userId) { const url = USER_ENDPOINTS.PROFILE.replace(':user_id', userId); const response = await fetch(url); if (!response.ok) { const error = await response.json(); throw new Error(error.error || 'Failed to get user profile'); } return response.json(); } async getUserRoles(userId) { const url = USER_ENDPOINTS.ROLES.replace(':user_id', userId); const response = await fetch(url); if (!response.ok) { const error = await response.json(); throw new Error(error.error || 'Failed to get user roles'); } return response.json(); } } class GroupService { constructor(authService) { this.authService = authService; } async listGroups(page, perPage, search) { const params = new URLSearchParams(); if (page) params.append('page', page); if (perPage) params.append('per_page', perPage); if (search) params.append('search', search); const url = GROUP_ENDPOINTS.LIST + (params.toString() ? '?' + params.toString() : ''); const response = await fetch(url); if (!response.ok) { const error = await response.json(); throw new Error(error.error || 'Failed to list groups'); } return response.json(); } async createGroup(groupData) { const response = await fetch(GROUP_ENDPOINTS.CREATE, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(groupData) }); if (!response.ok) { const error = await response.json(); throw new Error(error.error || 'Failed to create group'); } return response.json(); } async updateGroup(groupId, groupData) { const url = GROUP_ENDPOINTS.UPDATE.replace(':group_id', groupId); const response = await fetch(url, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(groupData) }); if (!response.ok) { const error = await response.json(); throw new Error(error.error || 'Failed to update group'); } return response.json(); } async deleteGroup(groupId) { const url = GROUP_ENDPOINTS.DELETE.replace(':group_id', groupId); const response = await fetch(url, { method: 'DELETE' }); if (!response.ok) { const error = await response.json(); throw new Error(error.error || 'Failed to delete group'); } return response.json(); } async getGroupMembers(groupId) { const url = GROUP_ENDPOINTS.MEMBERS.replace(':group_id', groupId); const response = await fetch(url); if (!response.ok) { const error = await response.json(); throw new Error(error.error || 'Failed to get group members'); } return response.json(); } async addGroupMember(groupId, userId, roles) { const url = GROUP_ENDPOINTS.ADD_MEMBER.replace(':group_id', groupId); const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ user_id: userId, roles: roles || [] }) }); if (!response.ok) { const error = await response.json(); throw new Error(error.error || 'Failed to add group member'); } return response.json(); } async removeGroupMember(groupId, userId) { const url = GROUP_ENDPOINTS.REMOVE_MEMBER.replace(':group_id', groupId); const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ user_id: userId }) }); if (!response.ok) { const error = await response.json(); throw new Error(error.error || 'Failed to remove group member'); } return response.json(); } } const authService = new AuthService(); const userService = new UserService(authService); const groupService = new GroupService(authService); window.AuthService = authService; window.UserService = userService; window.GroupService = groupService; window.GBAuth = { service: authService, users: userService, groups: groupService, login: function (email, password, remember) { return authService.login(email, password, remember); }, logout: function () { return authService.logout(); }, isAuthenticated: function () { return authService.isAuthenticated(); }, getCurrentUser: function () { return authService.getCurrentUser(); }, hasRole: function (role) { return authService.hasRole(role); }, isAdmin: function () { return authService.isAdmin(); }, on: function (event, callback) { authService.on(event, callback); }, off: function (event, callback) { authService.off(event, callback); } }; document.addEventListener('DOMContentLoaded', function () { if (authService.isAuthenticated()) { authService.fetchCurrentUser().then(function (user) { if (user) { document.body.classList.add('authenticated'); if (authService.isAdmin()) { document.body.classList.add('is-admin'); } } }); } }); })(window);