SAAAA
This commit is contained in:
parent
156125e1db
commit
5246a6151d
@ -737,6 +737,51 @@ async def dashboard_logout(request: Request):
|
||||
log.info(f"Dashboard: User {user_id} logged out.")
|
||||
return
|
||||
|
||||
@dashboard_api_app.get("/auth/status", tags=["Dashboard Authentication"])
|
||||
async def dashboard_auth_status(request: Request):
|
||||
"""Checks if the user is authenticated in the dashboard session."""
|
||||
user_id = request.session.get('user_id')
|
||||
username = request.session.get('username')
|
||||
access_token = request.session.get('access_token')
|
||||
|
||||
if not user_id or not username or not access_token:
|
||||
log.debug("Dashboard: Auth status check - user not authenticated")
|
||||
return {"authenticated": False, "message": "User is not authenticated"}
|
||||
|
||||
# Verify the token is still valid with Discord
|
||||
try:
|
||||
if not http_session:
|
||||
log.error("Dashboard: aiohttp session not initialized.")
|
||||
return {"authenticated": False, "message": "Internal server error: HTTP session not ready"}
|
||||
|
||||
user_headers = {'Authorization': f'Bearer {access_token}'}
|
||||
async with http_session.get(DISCORD_USER_URL, headers=user_headers) as resp:
|
||||
if resp.status != 200:
|
||||
log.warning(f"Dashboard: Auth status check - invalid token for user {user_id}")
|
||||
# Clear the invalid session
|
||||
request.session.clear()
|
||||
return {"authenticated": False, "message": "Discord token invalid or expired"}
|
||||
|
||||
# Token is valid, get the latest user data
|
||||
user_data = await resp.json()
|
||||
|
||||
# Update session with latest data
|
||||
request.session['username'] = user_data.get('username')
|
||||
request.session['avatar'] = user_data.get('avatar')
|
||||
|
||||
log.debug(f"Dashboard: Auth status check - user {user_id} is authenticated")
|
||||
return {
|
||||
"authenticated": True,
|
||||
"user": {
|
||||
"id": user_id,
|
||||
"username": user_data.get('username'),
|
||||
"avatar": user_data.get('avatar')
|
||||
}
|
||||
}
|
||||
except Exception as e:
|
||||
log.exception(f"Dashboard: Error checking auth status: {e}")
|
||||
return {"authenticated": False, "message": f"Error checking auth status: {str(e)}"}
|
||||
|
||||
# --- Dashboard User Endpoints ---
|
||||
@dashboard_api_app.get("/user/me", tags=["Dashboard User"])
|
||||
async def dashboard_get_user_me(current_user: dict = Depends(get_dashboard_user)):
|
||||
@ -745,7 +790,28 @@ async def dashboard_get_user_me(current_user: dict = Depends(get_dashboard_user)
|
||||
# del user_info['access_token'] # Optional: Don't expose token to frontend
|
||||
return user_info
|
||||
|
||||
@dashboard_api_app.get("/auth/user", tags=["Dashboard Authentication"])
|
||||
async def dashboard_get_auth_user(request: Request):
|
||||
"""Returns information about the currently logged-in dashboard user for the frontend."""
|
||||
user_id = request.session.get('user_id')
|
||||
username = request.session.get('username')
|
||||
avatar = request.session.get('avatar')
|
||||
|
||||
if not user_id or not username:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Not authenticated",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
return {
|
||||
"id": user_id,
|
||||
"username": username,
|
||||
"avatar": avatar
|
||||
}
|
||||
|
||||
@dashboard_api_app.get("/user/guilds", tags=["Dashboard User"])
|
||||
@dashboard_api_app.get("/guilds", tags=["Dashboard Guild Settings"])
|
||||
async def dashboard_get_user_guilds(current_user: dict = Depends(get_dashboard_user)):
|
||||
"""Returns a list of guilds the user is an administrator in AND the bot is also in."""
|
||||
global http_session # Use the global aiohttp session
|
||||
|
@ -6,16 +6,16 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Initialize modals
|
||||
Modal.init();
|
||||
|
||||
|
||||
// Initialize sidebar toggle
|
||||
initSidebar();
|
||||
|
||||
|
||||
// Initialize authentication
|
||||
initAuth();
|
||||
|
||||
|
||||
// Initialize tabs
|
||||
initTabs();
|
||||
|
||||
|
||||
// Initialize dropdowns
|
||||
initDropdowns();
|
||||
});
|
||||
@ -26,23 +26,23 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
function initSidebar() {
|
||||
const sidebarToggle = document.getElementById('sidebar-toggle');
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
|
||||
|
||||
if (sidebarToggle && sidebar) {
|
||||
sidebarToggle.addEventListener('click', () => {
|
||||
sidebar.classList.toggle('show');
|
||||
});
|
||||
|
||||
|
||||
// Close sidebar when clicking outside on mobile
|
||||
document.addEventListener('click', (event) => {
|
||||
if (window.innerWidth <= 768 &&
|
||||
sidebar.classList.contains('show') &&
|
||||
!sidebar.contains(event.target) &&
|
||||
if (window.innerWidth <= 768 &&
|
||||
sidebar.classList.contains('show') &&
|
||||
!sidebar.contains(event.target) &&
|
||||
event.target !== sidebarToggle) {
|
||||
sidebar.classList.remove('show');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Set active nav item based on current page
|
||||
const currentPath = window.location.pathname;
|
||||
document.querySelectorAll('.nav-item').forEach(item => {
|
||||
@ -61,54 +61,100 @@ function initAuth() {
|
||||
const logoutButton = document.getElementById('logout-button');
|
||||
const authSection = document.getElementById('auth-section');
|
||||
const dashboardSection = document.getElementById('dashboard-container');
|
||||
|
||||
|
||||
// Check authentication status
|
||||
checkAuthStatus();
|
||||
|
||||
|
||||
// Login button event
|
||||
if (loginButton) {
|
||||
loginButton.addEventListener('click', () => {
|
||||
// Show loading state
|
||||
loginButton.disabled = true;
|
||||
loginButton.classList.add('btn-loading');
|
||||
|
||||
// Redirect to login page
|
||||
window.location.href = '/dashboard/api/auth/login';
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Logout button event
|
||||
if (logoutButton) {
|
||||
logoutButton.addEventListener('click', () => {
|
||||
// Show loading state
|
||||
logoutButton.disabled = true;
|
||||
logoutButton.classList.add('btn-loading');
|
||||
|
||||
// Clear session
|
||||
fetch('/dashboard/api/auth/logout', { method: 'POST' })
|
||||
fetch('/dashboard/api/auth/logout', {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin' // Important for cookies
|
||||
})
|
||||
.then(() => {
|
||||
// Redirect to login page
|
||||
window.location.reload();
|
||||
Toast.success('Logged out successfully');
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1000);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Logout error:', error);
|
||||
Toast.error('Failed to logout. Please try again.');
|
||||
logoutButton.disabled = false;
|
||||
logoutButton.classList.remove('btn-loading');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if user is authenticated
|
||||
*/
|
||||
function checkAuthStatus() {
|
||||
fetch('/dashboard/api/auth/status')
|
||||
.then(response => response.json())
|
||||
// Show loading indicator
|
||||
const loadingContainer = document.createElement('div');
|
||||
loadingContainer.className = 'loading-container';
|
||||
loadingContainer.innerHTML = '<div class="loading-spinner"></div>';
|
||||
loadingContainer.style.position = 'fixed';
|
||||
loadingContainer.style.top = '50%';
|
||||
loadingContainer.style.left = '50%';
|
||||
loadingContainer.style.transform = 'translate(-50%, -50%)';
|
||||
document.body.appendChild(loadingContainer);
|
||||
|
||||
fetch('/dashboard/api/auth/status', {
|
||||
credentials: 'same-origin' // Important for cookies
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`Status check failed: ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
console.log('Auth status:', data);
|
||||
|
||||
if (data.authenticated) {
|
||||
// User is authenticated, show dashboard
|
||||
if (authSection) authSection.style.display = 'none';
|
||||
if (dashboardSection) dashboardSection.style.display = 'block';
|
||||
|
||||
// Load user info
|
||||
loadUserInfo();
|
||||
|
||||
|
||||
// If user data is included in the response, use it
|
||||
if (data.user) {
|
||||
updateUserDisplay(data.user);
|
||||
} else {
|
||||
// Otherwise load user info separately
|
||||
loadUserInfo();
|
||||
}
|
||||
|
||||
// Load initial data
|
||||
loadDashboardData();
|
||||
} else {
|
||||
// User is not authenticated, show login
|
||||
if (authSection) authSection.style.display = 'block';
|
||||
if (dashboardSection) dashboardSection.style.display = 'none';
|
||||
|
||||
// Show message if provided
|
||||
if (data.message) {
|
||||
console.log('Auth message:', data.message);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
@ -116,38 +162,69 @@ function initAuth() {
|
||||
// Assume not authenticated on error
|
||||
if (authSection) authSection.style.display = 'block';
|
||||
if (dashboardSection) dashboardSection.style.display = 'none';
|
||||
|
||||
// Show error toast
|
||||
Toast.error('Failed to check authentication status. Please try again.');
|
||||
})
|
||||
.finally(() => {
|
||||
// Remove loading indicator
|
||||
document.body.removeChild(loadingContainer);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Load user information
|
||||
*/
|
||||
function loadUserInfo() {
|
||||
fetch('/dashboard/api/auth/user')
|
||||
.then(response => response.json())
|
||||
.then(user => {
|
||||
// Update username display
|
||||
const usernameSpan = document.getElementById('username');
|
||||
if (usernameSpan) {
|
||||
usernameSpan.textContent = user.username;
|
||||
}
|
||||
|
||||
// Update avatar if available
|
||||
const userAvatar = document.getElementById('user-avatar');
|
||||
if (userAvatar) {
|
||||
if (user.avatar) {
|
||||
userAvatar.style.backgroundImage = `url(https://cdn.discordapp.com/avatars/${user.id}/${user.avatar}.png)`;
|
||||
userAvatar.textContent = '';
|
||||
} else {
|
||||
// Set initials as fallback
|
||||
userAvatar.textContent = user.username.substring(0, 1).toUpperCase();
|
||||
fetch('/dashboard/api/auth/user', {
|
||||
credentials: 'same-origin' // Important for cookies
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
if (response.status === 401) {
|
||||
// User is not authenticated, show login
|
||||
if (authSection) authSection.style.display = 'block';
|
||||
if (dashboardSection) dashboardSection.style.display = 'none';
|
||||
throw new Error('Not authenticated');
|
||||
}
|
||||
throw new Error(`Failed to load user info: ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(user => {
|
||||
updateUserDisplay(user);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error loading user info:', error);
|
||||
if (error.message !== 'Not authenticated') {
|
||||
Toast.error('Failed to load user information');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update user display with user data
|
||||
* @param {Object} user - User data object
|
||||
*/
|
||||
function updateUserDisplay(user) {
|
||||
// Update username display
|
||||
const usernameSpan = document.getElementById('username');
|
||||
if (usernameSpan) {
|
||||
usernameSpan.textContent = user.username;
|
||||
}
|
||||
|
||||
// Update avatar if available
|
||||
const userAvatar = document.getElementById('user-avatar');
|
||||
if (userAvatar) {
|
||||
if (user.avatar) {
|
||||
userAvatar.style.backgroundImage = `url(https://cdn.discordapp.com/avatars/${user.id}/${user.avatar}.png)`;
|
||||
userAvatar.textContent = '';
|
||||
} else {
|
||||
// Set initials as fallback
|
||||
userAvatar.textContent = user.username.substring(0, 1).toUpperCase();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -156,25 +233,25 @@ function initAuth() {
|
||||
function initTabs() {
|
||||
document.querySelectorAll('.tabs').forEach(tabContainer => {
|
||||
const tabs = tabContainer.querySelectorAll('.tab');
|
||||
|
||||
|
||||
tabs.forEach(tab => {
|
||||
tab.addEventListener('click', () => {
|
||||
// Get target content ID
|
||||
const target = tab.getAttribute('data-target');
|
||||
if (!target) return;
|
||||
|
||||
|
||||
// Remove active class from all tabs
|
||||
tabs.forEach(t => t.classList.remove('active'));
|
||||
|
||||
|
||||
// Add active class to clicked tab
|
||||
tab.classList.add('active');
|
||||
|
||||
|
||||
// Hide all tab content
|
||||
const tabContents = document.querySelectorAll('.tab-content');
|
||||
tabContents.forEach(content => {
|
||||
content.classList.remove('active');
|
||||
});
|
||||
|
||||
|
||||
// Show target content
|
||||
const targetContent = document.getElementById(target);
|
||||
if (targetContent) {
|
||||
@ -193,22 +270,22 @@ function initDropdowns() {
|
||||
toggle.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
|
||||
const dropdown = toggle.closest('.dropdown');
|
||||
const menu = dropdown.querySelector('.dropdown-menu');
|
||||
|
||||
|
||||
// Close all other dropdowns
|
||||
document.querySelectorAll('.dropdown-menu.show').forEach(openMenu => {
|
||||
if (openMenu !== menu) {
|
||||
openMenu.classList.remove('show');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Toggle this dropdown
|
||||
menu.classList.toggle('show');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// Close dropdowns when clicking outside
|
||||
document.addEventListener('click', (event) => {
|
||||
if (!event.target.closest('.dropdown')) {
|
||||
@ -225,7 +302,7 @@ function initDropdowns() {
|
||||
function loadDashboardData() {
|
||||
// Load guilds for server select
|
||||
loadGuilds();
|
||||
|
||||
|
||||
// Load global settings
|
||||
loadGlobalSettings();
|
||||
}
|
||||
@ -236,17 +313,17 @@ function loadDashboardData() {
|
||||
function loadGuilds() {
|
||||
const guildSelect = document.getElementById('guild-select');
|
||||
if (!guildSelect) return;
|
||||
|
||||
|
||||
// Show loading state
|
||||
guildSelect.disabled = true;
|
||||
guildSelect.innerHTML = '<option value="">Loading servers...</option>';
|
||||
|
||||
|
||||
// Fetch guilds from API
|
||||
API.get('/dashboard/api/guilds')
|
||||
.then(guilds => {
|
||||
// Clear loading state
|
||||
guildSelect.innerHTML = '<option value="">--Please choose a server--</option>';
|
||||
|
||||
|
||||
// Add guilds to select
|
||||
guilds.forEach(guild => {
|
||||
const option = document.createElement('option');
|
||||
@ -254,10 +331,10 @@ function loadGuilds() {
|
||||
option.textContent = guild.name;
|
||||
guildSelect.appendChild(option);
|
||||
});
|
||||
|
||||
|
||||
// Enable select
|
||||
guildSelect.disabled = false;
|
||||
|
||||
|
||||
// Add change event
|
||||
guildSelect.addEventListener('change', () => {
|
||||
const guildId = guildSelect.value;
|
||||
@ -287,29 +364,29 @@ function loadGuilds() {
|
||||
function loadGuildSettings(guildId) {
|
||||
const settingsForm = document.getElementById('settings-form');
|
||||
if (!settingsForm) return;
|
||||
|
||||
|
||||
// Show loading state
|
||||
const loadingContainer = document.createElement('div');
|
||||
loadingContainer.className = 'loading-container';
|
||||
loadingContainer.innerHTML = '<div class="loading-spinner"></div><p>Loading server settings...</p>';
|
||||
loadingContainer.style.textAlign = 'center';
|
||||
loadingContainer.style.padding = '2rem';
|
||||
|
||||
|
||||
settingsForm.style.display = 'none';
|
||||
settingsForm.parentNode.insertBefore(loadingContainer, settingsForm);
|
||||
|
||||
|
||||
// Fetch guild settings from API
|
||||
API.get(`/dashboard/api/guilds/${guildId}/settings`)
|
||||
.then(settings => {
|
||||
// Remove loading container
|
||||
loadingContainer.remove();
|
||||
|
||||
|
||||
// Show settings form
|
||||
settingsForm.style.display = 'block';
|
||||
|
||||
|
||||
// Populate form with settings
|
||||
populateGuildSettings(settings);
|
||||
|
||||
|
||||
// Load additional data
|
||||
loadGuildChannels(guildId);
|
||||
loadGuildRoles(guildId);
|
||||
@ -332,43 +409,43 @@ function populateGuildSettings(settings) {
|
||||
if (prefixInput) {
|
||||
prefixInput.value = settings.prefix || '!';
|
||||
}
|
||||
|
||||
|
||||
// Welcome settings
|
||||
const welcomeChannel = document.getElementById('welcome-channel');
|
||||
const welcomeMessage = document.getElementById('welcome-message');
|
||||
|
||||
|
||||
if (welcomeChannel && welcomeMessage) {
|
||||
welcomeChannel.value = settings.welcome_channel_id || '';
|
||||
welcomeMessage.value = settings.welcome_message || '';
|
||||
}
|
||||
|
||||
|
||||
// Goodbye settings
|
||||
const goodbyeChannel = document.getElementById('goodbye-channel');
|
||||
const goodbyeMessage = document.getElementById('goodbye-message');
|
||||
|
||||
|
||||
if (goodbyeChannel && goodbyeMessage) {
|
||||
goodbyeChannel.value = settings.goodbye_channel_id || '';
|
||||
goodbyeMessage.value = settings.goodbye_message || '';
|
||||
}
|
||||
|
||||
|
||||
// Cogs (modules)
|
||||
const cogsList = document.getElementById('cogs-list');
|
||||
if (cogsList && settings.enabled_cogs) {
|
||||
cogsList.innerHTML = '';
|
||||
|
||||
|
||||
Object.entries(settings.enabled_cogs).forEach(([cogName, enabled]) => {
|
||||
const cogDiv = document.createElement('div');
|
||||
|
||||
|
||||
const checkbox = document.createElement('input');
|
||||
checkbox.type = 'checkbox';
|
||||
checkbox.id = `cog-${cogName}`;
|
||||
checkbox.name = `cog-${cogName}`;
|
||||
checkbox.checked = enabled;
|
||||
|
||||
|
||||
const label = document.createElement('label');
|
||||
label.htmlFor = `cog-${cogName}`;
|
||||
label.textContent = cogName;
|
||||
|
||||
|
||||
cogDiv.appendChild(checkbox);
|
||||
cogDiv.appendChild(label);
|
||||
cogsList.appendChild(cogDiv);
|
||||
@ -383,32 +460,32 @@ function populateGuildSettings(settings) {
|
||||
function loadGuildChannels(guildId) {
|
||||
const welcomeChannelSelect = document.getElementById('welcome-channel-select');
|
||||
const goodbyeChannelSelect = document.getElementById('goodbye-channel-select');
|
||||
|
||||
|
||||
if (!welcomeChannelSelect && !goodbyeChannelSelect) return;
|
||||
|
||||
|
||||
// Fetch channels from API
|
||||
API.get(`/dashboard/api/guilds/${guildId}/channels`)
|
||||
.then(channels => {
|
||||
// Filter text channels
|
||||
const textChannels = channels.filter(channel => channel.type === 0);
|
||||
|
||||
|
||||
// Populate welcome channel select
|
||||
if (welcomeChannelSelect) {
|
||||
welcomeChannelSelect.innerHTML = '<option value="">-- Select Channel --</option>';
|
||||
|
||||
|
||||
textChannels.forEach(channel => {
|
||||
const option = document.createElement('option');
|
||||
option.value = channel.id;
|
||||
option.textContent = `#${channel.name}`;
|
||||
welcomeChannelSelect.appendChild(option);
|
||||
});
|
||||
|
||||
|
||||
// Set current value if available
|
||||
const welcomeChannelInput = document.getElementById('welcome-channel');
|
||||
if (welcomeChannelInput && welcomeChannelInput.value) {
|
||||
welcomeChannelSelect.value = welcomeChannelInput.value;
|
||||
}
|
||||
|
||||
|
||||
// Add change event
|
||||
welcomeChannelSelect.addEventListener('change', () => {
|
||||
if (welcomeChannelInput) {
|
||||
@ -416,24 +493,24 @@ function loadGuildChannels(guildId) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Populate goodbye channel select
|
||||
if (goodbyeChannelSelect) {
|
||||
goodbyeChannelSelect.innerHTML = '<option value="">-- Select Channel --</option>';
|
||||
|
||||
|
||||
textChannels.forEach(channel => {
|
||||
const option = document.createElement('option');
|
||||
option.value = channel.id;
|
||||
option.textContent = `#${channel.name}`;
|
||||
goodbyeChannelSelect.appendChild(option);
|
||||
});
|
||||
|
||||
|
||||
// Set current value if available
|
||||
const goodbyeChannelInput = document.getElementById('goodbye-channel');
|
||||
if (goodbyeChannelInput && goodbyeChannelInput.value) {
|
||||
goodbyeChannelSelect.value = goodbyeChannelInput.value;
|
||||
}
|
||||
|
||||
|
||||
// Add change event
|
||||
goodbyeChannelSelect.addEventListener('change', () => {
|
||||
if (goodbyeChannelInput) {
|
||||
@ -455,22 +532,22 @@ function loadGuildChannels(guildId) {
|
||||
function loadGuildRoles(guildId) {
|
||||
const roleSelect = document.getElementById('role-select');
|
||||
if (!roleSelect) return;
|
||||
|
||||
|
||||
// Fetch roles from API
|
||||
API.get(`/dashboard/api/guilds/${guildId}/roles`)
|
||||
.then(roles => {
|
||||
roleSelect.innerHTML = '<option value="">-- Select Role --</option>';
|
||||
|
||||
|
||||
roles.forEach(role => {
|
||||
const option = document.createElement('option');
|
||||
option.value = role.id;
|
||||
option.textContent = role.name;
|
||||
|
||||
|
||||
// Set color if available
|
||||
if (role.color) {
|
||||
option.style.color = `#${role.color.toString(16).padStart(6, '0')}`;
|
||||
}
|
||||
|
||||
|
||||
roleSelect.appendChild(option);
|
||||
});
|
||||
})
|
||||
@ -487,25 +564,25 @@ function loadGuildRoles(guildId) {
|
||||
function loadGuildCommands(guildId) {
|
||||
const commandSelect = document.getElementById('command-select');
|
||||
if (!commandSelect) return;
|
||||
|
||||
|
||||
// Fetch commands from API
|
||||
API.get(`/dashboard/api/guilds/${guildId}/commands`)
|
||||
.then(commands => {
|
||||
commandSelect.innerHTML = '<option value="">-- Select Command --</option>';
|
||||
|
||||
|
||||
commands.forEach(command => {
|
||||
const option = document.createElement('option');
|
||||
option.value = command.name;
|
||||
option.textContent = command.name;
|
||||
|
||||
|
||||
// Add description as title attribute
|
||||
if (command.description) {
|
||||
option.title = command.description;
|
||||
}
|
||||
|
||||
|
||||
commandSelect.appendChild(option);
|
||||
});
|
||||
|
||||
|
||||
// Load command permissions
|
||||
loadCommandPermissions(guildId);
|
||||
})
|
||||
@ -522,48 +599,48 @@ function loadGuildCommands(guildId) {
|
||||
function loadCommandPermissions(guildId) {
|
||||
const currentPerms = document.getElementById('current-perms');
|
||||
if (!currentPerms) return;
|
||||
|
||||
|
||||
// Show loading state
|
||||
currentPerms.innerHTML = '<div class="loading-spinner-container"><div class="loading-spinner loading-spinner-sm"></div></div>';
|
||||
|
||||
|
||||
// Fetch command permissions from API
|
||||
API.get(`/dashboard/api/guilds/${guildId}/command-permissions`)
|
||||
.then(permissions => {
|
||||
// Clear loading state
|
||||
currentPerms.innerHTML = '';
|
||||
|
||||
|
||||
if (permissions.length === 0) {
|
||||
currentPerms.innerHTML = '<div class="text-gray">No custom permissions set.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Group permissions by command
|
||||
const permsByCommand = {};
|
||||
|
||||
|
||||
permissions.forEach(perm => {
|
||||
if (!permsByCommand[perm.command]) {
|
||||
permsByCommand[perm.command] = [];
|
||||
}
|
||||
|
||||
|
||||
permsByCommand[perm.command].push(perm);
|
||||
});
|
||||
|
||||
|
||||
// Create permission elements
|
||||
Object.entries(permsByCommand).forEach(([command, perms]) => {
|
||||
const commandDiv = document.createElement('div');
|
||||
commandDiv.className = 'command-perms';
|
||||
commandDiv.innerHTML = `<div class="command-name">${command}</div>`;
|
||||
|
||||
|
||||
const rolesList = document.createElement('div');
|
||||
rolesList.className = 'roles-list';
|
||||
|
||||
|
||||
perms.forEach(perm => {
|
||||
const roleSpan = document.createElement('span');
|
||||
roleSpan.className = 'role-badge';
|
||||
roleSpan.textContent = perm.role_name;
|
||||
roleSpan.dataset.roleId = perm.role_id;
|
||||
roleSpan.dataset.command = command;
|
||||
|
||||
|
||||
// Add remove button
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'role-remove';
|
||||
@ -571,11 +648,11 @@ function loadCommandPermissions(guildId) {
|
||||
removeBtn.addEventListener('click', () => {
|
||||
removeCommandPermission(guildId, command, perm.role_id);
|
||||
});
|
||||
|
||||
|
||||
roleSpan.appendChild(removeBtn);
|
||||
rolesList.appendChild(roleSpan);
|
||||
});
|
||||
|
||||
|
||||
commandDiv.appendChild(rolesList);
|
||||
currentPerms.appendChild(commandDiv);
|
||||
});
|
||||
@ -595,17 +672,17 @@ function loadCommandPermissions(guildId) {
|
||||
function addCommandPermission(guildId, command, roleId) {
|
||||
const addPermButton = document.getElementById('add-perm-button');
|
||||
const permsFeedback = document.getElementById('perms-feedback');
|
||||
|
||||
|
||||
if (!command || !roleId) {
|
||||
permsFeedback.textContent = 'Please select both a command and a role.';
|
||||
permsFeedback.className = 'error';
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Show loading state
|
||||
addPermButton.disabled = true;
|
||||
addPermButton.classList.add('btn-loading');
|
||||
|
||||
|
||||
// Send request to API
|
||||
API.post(`/dashboard/api/guilds/${guildId}/command-permissions`, {
|
||||
command,
|
||||
@ -615,10 +692,10 @@ function addCommandPermission(guildId, command, roleId) {
|
||||
// Show success message
|
||||
permsFeedback.textContent = 'Permission added successfully.';
|
||||
permsFeedback.className = '';
|
||||
|
||||
|
||||
// Reload permissions
|
||||
loadCommandPermissions(guildId);
|
||||
|
||||
|
||||
// Reset form
|
||||
document.getElementById('command-select').value = '';
|
||||
document.getElementById('role-select').value = '';
|
||||
@ -647,7 +724,7 @@ function removeCommandPermission(guildId, command, roleId) {
|
||||
.then(() => {
|
||||
// Show success message
|
||||
Toast.success('Permission removed successfully.');
|
||||
|
||||
|
||||
// Reload permissions
|
||||
loadCommandPermissions(guildId);
|
||||
})
|
||||
@ -663,7 +740,7 @@ function removeCommandPermission(guildId, command, roleId) {
|
||||
function loadGlobalSettings() {
|
||||
const aiSettingsSection = document.getElementById('ai-settings-section');
|
||||
if (!aiSettingsSection) return;
|
||||
|
||||
|
||||
// Fetch global settings from API
|
||||
API.get('/dashboard/api/settings')
|
||||
.then(settings => {
|
||||
@ -672,44 +749,44 @@ function loadGlobalSettings() {
|
||||
if (modelSelect && settings.model) {
|
||||
modelSelect.value = settings.model;
|
||||
}
|
||||
|
||||
|
||||
// Populate temperature slider
|
||||
const temperatureSlider = document.getElementById('ai-temperature');
|
||||
const temperatureValue = document.getElementById('temperature-value');
|
||||
if (temperatureSlider && temperatureValue && settings.temperature) {
|
||||
temperatureSlider.value = settings.temperature;
|
||||
temperatureValue.textContent = settings.temperature;
|
||||
|
||||
|
||||
// Add input event for live update
|
||||
temperatureSlider.addEventListener('input', () => {
|
||||
temperatureValue.textContent = temperatureSlider.value;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Populate max tokens
|
||||
const maxTokensInput = document.getElementById('ai-max-tokens');
|
||||
if (maxTokensInput && settings.max_tokens) {
|
||||
maxTokensInput.value = settings.max_tokens;
|
||||
}
|
||||
|
||||
|
||||
// Populate character settings
|
||||
const characterInput = document.getElementById('ai-character');
|
||||
const characterInfoInput = document.getElementById('ai-character-info');
|
||||
|
||||
|
||||
if (characterInput && settings.character) {
|
||||
characterInput.value = settings.character;
|
||||
}
|
||||
|
||||
|
||||
if (characterInfoInput && settings.character_info) {
|
||||
characterInfoInput.value = settings.character_info;
|
||||
}
|
||||
|
||||
|
||||
// Populate system prompt
|
||||
const systemPromptInput = document.getElementById('ai-system-prompt');
|
||||
if (systemPromptInput && settings.system_message) {
|
||||
systemPromptInput.value = settings.system_message;
|
||||
}
|
||||
|
||||
|
||||
// Populate custom instructions
|
||||
const customInstructionsInput = document.getElementById('ai-custom-instructions');
|
||||
if (customInstructionsInput && settings.custom_instructions) {
|
||||
|
@ -5,7 +5,7 @@
|
||||
// Toast notification system
|
||||
const Toast = {
|
||||
container: null,
|
||||
|
||||
|
||||
/**
|
||||
* Initialize the toast container
|
||||
*/
|
||||
@ -17,7 +17,7 @@ const Toast = {
|
||||
document.body.appendChild(this.container);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Show a toast notification
|
||||
* @param {string} message - The message to display
|
||||
@ -27,11 +27,11 @@ const Toast = {
|
||||
*/
|
||||
show(message, type = 'info', title = '', duration = 5000) {
|
||||
this.init();
|
||||
|
||||
|
||||
// Create toast element
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `toast toast-${type}`;
|
||||
|
||||
|
||||
// Create icon based on type
|
||||
let iconSvg = '';
|
||||
switch (type) {
|
||||
@ -47,7 +47,7 @@ const Toast = {
|
||||
default: // info
|
||||
iconSvg = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#5865F2" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg>';
|
||||
}
|
||||
|
||||
|
||||
// Set toast content
|
||||
toast.innerHTML = `
|
||||
<div class="toast-icon">${iconSvg}</div>
|
||||
@ -57,22 +57,22 @@ const Toast = {
|
||||
</div>
|
||||
<button class="toast-close">×</button>
|
||||
`;
|
||||
|
||||
|
||||
// Add to container
|
||||
this.container.appendChild(toast);
|
||||
|
||||
|
||||
// Add close event
|
||||
const closeBtn = toast.querySelector('.toast-close');
|
||||
closeBtn.addEventListener('click', () => this.hide(toast));
|
||||
|
||||
|
||||
// Auto-hide after duration
|
||||
if (duration) {
|
||||
setTimeout(() => this.hide(toast), duration);
|
||||
}
|
||||
|
||||
|
||||
return toast;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Hide a toast notification
|
||||
* @param {HTMLElement} toast - The toast element to hide
|
||||
@ -85,7 +85,7 @@ const Toast = {
|
||||
}
|
||||
}, 300); // Match animation duration
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Show a success toast
|
||||
* @param {string} message - The message to display
|
||||
@ -94,7 +94,7 @@ const Toast = {
|
||||
success(message, title = 'Success') {
|
||||
return this.show(message, 'success', title);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Show an error toast
|
||||
* @param {string} message - The message to display
|
||||
@ -103,7 +103,7 @@ const Toast = {
|
||||
error(message, title = 'Error') {
|
||||
return this.show(message, 'error', title);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Show a warning toast
|
||||
* @param {string} message - The message to display
|
||||
@ -112,7 +112,7 @@ const Toast = {
|
||||
warning(message, title = 'Warning') {
|
||||
return this.show(message, 'warning', title);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Show an info toast
|
||||
* @param {string} message - The message to display
|
||||
@ -136,7 +136,10 @@ const API = {
|
||||
// Set default headers
|
||||
options.headers = options.headers || {};
|
||||
options.headers['Content-Type'] = options.headers['Content-Type'] || 'application/json';
|
||||
|
||||
|
||||
// Always include credentials for session cookies
|
||||
options.credentials = options.credentials || 'same-origin';
|
||||
|
||||
// Add loading state
|
||||
if (loadingElement) {
|
||||
if (loadingElement.tagName === 'BUTTON') {
|
||||
@ -149,23 +152,25 @@ const API = {
|
||||
overlay = document.createElement('div');
|
||||
overlay.className = 'loading-overlay';
|
||||
overlay.innerHTML = '<div class="loading-spinner"></div>';
|
||||
|
||||
|
||||
// Make sure the element has position relative for absolute positioning
|
||||
const computedStyle = window.getComputedStyle(loadingElement);
|
||||
if (computedStyle.position === 'static') {
|
||||
loadingElement.style.position = 'relative';
|
||||
}
|
||||
|
||||
|
||||
loadingElement.appendChild(overlay);
|
||||
} else {
|
||||
overlay.style.display = 'flex';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
console.log(`API Request: ${options.method || 'GET'} ${url}`);
|
||||
const response = await fetch(url, options);
|
||||
|
||||
console.log(`API Response: ${response.status} ${response.statusText}`);
|
||||
|
||||
// Parse JSON response
|
||||
let data;
|
||||
const contentType = response.headers.get('content-type');
|
||||
@ -174,7 +179,7 @@ const API = {
|
||||
} else {
|
||||
data = await response.text();
|
||||
}
|
||||
|
||||
|
||||
// Remove loading state
|
||||
if (loadingElement) {
|
||||
if (loadingElement.tagName === 'BUTTON') {
|
||||
@ -187,15 +192,28 @@ const API = {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Handle error responses
|
||||
if (!response.ok) {
|
||||
const error = new Error(data.message || data.error || 'API request failed');
|
||||
const errorMessage = data.detail || data.message || data.error || 'API request failed';
|
||||
console.error(`API Error (${response.status}): ${errorMessage}`, data);
|
||||
|
||||
const error = new Error(errorMessage);
|
||||
error.status = response.status;
|
||||
error.data = data;
|
||||
|
||||
// Handle authentication errors
|
||||
if (response.status === 401) {
|
||||
console.log('Authentication error detected, redirecting to login');
|
||||
// Redirect to login after a short delay to show the error
|
||||
setTimeout(() => {
|
||||
window.location.href = '/dashboard/api/auth/login';
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
// Remove loading state
|
||||
@ -210,14 +228,14 @@ const API = {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Show error toast
|
||||
Toast.error(error.message || 'An error occurred');
|
||||
|
||||
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Make a GET request
|
||||
* @param {string} url - The API endpoint URL
|
||||
@ -227,7 +245,7 @@ const API = {
|
||||
async get(url, loadingElement = null) {
|
||||
return this.request(url, { method: 'GET' }, loadingElement);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Make a POST request
|
||||
* @param {string} url - The API endpoint URL
|
||||
@ -245,7 +263,7 @@ const API = {
|
||||
loadingElement
|
||||
);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Make a PUT request
|
||||
* @param {string} url - The API endpoint URL
|
||||
@ -263,7 +281,7 @@ const API = {
|
||||
loadingElement
|
||||
);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Make a DELETE request
|
||||
* @param {string} url - The API endpoint URL
|
||||
@ -288,7 +306,7 @@ const Modal = {
|
||||
document.body.style.overflow = 'hidden'; // Prevent scrolling
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Close a modal
|
||||
* @param {string} modalId - The ID of the modal to close
|
||||
@ -300,7 +318,7 @@ const Modal = {
|
||||
document.body.style.overflow = ''; // Restore scrolling
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Initialize modal close buttons
|
||||
*/
|
||||
@ -315,7 +333,7 @@ const Modal = {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// Close modal when clicking outside the modal content
|
||||
document.querySelectorAll('.modal').forEach(modal => {
|
||||
modal.addEventListener('click', (event) => {
|
||||
@ -338,7 +356,7 @@ const Form = {
|
||||
serialize(form) {
|
||||
const formData = new FormData(form);
|
||||
const data = {};
|
||||
|
||||
|
||||
for (const [key, value] of formData.entries()) {
|
||||
// Handle checkboxes
|
||||
if (form.elements[key].type === 'checkbox') {
|
||||
@ -347,10 +365,10 @@ const Form = {
|
||||
data[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return data;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Populate a form with data
|
||||
* @param {HTMLFormElement} form - The form element
|
||||
@ -360,7 +378,7 @@ const Form = {
|
||||
for (const key in data) {
|
||||
if (form.elements[key]) {
|
||||
const element = form.elements[key];
|
||||
|
||||
|
||||
if (element.type === 'checkbox') {
|
||||
element.checked = Boolean(data[key]);
|
||||
} else if (element.type === 'radio') {
|
||||
@ -371,14 +389,14 @@ const Form = {
|
||||
} else {
|
||||
element.value = data[key];
|
||||
}
|
||||
|
||||
|
||||
// Trigger change event for elements like select
|
||||
const event = new Event('change');
|
||||
element.dispatchEvent(event);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Validate a form
|
||||
* @param {HTMLFormElement} form - The form element
|
||||
@ -386,12 +404,12 @@ const Form = {
|
||||
*/
|
||||
validate(form) {
|
||||
let isValid = true;
|
||||
|
||||
|
||||
// Remove existing error messages
|
||||
form.querySelectorAll('.form-error').forEach(error => {
|
||||
error.remove();
|
||||
});
|
||||
|
||||
|
||||
// Check required fields
|
||||
form.querySelectorAll('[required]').forEach(field => {
|
||||
if (!field.value.trim()) {
|
||||
@ -399,7 +417,7 @@ const Form = {
|
||||
this.showError(field, 'This field is required');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Check email fields
|
||||
form.querySelectorAll('input[type="email"]').forEach(field => {
|
||||
if (field.value && !this.isValidEmail(field.value)) {
|
||||
@ -407,10 +425,10 @@ const Form = {
|
||||
this.showError(field, 'Please enter a valid email address');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
return isValid;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Show an error message for a form field
|
||||
* @param {HTMLElement} field - The form field
|
||||
@ -424,13 +442,13 @@ const Form = {
|
||||
error.style.color = 'var(--danger-color)';
|
||||
error.style.fontSize = '0.875rem';
|
||||
error.style.marginTop = '0.25rem';
|
||||
|
||||
|
||||
// Add error class to field
|
||||
field.classList.add('is-invalid');
|
||||
|
||||
|
||||
// Insert error after field
|
||||
field.parentNode.insertBefore(error, field.nextSibling);
|
||||
|
||||
|
||||
// Add event listener to remove error when field is changed
|
||||
field.addEventListener('input', () => {
|
||||
field.classList.remove('is-invalid');
|
||||
@ -439,7 +457,7 @@ const Form = {
|
||||
}
|
||||
}, { once: true });
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Check if an email is valid
|
||||
* @param {string} email - The email to check
|
||||
@ -462,7 +480,7 @@ const DOM = {
|
||||
*/
|
||||
createElement(tag, attrs = {}, children = []) {
|
||||
const element = document.createElement(tag);
|
||||
|
||||
|
||||
// Set attributes
|
||||
for (const key in attrs) {
|
||||
if (key === 'className') {
|
||||
@ -476,7 +494,7 @@ const DOM = {
|
||||
element.setAttribute(key, attrs[key]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Add children
|
||||
if (Array.isArray(children)) {
|
||||
children.forEach(child => {
|
||||
@ -489,10 +507,10 @@ const DOM = {
|
||||
} else if (typeof children === 'string') {
|
||||
element.textContent = children;
|
||||
}
|
||||
|
||||
|
||||
return element;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Create a loading spinner
|
||||
* @param {string} size - The size of the spinner (sm, md, lg)
|
||||
@ -503,7 +521,7 @@ const DOM = {
|
||||
spinner.className = `loading-spinner loading-spinner-${size}`;
|
||||
return spinner;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Show a loading spinner in a container
|
||||
* @param {HTMLElement} container - The container element
|
||||
@ -513,18 +531,18 @@ const DOM = {
|
||||
showSpinner(container, size = 'md') {
|
||||
// Clear container
|
||||
container.innerHTML = '';
|
||||
|
||||
|
||||
// Create spinner container
|
||||
const spinnerContainer = document.createElement('div');
|
||||
spinnerContainer.className = 'loading-spinner-container';
|
||||
|
||||
|
||||
// Create spinner
|
||||
const spinner = this.createSpinner(size);
|
||||
spinnerContainer.appendChild(spinner);
|
||||
|
||||
|
||||
// Add to container
|
||||
container.appendChild(spinnerContainer);
|
||||
|
||||
|
||||
return spinnerContainer;
|
||||
}
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user