refactor: remove unused authentication files and dependencies; update package.json to streamline dependencies and improve project structure
This commit is contained in:
@@ -27,14 +27,11 @@
|
|||||||
"vite": "^7.1.2"
|
"vite": "^7.1.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@convex-dev/better-auth": "^0.9.6",
|
|
||||||
"@dicebear/collection": "^9.2.4",
|
"@dicebear/collection": "^9.2.4",
|
||||||
"@dicebear/core": "^9.2.4",
|
"@dicebear/core": "^9.2.4",
|
||||||
"@internationalized/date": "^3.10.0",
|
"@internationalized/date": "^3.10.0",
|
||||||
"@mmailaender/convex-better-auth-svelte": "^0.2.0",
|
|
||||||
"@sgse-app/backend": "*",
|
"@sgse-app/backend": "*",
|
||||||
"@tanstack/svelte-form": "^1.19.2",
|
"@tanstack/svelte-form": "^1.19.2",
|
||||||
"better-auth": "1.3.27",
|
|
||||||
"convex": "^1.28.0",
|
"convex": "^1.28.0",
|
||||||
"convex-svelte": "^0.0.11",
|
"convex-svelte": "^0.0.11",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
@@ -43,4 +40,4 @@
|
|||||||
"jspdf-autotable": "^5.0.2",
|
"jspdf-autotable": "^5.0.2",
|
||||||
"zod": "^4.0.17"
|
"zod": "^4.0.17"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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">
|
<script lang="ts">
|
||||||
import { page } from "$app/state";
|
import { page } from "$app/state";
|
||||||
import MenuProtection from "$lib/components/MenuProtection.svelte";
|
import ActionGuard from "$lib/components/ActionGuard.svelte";
|
||||||
|
|
||||||
const { children } = $props();
|
const { children } = $props();
|
||||||
|
|
||||||
// Mapa de rotas para verificação de permissões
|
// Resolver recurso/ação a partir da rota
|
||||||
const ROUTE_PERMISSIONS: Record<string, { path: string; requireGravar?: boolean }> = {
|
const routeAction = $derived.by(() => {
|
||||||
// Recursos Humanos
|
const p = page.url.pathname;
|
||||||
"/recursos-humanos": { path: "/recursos-humanos" },
|
if (p === "/" || p === "/solicitar-acesso") return null;
|
||||||
"/recursos-humanos/funcionarios": { path: "/recursos-humanos/funcionarios" },
|
|
||||||
"/recursos-humanos/funcionarios/cadastro": { path: "/recursos-humanos/funcionarios", requireGravar: true },
|
// Funcionários
|
||||||
"/recursos-humanos/funcionarios/excluir": { path: "/recursos-humanos/funcionarios", requireGravar: true },
|
if (p.startsWith("/recursos-humanos/funcionarios")) {
|
||||||
"/recursos-humanos/funcionarios/relatorios": { path: "/recursos-humanos/funcionarios" },
|
if (p.includes("/cadastro"))
|
||||||
"/recursos-humanos/simbolos": { path: "/recursos-humanos/simbolos" },
|
return { recurso: "funcionarios", acao: "criar" };
|
||||||
"/recursos-humanos/simbolos/cadastro": { path: "/recursos-humanos/simbolos", requireGravar: true },
|
if (p.includes("/excluir"))
|
||||||
// Outros menus
|
return { recurso: "funcionarios", acao: "excluir" };
|
||||||
"/financeiro": { path: "/financeiro" },
|
if (p.includes("/editar") || p.includes("/funcionarioId"))
|
||||||
"/controladoria": { path: "/controladoria" },
|
return { recurso: "funcionarios", acao: "editar" };
|
||||||
"/licitacoes": { path: "/licitacoes" },
|
return { recurso: "funcionarios", acao: "listar" };
|
||||||
"/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" },
|
|
||||||
};
|
|
||||||
|
|
||||||
// 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];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verificar rotas dinâmicas (com [id])
|
// Símbolos
|
||||||
if (currentPath.includes("/editar") || currentPath.includes("/funcionarioId") || currentPath.includes("/simboloId")) {
|
if (p.startsWith("/recursos-humanos/simbolos")) {
|
||||||
// Extrair o caminho base
|
if (p.includes("/cadastro"))
|
||||||
if (currentPath.includes("/funcionarios/")) {
|
return { recurso: "simbolos", acao: "criar" };
|
||||||
return { path: "/recursos-humanos/funcionarios", requireGravar: true };
|
if (p.includes("/excluir"))
|
||||||
}
|
return { recurso: "simbolos", acao: "excluir" };
|
||||||
if (currentPath.includes("/simbolos/")) {
|
if (p.includes("/editar") || p.includes("/simboloId"))
|
||||||
return { path: "/recursos-humanos/simbolos", requireGravar: true };
|
return { recurso: "simbolos", acao: "editar" };
|
||||||
}
|
return { recurso: "simbolos", acao: "listar" };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rotas públicas (Dashboard, Solicitar Acesso, etc)
|
// Outras áreas (uso genérico: ver)
|
||||||
if (currentPath === "/" || currentPath === "/solicitar-acesso") {
|
if (p.startsWith("/financeiro"))
|
||||||
return null;
|
return { recurso: "financeiro", acao: "ver" };
|
||||||
}
|
if (p.startsWith("/controladoria"))
|
||||||
|
return { recurso: "controladoria", acao: "ver" };
|
||||||
// Para qualquer outra rota dentro do dashboard, verificar o primeiro segmento
|
if (p.startsWith("/licitacoes"))
|
||||||
const segments = currentPath.split("/").filter(Boolean);
|
return { recurso: "licitacoes", acao: "ver" };
|
||||||
if (segments.length > 0) {
|
if (p.startsWith("/compras")) return { recurso: "compras", acao: "ver" };
|
||||||
const firstSegment = "/" + segments[0];
|
if (p.startsWith("/juridico")) return { recurso: "juridico", acao: "ver" };
|
||||||
if (ROUTE_PERMISSIONS[firstSegment]) {
|
if (p.startsWith("/comunicacao"))
|
||||||
return ROUTE_PERMISSIONS[firstSegment];
|
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;
|
return null;
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if getCurrentRouteConfig}
|
{#if routeAction}
|
||||||
<MenuProtection menuPath={getCurrentRouteConfig.path} requireGravar={getCurrentRouteConfig.requireGravar || false}>
|
<ActionGuard recurso={routeAction.recurso} acao={routeAction.acao}>
|
||||||
<main
|
<main id="container-central" class="w-full max-w-none px-3 lg:px-4 py-4">
|
||||||
id="container-central"
|
|
||||||
class="w-full max-w-none px-3 lg:px-4 py-4"
|
|
||||||
>
|
|
||||||
{@render children()}
|
{@render children()}
|
||||||
</main>
|
</main>
|
||||||
</MenuProtection>
|
</ActionGuard>
|
||||||
{:else}
|
{:else}
|
||||||
<main
|
<main id="container-central" class="w-full max-w-none px-3 lg:px-4 py-4">
|
||||||
id="container-central"
|
|
||||||
class="w-full max-w-none px-3 lg:px-4 py-4"
|
|
||||||
>
|
|
||||||
{@render children()}
|
{@render children()}
|
||||||
</main>
|
</main>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -4,16 +4,46 @@
|
|||||||
import ProtectedRoute from "$lib/components/ProtectedRoute.svelte";
|
import ProtectedRoute from "$lib/components/ProtectedRoute.svelte";
|
||||||
import { goto } from "$app/navigation";
|
import { goto } from "$app/navigation";
|
||||||
import type { Id } from "@sgse-app/backend/convex/_generated/dataModel";
|
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();
|
const client = useConvexClient();
|
||||||
|
|
||||||
// Buscar matriz de permissões
|
// Carregar lista de roles e catálogo de recursos/ações
|
||||||
const matrizQuery = useQuery(api.menuPermissoes.obterMatrizPermissoes, {});
|
const rolesQuery = useQuery(api.roles.listar, {});
|
||||||
|
const catalogoQuery = useQuery(api.permissoesAcoes.listarRecursosEAcoes, {});
|
||||||
|
|
||||||
let salvando = $state(false);
|
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 busca = $state("");
|
||||||
let filtroRole = $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) {
|
function mostrarMensagem(tipo: "success" | "error", texto: string) {
|
||||||
mensagem = { tipo, texto };
|
mensagem = { tipo, texto };
|
||||||
@@ -22,89 +52,50 @@
|
|||||||
}, 3000);
|
}, 3000);
|
||||||
}
|
}
|
||||||
|
|
||||||
const dadosFiltrados = $derived.by(() => {
|
const rolesFiltradas = $derived.by(() => {
|
||||||
if (!matrizQuery.data) return [];
|
if (!rolesQuery.data) return [];
|
||||||
|
let rs: Array<RoleRow> = rolesQuery.data as Array<RoleRow>;
|
||||||
let resultado = matrizQuery.data;
|
if (filtroRole)
|
||||||
|
rs = rs.filter((r: RoleRow) => r._id === (filtroRole as any));
|
||||||
// Filtrar por role
|
|
||||||
if (filtroRole) {
|
|
||||||
resultado = resultado.filter(r => r.role._id === filtroRole);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filtrar por busca
|
|
||||||
if (busca.trim()) {
|
if (busca.trim()) {
|
||||||
const buscaLower = busca.toLowerCase();
|
const b = busca.toLowerCase();
|
||||||
resultado = resultado.map(roleData => ({
|
rs = rs.filter(
|
||||||
...roleData,
|
(r: RoleRow) =>
|
||||||
permissoes: roleData.permissoes.filter(p =>
|
r.descricao.toLowerCase().includes(b) ||
|
||||||
p.menuNome.toLowerCase().includes(buscaLower) ||
|
r.nome.toLowerCase().includes(b)
|
||||||
p.menuPath.toLowerCase().includes(buscaLower)
|
);
|
||||||
)
|
|
||||||
})).filter(roleData => roleData.permissoes.length > 0);
|
|
||||||
}
|
}
|
||||||
|
return rs;
|
||||||
return resultado;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
async function atualizarPermissao(
|
async function toggleAcao(
|
||||||
roleId: Id<"roles">,
|
roleId: Id<"roles">,
|
||||||
menuPath: string,
|
recurso: string,
|
||||||
campo: "podeAcessar" | "podeConsultar" | "podeGravar",
|
acao: string,
|
||||||
valor: boolean
|
conceder: boolean
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
salvando = true;
|
salvando = true;
|
||||||
|
await client.mutation(api.permissoesAcoes.atualizarPermissaoAcao, {
|
||||||
// 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, {
|
|
||||||
roleId,
|
roleId,
|
||||||
menuPath,
|
recurso,
|
||||||
podeAcessar,
|
acao,
|
||||||
podeConsultar,
|
conceder,
|
||||||
podeGravar,
|
|
||||||
});
|
});
|
||||||
|
// 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!");
|
mostrarMensagem("success", "Permissão atualizada com sucesso!");
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
mostrarMensagem("error", e.message || "Erro ao atualizar permissão");
|
mostrarMensagem("error", e.message || "Erro ao atualizar permissão");
|
||||||
@@ -113,16 +104,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function inicializarPermissoes(roleId: Id<"roles">) {
|
function isConcedida(roleId: Id<"roles">, recurso: string, acao: string) {
|
||||||
try {
|
const dados = permissoesPorRole[roleId];
|
||||||
salvando = true;
|
const entry = dados?.find((e) => e.recurso === recurso);
|
||||||
await client.mutation(api.menuPermissoes.inicializarPermissoesRole, { roleId });
|
return entry ? entry.acoes.includes(acao) : false;
|
||||||
mostrarMensagem("success", "Permissões inicializadas!");
|
|
||||||
} catch (e: any) {
|
|
||||||
mostrarMensagem("error", e.message || "Erro ao inicializar permissões");
|
|
||||||
} finally {
|
|
||||||
salvando = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -132,8 +117,19 @@
|
|||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
<a href="/" class="text-primary hover:text-primary-focus">
|
<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">
|
<svg
|
||||||
<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" />
|
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>
|
</svg>
|
||||||
Dashboard
|
Dashboard
|
||||||
</a>
|
</a>
|
||||||
@@ -149,17 +145,43 @@
|
|||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<div class="flex items-center gap-3 mb-2">
|
<div class="flex items-center gap-3 mb-2">
|
||||||
<div class="p-3 bg-primary/10 rounded-xl">
|
<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">
|
<svg
|
||||||
<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" />
|
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>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<h1 class="text-3xl font-bold text-base-content">Gerenciar Permissões de Acesso</h1>
|
<h1 class="text-3xl font-bold text-base-content">
|
||||||
<p class="text-base-content/60 mt-1">Configure as permissões de acesso aos menus do sistema por função</p>
|
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>
|
</div>
|
||||||
<button class="btn btn-ghost gap-2" onclick={() => goto("/ti")}>
|
<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">
|
<svg
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
|
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>
|
</svg>
|
||||||
Voltar
|
Voltar
|
||||||
</button>
|
</button>
|
||||||
@@ -168,14 +190,38 @@
|
|||||||
|
|
||||||
<!-- Alertas -->
|
<!-- Alertas -->
|
||||||
{#if mensagem}
|
{#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"}
|
{#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">
|
<svg
|
||||||
<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" />
|
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>
|
</svg>
|
||||||
{:else}
|
{: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">
|
<svg
|
||||||
<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" />
|
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>
|
</svg>
|
||||||
{/if}
|
{/if}
|
||||||
<span class="font-semibold">{mensagem.texto}</span>
|
<span class="font-semibold">{mensagem.texto}</span>
|
||||||
@@ -189,13 +235,13 @@
|
|||||||
<!-- Busca por menu -->
|
<!-- Busca por menu -->
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label" for="busca">
|
<label class="label" for="busca">
|
||||||
<span class="label-text font-semibold">Buscar Menu</span>
|
<span class="label-text font-semibold">Buscar Perfil</span>
|
||||||
</label>
|
</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<input
|
<input
|
||||||
id="busca"
|
id="busca"
|
||||||
type="text"
|
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"
|
class="input input-bordered w-full pr-10"
|
||||||
bind:value={busca}
|
bind:value={busca}
|
||||||
/>
|
/>
|
||||||
@@ -227,10 +273,10 @@
|
|||||||
bind:value={filtroRole}
|
bind:value={filtroRole}
|
||||||
>
|
>
|
||||||
<option value="">Todos os perfis</option>
|
<option value="">Todos os perfis</option>
|
||||||
{#if matrizQuery.data}
|
{#if rolesQuery.data}
|
||||||
{#each matrizQuery.data as roleData}
|
{#each rolesQuery.data as roleRow}
|
||||||
<option value={roleData.role._id}>
|
<option value={roleRow._id}>
|
||||||
{roleData.role.descricao} ({roleData.role.nome})
|
{roleRow.descricao} ({roleRow.nome})
|
||||||
</option>
|
</option>
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
@@ -272,8 +318,18 @@
|
|||||||
|
|
||||||
<!-- Informações sobre o sistema de permissões -->
|
<!-- Informações sobre o sistema de permissões -->
|
||||||
<div class="alert alert-info mb-6 shadow-lg">
|
<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">
|
<svg
|
||||||
<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" />
|
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>
|
</svg>
|
||||||
<div>
|
<div>
|
||||||
<h3 class="font-bold text-lg">Como funciona o sistema de permissões:</h3>
|
<h3 class="font-bold text-lg">Como funciona o sistema de permissões:</h3>
|
||||||
@@ -281,9 +337,13 @@
|
|||||||
<div>
|
<div>
|
||||||
<h4 class="font-semibold text-sm">Tipos de Permissão:</h4>
|
<h4 class="font-semibold text-sm">Tipos de Permissão:</h4>
|
||||||
<ul class="text-sm mt-1 space-y-1">
|
<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>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>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -291,27 +351,39 @@
|
|||||||
<ul class="text-sm mt-1 space-y-1">
|
<ul class="text-sm mt-1 space-y-1">
|
||||||
<li>• <strong>Admin e TI:</strong> Acesso total automático</li>
|
<li>• <strong>Admin e TI:</strong> Acesso total automático</li>
|
||||||
<li>• <strong>Dashboard:</strong> Público para todos</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>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Matriz de Permissões -->
|
<!-- Matriz de Permissões por Ação -->
|
||||||
{#if matrizQuery.isLoading}
|
{#if rolesQuery.isLoading || catalogoQuery.isLoading}
|
||||||
<div class="flex justify-center items-center py-12">
|
<div class="flex justify-center items-center py-12">
|
||||||
<span class="loading loading-spinner loading-lg text-primary"></span>
|
<span class="loading loading-spinner loading-lg text-primary"></span>
|
||||||
</div>
|
</div>
|
||||||
{:else if matrizQuery.error}
|
{:else if rolesQuery.error}
|
||||||
<div class="alert alert-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">
|
<svg
|
||||||
<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" />
|
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>
|
</svg>
|
||||||
<span>Erro ao carregar permissões: {matrizQuery.error.message}</span>
|
<span>Erro ao carregar perfis: {rolesQuery.error.message}</span>
|
||||||
</div>
|
</div>
|
||||||
{:else if matrizQuery.data}
|
{:else if rolesQuery.data && catalogoQuery.data}
|
||||||
{#if dadosFiltrados.length === 0}
|
{#if rolesFiltradas.length === 0}
|
||||||
<div class="card bg-base-100 shadow-xl">
|
<div class="card bg-base-100 shadow-xl">
|
||||||
<div class="card-body items-center text-center">
|
<div class="card-body items-center text-center">
|
||||||
<svg
|
<svg
|
||||||
@@ -330,7 +402,9 @@
|
|||||||
</svg>
|
</svg>
|
||||||
<h3 class="text-xl font-bold mt-4">Nenhum resultado encontrado</h3>
|
<h3 class="text-xl font-bold mt-4">Nenhum resultado encontrado</h3>
|
||||||
<p class="text-base-content/60">
|
<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>
|
</p>
|
||||||
<button
|
<button
|
||||||
class="btn btn-primary btn-sm mt-4"
|
class="btn btn-primary btn-sm mt-4"
|
||||||
@@ -344,159 +418,119 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#each dadosFiltrados as roleData}
|
{#each rolesFiltradas as roleRow}
|
||||||
<div class="card bg-base-100 shadow-xl mb-6">
|
<div class="card bg-base-100 shadow-xl mb-6">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="flex items-center justify-between mb-4 flex-wrap gap-4">
|
<div class="flex items-center justify-between mb-4 flex-wrap gap-4">
|
||||||
<div class="flex-1 min-w-[200px]">
|
<div class="flex-1 min-w-[200px]">
|
||||||
<div class="flex items-center gap-3 mb-2">
|
<div class="flex items-center gap-3 mb-2">
|
||||||
<h2 class="card-title text-2xl">{roleData.role.descricao}</h2>
|
<h2 class="card-title text-2xl">{roleRow.descricao}</h2>
|
||||||
<div class="badge badge-lg badge-primary">Nível {roleData.role.nivel}</div>
|
<div class="badge badge-lg badge-primary">
|
||||||
{#if roleData.role.nivel <= 1}
|
Nível {roleRow.nivel}
|
||||||
|
</div>
|
||||||
|
{#if roleRow.nivel <= 1}
|
||||||
<div class="badge badge-lg badge-success gap-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">
|
<svg
|
||||||
<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" />
|
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>
|
</svg>
|
||||||
Acesso Total
|
Acesso Total
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<p class="text-sm text-base-content/60">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if roleData.role.nivel > 1}
|
{#if roleRow.nivel > 1}
|
||||||
<button
|
<button
|
||||||
class="btn btn-sm btn-outline btn-primary"
|
class="btn btn-sm btn-outline"
|
||||||
onclick={() => inicializarPermissoes(roleData.role._id)}
|
onclick={async () => {
|
||||||
disabled={salvando}
|
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">
|
{expandido[roleRow._id] ? "Recolher" : "Expandir"}
|
||||||
<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
|
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if roleData.role.nivel <= 1}
|
{#if roleRow.nivel <= 1}
|
||||||
<div class="alert alert-success shadow-md">
|
<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">
|
<svg
|
||||||
<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" />
|
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>
|
</svg>
|
||||||
<div>
|
<div>
|
||||||
<h3 class="font-bold">Perfil Administrativo</h3>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else if expandido[roleRow._id]}
|
||||||
<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>
|
|
||||||
|
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto">
|
||||||
<table class="table table-zebra table-sm">
|
<table class="table table-zebra table-sm">
|
||||||
<thead class="bg-base-200">
|
<thead class="bg-base-200">
|
||||||
<tr>
|
<tr>
|
||||||
<th class="w-1/3">Menu</th>
|
<th class="w-1/3">Recurso</th>
|
||||||
<th class="text-center">
|
<th class="text-center">Ver</th>
|
||||||
<div class="flex items-center justify-center gap-1">
|
<th class="text-center">Listar</th>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<th class="text-center">Criar</th>
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
<th class="text-center">Editar</th>
|
||||||
<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" />
|
<th class="text-center">Excluir</th>
|
||||||
</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>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{#each roleData.permissoes as permissao}
|
{#each catalogoQuery.data as item}
|
||||||
<tr class="hover">
|
<tr class="hover">
|
||||||
<td>
|
<td>
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<span class="font-semibold">{permissao.menuNome}</span>
|
<span class="font-semibold">{item.recurso}</span>
|
||||||
<span class="text-xs text-base-content/60">{permissao.menuPath}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-center">
|
{#each ["ver", "listar", "criar", "editar", "excluir"] as ac}
|
||||||
<input
|
<td class="text-center">
|
||||||
type="checkbox"
|
<input
|
||||||
class="checkbox checkbox-primary"
|
type="checkbox"
|
||||||
checked={permissao.podeAcessar}
|
class="checkbox checkbox-primary"
|
||||||
disabled={salvando}
|
checked={isConcedida(roleRow._id, item.recurso, ac)}
|
||||||
onchange={(e) =>
|
disabled={salvando}
|
||||||
atualizarPermissao(
|
onchange={(e) =>
|
||||||
roleData.role._id,
|
toggleAcao(
|
||||||
permissao.menuPath,
|
roleRow._id,
|
||||||
"podeAcessar",
|
item.recurso,
|
||||||
e.currentTarget.checked
|
ac,
|
||||||
)}
|
e.currentTarget.checked
|
||||||
/>
|
)}
|
||||||
</td>
|
/>
|
||||||
<td class="text-center">
|
</td>
|
||||||
<input
|
{/each}
|
||||||
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>
|
|
||||||
</tr>
|
</tr>
|
||||||
{/each}
|
{/each}
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -508,4 +542,3 @@
|
|||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
|
|
||||||
|
|||||||
@@ -1,123 +1,6 @@
|
|||||||
<script lang="ts">
|
<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 ProtectedRoute from "$lib/components/ProtectedRoute.svelte";
|
||||||
import { goto } from "$app/navigation";
|
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>
|
</script>
|
||||||
|
|
||||||
<ProtectedRoute allowedRoles={["admin", "ti"]} maxLevel={1}>
|
<ProtectedRoute allowedRoles={["admin", "ti"]} maxLevel={1}>
|
||||||
@@ -126,8 +9,19 @@
|
|||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
<a href="/" class="text-primary hover:text-primary-focus">
|
<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">
|
<svg
|
||||||
<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" />
|
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>
|
</svg>
|
||||||
Dashboard
|
Dashboard
|
||||||
</a>
|
</a>
|
||||||
@@ -143,241 +37,71 @@
|
|||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<div class="flex items-center gap-3 mb-2">
|
<div class="flex items-center gap-3 mb-2">
|
||||||
<div class="p-3 bg-info/10 rounded-xl">
|
<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">
|
<svg
|
||||||
<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" />
|
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>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<h1 class="text-3xl font-bold text-base-content">Personalizar Permissões por Matrícula</h1>
|
<h1 class="text-3xl font-bold text-base-content">
|
||||||
<p class="text-base-content/60 mt-1">Configure permissões específicas para usuários individuais</p>
|
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>
|
</div>
|
||||||
<button class="btn btn-ghost gap-2" onclick={() => goto("/ti")}>
|
<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">
|
<svg
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
|
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>
|
</svg>
|
||||||
Voltar
|
Voltar
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="alert alert-info shadow-lg">
|
||||||
<!-- Alertas -->
|
<svg
|
||||||
{#if mensagem}
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
<div class="alert mb-6 shadow-lg" class:alert-success={mensagem.tipo === "success"} class:alert-error={mensagem.tipo === "error"}>
|
class="stroke-current shrink-0 h-6 w-6"
|
||||||
{#if mensagem.tipo === "success"}
|
fill="none"
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
|
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>
|
<path
|
||||||
{:else}
|
stroke-linecap="round"
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
|
stroke-linejoin="round"
|
||||||
<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" />
|
stroke-width="2"
|
||||||
</svg>
|
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||||
{/if}
|
/>
|
||||||
<span class="font-semibold">{mensagem.texto}</span>
|
</svg>
|
||||||
</div>
|
<span>
|
||||||
{/if}
|
A personalização por usuário foi substituída por <strong
|
||||||
|
>permissões por ação</strong
|
||||||
<!-- Card de Busca -->
|
>
|
||||||
<div class="card bg-base-100 shadow-xl mb-6">
|
por perfil. Utilize o
|
||||||
<div class="card-body">
|
<a href="/ti/painel-permissoes" class="link link-primary"
|
||||||
<h2 class="card-title">Buscar Usuário</h2>
|
>Painel de Permissões</a
|
||||||
<p class="text-sm text-base-content/60">Digite a matrícula do usuário para personalizar suas permissões</p>
|
> para configurar.
|
||||||
|
</span>
|
||||||
<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>
|
</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>
|
</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"
|
"typescript": "^5.9.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"convex": "^1.28.0",
|
"convex": "^1.28.0"
|
||||||
"better-auth": "1.3.27"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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 logsAcesso from "../logsAcesso.js";
|
||||||
import type * as logsAtividades from "../logsAtividades.js";
|
import type * as logsAtividades from "../logsAtividades.js";
|
||||||
import type * as logsLogin from "../logsLogin.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 migrarUsuariosAdmin from "../migrarUsuariosAdmin.js";
|
||||||
import type * as monitoramento from "../monitoramento.js";
|
import type * as monitoramento from "../monitoramento.js";
|
||||||
import type * as perfisCustomizados from "../perfisCustomizados.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 roles from "../roles.js";
|
||||||
import type * as seed from "../seed.js";
|
import type * as seed from "../seed.js";
|
||||||
import type * as simbolos from "../simbolos.js";
|
import type * as simbolos from "../simbolos.js";
|
||||||
@@ -76,10 +76,10 @@ declare const fullApi: ApiFromModules<{
|
|||||||
logsAcesso: typeof logsAcesso;
|
logsAcesso: typeof logsAcesso;
|
||||||
logsAtividades: typeof logsAtividades;
|
logsAtividades: typeof logsAtividades;
|
||||||
logsLogin: typeof logsLogin;
|
logsLogin: typeof logsLogin;
|
||||||
menuPermissoes: typeof menuPermissoes;
|
|
||||||
migrarUsuariosAdmin: typeof migrarUsuariosAdmin;
|
migrarUsuariosAdmin: typeof migrarUsuariosAdmin;
|
||||||
monitoramento: typeof monitoramento;
|
monitoramento: typeof monitoramento;
|
||||||
perfisCustomizados: typeof perfisCustomizados;
|
perfisCustomizados: typeof perfisCustomizados;
|
||||||
|
permissoesAcoes: typeof permissoesAcoes;
|
||||||
roles: typeof roles;
|
roles: typeof roles;
|
||||||
seed: typeof seed;
|
seed: typeof seed;
|
||||||
simbolos: typeof simbolos;
|
simbolos: typeof simbolos;
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
import { v } from "convex/values";
|
import { v } from "convex/values";
|
||||||
import { mutation, query } from "./_generated/server";
|
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 { registrarLogin } from "./logsLogin";
|
||||||
import { Id } from "./_generated/dataModel";
|
import { Id } from "./_generated/dataModel";
|
||||||
|
|
||||||
@@ -10,7 +16,7 @@ import { Id } from "./_generated/dataModel";
|
|||||||
async function verificarBloqueioUsuario(ctx: any, usuarioId: Id<"usuarios">) {
|
async function verificarBloqueioUsuario(ctx: any, usuarioId: Id<"usuarios">) {
|
||||||
const bloqueio = await ctx.db
|
const bloqueio = await ctx.db
|
||||||
.query("bloqueiosUsuarios")
|
.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))
|
.filter((q: any) => q.eq(q.field("ativo"), true))
|
||||||
.first();
|
.first();
|
||||||
|
|
||||||
@@ -23,7 +29,7 @@ async function verificarBloqueioUsuario(ctx: any, usuarioId: Id<"usuarios">) {
|
|||||||
async function verificarRateLimitIP(ctx: any, ipAddress: string) {
|
async function verificarRateLimitIP(ctx: any, ipAddress: string) {
|
||||||
// Últimas 15 minutos
|
// Últimas 15 minutos
|
||||||
const dataLimite = Date.now() - 15 * 60 * 1000;
|
const dataLimite = Date.now() - 15 * 60 * 1000;
|
||||||
|
|
||||||
const tentativas = await ctx.db
|
const tentativas = await ctx.db
|
||||||
.query("logsLogin")
|
.query("logsLogin")
|
||||||
.withIndex("by_ip", (q: any) => q.eq("ipAddress", ipAddress))
|
.withIndex("by_ip", (q: any) => q.eq("ipAddress", ipAddress))
|
||||||
@@ -31,7 +37,7 @@ async function verificarRateLimitIP(ctx: any, ipAddress: string) {
|
|||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
const falhas = tentativas.filter((t: any) => !t.sucesso).length;
|
const falhas = tentativas.filter((t: any) => !t.sucesso).length;
|
||||||
|
|
||||||
// Bloquear se 5 ou mais tentativas falhas em 15 minutos
|
// Bloquear se 5 ou mais tentativas falhas em 15 minutos
|
||||||
return falhas >= 5;
|
return falhas >= 5;
|
||||||
}
|
}
|
||||||
@@ -102,7 +108,9 @@ export const login = mutation({
|
|||||||
} else {
|
} else {
|
||||||
usuario = await ctx.db
|
usuario = await ctx.db
|
||||||
.query("usuarios")
|
.query("usuarios")
|
||||||
.withIndex("by_matricula", (q) => q.eq("matricula", args.matriculaOuEmail))
|
.withIndex("by_matricula", (q) =>
|
||||||
|
q.eq("matricula", args.matriculaOuEmail)
|
||||||
|
)
|
||||||
.first();
|
.first();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,7 +130,10 @@ export const login = mutation({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Verificar se usuário está bloqueado
|
// 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, {
|
await registrarLogin(ctx, {
|
||||||
usuarioId: usuario._id,
|
usuarioId: usuario._id,
|
||||||
matriculaOuEmail: args.matriculaOuEmail,
|
matriculaOuEmail: args.matriculaOuEmail,
|
||||||
@@ -172,7 +183,9 @@ export const login = mutation({
|
|||||||
userAgent: args.userAgent,
|
userAgent: args.userAgent,
|
||||||
});
|
});
|
||||||
|
|
||||||
const minutosRestantes = Math.ceil((TEMPO_BLOQUEIO - tempoDecorrido) / 60000);
|
const minutosRestantes = Math.ceil(
|
||||||
|
(TEMPO_BLOQUEIO - tempoDecorrido) / 60000
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
sucesso: false as const,
|
sucesso: false as const,
|
||||||
erro: `Conta temporariamente bloqueada. Tente novamente em ${minutosRestantes} minutos.`,
|
erro: `Conta temporariamente bloqueada. Tente novamente em ${minutosRestantes} minutos.`,
|
||||||
@@ -192,8 +205,9 @@ export const login = mutation({
|
|||||||
|
|
||||||
if (!senhaValida) {
|
if (!senhaValida) {
|
||||||
// Incrementar tentativas
|
// Incrementar tentativas
|
||||||
const novasTentativas = tempoDecorrido > TEMPO_BLOQUEIO ? 1 : tentativasRecentes + 1;
|
const novasTentativas =
|
||||||
|
tempoDecorrido > TEMPO_BLOQUEIO ? 1 : tentativasRecentes + 1;
|
||||||
|
|
||||||
await ctx.db.patch(usuario._id, {
|
await ctx.db.patch(usuario._id, {
|
||||||
tentativasLogin: novasTentativas,
|
tentativasLogin: novasTentativas,
|
||||||
ultimaTentativaLogin: Date.now(),
|
ultimaTentativaLogin: Date.now(),
|
||||||
@@ -367,7 +381,10 @@ export const verificarSessao = query({
|
|||||||
.first();
|
.first();
|
||||||
|
|
||||||
if (!sessao || !sessao.ativo) {
|
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
|
// Verificar se sessão expirou
|
||||||
@@ -380,7 +397,10 @@ export const verificarSessao = query({
|
|||||||
// Buscar usuário
|
// Buscar usuário
|
||||||
const usuario = await ctx.db.get(sessao.usuarioId);
|
const usuario = await ctx.db.get(sessao.usuarioId);
|
||||||
if (!usuario || !usuario.ativo) {
|
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
|
// Buscar role
|
||||||
@@ -428,7 +448,7 @@ export const limparSessoesExpiradas = mutation({
|
|||||||
for (const sessao of sessoes) {
|
for (const sessao of sessoes) {
|
||||||
if (sessao.expiraEm < agora) {
|
if (sessao.expiraEm < agora) {
|
||||||
await ctx.db.patch(sessao._id, { ativo: false });
|
await ctx.db.patch(sessao._id, { ativo: false });
|
||||||
|
|
||||||
await ctx.db.insert("logsAcesso", {
|
await ctx.db.insert("logsAcesso", {
|
||||||
usuarioId: sessao.usuarioId,
|
usuarioId: sessao.usuarioId,
|
||||||
tipo: "sessao_expirada",
|
tipo: "sessao_expirada",
|
||||||
@@ -511,4 +531,3 @@ export const alterarSenha = mutation({
|
|||||||
return { sucesso: true as const };
|
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 { defineApp } from "convex/server";
|
||||||
import betterAuth from "./betterAuth/convex.config";
|
|
||||||
|
|
||||||
const app = defineApp();
|
const app = defineApp();
|
||||||
app.use(betterAuth);
|
|
||||||
|
|
||||||
export default app;
|
export default app;
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
import { v } from "convex/values";
|
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 { Id } from "./_generated/dataModel";
|
||||||
import { renderizarTemplate } from "./templatesMensagens";
|
import { renderizarTemplate } from "./templatesMensagens";
|
||||||
|
import { internal } from "./_generated/api";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enfileirar email para envio
|
* Enfileirar email para envio
|
||||||
@@ -15,7 +22,10 @@ export const enfileirarEmail = mutation({
|
|||||||
templateId: v.optional(v.id("templatesMensagens")),
|
templateId: v.optional(v.id("templatesMensagens")),
|
||||||
enviadoPorId: v.id("usuarios"),
|
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) => {
|
handler: async (ctx, args) => {
|
||||||
// Validar email
|
// Validar email
|
||||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||||
@@ -51,7 +61,10 @@ export const enviarEmailComTemplate = mutation({
|
|||||||
variaveis: v.any(), // Record<string, string>
|
variaveis: v.any(), // Record<string, string>
|
||||||
enviadoPorId: v.id("usuarios"),
|
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) => {
|
handler: async (ctx, args) => {
|
||||||
// Buscar template
|
// Buscar template
|
||||||
const template = await ctx.db
|
const template = await ctx.db
|
||||||
@@ -90,25 +103,32 @@ export const enviarEmailComTemplate = mutation({
|
|||||||
*/
|
*/
|
||||||
export const listarFilaEmails = query({
|
export const listarFilaEmails = query({
|
||||||
args: {
|
args: {
|
||||||
status: v.optional(v.union(
|
status: v.optional(
|
||||||
v.literal("pendente"),
|
v.union(
|
||||||
v.literal("enviando"),
|
v.literal("pendente"),
|
||||||
v.literal("enviado"),
|
v.literal("enviando"),
|
||||||
v.literal("falha")
|
v.literal("enviado"),
|
||||||
)),
|
v.literal("falha")
|
||||||
|
)
|
||||||
|
),
|
||||||
limite: v.optional(v.number()),
|
limite: v.optional(v.number()),
|
||||||
},
|
},
|
||||||
|
returns: v.array(v.any()),
|
||||||
handler: async (ctx, args) => {
|
handler: async (ctx, args) => {
|
||||||
let query = ctx.db.query("notificacoesEmail");
|
|
||||||
|
|
||||||
if (args.status) {
|
if (args.status) {
|
||||||
query = query.withIndex("by_status", (q) => q.eq("status", args.status));
|
const emails = await ctx.db
|
||||||
} else {
|
.query("notificacoesEmail")
|
||||||
query = query.withIndex("by_criado_em");
|
.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;
|
return emails;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -141,9 +161,68 @@ export const reenviarEmail = mutation({
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Action para enviar email (será implementado com nodemailer)
|
* Action para enviar email (será implementado com nodemailer)
|
||||||
*
|
*
|
||||||
* NOTA: Este é um placeholder. Implementação real requer nodemailer.
|
* 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({
|
export const enviarEmailAction = action({
|
||||||
args: {
|
args: {
|
||||||
emailId: v.id("notificacoesEmail"),
|
emailId: v.id("notificacoesEmail"),
|
||||||
@@ -151,11 +230,11 @@ export const enviarEmailAction = action({
|
|||||||
returns: v.object({ sucesso: v.boolean(), erro: v.optional(v.string()) }),
|
returns: v.object({ sucesso: v.boolean(), erro: v.optional(v.string()) }),
|
||||||
handler: async (ctx, args) => {
|
handler: async (ctx, args) => {
|
||||||
// TODO: Implementar com nodemailer quando instalado
|
// TODO: Implementar com nodemailer quando instalado
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Buscar email da fila
|
// Buscar email da fila
|
||||||
const email = await ctx.runQuery(async (ctx) => {
|
const email = await ctx.runQuery(internal.email.getEmailById, {
|
||||||
return await ctx.db.get(args.emailId);
|
emailId: args.emailId,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!email) {
|
if (!email) {
|
||||||
@@ -163,52 +242,41 @@ export const enviarEmailAction = action({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Buscar configuração SMTP
|
// Buscar configuração SMTP
|
||||||
const config = await ctx.runQuery(async (ctx) => {
|
const config = await ctx.runQuery(
|
||||||
return await ctx.db
|
internal.email.getActiveEmailConfig,
|
||||||
.query("configuracaoEmail")
|
{}
|
||||||
.withIndex("by_ativo", (q) => q.eq("ativo", true))
|
);
|
||||||
.first();
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!config) {
|
if (!config) {
|
||||||
return { sucesso: false, erro: "Configuração de email não encontrada" };
|
return { sucesso: false, erro: "Configuração de email não encontrada" };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Marcar como enviando
|
// Marcar como enviando
|
||||||
await ctx.runMutation(async (ctx) => {
|
await ctx.runMutation(internal.email.markEmailEnviando, {
|
||||||
await ctx.db.patch(args.emailId, {
|
emailId: args.emailId,
|
||||||
status: "enviando",
|
|
||||||
tentativas: (email.tentativas || 0) + 1,
|
|
||||||
ultimaTentativa: Date.now(),
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: Enviar email real com nodemailer aqui
|
// TODO: Enviar email real com nodemailer aqui
|
||||||
console.log("⚠️ AVISO: Envio de email simulado (nodemailer não instalado)");
|
console.log(
|
||||||
console.log(" Para:", email.destinatario);
|
"⚠️ AVISO: Envio de email simulado (nodemailer não instalado)"
|
||||||
console.log(" Assunto:", email.assunto);
|
);
|
||||||
|
console.log(" Para:", (email as any).destinatario);
|
||||||
|
console.log(" Assunto:", (email as any).assunto);
|
||||||
|
|
||||||
// Simular delay de envio
|
// Simular delay de envio
|
||||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||||
|
|
||||||
// Marcar como enviado
|
// Marcar como enviado
|
||||||
await ctx.runMutation(async (ctx) => {
|
await ctx.runMutation(internal.email.markEmailEnviado, {
|
||||||
await ctx.db.patch(args.emailId, {
|
emailId: args.emailId,
|
||||||
status: "enviado",
|
|
||||||
enviadoEm: Date.now(),
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return { sucesso: true };
|
return { sucesso: true };
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
// Marcar como falha
|
// Marcar como falha
|
||||||
await ctx.runMutation(async (ctx) => {
|
await ctx.runMutation(internal.email.markEmailFalha, {
|
||||||
const email = await ctx.db.get(args.emailId);
|
emailId: args.emailId,
|
||||||
await ctx.db.patch(args.emailId, {
|
erro: error.message || "Erro ao enviar email",
|
||||||
status: "falha",
|
|
||||||
erroDetalhes: error.message || "Erro desconhecido",
|
|
||||||
tentativas: (email?.tentativas || 0) + 1,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return { sucesso: false, 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({
|
export const processarFilaEmails = internalMutation({
|
||||||
args: {},
|
args: {},
|
||||||
|
returns: v.object({ processados: v.number() }),
|
||||||
handler: async (ctx) => {
|
handler: async (ctx) => {
|
||||||
// Buscar emails pendentes (max 10 por execução)
|
// Buscar emails pendentes (max 10 por execução)
|
||||||
const emailsPendentes = await ctx.db
|
const emailsPendentes = await ctx.db
|
||||||
@@ -255,5 +324,3 @@ export const processarFilaEmails = internalMutation({
|
|||||||
return { processados };
|
return { processados };
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,49 @@
|
|||||||
import { v } from "convex/values";
|
import { v } from "convex/values";
|
||||||
import { query, mutation } from "./_generated/server";
|
import { query, mutation } from "./_generated/server";
|
||||||
|
import { internal } from "./_generated/api";
|
||||||
import { simboloTipo } from "./schema";
|
import { simboloTipo } from "./schema";
|
||||||
|
|
||||||
// Validadores para campos opcionais
|
// Validadores para campos opcionais
|
||||||
const sexoValidator = v.optional(v.union(v.literal("masculino"), v.literal("feminino"), v.literal("outro")));
|
const sexoValidator = v.optional(
|
||||||
const estadoCivilValidator = v.optional(v.union(v.literal("solteiro"), v.literal("casado"), v.literal("divorciado"), v.literal("viuvo"), v.literal("uniao_estavel")));
|
v.union(v.literal("masculino"), v.literal("feminino"), v.literal("outro"))
|
||||||
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 estadoCivilValidator = v.optional(
|
||||||
const fatorRHValidator = v.optional(v.union(v.literal("positivo"), v.literal("negativo")));
|
v.union(
|
||||||
const aposentadoValidator = v.optional(v.union(v.literal("nao"), v.literal("funape_ipsep"), v.literal("inss")));
|
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({
|
export const getAll = query({
|
||||||
args: {},
|
args: {},
|
||||||
handler: async (ctx) => {
|
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();
|
const funcionarios = await ctx.db.query("funcionarios").collect();
|
||||||
// Retornar apenas os campos necessários para listagem
|
// Retornar apenas os campos necessários para listagem
|
||||||
return funcionarios.map((f: any) => ({
|
return funcionarios.map((f: any) => ({
|
||||||
@@ -40,6 +71,11 @@ export const getAll = query({
|
|||||||
export const getById = query({
|
export const getById = query({
|
||||||
args: { id: v.id("funcionarios") },
|
args: { id: v.id("funcionarios") },
|
||||||
handler: async (ctx, args) => {
|
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);
|
return await ctx.db.get(args.id);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -62,7 +98,7 @@ export const create = mutation({
|
|||||||
admissaoData: v.optional(v.string()),
|
admissaoData: v.optional(v.string()),
|
||||||
desligamentoData: v.optional(v.string()),
|
desligamentoData: v.optional(v.string()),
|
||||||
simboloTipo: simboloTipo,
|
simboloTipo: simboloTipo,
|
||||||
|
|
||||||
// Dados Pessoais Adicionais
|
// Dados Pessoais Adicionais
|
||||||
nomePai: v.optional(v.string()),
|
nomePai: v.optional(v.string()),
|
||||||
nomeMae: v.optional(v.string()),
|
nomeMae: v.optional(v.string()),
|
||||||
@@ -71,7 +107,7 @@ export const create = mutation({
|
|||||||
sexo: sexoValidator,
|
sexo: sexoValidator,
|
||||||
estadoCivil: estadoCivilValidator,
|
estadoCivil: estadoCivilValidator,
|
||||||
nacionalidade: v.optional(v.string()),
|
nacionalidade: v.optional(v.string()),
|
||||||
|
|
||||||
// Documentos Pessoais
|
// Documentos Pessoais
|
||||||
rgOrgaoExpedidor: v.optional(v.string()),
|
rgOrgaoExpedidor: v.optional(v.string()),
|
||||||
rgDataEmissao: v.optional(v.string()),
|
rgDataEmissao: v.optional(v.string()),
|
||||||
@@ -84,14 +120,14 @@ export const create = mutation({
|
|||||||
tituloEleitorZona: v.optional(v.string()),
|
tituloEleitorZona: v.optional(v.string()),
|
||||||
tituloEleitorSecao: v.optional(v.string()),
|
tituloEleitorSecao: v.optional(v.string()),
|
||||||
pisNumero: v.optional(v.string()),
|
pisNumero: v.optional(v.string()),
|
||||||
|
|
||||||
// Formação e Saúde
|
// Formação e Saúde
|
||||||
grauInstrucao: grauInstrucaoValidator,
|
grauInstrucao: grauInstrucaoValidator,
|
||||||
formacao: v.optional(v.string()),
|
formacao: v.optional(v.string()),
|
||||||
formacaoRegistro: v.optional(v.string()),
|
formacaoRegistro: v.optional(v.string()),
|
||||||
grupoSanguineo: grupoSanguineoValidator,
|
grupoSanguineo: grupoSanguineoValidator,
|
||||||
fatorRH: fatorRHValidator,
|
fatorRH: fatorRHValidator,
|
||||||
|
|
||||||
// Cargo e Vínculo
|
// Cargo e Vínculo
|
||||||
descricaoCargo: v.optional(v.string()),
|
descricaoCargo: v.optional(v.string()),
|
||||||
nomeacaoPortaria: v.optional(v.string()),
|
nomeacaoPortaria: v.optional(v.string()),
|
||||||
@@ -100,12 +136,12 @@ export const create = mutation({
|
|||||||
pertenceOrgaoPublico: v.optional(v.boolean()),
|
pertenceOrgaoPublico: v.optional(v.boolean()),
|
||||||
orgaoOrigem: v.optional(v.string()),
|
orgaoOrigem: v.optional(v.string()),
|
||||||
aposentado: aposentadoValidator,
|
aposentado: aposentadoValidator,
|
||||||
|
|
||||||
// Dados Bancários
|
// Dados Bancários
|
||||||
contaBradescoNumero: v.optional(v.string()),
|
contaBradescoNumero: v.optional(v.string()),
|
||||||
contaBradescoDV: v.optional(v.string()),
|
contaBradescoDV: v.optional(v.string()),
|
||||||
contaBradescoAgencia: v.optional(v.string()),
|
contaBradescoAgencia: v.optional(v.string()),
|
||||||
|
|
||||||
// Documentos Anexos (Storage IDs)
|
// Documentos Anexos (Storage IDs)
|
||||||
certidaoAntecedentesPF: v.optional(v.id("_storage")),
|
certidaoAntecedentesPF: v.optional(v.id("_storage")),
|
||||||
certidaoAntecedentesJFPE: v.optional(v.id("_storage")),
|
certidaoAntecedentesJFPE: v.optional(v.id("_storage")),
|
||||||
@@ -130,7 +166,7 @@ export const create = mutation({
|
|||||||
comprovanteEscolaridade: v.optional(v.id("_storage")),
|
comprovanteEscolaridade: v.optional(v.id("_storage")),
|
||||||
comprovanteResidencia: v.optional(v.id("_storage")),
|
comprovanteResidencia: v.optional(v.id("_storage")),
|
||||||
comprovanteContaBradesco: v.optional(v.id("_storage")),
|
comprovanteContaBradesco: v.optional(v.id("_storage")),
|
||||||
|
|
||||||
// Declarações (Storage IDs)
|
// Declarações (Storage IDs)
|
||||||
declaracaoAcumulacaoCargo: v.optional(v.id("_storage")),
|
declaracaoAcumulacaoCargo: v.optional(v.id("_storage")),
|
||||||
declaracaoDependentesIR: v.optional(v.id("_storage")),
|
declaracaoDependentesIR: v.optional(v.id("_storage")),
|
||||||
@@ -140,6 +176,11 @@ export const create = mutation({
|
|||||||
},
|
},
|
||||||
returns: v.id("funcionarios"),
|
returns: v.id("funcionarios"),
|
||||||
handler: async (ctx, args) => {
|
handler: async (ctx, args) => {
|
||||||
|
// Autorização: criar
|
||||||
|
await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, {
|
||||||
|
recurso: "funcionarios",
|
||||||
|
acao: "criar",
|
||||||
|
});
|
||||||
// Unicidade: CPF
|
// Unicidade: CPF
|
||||||
const cpfExists = await ctx.db
|
const cpfExists = await ctx.db
|
||||||
.query("funcionarios")
|
.query("funcionarios")
|
||||||
@@ -182,7 +223,7 @@ export const update = mutation({
|
|||||||
admissaoData: v.optional(v.string()),
|
admissaoData: v.optional(v.string()),
|
||||||
desligamentoData: v.optional(v.string()),
|
desligamentoData: v.optional(v.string()),
|
||||||
simboloTipo: simboloTipo,
|
simboloTipo: simboloTipo,
|
||||||
|
|
||||||
// Dados Pessoais Adicionais
|
// Dados Pessoais Adicionais
|
||||||
nomePai: v.optional(v.string()),
|
nomePai: v.optional(v.string()),
|
||||||
nomeMae: v.optional(v.string()),
|
nomeMae: v.optional(v.string()),
|
||||||
@@ -191,7 +232,7 @@ export const update = mutation({
|
|||||||
sexo: sexoValidator,
|
sexo: sexoValidator,
|
||||||
estadoCivil: estadoCivilValidator,
|
estadoCivil: estadoCivilValidator,
|
||||||
nacionalidade: v.optional(v.string()),
|
nacionalidade: v.optional(v.string()),
|
||||||
|
|
||||||
// Documentos Pessoais
|
// Documentos Pessoais
|
||||||
rgOrgaoExpedidor: v.optional(v.string()),
|
rgOrgaoExpedidor: v.optional(v.string()),
|
||||||
rgDataEmissao: v.optional(v.string()),
|
rgDataEmissao: v.optional(v.string()),
|
||||||
@@ -204,14 +245,14 @@ export const update = mutation({
|
|||||||
tituloEleitorZona: v.optional(v.string()),
|
tituloEleitorZona: v.optional(v.string()),
|
||||||
tituloEleitorSecao: v.optional(v.string()),
|
tituloEleitorSecao: v.optional(v.string()),
|
||||||
pisNumero: v.optional(v.string()),
|
pisNumero: v.optional(v.string()),
|
||||||
|
|
||||||
// Formação e Saúde
|
// Formação e Saúde
|
||||||
grauInstrucao: grauInstrucaoValidator,
|
grauInstrucao: grauInstrucaoValidator,
|
||||||
formacao: v.optional(v.string()),
|
formacao: v.optional(v.string()),
|
||||||
formacaoRegistro: v.optional(v.string()),
|
formacaoRegistro: v.optional(v.string()),
|
||||||
grupoSanguineo: grupoSanguineoValidator,
|
grupoSanguineo: grupoSanguineoValidator,
|
||||||
fatorRH: fatorRHValidator,
|
fatorRH: fatorRHValidator,
|
||||||
|
|
||||||
// Cargo e Vínculo
|
// Cargo e Vínculo
|
||||||
descricaoCargo: v.optional(v.string()),
|
descricaoCargo: v.optional(v.string()),
|
||||||
nomeacaoPortaria: v.optional(v.string()),
|
nomeacaoPortaria: v.optional(v.string()),
|
||||||
@@ -220,12 +261,12 @@ export const update = mutation({
|
|||||||
pertenceOrgaoPublico: v.optional(v.boolean()),
|
pertenceOrgaoPublico: v.optional(v.boolean()),
|
||||||
orgaoOrigem: v.optional(v.string()),
|
orgaoOrigem: v.optional(v.string()),
|
||||||
aposentado: aposentadoValidator,
|
aposentado: aposentadoValidator,
|
||||||
|
|
||||||
// Dados Bancários
|
// Dados Bancários
|
||||||
contaBradescoNumero: v.optional(v.string()),
|
contaBradescoNumero: v.optional(v.string()),
|
||||||
contaBradescoDV: v.optional(v.string()),
|
contaBradescoDV: v.optional(v.string()),
|
||||||
contaBradescoAgencia: v.optional(v.string()),
|
contaBradescoAgencia: v.optional(v.string()),
|
||||||
|
|
||||||
// Documentos Anexos (Storage IDs)
|
// Documentos Anexos (Storage IDs)
|
||||||
certidaoAntecedentesPF: v.optional(v.id("_storage")),
|
certidaoAntecedentesPF: v.optional(v.id("_storage")),
|
||||||
certidaoAntecedentesJFPE: v.optional(v.id("_storage")),
|
certidaoAntecedentesJFPE: v.optional(v.id("_storage")),
|
||||||
@@ -250,7 +291,7 @@ export const update = mutation({
|
|||||||
comprovanteEscolaridade: v.optional(v.id("_storage")),
|
comprovanteEscolaridade: v.optional(v.id("_storage")),
|
||||||
comprovanteResidencia: v.optional(v.id("_storage")),
|
comprovanteResidencia: v.optional(v.id("_storage")),
|
||||||
comprovanteContaBradesco: v.optional(v.id("_storage")),
|
comprovanteContaBradesco: v.optional(v.id("_storage")),
|
||||||
|
|
||||||
// Declarações (Storage IDs)
|
// Declarações (Storage IDs)
|
||||||
declaracaoAcumulacaoCargo: v.optional(v.id("_storage")),
|
declaracaoAcumulacaoCargo: v.optional(v.id("_storage")),
|
||||||
declaracaoDependentesIR: v.optional(v.id("_storage")),
|
declaracaoDependentesIR: v.optional(v.id("_storage")),
|
||||||
@@ -260,6 +301,11 @@ export const update = mutation({
|
|||||||
},
|
},
|
||||||
returns: v.null(),
|
returns: v.null(),
|
||||||
handler: async (ctx, args) => {
|
handler: async (ctx, args) => {
|
||||||
|
// Autorização: editar
|
||||||
|
await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, {
|
||||||
|
recurso: "funcionarios",
|
||||||
|
acao: "editar",
|
||||||
|
});
|
||||||
// Unicidade: CPF (excluindo o próprio registro)
|
// Unicidade: CPF (excluindo o próprio registro)
|
||||||
const cpfExists = await ctx.db
|
const cpfExists = await ctx.db
|
||||||
.query("funcionarios")
|
.query("funcionarios")
|
||||||
@@ -288,6 +334,11 @@ export const remove = mutation({
|
|||||||
args: { id: v.id("funcionarios") },
|
args: { id: v.id("funcionarios") },
|
||||||
returns: v.null(),
|
returns: v.null(),
|
||||||
handler: async (ctx, args) => {
|
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
|
// TODO: Talvez queiramos também remover os arquivos do storage
|
||||||
await ctx.db.delete(args.id);
|
await ctx.db.delete(args.id);
|
||||||
return null;
|
return null;
|
||||||
@@ -298,21 +349,27 @@ export const remove = mutation({
|
|||||||
export const getFichaCompleta = query({
|
export const getFichaCompleta = query({
|
||||||
args: { id: v.id("funcionarios") },
|
args: { id: v.id("funcionarios") },
|
||||||
handler: async (ctx, args) => {
|
handler: async (ctx, args) => {
|
||||||
|
await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, {
|
||||||
|
recurso: "funcionarios",
|
||||||
|
acao: "ver",
|
||||||
|
});
|
||||||
const funcionario = await ctx.db.get(args.id);
|
const funcionario = await ctx.db.get(args.id);
|
||||||
if (!funcionario) {
|
if (!funcionario) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Buscar informações do símbolo
|
// Buscar informações do símbolo
|
||||||
const simbolo = await ctx.db.get(funcionario.simboloId);
|
const simbolo = await ctx.db.get(funcionario.simboloId);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...funcionario,
|
...funcionario,
|
||||||
simbolo: simbolo ? {
|
simbolo: simbolo
|
||||||
nome: simbolo.nome,
|
? {
|
||||||
descricao: simbolo.descricao,
|
nome: simbolo.nome,
|
||||||
valor: simbolo.valor,
|
descricao: simbolo.descricao,
|
||||||
} : null,
|
valor: simbolo.valor,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export const listarTodosRoles = query({
|
|||||||
descricao: v.string(),
|
descricao: v.string(),
|
||||||
nivel: v.number(),
|
nivel: v.number(),
|
||||||
setor: v.optional(v.string()),
|
setor: v.optional(v.string()),
|
||||||
customizado: v.boolean(),
|
customizado: v.optional(v.boolean()),
|
||||||
editavel: v.optional(v.boolean()),
|
editavel: v.optional(v.boolean()),
|
||||||
_creationTime: v.number(),
|
_creationTime: v.number(),
|
||||||
})
|
})
|
||||||
@@ -35,7 +35,7 @@ export const listarTodosRoles = query({
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Limpar perfis antigos/duplicados
|
* Limpar perfis antigos/duplicados
|
||||||
*
|
*
|
||||||
* CRITÉRIOS:
|
* CRITÉRIOS:
|
||||||
* - Manter apenas: ti_master (nível 0), admin (nível 2), ti_usuario (nível 2)
|
* - Manter apenas: ti_master (nível 0), admin (nível 2), ti_usuario (nível 2)
|
||||||
* - Remover: admin antigo (nível 0), ti genérico (nível 1), outros duplicados
|
* - Remover: admin antigo (nível 0), ti genérico (nível 1), outros duplicados
|
||||||
@@ -61,14 +61,14 @@ export const limparPerfisAntigos = internalMutation({
|
|||||||
}),
|
}),
|
||||||
handler: async (ctx) => {
|
handler: async (ctx) => {
|
||||||
const roles = await ctx.db.query("roles").collect();
|
const roles = await ctx.db.query("roles").collect();
|
||||||
|
|
||||||
const removidos: Array<{
|
const removidos: Array<{
|
||||||
nome: string;
|
nome: string;
|
||||||
descricao: string;
|
descricao: string;
|
||||||
nivel: number;
|
nivel: number;
|
||||||
motivo: string;
|
motivo: string;
|
||||||
}> = [];
|
}> = [];
|
||||||
|
|
||||||
const mantidos: Array<{
|
const mantidos: Array<{
|
||||||
nome: string;
|
nome: string;
|
||||||
descricao: string;
|
descricao: string;
|
||||||
@@ -91,9 +91,10 @@ export const limparPerfisAntigos = internalMutation({
|
|||||||
deveManter = true;
|
deveManter = true;
|
||||||
perfisCorretos.set("ti_master", true);
|
perfisCorretos.set("ti_master", true);
|
||||||
} else {
|
} else {
|
||||||
motivo = role.nivel !== 0
|
motivo =
|
||||||
? "TI_MASTER deve ser nível 0, este é nível " + role.nivel
|
role.nivel !== 0
|
||||||
: "TI_MASTER duplicado";
|
? "TI_MASTER deve ser nível 0, este é nível " + role.nivel
|
||||||
|
: "TI_MASTER duplicado";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// ADMIN - Manter apenas o de nível 2
|
// ADMIN - Manter apenas o de nível 2
|
||||||
@@ -102,9 +103,10 @@ export const limparPerfisAntigos = internalMutation({
|
|||||||
deveManter = true;
|
deveManter = true;
|
||||||
perfisCorretos.set("admin", true);
|
perfisCorretos.set("admin", true);
|
||||||
} else {
|
} else {
|
||||||
motivo = role.nivel !== 2
|
motivo =
|
||||||
? "ADMIN deve ser nível 2, este é nível " + role.nivel
|
role.nivel !== 2
|
||||||
: "ADMIN duplicado";
|
? "ADMIN deve ser nível 2, este é nível " + role.nivel
|
||||||
|
: "ADMIN duplicado";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// TI_USUARIO - Manter apenas o de nível 2
|
// TI_USUARIO - Manter apenas o de nível 2
|
||||||
@@ -113,14 +115,16 @@ export const limparPerfisAntigos = internalMutation({
|
|||||||
deveManter = true;
|
deveManter = true;
|
||||||
perfisCorretos.set("ti_usuario", true);
|
perfisCorretos.set("ti_usuario", true);
|
||||||
} else {
|
} else {
|
||||||
motivo = role.nivel !== 2
|
motivo =
|
||||||
? "TI_USUARIO deve ser nível 2, este é nível " + role.nivel
|
role.nivel !== 2
|
||||||
: "TI_USUARIO duplicado";
|
? "TI_USUARIO deve ser nível 2, este é nível " + role.nivel
|
||||||
|
: "TI_USUARIO duplicado";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Perfis genéricos antigos (remover)
|
// Perfis genéricos antigos (remover)
|
||||||
else if (role.nome === "ti") {
|
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)
|
// Outros perfis específicos de setores (manter se forem nível >= 2)
|
||||||
else if (
|
else if (
|
||||||
@@ -157,7 +161,9 @@ export const limparPerfisAntigos = internalMutation({
|
|||||||
descricao: role.descricao,
|
descricao: role.descricao,
|
||||||
nivel: role.nivel,
|
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 {
|
} else {
|
||||||
// Verificar se há usuários usando este perfil
|
// Verificar se há usuários usando este perfil
|
||||||
const usuariosComRole = await ctx.db
|
const usuariosComRole = await ctx.db
|
||||||
@@ -286,5 +292,3 @@ export const verificarNiveisIncorretos = query({
|
|||||||
return problemas;
|
return problemas;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { Doc, Id } from "./_generated/dataModel";
|
|||||||
* Use em todas as mutations que modificam dados
|
* Use em todas as mutations que modificam dados
|
||||||
*/
|
*/
|
||||||
export async function registrarAtividade(
|
export async function registrarAtividade(
|
||||||
ctx: QueryCtx | MutationCtx,
|
ctx: MutationCtx,
|
||||||
usuarioId: Id<"usuarios">,
|
usuarioId: Id<"usuarios">,
|
||||||
acao: string,
|
acao: string,
|
||||||
recurso: string,
|
recurso: string,
|
||||||
@@ -37,21 +37,34 @@ export const listarAtividades = query({
|
|||||||
limite: v.optional(v.number()),
|
limite: v.optional(v.number()),
|
||||||
},
|
},
|
||||||
handler: async (ctx, args) => {
|
handler: async (ctx, args) => {
|
||||||
let query = ctx.db.query("logsAtividades");
|
let atividades;
|
||||||
|
|
||||||
// Aplicar filtros
|
|
||||||
if (args.usuarioId) {
|
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) {
|
} 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) {
|
} 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 {
|
} 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
|
// Filtrar por range de datas se fornecido
|
||||||
if (args.dataInicio || args.dataFim) {
|
if (args.dataInicio || args.dataFim) {
|
||||||
atividades = atividades.filter((log) => {
|
atividades = atividades.filter((log) => {
|
||||||
@@ -155,5 +168,3 @@ export const obterHistoricoRecurso = query({
|
|||||||
return atividadesComUsuarios;
|
return atividadesComUsuarios;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { Doc, Id } from "./_generated/dataModel";
|
|||||||
* Helper para registrar tentativas de login
|
* Helper para registrar tentativas de login
|
||||||
*/
|
*/
|
||||||
export async function registrarLogin(
|
export async function registrarLogin(
|
||||||
ctx: QueryCtx | MutationCtx,
|
ctx: MutationCtx,
|
||||||
dados: {
|
dados: {
|
||||||
usuarioId?: Id<"usuarios">;
|
usuarioId?: Id<"usuarios">;
|
||||||
matriculaOuEmail: string;
|
matriculaOuEmail: string;
|
||||||
@@ -170,26 +170,32 @@ export const obterEstatisticasLogin = query({
|
|||||||
|
|
||||||
// Logins por horário (hora do dia)
|
// Logins por horário (hora do dia)
|
||||||
const porHorario: Record<number, number> = {};
|
const porHorario: Record<number, number> = {};
|
||||||
logs.filter((l) => l.sucesso).forEach((log) => {
|
logs
|
||||||
const hora = new Date(log.timestamp).getHours();
|
.filter((l) => l.sucesso)
|
||||||
porHorario[hora] = (porHorario[hora] || 0) + 1;
|
.forEach((log) => {
|
||||||
});
|
const hora = new Date(log.timestamp).getHours();
|
||||||
|
porHorario[hora] = (porHorario[hora] || 0) + 1;
|
||||||
|
});
|
||||||
|
|
||||||
// Browser mais usado
|
// Browser mais usado
|
||||||
const porBrowser: Record<string, number> = {};
|
const porBrowser: Record<string, number> = {};
|
||||||
logs.filter((l) => l.sucesso).forEach((log) => {
|
logs
|
||||||
if (log.browser) {
|
.filter((l) => l.sucesso)
|
||||||
porBrowser[log.browser] = (porBrowser[log.browser] || 0) + 1;
|
.forEach((log) => {
|
||||||
}
|
if (log.browser) {
|
||||||
});
|
porBrowser[log.browser] = (porBrowser[log.browser] || 0) + 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Dispositivos mais usados
|
// Dispositivos mais usados
|
||||||
const porDevice: Record<string, number> = {};
|
const porDevice: Record<string, number> = {};
|
||||||
logs.filter((l) => l.sucesso).forEach((log) => {
|
logs
|
||||||
if (log.device) {
|
.filter((l) => l.sucesso)
|
||||||
porDevice[log.device] = (porDevice[log.device] || 0) + 1;
|
.forEach((log) => {
|
||||||
}
|
if (log.device) {
|
||||||
});
|
porDevice[log.device] = (porDevice[log.device] || 0) + 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
total: logs.length,
|
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 { v } from "convex/values";
|
||||||
import { mutation, query } from "./_generated/server";
|
import { mutation, query } from "./_generated/server";
|
||||||
import { registrarAtividade } from "./logsAtividades";
|
import { registrarAtividade } from "./logsAtividades";
|
||||||
|
import { api } from "./_generated/api";
|
||||||
|
import { Id } from "./_generated/dataModel";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Listar todos os perfis customizados
|
* Listar todos os perfis customizados
|
||||||
*/
|
*/
|
||||||
export const listarPerfisCustomizados = query({
|
export const listarPerfisCustomizados = query({
|
||||||
args: {},
|
args: {},
|
||||||
|
returns: v.array(v.any()),
|
||||||
handler: async (ctx) => {
|
handler: async (ctx) => {
|
||||||
const perfis = await ctx.db.query("perfisCustomizados").collect();
|
const perfis = await ctx.db.query("perfisCustomizados").collect();
|
||||||
|
|
||||||
@@ -15,7 +18,7 @@ export const listarPerfisCustomizados = query({
|
|||||||
perfis.map(async (perfil) => {
|
perfis.map(async (perfil) => {
|
||||||
const role = await ctx.db.get(perfil.roleId);
|
const role = await ctx.db.get(perfil.roleId);
|
||||||
const criador = await ctx.db.get(perfil.criadoPor);
|
const criador = await ctx.db.get(perfil.criadoPor);
|
||||||
|
|
||||||
// Contar usuários usando este perfil
|
// Contar usuários usando este perfil
|
||||||
const usuarios = await ctx.db
|
const usuarios = await ctx.db
|
||||||
.query("usuarios")
|
.query("usuarios")
|
||||||
@@ -42,6 +45,16 @@ export const obterPerfilComPermissoes = query({
|
|||||||
args: {
|
args: {
|
||||||
perfilId: v.id("perfisCustomizados"),
|
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) => {
|
handler: async (ctx, args) => {
|
||||||
const perfil = await ctx.db.get(args.perfilId);
|
const perfil = await ctx.db.get(args.perfilId);
|
||||||
if (!perfil) {
|
if (!perfil) {
|
||||||
@@ -99,20 +112,31 @@ export const criarPerfilCustomizado = mutation({
|
|||||||
criadoPorId: v.id("usuarios"),
|
criadoPorId: v.id("usuarios"),
|
||||||
},
|
},
|
||||||
returns: v.union(
|
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() })
|
v.object({ sucesso: v.literal(false), erro: v.string() })
|
||||||
),
|
),
|
||||||
handler: async (ctx, args) => {
|
handler: async (ctx, args) => {
|
||||||
// Validar nível (deve ser >= 3)
|
// Validar nível (deve ser >= 3)
|
||||||
if (args.nivel < 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
|
// Verificar se nome já existe
|
||||||
const roles = await ctx.db.query("roles").collect();
|
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) {
|
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
|
// Criar role correspondente
|
||||||
@@ -130,7 +154,7 @@ export const criarPerfilCustomizado = mutation({
|
|||||||
// Copiar permissões gerais
|
// Copiar permissões gerais
|
||||||
const permissoesClonar = await ctx.db
|
const permissoesClonar = await ctx.db
|
||||||
.query("rolePermissoes")
|
.query("rolePermissoes")
|
||||||
.withIndex("by_role", (q) => q.eq("roleId", args.clonarDeRoleId))
|
.withIndex("by_role", (q) => q.eq("roleId", args.clonarDeRoleId!))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
for (const perm of permissoesClonar) {
|
for (const perm of permissoesClonar) {
|
||||||
@@ -143,7 +167,7 @@ export const criarPerfilCustomizado = mutation({
|
|||||||
// Copiar permissões de menu
|
// Copiar permissões de menu
|
||||||
const menuPermsClonar = await ctx.db
|
const menuPermsClonar = await ctx.db
|
||||||
.query("menuPermissoes")
|
.query("menuPermissoes")
|
||||||
.withIndex("by_role", (q) => q.eq("roleId", args.clonarDeRoleId))
|
.withIndex("by_role", (q) => q.eq("roleId", args.clonarDeRoleId!))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
for (const menuPerm of menuPermsClonar) {
|
for (const menuPerm of menuPermsClonar) {
|
||||||
@@ -321,7 +345,10 @@ export const clonarPerfil = mutation({
|
|||||||
criadoPorId: v.id("usuarios"),
|
criadoPorId: v.id("usuarios"),
|
||||||
},
|
},
|
||||||
returns: v.union(
|
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() })
|
v.object({ sucesso: v.literal(false), erro: v.string() })
|
||||||
),
|
),
|
||||||
handler: async (ctx, args) => {
|
handler: async (ctx, args) => {
|
||||||
@@ -330,17 +357,80 @@ export const clonarPerfil = mutation({
|
|||||||
return { sucesso: false as const, erro: "Perfil origem não encontrado" };
|
return { sucesso: false as const, erro: "Perfil origem não encontrado" };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Criar novo perfil clonando o original
|
// Verificar se nome já existe
|
||||||
const resultado = await criarPerfilCustomizado(ctx, {
|
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,
|
nome: args.novoNome,
|
||||||
descricao: args.novaDescricao,
|
descricao: args.novaDescricao,
|
||||||
nivel: perfilOrigem.nivel,
|
nivel: perfilOrigem.nivel,
|
||||||
clonarDeRoleId: perfilOrigem.roleId,
|
roleId,
|
||||||
criadoPorId: args.criadoPorId,
|
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(),
|
descricao: v.string(),
|
||||||
nivel: v.number(),
|
nivel: v.number(),
|
||||||
setor: v.optional(v.string()),
|
setor: v.optional(v.string()),
|
||||||
customizado: v.boolean(),
|
customizado: v.optional(v.boolean()),
|
||||||
editavel: v.optional(v.boolean()),
|
editavel: v.optional(v.boolean()),
|
||||||
criadoPor: v.optional(v.id("usuarios")),
|
criadoPor: v.optional(v.id("usuarios")),
|
||||||
})
|
})
|
||||||
@@ -45,4 +45,3 @@ export const buscarPorId = query({
|
|||||||
return await ctx.db.get(args.roleId);
|
return await ctx.db.get(args.roleId);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import { defineSchema, defineTable } from "convex/server";
|
import { defineSchema, defineTable } from "convex/server";
|
||||||
import { Infer, v } from "convex/values";
|
import { Infer, v } from "convex/values";
|
||||||
import { tables } from "./betterAuth/schema";
|
|
||||||
import { cidrv4 } from "better-auth";
|
|
||||||
|
|
||||||
export const simboloTipo = v.union(
|
export const simboloTipo = v.union(
|
||||||
v.literal("cargo_comissionado"),
|
v.literal("cargo_comissionado"),
|
||||||
@@ -245,6 +243,7 @@ export default defineSchema({
|
|||||||
acao: v.string(), // "criar", "ler", "editar", "excluir"
|
acao: v.string(), // "criar", "ler", "editar", "excluir"
|
||||||
})
|
})
|
||||||
.index("by_recurso", ["recurso"])
|
.index("by_recurso", ["recurso"])
|
||||||
|
.index("by_recurso_e_acao", ["recurso", "acao"])
|
||||||
.index("by_nome", ["nome"]),
|
.index("by_nome", ["nome"]),
|
||||||
|
|
||||||
rolePermissoes: defineTable({
|
rolePermissoes: defineTable({
|
||||||
|
|||||||
@@ -337,7 +337,7 @@ export const seedDatabase = internalMutation({
|
|||||||
|
|
||||||
// 2. Criar usuários iniciais
|
// 2. Criar usuários iniciais
|
||||||
console.log("👤 Criando usuários iniciais...");
|
console.log("👤 Criando usuários iniciais...");
|
||||||
|
|
||||||
// TI Master
|
// TI Master
|
||||||
const senhaTIMaster = await hashPassword("TI@123");
|
const senhaTIMaster = await hashPassword("TI@123");
|
||||||
await ctx.db.insert("usuarios", {
|
await ctx.db.insert("usuarios", {
|
||||||
@@ -370,10 +370,59 @@ export const seedDatabase = internalMutation({
|
|||||||
});
|
});
|
||||||
console.log(" ✅ Admin criado (matrícula: 2000, senha: Admin@123)");
|
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
|
// 3. Inserir símbolos
|
||||||
console.log("📝 Inserindo símbolos...");
|
console.log("📝 Inserindo símbolos...");
|
||||||
const simbolosMap = new Map<string, string>();
|
const simbolosMap = new Map<string, string>();
|
||||||
|
|
||||||
for (const simbolo of simbolosData) {
|
for (const simbolo of simbolosData) {
|
||||||
const id = await ctx.db.insert("simbolos", {
|
const id = await ctx.db.insert("simbolos", {
|
||||||
descricao: simbolo.descricao,
|
descricao: simbolo.descricao,
|
||||||
@@ -393,7 +442,9 @@ export const seedDatabase = internalMutation({
|
|||||||
for (const funcionario of funcionariosData) {
|
for (const funcionario of funcionariosData) {
|
||||||
const simboloId = simbolosMap.get(funcionario.simboloNome);
|
const simboloId = simbolosMap.get(funcionario.simboloNome);
|
||||||
if (!simboloId) {
|
if (!simboloId) {
|
||||||
console.error(` ❌ Símbolo não encontrado: ${funcionario.simboloNome}`);
|
console.error(
|
||||||
|
` ❌ Símbolo não encontrado: ${funcionario.simboloNome}`
|
||||||
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -436,7 +487,9 @@ export const seedDatabase = internalMutation({
|
|||||||
criadoEm: Date.now(),
|
criadoEm: Date.now(),
|
||||||
atualizadoEm: 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
|
// 6. Inserir solicitações de acesso
|
||||||
@@ -462,28 +515,32 @@ export const seedDatabase = internalMutation({
|
|||||||
codigo: "USUARIO_BLOQUEADO",
|
codigo: "USUARIO_BLOQUEADO",
|
||||||
nome: "Usuário Bloqueado",
|
nome: "Usuário Bloqueado",
|
||||||
titulo: "Sua conta foi bloqueada",
|
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"],
|
variaveis: ["motivo"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
codigo: "USUARIO_DESBLOQUEADO",
|
codigo: "USUARIO_DESBLOQUEADO",
|
||||||
nome: "Usuário Desbloqueado",
|
nome: "Usuário Desbloqueado",
|
||||||
titulo: "Sua conta foi desbloqueada",
|
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: [],
|
variaveis: [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
codigo: "SENHA_RESETADA",
|
codigo: "SENHA_RESETADA",
|
||||||
nome: "Senha Resetada",
|
nome: "Senha Resetada",
|
||||||
titulo: "Sua senha foi 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"],
|
variaveis: ["senha"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
codigo: "PERMISSAO_ALTERADA",
|
codigo: "PERMISSAO_ALTERADA",
|
||||||
nome: "Permissão Alterada",
|
nome: "Permissão Alterada",
|
||||||
titulo: "Suas permissões foram atualizadas",
|
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: [],
|
variaveis: [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -497,7 +554,8 @@ export const seedDatabase = internalMutation({
|
|||||||
codigo: "BEM_VINDO",
|
codigo: "BEM_VINDO",
|
||||||
nome: "Boas-vindas",
|
nome: "Boas-vindas",
|
||||||
titulo: "Bem-vindo ao SGSE",
|
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"],
|
variaveis: ["nome", "matricula", "senha"],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -584,11 +642,15 @@ export const clearDatabase = internalMutation({
|
|||||||
console.log(` ✅ ${menuPermissoes.length} menu-permissões removidas`);
|
console.log(` ✅ ${menuPermissoes.length} menu-permissões removidas`);
|
||||||
|
|
||||||
// Limpar menu-permissões personalizadas
|
// 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) {
|
for (const mpp of menuPermissoesPersonalizadas) {
|
||||||
await ctx.db.delete(mpp._id);
|
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
|
// Limpar role-permissões
|
||||||
const rolePermissoes = await ctx.db.query("rolePermissoes").collect();
|
const rolePermissoes = await ctx.db.query("rolePermissoes").collect();
|
||||||
@@ -615,4 +677,3 @@ export const clearDatabase = internalMutation({
|
|||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { mutation, query } from "./_generated/server";
|
|||||||
import { hashPassword, generateToken } from "./auth/utils";
|
import { hashPassword, generateToken } from "./auth/utils";
|
||||||
import { registrarAtividade } from "./logsAtividades";
|
import { registrarAtividade } from "./logsAtividades";
|
||||||
import { Id } from "./_generated/dataModel";
|
import { Id } from "./_generated/dataModel";
|
||||||
|
import { api } from "./_generated/api";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Criar novo usuário (apenas TI)
|
* Criar novo usuário (apenas TI)
|
||||||
@@ -106,9 +107,7 @@ export const listar = query({
|
|||||||
|
|
||||||
// Filtrar por matrícula
|
// Filtrar por matrícula
|
||||||
if (args.matricula) {
|
if (args.matricula) {
|
||||||
usuarios = usuarios.filter((u) =>
|
usuarios = usuarios.filter((u) => u.matricula.includes(args.matricula!));
|
||||||
u.matricula.includes(args.matricula!)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filtrar por ativo
|
// Filtrar por ativo
|
||||||
@@ -349,9 +348,9 @@ export const atualizarPerfil = mutation({
|
|||||||
handler: async (ctx, args) => {
|
handler: async (ctx, args) => {
|
||||||
// TENTAR BETTER AUTH PRIMEIRO
|
// TENTAR BETTER AUTH PRIMEIRO
|
||||||
const identity = await ctx.auth.getUserIdentity();
|
const identity = await ctx.auth.getUserIdentity();
|
||||||
|
|
||||||
let usuarioAtual = null;
|
let usuarioAtual = null;
|
||||||
|
|
||||||
if (identity && identity.email) {
|
if (identity && identity.email) {
|
||||||
// Buscar por email (Better Auth)
|
// Buscar por email (Better Auth)
|
||||||
usuarioAtual = await ctx.db
|
usuarioAtual = await ctx.db
|
||||||
@@ -359,7 +358,7 @@ export const atualizarPerfil = mutation({
|
|||||||
.withIndex("by_email", (q) => q.eq("email", identity.email!))
|
.withIndex("by_email", (q) => q.eq("email", identity.email!))
|
||||||
.first();
|
.first();
|
||||||
}
|
}
|
||||||
|
|
||||||
// SE NÃO ENCONTROU, BUSCAR POR SESSÃO ATIVA (Sistema customizado)
|
// SE NÃO ENCONTROU, BUSCAR POR SESSÃO ATIVA (Sistema customizado)
|
||||||
if (!usuarioAtual) {
|
if (!usuarioAtual) {
|
||||||
const sessaoAtiva = await ctx.db
|
const sessaoAtiva = await ctx.db
|
||||||
@@ -367,7 +366,7 @@ export const atualizarPerfil = mutation({
|
|||||||
.filter((q) => q.eq(q.field("ativo"), true))
|
.filter((q) => q.eq(q.field("ativo"), true))
|
||||||
.order("desc")
|
.order("desc")
|
||||||
.first();
|
.first();
|
||||||
|
|
||||||
if (sessaoAtiva) {
|
if (sessaoAtiva) {
|
||||||
usuarioAtual = await ctx.db.get(sessaoAtiva.usuarioId);
|
usuarioAtual = await ctx.db.get(sessaoAtiva.usuarioId);
|
||||||
}
|
}
|
||||||
@@ -382,17 +381,20 @@ export const atualizarPerfil = mutation({
|
|||||||
|
|
||||||
// Atualizar apenas os campos fornecidos
|
// Atualizar apenas os campos fornecidos
|
||||||
const updates: any = { atualizadoEm: Date.now() };
|
const updates: any = { atualizadoEm: Date.now() };
|
||||||
|
|
||||||
if (args.avatar !== undefined) updates.avatar = args.avatar;
|
if (args.avatar !== undefined) updates.avatar = args.avatar;
|
||||||
if (args.fotoPerfil !== undefined) updates.fotoPerfil = args.fotoPerfil;
|
if (args.fotoPerfil !== undefined) updates.fotoPerfil = args.fotoPerfil;
|
||||||
if (args.setor !== undefined) updates.setor = args.setor;
|
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) {
|
if (args.statusPresenca !== undefined) {
|
||||||
updates.statusPresenca = args.statusPresenca;
|
updates.statusPresenca = args.statusPresenca;
|
||||||
updates.ultimaAtividade = Date.now();
|
updates.ultimaAtividade = Date.now();
|
||||||
}
|
}
|
||||||
if (args.notificacoesAtivadas !== undefined) updates.notificacoesAtivadas = args.notificacoesAtivadas;
|
if (args.notificacoesAtivadas !== undefined)
|
||||||
if (args.somNotificacao !== undefined) updates.somNotificacao = args.somNotificacao;
|
updates.notificacoesAtivadas = args.notificacoesAtivadas;
|
||||||
|
if (args.somNotificacao !== undefined)
|
||||||
|
updates.somNotificacao = args.somNotificacao;
|
||||||
|
|
||||||
await ctx.db.patch(usuarioAtual._id, updates);
|
await ctx.db.patch(usuarioAtual._id, updates);
|
||||||
|
|
||||||
@@ -405,15 +407,40 @@ export const atualizarPerfil = mutation({
|
|||||||
*/
|
*/
|
||||||
export const obterPerfil = query({
|
export const obterPerfil = query({
|
||||||
args: {},
|
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) => {
|
handler: async (ctx) => {
|
||||||
console.log("=== DEBUG obterPerfil ===");
|
console.log("=== DEBUG obterPerfil ===");
|
||||||
|
|
||||||
// TENTAR BETTER AUTH PRIMEIRO
|
// TENTAR BETTER AUTH PRIMEIRO
|
||||||
const identity = await ctx.auth.getUserIdentity();
|
const identity = await ctx.auth.getUserIdentity();
|
||||||
console.log("Identity:", identity ? "encontrado" : "null");
|
console.log("Identity:", identity ? "encontrado" : "null");
|
||||||
|
|
||||||
let usuarioAtual = null;
|
let usuarioAtual = null;
|
||||||
|
|
||||||
if (identity && identity.email) {
|
if (identity && identity.email) {
|
||||||
console.log("Tentando buscar por email:", identity.email);
|
console.log("Tentando buscar por email:", identity.email);
|
||||||
// Buscar por email (Better Auth)
|
// Buscar por email (Better Auth)
|
||||||
@@ -421,10 +448,13 @@ export const obterPerfil = query({
|
|||||||
.query("usuarios")
|
.query("usuarios")
|
||||||
.withIndex("by_email", (q) => q.eq("email", identity.email!))
|
.withIndex("by_email", (q) => q.eq("email", identity.email!))
|
||||||
.first();
|
.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)
|
// SE NÃO ENCONTROU, BUSCAR POR SESSÃO ATIVA (Sistema customizado)
|
||||||
if (!usuarioAtual) {
|
if (!usuarioAtual) {
|
||||||
console.log("Buscando por sessão ativa...");
|
console.log("Buscando por sessão ativa...");
|
||||||
@@ -433,24 +463,30 @@ export const obterPerfil = query({
|
|||||||
.filter((q) => q.eq(q.field("ativo"), true))
|
.filter((q) => q.eq(q.field("ativo"), true))
|
||||||
.order("desc")
|
.order("desc")
|
||||||
.first();
|
.first();
|
||||||
|
|
||||||
console.log("Sessão ativa encontrada:", sessaoAtiva ? "SIM" : "NÃO");
|
console.log("Sessão ativa encontrada:", sessaoAtiva ? "SIM" : "NÃO");
|
||||||
|
|
||||||
if (sessaoAtiva) {
|
if (sessaoAtiva) {
|
||||||
usuarioAtual = await ctx.db.get(sessaoAtiva.usuarioId);
|
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"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!usuarioAtual) {
|
if (!usuarioAtual) {
|
||||||
console.log("❌ Nenhum usuário encontrado");
|
console.log("❌ Nenhum usuário encontrado");
|
||||||
// Listar todos os usuários para debug
|
// Listar todos os usuários para debug
|
||||||
const todosUsuarios = await ctx.db.query("usuarios").collect();
|
const todosUsuarios = await ctx.db.query("usuarios").collect();
|
||||||
console.log("Total de usuários no banco:", todosUsuarios.length);
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("✅ Usuário encontrado:", usuarioAtual.nome);
|
console.log("✅ Usuário encontrado:", usuarioAtual.nome);
|
||||||
|
|
||||||
// Buscar fotoPerfil URL se existir
|
// Buscar fotoPerfil URL se existir
|
||||||
@@ -542,12 +578,13 @@ export const listarParaChat = query({
|
|||||||
*/
|
*/
|
||||||
export const uploadFotoPerfil = mutation({
|
export const uploadFotoPerfil = mutation({
|
||||||
args: {},
|
args: {},
|
||||||
|
returns: v.string(),
|
||||||
handler: async (ctx) => {
|
handler: async (ctx) => {
|
||||||
// TENTAR BETTER AUTH PRIMEIRO
|
// TENTAR BETTER AUTH PRIMEIRO
|
||||||
const identity = await ctx.auth.getUserIdentity();
|
const identity = await ctx.auth.getUserIdentity();
|
||||||
|
|
||||||
let usuarioAtual = null;
|
let usuarioAtual = null;
|
||||||
|
|
||||||
if (identity && identity.email) {
|
if (identity && identity.email) {
|
||||||
// Buscar por email (Better Auth)
|
// Buscar por email (Better Auth)
|
||||||
usuarioAtual = await ctx.db
|
usuarioAtual = await ctx.db
|
||||||
@@ -555,7 +592,7 @@ export const uploadFotoPerfil = mutation({
|
|||||||
.withIndex("by_email", (q) => q.eq("email", identity.email!))
|
.withIndex("by_email", (q) => q.eq("email", identity.email!))
|
||||||
.first();
|
.first();
|
||||||
}
|
}
|
||||||
|
|
||||||
// SE NÃO ENCONTROU, BUSCAR POR SESSÃO ATIVA (Sistema customizado)
|
// SE NÃO ENCONTROU, BUSCAR POR SESSÃO ATIVA (Sistema customizado)
|
||||||
if (!usuarioAtual) {
|
if (!usuarioAtual) {
|
||||||
const sessaoAtiva = await ctx.db
|
const sessaoAtiva = await ctx.db
|
||||||
@@ -563,7 +600,7 @@ export const uploadFotoPerfil = mutation({
|
|||||||
.filter((q) => q.eq(q.field("ativo"), true))
|
.filter((q) => q.eq(q.field("ativo"), true))
|
||||||
.order("desc")
|
.order("desc")
|
||||||
.first();
|
.first();
|
||||||
|
|
||||||
if (sessaoAtiva) {
|
if (sessaoAtiva) {
|
||||||
usuarioAtual = await ctx.db.get(sessaoAtiva.usuarioId);
|
usuarioAtual = await ctx.db.get(sessaoAtiva.usuarioId);
|
||||||
}
|
}
|
||||||
@@ -743,7 +780,8 @@ export const resetarSenhaUsuario = mutation({
|
|||||||
|
|
||||||
// Helper para gerar senha temporária
|
// Helper para gerar senha temporária
|
||||||
function gerarSenhaTemporaria(): string {
|
function gerarSenhaTemporaria(): string {
|
||||||
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@#$%";
|
const chars =
|
||||||
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@#$%";
|
||||||
let senha = "";
|
let senha = "";
|
||||||
for (let i = 0; i < 12; i++) {
|
for (let i = 0; i < 12; i++) {
|
||||||
senha += chars.charAt(Math.floor(Math.random() * chars.length));
|
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)
|
* Desativar usuário logicamente (soft delete - apenas TI_MASTER)
|
||||||
*/
|
*/
|
||||||
@@ -875,7 +1023,11 @@ export const criarUsuarioCompleto = mutation({
|
|||||||
enviarEmailBoasVindas: v.optional(v.boolean()),
|
enviarEmailBoasVindas: v.optional(v.boolean()),
|
||||||
},
|
},
|
||||||
returns: v.union(
|
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() })
|
v.object({ sucesso: v.literal(false), erro: v.string() })
|
||||||
),
|
),
|
||||||
handler: async (ctx, args) => {
|
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"
|
"typescript": "^5.9.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@convex-dev/better-auth": "^0.9.6",
|
|
||||||
"@dicebear/avataaars": "^9.2.4",
|
"@dicebear/avataaars": "^9.2.4",
|
||||||
"better-auth": "1.3.27",
|
|
||||||
"convex": "^1.28.0"
|
"convex": "^1.28.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user