feat: bitwise RBAC engine with channel-level permission overrides

Replace string-based role checks (role === 'admin') with a bitwise BigInt
permission system. Adds computePermissions() resolution engine following
Discord's model: @everyone base → role union → admin shortcut → channel
overrides (role deny/allow → member deny/allow). Ready payload now filters
channels by VIEW_CHANNEL and attaches per-user myPermissions to each
server and channel. Includes channel_overrides table, @everyone role
auto-creation, migration for existing servers, and override CRUD API.
This commit is contained in:
Jannis Braun
2026-02-24 05:08:59 +01:00
parent 024833c470
commit 8030c89c6c
19 changed files with 568 additions and 93 deletions
+7 -7
View File
@@ -3,7 +3,7 @@ import { eq, and, desc, lt, inArray } from 'drizzle-orm';
import { getDb, schema } from '../db/index.js';
import { authenticate } from '../utils/auth.js';
import { generateSnowflake } from '../utils/snowflake.js';
import { isMember, getChannelServerId, isAdmin } from '../utils/permissions.js';
import { hasPermission, getChannelServerId, PermissionBits } from '../utils/permissions.js';
import { connectionManager } from '../ws/handler.js';
import type {
CreateMessageRequest,
@@ -175,8 +175,8 @@ export async function messageRoutes(app: FastifyInstance): Promise<void> {
return reply.code(404).send({ error: 'Channel not found', statusCode: 404 });
}
if (!isMember(serverId, request.userId)) {
return reply.code(403).send({ error: 'You are not a member of this server', statusCode: 403 });
if (!hasPermission(request.userId, serverId, PermissionBits.VIEW_CHANNEL | PermissionBits.READ_MESSAGE_HISTORY, id)) {
return reply.code(403).send({ error: 'Missing VIEW_CHANNEL or READ_MESSAGE_HISTORY permission', statusCode: 403 });
}
const db = getDb();
@@ -268,8 +268,8 @@ export async function messageRoutes(app: FastifyInstance): Promise<void> {
return reply.code(404).send({ error: 'Channel not found', statusCode: 404 });
}
if (!isMember(serverId, request.userId)) {
return reply.code(403).send({ error: 'You are not a member of this server', statusCode: 403 });
if (!hasPermission(request.userId, serverId, PermissionBits.SEND_MESSAGES, id)) {
return reply.code(403).send({ error: 'Missing SEND_MESSAGES permission', statusCode: 403 });
}
if ((!content || typeof content !== 'string' || content.trim().length === 0) &&
@@ -418,9 +418,9 @@ export async function messageRoutes(app: FastifyInstance): Promise<void> {
}
const isAuthor = message.userId === request.userId;
const isAdminUser = isAdmin(serverId, request.userId);
const canManageMessages = hasPermission(request.userId, serverId, PermissionBits.MANAGE_MESSAGES, message.channelId);
if (!isAuthor && !isAdminUser) {
if (!isAuthor && !canManageMessages) {
return reply.code(403).send({ error: 'You cannot delete this message', statusCode: 403 });
}