Sistema de Permissões
- Visão Geral
- Estrutura de Dados
- Authorizers
- Verificação de Permissões
- Erros de Autorização
- Uso em Endpoints
- Fluxo de Autenticação
- Referência de Permissões por Feature
Visão Geral
Seção intitulada “Visão Geral”O sistema de permissões utiliza uma abordagem híbrida:
- TokenScope: Define o nível de acesso hierárquico (Admin, Owner, User)
- Permission: Permissões granulares para colaboradores (Users)
- CollaborationRole: Papel do usuário na colaboração
Regra Principal
Seção intitulada “Regra Principal”| Tipo de Usuário | Comportamento |
|---|---|
| Admin/Owner | Acesso total automático - ignora verificação de permissions |
| User (Colaborador) | Acesso controlado - verifica array de permissions |
Estrutura de Dados
Seção intitulada “Estrutura de Dados”TokenScope
Seção intitulada “TokenScope”const enum TokenScope { Refresh = "refresh", // Apenas para renovar tokens User = "user", // Colaborador com permissões limitadas Owner = "owner", // Dono da conta - acesso total Admin = "admin", // Administrador do sistema - acesso total + funções admin}Permission
Seção intitulada “Permission”const enum Permission { DASHBOARD = "dashboard", INBOX = "inbox", LIVECHAT = "livechat", CONTACTS = "contacts", RANKING = "ranking", WORKFLOWS = "workflows", TEMPLATES = "templates", SETTINGS = "settings", TEAM = "team", BILLING = "billing",}
const ALL_PERMISSIONS: Permission[] = [ Permission.DASHBOARD, Permission.INBOX, Permission.LIVECHAT, Permission.CONTACTS, Permission.RANKING, Permission.WORKFLOWS, Permission.TEMPLATES, Permission.SETTINGS, Permission.TEAM, Permission.BILLING,];CollaborationRole
Seção intitulada “CollaborationRole”const enum CollaborationRole { User = "user", // Colaborador padrão Owner = "owner", // Proprietário}UserIdentity
Seção intitulada “UserIdentity”type UserIdentity = { scopes: TokenScope[]; permissions: Permission[]; accountId: string; userId: string;};Authorizers
Seção intitulada “Authorizers”Os authorizers são funções que validam o token JWT e extraem a identity do usuário.
Tipos Disponíveis
Seção intitulada “Tipos Disponíveis”| Authorizer | Scopes Aceitos | Uso |
|---|---|---|
publicUsers | Admin, Owner, User | Operações gerais |
publicOwnerUsers | Admin, Owner | Gerenciamento de equipe, billing |
publicAdminUsers | Admin | Funções administrativas do sistema |
Implementação
Seção intitulada “Implementação”export function publicUsers( request: AuthorizeHeaderRequest,): AuthorizeResponse { const token = getAuthorizationToken(request.headers); const identity = parseToken(token, [ TokenScope.Admin, TokenScope.Owner, TokenScope.User, ]); return { identity };}export function publicOwnerUsers( request: AuthorizeHeaderRequest,): AuthorizeResponse { const token = getAuthorizationToken(request.headers); const identity = parseToken(token, [TokenScope.Admin, TokenScope.Owner]); return { identity };}export function publicAdminUsers( request: AuthorizeHeaderRequest,): AuthorizeResponse { const token = getAuthorizationToken(request.headers); const identity = parseToken(token, [TokenScope.Admin]); return { identity };}Uso nas Rotas
Seção intitulada “Uso nas Rotas”export type InvitationsRoutes = [ { path: "POST /api/console/create-invitation"; authorizer: typeof publicUsers; // Qualquer usuário pode chamar handler: typeof createInvitationHandler; }, { path: "PUT /api/console/update-user-permissions/{userId}"; authorizer: typeof publicOwnerUsers; // Apenas owner/admin handler: typeof updateUserPermissionsHandler; },];Verificação de Permissões
Seção intitulada “Verificação de Permissões”Função requirePermission
Seção intitulada “Função requirePermission”import { HttpForbiddenError } from "@ez4/gateway";import { TokenScope, type Permission, type UserIdentity } from "@gaio/common";
export const requirePermission = ( identity: UserIdentity, requiredPermission: Permission,): void => { // Admin e Owner sempre têm acesso if (hasElevatedScope(identity.scopes)) { return; }
// User precisa ter a permissão específica if (!identity.permissions?.includes(requiredPermission)) { throw new HttpForbiddenError(`Missing permission: ${requiredPermission}`); }};
const hasElevatedScope = (scopes: TokenScope[]): boolean => { return scopes.includes(TokenScope.Admin) || scopes.includes(TokenScope.Owner);};Lógica de Verificação de Permissões
Seção intitulada “Lógica de Verificação de Permissões”1. Verifica se scope = 'Admin' ou 'Owner' └─ Sim → Acesso liberado (return) └─ Não → Continua
2. Verifica se permissions[] contém a permissão requerida └─ Sim → Acesso liberado (return) └─ Não → Lança HttpForbiddenError (403)Erros de Autorização
Seção intitulada “Erros de Autorização”Códigos HTTP
Seção intitulada “Códigos HTTP”| Código | Tipo | Quando Ocorre |
|---|---|---|
| 401 Unauthorized | Autenticação | Token inválido, expirado ou ausente |
| 403 Forbidden | Autorização | Usuário autenticado mas sem permissão |
Erro 403 - Forbidden
Seção intitulada “Erro 403 - Forbidden”O erro 403 é retornado quando:
-
Scope insuficiente: Usuário tenta acessar rota que exige scope maior
- Exemplo: Usuário tentando acessar rota com
publicOwnerUsers
- Exemplo: Usuário tentando acessar rota com
-
Permission ausente: Usuário (scope User) não possui a permission requerida
- Exemplo: Usuário sem
TEAMtentando criar convite
- Exemplo: Usuário sem
Formato da Resposta
Seção intitulada “Formato da Resposta”{ "message": "Missing permission: team"}Cenários
Seção intitulada “Cenários”| Cenário | Resultado |
|---|---|
Usuário sem TEAM chama POST /create-invitation | 403 - Missing permission: team |
Usuário sem WORKFLOWS chama POST /create-workflow | 403 - Missing permission: workflows |
Usuário tenta PUT /update-user-permissions (rota Owner) | 403 - Forbidden |
| Token expirado em qualquer rota | 401 - Unauthorized |
Tratamento no Cliente
Seção intitulada “Tratamento no Cliente”try { await api.createInvitation(data);} catch (error) { if (error.status === 401) { // Redirecionar para login - token inválido redirectToLogin(); } else if (error.status === 403) { // Mostrar mensagem de permissão negada showError("Você não tem permissão para esta ação"); }}Uso em Endpoints
Seção intitulada “Uso em Endpoints”Exemplo Básico
Seção intitulada “Exemplo Básico”import { Permission } from "@gaio/common";import { requirePermission } from "../../permissions/utils/check-permission.js";
export async function createInvitationHandler( request: CreateInvitationRequest, context: Service.Context<InvitationsProvider>,): Promise<Http.SuccessEmptyResponse> { // Verifica permissão ANTES de qualquer operação requirePermission(request.identity, Permission.TEAM);
const { accountId, userId } = request.identity; // ... resto da implementação}Exemplo com Múltiplas Verificações
Seção intitulada “Exemplo com Múltiplas Verificações”export async function updateWorkflowHandler( request: UpdateWorkflowRequest, context: Service.Context<WorkflowsProvider>,): Promise<UpdateWorkflowResponse> { requirePermission(request.identity, Permission.WORKFLOWS);
const { accountId } = request.identity; const { workflowId } = request.parameters;
// Se for template, precisa de permissão adicional const workflow = await readWorkflow(consoleDb, accountId, workflowId);
if (workflow.is_template) { requirePermission(request.identity, Permission.TEMPLATES); }
// ... resto da implementação}Permissões por Domínio
Seção intitulada “Permissões por Domínio”| Domínio | Permission | Descrição |
|---|---|---|
| Tags | CONTACTS | CRUD de tags |
| Contacts | CONTACTS | CRUD de contatos |
| Workflows | WORKFLOWS | CRUD de workflows |
| Replies | WORKFLOWS | Respostas rápidas |
| Templates | TEMPLATES | Marketplace |
| Invitations | TEAM | Convites de equipe |
| Collaborations | TEAM | Gerenciamento de colaboradores |
| Settings | SETTINGS | Configurações da conta |
| Billing | BILLING | Faturamento |
Fluxo de Autenticação
Seção intitulada “Fluxo de Autenticação”Geração de Token
Seção intitulada “Geração de Token”export const issueToken = ( accountId: string, userId: string, scopes: TokenScope[], permissions: Permission[] = [], options?: IssueTokenOptions,) => { const accessPayload = { iss: accountId, sub: userId, scopes, permissions, };
return { accessToken: jwt.sign(accessPayload, issuerSecret, { expiresIn: issuerAccessTTL, }), refreshToken: jwt.sign(refreshPayload, issuerSecret, { expiresIn: issuerRefreshTTL, }), };};Determinação de Scope e Permissions
Seção intitulada “Determinação de Scope e Permissions”export const fetchUserScopeAndPermissions = async ( client: DbClient, accountId: string, userId: string,): Promise<FetchUserScopeAndPermissionsOutput | undefined> => { const [account, collaboration] = await Promise.all([ // Verifica se usuário e owner da conta client.accounts.findOne({ select: { id: true, status: true, type: true }, where: { deleted_at: null, owner_user_id: userId, id: accountId }, }), // Verifica se usuário tem colaboração client.collaborations.findOne({ select: { role: true }, where: { deleted_at: null, account_id: accountId, user_id: userId }, }), ]);
// Owner da conta: todas as permissões if (account) { return { scope: getAccountScope(account), // owner ou admin permissions: getAllPermissions(), }; }
// Colaborador: permissões do banco if (collaboration) { const permissions = await fetchUserPermissions(client, accountId, userId); return { scope: roleToScope(collaboration.role), // user permissions, }; }
return undefined;};Mapeamento Account → Scope
Seção intitulada “Mapeamento Account → Scope”const getAccountScope = (account: Pick<AccountSchema, "status" | "type">) => { if (account.type === AccountType.System) { return TokenScope.Admin; }
return TokenScope.Owner;};Mapeamento Role → Scope
Seção intitulada “Mapeamento Role → Scope”export const roleToScope = (role: CollaborationRole) => { switch (role) { case CollaborationRole.Owner: return TokenScope.Owner; default: return TokenScope.User; }};Referência de Permissões por Feature
Seção intitulada “Referência de Permissões por Feature”Contatos
Seção intitulada “Contatos”requirePermission(request.identity, Permission.CONTACTS);
// tags/endpoints/update.tsrequirePermission(request.identity, Permission.CONTACTS);
// tags/endpoints/delete.tsrequirePermission(request.identity, Permission.CONTACTS);
// contacts/endpoints/*.tsrequirePermission(request.identity, Permission.CONTACTS);Workflows
Seção intitulada “Workflows”// workflows/endpoints/*.tsrequirePermission(request.identity, Permission.WORKFLOWS);
// replies/endpoints/*.tsrequirePermission(request.identity, Permission.WORKFLOWS);Templates
Seção intitulada “Templates”requirePermission(request.identity, Permission.TEMPLATES);
// template-packages/endpoints/install.tsrequirePermission(request.identity, Permission.TEMPLATES);requirePermission(request.identity, Permission.TEAM);
// invitations/endpoints/update.tsrequirePermission(request.identity, Permission.TEAM);
// collaborations/endpoints/update.tsrequirePermission(request.identity, Permission.TEAM);Endpoints de Gerenciamento de Permissões
Seção intitulada “Endpoints de Gerenciamento de Permissões”Listar Todas as Permissões
Seção intitulada “Listar Todas as Permissões”GET /api/console/list-permissionsAuthorizer: publicUsersRetorna array com todas as permissões disponíveis (ALL_PERMISSIONS).
Ler Permissões de Usuário
Seção intitulada “Ler Permissões de Usuário”GET /api/console/read-user-permissions/{userId}Authorizer: publicOwnerUsersRetorna as permissões atribuídas a um colaborador específico.
Atualizar Permissões de Usuário
Seção intitulada “Atualizar Permissões de Usuário”PUT /api/console/update-user-permissions/{userId}Authorizer: publicOwnerUsersBody: { "permissions": ["inbox", "contacts"] }Define as permissões de um colaborador (substitui todas).
Checklist para Novos Endpoints
Seção intitulada “Checklist para Novos Endpoints”- Definir authorizer apropriado na rota (
publicUsers,publicOwnerUsers,publicAdminUsers) - Adicionar
requirePermission()no início do handler se necessário - Usar a Permission correta para o domínio
- Testar com usuário Owner (deve ter acesso)
- Testar com usuário User sem a permissão (deve retornar 403)
- Testar com usuário User com a permissão (deve ter acesso)