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
+109
View File
@@ -0,0 +1,109 @@
import { create } from 'zustand';
import { api } from '../lib/api.ts';
// ponytail: mirror backend permission bitflags exactly.
export const PERMS = {
VIEW_CHANNEL: 1,
SEND_MESSAGES: 2,
MANAGE_MESSAGES: 4,
KICK_MEMBERS: 8,
BAN_MEMBERS: 16,
MANAGE_SERVER: 32,
MANAGE_CHANNELS: 64,
ADMINISTRATOR: 128,
CONNECT_VOICE: 256,
SPEAK_VOICE: 512,
SHARE_SCREEN: 1024,
MUTE_MEMBERS: 2048,
CREATE_INSTANT_INVITE: 4096,
CHANGE_NICKNAME: 8192,
MANAGE_NICKNAMES: 16384,
MANAGE_ROLES: 32768,
MANAGE_WEBHOOKS: 65536,
EMBED_LINKS: 131072,
ATTACH_FILES: 262144,
ADD_REACTIONS: 524288,
USE_EXTERNAL_EMOJIS: 1048576,
MENTION_EVERYONE: 2097152,
} as const;
export type PermissionKey = keyof typeof PERMS;
export const PERMISSION_LABELS: Record<PermissionKey, string> = {
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',
};
export function hasPermission(perms: number, flag: number): boolean {
return (perms & flag) !== 0;
}
export interface ChannelOverride {
id: string;
channel_id: string;
target_type: 'role' | 'user';
target_id: string;
allow_bitflags: number;
deny_bitflags: number;
}
interface PermissionState {
overridesByChannel: Record<string, ChannelOverride[]>;
fetchOverrides: (channelId: string) => Promise<void>;
setOverride: (channelId: string, targetType: 'role' | 'user', targetId: string, allow: number, deny: number) => Promise<ChannelOverride>;
deleteOverride: (channelId: string, targetType: 'role' | 'user', targetId: string) => Promise<void>;
}
export const usePermissionStore = create<PermissionState>((set) => ({
overridesByChannel: {},
fetchOverrides: async (channelId) => {
const overrides = await api.get<ChannelOverride[]>(`/servers/_/channels/${channelId}/permissions`);
set((state) => ({
overridesByChannel: { ...state.overridesByChannel, [channelId]: Array.isArray(overrides) ? overrides : [] },
}));
},
setOverride: async (channelId, targetType, targetId, allow, deny) => {
const override = await api.put<ChannelOverride>(`/servers/_/channels/${channelId}/permissions/${targetType}/${targetId}`, { allow, deny });
set((state) => {
const list = state.overridesByChannel[channelId] || [];
const filtered = list.filter((o) => !(o.target_type === targetType && o.target_id === targetId));
return { overridesByChannel: { ...state.overridesByChannel, [channelId]: [...filtered, override] } };
});
return override;
},
deleteOverride: async (channelId, targetType, targetId) => {
await api.delete(`/servers/_/channels/${channelId}/permissions/${targetType}/${targetId}`);
set((state) => {
const list = state.overridesByChannel[channelId] || [];
return {
overridesByChannel: {
...state.overridesByChannel,
[channelId]: list.filter((o) => !(o.target_type === targetType && o.target_id === targetId)),
},
};
});
},
}));