refactor: Remove dedicated role management page and update authentication, roles, and permission handling across backend and frontend.

This commit is contained in:
2025-12-05 14:29:34 -03:00
parent c8d717b315
commit 69f32a342c
16 changed files with 358 additions and 958 deletions

View File

@@ -421,10 +421,7 @@ export const obterChamado = query({
throw new Error('Chamado não encontrado');
}
const podeVer =
ticket.solicitanteId === usuario._id ||
ticket.responsavelId === usuario._id ||
ticket.setorResponsavel === usuario.setor;
const podeVer = ticket.solicitanteId === usuario._id || ticket.responsavelId === usuario._id;
if (!podeVer) {
throw new Error('Acesso negado ao chamado');
@@ -524,7 +521,6 @@ export const atribuirResponsavel = mutation({
await ctx.db.patch(ticket._id, {
responsavelId: args.responsavelId,
setorResponsavel: responsavel.setor,
atualizadoEm: agora
});

View File

@@ -2056,8 +2056,7 @@ export const obterUsuariosOnline = query({
email: u.email,
fotoPerfil: u.fotoPerfil,
statusPresenca: u.statusPresenca,
statusMensagem: u.statusMensagem,
setor: u.setor
statusMensagem: u.statusMensagem
}));
}
});
@@ -2101,8 +2100,7 @@ export const listarTodosUsuarios = query({
fotoPerfil: u.fotoPerfil,
fotoPerfilUrl,
statusPresenca: u.statusPresenca,
statusMensagem: u.statusMensagem,
setor: u.setor
statusMensagem: u.statusMensagem
};
})
);

View File

