feat(phase2): channel permission overrides + expanded role UI + audit log cleanup

This commit is contained in:
2026-06-30 10:28:03 -04:00
parent d7c84647e5
commit bd260183ef
10 changed files with 598 additions and 19 deletions
+77
View File
@@ -0,0 +1,77 @@
package permissions
import (
"context"
"database/sql"
)
// ComputeChannelPermissions returns effective permissions for a user in a specific channel,
// layering channel overrides on top of server-wide role permissions.
func (c *Checker) ComputeChannelPermissions(ctx context.Context, serverID, channelID, userID string) (int64, error) {
base, err := c.GetUserPermissions(ctx, serverID, userID)
if err != nil {
return 0, err
}
if Has(base, ADMINISTRATOR) {
return base, nil
}
// Everyone role override applies first.
var everyoneID string
_ = c.db.QueryRowContext(ctx, `SELECT id FROM roles WHERE server_id = $1 AND is_default = TRUE LIMIT 1`, serverID).Scan(&everyoneID)
if everyoneID != "" {
allow, deny, err := c.getOverride(ctx, channelID, "role", everyoneID)
if err != nil {
return 0, err
}
base |= allow
base &^= deny
}
// Member-specific role overrides.
rows, err := c.db.QueryContext(ctx, `
SELECT r.id FROM roles r
INNER JOIN member_roles mr ON mr.role_id = r.id
WHERE mr.user_id = $1 AND mr.server_id = $2
`, userID, serverID)
if err != nil {
return 0, err
}
defer rows.Close()
for rows.Next() {
var roleID string
if err := rows.Scan(&roleID); err != nil {
return 0, err
}
allow, deny, err := c.getOverride(ctx, channelID, "role", roleID)
if err != nil {
return 0, err
}
base |= allow
base &^= deny
}
if err := rows.Err(); err != nil {
return 0, err
}
// User-specific override wins last.
allow, deny, err := c.getOverride(ctx, channelID, "user", userID)
if err != nil {
return 0, err
}
base |= allow
base &^= deny
return base, nil
}
func (c *Checker) getOverride(ctx context.Context, channelID, targetType, targetID string) (allow, deny int64, err error) {
err = c.db.QueryRowContext(ctx, `
SELECT allow_bitflags, deny_bitflags FROM channel_overrides
WHERE channel_id = $1 AND target_type = $2 AND target_id = $3
`, channelID, targetType, targetID).Scan(&allow, &deny)
if err == sql.ErrNoRows {
return 0, 0, nil
}
return allow, deny, err
}
+29
View File
@@ -26,6 +26,35 @@ const (
MENTION_EVERYONE int64 = 1 << 21
)
// AllPermissionLabels maps flags to human-friendly names.
var AllPermissionLabels = []struct {
Key string
Flag int64
}{
{"VIEW_CHANNEL", VIEW_CHANNEL},
{"SEND_MESSAGES", SEND_MESSAGES},
{"MANAGE_MESSAGES", MANAGE_MESSAGES},
{"KICK_MEMBERS", KICK_MEMBERS},
{"BAN_MEMBERS", BAN_MEMBERS},
{"MANAGE_SERVER", MANAGE_SERVER},
{"MANAGE_CHANNELS", MANAGE_CHANNELS},
{"ADMINISTRATOR", ADMINISTRATOR},
{"CONNECT_VOICE", CONNECT_VOICE},
{"SPEAK_VOICE", SPEAK_VOICE},
{"SHARE_SCREEN", SHARE_SCREEN},
{"MUTE_MEMBERS", MUTE_MEMBERS},
{"CREATE_INSTANT_INVITE", CREATE_INSTANT_INVITE},
{"CHANGE_NICKNAME", CHANGE_NICKNAME},
{"MANAGE_NICKNAMES", MANAGE_NICKNAMES},
{"MANAGE_ROLES", MANAGE_ROLES},
{"MANAGE_WEBHOOKS", MANAGE_WEBHOOKS},
{"EMBED_LINKS", EMBED_LINKS},
{"ATTACH_FILES", ATTACH_FILES},
{"ADD_REACTIONS", ADD_REACTIONS},
{"USE_EXTERNAL_EMOJIS", USE_EXTERNAL_EMOJIS},
{"MENTION_EVERYONE", MENTION_EVERYONE},
}
// DefaultEveryonePermissions is granted to the @everyone role when a server is created.
// VIEW_CHANNEL | SEND_MESSAGES | CONNECT_VOICE | SPEAK_VOICE | SHARE_SCREEN | CREATE_INSTANT_INVITE | EMBED_LINKS | ATTACH_FILES | ADD_REACTIONS = bits 0,1,8,9,10,12,17,18,19.
const DefaultEveryonePermissions int64 = VIEW_CHANNEL | SEND_MESSAGES | CONNECT_VOICE | SPEAK_VOICE | SHARE_SCREEN | CREATE_INSTANT_INVITE | EMBED_LINKS | ATTACH_FILES | ADD_REACTIONS