From 5494f1e4ee3047448c1447eef83f94d03cb1265e Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Sun, 15 Feb 2026 21:22:03 +0000 Subject: [PATCH] # et roles --- api_v1_channel_permissions.php | 45 ++++- api_v1_servers.php | 4 + assets/css/discord.css | 76 +++++++++ assets/js/main.js | 178 ++++++++++++++----- includes/permissions.php | 81 ++++++++- index.php | 302 +++++++++++++++++++++------------ 6 files changed, 518 insertions(+), 168 deletions(-) diff --git a/api_v1_channel_permissions.php b/api_v1_channel_permissions.php index 078e7f6..4373c26 100644 --- a/api_v1_channel_permissions.php +++ b/api_v1_channel_permissions.php @@ -9,6 +9,24 @@ $data = json_decode(file_get_contents('php://input'), true) ?? $_POST; if ($_SERVER['REQUEST_METHOD'] === 'GET') { $channel_id = $_GET['channel_id'] ?? 0; + // Get server_id for this channel + $stmt = db()->prepare("SELECT server_id FROM channels WHERE id = ?"); + $stmt->execute([$channel_id]); + $channel = $stmt->fetch(); + $server_id = $channel['server_id'] ?? 0; + + // Ensure @everyone role exists for this server + $stmt = db()->prepare("SELECT id FROM roles WHERE server_id = ? AND (name = '@everyone' OR name = 'Everyone') LIMIT 1"); + $stmt->execute([$server_id]); + $everyone = $stmt->fetch(); + if (!$everyone && $server_id) { + $stmt = db()->prepare("INSERT INTO roles (server_id, name, color, permissions, position) VALUES (?, '@everyone', '#99aab5', 0, 0)"); + $stmt->execute([$server_id]); + $everyone_role_id = db()->lastInsertId(); + } else { + $everyone_role_id = $everyone['id'] ?? 0; + } + // Fetch permissions for this channel $stmt = db()->prepare(" SELECT cp.*, r.name as role_name, r.color as role_color @@ -17,7 +35,32 @@ if ($_SERVER['REQUEST_METHOD'] === 'GET') { WHERE cp.channel_id = ? "); $stmt->execute([$channel_id]); - echo json_encode(['success' => true, 'permissions' => $stmt->fetchAll()]); + $permissions = $stmt->fetchAll(); + + // Check if @everyone is in permissions, if not add it manually to show up by default + $has_everyone = false; + foreach($permissions as $p) { + if ($p['role_id'] == $everyone_role_id) { + $has_everyone = true; + break; + } + } + + if (!$has_everyone && $everyone_role_id) { + $stmt = db()->prepare("SELECT name, color FROM roles WHERE id = ?"); + $stmt->execute([$everyone_role_id]); + $r = $stmt->fetch(); + $permissions[] = [ + 'channel_id' => (int)$channel_id, + 'role_id' => (int)$everyone_role_id, + 'allow_permissions' => 0, + 'deny_permissions' => 0, + 'role_name' => $r['name'], + 'role_color' => $r['color'] + ]; + } + + echo json_encode(['success' => true, 'permissions' => $permissions]); exit; } diff --git a/api_v1_servers.php b/api_v1_servers.php index f324857..05b064d 100644 --- a/api_v1_servers.php +++ b/api_v1_servers.php @@ -68,6 +68,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { // Create default channel $stmt = $db->prepare("INSERT INTO channels (server_id, name, type) VALUES (?, 'general', 'text')"); $stmt->execute([$server_id]); + + // Create default @everyone role + $stmt = $db->prepare("INSERT INTO roles (server_id, name, color, permissions, position) VALUES (?, '@everyone', '#99aab5', 0, 0)"); + $stmt->execute([$server_id]); $db->commit(); header('Location: index.php?server_id=' . $server_id); diff --git a/assets/css/discord.css b/assets/css/discord.css index db4b44c..77347c5 100644 --- a/assets/css/discord.css +++ b/assets/css/discord.css @@ -929,3 +929,79 @@ body { background-color: var(--active); color: var(--text-primary); } + +/* Permission Tri-state Toggles */ +.perm-tri-state { + background-color: #1e1f22; + padding: 2px; + border-radius: 4px; + display: inline-flex; +} + +.perm-tri-state .btn { + padding: 4px 10px; + font-size: 0.85rem; + border: none !important; + border-radius: 4px !important; + color: #b5bac1; + transition: all 0.2s; +} + +.perm-tri-state .btn:hover { + background-color: #35373c; + color: white; +} + +.perm-tri-state .btn-outline-danger:checked + label { + background-color: #f23f42 !important; + color: white !important; +} + +.perm-tri-state .btn-outline-secondary:checked + label { + background-color: #4e5058 !important; + color: white !important; +} + +.perm-tri-state .btn-outline-success:checked + label { + background-color: #23a559 !important; + color: white !important; +} + +#channel-permissions-roles-list .list-group-item.active { + background-color: rgba(78, 80, 88, 0.6) !important; + color: white !important; +} + +#channel-permissions-roles-list .list-group-item { + border-radius: 4px !important; + margin: 0 8px; + width: auto; + color: #dbdee1 !important; +} + +/* Modal text visibility fixes */ +.modal-content .text-muted { + color: #dbdee1 !important; /* Lighter than before */ +} + +.modal-content .small { + color: #dbdee1; +} + +.modal-content label { + color: #ffffff; /* Even brighter */ +} + +.modal-content .fw-bold { + color: #ffffff; +} + +.permission-item { + border-bottom: 1px solid rgba(255, 255, 255, 0.05); +} + +.permission-item:last-child { + border-bottom: none; +} + + diff --git a/assets/js/main.js b/assets/js/main.js index f5ee9f2..b4b3d00 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -745,21 +745,33 @@ document.addEventListener('DOMContentLoaded', () => { // Channel Permissions Management const channelPermissionsTabBtn = document.getElementById('channel-permissions-tab-btn'); - const channelPermissionsList = document.getElementById('channel-permissions-list'); + const channelPermissionsRolesList = document.getElementById('channel-permissions-roles-list'); const addPermRoleList = document.getElementById('add-permission-role-list'); + const channelPermissionsSettings = document.getElementById('channel-permissions-settings'); + const noRoleSelectedView = document.getElementById('no-role-selected-view'); + const selectedPermRoleName = document.getElementById('selected-perm-role-name'); + const removeSelectedPermRole = document.getElementById('remove-selected-perm-role'); + const permissionsTogglesContainer = document.getElementById('permissions-toggles-container'); + + let currentSelectedOverrideRole = null; + let channelPermissionsData = []; channelPermissionsTabBtn?.addEventListener('click', async () => { const channelId = document.getElementById('edit-channel-id').value; - loadChannelPermissions(channelId); - loadRolesForPermissions(channelId); + currentSelectedOverrideRole = null; + channelPermissionsSettings.style.display = 'none'; + noRoleSelectedView.style.display = 'flex'; + await loadChannelPermissions(channelId); + await loadRolesForPermissions(channelId); }); async function loadChannelPermissions(channelId) { - channelPermissionsList.innerHTML = '
Loading permissions...
'; + channelPermissionsRolesList.innerHTML = '
Loading...
'; const resp = await fetch(`api_v1_channel_permissions.php?channel_id=${channelId}`); const data = await resp.json(); if (data.success) { - renderChannelPermissions(channelId, data.permissions); + channelPermissionsData = data.permissions; + renderRoleOverridesList(channelId); } } @@ -768,7 +780,16 @@ document.addEventListener('DOMContentLoaded', () => { const resp = await fetch(`api_v1_roles.php?server_id=${activeServerId}`); const data = await resp.json(); if (data.success) { - data.roles.forEach(role => { + // Filter out roles already in overrides + const existingRoleIds = channelPermissionsData.map(p => parseInt(p.role_id)); + const availableRoles = data.roles.filter(role => !existingRoleIds.includes(parseInt(role.id))); + + if (availableRoles.length === 0) { + addPermRoleList.innerHTML = '
  • No more roles to add
  • '; + return; + } + + availableRoles.forEach(role => { const li = document.createElement('li'); li.innerHTML = `
    @@ -781,69 +802,122 @@ document.addEventListener('DOMContentLoaded', () => { headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ channel_id: channelId, role_id: role.id, allow: 0, deny: 0 }) }); - loadChannelPermissions(channelId); + await loadChannelPermissions(channelId); + selectOverrideRole(role.id, role.name); }; addPermRoleList.appendChild(li); }); } } - function renderChannelPermissions(channelId, permissions) { - channelPermissionsList.innerHTML = ''; - if (permissions.length === 0) { - channelPermissionsList.innerHTML = '
    No role overrides.
    '; + function renderRoleOverridesList(channelId) { + channelPermissionsRolesList.innerHTML = ''; + if (channelPermissionsData.length === 0) { + channelPermissionsRolesList.innerHTML = '
    No overrides configured for this channel.
    '; return; } - permissions.forEach(p => { + + // Sort: @everyone always at top, then by name + const sortedData = [...channelPermissionsData].sort((a, b) => { + const isAEveryone = a.role_name.toLowerCase().includes('everyone'); + const isBEveryone = b.role_name.toLowerCase().includes('everyone'); + if (isAEveryone && !isBEveryone) return -1; + if (!isAEveryone && isBEveryone) return 1; + return a.role_name.localeCompare(b.role_name); + }); + + sortedData.forEach(p => { const item = document.createElement('div'); - item.className = 'list-group-item bg-transparent text-white border-secondary p-2'; + item.className = `list-group-item list-group-item-action bg-transparent text-white border-0 mb-1 p-2 small d-flex align-items-center ${currentSelectedOverrideRole == p.role_id ? 'active' : ''}`; + item.style.cursor = 'pointer'; item.innerHTML = ` -
    -
    -
    - ${p.role_name} -
    - -
    -
    - -
    +
    + ${p.role_name} `; - channelPermissionsList.appendChild(item); + item.onclick = () => selectOverrideRole(p.role_id, p.role_name); + channelPermissionsRolesList.appendChild(item); }); } - channelPermissionsList?.addEventListener('click', async (e) => { + function selectOverrideRole(roleId, roleName) { + currentSelectedOverrideRole = roleId; const channelId = document.getElementById('edit-channel-id').value; - if (e.target.classList.contains('remove-perm-btn')) { - const roleId = e.target.dataset.roleId; - await fetch('api_v1_channel_permissions.php', { - method: 'DELETE', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ channel_id: channelId, role_id: roleId }) - }); - loadChannelPermissions(channelId); + + // Update list active state + renderRoleOverridesList(channelId); + + selectedPermRoleName.textContent = roleName; + noRoleSelectedView.style.display = 'none'; + channelPermissionsSettings.style.display = 'block'; + + // Load existing permissions for this role + const p = channelPermissionsData.find(perm => perm.role_id == roleId) || { allow_permissions: 0, deny_permissions: 0 }; + + // Update toggles (for now only bit 1: View Channel) + updateToggleUI(1, p.allow_permissions, p.deny_permissions); + } + + function updateToggleUI(bit, allowPerms, denyPerms) { + const group = document.querySelector(`.perm-tri-state[data-perm-bit="${bit}"]`); + if (!group) return; + + if (allowPerms & bit) { + group.querySelector('input[value="allow"]').checked = true; + } else if (denyPerms & bit) { + group.querySelector('input[value="deny"]').checked = true; + } else { + group.querySelector('input[value="neutral"]').checked = true; } + } + + removeSelectedPermRole?.addEventListener('click', async () => { + if (!currentSelectedOverrideRole) return; + const channelId = document.getElementById('edit-channel-id').value; + + await fetch('api_v1_channel_permissions.php', { + method: 'DELETE', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ channel_id: channelId, role_id: currentSelectedOverrideRole }) + }); + + currentSelectedOverrideRole = null; + channelPermissionsSettings.style.display = 'none'; + noRoleSelectedView.style.display = 'flex'; + loadChannelPermissions(channelId); }); - channelPermissionsList?.addEventListener('change', async (e) => { - if (e.target.classList.contains('perm-select')) { - const channelId = document.getElementById('edit-channel-id').value; - const roleId = e.target.dataset.roleId; + permissionsTogglesContainer?.addEventListener('change', async (e) => { + if (e.target.type === 'radio') { + const group = e.target.closest('.perm-tri-state'); + const bit = parseInt(group.dataset.permBit); const val = e.target.value; - let allow = 0, deny = 0; - if (val === 'allow') allow = 1; - if (val === 'deny') deny = 1; + const channelId = document.getElementById('edit-channel-id').value; + const roleId = currentSelectedOverrideRole; + + let p = channelPermissionsData.find(perm => perm.role_id == roleId); + if (!p) { + p = { role_id: roleId, allow_permissions: 0, deny_permissions: 0 }; + } + + let allow = parseInt(p.allow_permissions); + let deny = parseInt(p.deny_permissions); + + // Clear current bit + allow &= ~bit; + deny &= ~bit; + + if (val === 'allow') allow |= bit; + if (val === 'deny') deny |= bit; await fetch('api_v1_channel_permissions.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ channel_id: channelId, role_id: roleId, allow, deny }) }); + + // Update local data + p.allow_permissions = allow; + p.deny_permissions = deny; } }); @@ -871,16 +945,22 @@ document.addEventListener('DOMContentLoaded', () => { const modal = document.getElementById('editChannelModal'); const channelId = btn.dataset.id; const channelType = btn.dataset.type || 'chat'; + const channelName = btn.dataset.name; modal.querySelector('#edit-channel-id').value = channelId; - modal.querySelector('#edit-channel-name').value = btn.dataset.name; + modal.querySelector('#edit-channel-name').value = channelName; + modal.querySelector('#header-channel-name').textContent = channelName; modal.querySelector('#edit-channel-type').value = channelType; modal.querySelector('#edit-channel-files').checked = btn.dataset.files == '1'; modal.querySelector('#edit-channel-limit').value = btn.dataset.limit || ''; modal.querySelector('#edit-channel-status').value = btn.dataset.status || ''; modal.querySelector('#edit-channel-icon').value = btn.dataset.icon || ''; + modal.querySelector('#edit-channel-category-id').value = btn.dataset.category || ''; modal.querySelector('#delete-channel-id').value = channelId; + // Reset delete zone + document.getElementById('delete-confirm-zone').style.display = 'none'; + // Show/Hide RSS tab const rssTabNav = document.getElementById('rss-tab-nav'); const statusContainer = document.getElementById('edit-channel-status-container'); @@ -890,8 +970,8 @@ document.addEventListener('DOMContentLoaded', () => { } else { rssTabNav.style.display = 'none'; // Switch to General tab if we were on RSS - if (document.getElementById('rss-tab-btn').classList.contains('active')) { - bootstrap.Tab.getInstance(modal.querySelector('.nav-link.active')).hide(); + const rssTabBtn = document.getElementById('rss-tab-btn'); + if (rssTabBtn && rssTabBtn.classList.contains('active')) { bootstrap.Tab.getOrCreateInstance(modal.querySelector('[data-bs-target="#edit-channel-general"]')).show(); } } @@ -904,6 +984,10 @@ document.addEventListener('DOMContentLoaded', () => { }); }); + document.getElementById('delete-channel-trigger')?.addEventListener('click', () => { + document.getElementById('delete-confirm-zone').style.display = 'block'; + }); + // RSS Management const editChannelType = document.getElementById('edit-channel-type'); editChannelType?.addEventListener('change', () => { diff --git a/includes/permissions.php b/includes/permissions.php index 064c9e9..ab0b5ba 100644 --- a/includes/permissions.php +++ b/includes/permissions.php @@ -33,6 +33,56 @@ class Permissions { return ($perms & $permission) === $permission; } + public static function canViewChannel($user_id, $channel_id) { + $stmt = db()->prepare("SELECT server_id FROM channels WHERE id = ?"); + $stmt->execute([$channel_id]); + $c = $stmt->fetch(); + if (!$c) return false; + $server_id = $c['server_id']; + + // Check if owner or admin + if (self::hasPermission($user_id, $server_id, self::ADMINISTRATOR)) return true; + + // Fetch overrides for all roles the user has in this server + $stmt = db()->prepare(" + SELECT cp.allow_permissions, cp.deny_permissions + FROM channel_permissions cp + JOIN user_roles ur ON cp.role_id = ur.role_id + WHERE ur.user_id = ? AND cp.channel_id = ? + "); + $stmt->execute([$user_id, $channel_id]); + $overrides = $stmt->fetchAll(); + + // Check @everyone override specifically (even if user has no roles assigned) + $stmt = db()->prepare("SELECT id FROM roles WHERE server_id = ? AND (name = '@everyone' OR name = 'Everyone') LIMIT 1"); + $stmt->execute([$server_id]); + $everyone_role = $stmt->fetch(); + if ($everyone_role) { + $stmt = db()->prepare("SELECT allow_permissions, deny_permissions FROM channel_permissions WHERE channel_id = ? AND role_id = ?"); + $stmt->execute([$channel_id, $everyone_role['id']]); + $eo = $stmt->fetch(); + if ($eo) { + $overrides[] = $eo; + } + } + + if (empty($overrides)) { + return true; // Default to yes + } + + $allow = false; + $deny = false; + foreach($overrides as $o) { + if ($o['allow_permissions'] & self::VIEW_CHANNEL) $allow = true; + if ($o['deny_permissions'] & self::VIEW_CHANNEL) $deny = true; + } + + if ($allow) return true; + if ($deny) return false; + + return true; // Default to yes + } + public static function canSendInChannel($user_id, $channel_id) { $stmt = db()->prepare("SELECT server_id FROM channels WHERE id = ?"); $stmt->execute([$channel_id]); @@ -40,11 +90,8 @@ class Permissions { if (!$c) return false; $server_id = $c['server_id']; - // Check if owner - $stmt = db()->prepare("SELECT owner_id FROM servers WHERE id = ?"); - $stmt->execute([$server_id]); - $s = $stmt->fetch(); - if ($s && $s['owner_id'] == $user_id) return true; + // Check if owner or admin + if (self::hasPermission($user_id, $server_id, self::ADMINISTRATOR)) return true; // Check overrides $stmt = db()->prepare(" @@ -56,11 +103,29 @@ class Permissions { $stmt->execute([$user_id, $channel_id]); $overrides = $stmt->fetchAll(); - foreach($overrides as $o) { - if ($o['deny_permissions'] & 1) return false; // Bit 1 for SEND_MESSAGES in overrides - if ($o['allow_permissions'] & 1) return true; + // Check @everyone override + $stmt = db()->prepare("SELECT id FROM roles WHERE server_id = ? AND (name = '@everyone' OR name = 'Everyone') LIMIT 1"); + $stmt->execute([$server_id]); + $everyone_role = $stmt->fetch(); + if ($everyone_role) { + $stmt = db()->prepare("SELECT allow_permissions, deny_permissions FROM channel_permissions WHERE channel_id = ? AND role_id = ?"); + $stmt->execute([$channel_id, $everyone_role['id']]); + $eo = $stmt->fetch(); + if ($eo) { + $overrides[] = $eo; + } } + $allow = false; + $deny = false; + foreach($overrides as $o) { + if ($o['allow_permissions'] & self::SEND_MESSAGES) $allow = true; + if ($o['deny_permissions'] & self::SEND_MESSAGES) $deny = true; + } + + if ($allow) return true; + if ($deny) return false; + return self::hasPermission($user_id, $server_id, self::SEND_MESSAGES); } } diff --git a/index.php b/index.php index 6b54385..5f3b5ab 100644 --- a/index.php +++ b/index.php @@ -4,8 +4,12 @@ require_once 'auth/session.php'; function renderRoleIcon($icon, $size = '12px') { if (empty($icon)) return ''; $isUrl = (strpos($icon, 'http') === 0 || strpos($icon, '/') === 0); + $isFa = (strpos($icon, 'fa-') === 0); + if ($isUrl) { return ''; + } elseif ($isFa) { + return ''; } else { return '' . htmlspecialchars($icon) . ''; } @@ -75,8 +79,17 @@ if ($is_dm_view) { // Fetch channels $stmt = db()->prepare("SELECT * FROM channels WHERE server_id = ? ORDER BY position ASC, id ASC"); $stmt->execute([$active_server_id]); - $channels = $stmt->fetchAll(); - $active_channel_id = $_GET['channel_id'] ?? ($channels[0]['id'] ?? 1); + $all_channels = $stmt->fetchAll(); + + require_once 'includes/permissions.php'; + $channels = []; + foreach($all_channels as $c) { + if (Permissions::canViewChannel($current_user_id, $c['id'])) { + $channels[] = $c; + } + } + + $active_channel_id = $_GET['channel_id'] ?? ($channels[0]['id'] ?? 0); // Fetch active channel details for theme $active_channel = null; @@ -87,7 +100,6 @@ if ($is_dm_view) { } } - require_once 'includes/permissions.php'; $is_owner = false; $can_manage_channels = false; $can_manage_server = false; @@ -340,7 +352,7 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? ''; ?> - + @@ -436,7 +448,7 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
    - + '; elseif ($active_channel['type'] === 'voice') echo ''; else echo ''; + + if (!empty($active_channel['icon'])) { + echo ' ' . renderRoleIcon($active_channel['icon'], '16px') . ''; + } } ?> - - -
    @@ -1109,152 +1122,217 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';