Feat ajuste acesso #5
@@ -27,14 +27,11 @@
|
||||
"vite": "^7.1.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@convex-dev/better-auth": "^0.9.6",
|
||||
"@dicebear/collection": "^9.2.4",
|
||||
"@dicebear/core": "^9.2.4",
|
||||
"@internationalized/date": "^3.10.0",
|
||||
"@mmailaender/convex-better-auth-svelte": "^0.2.0",
|
||||
"@sgse-app/backend": "*",
|
||||
"@tanstack/svelte-form": "^1.19.2",
|
||||
"better-auth": "1.3.27",
|
||||
"convex": "^1.28.0",
|
||||
"convex-svelte": "^0.0.11",
|
||||
"date-fns": "^4.1.0",
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { createAuthClient } from "better-auth/client";
|
||||
import { convexClient } from "@convex-dev/better-auth/client/plugins";
|
||||
|
||||
export const authClient = createAuthClient({
|
||||
baseURL: "http://localhost:5173",
|
||||
plugins: [convexClient()],
|
||||
});
|
||||
83
apps/web/src/lib/components/ActionGuard.svelte
Normal file
83
apps/web/src/lib/components/ActionGuard.svelte
Normal file
@@ -0,0 +1,83 @@
|
||||
<script lang="ts">
|
||||
import { useQuery } from "convex-svelte";
|
||||
import { api } from "@sgse-app/backend/convex/_generated/api";
|
||||
import type { Id } from "@sgse-app/backend/convex/_generated/dataModel";
|
||||
import { authStore } from "$lib/stores/auth.svelte";
|
||||
import { loginModalStore } from "$lib/stores/loginModal.svelte";
|
||||
|
||||
interface Props {
|
||||
recurso: string;
|
||||
acao: string;
|
||||
children?: any;
|
||||
}
|
||||
|
||||
let { recurso, acao, children }: Props = $props();
|
||||
|
||||
let verificando = $state(true);
|
||||
let permitido = $state(false);
|
||||
|
||||
const permissaoQuery = $derived(
|
||||
authStore.usuario
|
||||
? useQuery(api.permissoesAcoes.verificarAcao, {
|
||||
usuarioId: authStore.usuario._id as Id<"usuarios">,
|
||||
recurso,
|
||||
acao,
|
||||
})
|
||||
: null
|
||||
);
|
||||
|
||||
$effect(() => {
|
||||
if (!authStore.autenticado) {
|
||||
verificando = false;
|
||||
permitido = false;
|
||||
const currentPath = window.location.pathname;
|
||||
loginModalStore.open(currentPath);
|
||||
return;
|
||||
}
|
||||
|
||||
if (permissaoQuery?.error) {
|
||||
verificando = false;
|
||||
permitido = false;
|
||||
} else if (permissaoQuery && !permissaoQuery.isLoading) {
|
||||
// Backend retorna null quando permitido
|
||||
verificando = false;
|
||||
permitido = true;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if verificando}
|
||||
<div class="flex items-center justify-center min-h-screen">
|
||||
<div class="text-center">
|
||||
<span class="loading loading-spinner loading-lg text-primary"></span>
|
||||
<p class="mt-4 text-base-content/70">Verificando permissões...</p>
|
||||
</div>
|
||||
</div>
|
||||
{:else if permitido}
|
||||
{@render children?.()}
|
||||
{:else}
|
||||
<div class="flex items-center justify-center min-h-screen">
|
||||
<div class="text-center">
|
||||
<div class="p-4 bg-error/10 rounded-full inline-block mb-4">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-16 w-16 text-error"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="text-2xl font-bold text-base-content mb-2">Acesso Negado</h2>
|
||||
<p class="text-base-content/70">
|
||||
Você não tem permissão para acessar esta ação.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -1,84 +1,66 @@
|
||||
<script lang="ts">
|
||||
import { page } from "$app/state";
|
||||
import MenuProtection from "$lib/components/MenuProtection.svelte";
|
||||
import ActionGuard from "$lib/components/ActionGuard.svelte";
|
||||
|
||||
const { children } = $props();
|
||||
|
||||
// Mapa de rotas para verificação de permissões
|
||||
const ROUTE_PERMISSIONS: Record<string, { path: string; requireGravar?: boolean }> = {
|
||||
// Recursos Humanos
|
||||
"/recursos-humanos": { path: "/recursos-humanos" },
|
||||
"/recursos-humanos/funcionarios": { path: "/recursos-humanos/funcionarios" },
|
||||
"/recursos-humanos/funcionarios/cadastro": { path: "/recursos-humanos/funcionarios", requireGravar: true },
|
||||
"/recursos-humanos/funcionarios/excluir": { path: "/recursos-humanos/funcionarios", requireGravar: true },
|
||||
"/recursos-humanos/funcionarios/relatorios": { path: "/recursos-humanos/funcionarios" },
|
||||
"/recursos-humanos/simbolos": { path: "/recursos-humanos/simbolos" },
|
||||
"/recursos-humanos/simbolos/cadastro": { path: "/recursos-humanos/simbolos", requireGravar: true },
|
||||
// Outros menus
|
||||
"/financeiro": { path: "/financeiro" },
|
||||
"/controladoria": { path: "/controladoria" },
|
||||
"/licitacoes": { path: "/licitacoes" },
|
||||
"/compras": { path: "/compras" },
|
||||
"/juridico": { path: "/juridico" },
|
||||
"/comunicacao": { path: "/comunicacao" },
|
||||
"/programas-esportivos": { path: "/programas-esportivos" },
|
||||
"/secretaria-executiva": { path: "/secretaria-executiva" },
|
||||
"/gestao-pessoas": { path: "/gestao-pessoas" },
|
||||
"/ti": { path: "/ti" },
|
||||
};
|
||||
// Resolver recurso/ação a partir da rota
|
||||
const routeAction = $derived.by(() => {
|
||||
const p = page.url.pathname;
|
||||
if (p === "/" || p === "/solicitar-acesso") return null;
|
||||
|
||||
// Obter configuração para a rota atual
|
||||
const getCurrentRouteConfig = $derived.by(() => {
|
||||
const currentPath = page.url.pathname;
|
||||
|
||||
// Verificar correspondência exata
|
||||
if (ROUTE_PERMISSIONS[currentPath]) {
|
||||
return ROUTE_PERMISSIONS[currentPath];
|
||||
// Funcionários
|
||||
if (p.startsWith("/recursos-humanos/funcionarios")) {
|
||||
if (p.includes("/cadastro"))
|
||||
return { recurso: "funcionarios", acao: "criar" };
|
||||
if (p.includes("/excluir"))
|
||||
return { recurso: "funcionarios", acao: "excluir" };
|
||||
if (p.includes("/editar") || p.includes("/funcionarioId"))
|
||||
return { recurso: "funcionarios", acao: "editar" };
|
||||
return { recurso: "funcionarios", acao: "listar" };
|
||||
}
|
||||
|
||||
// Verificar rotas dinâmicas (com [id])
|
||||
if (currentPath.includes("/editar") || currentPath.includes("/funcionarioId") || currentPath.includes("/simboloId")) {
|
||||
// Extrair o caminho base
|
||||
if (currentPath.includes("/funcionarios/")) {
|
||||
return { path: "/recursos-humanos/funcionarios", requireGravar: true };
|
||||
}
|
||||
if (currentPath.includes("/simbolos/")) {
|
||||
return { path: "/recursos-humanos/simbolos", requireGravar: true };
|
||||
}
|
||||
// Símbolos
|
||||
if (p.startsWith("/recursos-humanos/simbolos")) {
|
||||
if (p.includes("/cadastro"))
|
||||
return { recurso: "simbolos", acao: "criar" };
|
||||
if (p.includes("/excluir"))
|
||||
return { recurso: "simbolos", acao: "excluir" };
|
||||
if (p.includes("/editar") || p.includes("/simboloId"))
|
||||
return { recurso: "simbolos", acao: "editar" };
|
||||
return { recurso: "simbolos", acao: "listar" };
|
||||
}
|
||||
|
||||
// Rotas públicas (Dashboard, Solicitar Acesso, etc)
|
||||
if (currentPath === "/" || currentPath === "/solicitar-acesso") {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Para qualquer outra rota dentro do dashboard, verificar o primeiro segmento
|
||||
const segments = currentPath.split("/").filter(Boolean);
|
||||
if (segments.length > 0) {
|
||||
const firstSegment = "/" + segments[0];
|
||||
if (ROUTE_PERMISSIONS[firstSegment]) {
|
||||
return ROUTE_PERMISSIONS[firstSegment];
|
||||
}
|
||||
}
|
||||
// Outras áreas (uso genérico: ver)
|
||||
if (p.startsWith("/financeiro"))
|
||||
return { recurso: "financeiro", acao: "ver" };
|
||||
if (p.startsWith("/controladoria"))
|
||||
return { recurso: "controladoria", acao: "ver" };
|
||||
if (p.startsWith("/licitacoes"))
|
||||
return { recurso: "licitacoes", acao: "ver" };
|
||||
if (p.startsWith("/compras")) return { recurso: "compras", acao: "ver" };
|
||||
if (p.startsWith("/juridico")) return { recurso: "juridico", acao: "ver" };
|
||||
if (p.startsWith("/comunicacao"))
|
||||
return { recurso: "comunicacao", acao: "ver" };
|
||||
if (p.startsWith("/programas-esportivos"))
|
||||
return { recurso: "programas_esportivos", acao: "ver" };
|
||||
if (p.startsWith("/secretaria-executiva"))
|
||||
return { recurso: "secretaria_executiva", acao: "ver" };
|
||||
if (p.startsWith("/gestao-pessoas"))
|
||||
return { recurso: "gestao_pessoas", acao: "ver" };
|
||||
|
||||
return null;
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if getCurrentRouteConfig}
|
||||
<MenuProtection menuPath={getCurrentRouteConfig.path} requireGravar={getCurrentRouteConfig.requireGravar || false}>
|
||||
<main
|
||||
id="container-central"
|
||||
class="w-full max-w-none px-3 lg:px-4 py-4"
|
||||
>
|
||||
{#if routeAction}
|
||||
<ActionGuard recurso={routeAction.recurso} acao={routeAction.acao}>
|
||||
<main id="container-central" class="w-full max-w-none px-3 lg:px-4 py-4">
|
||||
{@render children()}
|
||||
</main>
|
||||
</MenuProtection>
|
||||
</ActionGuard>
|
||||
{:else}
|
||||
<main
|
||||
id="container-central"
|
||||
class="w-full max-w-none px-3 lg:px-4 py-4"
|
||||
>
|
||||
<main id="container-central" class="w-full max-w-none px-3 lg:px-4 py-4">
|
||||
{@render children()}
|
||||
</main>
|
||||
{/if}
|
||||
|
||||
@@ -4,16 +4,46 @@
|
||||
import ProtectedRoute from "$lib/components/ProtectedRoute.svelte";
|
||||
import { goto } from "$app/navigation";
|
||||
import type { Id } from "@sgse-app/backend/convex/_generated/dataModel";
|
||||
type RoleRow = {
|
||||
_id: Id<"roles">;
|
||||
_creationTime: number;
|
||||
nome: string;
|
||||
descricao: string;
|
||||
nivel: number;
|
||||
setor?: string;
|
||||
customizado: boolean;
|
||||
editavel?: boolean;
|
||||
criadoPor?: Id<"usuarios">;
|
||||
};
|
||||
|
||||
const client = useConvexClient();
|
||||
|
||||
// Buscar matriz de permissões
|
||||
const matrizQuery = useQuery(api.menuPermissoes.obterMatrizPermissoes, {});
|
||||
// Carregar lista de roles e catálogo de recursos/ações
|
||||
const rolesQuery = useQuery(api.roles.listar, {});
|
||||
const catalogoQuery = useQuery(api.permissoesAcoes.listarRecursosEAcoes, {});
|
||||
|
||||
let salvando = $state(false);
|
||||
let mensagem = $state<{ tipo: "success" | "error"; texto: string } | null>(null);
|
||||
let mensagem = $state<{ tipo: "success" | "error"; texto: string } | null>(
|
||||
null
|
||||
);
|
||||
let busca = $state("");
|
||||
let filtroRole = $state("");
|
||||
let expandido: Record<string, boolean> = $state({});
|
||||
|
||||
// Cache de permissões por role
|
||||
let permissoesPorRole: Record<
|
||||
string,
|
||||
Array<{ recurso: string; acoes: Array<string> }>
|
||||
> = $state({});
|
||||
|
||||
async function carregarPermissoesRole(roleId: Id<"roles">) {
|
||||
if (permissoesPorRole[roleId]) return;
|
||||
const dados = await client.query(
|
||||
api.permissoesAcoes.listarPermissoesAcoesPorRole,
|
||||
{ roleId }
|
||||
);
|
||||
permissoesPorRole[roleId] = dados;
|
||||
}
|
||||
|
||||
function mostrarMensagem(tipo: "success" | "error", texto: string) {
|
||||
mensagem = { tipo, texto };
|
||||
@@ -22,89 +52,50 @@
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
const dadosFiltrados = $derived.by(() => {
|
||||
if (!matrizQuery.data) return [];
|
||||
|
||||
let resultado = matrizQuery.data;
|
||||
|
||||
// Filtrar por role
|
||||
if (filtroRole) {
|
||||
resultado = resultado.filter(r => r.role._id === filtroRole);
|
||||
}
|
||||
|
||||
// Filtrar por busca
|
||||
const rolesFiltradas = $derived.by(() => {
|
||||
if (!rolesQuery.data) return [];
|
||||
let rs: Array<RoleRow> = rolesQuery.data as Array<RoleRow>;
|
||||
if (filtroRole)
|
||||
rs = rs.filter((r: RoleRow) => r._id === (filtroRole as any));
|
||||
if (busca.trim()) {
|
||||
const buscaLower = busca.toLowerCase();
|
||||
resultado = resultado.map(roleData => ({
|
||||
...roleData,
|
||||
permissoes: roleData.permissoes.filter(p =>
|
||||
p.menuNome.toLowerCase().includes(buscaLower) ||
|
||||
p.menuPath.toLowerCase().includes(buscaLower)
|
||||
)
|
||||
})).filter(roleData => roleData.permissoes.length > 0);
|
||||
const b = busca.toLowerCase();
|
||||
rs = rs.filter(
|
||||
(r: RoleRow) =>
|
||||
r.descricao.toLowerCase().includes(b) ||
|
||||
r.nome.toLowerCase().includes(b)
|
||||
);
|
||||
}
|
||||
|
||||
return resultado;
|
||||
return rs;
|
||||
});
|
||||
|
||||
async function atualizarPermissao(
|
||||
async function toggleAcao(
|
||||
roleId: Id<"roles">,
|
||||
menuPath: string,
|
||||
campo: "podeAcessar" | "podeConsultar" | "podeGravar",
|
||||
valor: boolean
|
||||
recurso: string,
|
||||
acao: string,
|
||||
conceder: boolean
|
||||
) {
|
||||
try {
|
||||
salvando = true;
|
||||
|
||||
// Buscar a permissão atual
|
||||
const roleData = matrizQuery.data?.find((r) => r.role._id === roleId);
|
||||
const permissaoAtual = roleData?.permissoes.find((p) => p.menuPath === menuPath);
|
||||
|
||||
if (!permissaoAtual) {
|
||||
throw new Error("Permissão não encontrada");
|
||||
}
|
||||
|
||||
// Inicializar com valores atuais
|
||||
let podeAcessar = permissaoAtual.podeAcessar;
|
||||
let podeConsultar = permissaoAtual.podeConsultar;
|
||||
let podeGravar = permissaoAtual.podeGravar;
|
||||
|
||||
// Aplicar lógica de dependências baseada no campo alterado
|
||||
if (campo === "podeAcessar") {
|
||||
podeAcessar = valor;
|
||||
// Se desmarcou "Acessar", desmarcar tudo
|
||||
if (!valor) {
|
||||
podeConsultar = false;
|
||||
podeGravar = false;
|
||||
}
|
||||
// Se marcou "Acessar", manter os outros valores como estão
|
||||
} else if (campo === "podeConsultar") {
|
||||
podeConsultar = valor;
|
||||
// Se marcou "Consultar", marcar "Acessar" automaticamente
|
||||
if (valor) {
|
||||
podeAcessar = true;
|
||||
} else {
|
||||
// Se desmarcou "Consultar", desmarcar "Gravar"
|
||||
podeGravar = false;
|
||||
}
|
||||
} else if (campo === "podeGravar") {
|
||||
podeGravar = valor;
|
||||
// Se marcou "Gravar", marcar "Consultar" e "Acessar" automaticamente
|
||||
if (valor) {
|
||||
podeAcessar = true;
|
||||
podeConsultar = true;
|
||||
}
|
||||
// Se desmarcou "Gravar", manter os outros como estão
|
||||
}
|
||||
|
||||
await client.mutation(api.menuPermissoes.atualizarPermissao, {
|
||||
await client.mutation(api.permissoesAcoes.atualizarPermissaoAcao, {
|
||||
roleId,
|
||||
menuPath,
|
||||
podeAcessar,
|
||||
podeConsultar,
|
||||
podeGravar,
|
||||
recurso,
|
||||
acao,
|
||||
conceder,
|
||||
});
|
||||
|
||||
// Atualizar cache local
|
||||
const atual = permissoesPorRole[roleId] || [];
|
||||
const entry = atual.find((e) => e.recurso === recurso);
|
||||
if (entry) {
|
||||
const set = new Set(entry.acoes);
|
||||
if (conceder) set.add(acao);
|
||||
else set.delete(acao);
|
||||
entry.acoes = Array.from(set);
|
||||
} else {
|
||||
permissoesPorRole[roleId] = [
|
||||
...atual,
|
||||
{ recurso, acoes: conceder ? [acao] : [] },
|
||||
];
|
||||
}
|
||||
mostrarMensagem("success", "Permissão atualizada com sucesso!");
|
||||
} catch (e: any) {
|
||||
mostrarMensagem("error", e.message || "Erro ao atualizar permissão");
|
||||
@@ -113,16 +104,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function inicializarPermissoes(roleId: Id<"roles">) {
|
||||
try {
|
||||
salvando = true;
|
||||
await client.mutation(api.menuPermissoes.inicializarPermissoesRole, { roleId });
|
||||
mostrarMensagem("success", "Permissões inicializadas!");
|
||||
} catch (e: any) {
|
||||
mostrarMensagem("error", e.message || "Erro ao inicializar permissões");
|
||||
} finally {
|
||||
salvando = false;
|
||||
}
|
||||
function isConcedida(roleId: Id<"roles">, recurso: string, acao: string) {
|
||||
const dados = permissoesPorRole[roleId];
|
||||
const entry = dados?.find((e) => e.recurso === recurso);
|
||||
return entry ? entry.acoes.includes(acao) : false;
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -132,8 +117,19 @@
|
||||
<ul>
|
||||
<li>
|
||||
<a href="/" class="text-primary hover:text-primary-focus">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"
|
||||
/>
|
||||
</svg>
|
||||
Dashboard
|
||||
</a>
|
||||
@@ -149,17 +145,43 @@
|
||||
<div class="mb-6">
|
||||
<div class="flex items-center gap-3 mb-2">
|
||||
<div class="p-3 bg-primary/10 rounded-xl">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-8 w-8 text-primary"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h1 class="text-3xl font-bold text-base-content">Gerenciar Permissões de Acesso</h1>
|
||||
<p class="text-base-content/60 mt-1">Configure as permissões de acesso aos menus do sistema por função</p>
|
||||
<h1 class="text-3xl font-bold text-base-content">
|
||||
Gerenciar Permissões de Acesso
|
||||
</h1>
|
||||
<p class="text-base-content/60 mt-1">
|
||||
Configure as permissões de acesso aos menus do sistema por função
|
||||
</p>
|
||||
</div>
|
||||
<button class="btn btn-ghost gap-2" onclick={() => goto("/ti")}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M10 19l-7-7m0 0l7-7m-7 7h18"
|
||||
/>
|
||||
</svg>
|
||||
Voltar
|
||||
</button>
|
||||
@@ -168,14 +190,38 @@
|
||||
|
||||
<!-- Alertas -->
|
||||
{#if mensagem}
|
||||
<div class="alert mb-6 shadow-lg" class:alert-success={mensagem.tipo === "success"} class:alert-error={mensagem.tipo === "error"}>
|
||||
<div
|
||||
class="alert mb-6 shadow-lg"
|
||||
class:alert-success={mensagem.tipo === "success"}
|
||||
class:alert-error={mensagem.tipo === "error"}
|
||||
>
|
||||
{#if mensagem.tipo === "success"}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="stroke-current shrink-0 h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
{:else}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="stroke-current shrink-0 h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
<span class="font-semibold">{mensagem.texto}</span>
|
||||
@@ -189,13 +235,13 @@
|
||||
<!-- Busca por menu -->
|
||||
<div class="form-control">
|
||||
<label class="label" for="busca">
|
||||
<span class="label-text font-semibold">Buscar Menu</span>
|
||||
<span class="label-text font-semibold">Buscar Perfil</span>
|
||||
</label>
|
||||
<div class="relative">
|
||||
<input
|
||||
id="busca"
|
||||
type="text"
|
||||
placeholder="Digite o nome ou caminho do menu..."
|
||||
placeholder="Digite o nome/descrição do perfil..."
|
||||
class="input input-bordered w-full pr-10"
|
||||
bind:value={busca}
|
||||
/>
|
||||
@@ -227,10 +273,10 @@
|
||||
bind:value={filtroRole}
|
||||
>
|
||||
<option value="">Todos os perfis</option>
|
||||
{#if matrizQuery.data}
|
||||
{#each matrizQuery.data as roleData}
|
||||
<option value={roleData.role._id}>
|
||||
{roleData.role.descricao} ({roleData.role.nome})
|
||||
{#if rolesQuery.data}
|
||||
{#each rolesQuery.data as roleRow}
|
||||
<option value={roleRow._id}>
|
||||
{roleRow.descricao} ({roleRow.nome})
|
||||
</option>
|
||||
{/each}
|
||||
{/if}
|
||||
@@ -272,8 +318,18 @@
|
||||
|
||||
<!-- Informações sobre o sistema de permissões -->
|
||||
<div class="alert alert-info mb-6 shadow-lg">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="stroke-current shrink-0 h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
<div>
|
||||
<h3 class="font-bold text-lg">Como funciona o sistema de permissões:</h3>
|
||||
@@ -281,9 +337,13 @@
|
||||
<div>
|
||||
<h4 class="font-semibold text-sm">Tipos de Permissão:</h4>
|
||||
<ul class="text-sm mt-1 space-y-1">
|
||||
<li>• <strong>Acessar:</strong> Visualizar menu e acessar página</li>
|
||||
<li>
|
||||
• <strong>Acessar:</strong> Visualizar menu e acessar página
|
||||
</li>
|
||||
<li>• <strong>Consultar:</strong> Ver dados (requer "Acessar")</li>
|
||||
<li>• <strong>Gravar:</strong> Criar/editar/excluir (requer "Consultar")</li>
|
||||
<li>
|
||||
• <strong>Gravar:</strong> Criar/editar/excluir (requer "Consultar")
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
@@ -291,27 +351,39 @@
|
||||
<ul class="text-sm mt-1 space-y-1">
|
||||
<li>• <strong>Admin e TI:</strong> Acesso total automático</li>
|
||||
<li>• <strong>Dashboard:</strong> Público para todos</li>
|
||||
<li>• <strong>Perfil Customizado:</strong> Permissões personalizadas</li>
|
||||
<li>
|
||||
• <strong>Perfil Customizado:</strong> Permissões personalizadas
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Matriz de Permissões -->
|
||||
{#if matrizQuery.isLoading}
|
||||
<!-- Matriz de Permissões por Ação -->
|
||||
{#if rolesQuery.isLoading || catalogoQuery.isLoading}
|
||||
<div class="flex justify-center items-center py-12">
|
||||
<span class="loading loading-spinner loading-lg text-primary"></span>
|
||||
</div>
|
||||
{:else if matrizQuery.error}
|
||||
{:else if rolesQuery.error}
|
||||
<div class="alert alert-error">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="stroke-current shrink-0 h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
<span>Erro ao carregar permissões: {matrizQuery.error.message}</span>
|
||||
<span>Erro ao carregar perfis: {rolesQuery.error.message}</span>
|
||||
</div>
|
||||
{:else if matrizQuery.data}
|
||||
{#if dadosFiltrados.length === 0}
|
||||
{:else if rolesQuery.data && catalogoQuery.data}
|
||||
{#if rolesFiltradas.length === 0}
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body items-center text-center">
|
||||
<svg
|
||||
@@ -330,7 +402,9 @@
|
||||
</svg>
|
||||
<h3 class="text-xl font-bold mt-4">Nenhum resultado encontrado</h3>
|
||||
<p class="text-base-content/60">
|
||||
{busca ? `Não foram encontrados menus com "${busca}"` : "Nenhuma permissão corresponde aos filtros aplicados"}
|
||||
{busca
|
||||
? `Não foram encontrados perfis com "${busca}"`
|
||||
: "Nenhum perfil corresponde aos filtros aplicados"}
|
||||
</p>
|
||||
<button
|
||||
class="btn btn-primary btn-sm mt-4"
|
||||
@@ -345,158 +419,118 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#each dadosFiltrados as roleData}
|
||||
{#each rolesFiltradas as roleRow}
|
||||
<div class="card bg-base-100 shadow-xl mb-6">
|
||||
<div class="card-body">
|
||||
<div class="flex items-center justify-between mb-4 flex-wrap gap-4">
|
||||
<div class="flex-1 min-w-[200px]">
|
||||
<div class="flex items-center gap-3 mb-2">
|
||||
<h2 class="card-title text-2xl">{roleData.role.descricao}</h2>
|
||||
<div class="badge badge-lg badge-primary">Nível {roleData.role.nivel}</div>
|
||||
{#if roleData.role.nivel <= 1}
|
||||
<h2 class="card-title text-2xl">{roleRow.descricao}</h2>
|
||||
<div class="badge badge-lg badge-primary">
|
||||
Nível {roleRow.nivel}
|
||||
</div>
|
||||
{#if roleRow.nivel <= 1}
|
||||
<div class="badge badge-lg badge-success gap-1">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
Acesso Total
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<p class="text-sm text-base-content/60">
|
||||
<span class="font-mono bg-base-200 px-2 py-1 rounded">{roleData.role.nome}</span>
|
||||
<span class="font-mono bg-base-200 px-2 py-1 rounded"
|
||||
>{roleRow.nome}</span
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{#if roleData.role.nivel > 1}
|
||||
{#if roleRow.nivel > 1}
|
||||
<button
|
||||
class="btn btn-sm btn-outline btn-primary"
|
||||
onclick={() => inicializarPermissoes(roleData.role._id)}
|
||||
disabled={salvando}
|
||||
class="btn btn-sm btn-outline"
|
||||
onclick={async () => {
|
||||
expandido[roleRow._id] = !expandido[roleRow._id];
|
||||
if (expandido[roleRow._id])
|
||||
await carregarPermissoesRole(roleRow._id);
|
||||
}}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
Inicializar Permissões
|
||||
{expandido[roleRow._id] ? "Recolher" : "Expandir"}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if roleData.role.nivel <= 1}
|
||||
{#if roleRow.nivel <= 1}
|
||||
<div class="alert alert-success shadow-md">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="stroke-current shrink-0 h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
<div>
|
||||
<h3 class="font-bold">Perfil Administrativo</h3>
|
||||
<div class="text-sm">Este perfil possui acesso total ao sistema automaticamente, sem necessidade de configuração manual.</div>
|
||||
<div class="text-sm">
|
||||
Este perfil possui acesso total ao sistema automaticamente,
|
||||
sem necessidade de configuração manual.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="stats stats-vertical lg:stats-horizontal shadow mb-4 w-full">
|
||||
<div class="stat">
|
||||
<div class="stat-title">Total de Menus</div>
|
||||
<div class="stat-value text-primary">{roleData.permissoes.length}</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-title">Com Acesso</div>
|
||||
<div class="stat-value text-info">{roleData.permissoes.filter(p => p.podeAcessar).length}</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-title">Pode Consultar</div>
|
||||
<div class="stat-value text-success">{roleData.permissoes.filter(p => p.podeConsultar).length}</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-title">Pode Gravar</div>
|
||||
<div class="stat-value text-warning">{roleData.permissoes.filter(p => p.podeGravar).length}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{:else if expandido[roleRow._id]}
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table table-zebra table-sm">
|
||||
<thead class="bg-base-200">
|
||||
<tr>
|
||||
<th class="w-1/3">Menu</th>
|
||||
<th class="text-center">
|
||||
<div class="flex items-center justify-center gap-1">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
||||
</svg>
|
||||
Acessar
|
||||
</div>
|
||||
</th>
|
||||
<th class="text-center">
|
||||
<div class="flex items-center justify-center gap-1">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
Consultar
|
||||
</div>
|
||||
</th>
|
||||
<th class="text-center">
|
||||
<div class="flex items-center justify-center gap-1">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
||||
</svg>
|
||||
Gravar
|
||||
</div>
|
||||
</th>
|
||||
<th class="w-1/3">Recurso</th>
|
||||
<th class="text-center">Ver</th>
|
||||
<th class="text-center">Listar</th>
|
||||
<th class="text-center">Criar</th>
|
||||
<th class="text-center">Editar</th>
|
||||
<th class="text-center">Excluir</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each roleData.permissoes as permissao}
|
||||
{#each catalogoQuery.data as item}
|
||||
<tr class="hover">
|
||||
<td>
|
||||
<div class="flex flex-col">
|
||||
<span class="font-semibold">{permissao.menuNome}</span>
|
||||
<span class="text-xs text-base-content/60">{permissao.menuPath}</span>
|
||||
<span class="font-semibold">{item.recurso}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="checkbox checkbox-primary"
|
||||
checked={permissao.podeAcessar}
|
||||
disabled={salvando}
|
||||
onchange={(e) =>
|
||||
atualizarPermissao(
|
||||
roleData.role._id,
|
||||
permissao.menuPath,
|
||||
"podeAcessar",
|
||||
e.currentTarget.checked
|
||||
)}
|
||||
/>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="checkbox checkbox-info"
|
||||
checked={permissao.podeConsultar}
|
||||
disabled={salvando || !permissao.podeAcessar}
|
||||
onchange={(e) =>
|
||||
atualizarPermissao(
|
||||
roleData.role._id,
|
||||
permissao.menuPath,
|
||||
"podeConsultar",
|
||||
e.currentTarget.checked
|
||||
)}
|
||||
/>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="checkbox checkbox-success"
|
||||
checked={permissao.podeGravar}
|
||||
disabled={salvando || !permissao.podeConsultar}
|
||||
onchange={(e) =>
|
||||
atualizarPermissao(
|
||||
roleData.role._id,
|
||||
permissao.menuPath,
|
||||
"podeGravar",
|
||||
e.currentTarget.checked
|
||||
)}
|
||||
/>
|
||||
</td>
|
||||
{#each ["ver", "listar", "criar", "editar", "excluir"] as ac}
|
||||
<td class="text-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="checkbox checkbox-primary"
|
||||
checked={isConcedida(roleRow._id, item.recurso, ac)}
|
||||
disabled={salvando}
|
||||
onchange={(e) =>
|
||||
toggleAcao(
|
||||
roleRow._id,
|
||||
item.recurso,
|
||||
ac,
|
||||
e.currentTarget.checked
|
||||
)}
|
||||
/>
|
||||
</td>
|
||||
{/each}
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
@@ -508,4 +542,3 @@
|
||||
{/each}
|
||||
{/if}
|
||||
</ProtectedRoute>
|
||||
|
||||
|
||||
@@ -1,123 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { useQuery, useConvexClient } from "convex-svelte";
|
||||
import { api } from "@sgse-app/backend/convex/_generated/api";
|
||||
import ProtectedRoute from "$lib/components/ProtectedRoute.svelte";
|
||||
import { goto } from "$app/navigation";
|
||||
|
||||
const client = useConvexClient();
|
||||
|
||||
let matriculaBusca = $state("");
|
||||
let usuarioEncontrado = $state<any>(null);
|
||||
let buscando = $state(false);
|
||||
let salvando = $state(false);
|
||||
let mensagem = $state<{ tipo: "success" | "error"; texto: string } | null>(null);
|
||||
|
||||
// Buscar permissões personalizadas do usuário
|
||||
const permissoesQuery = $derived(
|
||||
usuarioEncontrado
|
||||
? useQuery(api.menuPermissoes.listarPermissoesPersonalizadas, {
|
||||
matricula: usuarioEncontrado.matricula,
|
||||
})
|
||||
: null
|
||||
);
|
||||
|
||||
// Buscar menus disponíveis
|
||||
const menusQuery = useQuery(api.menuPermissoes.listarMenus, {});
|
||||
|
||||
async function buscarUsuario() {
|
||||
if (!matriculaBusca.trim()) {
|
||||
mensagem = { tipo: "error", texto: "Digite uma matrícula para buscar" };
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
buscando = true;
|
||||
const usuario = await client.query(api.menuPermissoes.buscarUsuarioPorMatricula, {
|
||||
matricula: matriculaBusca.trim(),
|
||||
});
|
||||
|
||||
if (usuario) {
|
||||
usuarioEncontrado = usuario;
|
||||
mensagem = null;
|
||||
} else {
|
||||
usuarioEncontrado = null;
|
||||
mensagem = { tipo: "error", texto: "Usuário não encontrado com esta matrícula" };
|
||||
}
|
||||
} catch (e: any) {
|
||||
mensagem = { tipo: "error", texto: e.message || "Erro ao buscar usuário" };
|
||||
} finally {
|
||||
buscando = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function atualizarPermissao(
|
||||
menuPath: string,
|
||||
campo: "podeAcessar" | "podeConsultar" | "podeGravar",
|
||||
valor: boolean
|
||||
) {
|
||||
if (!usuarioEncontrado) return;
|
||||
|
||||
try {
|
||||
salvando = true;
|
||||
|
||||
// Obter permissão atual do menu
|
||||
const permissaoAtual = permissoesQuery?.data?.find((p) => p.menuPath === menuPath);
|
||||
|
||||
let podeAcessar = valor;
|
||||
let podeConsultar = false;
|
||||
let podeGravar = false;
|
||||
|
||||
// Aplicar lógica de dependências
|
||||
if (campo === "podeGravar" && valor) {
|
||||
podeAcessar = true;
|
||||
podeConsultar = true;
|
||||
podeGravar = true;
|
||||
} else if (campo === "podeConsultar" && valor) {
|
||||
podeAcessar = true;
|
||||
podeConsultar = true;
|
||||
podeGravar = permissaoAtual?.podeGravar || false;
|
||||
} else if (campo === "podeAcessar" && !valor) {
|
||||
podeAcessar = false;
|
||||
podeConsultar = false;
|
||||
podeGravar = false;
|
||||
} else if (campo === "podeConsultar" && !valor) {
|
||||
podeAcessar = permissaoAtual?.podeAcessar !== undefined ? permissaoAtual.podeAcessar : false;
|
||||
podeConsultar = false;
|
||||
podeGravar = false;
|
||||
} else if (campo === "podeGravar" && !valor) {
|
||||
podeAcessar = permissaoAtual?.podeAcessar !== undefined ? permissaoAtual.podeAcessar : false;
|
||||
podeConsultar = permissaoAtual?.podeConsultar !== undefined ? permissaoAtual.podeConsultar : false;
|
||||
podeGravar = false;
|
||||
} else if (permissaoAtual) {
|
||||
podeAcessar = permissaoAtual.podeAcessar;
|
||||
podeConsultar = permissaoAtual.podeConsultar;
|
||||
podeGravar = permissaoAtual.podeGravar;
|
||||
}
|
||||
|
||||
await client.mutation(api.menuPermissoes.atualizarPermissaoPersonalizada, {
|
||||
matricula: usuarioEncontrado.matricula,
|
||||
menuPath,
|
||||
podeAcessar,
|
||||
podeConsultar,
|
||||
podeGravar,
|
||||
});
|
||||
|
||||
mensagem = { tipo: "success", texto: "Permissão personalizada atualizada!" };
|
||||
setTimeout(() => {
|
||||
mensagem = null;
|
||||
}, 3000);
|
||||
} catch (e: any) {
|
||||
mensagem = { tipo: "error", texto: e.message || "Erro ao atualizar permissão" };
|
||||
} finally {
|
||||
salvando = false;
|
||||
}
|
||||
}
|
||||
|
||||
function limparBusca() {
|
||||
matriculaBusca = "";
|
||||
usuarioEncontrado = null;
|
||||
mensagem = null;
|
||||
}
|
||||
</script>
|
||||
|
||||
<ProtectedRoute allowedRoles={["admin", "ti"]} maxLevel={1}>
|
||||
@@ -126,8 +9,19 @@
|
||||
<ul>
|
||||
<li>
|
||||
<a href="/" class="text-primary hover:text-primary-focus">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"
|
||||
/>
|
||||
</svg>
|
||||
Dashboard
|
||||
</a>
|
||||
@@ -143,241 +37,71 @@
|
||||
<div class="mb-6">
|
||||
<div class="flex items-center gap-3 mb-2">
|
||||
<div class="p-3 bg-info/10 rounded-xl">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-info" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-8 w-8 text-info"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h1 class="text-3xl font-bold text-base-content">Personalizar Permissões por Matrícula</h1>
|
||||
<p class="text-base-content/60 mt-1">Configure permissões específicas para usuários individuais</p>
|
||||
<h1 class="text-3xl font-bold text-base-content">
|
||||
Funcionalidade descontinuada
|
||||
</h1>
|
||||
<p class="text-base-content/60 mt-1">
|
||||
Agora as permissões são configuradas por ação em cada perfil no painel
|
||||
de permissões.
|
||||
</p>
|
||||
</div>
|
||||
<button class="btn btn-ghost gap-2" onclick={() => goto("/ti")}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M10 19l-7-7m0 0l7-7m-7 7h18"
|
||||
/>
|
||||
</svg>
|
||||
Voltar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Alertas -->
|
||||
{#if mensagem}
|
||||
<div class="alert mb-6 shadow-lg" class:alert-success={mensagem.tipo === "success"} class:alert-error={mensagem.tipo === "error"}>
|
||||
{#if mensagem.tipo === "success"}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
{:else}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
{/if}
|
||||
<span class="font-semibold">{mensagem.texto}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Card de Busca -->
|
||||
<div class="card bg-base-100 shadow-xl mb-6">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">Buscar Usuário</h2>
|
||||
<p class="text-sm text-base-content/60">Digite a matrícula do usuário para personalizar suas permissões</p>
|
||||
|
||||
<div class="flex gap-4 mt-4">
|
||||
<div class="form-control flex-1">
|
||||
<label class="label" for="matricula-busca">
|
||||
<span class="label-text font-semibold">Matrícula</span>
|
||||
</label>
|
||||
<input
|
||||
id="matricula-busca"
|
||||
type="text"
|
||||
class="input input-bordered input-primary w-full"
|
||||
placeholder="Digite a matrícula..."
|
||||
bind:value={matriculaBusca}
|
||||
disabled={buscando}
|
||||
onkeydown={(e) => e.key === "Enter" && buscarUsuario()}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-end gap-2">
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
onclick={buscarUsuario}
|
||||
disabled={buscando || !matriculaBusca.trim()}
|
||||
>
|
||||
{#if buscando}
|
||||
<span class="loading loading-spinner loading-sm"></span>
|
||||
{:else}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
{/if}
|
||||
Buscar
|
||||
</button>
|
||||
|
||||
{#if usuarioEncontrado}
|
||||
<button class="btn btn-ghost" onclick={limparBusca}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
Limpar
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="alert alert-info shadow-lg">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="stroke-current shrink-0 h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
<span>
|
||||
A personalização por usuário foi substituída por <strong
|
||||
>permissões por ação</strong
|
||||
>
|
||||
por perfil. Utilize o
|
||||
<a href="/ti/painel-permissoes" class="link link-primary"
|
||||
>Painel de Permissões</a
|
||||
> para configurar.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Informações do Usuário -->
|
||||
{#if usuarioEncontrado}
|
||||
<div class="card bg-gradient-to-br from-info/10 to-info/5 shadow-xl mb-6 border-2 border-info/20">
|
||||
<div class="card-body">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="avatar placeholder">
|
||||
<div class="bg-info text-info-content rounded-full w-16">
|
||||
<span class="text-2xl font-bold">{usuarioEncontrado.nome.charAt(0)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-1">
|
||||
<h3 class="text-xl font-bold">{usuarioEncontrado.nome}</h3>
|
||||
<div class="flex gap-4 mt-1 text-sm">
|
||||
<span class="flex items-center gap-1">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" />
|
||||
</svg>
|
||||
<strong>Matrícula:</strong> {usuarioEncontrado.matricula}
|
||||
</span>
|
||||
<span class="flex items-center gap-1">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<strong>Email:</strong> {usuarioEncontrado.email}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-right">
|
||||
<div class="badge badge-primary badge-lg">
|
||||
Nível {usuarioEncontrado.role.nivel}
|
||||
</div>
|
||||
<p class="text-sm mt-1">{usuarioEncontrado.role.descricao}</p>
|
||||
<div class="badge mt-2" class:badge-success={usuarioEncontrado.ativo} class:badge-error={!usuarioEncontrado.ativo}>
|
||||
{usuarioEncontrado.ativo ? "Ativo" : "Inativo"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tabela de Permissões -->
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">Permissões Personalizadas</h2>
|
||||
<div class="alert alert-info mb-4">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<div>
|
||||
<p class="text-sm">
|
||||
<strong>Permissões personalizadas sobrepõem as permissões da função.</strong><br />
|
||||
Configure apenas os menus que deseja personalizar para este usuário.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if menusQuery.isLoading}
|
||||
<div class="flex justify-center py-12">
|
||||
<span class="loading loading-spinner loading-lg text-primary"></span>
|
||||
</div>
|
||||
{:else if menusQuery.data}
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table table-zebra table-sm">
|
||||
<thead class="bg-base-200">
|
||||
<tr>
|
||||
<th class="w-1/3">Menu</th>
|
||||
<th class="text-center">
|
||||
<div class="flex items-center justify-center gap-1">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
||||
</svg>
|
||||
Acessar
|
||||
</div>
|
||||
</th>
|
||||
<th class="text-center">
|
||||
<div class="flex items-center justify-center gap-1">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
Consultar
|
||||
</div>
|
||||
</th>
|
||||
<th class="text-center">
|
||||
<div class="flex items-center justify-center gap-1">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
||||
</svg>
|
||||
Gravar
|
||||
</div>
|
||||
</th>
|
||||
<th class="text-center">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each menusQuery.data as menu}
|
||||
{@const permissao = permissoesQuery?.data?.find((p) => p.menuPath === menu.path)}
|
||||
<tr class="hover">
|
||||
<td>
|
||||
<div class="flex flex-col">
|
||||
<span class="font-semibold">{menu.nome}</span>
|
||||
<span class="text-xs text-base-content/60">{menu.path}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="checkbox checkbox-primary"
|
||||
checked={permissao?.podeAcessar || false}
|
||||
disabled={salvando}
|
||||
onchange={(e) =>
|
||||
atualizarPermissao(menu.path, "podeAcessar", e.currentTarget.checked)}
|
||||
/>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="checkbox checkbox-info"
|
||||
checked={permissao?.podeConsultar || false}
|
||||
disabled={salvando || !permissao?.podeAcessar}
|
||||
onchange={(e) =>
|
||||
atualizarPermissao(menu.path, "podeConsultar", e.currentTarget.checked)}
|
||||
/>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="checkbox checkbox-success"
|
||||
checked={permissao?.podeGravar || false}
|
||||
disabled={salvando || !permissao?.podeConsultar}
|
||||
onchange={(e) =>
|
||||
atualizarPermissao(menu.path, "podeGravar", e.currentTarget.checked)}
|
||||
/>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
{#if permissao}
|
||||
<div class="badge badge-warning badge-sm">Personalizado</div>
|
||||
{:else}
|
||||
<div class="badge badge-ghost badge-sm">Padrão da Função</div>
|
||||
{/if}
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</ProtectedRoute>
|
||||
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
import { createSvelteKitHandler } from "@mmailaender/convex-better-auth-svelte/sveltekit";
|
||||
|
||||
export const { GET, POST } = createSvelteKitHandler();
|
||||
@@ -10,7 +10,6 @@
|
||||
"typescript": "^5.9.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"convex": "^1.28.0",
|
||||
"better-auth": "1.3.27"
|
||||
"convex": "^1.28.0"
|
||||
}
|
||||
}
|
||||
4
packages/backend/convex/_generated/api.d.ts
vendored
4
packages/backend/convex/_generated/api.d.ts
vendored
@@ -28,10 +28,10 @@ import type * as limparPerfisAntigos from "../limparPerfisAntigos.js";
|
||||
import type * as logsAcesso from "../logsAcesso.js";
|
||||
import type * as logsAtividades from "../logsAtividades.js";
|
||||
import type * as logsLogin from "../logsLogin.js";
|
||||
import type * as menuPermissoes from "../menuPermissoes.js";
|
||||
import type * as migrarUsuariosAdmin from "../migrarUsuariosAdmin.js";
|
||||
import type * as monitoramento from "../monitoramento.js";
|
||||
import type * as perfisCustomizados from "../perfisCustomizados.js";
|
||||
import type * as permissoesAcoes from "../permissoesAcoes.js";
|
||||
import type * as roles from "../roles.js";
|
||||
import type * as seed from "../seed.js";
|
||||
import type * as simbolos from "../simbolos.js";
|
||||
@@ -76,10 +76,10 @@ declare const fullApi: ApiFromModules<{
|
||||
logsAcesso: typeof logsAcesso;
|
||||
logsAtividades: typeof logsAtividades;
|
||||
logsLogin: typeof logsLogin;
|
||||
menuPermissoes: typeof menuPermissoes;
|
||||
migrarUsuariosAdmin: typeof migrarUsuariosAdmin;
|
||||
monitoramento: typeof monitoramento;
|
||||
perfisCustomizados: typeof perfisCustomizados;
|
||||
permissoesAcoes: typeof permissoesAcoes;
|
||||
roles: typeof roles;
|
||||
seed: typeof seed;
|
||||
simbolos: typeof simbolos;
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { v } from "convex/values";
|
||||
import { mutation, query } from "./_generated/server";
|
||||
import { hashPassword, verifyPassword, generateToken, validarMatricula, validarSenha } from "./auth/utils";
|
||||
import {
|
||||
hashPassword,
|
||||
verifyPassword,
|
||||
generateToken,
|
||||
validarMatricula,
|
||||
validarSenha,
|
||||
} from "./auth/utils";
|
||||
import { registrarLogin } from "./logsLogin";
|
||||
import { Id } from "./_generated/dataModel";
|
||||
|
||||
@@ -10,7 +16,7 @@ import { Id } from "./_generated/dataModel";
|
||||
async function verificarBloqueioUsuario(ctx: any, usuarioId: Id<"usuarios">) {
|
||||
const bloqueio = await ctx.db
|
||||
.query("bloqueiosUsuarios")
|
||||
.withIndex("by_usuario", (q) => q.eq("usuarioId", usuarioId))
|
||||
.withIndex("by_usuario", (q: any) => q.eq("usuarioId", usuarioId))
|
||||
.filter((q: any) => q.eq(q.field("ativo"), true))
|
||||
.first();
|
||||
|
||||
@@ -102,7 +108,9 @@ export const login = mutation({
|
||||
} else {
|
||||
usuario = await ctx.db
|
||||
.query("usuarios")
|
||||
.withIndex("by_matricula", (q) => q.eq("matricula", args.matriculaOuEmail))
|
||||
.withIndex("by_matricula", (q) =>
|
||||
q.eq("matricula", args.matriculaOuEmail)
|
||||
)
|
||||
.first();
|
||||
}
|
||||
|
||||
@@ -122,7 +130,10 @@ export const login = mutation({
|
||||
}
|
||||
|
||||
// Verificar se usuário está bloqueado
|
||||
if (usuario.bloqueado || (await verificarBloqueioUsuario(ctx, usuario._id))) {
|
||||
if (
|
||||
usuario.bloqueado ||
|
||||
(await verificarBloqueioUsuario(ctx, usuario._id))
|
||||
) {
|
||||
await registrarLogin(ctx, {
|
||||
usuarioId: usuario._id,
|
||||
matriculaOuEmail: args.matriculaOuEmail,
|
||||
@@ -172,7 +183,9 @@ export const login = mutation({
|
||||
userAgent: args.userAgent,
|
||||
});
|
||||
|
||||
const minutosRestantes = Math.ceil((TEMPO_BLOQUEIO - tempoDecorrido) / 60000);
|
||||
const minutosRestantes = Math.ceil(
|
||||
(TEMPO_BLOQUEIO - tempoDecorrido) / 60000
|
||||
);
|
||||
return {
|
||||
sucesso: false as const,
|
||||
erro: `Conta temporariamente bloqueada. Tente novamente em ${minutosRestantes} minutos.`,
|
||||
@@ -192,7 +205,8 @@ export const login = mutation({
|
||||
|
||||
if (!senhaValida) {
|
||||
// Incrementar tentativas
|
||||
const novasTentativas = tempoDecorrido > TEMPO_BLOQUEIO ? 1 : tentativasRecentes + 1;
|
||||
const novasTentativas =
|
||||
tempoDecorrido > TEMPO_BLOQUEIO ? 1 : tentativasRecentes + 1;
|
||||
|
||||
await ctx.db.patch(usuario._id, {
|
||||
tentativasLogin: novasTentativas,
|
||||
@@ -367,7 +381,10 @@ export const verificarSessao = query({
|
||||
.first();
|
||||
|
||||
if (!sessao || !sessao.ativo) {
|
||||
return { valido: false as const, motivo: "Sessão não encontrada ou inativa" };
|
||||
return {
|
||||
valido: false as const,
|
||||
motivo: "Sessão não encontrada ou inativa",
|
||||
};
|
||||
}
|
||||
|
||||
// Verificar se sessão expirou
|
||||
@@ -380,7 +397,10 @@ export const verificarSessao = query({
|
||||
// Buscar usuário
|
||||
const usuario = await ctx.db.get(sessao.usuarioId);
|
||||
if (!usuario || !usuario.ativo) {
|
||||
return { valido: false as const, motivo: "Usuário não encontrado ou inativo" };
|
||||
return {
|
||||
valido: false as const,
|
||||
motivo: "Usuário não encontrado ou inativo",
|
||||
};
|
||||
}
|
||||
|
||||
// Buscar role
|
||||
@@ -511,4 +531,3 @@ export const alterarSenha = mutation({
|
||||
return { sucesso: true as const };
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
import { createClient, type GenericCtx } from "@convex-dev/better-auth";
|
||||
import { convex } from "@convex-dev/better-auth/plugins";
|
||||
import { components } from "./_generated/api";
|
||||
import { type DataModel } from "./_generated/dataModel";
|
||||
import { query } from "./_generated/server";
|
||||
import { betterAuth } from "better-auth";
|
||||
import schema from "./betterAuth/schema";
|
||||
|
||||
// Configurações de ambiente para produção
|
||||
const siteUrl = process.env.SITE_URL || process.env.CONVEX_SITE_URL || "http://localhost:5173";
|
||||
const authSecret = process.env.BETTER_AUTH_SECRET;
|
||||
|
||||
// The component client has methods needed for integrating Convex with Better Auth,
|
||||
// as well as helper methods for general use.
|
||||
export const authComponent = createClient<DataModel>(components.betterAuth, {
|
||||
local: {
|
||||
schema: schema as any,
|
||||
},
|
||||
});
|
||||
|
||||
export const createAuth = (
|
||||
ctx: GenericCtx<DataModel>,
|
||||
{ optionsOnly } = { optionsOnly: false }
|
||||
) => {
|
||||
return betterAuth({
|
||||
// Secret para criptografia de tokens - OBRIGATÓRIO em produção
|
||||
secret: authSecret,
|
||||
// disable logging when createAuth is called just to generate options.
|
||||
// this is not required, but there's a lot of noise in logs without it.
|
||||
logger: {
|
||||
disabled: optionsOnly,
|
||||
},
|
||||
baseURL: siteUrl,
|
||||
database: authComponent.adapter(ctx),
|
||||
// Configure simple, non-verified email/password to get started
|
||||
emailAndPassword: {
|
||||
enabled: true,
|
||||
requireEmailVerification: false,
|
||||
},
|
||||
plugins: [
|
||||
// The Convex plugin is required for Convex compatibility
|
||||
convex(),
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
// Example function for getting the current user
|
||||
// Feel free to edit, omit, etc.
|
||||
export const getCurrentUser = query({
|
||||
args: {},
|
||||
handler: async (ctx) => {
|
||||
return authComponent.getAuthUser(ctx as any);
|
||||
},
|
||||
});
|
||||
@@ -1,13 +0,0 @@
|
||||
import { createApi } from "@convex-dev/better-auth";
|
||||
import schema from "./schema";
|
||||
import { createAuth } from "../auth";
|
||||
|
||||
export const {
|
||||
create,
|
||||
findOne,
|
||||
findMany,
|
||||
updateOne,
|
||||
updateMany,
|
||||
deleteOne,
|
||||
deleteMany,
|
||||
} = createApi(schema, createAuth);
|
||||
@@ -1,5 +0,0 @@
|
||||
import { createAuth } from "../auth";
|
||||
import { getStaticAuth } from "@convex-dev/better-auth";
|
||||
|
||||
// Export a static instance for Better Auth schema generation
|
||||
export const auth = getStaticAuth(createAuth);
|
||||
@@ -1,5 +0,0 @@
|
||||
import { defineComponent } from "convex/server";
|
||||
|
||||
const component = defineComponent("betterAuth");
|
||||
|
||||
export default component;
|
||||
@@ -1,70 +0,0 @@
|
||||
// This file is auto-generated. Do not edit this file manually.
|
||||
// To regenerate the schema, run:
|
||||
// `npx @better-auth/cli generate --output undefined -y`
|
||||
|
||||
import { defineSchema, defineTable } from "convex/server";
|
||||
import { v } from "convex/values";
|
||||
|
||||
export const tables = {
|
||||
user: defineTable({
|
||||
name: v.string(),
|
||||
email: v.string(),
|
||||
emailVerified: v.boolean(),
|
||||
image: v.optional(v.union(v.null(), v.string())),
|
||||
createdAt: v.number(),
|
||||
updatedAt: v.number(),
|
||||
userId: v.optional(v.union(v.null(), v.string())),
|
||||
})
|
||||
.index("email_name", ["email","name"])
|
||||
.index("name", ["name"])
|
||||
.index("userId", ["userId"]),
|
||||
session: defineTable({
|
||||
expiresAt: v.number(),
|
||||
token: v.string(),
|
||||
createdAt: v.number(),
|
||||
updatedAt: v.number(),
|
||||
ipAddress: v.optional(v.union(v.null(), v.string())),
|
||||
userAgent: v.optional(v.union(v.null(), v.string())),
|
||||
userId: v.string(),
|
||||
})
|
||||
.index("expiresAt", ["expiresAt"])
|
||||
.index("expiresAt_userId", ["expiresAt","userId"])
|
||||
.index("token", ["token"])
|
||||
.index("userId", ["userId"]),
|
||||
account: defineTable({
|
||||
accountId: v.string(),
|
||||
providerId: v.string(),
|
||||
userId: v.string(),
|
||||
accessToken: v.optional(v.union(v.null(), v.string())),
|
||||
refreshToken: v.optional(v.union(v.null(), v.string())),
|
||||
idToken: v.optional(v.union(v.null(), v.string())),
|
||||
accessTokenExpiresAt: v.optional(v.union(v.null(), v.number())),
|
||||
refreshTokenExpiresAt: v.optional(v.union(v.null(), v.number())),
|
||||
scope: v.optional(v.union(v.null(), v.string())),
|
||||
password: v.optional(v.union(v.null(), v.string())),
|
||||
createdAt: v.number(),
|
||||
updatedAt: v.number(),
|
||||
})
|
||||
.index("accountId", ["accountId"])
|
||||
.index("accountId_providerId", ["accountId","providerId"])
|
||||
.index("providerId_userId", ["providerId","userId"])
|
||||
.index("userId", ["userId"]),
|
||||
verification: defineTable({
|
||||
identifier: v.string(),
|
||||
value: v.string(),
|
||||
expiresAt: v.number(),
|
||||
createdAt: v.number(),
|
||||
updatedAt: v.number(),
|
||||
})
|
||||
.index("expiresAt", ["expiresAt"])
|
||||
.index("identifier", ["identifier"]),
|
||||
jwks: defineTable({
|
||||
publicKey: v.string(),
|
||||
privateKey: v.string(),
|
||||
createdAt: v.number(),
|
||||
}),
|
||||
};
|
||||
|
||||
const schema = defineSchema(tables);
|
||||
|
||||
export default schema;
|
||||
@@ -1,7 +1,4 @@
|
||||
import { defineApp } from "convex/server";
|
||||
import betterAuth from "./betterAuth/convex.config";
|
||||
|
||||
const app = defineApp();
|
||||
app.use(betterAuth);
|
||||
|
||||
export default app;
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import { v } from "convex/values";
|
||||
import { mutation, query, action, internalMutation } from "./_generated/server";
|
||||
import {
|
||||
mutation,
|
||||
query,
|
||||
action,
|
||||
internalMutation,
|
||||
internalQuery,
|
||||
} from "./_generated/server";
|
||||
import { Id } from "./_generated/dataModel";
|
||||
import { renderizarTemplate } from "./templatesMensagens";
|
||||
import { internal } from "./_generated/api";
|
||||
|
||||
/**
|
||||
* Enfileirar email para envio
|
||||
@@ -15,7 +22,10 @@ export const enfileirarEmail = mutation({
|
||||
templateId: v.optional(v.id("templatesMensagens")),
|
||||
enviadoPorId: v.id("usuarios"),
|
||||
},
|
||||
returns: v.object({ sucesso: v.boolean(), emailId: v.optional(v.id("notificacoesEmail")) }),
|
||||
returns: v.object({
|
||||
sucesso: v.boolean(),
|
||||
emailId: v.optional(v.id("notificacoesEmail")),
|
||||
}),
|
||||
handler: async (ctx, args) => {
|
||||
// Validar email
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
@@ -51,7 +61,10 @@ export const enviarEmailComTemplate = mutation({
|
||||
variaveis: v.any(), // Record<string, string>
|
||||
enviadoPorId: v.id("usuarios"),
|
||||
},
|
||||
returns: v.object({ sucesso: v.boolean(), emailId: v.optional(v.id("notificacoesEmail")) }),
|
||||
returns: v.object({
|
||||
sucesso: v.boolean(),
|
||||
emailId: v.optional(v.id("notificacoesEmail")),
|
||||
}),
|
||||
handler: async (ctx, args) => {
|
||||
// Buscar template
|
||||
const template = await ctx.db
|
||||
@@ -90,25 +103,32 @@ export const enviarEmailComTemplate = mutation({
|
||||
*/
|
||||
export const listarFilaEmails = query({
|
||||
args: {
|
||||
status: v.optional(v.union(
|
||||
v.literal("pendente"),
|
||||
v.literal("enviando"),
|
||||
v.literal("enviado"),
|
||||
v.literal("falha")
|
||||
)),
|
||||
status: v.optional(
|
||||
v.union(
|
||||
v.literal("pendente"),
|
||||
v.literal("enviando"),
|
||||
v.literal("enviado"),
|
||||
v.literal("falha")
|
||||
)
|
||||
),
|
||||
limite: v.optional(v.number()),
|
||||
},
|
||||
returns: v.array(v.any()),
|
||||
handler: async (ctx, args) => {
|
||||
let query = ctx.db.query("notificacoesEmail");
|
||||
|
||||
if (args.status) {
|
||||
query = query.withIndex("by_status", (q) => q.eq("status", args.status));
|
||||
} else {
|
||||
query = query.withIndex("by_criado_em");
|
||||
const emails = await ctx.db
|
||||
.query("notificacoesEmail")
|
||||
.withIndex("by_status", (q) => q.eq("status", args.status!))
|
||||
.order("desc")
|
||||
.take(args.limite ?? 100);
|
||||
return emails;
|
||||
}
|
||||
|
||||
const emails = await query.order("desc").take(args.limite || 100);
|
||||
|
||||
const emails = await ctx.db
|
||||
.query("notificacoesEmail")
|
||||
.withIndex("by_criado_em")
|
||||
.order("desc")
|
||||
.take(args.limite ?? 100);
|
||||
return emails;
|
||||
},
|
||||
});
|
||||
@@ -144,6 +164,65 @@ export const reenviarEmail = mutation({
|
||||
*
|
||||
* NOTA: Este é um placeholder. Implementação real requer nodemailer.
|
||||
*/
|
||||
export const getEmailById = internalQuery({
|
||||
args: { emailId: v.id("notificacoesEmail") },
|
||||
returns: v.union(v.any(), v.null()),
|
||||
handler: async (ctx, args) => {
|
||||
return await ctx.db.get(args.emailId);
|
||||
},
|
||||
});
|
||||
|
||||
export const getActiveEmailConfig = internalQuery({
|
||||
args: {},
|
||||
returns: v.union(v.any(), v.null()),
|
||||
handler: async (ctx) => {
|
||||
return await ctx.db
|
||||
.query("configuracaoEmail")
|
||||
.withIndex("by_ativo", (q) => q.eq("ativo", true))
|
||||
.first();
|
||||
},
|
||||
});
|
||||
|
||||
export const markEmailEnviando = internalMutation({
|
||||
args: { emailId: v.id("notificacoesEmail") },
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
const email = await ctx.db.get(args.emailId);
|
||||
await ctx.db.patch(args.emailId, {
|
||||
status: "enviando",
|
||||
tentativas: ((email as any)?.tentativas || 0) + 1,
|
||||
ultimaTentativa: Date.now(),
|
||||
});
|
||||
return null;
|
||||
},
|
||||
});
|
||||
|
||||
export const markEmailEnviado = internalMutation({
|
||||
args: { emailId: v.id("notificacoesEmail") },
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
await ctx.db.patch(args.emailId, {
|
||||
status: "enviado",
|
||||
enviadoEm: Date.now(),
|
||||
});
|
||||
return null;
|
||||
},
|
||||
});
|
||||
|
||||
export const markEmailFalha = internalMutation({
|
||||
args: { emailId: v.id("notificacoesEmail"), erro: v.string() },
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
const email = await ctx.db.get(args.emailId);
|
||||
await ctx.db.patch(args.emailId, {
|
||||
status: "falha",
|
||||
erroDetalhes: args.erro,
|
||||
tentativas: ((email as any)?.tentativas || 0) + 1,
|
||||
});
|
||||
return null;
|
||||
},
|
||||
});
|
||||
|
||||
export const enviarEmailAction = action({
|
||||
args: {
|
||||
emailId: v.id("notificacoesEmail"),
|
||||
@@ -154,8 +233,8 @@ export const enviarEmailAction = action({
|
||||
|
||||
try {
|
||||
// Buscar email da fila
|
||||
const email = await ctx.runQuery(async (ctx) => {
|
||||
return await ctx.db.get(args.emailId);
|
||||
const email = await ctx.runQuery(internal.email.getEmailById, {
|
||||
emailId: args.emailId,
|
||||
});
|
||||
|
||||
if (!email) {
|
||||
@@ -163,52 +242,41 @@ export const enviarEmailAction = action({
|
||||
}
|
||||
|
||||
// Buscar configuração SMTP
|
||||
const config = await ctx.runQuery(async (ctx) => {
|
||||
return await ctx.db
|
||||
.query("configuracaoEmail")
|
||||
.withIndex("by_ativo", (q) => q.eq("ativo", true))
|
||||
.first();
|
||||
});
|
||||
const config = await ctx.runQuery(
|
||||
internal.email.getActiveEmailConfig,
|
||||
{}
|
||||
);
|
||||
|
||||
if (!config) {
|
||||
return { sucesso: false, erro: "Configuração de email não encontrada" };
|
||||
}
|
||||
|
||||
// Marcar como enviando
|
||||
await ctx.runMutation(async (ctx) => {
|
||||
await ctx.db.patch(args.emailId, {
|
||||
status: "enviando",
|
||||
tentativas: (email.tentativas || 0) + 1,
|
||||
ultimaTentativa: Date.now(),
|
||||
});
|
||||
await ctx.runMutation(internal.email.markEmailEnviando, {
|
||||
emailId: args.emailId,
|
||||
});
|
||||
|
||||
// TODO: Enviar email real com nodemailer aqui
|
||||
console.log("⚠️ AVISO: Envio de email simulado (nodemailer não instalado)");
|
||||
console.log(" Para:", email.destinatario);
|
||||
console.log(" Assunto:", email.assunto);
|
||||
console.log(
|
||||
"⚠️ AVISO: Envio de email simulado (nodemailer não instalado)"
|
||||
);
|
||||
console.log(" Para:", (email as any).destinatario);
|
||||
console.log(" Assunto:", (email as any).assunto);
|
||||
|
||||
// Simular delay de envio
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
|
||||
// Marcar como enviado
|
||||
await ctx.runMutation(async (ctx) => {
|
||||
await ctx.db.patch(args.emailId, {
|
||||
status: "enviado",
|
||||
enviadoEm: Date.now(),
|
||||
});
|
||||
await ctx.runMutation(internal.email.markEmailEnviado, {
|
||||
emailId: args.emailId,
|
||||
});
|
||||
|
||||
return { sucesso: true };
|
||||
} catch (error: any) {
|
||||
// Marcar como falha
|
||||
await ctx.runMutation(async (ctx) => {
|
||||
const email = await ctx.db.get(args.emailId);
|
||||
await ctx.db.patch(args.emailId, {
|
||||
status: "falha",
|
||||
erroDetalhes: error.message || "Erro desconhecido",
|
||||
tentativas: (email?.tentativas || 0) + 1,
|
||||
});
|
||||
await ctx.runMutation(internal.email.markEmailFalha, {
|
||||
emailId: args.emailId,
|
||||
erro: error.message || "Erro ao enviar email",
|
||||
});
|
||||
|
||||
return { sucesso: false, erro: error.message || "Erro ao enviar email" };
|
||||
@@ -221,6 +289,7 @@ export const enviarEmailAction = action({
|
||||
*/
|
||||
export const processarFilaEmails = internalMutation({
|
||||
args: {},
|
||||
returns: v.object({ processados: v.number() }),
|
||||
handler: async (ctx) => {
|
||||
// Buscar emails pendentes (max 10 por execução)
|
||||
const emailsPendentes = await ctx.db
|
||||
@@ -255,5 +324,3 @@ export const processarFilaEmails = internalMutation({
|
||||
return { processados };
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -1,18 +1,49 @@
|
||||
import { v } from "convex/values";
|
||||
import { query, mutation } from "./_generated/server";
|
||||
import { internal } from "./_generated/api";
|
||||
import { simboloTipo } from "./schema";
|
||||
|
||||
// Validadores para campos opcionais
|
||||
const sexoValidator = v.optional(v.union(v.literal("masculino"), v.literal("feminino"), v.literal("outro")));
|
||||
const estadoCivilValidator = v.optional(v.union(v.literal("solteiro"), v.literal("casado"), v.literal("divorciado"), v.literal("viuvo"), v.literal("uniao_estavel")));
|
||||
const grauInstrucaoValidator = v.optional(v.union(v.literal("fundamental"), v.literal("medio"), v.literal("superior"), v.literal("pos_graduacao"), v.literal("mestrado"), v.literal("doutorado")));
|
||||
const grupoSanguineoValidator = v.optional(v.union(v.literal("A"), v.literal("B"), v.literal("AB"), v.literal("O")));
|
||||
const fatorRHValidator = v.optional(v.union(v.literal("positivo"), v.literal("negativo")));
|
||||
const aposentadoValidator = v.optional(v.union(v.literal("nao"), v.literal("funape_ipsep"), v.literal("inss")));
|
||||
const sexoValidator = v.optional(
|
||||
v.union(v.literal("masculino"), v.literal("feminino"), v.literal("outro"))
|
||||
);
|
||||
const estadoCivilValidator = v.optional(
|
||||
v.union(
|
||||
v.literal("solteiro"),
|
||||
v.literal("casado"),
|
||||
v.literal("divorciado"),
|
||||
v.literal("viuvo"),
|
||||
v.literal("uniao_estavel")
|
||||
)
|
||||
);
|
||||
const grauInstrucaoValidator = v.optional(
|
||||
v.union(
|
||||
v.literal("fundamental"),
|
||||
v.literal("medio"),
|
||||
v.literal("superior"),
|
||||
v.literal("pos_graduacao"),
|
||||
v.literal("mestrado"),
|
||||
v.literal("doutorado")
|
||||
)
|
||||
);
|
||||
const grupoSanguineoValidator = v.optional(
|
||||
v.union(v.literal("A"), v.literal("B"), v.literal("AB"), v.literal("O"))
|
||||
);
|
||||
const fatorRHValidator = v.optional(
|
||||
v.union(v.literal("positivo"), v.literal("negativo"))
|
||||
);
|
||||
const aposentadoValidator = v.optional(
|
||||
v.union(v.literal("nao"), v.literal("funape_ipsep"), v.literal("inss"))
|
||||
);
|
||||
|
||||
export const getAll = query({
|
||||
args: {},
|
||||
handler: async (ctx) => {
|
||||
// Autorização: listar funcionários
|
||||
await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, {
|
||||
recurso: "funcionarios",
|
||||
acao: "listar",
|
||||
});
|
||||
const funcionarios = await ctx.db.query("funcionarios").collect();
|
||||
// Retornar apenas os campos necessários para listagem
|
||||
return funcionarios.map((f: any) => ({
|
||||
@@ -40,6 +71,11 @@ export const getAll = query({
|
||||
export const getById = query({
|
||||
args: { id: v.id("funcionarios") },
|
||||
handler: async (ctx, args) => {
|
||||
// Autorização: ver funcionário
|
||||
await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, {
|
||||
recurso: "funcionarios",
|
||||
acao: "ver",
|
||||
});
|
||||
return await ctx.db.get(args.id);
|
||||
},
|
||||
});
|
||||
@@ -140,6 +176,11 @@ export const create = mutation({
|
||||
},
|
||||
returns: v.id("funcionarios"),
|
||||
handler: async (ctx, args) => {
|
||||
// Autorização: criar
|
||||
await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, {
|
||||
recurso: "funcionarios",
|
||||
acao: "criar",
|
||||
});
|
||||
// Unicidade: CPF
|
||||
const cpfExists = await ctx.db
|
||||
.query("funcionarios")
|
||||
@@ -260,6 +301,11 @@ export const update = mutation({
|
||||
},
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
// Autorização: editar
|
||||
await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, {
|
||||
recurso: "funcionarios",
|
||||
acao: "editar",
|
||||
});
|
||||
// Unicidade: CPF (excluindo o próprio registro)
|
||||
const cpfExists = await ctx.db
|
||||
.query("funcionarios")
|
||||
@@ -288,6 +334,11 @@ export const remove = mutation({
|
||||
args: { id: v.id("funcionarios") },
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
// Autorização: excluir
|
||||
await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, {
|
||||
recurso: "funcionarios",
|
||||
acao: "excluir",
|
||||
});
|
||||
// TODO: Talvez queiramos também remover os arquivos do storage
|
||||
await ctx.db.delete(args.id);
|
||||
return null;
|
||||
@@ -298,6 +349,10 @@ export const remove = mutation({
|
||||
export const getFichaCompleta = query({
|
||||
args: { id: v.id("funcionarios") },
|
||||
handler: async (ctx, args) => {
|
||||
await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, {
|
||||
recurso: "funcionarios",
|
||||
acao: "ver",
|
||||
});
|
||||
const funcionario = await ctx.db.get(args.id);
|
||||
if (!funcionario) {
|
||||
return null;
|
||||
@@ -308,11 +363,13 @@ export const getFichaCompleta = query({
|
||||
|
||||
return {
|
||||
...funcionario,
|
||||
simbolo: simbolo ? {
|
||||
nome: simbolo.nome,
|
||||
descricao: simbolo.descricao,
|
||||
valor: simbolo.valor,
|
||||
} : null,
|
||||
simbolo: simbolo
|
||||
? {
|
||||
nome: simbolo.nome,
|
||||
descricao: simbolo.descricao,
|
||||
valor: simbolo.valor,
|
||||
}
|
||||
: null,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@@ -13,7 +13,7 @@ export const listarTodosRoles = query({
|
||||
descricao: v.string(),
|
||||
nivel: v.number(),
|
||||
setor: v.optional(v.string()),
|
||||
customizado: v.boolean(),
|
||||
customizado: v.optional(v.boolean()),
|
||||
editavel: v.optional(v.boolean()),
|
||||
_creationTime: v.number(),
|
||||
})
|
||||
@@ -91,9 +91,10 @@ export const limparPerfisAntigos = internalMutation({
|
||||
deveManter = true;
|
||||
perfisCorretos.set("ti_master", true);
|
||||
} else {
|
||||
motivo = role.nivel !== 0
|
||||
? "TI_MASTER deve ser nível 0, este é nível " + role.nivel
|
||||
: "TI_MASTER duplicado";
|
||||
motivo =
|
||||
role.nivel !== 0
|
||||
? "TI_MASTER deve ser nível 0, este é nível " + role.nivel
|
||||
: "TI_MASTER duplicado";
|
||||
}
|
||||
}
|
||||
// ADMIN - Manter apenas o de nível 2
|
||||
@@ -102,9 +103,10 @@ export const limparPerfisAntigos = internalMutation({
|
||||
deveManter = true;
|
||||
perfisCorretos.set("admin", true);
|
||||
} else {
|
||||
motivo = role.nivel !== 2
|
||||
? "ADMIN deve ser nível 2, este é nível " + role.nivel
|
||||
: "ADMIN duplicado";
|
||||
motivo =
|
||||
role.nivel !== 2
|
||||
? "ADMIN deve ser nível 2, este é nível " + role.nivel
|
||||
: "ADMIN duplicado";
|
||||
}
|
||||
}
|
||||
// TI_USUARIO - Manter apenas o de nível 2
|
||||
@@ -113,14 +115,16 @@ export const limparPerfisAntigos = internalMutation({
|
||||
deveManter = true;
|
||||
perfisCorretos.set("ti_usuario", true);
|
||||
} else {
|
||||
motivo = role.nivel !== 2
|
||||
? "TI_USUARIO deve ser nível 2, este é nível " + role.nivel
|
||||
: "TI_USUARIO duplicado";
|
||||
motivo =
|
||||
role.nivel !== 2
|
||||
? "TI_USUARIO deve ser nível 2, este é nível " + role.nivel
|
||||
: "TI_USUARIO duplicado";
|
||||
}
|
||||
}
|
||||
// Perfis genéricos antigos (remover)
|
||||
else if (role.nome === "ti") {
|
||||
motivo = "Perfil genérico 'ti' obsoleto - usar 'ti_master' ou 'ti_usuario'";
|
||||
motivo =
|
||||
"Perfil genérico 'ti' obsoleto - usar 'ti_master' ou 'ti_usuario'";
|
||||
}
|
||||
// Outros perfis específicos de setores (manter se forem nível >= 2)
|
||||
else if (
|
||||
@@ -157,7 +161,9 @@ export const limparPerfisAntigos = internalMutation({
|
||||
descricao: role.descricao,
|
||||
nivel: role.nivel,
|
||||
});
|
||||
console.log(`✅ MANTIDO: ${role.nome} (${role.descricao}) - Nível ${role.nivel}`);
|
||||
console.log(
|
||||
`✅ MANTIDO: ${role.nome} (${role.descricao}) - Nível ${role.nivel}`
|
||||
);
|
||||
} else {
|
||||
// Verificar se há usuários usando este perfil
|
||||
const usuariosComRole = await ctx.db
|
||||
@@ -286,5 +292,3 @@ export const verificarNiveisIncorretos = query({
|
||||
return problemas;
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Doc, Id } from "./_generated/dataModel";
|
||||
* Use em todas as mutations que modificam dados
|
||||
*/
|
||||
export async function registrarAtividade(
|
||||
ctx: QueryCtx | MutationCtx,
|
||||
ctx: MutationCtx,
|
||||
usuarioId: Id<"usuarios">,
|
||||
acao: string,
|
||||
recurso: string,
|
||||
@@ -37,21 +37,34 @@ export const listarAtividades = query({
|
||||
limite: v.optional(v.number()),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
let query = ctx.db.query("logsAtividades");
|
||||
let atividades;
|
||||
|
||||
// Aplicar filtros
|
||||
if (args.usuarioId) {
|
||||
query = query.withIndex("by_usuario", (q) => q.eq("usuarioId", args.usuarioId));
|
||||
atividades = await ctx.db
|
||||
.query("logsAtividades")
|
||||
.withIndex("by_usuario", (q) => q.eq("usuarioId", args.usuarioId!))
|
||||
.order("desc")
|
||||
.take(args.limite || 100);
|
||||
} else if (args.acao) {
|
||||
query = query.withIndex("by_acao", (q) => q.eq("acao", args.acao));
|
||||
atividades = await ctx.db
|
||||
.query("logsAtividades")
|
||||
.withIndex("by_acao", (q) => q.eq("acao", args.acao!))
|
||||
.order("desc")
|
||||
.take(args.limite || 100);
|
||||
} else if (args.recurso) {
|
||||
query = query.withIndex("by_recurso", (q) => q.eq("recurso", args.recurso));
|
||||
atividades = await ctx.db
|
||||
.query("logsAtividades")
|
||||
.withIndex("by_recurso", (q) => q.eq("recurso", args.recurso!))
|
||||
.order("desc")
|
||||
.take(args.limite || 100);
|
||||
} else {
|
||||
query = query.withIndex("by_timestamp");
|
||||
atividades = await ctx.db
|
||||
.query("logsAtividades")
|
||||
.withIndex("by_timestamp")
|
||||
.order("desc")
|
||||
.take(args.limite || 100);
|
||||
}
|
||||
|
||||
let atividades = await query.order("desc").take(args.limite || 100);
|
||||
|
||||
// Filtrar por range de datas se fornecido
|
||||
if (args.dataInicio || args.dataFim) {
|
||||
atividades = atividades.filter((log) => {
|
||||
@@ -155,5 +168,3 @@ export const obterHistoricoRecurso = query({
|
||||
return atividadesComUsuarios;
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Doc, Id } from "./_generated/dataModel";
|
||||
* Helper para registrar tentativas de login
|
||||
*/
|
||||
export async function registrarLogin(
|
||||
ctx: QueryCtx | MutationCtx,
|
||||
ctx: MutationCtx,
|
||||
dados: {
|
||||
usuarioId?: Id<"usuarios">;
|
||||
matriculaOuEmail: string;
|
||||
@@ -170,26 +170,32 @@ export const obterEstatisticasLogin = query({
|
||||
|
||||
// Logins por horário (hora do dia)
|
||||
const porHorario: Record<number, number> = {};
|
||||
logs.filter((l) => l.sucesso).forEach((log) => {
|
||||
const hora = new Date(log.timestamp).getHours();
|
||||
porHorario[hora] = (porHorario[hora] || 0) + 1;
|
||||
});
|
||||
logs
|
||||
.filter((l) => l.sucesso)
|
||||
.forEach((log) => {
|
||||
const hora = new Date(log.timestamp).getHours();
|
||||
porHorario[hora] = (porHorario[hora] || 0) + 1;
|
||||
});
|
||||
|
||||
// Browser mais usado
|
||||
const porBrowser: Record<string, number> = {};
|
||||
logs.filter((l) => l.sucesso).forEach((log) => {
|
||||
if (log.browser) {
|
||||
porBrowser[log.browser] = (porBrowser[log.browser] || 0) + 1;
|
||||
}
|
||||
});
|
||||
logs
|
||||
.filter((l) => l.sucesso)
|
||||
.forEach((log) => {
|
||||
if (log.browser) {
|
||||
porBrowser[log.browser] = (porBrowser[log.browser] || 0) + 1;
|
||||
}
|
||||
});
|
||||
|
||||
// Dispositivos mais usados
|
||||
const porDevice: Record<string, number> = {};
|
||||
logs.filter((l) => l.sucesso).forEach((log) => {
|
||||
if (log.device) {
|
||||
porDevice[log.device] = (porDevice[log.device] || 0) + 1;
|
||||
}
|
||||
});
|
||||
logs
|
||||
.filter((l) => l.sucesso)
|
||||
.forEach((log) => {
|
||||
if (log.device) {
|
||||
porDevice[log.device] = (porDevice[log.device] || 0) + 1;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
total: logs.length,
|
||||
@@ -231,4 +237,3 @@ export const verificarIPSuspeito = query({
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,528 +0,0 @@
|
||||
import { v } from "convex/values";
|
||||
import { mutation, query } from "./_generated/server";
|
||||
import type { Id } from "./_generated/dataModel";
|
||||
|
||||
/**
|
||||
* Lista de menus do sistema
|
||||
*/
|
||||
export const MENUS_SISTEMA = [
|
||||
{ path: "/recursos-humanos", nome: "Recursos Humanos", descricao: "Gestão de funcionários e símbolos" },
|
||||
{ path: "/recursos-humanos/funcionarios", nome: "Funcionários", descricao: "Cadastro e gestão de funcionários" },
|
||||
{ path: "/recursos-humanos/simbolos", nome: "Símbolos", descricao: "Cadastro e gestão de símbolos" },
|
||||
{ path: "/financeiro", nome: "Financeiro", descricao: "Gestão financeira" },
|
||||
{ path: "/controladoria", nome: "Controladoria", descricao: "Controle e auditoria" },
|
||||
{ path: "/licitacoes", nome: "Licitações", descricao: "Gestão de licitações" },
|
||||
{ path: "/compras", nome: "Compras", descricao: "Gestão de compras" },
|
||||
{ path: "/juridico", nome: "Jurídico", descricao: "Departamento jurídico" },
|
||||
{ path: "/comunicacao", nome: "Comunicação", descricao: "Gestão de comunicação" },
|
||||
{ path: "/programas-esportivos", nome: "Programas Esportivos", descricao: "Gestão de programas esportivos" },
|
||||
{ path: "/secretaria-executiva", nome: "Secretaria Executiva", descricao: "Secretaria executiva" },
|
||||
{ path: "/gestao-pessoas", nome: "Gestão de Pessoas", descricao: "Gestão de recursos humanos" },
|
||||
{ path: "/ti", nome: "Tecnologia da Informação", descricao: "TI e suporte técnico" },
|
||||
{ path: "/ti/painel-administrativo", nome: "Painel Administrativo TI", descricao: "Painel de administração do sistema" },
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* Listar todas as permissões de menu para uma role
|
||||
*/
|
||||
export const listarPorRole = query({
|
||||
args: { roleId: v.id("roles") },
|
||||
returns: v.array(
|
||||
v.object({
|
||||
_id: v.id("menuPermissoes"),
|
||||
roleId: v.id("roles"),
|
||||
menuPath: v.string(),
|
||||
podeAcessar: v.boolean(),
|
||||
podeConsultar: v.boolean(),
|
||||
podeGravar: v.boolean(),
|
||||
})
|
||||
),
|
||||
handler: async (ctx, args) => {
|
||||
return await ctx.db
|
||||
.query("menuPermissoes")
|
||||
.withIndex("by_role", (q) => q.eq("roleId", args.roleId))
|
||||
.collect();
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Verificar se um usuário tem permissão para acessar um menu
|
||||
* Prioridade: Permissão personalizada > Permissão da role
|
||||
*/
|
||||
export const verificarAcesso = query({
|
||||
args: {
|
||||
usuarioId: v.id("usuarios"),
|
||||
menuPath: v.string(),
|
||||
},
|
||||
returns: v.object({
|
||||
podeAcessar: v.boolean(),
|
||||
podeConsultar: v.boolean(),
|
||||
podeGravar: v.boolean(),
|
||||
motivo: v.optional(v.string()),
|
||||
}),
|
||||
handler: async (ctx, args) => {
|
||||
// Buscar o usuário
|
||||
const usuario = await ctx.db.get(args.usuarioId);
|
||||
if (!usuario) {
|
||||
return {
|
||||
podeAcessar: false,
|
||||
podeConsultar: false,
|
||||
podeGravar: false,
|
||||
motivo: "Usuário não encontrado",
|
||||
};
|
||||
}
|
||||
|
||||
// Verificar se o usuário está ativo
|
||||
if (!usuario.ativo) {
|
||||
return {
|
||||
podeAcessar: false,
|
||||
podeConsultar: false,
|
||||
podeGravar: false,
|
||||
motivo: "Usuário inativo",
|
||||
};
|
||||
}
|
||||
|
||||
// Buscar a role do usuário
|
||||
const role = await ctx.db.get(usuario.roleId);
|
||||
if (!role) {
|
||||
return {
|
||||
podeAcessar: false,
|
||||
podeConsultar: false,
|
||||
podeGravar: false,
|
||||
motivo: "Role não encontrada",
|
||||
};
|
||||
}
|
||||
|
||||
// Apenas TI_MASTER (nível 0) tem acesso total irrestrito
|
||||
// Admin, TI_USUARIO e outros (nível >= 1) têm permissões configuráveis
|
||||
if (role.nivel === 0) {
|
||||
return {
|
||||
podeAcessar: true,
|
||||
podeConsultar: true,
|
||||
podeGravar: true,
|
||||
};
|
||||
}
|
||||
|
||||
// Dashboard e Solicitar Acesso são públicos
|
||||
if (args.menuPath === "/" || args.menuPath === "/solicitar-acesso") {
|
||||
return {
|
||||
podeAcessar: true,
|
||||
podeConsultar: true,
|
||||
podeGravar: false,
|
||||
};
|
||||
}
|
||||
|
||||
// 1. Verificar se existe permissão personalizada para este usuário
|
||||
const permissaoPersonalizada = await ctx.db
|
||||
.query("menuPermissoesPersonalizadas")
|
||||
.withIndex("by_usuario_and_menu", (q) =>
|
||||
q.eq("usuarioId", args.usuarioId).eq("menuPath", args.menuPath)
|
||||
)
|
||||
.first();
|
||||
|
||||
if (permissaoPersonalizada) {
|
||||
return {
|
||||
podeAcessar: permissaoPersonalizada.podeAcessar,
|
||||
podeConsultar: permissaoPersonalizada.podeConsultar,
|
||||
podeGravar: permissaoPersonalizada.podeGravar,
|
||||
};
|
||||
}
|
||||
|
||||
// 2. Se não houver permissão personalizada, verificar permissão da role
|
||||
const permissaoRole = await ctx.db
|
||||
.query("menuPermissoes")
|
||||
.withIndex("by_role_and_menu", (q) =>
|
||||
q.eq("roleId", usuario.roleId).eq("menuPath", args.menuPath)
|
||||
)
|
||||
.first();
|
||||
|
||||
if (!permissaoRole) {
|
||||
return {
|
||||
podeAcessar: false,
|
||||
podeConsultar: false,
|
||||
podeGravar: false,
|
||||
motivo: "Sem permissão configurada para este menu",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
podeAcessar: permissaoRole.podeAcessar,
|
||||
podeConsultar: permissaoRole.podeConsultar,
|
||||
podeGravar: permissaoRole.podeGravar,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Atualizar ou criar permissão de menu para uma role
|
||||
*/
|
||||
export const atualizarPermissao = mutation({
|
||||
args: {
|
||||
roleId: v.id("roles"),
|
||||
menuPath: v.string(),
|
||||
podeAcessar: v.boolean(),
|
||||
podeConsultar: v.boolean(),
|
||||
podeGravar: v.boolean(),
|
||||
},
|
||||
returns: v.id("menuPermissoes"),
|
||||
handler: async (ctx, args) => {
|
||||
// Verificar se já existe uma permissão
|
||||
const existente = await ctx.db
|
||||
.query("menuPermissoes")
|
||||
.withIndex("by_role_and_menu", (q) =>
|
||||
q.eq("roleId", args.roleId).eq("menuPath", args.menuPath)
|
||||
)
|
||||
.first();
|
||||
|
||||
if (existente) {
|
||||
// Atualizar permissão existente
|
||||
await ctx.db.patch(existente._id, {
|
||||
podeAcessar: args.podeAcessar,
|
||||
podeConsultar: args.podeConsultar,
|
||||
podeGravar: args.podeGravar,
|
||||
});
|
||||
return existente._id;
|
||||
} else {
|
||||
// Criar nova permissão
|
||||
return await ctx.db.insert("menuPermissoes", {
|
||||
roleId: args.roleId,
|
||||
menuPath: args.menuPath,
|
||||
podeAcessar: args.podeAcessar,
|
||||
podeConsultar: args.podeConsultar,
|
||||
podeGravar: args.podeGravar,
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Remover permissão de menu
|
||||
*/
|
||||
export const removerPermissao = mutation({
|
||||
args: {
|
||||
permissaoId: v.id("menuPermissoes"),
|
||||
},
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
await ctx.db.delete(args.permissaoId);
|
||||
return null;
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Inicializar permissões padrão para uma role
|
||||
*/
|
||||
export const inicializarPermissoesRole = mutation({
|
||||
args: {
|
||||
roleId: v.id("roles"),
|
||||
},
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
// Buscar a role
|
||||
const role = await ctx.db.get(args.roleId);
|
||||
if (!role) {
|
||||
throw new Error("Role não encontrada");
|
||||
}
|
||||
|
||||
// Admin e TI não precisam de permissões específicas (acesso total)
|
||||
if (role.nivel <= 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Para outras roles, criar permissões básicas (apenas consulta)
|
||||
for (const menu of MENUS_SISTEMA) {
|
||||
// Verificar se já existe permissão
|
||||
const existente = await ctx.db
|
||||
.query("menuPermissoes")
|
||||
.withIndex("by_role_and_menu", (q) =>
|
||||
q.eq("roleId", args.roleId).eq("menuPath", menu.path)
|
||||
)
|
||||
.first();
|
||||
|
||||
if (!existente) {
|
||||
// Criar permissão padrão (sem acesso)
|
||||
await ctx.db.insert("menuPermissoes", {
|
||||
roleId: args.roleId,
|
||||
menuPath: menu.path,
|
||||
podeAcessar: false,
|
||||
podeConsultar: false,
|
||||
podeGravar: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Listar todos os menus do sistema
|
||||
*/
|
||||
export const listarMenus = query({
|
||||
args: {},
|
||||
returns: v.array(
|
||||
v.object({
|
||||
path: v.string(),
|
||||
nome: v.string(),
|
||||
descricao: v.string(),
|
||||
})
|
||||
),
|
||||
handler: async (ctx) => {
|
||||
return MENUS_SISTEMA.map((menu) => ({
|
||||
path: menu.path,
|
||||
nome: menu.nome,
|
||||
descricao: menu.descricao,
|
||||
}));
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Obter matriz de permissões (role x menu) para o painel de controle
|
||||
*/
|
||||
export const obterMatrizPermissoes = query({
|
||||
args: {},
|
||||
returns: v.array(
|
||||
v.object({
|
||||
role: v.object({
|
||||
_id: v.id("roles"),
|
||||
nome: v.string(),
|
||||
nivel: v.number(),
|
||||
descricao: v.string(),
|
||||
}),
|
||||
permissoes: v.array(
|
||||
v.object({
|
||||
menuPath: v.string(),
|
||||
menuNome: v.string(),
|
||||
podeAcessar: v.boolean(),
|
||||
podeConsultar: v.boolean(),
|
||||
podeGravar: v.boolean(),
|
||||
permissaoId: v.optional(v.id("menuPermissoes")),
|
||||
})
|
||||
),
|
||||
})
|
||||
),
|
||||
handler: async (ctx) => {
|
||||
// Buscar todas as roles
|
||||
// TI_MASTER (nível 0) aparece mas não é editável
|
||||
// Admin, TI_USUARIO e outros (nível >= 1) são configuráveis
|
||||
const roles = await ctx.db.query("roles").collect();
|
||||
|
||||
const matriz = [];
|
||||
|
||||
for (const role of roles) {
|
||||
const permissoes = [];
|
||||
|
||||
for (const menu of MENUS_SISTEMA) {
|
||||
// Buscar permissão específica
|
||||
const permissao = await ctx.db
|
||||
.query("menuPermissoes")
|
||||
.withIndex("by_role_and_menu", (q) =>
|
||||
q.eq("roleId", role._id).eq("menuPath", menu.path)
|
||||
)
|
||||
.first();
|
||||
|
||||
// Admin e TI têm acesso total automático
|
||||
if (role.nivel <= 1) {
|
||||
permissoes.push({
|
||||
menuPath: menu.path,
|
||||
menuNome: menu.nome,
|
||||
podeAcessar: true,
|
||||
podeConsultar: true,
|
||||
podeGravar: true,
|
||||
permissaoId: permissao?._id,
|
||||
});
|
||||
} else {
|
||||
permissoes.push({
|
||||
menuPath: menu.path,
|
||||
menuNome: menu.nome,
|
||||
podeAcessar: permissao?.podeAcessar ?? false,
|
||||
podeConsultar: permissao?.podeConsultar ?? false,
|
||||
podeGravar: permissao?.podeGravar ?? false,
|
||||
permissaoId: permissao?._id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
matriz.push({
|
||||
role: {
|
||||
_id: role._id,
|
||||
nome: role.nome,
|
||||
nivel: role.nivel,
|
||||
descricao: role.descricao,
|
||||
},
|
||||
permissoes,
|
||||
});
|
||||
}
|
||||
|
||||
return matriz;
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Criar ou atualizar permissão personalizada por matrícula
|
||||
*/
|
||||
export const atualizarPermissaoPersonalizada = mutation({
|
||||
args: {
|
||||
matricula: v.string(),
|
||||
menuPath: v.string(),
|
||||
podeAcessar: v.boolean(),
|
||||
podeConsultar: v.boolean(),
|
||||
podeGravar: v.boolean(),
|
||||
},
|
||||
returns: v.union(v.id("menuPermissoesPersonalizadas"), v.null()),
|
||||
handler: async (ctx, args) => {
|
||||
// Buscar usuário pela matrícula
|
||||
const usuario = await ctx.db
|
||||
.query("usuarios")
|
||||
.withIndex("by_matricula", (q) => q.eq("matricula", args.matricula))
|
||||
.first();
|
||||
|
||||
if (!usuario) {
|
||||
throw new Error("Usuário não encontrado com esta matrícula");
|
||||
}
|
||||
|
||||
// Verificar se já existe permissão personalizada
|
||||
const existente = await ctx.db
|
||||
.query("menuPermissoesPersonalizadas")
|
||||
.withIndex("by_usuario_and_menu", (q) =>
|
||||
q.eq("usuarioId", usuario._id).eq("menuPath", args.menuPath)
|
||||
)
|
||||
.first();
|
||||
|
||||
if (existente) {
|
||||
// Atualizar permissão existente
|
||||
await ctx.db.patch(existente._id, {
|
||||
podeAcessar: args.podeAcessar,
|
||||
podeConsultar: args.podeConsultar,
|
||||
podeGravar: args.podeGravar,
|
||||
});
|
||||
return existente._id;
|
||||
} else {
|
||||
// Criar nova permissão
|
||||
return await ctx.db.insert("menuPermissoesPersonalizadas", {
|
||||
usuarioId: usuario._id,
|
||||
matricula: args.matricula,
|
||||
menuPath: args.menuPath,
|
||||
podeAcessar: args.podeAcessar,
|
||||
podeConsultar: args.podeConsultar,
|
||||
podeGravar: args.podeGravar,
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Remover permissão personalizada
|
||||
*/
|
||||
export const removerPermissaoPersonalizada = mutation({
|
||||
args: {
|
||||
permissaoId: v.id("menuPermissoesPersonalizadas"),
|
||||
},
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
await ctx.db.delete(args.permissaoId);
|
||||
return null;
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Listar permissões personalizadas de um usuário por matrícula
|
||||
*/
|
||||
export const listarPermissoesPersonalizadas = query({
|
||||
args: {
|
||||
matricula: v.string(),
|
||||
},
|
||||
returns: v.array(
|
||||
v.object({
|
||||
_id: v.id("menuPermissoesPersonalizadas"),
|
||||
menuPath: v.string(),
|
||||
menuNome: v.string(),
|
||||
podeAcessar: v.boolean(),
|
||||
podeConsultar: v.boolean(),
|
||||
podeGravar: v.boolean(),
|
||||
})
|
||||
),
|
||||
handler: async (ctx, args) => {
|
||||
// Buscar usuário
|
||||
const usuario = await ctx.db
|
||||
.query("usuarios")
|
||||
.withIndex("by_matricula", (q) => q.eq("matricula", args.matricula))
|
||||
.first();
|
||||
|
||||
if (!usuario) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Buscar permissões personalizadas
|
||||
const permissoes = await ctx.db
|
||||
.query("menuPermissoesPersonalizadas")
|
||||
.withIndex("by_usuario", (q) => q.eq("usuarioId", usuario._id))
|
||||
.collect();
|
||||
|
||||
// Mapear com nomes dos menus
|
||||
return permissoes.map((p) => {
|
||||
const menu = MENUS_SISTEMA.find((m) => m.path === p.menuPath);
|
||||
return {
|
||||
_id: p._id,
|
||||
menuPath: p.menuPath,
|
||||
menuNome: menu?.nome || p.menuPath,
|
||||
podeAcessar: p.podeAcessar,
|
||||
podeConsultar: p.podeConsultar,
|
||||
podeGravar: p.podeGravar,
|
||||
};
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Buscar usuário por matrícula para o painel de personalização
|
||||
*/
|
||||
export const buscarUsuarioPorMatricula = query({
|
||||
args: {
|
||||
matricula: v.string(),
|
||||
},
|
||||
returns: v.union(
|
||||
v.object({
|
||||
_id: v.id("usuarios"),
|
||||
matricula: v.string(),
|
||||
nome: v.string(),
|
||||
email: v.string(),
|
||||
role: v.object({
|
||||
nome: v.string(),
|
||||
nivel: v.number(),
|
||||
descricao: v.string(),
|
||||
}),
|
||||
ativo: v.boolean(),
|
||||
}),
|
||||
v.null()
|
||||
),
|
||||
handler: async (ctx, args) => {
|
||||
const usuario = await ctx.db
|
||||
.query("usuarios")
|
||||
.withIndex("by_matricula", (q) => q.eq("matricula", args.matricula))
|
||||
.first();
|
||||
|
||||
if (!usuario) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const role = await ctx.db.get(usuario.roleId);
|
||||
if (!role) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
_id: usuario._id,
|
||||
matricula: usuario.matricula,
|
||||
nome: usuario.nome,
|
||||
email: usuario.email,
|
||||
role: {
|
||||
nome: role.nome,
|
||||
nivel: role.nivel,
|
||||
descricao: role.descricao,
|
||||
},
|
||||
ativo: usuario.ativo,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import { v } from "convex/values";
|
||||
import { mutation, query } from "./_generated/server";
|
||||
import { registrarAtividade } from "./logsAtividades";
|
||||
import { api } from "./_generated/api";
|
||||
import { Id } from "./_generated/dataModel";
|
||||
|
||||
/**
|
||||
* Listar todos os perfis customizados
|
||||
*/
|
||||
export const listarPerfisCustomizados = query({
|
||||
args: {},
|
||||
returns: v.array(v.any()),
|
||||
handler: async (ctx) => {
|
||||
const perfis = await ctx.db.query("perfisCustomizados").collect();
|
||||
|
||||
@@ -42,6 +45,16 @@ export const obterPerfilComPermissoes = query({
|
||||
args: {
|
||||
perfilId: v.id("perfisCustomizados"),
|
||||
},
|
||||
returns: v.union(
|
||||
v.object({
|
||||
perfil: v.any(),
|
||||
role: v.any(),
|
||||
permissoes: v.array(v.any()),
|
||||
menuPermissoes: v.array(v.any()),
|
||||
usuarios: v.array(v.any()),
|
||||
}),
|
||||
v.null()
|
||||
),
|
||||
handler: async (ctx, args) => {
|
||||
const perfil = await ctx.db.get(args.perfilId);
|
||||
if (!perfil) {
|
||||
@@ -99,20 +112,31 @@ export const criarPerfilCustomizado = mutation({
|
||||
criadoPorId: v.id("usuarios"),
|
||||
},
|
||||
returns: v.union(
|
||||
v.object({ sucesso: v.literal(true), perfilId: v.id("perfisCustomizados") }),
|
||||
v.object({
|
||||
sucesso: v.literal(true),
|
||||
perfilId: v.id("perfisCustomizados"),
|
||||
}),
|
||||
v.object({ sucesso: v.literal(false), erro: v.string() })
|
||||
),
|
||||
handler: async (ctx, args) => {
|
||||
// Validar nível (deve ser >= 3)
|
||||
if (args.nivel < 3) {
|
||||
return { sucesso: false as const, erro: "Perfis customizados devem ter nível >= 3" };
|
||||
return {
|
||||
sucesso: false as const,
|
||||
erro: "Perfis customizados devem ter nível >= 3",
|
||||
};
|
||||
}
|
||||
|
||||
// Verificar se nome já existe
|
||||
const roles = await ctx.db.query("roles").collect();
|
||||
const nomeExiste = roles.some((r) => r.nome.toLowerCase() === args.nome.toLowerCase());
|
||||
const nomeExiste = roles.some(
|
||||
(r) => r.nome.toLowerCase() === args.nome.toLowerCase()
|
||||
);
|
||||
if (nomeExiste) {
|
||||
return { sucesso: false as const, erro: "Já existe um perfil com este nome" };
|
||||
return {
|
||||
sucesso: false as const,
|
||||
erro: "Já existe um perfil com este nome",
|
||||
};
|
||||
}
|
||||
|
||||
// Criar role correspondente
|
||||
@@ -130,7 +154,7 @@ export const criarPerfilCustomizado = mutation({
|
||||
// Copiar permissões gerais
|
||||
const permissoesClonar = await ctx.db
|
||||
.query("rolePermissoes")
|
||||
.withIndex("by_role", (q) => q.eq("roleId", args.clonarDeRoleId))
|
||||
.withIndex("by_role", (q) => q.eq("roleId", args.clonarDeRoleId!))
|
||||
.collect();
|
||||
|
||||
for (const perm of permissoesClonar) {
|
||||
@@ -143,7 +167,7 @@ export const criarPerfilCustomizado = mutation({
|
||||
// Copiar permissões de menu
|
||||
const menuPermsClonar = await ctx.db
|
||||
.query("menuPermissoes")
|
||||
.withIndex("by_role", (q) => q.eq("roleId", args.clonarDeRoleId))
|
||||
.withIndex("by_role", (q) => q.eq("roleId", args.clonarDeRoleId!))
|
||||
.collect();
|
||||
|
||||
for (const menuPerm of menuPermsClonar) {
|
||||
@@ -321,7 +345,10 @@ export const clonarPerfil = mutation({
|
||||
criadoPorId: v.id("usuarios"),
|
||||
},
|
||||
returns: v.union(
|
||||
v.object({ sucesso: v.literal(true), perfilId: v.id("perfisCustomizados") }),
|
||||
v.object({
|
||||
sucesso: v.literal(true),
|
||||
perfilId: v.id("perfisCustomizados"),
|
||||
}),
|
||||
v.object({ sucesso: v.literal(false), erro: v.string() })
|
||||
),
|
||||
handler: async (ctx, args) => {
|
||||
@@ -330,17 +357,80 @@ export const clonarPerfil = mutation({
|
||||
return { sucesso: false as const, erro: "Perfil origem não encontrado" };
|
||||
}
|
||||
|
||||
// Criar novo perfil clonando o original
|
||||
const resultado = await criarPerfilCustomizado(ctx, {
|
||||
// Verificar se nome já existe
|
||||
const roles = await ctx.db.query("roles").collect();
|
||||
const nomeExiste = roles.some(
|
||||
(r) => r.nome.toLowerCase() === args.novoNome.toLowerCase()
|
||||
);
|
||||
if (nomeExiste) {
|
||||
return {
|
||||
sucesso: false as const,
|
||||
erro: "Já existe um perfil com este nome",
|
||||
};
|
||||
}
|
||||
|
||||
// Criar role correspondente
|
||||
const roleId = await ctx.db.insert("roles", {
|
||||
nome: args.novoNome.toLowerCase().replace(/\s+/g, "_"),
|
||||
descricao: args.novaDescricao,
|
||||
nivel: perfilOrigem.nivel,
|
||||
customizado: true,
|
||||
criadoPor: args.criadoPorId,
|
||||
editavel: true,
|
||||
});
|
||||
|
||||
// Copiar permissões gerais do perfil de origem
|
||||
const permissoesClonar = await ctx.db
|
||||
.query("rolePermissoes")
|
||||
.withIndex("by_role", (q) => q.eq("roleId", perfilOrigem.roleId))
|
||||
.collect();
|
||||
for (const perm of permissoesClonar) {
|
||||
await ctx.db.insert("rolePermissoes", {
|
||||
roleId,
|
||||
permissaoId: perm.permissaoId,
|
||||
});
|
||||
}
|
||||
|
||||
// Copiar permissões de menu
|
||||
const menuPermsClonar = await ctx.db
|
||||
.query("menuPermissoes")
|
||||
.withIndex("by_role", (q) => q.eq("roleId", perfilOrigem.roleId))
|
||||
.collect();
|
||||
for (const menuPerm of menuPermsClonar) {
|
||||
await ctx.db.insert("menuPermissoes", {
|
||||
roleId,
|
||||
menuPath: menuPerm.menuPath,
|
||||
podeAcessar: menuPerm.podeAcessar,
|
||||
podeConsultar: menuPerm.podeConsultar,
|
||||
podeGravar: menuPerm.podeGravar,
|
||||
});
|
||||
}
|
||||
|
||||
// Criar perfil customizado
|
||||
const perfilId = await ctx.db.insert("perfisCustomizados", {
|
||||
nome: args.novoNome,
|
||||
descricao: args.novaDescricao,
|
||||
nivel: perfilOrigem.nivel,
|
||||
clonarDeRoleId: perfilOrigem.roleId,
|
||||
criadoPorId: args.criadoPorId,
|
||||
roleId,
|
||||
criadoPor: args.criadoPorId,
|
||||
criadoEm: Date.now(),
|
||||
atualizadoEm: Date.now(),
|
||||
});
|
||||
|
||||
return resultado;
|
||||
// Log de atividade
|
||||
await registrarAtividade(
|
||||
ctx as any,
|
||||
args.criadoPorId,
|
||||
"criar",
|
||||
"perfis",
|
||||
JSON.stringify({
|
||||
perfilId,
|
||||
nome: args.novoNome,
|
||||
nivel: perfilOrigem.nivel,
|
||||
}),
|
||||
perfilId
|
||||
);
|
||||
|
||||
return { sucesso: true as const, perfilId };
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
|
||||
210
packages/backend/convex/permissoesAcoes.ts
Normal file
210
packages/backend/convex/permissoesAcoes.ts
Normal file
@@ -0,0 +1,210 @@
|
||||
import { query, mutation, internalQuery } from "./_generated/server";
|
||||
import { v } from "convex/values";
|
||||
|
||||
// Catálogo base de recursos e ações
|
||||
// Ajuste/expanda conforme os módulos disponíveis no sistema
|
||||
export const CATALOGO_RECURSOS = [
|
||||
{
|
||||
recurso: "funcionarios",
|
||||
acoes: ["dashboard", "ver", "listar", "criar", "editar", "excluir"],
|
||||
},
|
||||
{
|
||||
recurso: "simbolos",
|
||||
acoes: ["dashboard", "ver", "listar", "criar", "editar", "excluir"],
|
||||
},
|
||||
] as const;
|
||||
|
||||
export const listarRecursosEAcoes = query({
|
||||
args: {},
|
||||
returns: v.array(
|
||||
v.object({
|
||||
recurso: v.string(),
|
||||
acoes: v.array(v.string()),
|
||||
})
|
||||
),
|
||||
handler: async () => {
|
||||
return CATALOGO_RECURSOS.map((r) => ({
|
||||
recurso: r.recurso,
|
||||
acoes: [...r.acoes],
|
||||
}));
|
||||
},
|
||||
});
|
||||
|
||||
export const listarPermissoesAcoesPorRole = query({
|
||||
args: { roleId: v.id("roles") },
|
||||
returns: v.array(
|
||||
v.object({
|
||||
recurso: v.string(),
|
||||
acoes: v.array(v.string()),
|
||||
})
|
||||
),
|
||||
handler: async (ctx, args) => {
|
||||
// Buscar vínculos permissao<-role
|
||||
const rolePerms = await ctx.db
|
||||
.query("rolePermissoes")
|
||||
.withIndex("by_role", (q) => q.eq("roleId", args.roleId))
|
||||
.collect();
|
||||
|
||||
// Carregar documentos de permissões
|
||||
const actionsByResource: Record<string, Set<string>> = {};
|
||||
for (const rp of rolePerms) {
|
||||
const perm = await ctx.db.get(rp.permissaoId);
|
||||
if (!perm) continue;
|
||||
const set = (actionsByResource[perm.recurso] ||= new Set<string>());
|
||||
set.add(perm.acao);
|
||||
}
|
||||
|
||||
// Normalizar para todos os recursos do catálogo
|
||||
const result: Array<{ recurso: string; acoes: Array<string> }> = [];
|
||||
for (const item of CATALOGO_RECURSOS) {
|
||||
const granted = Array.from(
|
||||
actionsByResource[item.recurso] ?? new Set<string>()
|
||||
);
|
||||
result.push({ recurso: item.recurso, acoes: granted });
|
||||
}
|
||||
return result;
|
||||
},
|
||||
});
|
||||
|
||||
export const atualizarPermissaoAcao = mutation({
|
||||
args: {
|
||||
roleId: v.id("roles"),
|
||||
recurso: v.string(),
|
||||
acao: v.string(),
|
||||
conceder: v.boolean(),
|
||||
},
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
// Garantir documento de permissão (recurso+acao)
|
||||
let permissao = await ctx.db
|
||||
.query("permissoes")
|
||||
.withIndex("by_recurso_e_acao", (q) =>
|
||||
q.eq("recurso", args.recurso).eq("acao", args.acao)
|
||||
)
|
||||
.first();
|
||||
|
||||
if (!permissao) {
|
||||
const nome = `${args.recurso}.${args.acao}`;
|
||||
const descricao = `Permite ${args.acao} em ${args.recurso}`;
|
||||
const id = await ctx.db.insert("permissoes", {
|
||||
nome,
|
||||
descricao,
|
||||
recurso: args.recurso,
|
||||
acao: args.acao,
|
||||
});
|
||||
permissao = await ctx.db.get(id);
|
||||
}
|
||||
|
||||
if (!permissao) return null;
|
||||
|
||||
// Verificar vínculo atual
|
||||
const existente = await ctx.db
|
||||
.query("rolePermissoes")
|
||||
.withIndex("by_role", (q) => q.eq("roleId", args.roleId))
|
||||
.collect();
|
||||
|
||||
const vinculo = existente.find((rp) => rp.permissaoId === permissao!._id);
|
||||
|
||||
if (args.conceder) {
|
||||
if (!vinculo) {
|
||||
await ctx.db.insert("rolePermissoes", {
|
||||
roleId: args.roleId,
|
||||
permissaoId: permissao._id,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (vinculo) {
|
||||
await ctx.db.delete(vinculo._id);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
});
|
||||
|
||||
export const verificarAcao = query({
|
||||
args: {
|
||||
usuarioId: v.id("usuarios"),
|
||||
recurso: v.string(),
|
||||
acao: v.string(),
|
||||
},
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
const usuario = await ctx.db.get(args.usuarioId);
|
||||
if (!usuario) throw new Error("acesso_negado");
|
||||
|
||||
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;
|
||||
|
||||
// Encontrar permissão
|
||||
const permissao = await ctx.db
|
||||
.query("permissoes")
|
||||
.withIndex("by_recurso_e_acao", (q) =>
|
||||
q.eq("recurso", args.recurso).eq("acao", args.acao)
|
||||
)
|
||||
.first();
|
||||
if (!permissao) throw new Error("acesso_negado");
|
||||
|
||||
const hasLink = await ctx.db
|
||||
.query("rolePermissoes")
|
||||
.withIndex("by_role", (q) => q.eq("roleId", usuario.roleId))
|
||||
.collect();
|
||||
const permitido = hasLink.some((rp) => rp.permissaoId === permissao!._id);
|
||||
if (!permitido) throw new Error("acesso_negado");
|
||||
return null;
|
||||
},
|
||||
});
|
||||
|
||||
export const assertPermissaoAcaoAtual = internalQuery({
|
||||
args: {
|
||||
recurso: v.string(),
|
||||
acao: v.string(),
|
||||
},
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
const identity = await ctx.auth.getUserIdentity();
|
||||
let usuarioAtual: any = null;
|
||||
|
||||
if (identity && identity.email) {
|
||||
usuarioAtual = await ctx.db
|
||||
.query("usuarios")
|
||||
.withIndex("by_email", (q) => q.eq("email", identity.email!))
|
||||
.first();
|
||||
}
|
||||
|
||||
if (!usuarioAtual) {
|
||||
const sessaoAtiva = await ctx.db
|
||||
.query("sessoes")
|
||||
.filter((q) => q.eq(q.field("ativo"), true))
|
||||
.order("desc")
|
||||
.first();
|
||||
if (sessaoAtiva) {
|
||||
usuarioAtual = await ctx.db.get(sessaoAtiva.usuarioId);
|
||||
}
|
||||
}
|
||||
|
||||
if (!usuarioAtual) throw new Error("acesso_negado");
|
||||
|
||||
const role: any = await ctx.db.get(usuarioAtual.roleId as any);
|
||||
if (!role) throw new Error("acesso_negado");
|
||||
if ((role as any).nivel <= 1) return null;
|
||||
|
||||
const permissao = await ctx.db
|
||||
.query("permissoes")
|
||||
.withIndex("by_recurso_e_acao", (q) =>
|
||||
q.eq("recurso", args.recurso).eq("acao", args.acao)
|
||||
)
|
||||
.first();
|
||||
if (!permissao) throw new Error("acesso_negado");
|
||||
|
||||
const links = await ctx.db
|
||||
.query("rolePermissoes")
|
||||
.withIndex("by_role", (q) => q.eq("roleId", (role as any)._id as any))
|
||||
.collect();
|
||||
const ok = links.some((rp) => rp.permissaoId === permissao!._id);
|
||||
if (!ok) throw new Error("acesso_negado");
|
||||
return null;
|
||||
},
|
||||
});
|
||||
@@ -14,7 +14,7 @@ export const listar = query({
|
||||
descricao: v.string(),
|
||||
nivel: v.number(),
|
||||
setor: v.optional(v.string()),
|
||||
customizado: v.boolean(),
|
||||
customizado: v.optional(v.boolean()),
|
||||
editavel: v.optional(v.boolean()),
|
||||
criadoPor: v.optional(v.id("usuarios")),
|
||||
})
|
||||
@@ -45,4 +45,3 @@ export const buscarPorId = query({
|
||||
return await ctx.db.get(args.roleId);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { defineSchema, defineTable } from "convex/server";
|
||||
import { Infer, v } from "convex/values";
|
||||
import { tables } from "./betterAuth/schema";
|
||||
import { cidrv4 } from "better-auth";
|
||||
|
||||
export const simboloTipo = v.union(
|
||||
v.literal("cargo_comissionado"),
|
||||
@@ -245,6 +243,7 @@ export default defineSchema({
|
||||
acao: v.string(), // "criar", "ler", "editar", "excluir"
|
||||
})
|
||||
.index("by_recurso", ["recurso"])
|
||||
.index("by_recurso_e_acao", ["recurso", "acao"])
|
||||
.index("by_nome", ["nome"]),
|
||||
|
||||
rolePermissoes: defineTable({
|
||||
|
||||
@@ -370,6 +370,55 @@ export const seedDatabase = internalMutation({
|
||||
});
|
||||
console.log(" ✅ Admin criado (matrícula: 2000, senha: Admin@123)");
|
||||
|
||||
// 2.1 Criar catálogo de permissões por ação e conceder a Admin/TI
|
||||
console.log("🔐 Criando permissões por ação...");
|
||||
const CATALOGO_RECURSOS = [
|
||||
{ recurso: "dashboard", acoes: ["ver"] },
|
||||
{
|
||||
recurso: "funcionarios",
|
||||
acoes: ["ver", "listar", "criar", "editar", "excluir"],
|
||||
},
|
||||
{
|
||||
recurso: "simbolos",
|
||||
acoes: ["ver", "listar", "criar", "editar", "excluir"],
|
||||
},
|
||||
{
|
||||
recurso: "usuarios",
|
||||
acoes: ["ver", "listar", "criar", "editar", "excluir"],
|
||||
},
|
||||
{
|
||||
recurso: "perfis",
|
||||
acoes: ["ver", "listar", "criar", "editar", "excluir"],
|
||||
},
|
||||
] as const;
|
||||
|
||||
const permissaoKeyToId = new Map<string, string>();
|
||||
for (const item of CATALOGO_RECURSOS) {
|
||||
for (const acao of item.acoes) {
|
||||
const nome = `${item.recurso}.${acao}`;
|
||||
const id = await ctx.db.insert("permissoes", {
|
||||
nome,
|
||||
descricao: `Permite ${acao} em ${item.recurso}`,
|
||||
recurso: item.recurso,
|
||||
acao,
|
||||
});
|
||||
permissaoKeyToId.set(nome, id);
|
||||
}
|
||||
}
|
||||
console.log(` ✅ ${permissaoKeyToId.size} permissões criadas`);
|
||||
|
||||
// Conceder todas permissões a Admin e TI
|
||||
const rolesParaConceder = [roleAdmin, roleTIUsuario, roleTIMaster];
|
||||
for (const roleId of rolesParaConceder) {
|
||||
for (const [, permId] of permissaoKeyToId) {
|
||||
await ctx.db.insert("rolePermissoes", {
|
||||
roleId: roleId as any,
|
||||
permissaoId: permId as any,
|
||||
});
|
||||
}
|
||||
}
|
||||
console.log(" ✅ Todas as permissões concedidas a Admin e TI");
|
||||
|
||||
// 3. Inserir símbolos
|
||||
console.log("📝 Inserindo símbolos...");
|
||||
const simbolosMap = new Map<string, string>();
|
||||
@@ -393,7 +442,9 @@ export const seedDatabase = internalMutation({
|
||||
for (const funcionario of funcionariosData) {
|
||||
const simboloId = simbolosMap.get(funcionario.simboloNome);
|
||||
if (!simboloId) {
|
||||
console.error(` ❌ Símbolo não encontrado: ${funcionario.simboloNome}`);
|
||||
console.error(
|
||||
` ❌ Símbolo não encontrado: ${funcionario.simboloNome}`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -436,7 +487,9 @@ export const seedDatabase = internalMutation({
|
||||
criadoEm: Date.now(),
|
||||
atualizadoEm: Date.now(),
|
||||
});
|
||||
console.log(` ✅ Usuário criado: ${funcionario.nome} (senha: Mudar@123)`);
|
||||
console.log(
|
||||
` ✅ Usuário criado: ${funcionario.nome} (senha: Mudar@123)`
|
||||
);
|
||||
}
|
||||
|
||||
// 6. Inserir solicitações de acesso
|
||||
@@ -462,28 +515,32 @@ export const seedDatabase = internalMutation({
|
||||
codigo: "USUARIO_BLOQUEADO",
|
||||
nome: "Usuário Bloqueado",
|
||||
titulo: "Sua conta foi bloqueada",
|
||||
corpo: "Sua conta no SGSE foi bloqueada.\\n\\nMotivo: {{motivo}}\\n\\nPara mais informações, entre em contato com a TI.",
|
||||
corpo:
|
||||
"Sua conta no SGSE foi bloqueada.\\n\\nMotivo: {{motivo}}\\n\\nPara mais informações, entre em contato com a TI.",
|
||||
variaveis: ["motivo"],
|
||||
},
|
||||
{
|
||||
codigo: "USUARIO_DESBLOQUEADO",
|
||||
nome: "Usuário Desbloqueado",
|
||||
titulo: "Sua conta foi desbloqueada",
|
||||
corpo: "Sua conta no SGSE foi desbloqueada e você já pode acessar o sistema normalmente.",
|
||||
corpo:
|
||||
"Sua conta no SGSE foi desbloqueada e você já pode acessar o sistema normalmente.",
|
||||
variaveis: [],
|
||||
},
|
||||
{
|
||||
codigo: "SENHA_RESETADA",
|
||||
nome: "Senha Resetada",
|
||||
titulo: "Sua senha foi resetada",
|
||||
corpo: "Sua senha foi resetada pela equipe de TI.\\n\\nNova senha temporária: {{senha}}\\n\\nPor favor, altere sua senha no próximo login.",
|
||||
corpo:
|
||||
"Sua senha foi resetada pela equipe de TI.\\n\\nNova senha temporária: {{senha}}\\n\\nPor favor, altere sua senha no próximo login.",
|
||||
variaveis: ["senha"],
|
||||
},
|
||||
{
|
||||
codigo: "PERMISSAO_ALTERADA",
|
||||
nome: "Permissão Alterada",
|
||||
titulo: "Suas permissões foram atualizadas",
|
||||
corpo: "Suas permissões de acesso ao sistema foram atualizadas.\\n\\nPara verificar suas novas permissões, acesse o menu de perfil.",
|
||||
corpo:
|
||||
"Suas permissões de acesso ao sistema foram atualizadas.\\n\\nPara verificar suas novas permissões, acesse o menu de perfil.",
|
||||
variaveis: [],
|
||||
},
|
||||
{
|
||||
@@ -497,7 +554,8 @@ export const seedDatabase = internalMutation({
|
||||
codigo: "BEM_VINDO",
|
||||
nome: "Boas-vindas",
|
||||
titulo: "Bem-vindo ao SGSE",
|
||||
corpo: "Olá {{nome}},\\n\\nSeja bem-vindo ao Sistema de Gestão da Secretaria de Esportes!\\n\\nSuas credenciais de acesso:\\nMatrícula: {{matricula}}\\nSenha temporária: {{senha}}\\n\\nPor favor, altere sua senha no primeiro acesso.\\n\\nEquipe de TI",
|
||||
corpo:
|
||||
"Olá {{nome}},\\n\\nSeja bem-vindo ao Sistema de Gestão da Secretaria de Esportes!\\n\\nSuas credenciais de acesso:\\nMatrícula: {{matricula}}\\nSenha temporária: {{senha}}\\n\\nPor favor, altere sua senha no primeiro acesso.\\n\\nEquipe de TI",
|
||||
variaveis: ["nome", "matricula", "senha"],
|
||||
},
|
||||
];
|
||||
@@ -584,11 +642,15 @@ export const clearDatabase = internalMutation({
|
||||
console.log(` ✅ ${menuPermissoes.length} menu-permissões removidas`);
|
||||
|
||||
// Limpar menu-permissões personalizadas
|
||||
const menuPermissoesPersonalizadas = await ctx.db.query("menuPermissoesPersonalizadas").collect();
|
||||
const menuPermissoesPersonalizadas = await ctx.db
|
||||
.query("menuPermissoesPersonalizadas")
|
||||
.collect();
|
||||
for (const mpp of menuPermissoesPersonalizadas) {
|
||||
await ctx.db.delete(mpp._id);
|
||||
}
|
||||
console.log(` ✅ ${menuPermissoesPersonalizadas.length} menu-permissões personalizadas removidas`);
|
||||
console.log(
|
||||
` ✅ ${menuPermissoesPersonalizadas.length} menu-permissões personalizadas removidas`
|
||||
);
|
||||
|
||||
// Limpar role-permissões
|
||||
const rolePermissoes = await ctx.db.query("rolePermissoes").collect();
|
||||
@@ -615,4 +677,3 @@ export const clearDatabase = internalMutation({
|
||||
return null;
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { mutation, query } from "./_generated/server";
|
||||
import { hashPassword, generateToken } from "./auth/utils";
|
||||
import { registrarAtividade } from "./logsAtividades";
|
||||
import { Id } from "./_generated/dataModel";
|
||||
import { api } from "./_generated/api";
|
||||
|
||||
/**
|
||||
* Criar novo usuário (apenas TI)
|
||||
@@ -106,9 +107,7 @@ export const listar = query({
|
||||
|
||||
// Filtrar por matrícula
|
||||
if (args.matricula) {
|
||||
usuarios = usuarios.filter((u) =>
|
||||
u.matricula.includes(args.matricula!)
|
||||
);
|
||||
usuarios = usuarios.filter((u) => u.matricula.includes(args.matricula!));
|
||||
}
|
||||
|
||||
// Filtrar por ativo
|
||||
@@ -386,13 +385,16 @@ export const atualizarPerfil = mutation({
|
||||
if (args.avatar !== undefined) updates.avatar = args.avatar;
|
||||
if (args.fotoPerfil !== undefined) updates.fotoPerfil = args.fotoPerfil;
|
||||
if (args.setor !== undefined) updates.setor = args.setor;
|
||||
if (args.statusMensagem !== undefined) updates.statusMensagem = args.statusMensagem;
|
||||
if (args.statusMensagem !== undefined)
|
||||
updates.statusMensagem = args.statusMensagem;
|
||||
if (args.statusPresenca !== undefined) {
|
||||
updates.statusPresenca = args.statusPresenca;
|
||||
updates.ultimaAtividade = Date.now();
|
||||
}
|
||||
if (args.notificacoesAtivadas !== undefined) updates.notificacoesAtivadas = args.notificacoesAtivadas;
|
||||
if (args.somNotificacao !== undefined) updates.somNotificacao = args.somNotificacao;
|
||||
if (args.notificacoesAtivadas !== undefined)
|
||||
updates.notificacoesAtivadas = args.notificacoesAtivadas;
|
||||
if (args.somNotificacao !== undefined)
|
||||
updates.somNotificacao = args.somNotificacao;
|
||||
|
||||
await ctx.db.patch(usuarioAtual._id, updates);
|
||||
|
||||
@@ -405,6 +407,31 @@ export const atualizarPerfil = mutation({
|
||||
*/
|
||||
export const obterPerfil = query({
|
||||
args: {},
|
||||
returns: v.union(
|
||||
v.object({
|
||||
_id: v.id("usuarios"),
|
||||
nome: v.string(),
|
||||
email: v.string(),
|
||||
matricula: v.string(),
|
||||
avatar: v.optional(v.string()),
|
||||
fotoPerfil: v.optional(v.id("_storage")),
|
||||
fotoPerfilUrl: v.union(v.string(), v.null()),
|
||||
setor: v.optional(v.string()),
|
||||
statusMensagem: v.optional(v.string()),
|
||||
statusPresenca: v.optional(
|
||||
v.union(
|
||||
v.literal("online"),
|
||||
v.literal("offline"),
|
||||
v.literal("ausente"),
|
||||
v.literal("externo"),
|
||||
v.literal("em_reuniao")
|
||||
)
|
||||
),
|
||||
notificacoesAtivadas: v.boolean(),
|
||||
somNotificacao: v.boolean(),
|
||||
}),
|
||||
v.null()
|
||||
),
|
||||
handler: async (ctx) => {
|
||||
console.log("=== DEBUG obterPerfil ===");
|
||||
|
||||
@@ -422,7 +449,10 @@ export const obterPerfil = query({
|
||||
.withIndex("by_email", (q) => q.eq("email", identity.email!))
|
||||
.first();
|
||||
|
||||
console.log("Usuário encontrado por email:", usuarioAtual ? "SIM" : "NÃO");
|
||||
console.log(
|
||||
"Usuário encontrado por email:",
|
||||
usuarioAtual ? "SIM" : "NÃO"
|
||||
);
|
||||
}
|
||||
|
||||
// SE NÃO ENCONTROU, BUSCAR POR SESSÃO ATIVA (Sistema customizado)
|
||||
@@ -438,7 +468,10 @@ export const obterPerfil = query({
|
||||
|
||||
if (sessaoAtiva) {
|
||||
usuarioAtual = await ctx.db.get(sessaoAtiva.usuarioId);
|
||||
console.log("Usuário da sessão encontrado:", usuarioAtual ? "SIM" : "NÃO");
|
||||
console.log(
|
||||
"Usuário da sessão encontrado:",
|
||||
usuarioAtual ? "SIM" : "NÃO"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -447,7 +480,10 @@ export const obterPerfil = query({
|
||||
// Listar todos os usuários para debug
|
||||
const todosUsuarios = await ctx.db.query("usuarios").collect();
|
||||
console.log("Total de usuários no banco:", todosUsuarios.length);
|
||||
console.log("Emails cadastrados:", todosUsuarios.map(u => u.email));
|
||||
console.log(
|
||||
"Emails cadastrados:",
|
||||
todosUsuarios.map((u) => u.email)
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -542,6 +578,7 @@ export const listarParaChat = query({
|
||||
*/
|
||||
export const uploadFotoPerfil = mutation({
|
||||
args: {},
|
||||
returns: v.string(),
|
||||
handler: async (ctx) => {
|
||||
// TENTAR BETTER AUTH PRIMEIRO
|
||||
const identity = await ctx.auth.getUserIdentity();
|
||||
@@ -743,7 +780,8 @@ export const resetarSenhaUsuario = mutation({
|
||||
|
||||
// Helper para gerar senha temporária
|
||||
function gerarSenhaTemporaria(): string {
|
||||
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@#$%";
|
||||
const chars =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@#$%";
|
||||
let senha = "";
|
||||
for (let i = 0; i < 12; i++) {
|
||||
senha += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
@@ -811,6 +849,116 @@ export const editarUsuario = mutation({
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Criar/Promover usuário Admin Master (TI_MASTER - nível 0)
|
||||
*/
|
||||
export const criarAdminMaster = mutation({
|
||||
args: {
|
||||
matricula: v.string(),
|
||||
nome: v.string(),
|
||||
email: v.string(),
|
||||
senha: v.optional(v.string()),
|
||||
},
|
||||
returns: v.union(
|
||||
v.object({
|
||||
sucesso: v.literal(true),
|
||||
usuarioId: v.id("usuarios"),
|
||||
senhaTemporaria: v.string(),
|
||||
}),
|
||||
v.object({ sucesso: v.literal(false), erro: v.string() })
|
||||
),
|
||||
handler: async (ctx, args) => {
|
||||
// Garantir que a role TI_MASTER exista (nível 0)
|
||||
let roleTIMaster = await ctx.db
|
||||
.query("roles")
|
||||
.withIndex("by_nome", (q) => q.eq("nome", "ti_master"))
|
||||
.first();
|
||||
|
||||
if (!roleTIMaster) {
|
||||
const roleId = await ctx.db.insert("roles", {
|
||||
nome: "ti_master",
|
||||
descricao: "TI Master",
|
||||
nivel: 0,
|
||||
setor: "ti",
|
||||
customizado: false,
|
||||
editavel: false,
|
||||
});
|
||||
roleTIMaster = await ctx.db.get(roleId);
|
||||
}
|
||||
|
||||
if (!roleTIMaster) {
|
||||
return {
|
||||
sucesso: false as const,
|
||||
erro: "Falha ao garantir role TI Master",
|
||||
};
|
||||
}
|
||||
|
||||
// Se já existir usuário por matrícula, promove/atualiza
|
||||
const existentePorMatricula = await ctx.db
|
||||
.query("usuarios")
|
||||
.withIndex("by_matricula", (q) => q.eq("matricula", args.matricula))
|
||||
.first();
|
||||
|
||||
const senhaTemporaria = args.senha || gerarSenhaTemporaria();
|
||||
const senhaHash = await hashPassword(senhaTemporaria);
|
||||
|
||||
if (existentePorMatricula) {
|
||||
await ctx.db.patch(existentePorMatricula._id, {
|
||||
nome: args.nome,
|
||||
email: args.email,
|
||||
senhaHash,
|
||||
roleId: roleTIMaster._id,
|
||||
ativo: true,
|
||||
primeiroAcesso: true,
|
||||
atualizadoEm: Date.now(),
|
||||
});
|
||||
return {
|
||||
sucesso: true as const,
|
||||
usuarioId: existentePorMatricula._id,
|
||||
senhaTemporaria,
|
||||
};
|
||||
}
|
||||
|
||||
// Verificar se email já existe
|
||||
const existentePorEmail = await ctx.db
|
||||
.query("usuarios")
|
||||
.withIndex("by_email", (q) => q.eq("email", args.email))
|
||||
.first();
|
||||
if (existentePorEmail) {
|
||||
// Promove usuário existente por email
|
||||
await ctx.db.patch(existentePorEmail._id, {
|
||||
matricula: args.matricula,
|
||||
nome: args.nome,
|
||||
senhaHash,
|
||||
roleId: roleTIMaster._id,
|
||||
ativo: true,
|
||||
primeiroAcesso: true,
|
||||
atualizadoEm: Date.now(),
|
||||
});
|
||||
return {
|
||||
sucesso: true as const,
|
||||
usuarioId: existentePorEmail._id,
|
||||
senhaTemporaria,
|
||||
};
|
||||
}
|
||||
|
||||
// Criar novo usuário TI Master
|
||||
const usuarioId = await ctx.db.insert("usuarios", {
|
||||
matricula: args.matricula,
|
||||
senhaHash,
|
||||
nome: args.nome,
|
||||
email: args.email,
|
||||
roleId: roleTIMaster._id,
|
||||
ativo: true,
|
||||
primeiroAcesso: true,
|
||||
criadoEm: Date.now(),
|
||||
atualizadoEm: Date.now(),
|
||||
});
|
||||
|
||||
return { sucesso: true as const, usuarioId, senhaTemporaria };
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Desativar usuário logicamente (soft delete - apenas TI_MASTER)
|
||||
*/
|
||||
@@ -875,7 +1023,11 @@ export const criarUsuarioCompleto = mutation({
|
||||
enviarEmailBoasVindas: v.optional(v.boolean()),
|
||||
},
|
||||
returns: v.union(
|
||||
v.object({ sucesso: v.literal(true), usuarioId: v.id("usuarios"), senhaTemporaria: v.string() }),
|
||||
v.object({
|
||||
sucesso: v.literal(true),
|
||||
usuarioId: v.id("usuarios"),
|
||||
senhaTemporaria: v.string(),
|
||||
}),
|
||||
v.object({ sucesso: v.literal(false), erro: v.string() })
|
||||
),
|
||||
handler: async (ctx, args) => {
|
||||
@@ -934,3 +1086,85 @@ export const criarUsuarioCompleto = mutation({
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Criar (ou garantir) um usuário ADMIN padrão
|
||||
*/
|
||||
export const criarAdminPadrao = mutation({
|
||||
args: {
|
||||
matricula: v.optional(v.string()),
|
||||
nome: v.optional(v.string()),
|
||||
email: v.optional(v.string()),
|
||||
senha: v.optional(v.string()),
|
||||
},
|
||||
returns: v.object({
|
||||
sucesso: v.boolean(),
|
||||
usuarioId: v.optional(v.id("usuarios")),
|
||||
}),
|
||||
handler: async (ctx, args) => {
|
||||
const matricula = args.matricula ?? "0000";
|
||||
const nome = args.nome ?? "Administrador Geral";
|
||||
const email = args.email ?? "admin@sgse.pe.gov.br";
|
||||
const senha = args.senha ?? "Admin@123";
|
||||
|
||||
// Garantir role ADMIN (nível 2)
|
||||
let roleAdmin = await ctx.db
|
||||
.query("roles")
|
||||
.withIndex("by_nome", (q) => q.eq("nome", "admin"))
|
||||
.first();
|
||||
if (!roleAdmin) {
|
||||
const roleId = await ctx.db.insert("roles", {
|
||||
nome: "admin",
|
||||
descricao: "Administrador Geral",
|
||||
nivel: 2,
|
||||
setor: "administrativo",
|
||||
customizado: false,
|
||||
editavel: true,
|
||||
});
|
||||
roleAdmin = await ctx.db.get(roleId);
|
||||
}
|
||||
|
||||
if (!roleAdmin) return { sucesso: false };
|
||||
|
||||
// Verificar se já existe por matrícula ou email
|
||||
const existentePorMatricula = await ctx.db
|
||||
.query("usuarios")
|
||||
.withIndex("by_matricula", (q) => q.eq("matricula", matricula))
|
||||
.first();
|
||||
|
||||
const existentePorEmail = await ctx.db
|
||||
.query("usuarios")
|
||||
.withIndex("by_email", (q) => q.eq("email", email))
|
||||
.first();
|
||||
|
||||
const senhaHash = await hashPassword(senha);
|
||||
|
||||
if (existentePorMatricula || existentePorEmail) {
|
||||
const alvo = existentePorMatricula ?? existentePorEmail!;
|
||||
await ctx.db.patch(alvo._id, {
|
||||
matricula,
|
||||
nome,
|
||||
email,
|
||||
senhaHash,
|
||||
roleId: roleAdmin._id,
|
||||
ativo: true,
|
||||
primeiroAcesso: false,
|
||||
atualizadoEm: Date.now(),
|
||||
});
|
||||
return { sucesso: true, usuarioId: alvo._id };
|
||||
}
|
||||
|
||||
const usuarioId = await ctx.db.insert("usuarios", {
|
||||
matricula,
|
||||
senhaHash,
|
||||
nome,
|
||||
email,
|
||||
roleId: roleAdmin._id,
|
||||
ativo: true,
|
||||
primeiroAcesso: false,
|
||||
criadoEm: Date.now(),
|
||||
atualizadoEm: Date.now(),
|
||||
});
|
||||
|
||||
return { sucesso: true, usuarioId };
|
||||
},
|
||||
});
|
||||
|
||||
@@ -13,9 +13,7 @@
|
||||
"typescript": "^5.9.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@convex-dev/better-auth": "^0.9.6",
|
||||
"@dicebear/avataaars": "^9.2.4",
|
||||
"better-auth": "1.3.27",
|
||||
"convex": "^1.28.0"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user