260 lines
6.5 KiB
TypeScript
260 lines
6.5 KiB
TypeScript
import { v } from 'convex/values';
|
|
import type { Id } from './_generated/dataModel';
|
|
import { internalMutation, mutation, query } from './_generated/server';
|
|
import { getCurrentUserFunction } from './auth';
|
|
|
|
/**
|
|
* Listar todas as roles
|
|
*/
|
|
export const listar = query({
|
|
args: {},
|
|
handler: async (ctx) => {
|
|
return await ctx.db.query('roles').collect();
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Buscar role por ID
|
|
*/
|
|
export const buscarPorId = query({
|
|
args: {
|
|
roleId: v.id('roles')
|
|
},
|
|
returns: v.union(
|
|
v.object({
|
|
_id: v.id('roles'),
|
|
nome: v.string(),
|
|
descricao: v.string(),
|
|
admin: v.optional(v.boolean())
|
|
}),
|
|
v.null()
|
|
),
|
|
handler: async (ctx, args) => {
|
|
return await ctx.db.get(args.roleId);
|
|
}
|
|
});
|
|
|
|
const slugify = (value: string) =>
|
|
value
|
|
.normalize('NFD')
|
|
.replace(/[\u0300-\u036f]/g, '')
|
|
.trim()
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9]+/g, '_')
|
|
.replace(/^_+|_+$/g, '')
|
|
.replace(/_{2,}/g, '_');
|
|
|
|
export const criar = mutation({
|
|
args: {
|
|
nome: v.string(),
|
|
descricao: v.string(),
|
|
copiarDeRoleId: v.optional(v.id('roles'))
|
|
},
|
|
returns: v.union(
|
|
v.object({ sucesso: v.literal(true), roleId: v.id('roles') }),
|
|
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 nomeNormalizado = slugify(args.nome);
|
|
if (!nomeNormalizado) {
|
|
return { sucesso: false as const, erro: 'nome_invalido' };
|
|
}
|
|
|
|
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' };
|
|
}
|
|
|
|
let permissoesParaCopiar: Array<Id<'permissoes'>> = [];
|
|
|
|
if (args.copiarDeRoleId) {
|
|
const roleOrigem = await ctx.db.get(args.copiarDeRoleId);
|
|
if (!roleOrigem) {
|
|
return { sucesso: false as const, erro: 'role_origem_nao_encontrada' };
|
|
}
|
|
|
|
const permissoesOrigem = await ctx.db
|
|
.query('rolePermissoes')
|
|
.withIndex('by_role', (q) => q.eq('roleId', args.copiarDeRoleId!))
|
|
.collect();
|
|
|
|
permissoesParaCopiar = permissoesOrigem.map((item) => item.permissaoId);
|
|
}
|
|
|
|
// 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(),
|
|
admin: false
|
|
});
|
|
|
|
if (permissoesParaCopiar.length > 0) {
|
|
for (const permissaoId of permissoesParaCopiar) {
|
|
await ctx.db.insert('rolePermissoes', {
|
|
roleId,
|
|
permissaoId
|
|
});
|
|
}
|
|
}
|
|
|
|
return { sucesso: true as const, roleId };
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Editar uma role existente
|
|
* Apenas admins podem editar roles
|
|
*/
|
|
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) {
|
|
// 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, {
|
|
admin: isAdmin
|
|
});
|
|
}
|
|
|
|
return null;
|
|
}
|
|
});
|