4da08d91bc
Split-pane role editor with tri-state channel overrides (roles/members). CSS-var themes (Gruvbox default + 9 IDE palettes) in top bar and settings.
142 lines
4.4 KiB
TypeScript
142 lines
4.4 KiB
TypeScript
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',
|
|
};
|
|
|
|
/** Grouped for role/channel permission UIs. ADMINISTRATOR is special-cased in editors. */
|
|
export const PERM_CATEGORIES: { name: string; keys: PermissionKey[] }[] = [
|
|
{
|
|
name: 'General',
|
|
keys: [
|
|
'VIEW_CHANNEL',
|
|
'SEND_MESSAGES',
|
|
'MANAGE_MESSAGES',
|
|
'ADD_REACTIONS',
|
|
'EMBED_LINKS',
|
|
'ATTACH_FILES',
|
|
'MENTION_EVERYONE',
|
|
'USE_EXTERNAL_EMOJIS',
|
|
'CREATE_INSTANT_INVITE',
|
|
'CHANGE_NICKNAME',
|
|
],
|
|
},
|
|
{
|
|
name: 'Moderation',
|
|
keys: ['KICK_MEMBERS', 'BAN_MEMBERS', 'MUTE_MEMBERS', 'MANAGE_NICKNAMES'],
|
|
},
|
|
{
|
|
name: 'Server',
|
|
keys: ['MANAGE_SERVER', 'MANAGE_CHANNELS', 'MANAGE_ROLES', 'MANAGE_WEBHOOKS'],
|
|
},
|
|
{
|
|
name: 'Voice',
|
|
keys: ['CONNECT_VOICE', 'SPEAK_VOICE', 'SHARE_SCREEN'],
|
|
},
|
|
];
|
|
|
|
export function hasPermission(perms: number, flag: number): boolean {
|
|
// Match backend permissions.Has: all required bits must be present.
|
|
return (perms & flag) === flag;
|
|
}
|
|
|
|
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)),
|
|
},
|
|
};
|
|
});
|
|
},
|
|
}));
|