refactor: remove authentication module and integrate Better Auth

- Deleted the `autenticacao.ts` file to streamline the authentication process.
- Updated the `auth.ts` file to include new functions for user management and password updates.
- Modified the schema to enforce the presence of `authId` for users, ensuring integration with Better Auth.
- Refactored the seed process to create users with Better Auth integration, enhancing user management capabilities.
- Cleaned up the `usuarios.ts` file to utilize the new authentication functions and improve code clarity.
This commit is contained in:
2025-11-07 23:33:09 -03:00
parent 427c78ec37
commit 3a32f5e4eb
6 changed files with 377 additions and 1350 deletions

View File

@@ -15,7 +15,6 @@ import type * as actions_smtp from "../actions/smtp.js";
import type * as actions_utils_nodeCrypto from "../actions/utils/nodeCrypto.js"; import type * as actions_utils_nodeCrypto from "../actions/utils/nodeCrypto.js";
import type * as atestadosLicencas from "../atestadosLicencas.js"; import type * as atestadosLicencas from "../atestadosLicencas.js";
import type * as ausencias from "../ausencias.js"; import type * as ausencias from "../ausencias.js";
import type * as autenticacao from "../autenticacao.js";
import type * as auth_utils from "../auth/utils.js"; import type * as auth_utils from "../auth/utils.js";
import type * as auth from "../auth.js"; import type * as auth from "../auth.js";
import type * as chat from "../chat.js"; import type * as chat from "../chat.js";
@@ -71,7 +70,6 @@ declare const fullApi: ApiFromModules<{
"actions/utils/nodeCrypto": typeof actions_utils_nodeCrypto; "actions/utils/nodeCrypto": typeof actions_utils_nodeCrypto;
atestadosLicencas: typeof atestadosLicencas; atestadosLicencas: typeof atestadosLicencas;
ausencias: typeof ausencias; ausencias: typeof ausencias;
autenticacao: typeof autenticacao;
"auth/utils": typeof auth_utils; "auth/utils": typeof auth_utils;
auth: typeof auth; auth: typeof auth;
chat: typeof chat; chat: typeof chat;

View File

@@ -1,834 +0,0 @@
import { v } from "convex/values";
import { mutation, query, internalMutation } from "./_generated/server";
import {
hashPassword,
verifyPassword,
generateToken,
validarMatricula,
validarSenha,
} from "./auth/utils";
import { registrarLogin } from "./logsLogin";
import { Id, Doc } from "./_generated/dataModel";
import type { QueryCtx, MutationCtx } from "./_generated/server";
/**
* Helper para verificar se usuário está bloqueado
*/
async function verificarBloqueioUsuario(ctx: QueryCtx, usuarioId: Id<"usuarios">) {
const bloqueio = await ctx.db
.query("bloqueiosUsuarios")
.withIndex("by_usuario", (q) => q.eq("usuarioId", usuarioId))
.filter((q) => q.eq(q.field("ativo"), true))
.first();
return bloqueio !== null;
}
/**
* Helper para verificar rate limiting por IP
*/
async function verificarRateLimitIP(ctx: QueryCtx, ipAddress: string) {
// Últimas 15 minutos
const dataLimite = Date.now() - 15 * 60 * 1000;
const tentativas = await ctx.db
.query("logsLogin")
.withIndex("by_ip", (q) => q.eq("ipAddress", ipAddress))
.filter((q) => q.gte(q.field("timestamp"), dataLimite))
.collect();
const falhas = tentativas.filter((t) => !t.sucesso).length;
// Bloquear se 5 ou mais tentativas falhas em 15 minutos
return falhas >= 5;
}
/**
* Login do usuário (aceita matrícula OU email)
*/
export const login = mutation({
args: {
matriculaOuEmail: v.string(), // Aceita matrícula ou email
senha: v.string(),
ipAddress: v.optional(v.string()),
userAgent: v.optional(v.string()),
},
returns: v.union(
v.object({
sucesso: v.literal(true),
token: v.string(),
usuario: v.object({
_id: v.id("usuarios"),
matricula: v.string(),
nome: v.string(),
email: v.string(),
funcionarioId: v.optional(v.id("funcionarios")),
role: v.object({
_id: v.id("roles"),
nome: v.string(),
nivel: v.number(),
setor: v.optional(v.string()),
}),
primeiroAcesso: v.boolean(),
}),
}),
v.object({
sucesso: v.literal(false),
erro: v.string(),
})
),
handler: async (ctx, args) => {
// Verificar rate limiting por IP
if (args.ipAddress) {
const ipBloqueado = await verificarRateLimitIP(ctx, args.ipAddress);
if (ipBloqueado) {
await registrarLogin(ctx, {
matriculaOuEmail: args.matriculaOuEmail,
sucesso: false,
motivoFalha: "rate_limit_excedido",
ipAddress: args.ipAddress,
userAgent: args.userAgent,
});
return {
sucesso: false as const,
erro: "Muitas tentativas de login. Tente novamente em 15 minutos.",
};
}
}
// Determinar se é email ou matrícula
const isEmail = args.matriculaOuEmail.includes("@");
// Buscar usuário
let usuario: Doc<"usuarios"> | null = null;
if (isEmail) {
usuario = await ctx.db
.query("usuarios")
.withIndex("by_email", (q) => q.eq("email", args.matriculaOuEmail))
.first();
} else {
const funcionario: Doc<"funcionarios"> | null = await ctx.db.query("funcionarios").withIndex("by_matricula", (q) => q.eq("matricula", args.matriculaOuEmail)).first();
if (funcionario) {
usuario = await ctx.db
.query("usuarios")
.withIndex("by_funcionarioId", (q) => q.eq("funcionarioId", funcionario._id))
.first();
}
}
if (!usuario) {
await registrarLogin(ctx, {
matriculaOuEmail: args.matriculaOuEmail,
sucesso: false,
motivoFalha: "usuario_inexistente",
ipAddress: args.ipAddress,
userAgent: args.userAgent,
});
return {
sucesso: false as const,
erro: "Credenciais incorretas.",
};
}
// Verificar se usuário está bloqueado
if (
usuario.bloqueado ||
(await verificarBloqueioUsuario(ctx, usuario._id))
) {
await registrarLogin(ctx, {
usuarioId: usuario._id,
matriculaOuEmail: args.matriculaOuEmail,
sucesso: false,
motivoFalha: "usuario_bloqueado",
ipAddress: args.ipAddress,
userAgent: args.userAgent,
});
return {
sucesso: false as const,
erro: "Usuário bloqueado. Entre em contato com o TI.",
};
}
// Verificar se usuário está ativo
if (!usuario.ativo) {
await registrarLogin(ctx, {
usuarioId: usuario._id,
matriculaOuEmail: args.matriculaOuEmail,
sucesso: false,
motivoFalha: "usuario_inativo",
ipAddress: args.ipAddress,
userAgent: args.userAgent,
});
return {
sucesso: false as const,
erro: "Usuário inativo. Entre em contato com o TI.",
};
}
// Verificar tentativas de login (bloqueio temporário)
const tentativasRecentes = usuario.tentativasLogin || 0;
const ultimaTentativa = usuario.ultimaTentativaLogin || 0;
const tempoDecorrido = Date.now() - ultimaTentativa;
const TEMPO_BLOQUEIO = 30 * 60 * 1000; // 30 minutos
// Se tentou 5 vezes e ainda não passou o tempo de bloqueio
if (tentativasRecentes >= 5 && tempoDecorrido < TEMPO_BLOQUEIO) {
await registrarLogin(ctx, {
usuarioId: usuario._id,
matriculaOuEmail: args.matriculaOuEmail,
sucesso: false,
motivoFalha: "bloqueio_temporario",
ipAddress: args.ipAddress,
userAgent: args.userAgent,
});
const minutosRestantes = Math.ceil(
(TEMPO_BLOQUEIO - tempoDecorrido) / 60000
);
return {
sucesso: false as const,
erro: `Conta temporariamente bloqueada. Tente novamente em ${minutosRestantes} minutos.`,
};
}
// Resetar tentativas se passou o tempo de bloqueio
if (tempoDecorrido > TEMPO_BLOQUEIO) {
await ctx.db.patch(usuario._id, {
tentativasLogin: 0,
ultimaTentativaLogin: Date.now(),
});
}
// Verificar senha
const senhaValida = await verifyPassword(args.senha, usuario.senhaHash);
if (!senhaValida) {
// Incrementar tentativas
const novasTentativas =
tempoDecorrido > TEMPO_BLOQUEIO ? 1 : tentativasRecentes + 1;
await ctx.db.patch(usuario._id, {
tentativasLogin: novasTentativas,
ultimaTentativaLogin: Date.now(),
});
await registrarLogin(ctx, {
usuarioId: usuario._id,
matriculaOuEmail: args.matriculaOuEmail,
sucesso: false,
motivoFalha: "senha_incorreta",
ipAddress: args.ipAddress,
userAgent: args.userAgent,
});
const tentativasRestantes = 5 - novasTentativas;
if (tentativasRestantes > 0) {
return {
sucesso: false as const,
erro: `Credenciais incorretas. ${tentativasRestantes} tentativas restantes.`,
};
} else {
return {
sucesso: false as const,
erro: "Conta bloqueada por 30 minutos devido a múltiplas tentativas falhas.",
};
}
}
// Login bem-sucedido! Resetar tentativas
await ctx.db.patch(usuario._id, {
tentativasLogin: 0,
ultimaTentativaLogin: undefined,
});
// Buscar role do usuário
const role: Doc<"roles"> | null = await ctx.db.get(usuario.roleId);
if (!role) {
return {
sucesso: false as const,
erro: "Erro ao carregar permissões do usuário.",
};
}
// Gerar token de sessão
const token = generateToken();
const agora = Date.now();
const expiraEm = agora + 8 * 60 * 60 * 1000; // 8 horas
// Criar sessão
await ctx.db.insert("sessoes", {
usuarioId: usuario._id,
token,
ipAddress: args.ipAddress,
userAgent: args.userAgent,
criadoEm: agora,
expiraEm,
ativo: true,
});
// Atualizar último acesso
await ctx.db.patch(usuario._id, {
ultimoAcesso: agora,
atualizadoEm: agora,
});
// Log de login bem-sucedido
await registrarLogin(ctx, {
usuarioId: usuario._id,
matriculaOuEmail: args.matriculaOuEmail,
sucesso: true,
ipAddress: args.ipAddress,
userAgent: args.userAgent,
});
await ctx.db.insert("logsAcesso", {
usuarioId: usuario._id,
tipo: "login",
ipAddress: args.ipAddress,
userAgent: args.userAgent,
detalhes: "Login realizado com sucesso",
timestamp: agora,
});
// Obter matrícula do funcionário se houver
let matricula: string | undefined = undefined;
if (usuario.funcionarioId) {
const funcionario = await ctx.db.get(usuario.funcionarioId);
matricula = funcionario?.matricula;
}
return {
sucesso: true as const,
token,
usuario: {
_id: usuario._id,
matricula: matricula || "",
nome: usuario.nome,
email: usuario.email,
funcionarioId: usuario.funcionarioId,
role: {
_id: role._id,
nome: role.nome,
nivel: role.nivel,
setor: role.setor,
},
primeiroAcesso: usuario.primeiroAcesso,
},
};
},
});
/**
* Mutation interna para login via HTTP (com IP extraído do request)
* Usada pelo endpoint HTTP /api/login
*/
export const loginComIP = internalMutation({
args: {
matriculaOuEmail: v.string(),
senha: v.string(),
ipAddress: v.optional(v.string()),
userAgent: v.optional(v.string()),
},
returns: v.union(
v.object({
sucesso: v.literal(true),
token: v.string(),
usuario: v.object({
_id: v.id("usuarios"),
matricula: v.string(),
nome: v.string(),
email: v.string(),
funcionarioId: v.optional(v.id("funcionarios")),
role: v.object({
_id: v.id("roles"),
nome: v.string(),
nivel: v.number(),
setor: v.optional(v.string()),
}),
primeiroAcesso: v.boolean(),
}),
}),
v.object({
sucesso: v.literal(false),
erro: v.string(),
})
),
handler: async (ctx, args) => {
// Reutilizar a mesma lógica da mutation pública
// Verificar rate limiting por IP
if (args.ipAddress) {
const ipBloqueado = await verificarRateLimitIP(ctx, args.ipAddress);
if (ipBloqueado) {
await registrarLogin(ctx, {
matriculaOuEmail: args.matriculaOuEmail,
sucesso: false,
motivoFalha: "rate_limit_excedido",
ipAddress: args.ipAddress,
userAgent: args.userAgent,
});
return {
sucesso: false as const,
erro: "Muitas tentativas de login. Tente novamente em 15 minutos.",
};
}
}
// Determinar se é email ou matrícula
const isEmail = args.matriculaOuEmail.includes("@");
// Buscar usuário
let usuario: Doc<"usuarios"> | null = null;
if (isEmail) {
usuario = await ctx.db
.query("usuarios")
.withIndex("by_email", (q) => q.eq("email", args.matriculaOuEmail))
.first();
} else {
const funcionario: Doc<"funcionarios"> | null = await ctx.db.query("funcionarios").withIndex("by_matricula", (q) => q.eq("matricula", args.matriculaOuEmail)).first();
if (funcionario) {
usuario = await ctx.db
.query("usuarios")
.withIndex("by_funcionarioId", (q) => q.eq("funcionarioId", funcionario._id))
.first();
}
}
if (!usuario) {
await registrarLogin(ctx, {
matriculaOuEmail: args.matriculaOuEmail,
sucesso: false,
motivoFalha: "usuario_inexistente",
ipAddress: args.ipAddress,
userAgent: args.userAgent,
});
return {
sucesso: false as const,
erro: "Credenciais incorretas.",
};
}
// Verificar se usuário está bloqueado
if (
usuario.bloqueado ||
(await verificarBloqueioUsuario(ctx, usuario._id))
) {
await registrarLogin(ctx, {
usuarioId: usuario._id,
matriculaOuEmail: args.matriculaOuEmail,
sucesso: false,
motivoFalha: "usuario_bloqueado",
ipAddress: args.ipAddress,
userAgent: args.userAgent,
});
return {
sucesso: false as const,
erro: "Usuário bloqueado. Entre em contato com o TI.",
};
}
// Verificar se usuário está ativo
if (!usuario.ativo) {
await registrarLogin(ctx, {
usuarioId: usuario._id,
matriculaOuEmail: args.matriculaOuEmail,
sucesso: false,
motivoFalha: "usuario_inativo",
ipAddress: args.ipAddress,
userAgent: args.userAgent,
});
return {
sucesso: false as const,
erro: "Usuário inativo. Entre em contato com o TI.",
};
}
// Verificar tentativas de login (bloqueio temporário)
const tentativasRecentes = usuario.tentativasLogin || 0;
const ultimaTentativa = usuario.ultimaTentativaLogin || 0;
const tempoDecorrido = Date.now() - ultimaTentativa;
const TEMPO_BLOQUEIO = 30 * 60 * 1000; // 30 minutos
// Se tentou 5 vezes e ainda não passou o tempo de bloqueio
if (tentativasRecentes >= 5 && tempoDecorrido < TEMPO_BLOQUEIO) {
await registrarLogin(ctx, {
usuarioId: usuario._id,
matriculaOuEmail: args.matriculaOuEmail,
sucesso: false,
motivoFalha: "bloqueio_temporario",
ipAddress: args.ipAddress,
userAgent: args.userAgent,
});
const minutosRestantes = Math.ceil(
(TEMPO_BLOQUEIO - tempoDecorrido) / 60000
);
return {
sucesso: false as const,
erro: `Conta temporariamente bloqueada. Tente novamente em ${minutosRestantes} minutos.`,
};
}
// Resetar tentativas se passou o tempo de bloqueio
if (tempoDecorrido > TEMPO_BLOQUEIO) {
await ctx.db.patch(usuario._id, {
tentativasLogin: 0,
ultimaTentativaLogin: Date.now(),
});
}
// Verificar senha
const senhaValida = await verifyPassword(args.senha, usuario.senhaHash);
if (!senhaValida) {
// Incrementar tentativas
const novasTentativas =
tempoDecorrido > TEMPO_BLOQUEIO ? 1 : tentativasRecentes + 1;
await ctx.db.patch(usuario._id, {
tentativasLogin: novasTentativas,
ultimaTentativaLogin: Date.now(),
});
await registrarLogin(ctx, {
usuarioId: usuario._id,
matriculaOuEmail: args.matriculaOuEmail,
sucesso: false,
motivoFalha: "senha_incorreta",
ipAddress: args.ipAddress,
userAgent: args.userAgent,
});
const tentativasRestantes = 5 - novasTentativas;
if (tentativasRestantes > 0) {
return {
sucesso: false as const,
erro: `Credenciais incorretas. ${tentativasRestantes} tentativas restantes.`,
};
} else {
return {
sucesso: false as const,
erro: "Conta bloqueada por 30 minutos devido a múltiplas tentativas falhas.",
};
}
}
// Login bem-sucedido! Resetar tentativas
await ctx.db.patch(usuario._id, {
tentativasLogin: 0,
ultimaTentativaLogin: undefined,
});
// Buscar role do usuário
const role: Doc<"roles"> | null = await ctx.db.get(usuario.roleId);
if (!role) {
return {
sucesso: false as const,
erro: "Erro ao carregar permissões do usuário.",
};
}
// Gerar token de sessão
const token = generateToken();
const agora = Date.now();
const expiraEm = agora + 8 * 60 * 60 * 1000; // 8 horas
// Criar sessão
await ctx.db.insert("sessoes", {
usuarioId: usuario._id,
token,
ipAddress: args.ipAddress,
userAgent: args.userAgent,
criadoEm: agora,
expiraEm,
ativo: true,
});
// Atualizar último acesso
await ctx.db.patch(usuario._id, {
ultimoAcesso: agora,
atualizadoEm: agora,
});
// Log de login bem-sucedido
await registrarLogin(ctx, {
usuarioId: usuario._id,
matriculaOuEmail: args.matriculaOuEmail,
sucesso: true,
ipAddress: args.ipAddress,
userAgent: args.userAgent,
});
await ctx.db.insert("logsAcesso", {
usuarioId: usuario._id,
tipo: "login",
ipAddress: args.ipAddress,
userAgent: args.userAgent,
detalhes: "Login realizado com sucesso",
timestamp: agora,
});
// Obter matrícula do funcionário se houver
let matricula: string | undefined = undefined;
if (usuario.funcionarioId) {
const funcionario = await ctx.db.get(usuario.funcionarioId);
matricula = funcionario?.matricula;
}
return {
sucesso: true as const,
token,
usuario: {
_id: usuario._id,
matricula: matricula || "",
nome: usuario.nome,
email: usuario.email,
funcionarioId: usuario.funcionarioId,
role: {
_id: role._id,
nome: role.nome,
nivel: role.nivel,
setor: role.setor,
},
primeiroAcesso: usuario.primeiroAcesso,
},
};
},
});
/**
* Logout do usuário
*/
export const logout = mutation({
args: {
token: v.string(),
},
returns: v.null(),
handler: async (ctx, args) => {
// Buscar sessão
const sessao = await ctx.db
.query("sessoes")
.withIndex("by_token", (q) => q.eq("token", args.token))
.first();
if (sessao) {
// Desativar sessão
await ctx.db.patch(sessao._id, {
ativo: false,
});
// Log de logout
await ctx.db.insert("logsAcesso", {
usuarioId: sessao.usuarioId,
tipo: "logout",
timestamp: Date.now(),
});
}
return null;
},
});
/**
* Verificar se token é válido e retornar usuário
*/
export const verificarSessao = query({
args: {
token: v.string(),
},
returns: v.union(
v.object({
valido: v.literal(true),
usuario: v.object({
_id: v.id("usuarios"),
matricula: v.string(),
nome: v.string(),
email: v.string(),
funcionarioId: v.optional(v.id("funcionarios")),
role: v.object({
_id: v.id("roles"),
nome: v.string(),
nivel: v.number(),
setor: v.optional(v.string()),
}),
primeiroAcesso: v.boolean(),
}),
}),
v.object({
valido: v.literal(false),
motivo: v.optional(v.string()),
})
),
handler: async (ctx, args) => {
// Buscar sessão
const sessao: Doc<"sessoes"> | null = await ctx.db
.query("sessoes")
.withIndex("by_token", (q) => q.eq("token", args.token))
.first();
if (!sessao || !sessao.ativo) {
return {
valido: false as const,
motivo: "Sessão não encontrada ou inativa",
};
}
// Verificar se sessão expirou
if (sessao.expiraEm < Date.now()) {
// Não podemos fazer patch/insert em uma query
// A expiração será tratada por uma mutation separada
return { valido: false as const, motivo: "Sessão expirada" };
}
// Buscar usuário
const usuario: Doc<"usuarios"> | null = await ctx.db.get(sessao.usuarioId);
if (!usuario || !usuario.ativo) {
return {
valido: false as const,
motivo: "Usuário não encontrado ou inativo",
};
}
// Buscar role
const role: Doc<"roles"> | null = await ctx.db.get(usuario.roleId);
if (!role) {
return { valido: false as const, motivo: "Role não encontrada" };
}
// Obter matrícula do funcionário se houver
let matricula: string | undefined = undefined;
if (usuario.funcionarioId) {
const funcionario = await ctx.db.get(usuario.funcionarioId);
matricula = funcionario?.matricula;
}
return {
valido: true as const,
usuario: {
_id: usuario._id,
matricula: matricula || "",
nome: usuario.nome,
email: usuario.email,
funcionarioId: usuario.funcionarioId,
role: {
_id: role._id,
nome: role.nome,
nivel: role.nivel,
setor: role.setor,
},
primeiroAcesso: usuario.primeiroAcesso,
},
};
},
});
/**
* Limpar sessões expiradas (chamada periodicamente)
*/
export const limparSessoesExpiradas = mutation({
args: {},
returns: v.object({
sessoesLimpas: v.number(),
}),
handler: async (ctx) => {
const agora = Date.now();
const sessoes = await ctx.db
.query("sessoes")
.withIndex("by_ativo", (q) => q.eq("ativo", true))
.collect();
let sessoesLimpas = 0;
for (const sessao of sessoes) {
if (sessao.expiraEm < agora) {
await ctx.db.patch(sessao._id, { ativo: false });
await ctx.db.insert("logsAcesso", {
usuarioId: sessao.usuarioId,
tipo: "sessao_expirada",
timestamp: agora,
});
sessoesLimpas++;
}
}
return { sessoesLimpas };
},
});
/**
* Alterar senha (primeiro acesso ou reset)
*/
export const alterarSenha = mutation({
args: {
token: v.string(),
senhaAtual: v.optional(v.string()),
novaSenha: v.string(),
},
returns: v.union(
v.object({ sucesso: v.literal(true) }),
v.object({ sucesso: v.literal(false), erro: v.string() })
),
handler: async (ctx, args) => {
// Verificar sessão
const sessao: Doc<"sessoes"> | null = await ctx.db
.query("sessoes")
.withIndex("by_token", (q) => q.eq("token", args.token))
.first();
if (!sessao || !sessao.ativo) {
return { sucesso: false as const, erro: "Sessão inválida" };
}
const usuario: Doc<"usuarios"> | null = await ctx.db.get(sessao.usuarioId);
if (!usuario) {
return { sucesso: false as const, erro: "Usuário não encontrado" };
}
// Se não for primeiro acesso, verificar senha atual
if (!usuario.primeiroAcesso && args.senhaAtual) {
const senhaAtualValida = await verifyPassword(
args.senhaAtual,
usuario.senhaHash
);
if (!senhaAtualValida) {
return { sucesso: false as const, erro: "Senha atual incorreta" };
}
}
// Validar nova senha
if (!validarSenha(args.novaSenha)) {
return {
sucesso: false as const,
erro: "Senha deve ter no mínimo 8 caracteres, incluindo letras, números e símbolos",
};
}
// Gerar hash da nova senha
const novoHash = await hashPassword(args.novaSenha);
// Atualizar senha
await ctx.db.patch(usuario._id, {
senhaHash: novoHash,
primeiroAcesso: false,
atualizadoEm: Date.now(),
});
// Log
await ctx.db.insert("logsAcesso", {
usuarioId: usuario._id,
tipo: "senha_alterada",
timestamp: Date.now(),
});
return { sucesso: true as const };
},
});

View File

@@ -2,8 +2,9 @@ import { createClient, type GenericCtx } from "@convex-dev/better-auth";
import { convex } from "@convex-dev/better-auth/plugins"; import { convex } from "@convex-dev/better-auth/plugins";
import { components } from "./_generated/api"; import { components } from "./_generated/api";
import { type DataModel } from "./_generated/dataModel"; import { type DataModel } from "./_generated/dataModel";
import { query } from "./_generated/server"; import { mutation, MutationCtx, query, QueryCtx } from "./_generated/server";
import { betterAuth } from "better-auth"; import { betterAuth } from "better-auth";
import { v } from "convex/values";
const siteUrl = process.env.SITE_URL!; const siteUrl = process.env.SITE_URL!;
@@ -55,3 +56,52 @@ export const getCurrentUser = query({
return user; return user;
}, },
}); });
export const getCurrentUserFunction = async (ctx: QueryCtx | MutationCtx) => {
const authUser = await authComponent.safeGetAuthUser(ctx as any);
if (!authUser) {
return;
}
const user = await ctx.db
.query("usuarios")
.withIndex("authId", (q) => q.eq("authId", authUser._id))
.unique();
if (!user) {
return;
}
return user;
};
export const createAuthUser = async (
ctx: MutationCtx,
args: { nome: string; email: string; password: string }
) => {
const { auth, headers } = await authComponent.getAuth(createAuth, ctx as any);
const result = await auth.api.signUpEmail({
headers,
body: {
name: args.nome,
email: args.email,
password: args.password,
},
});
return result.user.id;
};
export const updatePassword = async (
ctx: MutationCtx,
args: { newPassword: string; currentPassword: string }
) => {
const { auth, headers } = await authComponent.getAuth(createAuth, ctx as any);
await auth.api.changePassword({
headers,
body: {
currentPassword: args.currentPassword,
newPassword: args.newPassword,
},
});
};

