feat: Add 'atas' (minutes/records) management feature, and implement various improvements across UI, backend logic, and authentication.

This commit is contained in:
2025-12-02 16:37:48 -03:00
parent 05e7f1181d
commit 4bd9e21748
265 changed files with 29156 additions and 26460 deletions

View File

@@ -1,7 +1,7 @@
import { v } from "convex/values";
import { mutation, query, action, internalMutation } from "./_generated/server";
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 { registrarAtividade } from './logsAtividades';
/**
* Obter configuração de Jitsi ativa
@@ -10,8 +10,8 @@ export const obterConfigJitsi = query({
args: {},
handler: async (ctx) => {
const config = await ctx.db
.query("configuracaoJitsi")
.withIndex("by_ativo", (q) => q.eq("ativo", true))
.query('configuracaoJitsi')
.withIndex('by_ativo', (q) => q.eq('ativo', true))
.first();
if (!config) {
@@ -27,12 +27,11 @@ export const obterConfigJitsi = query({
acceptSelfSignedCert: config.acceptSelfSignedCert ?? false, // Default para false se não existir
ativo: config.ativo,
testadoEm: config.testadoEm,
atualizadoEm: config.atualizadoEm,
atualizadoEm: config.atualizadoEm
};
},
}
});
/**
* Salvar configuração de Jitsi (apenas TI_MASTER)
*/
@@ -43,26 +42,29 @@ export const salvarConfigJitsi = mutation({
roomPrefix: v.string(),
useHttps: v.boolean(),
acceptSelfSignedCert: v.boolean(),
configuradoPorId: v.id("usuarios"),
configuradoPorId: v.id('usuarios')
},
returns: v.union(
v.object({ sucesso: v.literal(true), configId: v.id("configuracaoJitsi") }),
v.object({ sucesso: v.literal(true), configId: v.id('configuracaoJitsi') }),
v.object({ sucesso: v.literal(false), erro: v.string() })
),
handler: async (ctx, args) => {
// Validar domínio (deve ser não vazio)
if (!args.domain || args.domain.trim().length === 0) {
return { sucesso: false as const, erro: "Domínio não pode estar vazio" };
return { sucesso: false as const, erro: 'Domínio não pode estar vazio' };
}
// Validar appId (deve ser não vazio)
if (!args.appId || args.appId.trim().length === 0) {
return { sucesso: false as const, erro: "App ID não pode estar vazio" };
return { sucesso: false as const, erro: 'App ID não pode estar vazio' };
}
// Validar roomPrefix (deve ser não vazio e alfanumérico)
if (!args.roomPrefix || args.roomPrefix.trim().length === 0) {
return { sucesso: false as const, erro: "Prefixo de sala não pode estar vazio" };
return {
sucesso: false as const,
erro: 'Prefixo de sala não pode estar vazio'
};
}
// Validar formato do roomPrefix (apenas letras, números e hífens)
@@ -70,14 +72,14 @@ export const salvarConfigJitsi = mutation({
if (!roomPrefixRegex.test(args.roomPrefix.trim())) {
return {
sucesso: false as const,
erro: "Prefixo de sala deve conter apenas letras, números e hífens",
erro: 'Prefixo de sala deve conter apenas letras, números e hífens'
};
}
// Desativar config anterior
const configsAntigas = await ctx.db
.query("configuracaoJitsi")
.withIndex("by_ativo", (q) => q.eq("ativo", true))
.query('configuracaoJitsi')
.withIndex('by_ativo', (q) => q.eq('ativo', true))
.collect();
for (const config of configsAntigas) {
@@ -85,7 +87,7 @@ export const salvarConfigJitsi = mutation({
}
// Criar nova config
const configId = await ctx.db.insert("configuracaoJitsi", {
const configId = await ctx.db.insert('configuracaoJitsi', {
domain: args.domain.trim(),
appId: args.appId.trim(),
roomPrefix: args.roomPrefix.trim(),
@@ -93,21 +95,21 @@ export const salvarConfigJitsi = mutation({
acceptSelfSignedCert: args.acceptSelfSignedCert,
ativo: true,
configuradoPor: args.configuradoPorId,
atualizadoEm: Date.now(),
atualizadoEm: Date.now()
});
// Log de atividade
await registrarAtividade(
ctx,
args.configuradoPorId,
"configurar",
"jitsi",
'configurar',
'jitsi',
JSON.stringify({ domain: args.domain, appId: args.appId }),
configId
);
return { sucesso: true as const, configId };
},
}
});
/**
@@ -115,15 +117,15 @@ export const salvarConfigJitsi = mutation({
*/
export const atualizarTestadoEm = internalMutation({
args: {
configId: v.id("configuracaoJitsi"),
configId: v.id('configuracaoJitsi')
},
returns: v.null(),
handler: async (ctx, args) => {
await ctx.db.patch(args.configId, {
testadoEm: Date.now(),
testadoEm: Date.now()
});
return null;
},
}
});
/**
@@ -133,22 +135,25 @@ export const testarConexaoJitsi = action({
args: {
domain: v.string(),
useHttps: v.boolean(),
acceptSelfSignedCert: v.optional(v.boolean()),
acceptSelfSignedCert: v.optional(v.boolean())
},
returns: v.union(
v.object({ sucesso: v.literal(true), aviso: v.optional(v.string()) }),
v.object({ sucesso: v.literal(false), erro: v.string() })
),
handler: async (ctx, args): Promise<{ sucesso: true; aviso?: string } | { sucesso: false; erro: string }> => {
handler: async (
ctx,
args
): Promise<{ sucesso: true; aviso?: string } | { sucesso: false; erro: string }> => {
// Validações básicas
if (!args.domain || args.domain.trim().length === 0) {
return { sucesso: false as const, erro: "Domínio não pode estar vazio" };
return { sucesso: false as const, erro: 'Domínio não pode estar vazio' };
}
try {
const protocol = args.useHttps ? "https" : "http";
const protocol = args.useHttps ? 'https' : 'http';
// Extrair host e porta do domain
const [host, portStr] = args.domain.split(":");
const [host, portStr] = args.domain.split(':');
const port = portStr ? parseInt(portStr, 10) : args.useHttps ? 443 : 80;
const url = `${protocol}://${host}:${port}/http-bind`;
@@ -159,11 +164,11 @@ export const testarConexaoJitsi = action({
try {
const response = await fetch(url, {
method: "GET",
method: 'GET',
signal: controller.signal,
headers: {
"Content-Type": "application/xml",
},
'Content-Type': 'application/xml'
}
});
clearTimeout(timeoutId);
@@ -176,7 +181,7 @@ export const testarConexaoJitsi = action({
if (configAtiva) {
await ctx.runMutation(internal.configuracaoJitsi.atualizarTestadoEm, {
configId: configAtiva._id,
configId: configAtiva._id
});
}
@@ -184,30 +189,29 @@ export const testarConexaoJitsi = action({
} else {
return {
sucesso: false as const,
erro: `Servidor retornou status ${response.status}`,
erro: `Servidor retornou status ${response.status}`
};
}
} catch (fetchError: unknown) {
clearTimeout(timeoutId);
const errorMessage =
fetchError instanceof Error ? fetchError.message : String(fetchError);
const errorMessage = fetchError instanceof Error ? fetchError.message : String(fetchError);
// Se for erro de timeout
if (errorMessage.includes("aborted") || errorMessage.includes("timeout")) {
if (errorMessage.includes('aborted') || errorMessage.includes('timeout')) {
return {
sucesso: false as const,
erro: "Timeout: Servidor não respondeu em 5 segundos",
erro: 'Timeout: Servidor não respondeu em 5 segundos'
};
}
// Verificar se é erro de certificado SSL autoassinado
const isSSLError =
errorMessage.includes("CERTIFICATE_VERIFY_FAILED") ||
errorMessage.includes("self signed certificate") ||
errorMessage.includes("self-signed certificate") ||
errorMessage.includes("certificate") ||
errorMessage.includes("SSL") ||
errorMessage.includes("certificate verify failed");
const isSSLError =
errorMessage.includes('CERTIFICATE_VERIFY_FAILED') ||
errorMessage.includes('self signed certificate') ||
errorMessage.includes('self-signed certificate') ||
errorMessage.includes('certificate') ||
errorMessage.includes('SSL') ||
errorMessage.includes('certificate verify failed');
// Se for erro de certificado e aceitar autoassinado está configurado
if (isSSLError && args.acceptSelfSignedCert) {
@@ -218,26 +222,27 @@ export const testarConexaoJitsi = action({
if (configAtiva) {
await ctx.runMutation(internal.configuracaoJitsi.atualizarTestadoEm, {
configId: configAtiva._id,
configId: configAtiva._id
});
}
return {
return {
sucesso: true as const,
aviso: "Servidor acessível com certificado autoassinado. No navegador, você precisará aceitar o certificado manualmente na primeira conexão."
aviso:
'Servidor acessível com certificado autoassinado. No navegador, você precisará aceitar o certificado manualmente na primeira conexão.'
};
}
// Para servidores Jitsi, pode ser normal receber erro 405 (Method Not Allowed)
// para GET em /http-bind, pois esse endpoint espera POST (BOSH)
// Isso indica que o servidor está acessível, apenas não aceita GET
if (errorMessage.includes("405") || errorMessage.includes("Method Not Allowed")) {
if (errorMessage.includes('405') || errorMessage.includes('Method Not Allowed')) {
// Se o teste foi bem-sucedido e há uma config ativa, atualizar testadoEm
const configAtiva = await ctx.runQuery(api.configuracaoJitsi.obterConfigJitsi, {});
if (configAtiva) {
await ctx.runMutation(internal.configuracaoJitsi.atualizarTestadoEm, {
configId: configAtiva._id,
configId: configAtiva._id
});
}
@@ -248,23 +253,23 @@ export const testarConexaoJitsi = action({
if (isSSLError) {
return {
sucesso: false as const,
erro: `Erro de certificado SSL: O servidor está usando um certificado não confiável (provavelmente autoassinado). Para desenvolvimento local, habilite "Aceitar Certificados Autoassinados" nas configurações de segurança. Em produção, use um certificado válido (ex: Let's Encrypt).`,
erro: `Erro de certificado SSL: O servidor está usando um certificado não confiável (provavelmente autoassinado). Para desenvolvimento local, habilite "Aceitar Certificados Autoassinados" nas configurações de segurança. Em produção, use um certificado válido (ex: Let's Encrypt).`
};
}
return {
sucesso: false as const,
erro: `Erro ao conectar: ${errorMessage}`,
erro: `Erro ao conectar: ${errorMessage}`
};
}
} 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 Jitsi",
erro: errorMessage || 'Erro ao conectar com o servidor Jitsi'
};
}
},
}
});
/**
@@ -272,13 +277,13 @@ export const testarConexaoJitsi = action({
*/
export const marcarConfigTestada = mutation({
args: {
configId: v.id("configuracaoJitsi"),
configId: v.id('configuracaoJitsi')
},
handler: async (ctx, args) => {
await ctx.db.patch(args.configId, {
testadoEm: Date.now(),
testadoEm: Date.now()
});
},
}
});
/**
@@ -286,16 +291,15 @@ export const marcarConfigTestada = mutation({
*/
export const marcarConfiguradoNoServidor = internalMutation({
args: {
configId: v.id("configuracaoJitsi"),
configId: v.id('configuracaoJitsi')
},
returns: v.null(),
handler: async (ctx, args) => {
await ctx.db.patch(args.configId, {
configuradoNoServidor: true,
configuradoNoServidorEm: Date.now(),
configuradoEm: Date.now(),
configuradoEm: Date.now()
});
return null;
},
}
});