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:
@@ -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
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user