View File

@@ -381,8 +381,7 @@ export default defineSchema({
// Sistema de Autenticação e Controle de Acesso // Sistema de Autenticação e Controle de Acesso
usuarios: defineTable({ usuarios: defineTable({
authId: v.optional(v.string()), authId: v.string(),
senhaHash: v.string(), // Senha criptografada com bcrypt
nome: v.string(), nome: v.string(),
email: v.string(), email: v.string(),
funcionarioId: v.optional(v.id("funcionarios")), funcionarioId: v.optional(v.id("funcionarios")),

View File

@@ -1,8 +1,15 @@
import { internalMutation, mutation, query } from "./_generated/server"; import {
action,
internalAction,
internalMutation,
mutation,
query,
} from "./_generated/server";
import { internal } from "./_generated/api"; import { internal } from "./_generated/api";
import { v } from "convex/values"; import { v } from "convex/values";
import { hashPassword } from "./auth/utils"; import { hashPassword } from "./auth/utils";
import { Id } from "./_generated/dataModel"; import { Id } from "./_generated/dataModel";
import { createAuthUser } from "./auth";
// Dados exportados do Convex Cloud // Dados exportados do Convex Cloud
const simbolosData = [ const simbolosData = [
@@ -187,83 +194,63 @@ const solicitacoesAcessoData = [
/** /**
* Seed inicial do banco de dados com os dados exportados do Convex Cloud * Seed inicial do banco de dados com os dados exportados do Convex Cloud
*/ */
export const seedDatabase = internalMutation({ export const seedCreateRoles = internalMutation({
args: {}, args: {},
returns: v.null(), returns: v.null(),
handler: async (ctx) => { handler: async (ctx) => {
console.log("🌱 Iniciando seed do banco de dados...");
// 1. Criar Roles (Perfis de Acesso)
console.log("🔐 Criando roles..."); console.log("🔐 Criando roles...");
// TI_MASTER - Nível 0 - Acesso total irrestrito const ensureRole = async (
const roleTIMaster = await ctx.db.insert("roles", { nome: string,
nome: "ti_master", descricao: string,
descricao: "TI Master", nivel: number,
nivel: 0, setor?: string,
setor: "ti", editavel?: boolean
) => {
const existing = await ctx.db
.query("roles")
.withIndex("by_nome", (q) => q.eq("nome", nome))
.first();
if (existing) {
console.log(` Role já existe: ${nome}`);
return existing._id;
}
const id = await ctx.db.insert("roles", {
nome,
descricao,
nivel,
setor,
customizado: false, customizado: false,
editavel: false, editavel,
}); });
console.log(" ✅ Role criada: ti_master (Nível 0 - Acesso Total)"); console.log(` ✅ Role criada: ${nome}`);
return id;
};
// ADMIN - Nível 2 - Permissões configuráveis await ensureRole("ti_master", "TI Master", 0, "ti", false);
const roleAdmin = await ctx.db.insert("roles", { await ensureRole("admin", "Administrador Geral", 2, "administrativo", true);
nome: "admin", await ensureRole("ti_usuario", "TI Usuário", 2, "ti", true);
descricao: "Administrador Geral", await ensureRole("rh", "Recursos Humanos", 2, "recursos_humanos", false);
nivel: 2, await ensureRole("financeiro", "Financeiro", 2, "financeiro", false);
setor: "administrativo", await ensureRole("usuario", "Usuário Padrão", 3, undefined, false);
customizado: false, // Encadeia próxima etapa
editavel: true, // Permissões configuráveis await ctx.scheduler.runAfter(0, internal.seed.seedCreateSimbolos, {});
return null;
},
}); });
console.log(" ✅ Role criada: admin (Nível 2 - Configurável)");
// TI_USUARIO - Nível 2 - Suporte técnico export const seedCreateSimbolos = internalMutation({
const roleTIUsuario = await ctx.db.insert("roles", { args: {},
nome: "ti_usuario", returns: v.null(),
descricao: "TI Usuário", handler: async (ctx) => {
nivel: 2,
setor: "ti",
customizado: false,
editavel: true,
});
console.log(" ✅ Role criada: ti_usuario (Nível 2 - Suporte)");
const roleRH = await ctx.db.insert("roles", {
nome: "rh",
descricao: "Recursos Humanos",
nivel: 2,
setor: "recursos_humanos",
customizado: false,
editavel: false,
});
console.log(" ✅ Role criada: rh");
const roleFinanceiro = await ctx.db.insert("roles", {
nome: "financeiro",
descricao: "Financeiro",
nivel: 2,
setor: "financeiro",
customizado: false,
editavel: false,
});
console.log(" ✅ Role criada: financeiro");
const roleUsuario = await ctx.db.insert("roles", {
nome: "usuario",
descricao: "Usuário Padrão",
nivel: 3,
setor: undefined,
customizado: false,
editavel: false,
});
console.log(" ✅ Role criada: usuario (Nível 3 - Padrão)");
// 2. Criar Símbolos (Cargos)
console.log("💰 Criando símbolos..."); console.log("💰 Criando símbolos...");
const simbolosMap = new Map<string, Id<"simbolos">>(); const existentes = await ctx.db.query("simbolos").collect();
const nomesExistentes = new Set(existentes.map((s) => s.nome));
for (const simbolo of simbolosData) { for (const simbolo of simbolosData) {
const simboloId = await ctx.db.insert("simbolos", { if (nomesExistentes.has(simbolo.nome)) {
console.log(` Símbolo já existe: ${simbolo.nome}`);
continue;
}
await ctx.db.insert("simbolos", {
nome: simbolo.nome, nome: simbolo.nome,
descricao: simbolo.descricao, descricao: simbolo.descricao,
tipo: simbolo.tipo, tipo: simbolo.tipo,
@@ -271,24 +258,43 @@ export const seedDatabase = internalMutation({
repValor: simbolo.repValor || "", repValor: simbolo.repValor || "",
vencValor: simbolo.vencValor || "", vencValor: simbolo.vencValor || "",
}); });
simbolosMap.set(simbolo.nome, simboloId);
console.log(` ✅ Símbolo criado: ${simbolo.nome}`); console.log(` ✅ Símbolo criado: ${simbolo.nome}`);
} }
// Encadeia próxima etapa
await ctx.scheduler.runAfter(0, internal.seed.seedCreateFuncionarios, {});
return null;
},
});
// 3. Criar Funcionários export const seedCreateFuncionarios = internalMutation({
args: {},
returns: v.null(),
handler: async (ctx) => {
console.log("👥 Criando funcionários..."); console.log("👥 Criando funcionários...");
const funcionariosMap = new Map<string, Id<"funcionarios">>(); const simbolos = await ctx.db.query("simbolos").collect();
const simbolosMap = new Map<string, Id<"simbolos">>();
for (const s of simbolos) {
simbolosMap.set(s.nome, s._id as Id<"simbolos">);
}
for (const funcionario of funcionariosData) { for (const funcionario of funcionariosData) {
const simboloId = simbolosMap.get(funcionario.simboloNome); // Evitar duplicar por CPF
if (!simboloId) { const existente = await ctx.db
.query("funcionarios")
.withIndex("by_cpf", (q) => q.eq("cpf", funcionario.cpf))
.first();
if (existente) {
console.log( console.log(
` ❌ Símbolo não encontrado: ${funcionario.simboloNome}` ` Funcionário já existe (CPF ${funcionario.cpf}): ${existente.nome}`
); );
continue; continue;
} }
const simboloId = simbolosMap.get(funcionario.simboloNome);
const funcId = await ctx.db.insert("funcionarios", { if (!simboloId) {
console.log(` ❌ Símbolo não encontrado: ${funcionario.simboloNome}`);
continue;
}
await ctx.db.insert("funcionarios", {
admissaoData: funcionario.admissaoData, admissaoData: funcionario.admissaoData,
cep: funcionario.cep, cep: funcionario.cep,
cidade: funcionario.cidade, cidade: funcionario.cidade,
@@ -304,36 +310,126 @@ export const seedDatabase = internalMutation({
telefone: funcionario.telefone, telefone: funcionario.telefone,
uf: funcionario.uf, uf: funcionario.uf,
}); });
funcionariosMap.set(funcionario.matricula, funcId);
console.log(` ✅ Funcionário criado: ${funcionario.nome}`); console.log(` ✅ Funcionário criado: ${funcionario.nome}`);
} }
// Encadeia próxima etapa
await ctx.scheduler.runAfter(
0,
internal.seed.seedCreateUsuariosParaFuncionarios,
{}
);
return null;
},
});
// 5. Criar usuários para os funcionários export const seedCreateUsuariosParaFuncionarios = internalMutation({
args: {},
returns: v.null(),
handler: async (ctx) => {
console.log("👤 Criando usuários para funcionários..."); console.log("👤 Criando usuários para funcionários...");
// Agenda criação por funcionário para evitar timeout
let delay = 0;
for (const funcionario of funcionariosData) { for (const funcionario of funcionariosData) {
const funcId = funcionariosMap.get(funcionario.matricula); await ctx.scheduler.runAfter(
if (!funcId) continue; delay,
internal.seed.seedCreateUsuarioParaFuncionario,
const senhaInicial = await hashPassword("Mudar@123"); {
await ctx.db.insert("usuarios", { matricula: funcionario.matricula,
senhaHash: senhaInicial,
nome: funcionario.nome, nome: funcionario.nome,
email: funcionario.email, email: funcionario.email,
funcionarioId: funcId as Id<"funcionarios">, }
roleId: roleUsuario, );
delay += 50;
}
// Agenda próxima etapa após as criações individuais
await ctx.scheduler.runAfter(
delay + 300,
internal.seed.seedInserirSolicitacoesAcesso,
{}
);
return null;
},
});
export const seedCreateUsuarioParaFuncionario = internalMutation({
args: {
matricula: v.string(),
nome: v.string(),
email: v.string(),
},
returns: v.null(),
handler: async (ctx, args) => {
// Role "usuario"
const roleUsuario = await ctx.db
.query("roles")
.withIndex("by_nome", (q) => q.eq("nome", "usuario"))
.first();
if (!roleUsuario) {
console.log(' ❌ Role "usuario" não encontrada');
return null;
}
const funcionarioDoc = await ctx.db
.query("funcionarios")
.withIndex("by_matricula", (q) => q.eq("matricula", args.matricula))
.first();
if (!funcionarioDoc) {
console.log(
` ❌ Funcionário não encontrado pela matrícula: ${args.matricula}`
);
return null;
}
const usuarioExistente = await ctx.db
.query("usuarios")
.withIndex("by_email", (q) => q.eq("email", args.email))
.first();
if (usuarioExistente) {
console.log(` Usuário já existe para ${args.email}`);
return null;
}
const authUserId = await createAuthUser(ctx, {
nome: args.nome,
email: args.email,
password: "Mudar@123",
});
await ctx.db.insert("usuarios", {
authId: authUserId,
nome: args.nome,
email: args.email,
funcionarioId: funcionarioDoc._id as Id<"funcionarios">,
roleId: roleUsuario._id,
ativo: true, ativo: true,
primeiroAcesso: true, primeiroAcesso: true,
criadoEm: Date.now(), criadoEm: Date.now(),
atualizadoEm: Date.now(), atualizadoEm: Date.now(),
}); });
console.log( console.log(` ✅ Usuário criado: ${args.nome} (senha: Mudar@123)`);
` ✅ Usuário criado: ${funcionario.nome} (senha: Mudar@123)` return null;
); },
} });
// 6. Inserir solicitações de acesso export const seedInserirSolicitacoesAcesso = internalMutation({
args: {},
returns: v.null(),
handler: async (ctx) => {
console.log("📋 Inserindo solicitações de acesso..."); console.log("📋 Inserindo solicitações de acesso...");
for (const solicitacao of solicitacoesAcessoData) { for (const solicitacao of solicitacoesAcessoData) {
// Evitar duplicidade por matrícula
const existente = await ctx.db
.query("solicitacoesAcesso")
.withIndex("by_matricula", (q) =>
q.eq("matricula", solicitacao.matricula)
)
.first();
if (existente) {
console.log(
` Solicitação já existe p/ matrícula ${solicitacao.matricula}`
);
continue;
}
const dadosSolicitacao: { const dadosSolicitacao: {
nome: string; nome: string;
matricula: string; matricula: string;
@@ -365,10 +461,24 @@ export const seedDatabase = internalMutation({
` ✅ Solicitação criada: ${solicitacao.nome} (${solicitacao.status})` ` ✅ Solicitação criada: ${solicitacao.nome} (${solicitacao.status})`
); );
} }
console.log("✨ Seed concluído!");
console.log("✨ Seed do banco de dados concluído com sucesso!");
return null; return null;
} },
});
export const seedDatabase = internalAction({
args: {},
returns: v.null(),
handler: async (ctx) => {
console.log("🌱 Iniciando seed do banco de dados (action)...");
await ctx.runMutation(internal.seed.seedCreateRoles, {});
await ctx.runMutation(internal.seed.seedCreateSimbolos, {});
await ctx.runMutation(internal.seed.seedCreateFuncionarios, {});
await ctx.runMutation(internal.seed.seedCreateUsuariosParaFuncionarios, {});
await ctx.runMutation(internal.seed.seedInserirSolicitacoesAcesso, {});
console.log("✨ Seed do banco de dados concluído com sucesso pela action!");
return null;
},
}); });
/** /**
@@ -379,10 +489,12 @@ export const popularBanco = mutation({
args: {}, args: {},
returns: v.null(), returns: v.null(),
handler: async (ctx) => { handler: async (ctx) => {
console.log("🌱 Executando popularBanco (wrapper público para seedDatabase)..."); console.log(
// Chama a internalMutation para reaproveitar a lógica de seed "🌱 Executando popularBanco (wrapper público para seedDatabase)..."
await ctx.runMutation(internal.seed.seedDatabase, {}); );
console.log("✅ Seed concluído pelo wrapper público"); // Agenda apenas a primeira etapa; as demais serão encadeadas internamente
await ctx.scheduler.runAfter(0, internal.seed.seedCreateRoles, {});
console.log("✅ Seed iniciado (etapa 1 agendada)");
return null; return null;
}, },
}); });
@@ -948,7 +1060,8 @@ export const verificarBanco = query({
funcionarios: funcionarios.length, funcionarios: funcionarios.length,
roles: roles.length, roles: roles.length,
simbolos: simbolos.length, simbolos: simbolos.length,
total: usuarios.length + funcionarios.length + roles.length + simbolos.length, total:
usuarios.length + funcionarios.length + roles.length + simbolos.length,
}; };
}, },
}); });

View File

@@ -1,10 +1,10 @@
import { v } from "convex/values"; import { v } from "convex/values";
import { mutation, query } from "./_generated/server"; import { mutation, query } from "./_generated/server";
import { hashPassword, generateToken } from "./auth/utils"; import { hashPassword } from "./auth/utils";
import { registrarAtividade } from "./logsAtividades"; import { registrarAtividade } from "./logsAtividades";
import { Id, Doc } from "./_generated/dataModel"; import { Id, Doc } from "./_generated/dataModel";
import { api } from "./_generated/api";
import type { QueryCtx, MutationCtx } from "./_generated/server"; import type { QueryCtx, MutationCtx } from "./_generated/server";
import { createAuthUser, getCurrentUserFunction } from "./auth";
/** /**
* Helper para obter a matrícula do usuário (do funcionário se houver) * Helper para obter a matrícula do usuário (do funcionário se houver)
@@ -20,33 +20,6 @@ async function obterMatriculaUsuario(
return undefined; return undefined;
} }
/**
* Helper para obter usuário autenticado (Better Auth ou Sessão)
* Usa a mesma lógica do obterPerfil para garantir consistência
*/
async function getUsuarioAutenticado(ctx: QueryCtx | MutationCtx) {
// FASE 1 IMPLEMENTADA: Usa Custom Auth Provider configurado no convex.config.ts
// O provider busca sessão por token específico (seguro) ou Better Auth
const identity = await ctx.auth.getUserIdentity();
let usuarioAtual = null;
if (identity && identity.email) {
usuarioAtual = await ctx.db
.query("usuarios")
.withIndex("by_email", (q) => q.eq("email", identity.email!))
.first();
}
if (!usuarioAtual && identity) {
console.error("⚠️ [getUsuarioAutenticado] Identity encontrada mas usuário não encontrado no banco:", {
email: identity.email,
subject: identity.subject
});
}
return usuarioAtual;
}
/** /**
* Associar funcionário a um usuário * Associar funcionário a um usuário
*/ */
@@ -132,12 +105,17 @@ export const criar = mutation({
return { sucesso: false as const, erro: "E-mail já cadastrado" }; return { sucesso: false as const, erro: "E-mail já cadastrado" };
} }
// Gerar hash da senha inicial const senhaTemporaria = args.senhaInicial;
const senhaHash = await hashPassword(args.senhaInicial);
const authUserId = await createAuthUser(ctx, {
nome: args.nome,
email: args.email,
password: senhaTemporaria,
});
// Criar usuário // Criar usuário
const usuarioId = await ctx.db.insert("usuarios", { const usuarioId = await ctx.db.insert("usuarios", {
senhaHash, authId: authUserId,
nome: args.nome, nome: args.nome,
email: args.email, email: args.email,
funcionarioId: args.funcionarioId, funcionarioId: args.funcionarioId,
@@ -161,69 +139,6 @@ export const listar = query({
matricula: v.optional(v.string()), matricula: v.optional(v.string()),
ativo: v.optional(v.boolean()), ativo: v.optional(v.boolean()),
}, },
// returns: v.array(
// v.object({
// _id: v.id("usuarios"),
// matricula: v.string(),
// nome: v.string(),
// email: v.string(),
// ativo: v.boolean(),
// bloqueado: v.optional(v.boolean()),
// motivoBloqueio: v.optional(v.string()),
// primeiroAcesso: v.boolean(),
// ultimoAcesso: v.optional(v.number()),
// criadoEm: v.number(),
// role: v.union(
// v.object({
// _id: v.id("roles"),
// _creationTime: v.optional(v.number()),
// criadoPor: v.optional(v.id("usuarios")),
// customizado: v.optional(v.boolean()),
// descricao: v.string(),
// editavel: v.optional(v.boolean()),
// nome: v.string(),
// nivel: v.number(),
// setor: v.optional(v.string()),
// }),
// v.object({
// _id: v.id("roles"),
// _creationTime: v.optional(v.number()),
// criadoPor: v.optional(v.id("usuarios")),
// customizado: v.optional(v.boolean()),
// descricao: v.literal("Perfil não encontrado"),
// editavel: v.optional(v.boolean()),
// nome: v.literal("erro_role_ausente"),
// nivel: v.literal(999),
// setor: v.optional(v.string()),
// erro: v.literal(true),
// })
// ),
// funcionario: v.optional(
// v.object({
// _id: v.id("funcionarios"),
// nome: v.string(),
// matricula: v.optional(v.string()),
// descricaoCargo: v.optional(v.string()),
// simboloTipo: v.union(
// v.literal("cargo_comissionado"),
// v.literal("funcao_gratificada")
// ),
// })
// ),
// avisos: v.optional(
// v.array(
// v.object({
// tipo: v.union(
// v.literal("erro"),
// v.literal("aviso"),
// v.literal("info")
// ),
// mensagem: v.string(),
// })
// )
// ),
// })
// ),
handler: async (ctx, args) => { handler: async (ctx, args) => {
let usuarios = await ctx.db.query("usuarios").collect(); let usuarios = await ctx.db.query("usuarios").collect();
@@ -439,34 +354,34 @@ export const alterarStatus = mutation({
/** /**
* Resetar senha do usuário * Resetar senha do usuário
*/ */
export const resetarSenha = mutation({ // export const resetarSenha = mutation({
args: { // args: {
usuarioId: v.id("usuarios"), // usuarioId: v.id("usuarios"),
novaSenha: v.string(), // novaSenha: v.string(),
}, // },
returns: v.null(), // returns: v.null(),
handler: async (ctx, args) => { // handler: async (ctx, args) => {
const senhaHash = await hashPassword(args.novaSenha); // const senhaHash = await hashPassword(args.novaSenha);
await ctx.db.patch(args.usuarioId, { // await ctx.db.patch(args.usuarioId, {
senhaHash, // senhaHash,
primeiroAcesso: true, // primeiroAcesso: true,
atualizadoEm: Date.now(), // atualizadoEm: Date.now(),
}); // });
// Desativar todas as sessões // // Desativar todas as sessões
const sessoes = await ctx.db // const sessoes = await ctx.db
.query("sessoes") // .query("sessoes")
.withIndex("by_usuario", (q) => q.eq("usuarioId", args.usuarioId)) // .withIndex("by_usuario", (q) => q.eq("usuarioId", args.usuarioId))
.collect(); // .collect();
for (const sessao of sessoes) { // for (const sessao of sessoes) {
await ctx.db.patch(sessao._id, { ativo: false }); // await ctx.db.patch(sessao._id, { ativo: false });
} // }
return null; // return null;
}, // },
}); // });
/** /**
* Excluir usuário * Excluir usuário
@@ -601,19 +516,6 @@ export const atualizarPerfil = mutation({
.first(); .first();
} }
// SE NÃO ENCONTROU, BUSCAR POR SESSÃO ATIVA (Sistema customizado)
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("Usuário não encontrado"); if (!usuarioAtual) throw new Error("Usuário não encontrado");
// Validar statusMensagem (max 100 chars) // Validar statusMensagem (max 100 chars)
@@ -678,9 +580,6 @@ export const obterPerfil = query({
v.null() v.null()
), ),
handler: async (ctx) => { handler: async (ctx) => {
console.log("=== DEBUG obterPerfil ===");
// 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");
@@ -693,42 +592,9 @@ 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"
);
}
// SE NÃO ENCONTROU, BUSCAR POR SESSÃO ATIVA (Sistema customizado)
if (!usuarioAtual) {
console.log("Buscando por sessão ativa...");
const sessaoAtiva = await ctx.db
.query("sessoes")
.filter((q) => q.eq(q.field("ativo"), true))
.order("desc")
.first();
console.log("Sessão ativa encontrada:", sessaoAtiva ? "SIM" : "NÃO");
if (sessaoAtiva) {
usuarioAtual = await ctx.db.get(sessaoAtiva.usuarioId);
console.log(
"Usuário da sessão encontrado:",
usuarioAtual ? "SIM" : "NÃO"
);
}
} }
if (!usuarioAtual) { if (!usuarioAtual) {
console.log("❌ Nenhum usuário encontrado");
// Listar todos os usuários para debug
const todosUsuarios = await ctx.db.query("usuarios").collect();
console.log("Total de usuários no banco:", todosUsuarios.length);
console.log(
"Emails cadastrados:",
todosUsuarios.map((u) => u.email)
);
return null; return null;
} }
@@ -789,7 +655,10 @@ export const listarParaChat = query({
), ),
handler: async (ctx) => { handler: async (ctx) => {
// Obter usuário autenticado usando função helper compartilhada // Obter usuário autenticado usando função helper compartilhada
const usuarioAtual = await getUsuarioAutenticado(ctx); const usuarioAtual = await getCurrentUserFunction(ctx);
if (!usuarioAtual) {
return [];
}
// Buscar todos os usuários ativos // Buscar todos os usuários ativos
const usuarios = await ctx.db const usuarios = await ctx.db
@@ -888,6 +757,11 @@ export const bloquearUsuario = mutation({
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) => {
const usuarioAtual = await getCurrentUserFunction(ctx);
if (!usuarioAtual) {
return { sucesso: false as const, erro: "Usuário não autenticado" };
}
const usuario = await ctx.db.get(args.usuarioId); const usuario = await ctx.db.get(args.usuarioId);
if (!usuario) { if (!usuario) {
return { sucesso: false as const, erro: "Usuário não encontrado" }; return { sucesso: false as const, erro: "Usuário não encontrado" };
@@ -948,6 +822,11 @@ export const desbloquearUsuario = mutation({
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) => {
const usuarioAtual = await getCurrentUserFunction(ctx);
if (!usuarioAtual) {
return { sucesso: false as const, erro: "Usuário não autenticado" };
}
const usuario = await ctx.db.get(args.usuarioId); const usuario = await ctx.db.get(args.usuarioId);
if (!usuario) { if (!usuario) {
return { sucesso: false as const, erro: "Usuário não encontrado" }; return { sucesso: false as const, erro: "Usuário não encontrado" };
@@ -995,48 +874,48 @@ export const desbloquearUsuario = mutation({
/** /**
* Resetar senha de usuário (apenas TI_MASTER) * Resetar senha de usuário (apenas TI_MASTER)
*/ */
export const resetarSenhaUsuario = mutation({ // export const resetarSenhaUsuario = mutation({
args: { // args: {
usuarioId: v.id("usuarios"), // usuarioId: v.id("usuarios"),
resetadoPorId: v.id("usuarios"), // resetadoPorId: v.id("usuarios"),
novaSenhaTemporaria: v.optional(v.string()), // Se não fornecer, gera automática // novaSenhaTemporaria: v.optional(v.string()), // Se não fornecer, gera automática
}, // },
returns: v.union( // returns: v.union(
v.object({ sucesso: v.literal(true), senhaTemporaria: v.string() }), // v.object({ sucesso: v.literal(true), 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) => {
const usuario = await ctx.db.get(args.usuarioId); // const usuario = await ctx.db.get(args.usuarioId);
if (!usuario) { // if (!usuario) {
return { sucesso: false as const, erro: "Usuário não encontrado" }; // return { sucesso: false as const, erro: "Usuário não encontrado" };
} // }
// Gerar senha temporária se não foi fornecida // // Gerar senha temporária se não foi fornecida
const senhaTemporaria = args.novaSenhaTemporaria || gerarSenhaTemporaria(); // const senhaTemporaria = args.novaSenhaTemporaria || gerarSenhaTemporaria();
const senhaHash = await hashPassword(senhaTemporaria); // const senhaHash = await hashPassword(senhaTemporaria);
// Atualizar usuário // // Atualizar usuário
await ctx.db.patch(args.usuarioId, { // await ctx.db.patch(args.usuarioId, {
senhaHash, // senhaHash,
primeiroAcesso: true, // Força mudança de senha no próximo login // primeiroAcesso: true, // Força mudança de senha no próximo login
tentativasLogin: 0, // tentativasLogin: 0,
ultimaTentativaLogin: undefined, // ultimaTentativaLogin: undefined,
atualizadoEm: Date.now(), // atualizadoEm: Date.now(),
}); // });
// Log de atividade // // Log de atividade
await registrarAtividade( // await registrarAtividade(
ctx, // ctx,
args.resetadoPorId, // args.resetadoPorId,
"resetar_senha", // "resetar_senha",
"usuarios", // "usuarios",
JSON.stringify({ usuarioId: args.usuarioId }), // JSON.stringify({ usuarioId: args.usuarioId }),
args.usuarioId // args.usuarioId
); // );
return { sucesso: true as const, senhaTemporaria }; // return { sucesso: true as const, senhaTemporaria };
}, // },
}); // });
// Helper para gerar senha temporária // Helper para gerar senha temporária
function gerarSenhaTemporaria(): string { function gerarSenhaTemporaria(): string {
@@ -1066,6 +945,11 @@ export const editarUsuario = mutation({
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) => {
const usuarioAtual = await getCurrentUserFunction(ctx);
if (!usuarioAtual) {
return { sucesso: false as const, erro: "Usuário não autenticado" };
}
const usuario = await ctx.db.get(args.usuarioId); const usuario = await ctx.db.get(args.usuarioId);
if (!usuario) { if (!usuario) {
return { sucesso: false as const, erro: "Usuário não encontrado" }; return { sucesso: false as const, erro: "Usuário não encontrado" };
@@ -1153,7 +1037,12 @@ export const criarAdminMaster = mutation({
} }
const senhaTemporaria = args.senha || gerarSenhaTemporaria(); const senhaTemporaria = args.senha || gerarSenhaTemporaria();
const senhaHash = await hashPassword(senhaTemporaria);
const authUserId = await createAuthUser(ctx, {
nome: args.nome,
email: args.email,
password: senhaTemporaria,
});
// Verificar se email já existe // Verificar se email já existe
const existentePorEmail = await ctx.db const existentePorEmail = await ctx.db
@@ -1164,11 +1053,11 @@ export const criarAdminMaster = mutation({
// Promove usuário existente por email // Promove usuário existente por email
await ctx.db.patch(existentePorEmail._id, { await ctx.db.patch(existentePorEmail._id, {
nome: args.nome, nome: args.nome,
senhaHash,
roleId: roleTIMaster._id, roleId: roleTIMaster._id,
ativo: true, ativo: true,
primeiroAcesso: true, primeiroAcesso: true,
atualizadoEm: Date.now(), atualizadoEm: Date.now(),
authId: authUserId,
}); });
return { return {
sucesso: true as const, sucesso: true as const,
@@ -1179,7 +1068,7 @@ export const criarAdminMaster = mutation({
// Criar novo usuário TI Master // Criar novo usuário TI Master
const usuarioId = await ctx.db.insert("usuarios", { const usuarioId = await ctx.db.insert("usuarios", {
senhaHash, authId: authUserId,
nome: args.nome, nome: args.nome,
email: args.email, email: args.email,
roleId: roleTIMaster._id, roleId: roleTIMaster._id,
@@ -1192,191 +1081,3 @@ export const criarAdminMaster = mutation({
return { sucesso: true as const, usuarioId, senhaTemporaria }; return { sucesso: true as const, usuarioId, senhaTemporaria };
}, },
}); });
/**
* Desativar usuário logicamente (soft delete - apenas TI_MASTER)
*/
export const excluirUsuarioLogico = mutation({
args: {
usuarioId: v.id("usuarios"),
excluidoPorId: v.id("usuarios"),
},
returns: v.union(
v.object({ sucesso: v.literal(true) }),
v.object({ sucesso: v.literal(false), erro: v.string() })
),
handler: async (ctx, args) => {
const usuario = await ctx.db.get(args.usuarioId);
if (!usuario) {
return { sucesso: false as const, erro: "Usuário não encontrado" };
}
// Marcar como inativo
await ctx.db.patch(args.usuarioId, {
ativo: false,
atualizadoEm: Date.now(),
});
// Desativar todas as sessões
const sessoes = await ctx.db
.query("sessoes")
.withIndex("by_usuario", (q) => q.eq("usuarioId", args.usuarioId))
.filter((q) => q.eq(q.field("ativo"), true))
.collect();
for (const sessao of sessoes) {
await ctx.db.patch(sessao._id, { ativo: false });
}
// Log de atividade
await registrarAtividade(
ctx,
args.excluidoPorId,
"excluir",
"usuarios",
JSON.stringify({ usuarioId: args.usuarioId }),
args.usuarioId
);
return { sucesso: true as const };
},
});
/**
* Criar usuário completo com permissões (TI_MASTER)
*/
export const criarUsuarioCompleto = mutation({
args: {
nome: v.string(),
email: v.string(),
roleId: v.id("roles"),
setor: v.optional(v.string()),
senhaInicial: v.optional(v.string()),
criadoPorId: v.id("usuarios"),
enviarEmailBoasVindas: v.optional(v.boolean()),
},
returns: v.union(
v.object({
sucesso: v.literal(true),
usuarioId: v.id("usuarios"),
senhaTemporaria: v.string(),
}),
v.object({ sucesso: v.literal(false), erro: v.string() })
),
handler: async (ctx, args) => {
// Verificar se email já existe
const emailExistente = await ctx.db
.query("usuarios")
.withIndex("by_email", (q) => q.eq("email", args.email))
.first();
if (emailExistente) {
return { sucesso: false as const, erro: "E-mail já cadastrado" };
}
// Gerar senha inicial se não fornecida
const senhaTemporaria = args.senhaInicial || gerarSenhaTemporaria();
const senhaHash = await hashPassword(senhaTemporaria);
// Criar usuário
const usuarioId = await ctx.db.insert("usuarios", {
senhaHash,
nome: args.nome,
email: args.email,
roleId: args.roleId,
setor: args.setor,
ativo: true,
primeiroAcesso: true,
criadoEm: Date.now(),
atualizadoEm: Date.now(),
});
// Log de atividade
await registrarAtividade(
ctx,
args.criadoPorId,
"criar",
"usuarios",
JSON.stringify({ usuarioId, nome: args.nome }),
usuarioId
);
// TODO: Se enviarEmailBoasVindas = true, enfileirar email
// Isso será implementado quando criarmos o sistema de emails
return { sucesso: true as const, usuarioId, senhaTemporaria };
},
});
/**
* Criar (ou garantir) um usuário ADMIN padrão
*/
export const criarAdminPadrao = mutation({
args: {
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 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 email
const existentePorEmail = await ctx.db
.query("usuarios")
.withIndex("by_email", (q) => q.eq("email", email))
.first();
const senhaHash = await hashPassword(senha);
if (existentePorEmail) {
await ctx.db.patch(existentePorEmail._id, {
nome,
email,
senhaHash,
roleId: roleAdmin._id,
ativo: true,
primeiroAcesso: false,
atualizadoEm: Date.now(),
});
return { sucesso: true, usuarioId: existentePorEmail._id };
}
const usuarioId = await ctx.db.insert("usuarios", {
senhaHash,
nome,
email,
roleId: roleAdmin._id,
ativo: true,
primeiroAcesso: false,
criadoEm: Date.now(),
atualizadoEm: Date.now(),
});
return { sucesso: true, usuarioId };
},
});