/
dmitriysturov
/
workspaces_platform
Обзор
Документация
Войти
/
dmitriysturov
/
workspaces_platform
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
frontend/src/components/AccessControlModal.tsx
357 строк
13 KB
dmitriysturov
vkr
04 июн 2026, 22:24
04 июн 2026, 22:24
971dfdc
Код
Авторство
О чём код?
import { useEffect, useMemo, useState } from 'react' import { authApi } from '../api/authApi' import { policyApi } from '../api/policyApi' import { useAuth } from '../auth/AuthContext' import type { AccessGroup, EffectivePermission, PermissionDefinition, PermissionEffect, PermissionGrant, PermissionSubjectType, PolicyScopeType, UserSearchResult } from '../types' import { permissionDisplayLabel } from '../utils/accessLabels' import { friendlyError } from '../utils/format' import { Badge } from './ui/Badge' import { Button } from './ui/Button' import { Modal } from './ui/Modal' type AccessControlModalProps = { open: boolean onClose: () => void scopeType: PolicyScopeType scopeId: string resourceType: string resourceId: string resourceName: string allowedPermissionPrefix?: string permissionKeys?: string[] canManagePermissions: boolean onChanged?: () => void } export function AccessControlModal({ open, onClose, scopeType, scopeId, resourceType, resourceId, resourceName, allowedPermissionPrefix, permissionKeys, canManagePermissions, onChanged, }: AccessControlModalProps) { const { token } = useAuth() const [groups, setGroups] = useState<AccessGroup[]>([]) const [definitions, setDefinitions] = useState<PermissionDefinition[]>([]) const [grants, setGrants] = useState<PermissionGrant[]>([]) const [effectivePermissions, setEffectivePermissions] = useState<EffectivePermission[]>([]) const [subjectType, setSubjectType] = useState<PermissionSubjectType>('GROUP') const [subjectId, setSubjectId] = useState('') const [permissionKey, setPermissionKey] = useState('') const [effect, setEffect] = useState<PermissionEffect>('ALLOW') const [userQuery, setUserQuery] = useState('') const [users, setUsers] = useState<UserSearchResult[]>([]) const [usersById, setUsersById] = useState<Record<string, UserSearchResult>>({}) const [isLoading, setIsLoading] = useState(false) const [error, setError] = useState<string | null>(null) const filteredDefinitions = useMemo(() => { if (permissionKeys?.length) { return definitions.filter((definition) => permissionKeys.includes(definition.permissionKey)) } if (allowedPermissionPrefix) { return definitions.filter((definition) => definition.permissionKey.startsWith(`${allowedPermissionPrefix}.`)) } return definitions }, [allowedPermissionPrefix, definitions, permissionKeys]) const definitionsByKey = useMemo(() => new Map(definitions.map((definition) => [definition.permissionKey, definition])), [definitions]) useEffect(() => { if (!open || !token) { return } let cancelled = false async function load() { try { setIsLoading(true) setError(null) const [loadedGroups, loadedDefinitions, loadedGrants, loadedEffective] = await Promise.all([ policyApi.getGroups(token!, scopeType, scopeId), policyApi.getDefinitions(token!, scopeType, scopeId), policyApi.getResourcePermissions(token!, scopeType, scopeId, resourceType, resourceId), policyApi.getEffectiveResourcePermissions(token!, scopeType, scopeId, resourceType, resourceId), ]) if (cancelled) { return } setGroups(loadedGroups) setDefinitions(loadedDefinitions) setGrants(loadedGrants) setEffectivePermissions(loadedEffective) const userIds = Array.from(new Set(loadedGrants.filter((grant) => grant.subjectType === 'USER').map((grant) => grant.subjectId))) if (userIds.length > 0) { const loadedUsers = await authApi.getUsersByIds(token!, userIds) if (!cancelled) { setUsersById(Object.fromEntries(loadedUsers.map((user) => [user.id, { id: user.id, name: user.name, lastName: user.lastName, email: user.email, displayName: displayName(user.name, user.lastName, user.email), }]))) } } else { setUsersById({}) } } catch (err) { if (!cancelled) { setError(friendlyError(err, 'Не удалось загрузить правила доступа.')) } } finally { if (!cancelled) { setIsLoading(false) } } } void load() return () => { cancelled = true } }, [open, resourceId, resourceType, scopeId, scopeType, token]) useEffect(() => { if (!open) { return } setPermissionKey(filteredDefinitions[0]?.permissionKey ?? '') }, [filteredDefinitions, open]) useEffect(() => { if (!open) { return } setSubjectId(subjectType === 'GROUP' ? groups[0]?.id ?? '' : '') }, [groups, open, subjectType]) useEffect(() => { if (!open || !token || subjectType !== 'USER' || userQuery.trim().length < 2) { setUsers([]) return } let cancelled = false const timer = window.setTimeout(async () => { try { const found = await authApi.searchUsers(token, userQuery.trim()) if (!cancelled) { setUsers(found) } } catch { if (!cancelled) { setUsers([]) } } }, 250) return () => { cancelled = true window.clearTimeout(timer) } }, [open, subjectType, token, userQuery]) if (!open) { return null } async function reloadEffective() { if (!token) { return } setEffectivePermissions(await policyApi.getEffectiveResourcePermissions(token, scopeType, scopeId, resourceType, resourceId)) } async function addGrant() { if (!token || !subjectId || !permissionKey) { return } try { setIsLoading(true) setError(null) const created = await policyApi.createResourcePermission(token, scopeType, scopeId, resourceType, resourceId, { subjectType, subjectId, permissionKey, effect, }) setGrants((current) => [...current.filter((grant) => grant.id !== created.id), created]) if (subjectType === 'USER') { const selectedUser = users.find((user) => user.id === subjectId) if (selectedUser) { setUsersById((current) => ({ ...current, [selectedUser.id]: selectedUser })) } } await reloadEffective() onChanged?.() } catch (err) { setError(friendlyError(err, 'Не удалось добавить правило доступа.')) } finally { setIsLoading(false) } } async function deleteGrant(grantId: string) { if (!token) { return } try { setIsLoading(true) setError(null) await policyApi.deleteResourcePermission(token, scopeType, scopeId, resourceType, resourceId, grantId) setGrants((current) => current.filter((grant) => grant.id !== grantId)) await reloadEffective() onChanged?.() } catch (err) { setError(friendlyError(err, 'Не удалось удалить правило доступа.')) } finally { setIsLoading(false) } } return ( <Modal title="Управление доступом" description={resourceName} onClose={onClose}> <div className="create-step"> {error && <div className="form-error">{error}</div>} {isLoading && <div className="muted">Загрузка...</div>} <div className="settings-list"> {grants.length === 0 ? ( <p className="compact-empty">Для этого объекта еще нет отдельных правил доступа.</p> ) : ( grants.map((grant) => ( <div key={grant.id} className="settings-row"> <div> <strong>{grant.subjectType === 'GROUP' ? groups.find((group) => group.id === grant.subjectId)?.name ?? grant.subjectId : userLabel(usersById[grant.subjectId], grant.subjectId)}</strong> <p className="muted"> {permissionLabel(definitionsByKey.get(grant.permissionKey), grant.permissionKey)} · {effectLabel(grant.effect)} </p> </div> {canManagePermissions && ( <Button type="button" variant="secondary" disabled={isLoading} onClick={() => void deleteGrant(grant.id)}> Удалить </Button> )} </div> )) )} </div> {canManagePermissions && ( <> <div className="settings-grid"> <label className="form-field"> <span>Кому</span> <select value={subjectType} onChange={(event) => setSubjectType(event.currentTarget.value as PermissionSubjectType)}> <option value="GROUP">Группа</option> <option value="USER">Пользователь</option> </select> </label> {subjectType === 'GROUP' ? ( <label className="form-field"> <span>Группа</span> <select value={subjectId} onChange={(event) => setSubjectId(event.currentTarget.value)}> {groups.map((group) => ( <option key={group.id} value={group.id}>{group.name}</option> ))} </select> </label> ) : ( <label className="form-field"> <span>Пользователь</span> <input value={userQuery} onChange={(event) => setUserQuery(event.currentTarget.value)} placeholder="Email или имя" /> </label> )} <label className="form-field"> <span>Право</span> <select value={permissionKey} onChange={(event) => setPermissionKey(event.currentTarget.value)}> {filteredDefinitions.map((definition) => ( <option key={definition.permissionKey} value={definition.permissionKey}> {permissionLabel(definition, definition.permissionKey)} </option> ))} </select> </label> <label className="form-field"> <span>Действие</span> <select value={effect} onChange={(event) => setEffect(event.currentTarget.value as PermissionEffect)}> <option value="ALLOW">Разрешить</option> <option value="DENY">Запретить</option> </select> </label> </div> {subjectType === 'USER' && users.length > 0 && ( <div className="settings-list"> {users.map((user) => ( <button key={user.id} type="button" className="settings-row" onClick={() => { setSubjectId(user.id) setUsersById((current) => ({ ...current, [user.id]: user })) setUserQuery(`${user.displayName} <${user.email}>`) }}> <span>{user.displayName}</span> <span className="muted">{user.email}</span> </button> ))} </div> )} <div className="modal-actions"> <Button type="button" disabled={isLoading || !subjectId || !permissionKey} onClick={() => void addGrant()}> Добавить </Button> </div> </> )} <div className="settings-list"> <h4>Итоговые права</h4> {effectivePermissions.map((permission) => ( <div key={permission.permissionKey} className="settings-row"> <span>{permissionLabel(definitionsByKey.get(permission.permissionKey), permission.permissionKey)}</span> <Badge tone={permission.allowed ? 'success' : 'neutral'}>{sourceLabel(permission.source)}</Badge> </div> ))} </div> </div> </Modal> ) } function displayName(name: string, lastName: string | null | undefined, email: string) { const fullName = `${name ?? ''} ${lastName ?? ''}`.trim() return fullName || email } function userLabel(user: UserSearchResult | undefined, fallbackId: string) { if (!user) { return fallbackId } return `${displayName(user.name, user.lastName, user.email)} <${user.email}>` } function permissionLabel(definition: PermissionDefinition | undefined, fallbackKey: string) { return permissionDisplayLabel(definition, fallbackKey) } function effectLabel(effect: PermissionEffect) { return effect === 'ALLOW' ? 'Разрешить' : 'Запретить' } function sourceLabel(source: string) { if (source === 'GRANT') { return 'Правило' } if (source === 'DENY') { return 'Запрет' } if (source === 'DEFAULT') { return 'По умолчанию' } if (source === 'GROUP') { return 'Группа' } if (source === 'OWNER') { return 'Владелец' } return source }