botui/ui/suite/js/auth-service.js

702 lines
22 KiB
JavaScript
Raw Normal View History

/**
* 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);