feat: enhance chat components with improved accessibility features, including ARIA attributes for search and user status, and implement message length validation and file type checks in message input handling

This commit is contained in:
2025-12-08 23:16:05 -03:00
parent e46738c5bf
commit 1810cbabe2
22 changed files with 1364 additions and 249 deletions

View File

@@ -281,6 +281,59 @@ export const enviarMensagem = mutation({
});
}
// Validar tamanho da mensagem
const MAX_MENSAGEM_LENGTH = 5000;
if (args.conteudo.length > MAX_MENSAGEM_LENGTH) {
throw new Error(`Mensagem muito longa. O limite é de ${MAX_MENSAGEM_LENGTH} caracteres.`);
}
// Validação de tipo de arquivo (se houver arquivo)
if (args.arquivoTipo) {
const TIPOS_PERMITIDOS = [
// Imagens
'image/jpeg',
'image/png',
'image/gif',
'image/webp',
'image/svg+xml',
// Documentos
'application/pdf',
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'application/vnd.ms-powerpoint',
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'text/plain',
'text/csv',
// Arquivos
'application/zip',
'application/x-rar-compressed',
'application/x-7z-compressed',
'application/x-tar',
'application/gzip'
];
if (!TIPOS_PERMITIDOS.includes(args.arquivoTipo)) {
throw new Error('Tipo de arquivo não permitido');
}
// Validar tamanho de arquivo (10MB)
const MAX_FILE_SIZE = 10 * 1024 * 1024;
if (args.arquivoTamanho && args.arquivoTamanho > MAX_FILE_SIZE) {
throw new Error(`Arquivo muito grande. O tamanho máximo é ${MAX_FILE_SIZE / 1024 / 1024}MB.`);
}
// Validar nome do arquivo (sanitizar)
if (args.arquivoNome) {
const nomeSanitizado = args.arquivoNome.replace(/[^a-zA-Z0-9._-]/g, '_');
if (nomeSanitizado !== args.arquivoNome) {
// Se o nome foi alterado, usar o sanitizado
args.arquivoNome = nomeSanitizado;
}
}
}
// Verificar se usuário pertence à conversa
const conversa = await ctx.db.get(args.conversaId);
if (!conversa) throw new Error('Conversa não encontrada');
@@ -1716,28 +1769,42 @@ export const listarConversas = query({
export const obterMensagens = query({
args: {
conversaId: v.id('conversas'),
limit: v.optional(v.number())
limit: v.optional(v.number()),
cursor: v.optional(v.id('mensagens')) // ID da última mensagem carregada (para paginação)
},
handler: async (ctx, args) => {
const usuarioAtual = await getUsuarioAutenticado(ctx);
if (!usuarioAtual) return [];
if (!usuarioAtual) return { mensagens: [], hasMore: false };
// Verificar se usuário pertence à conversa (SEGURANÇA CRÍTICA)
const conversa = await ctx.db.get(args.conversaId);
if (!conversa || !conversa.participantes.includes(usuarioAtual._id)) {
return [];
return { mensagens: [], hasMore: false };
}
// Buscar mensagens (excluir agendadas)
const mensagens = await ctx.db
const limit = args.limit || 50;
let query = ctx.db
.query('mensagens')
.withIndex('by_conversa', (q) => q.eq('conversaId', args.conversaId))
.order('desc')
.take(args.limit || 50);
.order('desc');
// Se há cursor, buscar mensagens anteriores a ele
if (args.cursor) {
const cursorMsg = await ctx.db.get(args.cursor);
if (cursorMsg && cursorMsg.conversaId === args.conversaId) {
// Buscar mensagens anteriores à mensagem do cursor
query = query.filter((q) => q.lt(q.field('_creationTime'), cursorMsg._creationTime));
}
}
// Buscar uma mensagem a mais para verificar se há mais mensagens
const mensagens = await query.take(limit + 1);
const hasMore = mensagens.length > limit;
const mensagensParaRetornar = hasMore ? mensagens.slice(0, limit) : mensagens;
// Filtrar mensagens agendadas e garantir que são da conversa correta
// SEGURANÇA: Apenas mensagens de participantes da conversa são retornadas
const mensagensFiltradas = mensagens.filter((m) => {
const mensagensFiltradas = mensagensParaRetornar.filter((m) => {
// Excluir agendadas
if (m.agendadaPara) return false;
@@ -1751,7 +1818,7 @@ export const obterMensagens = query({
// Enriquecer com informações do remetente e mensagem respondida
const mensagensEnriquecidas = await Promise.all(
mensagensFiltradas.map(async (mensagem) => {
mensagensParaRetornar.map(async (mensagem) => {
const remetente = await ctx.db.get(mensagem.remetenteId);
// SEGURANÇA: Não retornar informações de remetente se não for participante
@@ -1794,7 +1861,15 @@ export const obterMensagens = query({
);
// Filtrar nulls (caso alguma mensagem tenha sido rejeitada por segurança)
return mensagensEnriquecidas.filter((m) => m !== null).reverse();
const mensagensFinais = mensagensEnriquecidas.filter((m) => m !== null).reverse();
return {
mensagens: mensagensFinais,
hasMore,
nextCursor: hasMore && mensagensFinais.length > 0
? mensagensFinais[mensagensFinais.length - 1]._id
: null
};
}
});