diff --git a/frontend/src/components/HelloWorld.vue b/frontend/src/components/HelloWorld.vue deleted file mode 100644 index 4d9a7a8..0000000 --- a/frontend/src/components/HelloWorld.vue +++ /dev/null @@ -1,103 +0,0 @@ - - - diff --git a/frontend/src/components/README.md b/frontend/src/components/README.md deleted file mode 100644 index d1dc92f..0000000 --- a/frontend/src/components/README.md +++ /dev/null @@ -1,35 +0,0 @@ -# Components - -Vue template files in this folder are automatically imported. - -## 🚀 Usage - -Importing is handled by [unplugin-vue-components](https://github.com/unplugin/unplugin-vue-components). This plugin automatically imports `.vue` files created in the `src/components` directory, and registers them as global components. This means that you can use any component in your application without having to manually import it. - -The following example assumes a component located at `src/components/MyComponent.vue`: - -```vue - - - -``` - -When your template is rendered, the component's import will automatically be inlined, which renders to this: - -```vue - - - -``` diff --git a/frontend/src/components/permissions/ChannelPermissionEditor.vue b/frontend/src/components/permissions/ChannelPermissionEditor.vue new file mode 100644 index 0000000..570f435 --- /dev/null +++ b/frontend/src/components/permissions/ChannelPermissionEditor.vue @@ -0,0 +1,181 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/components/permissions/ServerPermissionEditor.vue b/frontend/src/components/permissions/ServerPermissionEditor.vue new file mode 100644 index 0000000..646bef7 --- /dev/null +++ b/frontend/src/components/permissions/ServerPermissionEditor.vue @@ -0,0 +1,175 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/composables/usePermissions.ts b/frontend/src/composables/usePermissions.ts new file mode 100644 index 0000000..eeecf69 --- /dev/null +++ b/frontend/src/composables/usePermissions.ts @@ -0,0 +1,235 @@ +import {useApi} from '@/composables/useApi' +import { + type ChannelRolePermission, + type ChannelRolePermissionDto, + channelRolePermissionFromDto, + type ChannelUserPermission, + type ChannelUserPermissionDto, + channelUserPermissionFromDto, + type PermissionMaskInput, + permissionMaskToJson, + type ServerRolePermission, + type ServerRolePermissionDto, + serverRolePermissionFromDto, + type ServerUserPermission, + type ServerUserPermissionDto, + serverUserPermissionFromDto, +} from '@/types/permissions' + +export function usePermissions() { + const api = useApi() + + async function parseResponse(response: Response): Promise { + if (!response.ok) { + const error = await response.json().catch(() => null) + + throw new Error( + error?.message || 'Erreur lors de la gestion des permissions', + ) + } + + if (response.status === 204) { + return undefined as T + } + + return response.json() as Promise + } + + // --------------------------------------------------------------------------- + // Permissions serveur - utilisateur + // --------------------------------------------------------------------------- + + async function getServerUserPermission( + serverId: string, + userId: string, + ): Promise { + const response = await api.get( + `/servers/${serverId}/permissions/users/${userId}`, + ) + + const dto = await parseResponse(response) + + return serverUserPermissionFromDto(dto) + } + + async function setServerUserPermission( + serverId: string, + userId: string, + permissions: PermissionMaskInput, + ): Promise { + const response = await api.put( + `/servers/${serverId}/permissions/users/${userId}`, + { + permissions: permissionMaskToJson(permissions), + }, + ) + + const dto = await parseResponse(response) + + return serverUserPermissionFromDto(dto) + } + + async function removeServerUserPermission( + serverId: string, + userId: string, + ): Promise { + const response = await api.delete( + `/servers/${serverId}/permissions/users/${userId}`, + ) + + await parseResponse(response) + } + + // --------------------------------------------------------------------------- + // Permissions serveur - rôle + // --------------------------------------------------------------------------- + + async function getServerRolePermission( + serverId: string, + roleId: string, + ): Promise { + const response = await api.get( + `/servers/${serverId}/permissions/roles/${roleId}`, + ) + + const dto = await parseResponse(response) + + return serverRolePermissionFromDto(dto) + } + + async function setServerRolePermission( + serverId: string, + roleId: string, + permissions: PermissionMaskInput, + ): Promise { + const response = await api.put( + `/servers/${serverId}/permissions/roles/${roleId}`, + { + permissions: permissionMaskToJson(permissions), + }, + ) + + const dto = await parseResponse(response) + + return serverRolePermissionFromDto(dto) + } + + async function removeServerRolePermission( + serverId: string, + roleId: string, + ): Promise { + const response = await api.delete( + `/servers/${serverId}/permissions/roles/${roleId}`, + ) + + await parseResponse(response) + } + + // --------------------------------------------------------------------------- + // Permissions canal - utilisateur + // --------------------------------------------------------------------------- + + async function getChannelUserPermission( + channelId: string, + userId: string, + ): Promise { + const response = await api.get( + `/channels/${channelId}/permissions/users/${userId}`, + ) + + const dto = await parseResponse(response) + + return channelUserPermissionFromDto(dto) + } + + async function setChannelUserPermission( + channelId: string, + userId: string, + permissions: PermissionMaskInput, + ): Promise { + const response = await api.put( + `/channels/${channelId}/permissions/users/${userId}`, + { + permissions: permissionMaskToJson(permissions), + }, + ) + + const dto = await parseResponse(response) + + return channelUserPermissionFromDto(dto) + } + + async function removeChannelUserPermission( + channelId: string, + userId: string, + ): Promise { + const response = await api.delete( + `/channels/${channelId}/permissions/users/${userId}`, + ) + + await parseResponse(response) + } + + // --------------------------------------------------------------------------- + // Permissions canal - rôle + // --------------------------------------------------------------------------- + + async function getChannelRolePermission( + channelId: string, + roleId: string, + ): Promise { + const response = await api.get( + `/channels/${channelId}/permissions/roles/${roleId}`, + ) + + const dto = await parseResponse(response) + + return channelRolePermissionFromDto(dto) + } + + async function setChannelRolePermission( + channelId: string, + roleId: string, + permissions: PermissionMaskInput, + ): Promise { + const response = await api.put( + `/channels/${channelId}/permissions/roles/${roleId}`, + { + permissions: permissionMaskToJson(permissions), + }, + ) + + const dto = await parseResponse(response) + + return channelRolePermissionFromDto(dto) + } + + async function removeChannelRolePermission( + channelId: string, + roleId: string, + ): Promise { + const response = await api.delete( + `/channels/${channelId}/permissions/roles/${roleId}`, + ) + + await parseResponse(response) + } + + return { + getServerUserPermission, + setServerUserPermission, + removeServerUserPermission, + + getServerRolePermission, + setServerRolePermission, + removeServerRolePermission, + + getChannelUserPermission, + setChannelUserPermission, + removeChannelUserPermission, + + getChannelRolePermission, + setChannelRolePermission, + removeChannelRolePermission, + } +} \ No newline at end of file diff --git a/frontend/src/pages/admin/dashboard.vue b/frontend/src/pages/admin/dashboard.vue index 27324a8..5eb150b 100644 --- a/frontend/src/pages/admin/dashboard.vue +++ b/frontend/src/pages/admin/dashboard.vue @@ -1,11 +1,39 @@ - - \ No newline at end of file + + + + + + \ No newline at end of file diff --git a/frontend/src/pages/index.vue b/frontend/src/pages/index.vue index 7646ab7..fd71d59 100644 --- a/frontend/src/pages/index.vue +++ b/frontend/src/pages/index.vue @@ -1,7 +1,9 @@ - - + + + \ No newline at end of file diff --git a/frontend/src/types/permissions.ts b/frontend/src/types/permissions.ts new file mode 100644 index 0000000..4934235 --- /dev/null +++ b/frontend/src/types/permissions.ts @@ -0,0 +1,445 @@ +export type PermissionMaskInput = bigint | number | string + +export type ServerPermissionMask = bigint & { + readonly __brand: 'ServerPermissionMask' +} + +export type ChannelPermissionMask = bigint & { + readonly __brand: 'ChannelPermissionMask' +} + +export interface ServerPermissionDefinition { + key: string + label: string + description?: string + bit: ServerPermissionMask + category: 'management' | 'members' +} + +export interface ChannelPermissionDefinition { + key: string + label: string + description?: string + bit: ChannelPermissionMask + category: 'chat' | 'voice' | 'management' +} + +export interface ServerUserPermission { + id: string + server_id: string + user_id: string + permissions: ServerPermissionMask +} + +export interface ServerRolePermission { + id: string + server_id: string + role_id: string + permissions: ServerPermissionMask +} + +export interface ChannelUserPermission { + id: string + channel_id: string + user_id: string + permissions: ChannelPermissionMask +} + +export interface ChannelRolePermission { + id: string + channel_id: string + role_id: string + permissions: ChannelPermissionMask +} + +export interface ServerUserPermissionDto { + id: string + server_id: string + user_id: string + permissions: number | string +} + +export interface ServerRolePermissionDto { + id: string + server_id: string + role_id: string + permissions: number | string +} + +export interface ChannelUserPermissionDto { + id: string + channel_id: string + user_id: string + permissions: number | string +} + +export interface ChannelRolePermissionDto { + id: string + channel_id: string + role_id: string + permissions: number | string +} + +function asServerPermissionMask(value: bigint): ServerPermissionMask { + return value as ServerPermissionMask +} + +function asChannelPermissionMask(value: bigint): ChannelPermissionMask { + return value as ChannelPermissionMask +} + +function toBigInt(value: PermissionMaskInput): bigint { + if (typeof value === 'bigint') { + return value + } + + if (typeof value === 'number') { + if (!Number.isSafeInteger(value) || value < 0) { + throw new Error(`Invalid permission mask: ${value}`) + } + + return BigInt(value) + } + + if (!/^\d+$/.test(value)) { + throw new Error(`Invalid permission mask: ${value}`) + } + + return BigInt(value) +} + +export function toServerPermissionMask( + value: PermissionMaskInput, +): ServerPermissionMask { + return asServerPermissionMask(toBigInt(value)) +} + +export function toChannelPermissionMask( + value: PermissionMaskInput, +): ChannelPermissionMask { + return asChannelPermissionMask(toBigInt(value)) +} + +export const SERVER_PERMISSIONS = { + MANAGE_SERVER: asServerPermissionMask(1n << 0n), + MANAGE_ROLES: asServerPermissionMask(1n << 1n), + MANAGE_CATEGORIES: asServerPermissionMask(1n << 2n), + MANAGE_CHANNELS: asServerPermissionMask(1n << 3n), + KICK_MEMBERS: asServerPermissionMask(1n << 4n), + BAN_MEMBERS: asServerPermissionMask(1n << 5n), + MANAGE_MEMBERS: asServerPermissionMask(1n << 6n), + VIEW_MEMBERS: asServerPermissionMask(1n << 7n), +} as const satisfies Record + +export const CHANNEL_PERMISSIONS = { + READ_CHANNEL: asChannelPermissionMask(1n << 0n), + SEND_MESSAGE: asChannelPermissionMask(1n << 1n), + EDIT_OWN_MESSAGE: asChannelPermissionMask(1n << 2n), + DELETE_OWN_MESSAGE: asChannelPermissionMask(1n << 3n), + EDIT_OTHERS_MESSAGES: asChannelPermissionMask(1n << 4n), + DELETE_OTHERS_MESSAGES: asChannelPermissionMask(1n << 5n), + ADD_REACTIONS: asChannelPermissionMask(1n << 6n), + ATTACH_FILES: asChannelPermissionMask(1n << 7n), + MANAGE_CHANNEL: asChannelPermissionMask(1n << 8n), + MANAGE_MESSAGES: asChannelPermissionMask(1n << 10n), + JOIN_VOICE: asChannelPermissionMask(1n << 30n), + SPEAK: asChannelPermissionMask(1n << 31n), + STREAM: asChannelPermissionMask(1n << 32n), + MUTE_SELF: asChannelPermissionMask(1n << 33n), + MUTE_OTHERS: asChannelPermissionMask(1n << 34n), + MOVE_OTHERS: asChannelPermissionMask(1n << 35n), + DISCONNECT_OTHERS: asChannelPermissionMask(1n << 36n), + MANAGE_VOICE_CHANNEL: asChannelPermissionMask(1n << 37n), +} as const satisfies Record + +export const SERVER_PERMISSION_DEFINITIONS: ServerPermissionDefinition[] = [ + { + key: 'manage_server', + label: 'Gérer le serveur', + description: 'Modifier les paramètres généraux du serveur.', + bit: SERVER_PERMISSIONS.MANAGE_SERVER, + category: 'management', + }, + { + key: 'manage_roles', + label: 'Gérer les rôles', + description: 'Créer, modifier et supprimer des rôles.', + bit: SERVER_PERMISSIONS.MANAGE_ROLES, + category: 'management', + }, + { + key: 'manage_categories', + label: 'Gérer les catégories', + description: 'Créer, modifier et supprimer des catégories.', + bit: SERVER_PERMISSIONS.MANAGE_CATEGORIES, + category: 'management', + }, + { + key: 'manage_channels', + label: 'Gérer les canaux', + description: 'Créer, modifier et supprimer des canaux.', + bit: SERVER_PERMISSIONS.MANAGE_CHANNELS, + category: 'management', + }, + { + key: 'kick_members', + label: 'Expulser des membres', + description: 'Expulser un membre du serveur.', + bit: SERVER_PERMISSIONS.KICK_MEMBERS, + category: 'members', + }, + { + key: 'ban_members', + label: 'Bannir des membres', + description: 'Bannir un membre du serveur.', + bit: SERVER_PERMISSIONS.BAN_MEMBERS, + category: 'members', + }, + { + key: 'manage_members', + label: 'Gérer les membres', + description: 'Modifier le profil ou les rôles d’un membre.', + bit: SERVER_PERMISSIONS.MANAGE_MEMBERS, + category: 'members', + }, + { + key: 'view_members', + label: 'Voir les membres', + description: 'Voir la liste des membres du serveur.', + bit: SERVER_PERMISSIONS.VIEW_MEMBERS, + category: 'members', + }, +] + +export const CHANNEL_PERMISSION_DEFINITIONS: ChannelPermissionDefinition[] = [ + { + key: 'read_channel', + label: 'Lire le canal', + description: 'Voir le canal et son contenu.', + bit: CHANNEL_PERMISSIONS.READ_CHANNEL, + category: 'chat', + }, + { + key: 'send_message', + label: 'Envoyer des messages', + bit: CHANNEL_PERMISSIONS.SEND_MESSAGE, + category: 'chat', + }, + { + key: 'edit_own_message', + label: 'Modifier ses messages', + bit: CHANNEL_PERMISSIONS.EDIT_OWN_MESSAGE, + category: 'chat', + }, + { + key: 'delete_own_message', + label: 'Supprimer ses messages', + bit: CHANNEL_PERMISSIONS.DELETE_OWN_MESSAGE, + category: 'chat', + }, + { + key: 'edit_others_messages', + label: 'Modifier les messages des autres', + bit: CHANNEL_PERMISSIONS.EDIT_OTHERS_MESSAGES, + category: 'chat', + }, + { + key: 'delete_others_messages', + label: 'Supprimer les messages des autres', + bit: CHANNEL_PERMISSIONS.DELETE_OTHERS_MESSAGES, + category: 'chat', + }, + { + key: 'add_reactions', + label: 'Ajouter des réactions', + bit: CHANNEL_PERMISSIONS.ADD_REACTIONS, + category: 'chat', + }, + { + key: 'attach_files', + label: 'Joindre des fichiers', + bit: CHANNEL_PERMISSIONS.ATTACH_FILES, + category: 'chat', + }, + { + key: 'manage_channel', + label: 'Gérer le canal', + description: 'Modifier les paramètres du canal.', + bit: CHANNEL_PERMISSIONS.MANAGE_CHANNEL, + category: 'management', + }, + { + key: 'manage_messages', + label: 'Gérer les messages', + bit: CHANNEL_PERMISSIONS.MANAGE_MESSAGES, + category: 'management', + }, + { + key: 'join_voice', + label: 'Rejoindre le vocal', + bit: CHANNEL_PERMISSIONS.JOIN_VOICE, + category: 'voice', + }, + { + key: 'speak', + label: 'Parler', + bit: CHANNEL_PERMISSIONS.SPEAK, + category: 'voice', + }, + { + key: 'stream', + label: 'Diffuser', + bit: CHANNEL_PERMISSIONS.STREAM, + category: 'voice', + }, + { + key: 'mute_self', + label: 'Se mettre en sourdine', + bit: CHANNEL_PERMISSIONS.MUTE_SELF, + category: 'voice', + }, + { + key: 'mute_others', + label: 'Rendre les autres muets', + bit: CHANNEL_PERMISSIONS.MUTE_OTHERS, + category: 'voice', + }, + { + key: 'move_others', + label: 'Déplacer les autres', + bit: CHANNEL_PERMISSIONS.MOVE_OTHERS, + category: 'voice', + }, + { + key: 'disconnect_others', + label: 'Déconnecter les autres', + bit: CHANNEL_PERMISSIONS.DISCONNECT_OTHERS, + category: 'voice', + }, + { + key: 'manage_voice_channel', + label: 'Gérer le canal vocal', + bit: CHANNEL_PERMISSIONS.MANAGE_VOICE_CHANNEL, + category: 'management', + }, +] + +export function hasServerPermission( + mask: ServerPermissionMask, + required: ServerPermissionMask, +): boolean { + return (mask & required) === required +} + +export function grantServerPermission( + mask: ServerPermissionMask, + permission: ServerPermissionMask, +): ServerPermissionMask { + return asServerPermissionMask(mask | permission) +} + +export function revokeServerPermission( + mask: ServerPermissionMask, + permission: ServerPermissionMask, +): ServerPermissionMask { + return asServerPermissionMask(mask & ~permission) +} + +export function toggleServerPermission( + mask: ServerPermissionMask, + permission: ServerPermissionMask, + enabled: boolean, +): ServerPermissionMask { + return enabled + ? grantServerPermission(mask, permission) + : revokeServerPermission(mask, permission) +} + +export function hasChannelPermission( + mask: ChannelPermissionMask, + required: ChannelPermissionMask, +): boolean { + return (mask & required) === required +} + +export function grantChannelPermission( + mask: ChannelPermissionMask, + permission: ChannelPermissionMask, +): ChannelPermissionMask { + return asChannelPermissionMask(mask | permission) +} + +export function revokeChannelPermission( + mask: ChannelPermissionMask, + permission: ChannelPermissionMask, +): ChannelPermissionMask { + return asChannelPermissionMask(mask & ~permission) +} + +export function toggleChannelPermission( + mask: ChannelPermissionMask, + permission: ChannelPermissionMask, + enabled: boolean, +): ChannelPermissionMask { + return enabled + ? grantChannelPermission(mask, permission) + : revokeChannelPermission(mask, permission) +} + +export function permissionMaskToJson( + mask: PermissionMaskInput, +): number | string { + const value = toBigInt(mask) + + if (value < 0n) { + throw new Error('Permission mask cannot be negative') + } + + return value <= BigInt(Number.MAX_SAFE_INTEGER) + ? Number(value) + : value.toString() +} + +export function serverUserPermissionFromDto( + dto: ServerUserPermissionDto, +): ServerUserPermission { + return { + id: dto.id, + server_id: dto.server_id, + user_id: dto.user_id, + permissions: toServerPermissionMask(dto.permissions), + } +} + +export function serverRolePermissionFromDto( + dto: ServerRolePermissionDto, +): ServerRolePermission { + return { + id: dto.id, + server_id: dto.server_id, + role_id: dto.role_id, + permissions: toServerPermissionMask(dto.permissions), + } +} + +export function channelUserPermissionFromDto( + dto: ChannelUserPermissionDto, +): ChannelUserPermission { + return { + id: dto.id, + channel_id: dto.channel_id, + user_id: dto.user_id, + permissions: toChannelPermissionMask(dto.permissions), + } +} + +export function channelRolePermissionFromDto( + dto: ChannelRolePermissionDto, +): ChannelRolePermission { + return { + id: dto.id, + channel_id: dto.channel_id, + role_id: dto.role_id, + permissions: toChannelPermissionMask(dto.permissions), + } +} \ No newline at end of file diff --git a/migration/src/m20220101_000001_create_table.rs b/migration/src/m20220101_000001_create_table.rs index 135e606..f905dfa 100644 --- a/migration/src/m20220101_000001_create_table.rs +++ b/migration/src/m20220101_000001_create_table.rs @@ -495,11 +495,12 @@ impl MigrationTrait for Migration { .not_null() .default(0), ) - .primary_key( + .index( Index::create() - .name("pk_server_user_permission") + .name("uq_server_user_permission") .col(Alias::new("server_id")) - .col(Alias::new("user_id")), + .col(Alias::new("user_id")) + .unique(), ) .foreign_key( ForeignKey::create()