Como adaptar UX / UI sob permissões

Muitos projetos têm processos de autenticação (em um grau ou outro). Muitas práticas recomendadas foram escritas em todas as tecnologias conhecidas etc. etc.


Mas o usuário fez um login e? Afinal, ele não pode fazer tudo. Como determinar o que ele pode ver e o que não. Quais botões ele tem o direito de pressionar, o que pode alterar o que criar ou excluir.


Neste artigo, quero considerar uma abordagem para resolver esses problemas em aplicativos da web.


imagem


Para começar, a autorização real / efetiva só pode acontecer no servidor. No Front End, só podemos melhorar a interface do usuário (hmm, o idioma russo é poderoso, mas nem sempre ...), também é a experiência do usuário além do UX. Podemos ocultar os botões nos quais o usuário não tem o direito de clicar, impedir que ele acesse a página ou mostrar uma mensagem de que ele não tem direitos para essa ou aquela ação.


E aqui surge a pergunta: como fazê-lo da maneira mais correta possível.


Vamos começar definindo o problema.


Criamos o aplicativo Todo e ele tem diferentes tipos de usuários:


USUÁRIO - pode ver todas as tarefas e alterá-las (definir e remover V), mas não pode excluir ou criar e não vê estatísticas.


ADMIN - pode ver todas as tarefas e criar novas, mas não vê estatísticas.


SUPER_ADMIN - pode ver todas as tarefas, criar novas e excluir, também pode ver estatísticas.


ver tarefaCriar tarefamarcar / desmarcar tarefa (atualização)excluir tarefaver estatísticas
USERVXVXX
AdminVVVXX
SUPER_ADMINVVVVV

Em tal situação, podemos facilmente conviver com os papéis. No entanto, a situação pode mudar muito. Imagine que existe um usuário que deve ter os mesmos direitos que ADMIN, além de excluir tarefas. Ou apenas USUÁRIO com a capacidade de ver estatísticas.


Uma solução simples é criar novas funções.
Mas em sistemas grandes, com um número relativamente grande de usuários, rapidamente nos perderemos em um grande número de funções ... E aqui lembramos as permissões de "direitos de usuário". Para facilitar o gerenciamento, podemos criar grupos de várias permissões e anexá-las ao usuário. Sempre há a oportunidade de adicionar permissão específica a um usuário específico.


Soluções semelhantes podem ser encontradas em muitos serviços excelentes. AWS, Google Cloud, SalesForce etc.
Além disso, soluções semelhantes já estão implementadas em muitas estruturas, por exemplo, Django (python).


Eu quero dar um exemplo de implementação para aplicativos Angular. (No exemplo do mesmo aplicativo ToDo).


Primeiro você precisa determinar todas as permissões possíveis.


Dividido em recursos


  1. Temos tarefas e estatísticas.
  2. Determinamos as ações possíveis com cada uma delas.
    • Tarefa: criar, ler, atualizar, excluir
    • Estatísticas: leitura (no nosso exemplo, somente visualização)
  3. Crie um mapa (MAP) de funções e permissões

export const permissionsMap = { todos:{ create:'*', read:'*', update:'*', delete:'*' }, stats:'*' } 

Na minha opinião, esta é a melhor opção, mas na maioria dos casos o servidor retornará algo como isto:


 export permissions = [ 'todos_create', 'todos_read', 'todos_update', 'todos_delete', 'stas' ] 

Menos legível, mas também muito bom.


É assim que nosso aplicativo se parece se o usuário tiver todas as permissões possíveis.


imagem


Vamos começar com USER, é assim que suas permissões são:


 export const USERpermissionsMap = { todos:{ read:'*', update:'*', } } 

1) O USUÁRIO não pode ver as estatísticas, ou seja, em princípio, não pode ir para a página de estatísticas.


Para tais situações, a Angular possui guardas, registradas no nível de rotas ( documentação ).


