diff --git a/api_v1_channel_permissions.php b/api_v1_channel_permissions.php
index a6ce976..b09871c 100644
--- a/api_v1_channel_permissions.php
+++ b/api_v1_channel_permissions.php
@@ -27,15 +27,27 @@ if ($_SERVER['REQUEST_METHOD'] === 'GET') {
$everyone_role_id = $everyone['id'] ?? 0;
}
- // Fetch permissions for this channel
+ // Fetch permissions for this channel (roles and users)
$stmt = db()->prepare("
- SELECT cp.*, r.name as role_name, r.color as role_color
+ SELECT cp.*, r.name as role_name, r.color as role_color,
+ u.display_name as member_name, u.avatar_url as member_avatar
FROM channel_permissions cp
- JOIN roles r ON cp.role_id = r.id
+ LEFT JOIN roles r ON cp.role_id = r.id
+ LEFT JOIN users u ON cp.user_id = u.id
WHERE cp.channel_id = ?
");
$stmt->execute([$channel_id]);
- $permissions = $stmt->fetchAll();
+ $permissions = [];
+ while($row = $stmt->fetch()) {
+ if ($row['user_id']) {
+ $row['display_name'] = $row['member_name'] ?? 'Unknown Member';
+ $row['type'] = 'member';
+ } else {
+ $row['display_name'] = $row['role_name'] ?? 'Unknown Role';
+ $row['type'] = 'role';
+ }
+ $permissions[] = $row;
+ }
// Check if @everyone is in permissions, if not add it manually to show up by default
$has_everyone = false;
@@ -54,10 +66,13 @@ if ($_SERVER['REQUEST_METHOD'] === 'GET') {
array_unshift($permissions, [
'channel_id' => (int)$channel_id,
'role_id' => (int)$everyone_role_id,
+ 'user_id' => null,
'allow_permissions' => 0,
'deny_permissions' => 0,
'role_name' => $r['name'],
- 'role_color' => $r['color']
+ 'role_color' => $r['color'],
+ 'display_name' => $r['name'],
+ 'type' => 'role'
]);
}
}
@@ -68,10 +83,16 @@ if ($_SERVER['REQUEST_METHOD'] === 'GET') {
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$channel_id = $data['channel_id'] ?? 0;
- $role_id = $data['role_id'] ?? 0;
+ $role_id = $data['role_id'] ?? null;
+ $target_user_id = $data['user_id'] ?? null;
$allow = $data['allow'] ?? 0;
$deny = $data['deny'] ?? 0;
+ if (!$role_id && !$target_user_id) {
+ echo json_encode(['success' => false, 'error' => 'Missing role_id or user_id']);
+ exit;
+ }
+
// Check permissions: Owner or MANAGE_CHANNELS or ADMINISTRATOR
require_once 'includes/permissions.php';
$stmt = db()->prepare("SELECT server_id FROM channels WHERE id = ?");
@@ -89,11 +110,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if ($is_owner || $can_manage) {
$stmt = db()->prepare("
- INSERT INTO channel_permissions (channel_id, role_id, allow_permissions, deny_permissions)
- VALUES (?, ?, ?, ?)
+ INSERT INTO channel_permissions (channel_id, role_id, user_id, allow_permissions, deny_permissions)
+ VALUES (?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE allow_permissions = VALUES(allow_permissions), deny_permissions = VALUES(deny_permissions)
");
- $stmt->execute([$channel_id, $role_id, $allow, $deny]);
+ $stmt->execute([$channel_id, $role_id, $target_user_id, $allow, $deny]);
echo json_encode(['success' => true]);
} else {
echo json_encode(['success' => false, 'error' => 'Unauthorized']);
@@ -103,7 +124,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if ($_SERVER['REQUEST_METHOD'] === 'DELETE') {
$channel_id = $data['channel_id'] ?? 0;
- $role_id = $data['role_id'] ?? 0;
+ $role_id = $data['role_id'] ?? null;
+ $target_user_id = $data['user_id'] ?? null;
// Check permissions
require_once 'includes/permissions.php';
@@ -121,8 +143,13 @@ if ($_SERVER['REQUEST_METHOD'] === 'DELETE') {
Permissions::hasPermission($user_id, $server_id, Permissions::ADMINISTRATOR);
if ($is_owner || $can_manage) {
- $stmt = db()->prepare("DELETE FROM channel_permissions WHERE channel_id = ? AND role_id = ?");
- $stmt->execute([$channel_id, $role_id]);
+ if ($role_id !== null) {
+ $stmt = db()->prepare("DELETE FROM channel_permissions WHERE channel_id = ? AND role_id = ? AND user_id IS NULL");
+ $stmt->execute([$channel_id, $role_id]);
+ } else if ($target_user_id !== null) {
+ $stmt = db()->prepare("DELETE FROM channel_permissions WHERE channel_id = ? AND user_id = ? AND role_id IS NULL");
+ $stmt->execute([$channel_id, $target_user_id]);
+ }
echo json_encode(['success' => true]);
} else {
echo json_encode(['success' => false, 'error' => 'Unauthorized']);
diff --git a/assets/js/main.js b/assets/js/main.js
index e26e253..93261ba 100644
--- a/assets/js/main.js
+++ b/assets/js/main.js
@@ -1069,6 +1069,20 @@ document.addEventListener('DOMContentLoaded', () => {
await loadRolesForPermissions(channelId);
});
+ const searchChannelPerms = document.getElementById('search-channel-perms');
+ searchChannelPerms?.addEventListener('input', () => {
+ const query = searchChannelPerms.value.toLowerCase();
+ const items = channelPermissionsRolesList.querySelectorAll('.list-group-item');
+ items.forEach(item => {
+ const name = item.textContent.toLowerCase();
+ if (name.includes(query)) {
+ item.classList.remove('d-none');
+ } else {
+ item.classList.add('d-none');
+ }
+ });
+ });
+
async function loadChannelPermissions(channelId) {
channelPermissionsRolesList.innerHTML = '
Loading...
';
const resp = await fetch(`api_v1_channel_permissions.php?channel_id=${channelId}`);
@@ -1081,7 +1095,7 @@ document.addEventListener('DOMContentLoaded', () => {
async function loadRolesForPermissions(channelId) {
if (!addPermRoleList) return;
- addPermRoleList.innerHTML = 'Loading roles...';
+ addPermRoleList.innerHTML = 'Loading...';
try {
const resp = await fetch(`api_v1_roles.php?server_id=${activeServerId}`);
@@ -1090,57 +1104,73 @@ document.addEventListener('DOMContentLoaded', () => {
if (data.success) {
addPermRoleList.innerHTML = '';
- // Filter out roles already in overrides
- const existingRoleIds = channelPermissionsData.map(p => parseInt(p.role_id));
+ // Roles Section
+ const existingRoleIds = channelPermissionsData.filter(p => p.type === 'role').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';
- if (window.canManageServer) {
- const divider = document.createElement('li');
- divider.innerHTML = '
';
- addPermRoleList.appendChild(divider);
- const createLink = document.createElement('li');
- createLink.innerHTML = ' Create roles in Server Settings';
- addPermRoleList.appendChild(createLink);
- }
- return;
+ if (availableRoles.length > 0) {
+ const header = document.createElement('li');
+ header.innerHTML = '';
+ addPermRoleList.appendChild(header);
+
+ availableRoles.forEach(role => {
+ const li = document.createElement('li');
+ li.innerHTML = `
+
+ ${role.name}
+ `;
+ li.onclick = async (e) => {
+ e.preventDefault();
+ const postResp = await fetch('api_v1_channel_permissions.php', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ channel_id: channelId, role_id: role.id, allow: 0, deny: 0 })
+ });
+ const postData = await postResp.json();
+ if (postData.success) {
+ await loadChannelPermissions(channelId);
+ await loadRolesForPermissions(channelId);
+ selectOverrideItem(role.id, role.name, 'role');
+ }
+ };
+ addPermRoleList.appendChild(li);
+ });
}
- // Add Roles section
- const header = document.createElement('li');
- header.innerHTML = '';
- addPermRoleList.appendChild(header);
+ // Members Section
+ const existingUserIds = channelPermissionsData.filter(p => p.type === 'member').map(p => parseInt(p.user_id));
+ const availableMembers = data.members.filter(m => !existingUserIds.includes(parseInt(m.id)));
- availableRoles.forEach(role => {
- const li = document.createElement('li');
- li.innerHTML = `
-
- ${role.name}
- `;
- li.onclick = async (e) => {
- e.preventDefault();
- const postResp = await fetch('api_v1_channel_permissions.php', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ channel_id: channelId, role_id: role.id, allow: 0, deny: 0 })
- });
- const postData = await postResp.json();
- if (postData.success) {
- await loadChannelPermissions(channelId);
- await loadRolesForPermissions(channelId);
- selectOverrideRole(role.id, role.name);
- } else {
- alert("Error adding permission: " + (postData.error || "Unknown error"));
- }
- };
- addPermRoleList.appendChild(li);
- });
- } else {
- addPermRoleList.innerHTML = `Error: ${data.error || 'Failed to load'}`;
+ if (availableMembers.length > 0) {
+ const header = document.createElement('li');
+ header.innerHTML = '';
+ addPermRoleList.appendChild(header);
+
+ availableMembers.forEach(m => {
+ const li = document.createElement('li');
+ li.innerHTML = `
+
+ ${m.username}
+ `;
+ li.onclick = async (e) => {
+ e.preventDefault();
+ const postResp = await fetch('api_v1_channel_permissions.php', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ channel_id: channelId, user_id: m.id, allow: 0, deny: 0 })
+ });
+ const postData = await postResp.json();
+ if (postData.success) {
+ await loadChannelPermissions(channelId);
+ await loadRolesForPermissions(channelId);
+ selectOverrideItem(m.id, m.username, 'member');
+ }
+ };
+ addPermRoleList.appendChild(li);
+ });
+ }
}
} catch (err) {
- addPermRoleList.innerHTML = 'Network error';
console.error(err);
}
}
@@ -1148,14 +1178,14 @@ document.addEventListener('DOMContentLoaded', () => {
function renderRoleOverridesList(channelId) {
channelPermissionsRolesList.innerHTML = '';
if (channelPermissionsData.length === 0) {
- channelPermissionsRolesList.innerHTML = 'No overrides configured for this channel.
';
+ channelPermissionsRolesList.innerHTML = 'No overrides configured.
';
return;
}
- // Sort: @everyone always at top, then by name
const sortedData = [...channelPermissionsData].sort((a, b) => {
- const nameA = (a.role_name || '').toLowerCase();
- const nameB = (b.role_name || '').toLowerCase();
+ if (a.type !== b.type) return a.type === 'role' ? -1 : 1;
+ const nameA = (a.display_name || '').toLowerCase();
+ const nameB = (b.display_name || '').toLowerCase();
const isAEveryone = nameA.includes('everyone');
const isBEveryone = nameB.includes('everyone');
if (isAEveryone && !isBEveryone) return -1;
@@ -1165,34 +1195,46 @@ document.addEventListener('DOMContentLoaded', () => {
sortedData.forEach(p => {
const item = document.createElement('div');
- 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' : ''}`;
+ const isActive = currentSelectedOverrideRole == (p.type === 'role' ? p.role_id : p.user_id) && currentSelectedOverrideType === p.type;
+ 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 ${isActive ? 'active' : ''}`;
item.style.cursor = 'pointer';
+
+ let icon = '';
+ if (p.type === 'role') {
+ icon = ``;
+ } else {
+ icon = `
`;
+ }
+
item.innerHTML = `
-
- ${p.role_name || 'Unknown Role'}
+ ${icon}
+ ${p.display_name}
`;
- item.onclick = () => selectOverrideRole(p.role_id, p.role_name || 'Unknown Role');
+ item.onclick = () => selectOverrideItem(p.type === 'role' ? p.role_id : p.user_id, p.display_name, p.type);
channelPermissionsRolesList.appendChild(item);
});
}
- function selectOverrideRole(roleId, roleName) {
- currentSelectedOverrideRole = roleId;
+ let currentSelectedOverrideType = 'role';
+
+ function selectOverrideItem(id, name, type) {
+ currentSelectedOverrideRole = id;
+ currentSelectedOverrideType = type;
const channelId = document.getElementById('edit-channel-id').value;
- // Update list active state
renderRoleOverridesList(channelId);
- selectedPermRoleName.textContent = roleName;
+ selectedPermRoleName.textContent = name;
noRoleSelectedView.classList.add('d-none');
channelPermissionsSettings.classList.remove('d-none');
- // Load existing permissions for this role
- const p = channelPermissionsData.find(perm => perm.role_id == roleId) || { allow_permissions: 0, deny_permissions: 0 };
+ const p = channelPermissionsData.find(perm => {
+ if (type === 'role') return perm.role_id == id && perm.type === 'role';
+ return perm.user_id == id && perm.type === 'member';
+ }) || { allow_permissions: 0, deny_permissions: 0 };
- // Update toggles
- updateToggleUI(1, p.allow_permissions, p.deny_permissions); // View Channel
- updateToggleUI(2, p.allow_permissions, p.deny_permissions); // Send Messages
+ updateToggleUI(1, p.allow_permissions, p.deny_permissions);
+ updateToggleUI(2, p.allow_permissions, p.deny_permissions);
}
function updateToggleUI(bit, allowPerms, denyPerms) {
@@ -1212,10 +1254,17 @@ document.addEventListener('DOMContentLoaded', () => {
if (!currentSelectedOverrideRole) return;
const channelId = document.getElementById('edit-channel-id').value;
+ const payload = { channel_id: channelId };
+ if (currentSelectedOverrideType === 'role') {
+ payload.role_id = currentSelectedOverrideRole;
+ } else {
+ payload.user_id = currentSelectedOverrideRole;
+ }
+
await fetch('api_v1_channel_permissions.php', {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ channel_id: channelId, role_id: currentSelectedOverrideRole })
+ body: JSON.stringify(payload)
});
currentSelectedOverrideRole = null;
@@ -1230,11 +1279,17 @@ document.addEventListener('DOMContentLoaded', () => {
const bit = parseInt(group.dataset.permBit);
const val = e.target.value;
const channelId = document.getElementById('edit-channel-id').value;
- const roleId = currentSelectedOverrideRole;
+ const id = currentSelectedOverrideRole;
+ const type = currentSelectedOverrideType;
- let p = channelPermissionsData.find(perm => perm.role_id == roleId);
+ let p = channelPermissionsData.find(perm => {
+ if (type === 'role') return perm.role_id == id && perm.type === 'role';
+ return perm.user_id == id && perm.type === 'member';
+ });
+
if (!p) {
- p = { role_id: roleId, allow_permissions: 0, deny_permissions: 0 };
+ p = { channel_id: channelId, allow_permissions: 0, deny_permissions: 0 };
+ if (type === 'role') p.role_id = id; else p.user_id = id;
}
let allow = parseInt(p.allow_permissions);
@@ -1247,10 +1302,13 @@ document.addEventListener('DOMContentLoaded', () => {
if (val === 'allow') allow |= bit;
if (val === 'deny') deny |= bit;
+ const payload = { channel_id: channelId, allow, deny };
+ if (type === 'role') payload.role_id = id; else payload.user_id = id;
+
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 })
+ body: JSON.stringify(payload)
});
// Update local data
diff --git a/assets/pasted-20260218-160633-6ce717d1.png b/assets/pasted-20260218-160633-6ce717d1.png
new file mode 100644
index 0000000..ddd4797
Binary files /dev/null and b/assets/pasted-20260218-160633-6ce717d1.png differ
diff --git a/data/22.participants.json b/data/22.participants.json
index 3caf8fb..4cd408b 100644
--- a/data/22.participants.json
+++ b/data/22.participants.json
@@ -1 +1 @@
-{"de65a0b0b1a29c9a":{"id":"de65a0b0b1a29c9a","user_id":2,"name":"swefpifh ᵇʰᶠʳ","avatar_url":"","last_seen":1771343410040}}
\ No newline at end of file
+{"b2dca0f015f8373d":{"id":"b2dca0f015f8373d","user_id":3,"name":"swefheim","avatar_url":"","last_seen":1771431264239}}
\ No newline at end of file
diff --git a/data/3.participants.json b/data/3.participants.json
index 486a3e1..485c217 100644
--- a/data/3.participants.json
+++ b/data/3.participants.json
@@ -1 +1 @@
-{"920464469dc771ed":{"id":"920464469dc771ed","user_id":3,"name":"swefheim","avatar_url":"","last_seen":1771343410598}}
\ No newline at end of file
+{"42293ef053eb80b0":{"id":"42293ef053eb80b0","user_id":2,"name":"swefpifh ᵇʰᶠʳ","avatar_url":"","last_seen":1771431325495}}
\ No newline at end of file
diff --git a/index.php b/index.php
index 3576530..9135172 100644
--- a/index.php
+++ b/index.php
@@ -1999,14 +1999,20 @@ async function handleSaveUserSettings(btn) {
diff --git a/requests.log b/requests.log
index d8b5db0..1f22491 100644
--- a/requests.log
+++ b/requests.log
@@ -632,3 +632,10 @@
{"date":"2026-02-17 15:47:36","method":"POST","post":{"avatar_url":"","display_name":"swefpifh \u1d47\u02b0\u1da0\u02b3","theme":"dark","voice_mode":"ptt","voice_ptt_key":"v","voice_vox_threshold":"0.06","dnd_mode":"1","sound_notifications":"1"},"session":{"user_id":2},"user_id":2,"db_success":true}
{"date":"2026-02-17 15:48:00","method":"POST","post":{"avatar_url":"","display_name":"swefheim","theme":"light","voice_mode":"vox","voice_ptt_key":"v","voice_vox_threshold":"0","dnd_mode":"0","sound_notifications":"0"},"session":{"user_id":3},"user_id":3,"db_success":true}
{"date":"2026-02-17 15:48:41","method":"POST","post":{"avatar_url":"","display_name":"swefpifh \u1d47\u02b0\u1da0\u02b3","theme":"dark","voice_mode":"vox","voice_ptt_key":"v","voice_vox_threshold":"0.06","dnd_mode":"1","sound_notifications":"1"},"session":{"user_id":2},"user_id":2,"db_success":true}
+2026-02-18 16:03:28 - GET /?fl_project=38443 - POST: []
+2026-02-18 16:03:33 - GET / - POST: []
+2026-02-18 16:03:54 - GET /index.php?server_id=1&channel_id=3 - POST: []
+2026-02-18 16:04:41 - GET /?server_id=1&channel_id=18 - POST: []
+2026-02-18 16:04:48 - GET /?server_id=1&channel_id=18 - POST: []
+2026-02-18 16:12:25 - GET /?fl_project=38443 - POST: []
+2026-02-18 16:14:55 - GET /?server_id=1&channel_id=18 - POST: []