feat: Add 'atas' (minutes/records) management feature, and implement various improvements across UI, backend logic, and authentication.
This commit is contained in:
@@ -1,229 +1,247 @@
|
||||
import { v } from "convex/values";
|
||||
import { mutation, query, action, internalMutation } from "./_generated/server";
|
||||
import { encryptSMTPPassword } from "./auth/utils";
|
||||
import { registrarAtividade } from "./logsAtividades";
|
||||
import { api, internal } from "./_generated/api";
|
||||
import { v } from 'convex/values';
|
||||
import { api, internal } from './_generated/api';
|
||||
import { action, internalMutation, mutation, query } from './_generated/server';
|
||||
import { encryptSMTPPassword } from './auth/utils';
|
||||
import { registrarAtividade } from './logsAtividades';
|
||||
|
||||
/**
|
||||
* Obter configuração de email ativa (senha mascarada)
|
||||
*/
|
||||
export const obterConfigEmail = query({
|
||||
args: {},
|
||||
handler: async (ctx) => {
|
||||
const config = await ctx.db
|
||||
.query("configuracaoEmail")
|
||||
.withIndex("by_ativo", (q) => q.eq("ativo", true))
|
||||
.first();
|
||||
args: {},
|
||||
handler: async (ctx) => {
|
||||
const config = await ctx.db
|
||||
.query('configuracaoEmail')
|
||||
.withIndex('by_ativo', (q) => q.eq('ativo', true))
|
||||
.first();
|
||||
|
||||
if (!config) {
|
||||
return null;
|
||||
}
|
||||
if (!config) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Retornar config com senha mascarada
|
||||
return {
|
||||
_id: config._id,
|
||||
servidor: config.servidor,
|
||||
porta: config.porta,
|
||||
usuario: config.usuario,
|
||||
senhaHash: "********", // Mascarar senha
|
||||
emailRemetente: config.emailRemetente,
|
||||
nomeRemetente: config.nomeRemetente,
|
||||
usarSSL: config.usarSSL,
|
||||
usarTLS: config.usarTLS,
|
||||
ativo: config.ativo,
|
||||
testadoEm: config.testadoEm,
|
||||
atualizadoEm: config.atualizadoEm,
|
||||
};
|
||||
},
|
||||
// Retornar config com senha mascarada
|
||||
return {
|
||||
_id: config._id,
|
||||
servidor: config.servidor,
|
||||
porta: config.porta,
|
||||
usuario: config.usuario,
|
||||
senhaHash: '********', // Mascarar senha
|
||||
emailRemetente: config.emailRemetente,
|
||||
nomeRemetente: config.nomeRemetente,
|
||||
usarSSL: config.usarSSL,
|
||||
usarTLS: config.usarTLS,
|
||||
ativo: config.ativo,
|
||||
testadoEm: config.testadoEm,
|
||||
atualizadoEm: config.atualizadoEm
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Salvar configuração de email (apenas TI_MASTER)
|
||||
*/
|
||||
export const salvarConfigEmail = mutation({
|
||||
args: {
|
||||
servidor: v.string(),
|
||||
porta: v.number(),
|
||||
usuario: v.string(),
|
||||
senha: v.string(),
|
||||
emailRemetente: v.string(),
|
||||
nomeRemetente: v.string(),
|
||||
usarSSL: v.boolean(),
|
||||
usarTLS: v.boolean(),
|
||||
configuradoPorId: v.id("usuarios"),
|
||||
},
|
||||
returns: v.union(
|
||||
v.object({ sucesso: v.literal(true), configId: v.id("configuracaoEmail") }),
|
||||
v.object({ sucesso: v.literal(false), erro: v.string() })
|
||||
),
|
||||
handler: async (ctx, args) => {
|
||||
// Validar email
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(args.emailRemetente)) {
|
||||
return { sucesso: false as const, erro: "Email remetente inválido" };
|
||||
}
|
||||
args: {
|
||||
servidor: v.string(),
|
||||
porta: v.number(),
|
||||
usuario: v.string(),
|
||||
senha: v.string(),
|
||||
emailRemetente: v.string(),
|
||||
nomeRemetente: v.string(),
|
||||
usarSSL: v.boolean(),
|
||||
usarTLS: v.boolean(),
|
||||
configuradoPorId: v.id('usuarios')
|
||||
},
|
||||
returns: v.union(
|
||||
v.object({ sucesso: v.literal(true), configId: v.id('configuracaoEmail') }),
|
||||
v.object({ sucesso: v.literal(false), erro: v.string() })
|
||||
),
|
||||
handler: async (ctx, args) => {
|
||||
// Validar email
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(args.emailRemetente)) {
|
||||
return { sucesso: false as const, erro: 'Email remetente inválido' };
|
||||
}
|
||||
|
||||
// Validar porta
|
||||
if (args.porta < 1 || args.porta > 65535) {
|
||||
return { sucesso: false as const, erro: "Porta deve ser um número entre 1 e 65535" };
|
||||
}
|
||||
// Validar porta
|
||||
if (args.porta < 1 || args.porta > 65535) {
|
||||
return {
|
||||
sucesso: false as const,
|
||||
erro: 'Porta deve ser um número entre 1 e 65535'
|
||||
};
|
||||
}
|
||||
|
||||
// Buscar config ativa anterior para manter senha se não fornecida
|
||||
const configAtiva = await ctx.db
|
||||
.query("configuracaoEmail")
|
||||
.withIndex("by_ativo", (q) => q.eq("ativo", true))
|
||||
.first();
|
||||
// Buscar config ativa anterior para manter senha se não fornecida
|
||||
const configAtiva = await ctx.db
|
||||
.query('configuracaoEmail')
|
||||
.withIndex('by_ativo', (q) => q.eq('ativo', true))
|
||||
.first();
|
||||
|
||||
// Determinar senhaHash: usar nova senha se fornecida, senão manter a atual
|
||||
let senhaHash: string;
|
||||
if (args.senha && args.senha.trim().length > 0) {
|
||||
// Nova senha fornecida, criptografar usando criptografia reversível (AES)
|
||||
senhaHash = await encryptSMTPPassword(args.senha);
|
||||
} else if (configAtiva) {
|
||||
// Senha não fornecida, manter a atual (já criptografada)
|
||||
senhaHash = configAtiva.senhaHash;
|
||||
} else {
|
||||
// Sem senha e sem config existente - erro
|
||||
return { sucesso: false as const, erro: "Senha é obrigatória para nova configuração" };
|
||||
}
|
||||
// Determinar senhaHash: usar nova senha se fornecida, senão manter a atual
|
||||
let senhaHash: string;
|
||||
if (args.senha && args.senha.trim().length > 0) {
|
||||
// Nova senha fornecida, criptografar usando criptografia reversível (AES)
|
||||
senhaHash = await encryptSMTPPassword(args.senha);
|
||||
} else if (configAtiva) {
|
||||
// Senha não fornecida, manter a atual (já criptografada)
|
||||
senhaHash = configAtiva.senhaHash;
|
||||
} else {
|
||||
// Sem senha e sem config existente - erro
|
||||
return {
|
||||
sucesso: false as const,
|
||||
erro: 'Senha é obrigatória para nova configuração'
|
||||
};
|
||||
}
|
||||
|
||||
// Desativar config anterior
|
||||
const configsAntigas = await ctx.db
|
||||
.query("configuracaoEmail")
|
||||
.withIndex("by_ativo", (q) => q.eq("ativo", true))
|
||||
.collect();
|
||||
// Desativar config anterior
|
||||
const configsAntigas = await ctx.db
|
||||
.query('configuracaoEmail')
|
||||
.withIndex('by_ativo', (q) => q.eq('ativo', true))
|
||||
.collect();
|
||||
|
||||
for (const config of configsAntigas) {
|
||||
await ctx.db.patch(config._id, { ativo: false });
|
||||
}
|
||||
for (const config of configsAntigas) {
|
||||
await ctx.db.patch(config._id, { ativo: false });
|
||||
}
|
||||
|
||||
// Criar nova config
|
||||
const configId = await ctx.db.insert("configuracaoEmail", {
|
||||
servidor: args.servidor,
|
||||
porta: args.porta,
|
||||
usuario: args.usuario,
|
||||
senhaHash,
|
||||
emailRemetente: args.emailRemetente,
|
||||
nomeRemetente: args.nomeRemetente,
|
||||
usarSSL: args.usarSSL,
|
||||
usarTLS: args.usarTLS,
|
||||
ativo: true,
|
||||
configuradoPor: args.configuradoPorId,
|
||||
atualizadoEm: Date.now(),
|
||||
});
|
||||
// Criar nova config
|
||||
const configId = await ctx.db.insert('configuracaoEmail', {
|
||||
servidor: args.servidor,
|
||||
porta: args.porta,
|
||||
usuario: args.usuario,
|
||||
senhaHash,
|
||||
emailRemetente: args.emailRemetente,
|
||||
nomeRemetente: args.nomeRemetente,
|
||||
usarSSL: args.usarSSL,
|
||||
usarTLS: args.usarTLS,
|
||||
ativo: true,
|
||||
configuradoPor: args.configuradoPorId,
|
||||
atualizadoEm: Date.now()
|
||||
});
|
||||
|
||||
// Log de atividade
|
||||
await registrarAtividade(
|
||||
ctx,
|
||||
args.configuradoPorId,
|
||||
"configurar",
|
||||
"email",
|
||||
JSON.stringify({ servidor: args.servidor, porta: args.porta }),
|
||||
configId
|
||||
);
|
||||
// Log de atividade
|
||||
await registrarAtividade(
|
||||
ctx,
|
||||
args.configuradoPorId,
|
||||
'configurar',
|
||||
'email',
|
||||
JSON.stringify({ servidor: args.servidor, porta: args.porta }),
|
||||
configId
|
||||
);
|
||||
|
||||
return { sucesso: true as const, configId };
|
||||
},
|
||||
return { sucesso: true as const, configId };
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Mutation interna para atualizar testadoEm
|
||||
*/
|
||||
export const atualizarTestadoEm = internalMutation({
|
||||
args: {
|
||||
configId: v.id("configuracaoEmail"),
|
||||
},
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
await ctx.db.patch(args.configId, {
|
||||
testadoEm: Date.now(),
|
||||
});
|
||||
return null;
|
||||
},
|
||||
args: {
|
||||
configId: v.id('configuracaoEmail')
|
||||
},
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
await ctx.db.patch(args.configId, {
|
||||
testadoEm: Date.now()
|
||||
});
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Testar conexão SMTP (action que chama action real)
|
||||
*/
|
||||
export const testarConexaoSMTP = action({
|
||||
args: {
|
||||
servidor: v.string(),
|
||||
porta: v.number(),
|
||||
usuario: v.string(),
|
||||
senha: v.string(),
|
||||
usarSSL: v.boolean(),
|
||||
usarTLS: v.boolean(),
|
||||
},
|
||||
returns: v.union(
|
||||
v.object({ sucesso: v.literal(true) }),
|
||||
v.object({ sucesso: v.literal(false), erro: v.string() })
|
||||
),
|
||||
handler: async (ctx, args): Promise<{ sucesso: true } | { sucesso: false; erro: string }> => {
|
||||
// Validações básicas
|
||||
if (!args.servidor || args.servidor.trim().length === 0) {
|
||||
return { sucesso: false as const, erro: "Servidor SMTP não pode estar vazio" };
|
||||
}
|
||||
args: {
|
||||
servidor: v.string(),
|
||||
porta: v.number(),
|
||||
usuario: v.string(),
|
||||
senha: v.string(),
|
||||
usarSSL: v.boolean(),
|
||||
usarTLS: v.boolean()
|
||||
},
|
||||
returns: v.union(
|
||||
v.object({ sucesso: v.literal(true) }),
|
||||
v.object({ sucesso: v.literal(false), erro: v.string() })
|
||||
),
|
||||
handler: async (ctx, args): Promise<{ sucesso: true } | { sucesso: false; erro: string }> => {
|
||||
// Validações básicas
|
||||
if (!args.servidor || args.servidor.trim().length === 0) {
|
||||
return {
|
||||
sucesso: false as const,
|
||||
erro: 'Servidor SMTP não pode estar vazio'
|
||||
};
|
||||
}
|
||||
|
||||
if (!args.porta || args.porta < 1 || args.porta > 65535) {
|
||||
return { sucesso: false as const, erro: "Porta inválida. Deve ser entre 1 e 65535" };
|
||||
}
|
||||
if (!args.porta || args.porta < 1 || args.porta > 65535) {
|
||||
return {
|
||||
sucesso: false as const,
|
||||
erro: 'Porta inválida. Deve ser entre 1 e 65535'
|
||||
};
|
||||
}
|
||||
|
||||
if (!args.usuario || args.usuario.trim().length === 0) {
|
||||
return { sucesso: false as const, erro: "Usuário não pode estar vazio" };
|
||||
}
|
||||
if (!args.usuario || args.usuario.trim().length === 0) {
|
||||
return { sucesso: false as const, erro: 'Usuário não pode estar vazio' };
|
||||
}
|
||||
|
||||
if (!args.senha || args.senha.trim().length === 0) {
|
||||
return { sucesso: false as const, erro: "Senha não pode estar vazia" };
|
||||
}
|
||||
if (!args.senha || args.senha.trim().length === 0) {
|
||||
return { sucesso: false as const, erro: 'Senha não pode estar vazia' };
|
||||
}
|
||||
|
||||
// Validação de SSL/TLS mutuamente exclusivos
|
||||
if (args.usarSSL && args.usarTLS) {
|
||||
return { sucesso: false as const, erro: "SSL e TLS não podem estar habilitados simultaneamente" };
|
||||
}
|
||||
// Validação de SSL/TLS mutuamente exclusivos
|
||||
if (args.usarSSL && args.usarTLS) {
|
||||
return {
|
||||
sucesso: false as const,
|
||||
erro: 'SSL e TLS não podem estar habilitados simultaneamente'
|
||||
};
|
||||
}
|
||||
|
||||
// Chamar action de teste real (que usa nodemailer)
|
||||
try {
|
||||
const resultado: { sucesso: true } | { sucesso: false; erro: string } = await ctx.runAction(api.actions.smtp.testarConexao, {
|
||||
servidor: args.servidor,
|
||||
porta: args.porta,
|
||||
usuario: args.usuario,
|
||||
senha: args.senha,
|
||||
usarSSL: args.usarSSL,
|
||||
usarTLS: args.usarTLS,
|
||||
});
|
||||
// Chamar action de teste real (que usa nodemailer)
|
||||
try {
|
||||
const resultado: { sucesso: true } | { sucesso: false; erro: string } = await ctx.runAction(
|
||||
api.actions.smtp.testarConexao,
|
||||
{
|
||||
servidor: args.servidor,
|
||||
porta: args.porta,
|
||||
usuario: args.usuario,
|
||||
senha: args.senha,
|
||||
usarSSL: args.usarSSL,
|
||||
usarTLS: args.usarTLS
|
||||
}
|
||||
);
|
||||
|
||||
// Se o teste foi bem-sucedido e há uma config ativa, atualizar testadoEm
|
||||
if (resultado.sucesso) {
|
||||
const configAtiva = await ctx.runQuery(api.configuracaoEmail.obterConfigEmail, {});
|
||||
|
||||
if (configAtiva) {
|
||||
await ctx.runMutation(internal.configuracaoEmail.atualizarTestadoEm, {
|
||||
configId: configAtiva._id,
|
||||
});
|
||||
}
|
||||
}
|
||||
// Se o teste foi bem-sucedido e há uma config ativa, atualizar testadoEm
|
||||
if (resultado.sucesso) {
|
||||
const configAtiva = await ctx.runQuery(api.configuracaoEmail.obterConfigEmail, {});
|
||||
|
||||
return resultado;
|
||||
} catch (error: unknown) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
return {
|
||||
sucesso: false as const,
|
||||
erro: errorMessage || "Erro ao conectar com o servidor SMTP"
|
||||
};
|
||||
}
|
||||
},
|
||||
if (configAtiva) {
|
||||
await ctx.runMutation(internal.configuracaoEmail.atualizarTestadoEm, {
|
||||
configId: configAtiva._id
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return resultado;
|
||||
} catch (error: unknown) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
return {
|
||||
sucesso: false as const,
|
||||
erro: errorMessage || 'Erro ao conectar com o servidor SMTP'
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Marcar que a configuração foi testada com sucesso
|
||||
*/
|
||||
export const marcarConfigTestada = mutation({
|
||||
args: {
|
||||
configId: v.id("configuracaoEmail"),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
await ctx.db.patch(args.configId, {
|
||||
testadoEm: Date.now(),
|
||||
});
|
||||
},
|
||||
args: {
|
||||
configId: v.id('configuracaoEmail')
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
await ctx.db.patch(args.configId, {
|
||||
testadoEm: Date.now()
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user