No nosso caso, fica assim:


 const routes: Routes = [ // {...Other routes}, { path: 'stats', component: TodoStatisticsComponent, canActivate: [ PermissionsGuardService ], data: { permission: 'stats' }, } ]; 

Você deve prestar atenção ao objeto nos dados.


peremissions = 'stats', essas são as permissões que o usuário deve ter para ter acesso a esta página.


Com base no data.permissions exigido e nas permissões que o servidor PermissionsGuardService nos deu, ele decidirá se deve ou não deixar o USER ir para a página '/ stats'.


 @Injectable({ providedIn: 'root' }) export class PermissionsGuardService implements CanActivate { constructor( private store: Store<any>) { } canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | boolean { // Required permission to continue navigation const required = route.data.permission; // User permissions that we got from server const userPerms = this.getPermissions(); // verification const isPermitted = checkPermissions(required, userPerms); if (!isPermitted) { alert('ROUTE GUARD SAYS: \n You don\'t have permissions to see this page'); } return isPermitted; } getPermissions() { return localStorage.get('userPermissions') } } 

Esta função contém a lógica da decisão - existe uma permissão insubstituível (necessária) entre todas as permissões do USER (userPerms).


 export function checkPermissions(required: string, userPerms) { // 1) Separate feature and action const [feature, action] = required.split('_'); // 2) Check if user have any type of access to the feature if (!userPerms.hasOwnProperty(feature)) { return false; } // 3) Check if user have permission for required action if (!userPerms[feature].hasOwnProperty(action)) { return false; } return true; } 

imagem


E assim Nosso USUÁRIO não pode acessar a página de estatísticas. No entanto, ele ainda pode criar tarefas e excluí-las.


Para que o USER não consiga remover a tarefa, basta remover o vermelho (X) da linha de tarefas.
Para esse fim, usaremos a Diretiva Estrutural .


 <!-- Similar to other Structural Directives in angular (*ngIF, *ngFor..) we have '*' in directive name Input for directive is a required permission to see this btn. --> <button *appPermissions="'todos_delete'" class="delete-button" (click)="removeSingleTodo()"> X </button> 

 @Directive({ selector: '[appPermissions]' }) export class PermissionsDirective { private _required: string; private _viewRef: EmbeddedViewRef<any> | null = null; private _templateRef: TemplateRef<any> | null = null; @Input() set appPermissions(permission: string) { this._required = permission; this._viewRef = null; this.init(); } constructor(private templateRef: TemplateRef<any>, private viewContainerRef: ViewContainerRef) { this._templateRef = templateRef; } init() { const isPermitted = checkPermissions(this._required, this.getPermissions()); if (isPermitted) { this._viewRef = this.viewContainerRef.createEmbeddedView(this.templateRef); } else { console.log('PERMISSIONS DIRECTIVE says \n You don\'t have permissions to see it'); } } getPermissions() { localStorage.get('userPermissions') } } 

imagem


Agora, USER não vê o botão DELETE, mas ainda pode adicionar novas tarefas.


Para remover um campo de entrada - estragará todo o tipo de nossa aplicação. A solução correta nessa situação é disable campos de entrada.


Nós vamos usar cachimbo.


 <!-- as a value `permissions` pipe will get required permissions `permissions` pipe return true or false, that's why we have !('todos_create' | permissions) to set disable=true if pipe returns false --> <input class="centered-block" [disabled]="!('todos_create' | permissions)" placeholder="What needs to be done?" autofocus/> 

 @Pipe({ name: 'permissions' }) export class PermissionsPipe implements PipeTransform { constructor(private store: Store<any>) { } transform(required: any, args?: any): any { const isPermitted = checkPermissions(required, this.getPermissions()); if (isPermitted) { return true; } else { console.log('[PERMISSIONS PIPE] You don\'t have permissions'); return false; } } getPermissions() { return localStorage.get('userPermissions') } } 

E agora o USUÁRIO só pode ver as tarefas e alterá-las (V / X). No entanto, ainda temos mais um botão chamado 'Limpar concluído'.


Suponha que tenhamos os seguintes requisitos do gerente de produto:


  1. O botão "Limpar concluído" deve estar visível para todos e sempre.
  2. Também deve ser clicável.
  3. Se USER sem as permissões correspondentes pressionar o botão, uma mensagem deverá aparecer.

A diretiva de construção não nos ajudará, assim como os tubos.
Também não é muito conveniente gravar permissões verificando funções de componente.


Tudo o que precisamos é executar uma verificação de permissões entre o clique e a execução da função vinculada.


Na minha opinião, vale a pena usar decoradores.


 export function Permissions(required) { return (classProto, propertyKey, descriptor) => { const originalFunction = descriptor.value; descriptor.value = function (...args: any[]) { const userPerms = localStorage.get('userPermissions') const isPermitted = checkPermissions(required, userPerms); if (isPermitted) { originalFunction.apply(this, args); } else { alert('you have no permissions \n [PERMISSIONS DECORATOR]'); } }; return descriptor; }; } 

 @Component({ selector: 'app-actions', templateUrl: './actions.component.html', styleUrls: ['./actions.component.css'] }) export class ActionsComponent implements OnInit { @Output() deleteCompleted = new EventEmitter(); constructor() { } @Permissions('todos_delete') public deleteCompleted() { this.deleteCompleted.emit(); } } 

Os palestrantes merecem um artigo separado.


Nosso resultado final:


imagem


O resultado:


Essa abordagem nos permite adaptar de forma fácil e dinâmica nosso UX de acordo com as permissões que o usuário possui.


E o mais importante, para isso, não precisamos colocar serviços em todos os componentes ou martelar nos modelos '* ngIf'.


Todo o aplicativo está completo .


Ficarei feliz em comentar.

Source: https://habr.com/ru/post/pt439100/


All Articles