@@ -4,7 +4,7 @@ import { getCurrentUserFunction } from './auth';
/**
* Retorna as permissões do usuário atual para o frontend filtrar o menu localmente
* Retorna:
* - isMaster: true se o usuário é TI Master ou Admin (nível <= 1)
* - isMaster: true se o usuário é Admin
* - permissions: Set de strings no formato "recurso.acao" (ex: "funcionarios.listar")
*/
export const getUserPermissions = query({
@@ -20,8 +20,8 @@ export const getUserPermissions = query({
return { isMaster: false, permissions: [] };
}
// Se for TI Master ou Admin (nivel <= 1), retorna flag de master
if (role.nivel <= 1) {
// Se for Admin, retorna flag de master
if (role.admin === true) {
return { isMaster: true, permissions: [] };
}

View File

@@ -340,10 +340,10 @@ export const verificarAlertasInternal = internalMutation({
// Criar notificação no chat se configurado
if (alerta.notifyByChat) {
// Buscar roles administrativas (nível <= 1) e filtrar usuários por roleId
// Buscar roles administrativas (admin === true) e filtrar usuários por roleId
const rolesAdminOuTi = await ctx.db
.query('roles')
.filter((q) => q.lte(q.field('nivel'), 1))
.filter((q) => q.eq(q.field('admin'), true))
.collect();
const rolesPermitidas = new Set(rolesAdminOuTi.map((r) => r._id));
@@ -368,7 +368,7 @@ export const verificarAlertasInternal = internalMutation({
// Buscar usuários administradores/TI para receber o alerta por email
const rolesAdminOuTi = await ctx.db
.query('roles')
.filter((q) => q.lte(q.field('nivel'), 1))
.filter((q) => q.eq(q.field('admin'), true))
.collect();
const rolesPermitidas = new Set(rolesAdminOuTi.map((r) => r._id));

View File

@@ -633,8 +633,8 @@ export const verificarAcao = query({
const role = await ctx.db.get(usuario.roleId);
if (!role) throw new Error('acesso_negado');
// Níveis administrativos têm acesso total
if (role.nivel <= 1) return null;
// Admins têm acesso total
if (role.admin === true) return null;
// Encontrar permissão
const permissao = await ctx.db
@@ -665,7 +665,7 @@ export const assertPermissaoAcaoAtual = internalQuery({
const role = await ctx.db.get(usuarioAtual.roleId);
if (!role) throw new Error('acesso_negado');
if (role.nivel <= 1) return null;
if (role.admin === true) return null;
const permissao = await ctx.db
.query('permissoes')

View File

@@ -25,8 +25,7 @@ export const buscarPorId = query({
_id: v.id('roles'),
nome: v.string(),
descricao: v.string(),
nivel: v.number(),
setor: v.optional(v.string())
admin: v.optional(v.boolean())
}),
v.null()
),
@@ -49,8 +48,6 @@ export const criar = mutation({
args: {
nome: v.string(),
descricao: v.string(),
nivel: v.number(),
setor: v.optional(v.string()),
copiarDeRoleId: v.optional(v.id('roles'))
},
returns: v.union(
@@ -64,7 +61,7 @@ export const criar = mutation({
}
const roleAtual = await ctx.db.get(usuarioAtual.roleId);
if (!roleAtual || roleAtual.nivel > 1) {
if (!roleAtual || roleAtual.admin !== true) {
return { sucesso: false as const, erro: 'sem_permissao' };
}
@@ -98,20 +95,12 @@ export const criar = mutation({
permissoesParaCopiar = permissoesOrigem.map((item) => item.permissaoId);
}
// Agora só existem níveis 0 e 1.
// 0 = máximo (acesso total), 1 = administrativo (também com acesso total).
// Qualquer valor informado diferente de 0 é normalizado para 1.
const nivelNormalizado = Math.round(args.nivel) <= 0 ? 0 : 1;
const setor = args.setor?.trim();
// Novos perfis criados NÃO são admin por padrão
// O campo admin só pode ser alterado posteriormente por um admin existente
const roleId = await ctx.db.insert('roles', {
nome: nomeNormalizado,
descricao: args.descricao.trim() || args.nome.trim(),
nivel: nivelNormalizado,
setor: setor && setor.length > 0 ? setor : undefined,
customizado: true,
criadoPor: usuarioAtual._id,
editavel: true
admin: false
});
if (permissoesParaCopiar.length > 0) {
@@ -128,21 +117,140 @@ export const criar = mutation({
});
/**
* Migração de níveis de roles para o novo modelo (apenas 0 e 1).
* - Mantém níveis 0 e 1 como estão.
* - Converte qualquer nível > 1 para 1.
* Editar uma role existente
* Apenas admins podem editar roles
*/
export const migrarNiveisRoles = internalMutation({
export const editar = mutation({
args: {
roleId: v.id('roles'),
nome: v.optional(v.string()),
descricao: v.optional(v.string())
},
returns: v.union(
v.object({ sucesso: v.literal(true) }),
v.object({ sucesso: v.literal(false), erro: v.string() })
),
handler: async (ctx, args) => {
const usuarioAtual = await getCurrentUserFunction(ctx);
if (!usuarioAtual) {
return { sucesso: false as const, erro: 'nao_autenticado' };
}
const roleAtual = await ctx.db.get(usuarioAtual.roleId);
if (!roleAtual || roleAtual.admin !== true) {
return { sucesso: false as const, erro: 'sem_permissao' };
}
const roleParaEditar = await ctx.db.get(args.roleId);
if (!roleParaEditar) {
return { sucesso: false as const, erro: 'role_nao_encontrada' };
}
// Se estiver alterando o nome, verificar se já existe
if (args.nome) {
const nomeNormalizado = slugify(args.nome);
if (!nomeNormalizado) {
return { sucesso: false as const, erro: 'nome_invalido' };
}
if (nomeNormalizado !== roleParaEditar.nome) {
const existente = await ctx.db
.query('roles')
.withIndex('by_nome', (q) => q.eq('nome', nomeNormalizado))
.unique();
if (existente) {
return { sucesso: false as const, erro: 'nome_ja_utilizado' };
}
}
await ctx.db.patch(args.roleId, { nome: nomeNormalizado });
}
if (args.descricao !== undefined) {
await ctx.db.patch(args.roleId, { descricao: args.descricao.trim() });
}
return { sucesso: true as const };
}
});
/**
* Excluir uma role
* Apenas admins podem excluir roles
* Não pode excluir role que tenha usuários vinculados
*/
export const excluir = mutation({
args: {
roleId: v.id('roles')
},
returns: v.union(
v.object({ sucesso: v.literal(true) }),
v.object({ sucesso: v.literal(false), erro: v.string() })
),
handler: async (ctx, args) => {
const usuarioAtual = await getCurrentUserFunction(ctx);
if (!usuarioAtual) {
return { sucesso: false as const, erro: 'nao_autenticado' };
}
const roleAtual = await ctx.db.get(usuarioAtual.roleId);
if (!roleAtual || roleAtual.admin !== true) {
return { sucesso: false as const, erro: 'sem_permissao' };
}
const roleParaExcluir = await ctx.db.get(args.roleId);
if (!roleParaExcluir) {
return { sucesso: false as const, erro: 'role_nao_encontrada' };
}
// Verificar se existem usuários vinculados
const usuariosVinculados = await ctx.db
.query('usuarios')
.withIndex('by_role', (q) => q.eq('roleId', args.roleId))
.first();
if (usuariosVinculados) {
return { sucesso: false as const, erro: 'role_possui_usuarios' };
}
// Excluir permissões vinculadas primeiro
const permissoesVinculadas = await ctx.db
.query('rolePermissoes')
.withIndex('by_role', (q) => q.eq('roleId', args.roleId))
.collect();
for (const permissao of permissoesVinculadas) {
await ctx.db.delete(permissao._id);
}
// Excluir a role
await ctx.db.delete(args.roleId);
return { sucesso: true as const };
}
});
/**
* Migração de roles para o novo modelo com campo admin.
* - Perfis com nivel === 0 tornam-se admin: true
* - Todos os outros tornam-se admin: false
*/
export const migrarParaAdmin = internalMutation({
args: {},
returns: v.null(),
handler: async (ctx) => {
const roles = await ctx.db.query('roles').collect();
for (const role of roles) {
if (role.nivel <= 1) continue;
// Se já tem o campo admin definido, pula
if (role.admin !== undefined) continue;
// Perfis que eram nivel 0 (ti_master, admin) tornam-se admin: true
const isAdmin = (role as unknown as { nivel?: number }).nivel === 0;
await ctx.db.patch(role._id, {
nivel: 1
admin: isAdmin
});
}

View File

@@ -1524,7 +1524,7 @@ export const dispararAlertasInternos = internalMutation({
const rolesTi = await ctx.db
.query('roles')
.withIndex('by_nivel', (q) => q.lte('nivel', 1))
.filter((q) => q.eq(q.field('admin'), true))
.collect();
const usuariosNotificados: Id<'usuarios'>[] = [];

View File

@@ -172,13 +172,7 @@ export const seedCreateRoles = internalMutation({
returns: v.null(),
handler: async (ctx) => {
console.log('🔐 Criando roles...');
const ensureRole = async (
nome: string,
descricao: string,
nivel: number,
setor?: string,
editavel?: boolean
) => {
const ensureRole = async (nome: string, descricao: string, admin: boolean) => {
const existing = await ctx.db
.query('roles')
.withIndex('by_nome', (q) => q.eq('nome', nome))
@@ -190,23 +184,20 @@ export const seedCreateRoles = internalMutation({
const id = await ctx.db.insert('roles', {
nome,
descricao,
nivel,
setor,
customizado: false,
editavel
admin
});
console.log(` ✅ Role criada: ${nome}`);
return id;
};
// Níveis agora são apenas 0 e 1.
// 0 = máximo, 1 = administrativo (ambos com acesso total).
await ensureRole('ti_master', 'TI Master', 0, 'ti', false);
await ensureRole('admin', 'Administrador Geral', 1, 'administrativo', true);
await ensureRole('ti_usuario', 'TI Usuário', 1, 'ti', true);
await ensureRole('rh', 'Recursos Humanos', 1, 'recursos_humanos', false);
await ensureRole('financeiro', 'Financeiro', 1, 'financeiro', false);
await ensureRole('usuario', 'Usuário Padrão', 1, undefined, false);
// admin: true = acesso total ao sistema
// admin: false = permissões via rolePermissoes
await ensureRole('ti_master', 'TI Master', true);
await ensureRole('admin', 'Administrador Geral', true);
await ensureRole('ti_usuario', 'TI Usuário', false);
await ensureRole('rh', 'Recursos Humanos', false);
await ensureRole('financeiro', 'Financeiro', false);
await ensureRole('usuario', 'Usuário Padrão', false);
// Encadeia próximas etapas
await ctx.scheduler.runAfter(0, internal.seed.seedCreateSimbolos, {});
await ctx.scheduler.runAfter(0, internal.seed.seedCreatePermissoesBase, {});

View File

@@ -26,7 +26,6 @@ export const authTables = {
fotoPerfil: v.optional(v.id('_storage')),
avatar: v.optional(v.string()), // URL do avatar gerado (ex: DiceBear)
setor: v.optional(v.string()),
statusMensagem: v.optional(v.string()), // max 100 chars
statusPresenca: v.optional(
v.union(
@@ -53,16 +52,8 @@ export const authTables = {
roles: defineTable({
nome: v.string(), // "admin", "ti_master", "ti_usuario", "usuario_avancado", "usuario"
descricao: v.string(),
nivel: v.number(), // 0 = admin, 1 = ti_master, 2 = ti_usuario, 3+ = customizado
setor: v.optional(v.string()), // "ti", "rh", "financeiro", etc.
customizado: v.optional(v.boolean()), // se é um perfil customizado criado por TI_MASTER
criadoPor: v.optional(v.id('usuarios')), // usuário TI_MASTER que criou este perfil
editavel: v.optional(v.boolean()) // se pode ser editado (false para roles fixas)
})
.index('by_nome', ['nome'])
.index('by_nivel', ['nivel'])
.index('by_setor', ['setor'])
.index('by_customizado', ['customizado']),
admin: v.optional(v.boolean()) // true = acesso total ao sistema, false/undefined = permissões via rolePermissoes
}).index('by_nome', ['nome']),
permissoes: defineTable({
nome: v.string(), // "funcionarios.criar", "simbolos.editar", etc.

View File

@@ -223,7 +223,7 @@ export const listar = query({
_id: usuario.roleId,
descricao: 'Perfil não encontrado' as const,
nome: 'erro_role_ausente' as const,
nivel: 999 as const,
admin: false as const,
erro: true as const
},
funcionario,
@@ -237,11 +237,6 @@ export const listar = query({
continue;
}
// Filtrar por setor
if (args.setor && role.setor !== args.setor) {
continue;
}
// Buscar funcionário associado
let funcionario;
if (usuario.funcionarioId) {
@@ -264,18 +259,11 @@ export const listar = query({
}
}
// Construir objeto role - incluir _creationTime se existir (campo automático do Convex)
const roleObj = {
_id: role._id,
descricao: role.descricao,
nome: role.nome,
nivel: role.nivel,
...(role.criadoPor !== undefined && { criadoPor: role.criadoPor }),
...(role.customizado !== undefined && {
customizado: role.customizado
}),
...(role.editavel !== undefined && { editavel: role.editavel }),
...(role.setor !== undefined && { setor: role.setor })
admin: role.admin ?? false
};
const matriculaUsuario = await obterMatriculaUsuario(ctx, usuario);
@@ -514,7 +502,6 @@ export const atualizarPerfil = mutation({
if (args.fotoPerfil !== undefined) updates.fotoPerfil = args.fotoPerfil;
if (args.avatar !== undefined) updates.avatar = args.avatar;
if (args.setor !== undefined) updates.setor = args.setor;
if (args.statusMensagem !== undefined) updates.statusMensagem = args.statusMensagem;
if (args.statusPresenca !== undefined) {
updates.statusPresenca = args.statusPresenca;
@@ -610,7 +597,6 @@ export const obterPerfil = query({
fotoPerfil: usuarioAtual.fotoPerfil,
fotoPerfilUrl,
avatar: usuarioAtual.avatar,
setor: usuarioAtual.setor,
statusMensagem: usuarioAtual.statusMensagem,
statusPresenca: usuarioAtual.statusPresenca,
notificacoesAtivadas: usuarioAtual.notificacoesAtivadas ?? true,
@@ -941,7 +927,6 @@ export const editarUsuario = mutation({
if (args.nome !== undefined) updates.nome = args.nome;
if (args.email !== undefined) updates.email = args.email;
if (args.roleId !== undefined) updates.roleId = args.roleId;
if (args.setor !== undefined) updates.setor = args.setor;
await ctx.db.patch(args.usuarioId, updates);
@@ -987,10 +972,7 @@ export const criarAdminMaster = mutation({
const roleId = await ctx.db.insert('roles', {
nome: 'ti_master',
descricao: 'TI Master',
nivel: 0,
setor: 'ti',
customizado: false,
editavel: false
admin: true
});
roleTIMaster = await ctx.db.get(roleId);
}