refactor: streamline chat widget and backend user management

- Removed the .editorconfig file to simplify project configuration.
- Refactored the ChatWidget component to enhance readability and maintainability, including the integration of current user data and improved notification handling.
- Updated backend functions to utilize the new getCurrentUserFunction for user authentication, ensuring consistent user data retrieval across various modules.
- Cleaned up code in multiple backend files, enhancing clarity and performance while maintaining functionality.
- Improved error handling and user feedback mechanisms in user-related operations.
This commit is contained in:
2025-11-08 17:46:10 -03:00
parent 57b5f6821b
commit 5d76c375c2
8 changed files with 5153 additions and 5417 deletions

View File

@@ -1,12 +0,0 @@
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = false
insert_final_newline = false

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,211 +1,184 @@
import { query, mutation, internalQuery } from "./_generated/server";
import { v } from "convex/values";
import type { Doc } from "./_generated/dataModel";
import { query, mutation, internalQuery } from './_generated/server';
import { v } from 'convex/values';
import type { Doc } from './_generated/dataModel';
import { getCurrentUserFunction } from './auth';
// Catálogo base de recursos e ações
// Ajuste/expanda conforme os módulos disponíveis no sistema
export const CATALOGO_RECURSOS = [
{
recurso: "funcionarios",
acoes: ["dashboard", "ver", "listar", "criar", "editar", "excluir"],
},
{
recurso: "simbolos",
acoes: ["dashboard", "ver", "listar", "criar", "editar", "excluir"],
},
{
recurso: 'funcionarios',
acoes: ['dashboard', 'ver', 'listar', 'criar', 'editar', 'excluir']
},
{
recurso: 'simbolos',
acoes: ['dashboard', 'ver', 'listar', 'criar', 'editar', 'excluir']
}
] as const;
export const listarRecursosEAcoes = query({
args: {},
returns: v.array(
v.object({
recurso: v.string(),
acoes: v.array(v.string()),
})
),
handler: async () => {
return CATALOGO_RECURSOS.map((r) => ({
recurso: r.recurso,
acoes: [...r.acoes],
}));
},
args: {},
returns: v.array(
v.object({
recurso: v.string(),
acoes: v.array(v.string())
})
),
handler: async () => {
return CATALOGO_RECURSOS.map((r) => ({
recurso: r.recurso,
acoes: [...r.acoes]
}));
}
});
export const listarPermissoesAcoesPorRole = query({
args: { roleId: v.id("roles") },
returns: v.array(
v.object({
recurso: v.string(),
acoes: v.array(v.string()),
})
),
handler: async (ctx, args) => {
// Buscar vínculos permissao<-role
const rolePerms = await ctx.db
.query("rolePermissoes")
.withIndex("by_role", (q) => q.eq("roleId", args.roleId))
.collect();
args: { roleId: v.id('roles') },
returns: v.array(
v.object({
recurso: v.string(),
acoes: v.array(v.string())
})
),
handler: async (ctx, args) => {
// Buscar vínculos permissao<-role
const rolePerms = await ctx.db
.query('rolePermissoes')
.withIndex('by_role', (q) => q.eq('roleId', args.roleId))
.collect();
// Carregar documentos de permissões
const actionsByResource: Record<string, Set<string>> = {};
for (const rp of rolePerms) {
const perm = await ctx.db.get(rp.permissaoId);
if (!perm) continue;
const set = (actionsByResource[perm.recurso] ||= new Set<string>());
set.add(perm.acao);
}
// Carregar documentos de permissões
const actionsByResource: Record<string, Set<string>> = {};
for (const rp of rolePerms) {
const perm = await ctx.db.get(rp.permissaoId);
if (!perm) continue;
const set = (actionsByResource[perm.recurso] ||= new Set<string>());
set.add(perm.acao);
}
// Normalizar para todos os recursos do catálogo
const result: Array<{ recurso: string; acoes: Array<string> }> = [];
for (const item of CATALOGO_RECURSOS) {
const granted = Array.from(
actionsByResource[item.recurso] ?? new Set<string>()
);
result.push({ recurso: item.recurso, acoes: granted });
}
return result;
},
// Normalizar para todos os recursos do catálogo
const result: Array<{ recurso: string; acoes: Array<string> }> = [];
for (const item of CATALOGO_RECURSOS) {
const granted = Array.from(actionsByResource[item.recurso] ?? new Set<string>());
result.push({ recurso: item.recurso, acoes: granted });
}
return result;
}
});
export const atualizarPermissaoAcao = mutation({
args: {
roleId: v.id("roles"),
recurso: v.string(),
acao: v.string(),
conceder: v.boolean(),
},
returns: v.null(),
handler: async (ctx, args) => {
// Garantir documento de permissão (recurso+acao)
let permissao = await ctx.db
.query("permissoes")
.withIndex("by_recurso_e_acao", (q) =>
q.eq("recurso", args.recurso).eq("acao", args.acao)
)
.first();
args: {
roleId: v.id('roles'),
recurso: v.string(),
acao: v.string(),
conceder: v.boolean()
},
returns: v.null(),
handler: async (ctx, args) => {
// Garantir documento de permissão (recurso+acao)
let permissao = await ctx.db
.query('permissoes')
.withIndex('by_recurso_e_acao', (q) => q.eq('recurso', args.recurso).eq('acao', args.acao))
.first();
if (!permissao) {
const nome = `${args.recurso}.${args.acao}`;
const descricao = `Permite ${args.acao} em ${args.recurso}`;
const id = await ctx.db.insert("permissoes", {
nome,
descricao,
recurso: args.recurso,
acao: args.acao,
});
permissao = await ctx.db.get(id);
}
if (!permissao) {
const nome = `${args.recurso}.${args.acao}`;
const descricao = `Permite ${args.acao} em ${args.recurso}`;
const id = await ctx.db.insert('permissoes', {
nome,
descricao,
recurso: args.recurso,
acao: args.acao
});
permissao = await ctx.db.get(id);
}
if (!permissao) return null;
if (!permissao) return null;
// Verificar vínculo atual
const existente = await ctx.db
.query("rolePermissoes")
.withIndex("by_role", (q) => q.eq("roleId", args.roleId))
.collect();
// Verificar vínculo atual
const existente = await ctx.db
.query('rolePermissoes')
.withIndex('by_role', (q) => q.eq('roleId', args.roleId))
.collect();
const vinculo = existente.find((rp) => rp.permissaoId === permissao!._id);
const vinculo = existente.find((rp) => rp.permissaoId === permissao!._id);
if (args.conceder) {
if (!vinculo) {
await ctx.db.insert("rolePermissoes", {
roleId: args.roleId,
permissaoId: permissao._id,
});
}
} else {
if (vinculo) {
await ctx.db.delete(vinculo._id);
}
}
return null;
},
if (args.conceder) {
if (!vinculo) {
await ctx.db.insert('rolePermissoes', {
roleId: args.roleId,
permissaoId: permissao._id
});
}
} else {
if (vinculo) {
await ctx.db.delete(vinculo._id);
}
}
return null;
}
});
export const verificarAcao = query({
args: {
usuarioId: v.id("usuarios"),
recurso: v.string(),
acao: v.string(),
},
returns: v.null(),
handler: async (ctx, args) => {
const usuario = await ctx.db.get(args.usuarioId);
if (!usuario) throw new Error("acesso_negado");
args: {
usuarioId: v.id('usuarios'),
recurso: v.string(),
acao: v.string()
},
returns: v.null(),
handler: async (ctx, args) => {
const usuario = await ctx.db.get(args.usuarioId);
if (!usuario) throw new Error('acesso_negado');
const role = await ctx.db.get(usuario.roleId);
if (!role) throw new Error("acesso_negado");
const role = await ctx.db.get(usuario.roleId);
if (!role) throw new Error('acesso_negado');
// Níveis administrativos têm acesso total
if (role.nivel <= 1) return null;
// Níveis administrativos têm acesso total
if (role.nivel <= 1) return null;
// Encontrar permissão
const permissao = await ctx.db
.query("permissoes")
.withIndex("by_recurso_e_acao", (q) =>
q.eq("recurso", args.recurso).eq("acao", args.acao)
)
.first();
if (!permissao) throw new Error("acesso_negado");
// Encontrar permissão
const permissao = await ctx.db
.query('permissoes')
.withIndex('by_recurso_e_acao', (q) => q.eq('recurso', args.recurso).eq('acao', args.acao))
.first();
if (!permissao) throw new Error('acesso_negado');
const hasLink = await ctx.db
.query("rolePermissoes")
.withIndex("by_role", (q) => q.eq("roleId", usuario.roleId))
.collect();
const permitido = hasLink.some((rp) => rp.permissaoId === permissao!._id);
if (!permitido) throw new Error("acesso_negado");
return null;
},
const hasLink = await ctx.db
.query('rolePermissoes')
.withIndex('by_role', (q) => q.eq('roleId', usuario.roleId))
.collect();
const permitido = hasLink.some((rp) => rp.permissaoId === permissao!._id);
if (!permitido) throw new Error('acesso_negado');
return null;
}
});
export const assertPermissaoAcaoAtual = internalQuery({
args: {
recurso: v.string(),
acao: v.string(),
},
returns: v.null(),
handler: async (ctx, args) => {
const identity = await ctx.auth.getUserIdentity();
let usuarioAtual: Doc<"usuarios"> | null = null;
args: {
recurso: v.string(),
acao: v.string()
},
returns: v.null(),
handler: async (ctx, args) => {
const usuarioAtual: Doc<'usuarios'> | null = (await getCurrentUserFunction(ctx)) ?? null;
if (!usuarioAtual) throw new Error('acesso_negado');
if (identity && identity.email) {
usuarioAtual = await ctx.db
.query("usuarios")
.withIndex("by_email", (q) => q.eq("email", identity.email!))
.first();
}
const role = await ctx.db.get(usuarioAtual.roleId);
if (!role) throw new Error('acesso_negado');
if (role.nivel <= 1) return null;
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);
}
}
const permissao = await ctx.db
.query('permissoes')
.withIndex('by_recurso_e_acao', (q) => q.eq('recurso', args.recurso).eq('acao', args.acao))
.first();
if (!permissao) throw new Error('acesso_negado');
if (!usuarioAtual) throw new Error("acesso_negado");
const role = await ctx.db.get(usuarioAtual.roleId);
if (!role) throw new Error("acesso_negado");
if (role.nivel <= 1) return null;
const permissao = await ctx.db
.query("permissoes")
.withIndex("by_recurso_e_acao", (q) =>
q.eq("recurso", args.recurso).eq("acao", args.acao)
)
.first();
if (!permissao) throw new Error("acesso_negado");
const links = await ctx.db
.query("rolePermissoes")
.withIndex("by_role", (q) => q.eq("roleId", role._id))
.collect();
const ok = links.some((rp) => rp.permissaoId === permissao!._id);
if (!ok) throw new Error("acesso_negado");
return null;
},
const links = await ctx.db
.query('rolePermissoes')
.withIndex('by_role', (q) => q.eq('roleId', role._id))
.collect();
const ok = links.some((rp) => rp.permissaoId === permissao!._id);
if (!ok) throw new Error('acesso_negado');
return null;
}
});

View File

@@ -1,136 +1,113 @@
import { v } from "convex/values";
import { mutation, query } from "./_generated/server";
import { Id } from "./_generated/dataModel";
import { v } from 'convex/values';
import { mutation, query } from './_generated/server';
import { getCurrentUserFunction } from './auth';
/**
* Obter preferências de notificação para uma conversa
*/
export const obterPreferenciasConversa = query({
args: {
conversaId: v.id("conversas"),
},
returns: v.union(
v.object({
pushAtivado: v.boolean(),
emailAtivado: v.boolean(),
somAtivado: v.boolean(),
silenciado: v.boolean(),
apenasMencoes: v.boolean(),
}),
v.null()
),
handler: async (ctx, args) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity?.email) {
return null;
}
args: {
conversaId: v.id('conversas')
},
returns: v.union(
v.object({
pushAtivado: v.boolean(),
emailAtivado: v.boolean(),
somAtivado: v.boolean(),
silenciado: v.boolean(),
apenasMencoes: v.boolean()
}),
v.null()
),
handler: async (ctx, args) => {
const usuario = await getCurrentUserFunction(ctx);
if (!usuario) return null;
const usuario = await ctx.db
.query("usuarios")
.withIndex("by_email", (q) => q.eq("email", identity.email!))
.first();
const preferencias = await ctx.db
.query('preferenciasNotificacaoConversa')
.withIndex('by_usuario_conversa', (q) =>
q.eq('usuarioId', usuario._id).eq('conversaId', args.conversaId)
)
.first();
if (!usuario) {
return null;
}
if (!preferencias) {
// Retornar valores padrão
return {
pushAtivado: true,
emailAtivado: true,
somAtivado: true,
silenciado: false,
apenasMencoes: false
};
}
const preferencias = await ctx.db
.query("preferenciasNotificacaoConversa")
.withIndex("by_usuario_conversa", (q) =>
q.eq("usuarioId", usuario._id).eq("conversaId", args.conversaId)
)
.first();
if (!preferencias) {
// Retornar valores padrão
return {
pushAtivado: true,
emailAtivado: true,
somAtivado: true,
silenciado: false,
apenasMencoes: false,
};
}
return {
pushAtivado: preferencias.pushAtivado,
emailAtivado: preferencias.emailAtivado,
somAtivado: preferencias.somAtivado,
silenciado: preferencias.silenciado,
apenasMencoes: preferencias.apenasMencoes,
};
},
return {
pushAtivado: preferencias.pushAtivado,
emailAtivado: preferencias.emailAtivado,
somAtivado: preferencias.somAtivado,
silenciado: preferencias.silenciado,
apenasMencoes: preferencias.apenasMencoes
};
}
});
/**
* Atualizar preferências de notificação para uma conversa
*/
export const atualizarPreferenciasConversa = mutation({
args: {
conversaId: v.id("conversas"),
pushAtivado: v.optional(v.boolean()),
emailAtivado: v.optional(v.boolean()),
somAtivado: v.optional(v.boolean()),
silenciado: v.optional(v.boolean()),
apenasMencoes: v.optional(v.boolean()),
},
returns: v.object({ sucesso: v.boolean() }),
handler: async (ctx, args) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity?.email) {
return { sucesso: false };
}
args: {
conversaId: v.id('conversas'),
pushAtivado: v.optional(v.boolean()),
emailAtivado: v.optional(v.boolean()),
somAtivado: v.optional(v.boolean()),
silenciado: v.optional(v.boolean()),
apenasMencoes: v.optional(v.boolean())
},
returns: v.object({ sucesso: v.boolean() }),
handler: async (ctx, args) => {
const usuario = await getCurrentUserFunction(ctx);
if (!usuario) return { sucesso: false };
const usuario = await ctx.db
.query("usuarios")
.withIndex("by_email", (q) => q.eq("email", identity.email!))
.first();
// Verificar se usuário pertence à conversa
const conversa = await ctx.db.get(args.conversaId);
if (!conversa || !conversa.participantes.includes(usuario._id)) {
return { sucesso: false };
}
if (!usuario) {
return { sucesso: false };
}
const preferenciasExistentes = await ctx.db
.query('preferenciasNotificacaoConversa')
.withIndex('by_usuario_conversa', (q) =>
q.eq('usuarioId', usuario._id).eq('conversaId', args.conversaId)
)
.first();
// Verificar se usuário pertence à conversa
const conversa = await ctx.db.get(args.conversaId);
if (!conversa || !conversa.participantes.includes(usuario._id)) {
return { sucesso: false };
}
const agora = Date.now();
const preferenciasExistentes = await ctx.db
.query("preferenciasNotificacaoConversa")
.withIndex("by_usuario_conversa", (q) =>
q.eq("usuarioId", usuario._id).eq("conversaId", args.conversaId)
)
.first();
if (preferenciasExistentes) {
// Atualizar preferências existentes
await ctx.db.patch(preferenciasExistentes._id, {
pushAtivado: args.pushAtivado ?? preferenciasExistentes.pushAtivado,
emailAtivado: args.emailAtivado ?? preferenciasExistentes.emailAtivado,
somAtivado: args.somAtivado ?? preferenciasExistentes.somAtivado,
silenciado: args.silenciado ?? preferenciasExistentes.silenciado,
apenasMencoes: args.apenasMencoes ?? preferenciasExistentes.apenasMencoes,
atualizadoEm: agora
});
} else {
// Criar novas preferências com valores padrão
await ctx.db.insert('preferenciasNotificacaoConversa', {
usuarioId: usuario._id,
conversaId: args.conversaId,
pushAtivado: args.pushAtivado ?? true,
emailAtivado: args.emailAtivado ?? true,
somAtivado: args.somAtivado ?? true,
silenciado: args.silenciado ?? false,
apenasMencoes: args.apenasMencoes ?? false,
criadoEm: agora,
atualizadoEm: agora
});
}
const agora = Date.now();
if (preferenciasExistentes) {
// Atualizar preferências existentes
await ctx.db.patch(preferenciasExistentes._id, {
pushAtivado: args.pushAtivado ?? preferenciasExistentes.pushAtivado,
emailAtivado: args.emailAtivado ?? preferenciasExistentes.emailAtivado,
somAtivado: args.somAtivado ?? preferenciasExistentes.somAtivado,
silenciado: args.silenciado ?? preferenciasExistentes.silenciado,
apenasMencoes: args.apenasMencoes ?? preferenciasExistentes.apenasMencoes,
atualizadoEm: agora,
});
} else {
// Criar novas preferências com valores padrão
await ctx.db.insert("preferenciasNotificacaoConversa", {
usuarioId: usuario._id,
conversaId: args.conversaId,
pushAtivado: args.pushAtivado ?? true,
emailAtivado: args.emailAtivado ?? true,
somAtivado: args.somAtivado ?? true,
silenciado: args.silenciado ?? false,
apenasMencoes: args.apenasMencoes ?? false,
criadoEm: agora,
atualizadoEm: agora,
});
}
return { sucesso: true };
},
return { sucesso: true };
}
});

View File

@@ -1,120 +1,111 @@
import { v } from "convex/values";
import { mutation, query, internalMutation, internalQuery } from "./_generated/server";
import { Id } from "./_generated/dataModel";
import { internal, api } from "./_generated/api";
import { v } from 'convex/values';
import { mutation, internalMutation, internalQuery } from './_generated/server';
import { internal, api } from './_generated/api';
import { getCurrentUserFunction } from './auth';
/**
* Registrar subscription de push notification
*/
export const registrarPushSubscription = mutation({
args: {
endpoint: v.string(),
keys: v.object({
p256dh: v.string(),
auth: v.string(),
}),
userAgent: v.optional(v.string()),
},
returns: v.object({ sucesso: v.boolean(), erro: v.optional(v.string()) }),
handler: async (ctx, args) => {
// Obter usuário autenticado
const identity = await ctx.auth.getUserIdentity();
if (!identity?.email) {
return { sucesso: false, erro: "Usuário não autenticado" };
}
args: {
endpoint: v.string(),
keys: v.object({
p256dh: v.string(),
auth: v.string()
}),
userAgent: v.optional(v.string())
},
returns: v.object({ sucesso: v.boolean(), erro: v.optional(v.string()) }),
handler: async (ctx, args) => {
// Obter usuário autenticado
const usuario = await getCurrentUserFunction(ctx);
if (!usuario) {
return { sucesso: false, erro: 'Usuário não autenticado' };
}
const usuario = await ctx.db
.query("usuarios")
.withIndex("by_email", (q) => q.eq("email", identity.email!))
.first();
// Verificar se já existe subscription com este endpoint
const existente = await ctx.db
.query('pushSubscriptions')
.withIndex('by_endpoint', (q) => q.eq('endpoint', args.endpoint))
.first();
if (!usuario) {
return { sucesso: false, erro: "Usuário não encontrado" };
}
if (existente) {
// Atualizar subscription existente
await ctx.db.patch(existente._id, {
usuarioId: usuario._id,
keys: args.keys,
userAgent: args.userAgent,
ultimaAtividade: Date.now(),
ativo: true
});
} else {
// Criar nova subscription
await ctx.db.insert('pushSubscriptions', {
usuarioId: usuario._id,
endpoint: args.endpoint,
keys: args.keys,
userAgent: args.userAgent,
criadoEm: Date.now(),
ultimaAtividade: Date.now(),
ativo: true
});
}
// Verificar se já existe subscription com este endpoint
const existente = await ctx.db
.query("pushSubscriptions")
.withIndex("by_endpoint", (q) => q.eq("endpoint", args.endpoint))
.first();
if (existente) {
// Atualizar subscription existente
await ctx.db.patch(existente._id, {
usuarioId: usuario._id,
keys: args.keys,
userAgent: args.userAgent,
ultimaAtividade: Date.now(),
ativo: true,
});
} else {
// Criar nova subscription
await ctx.db.insert("pushSubscriptions", {
usuarioId: usuario._id,
endpoint: args.endpoint,
keys: args.keys,
userAgent: args.userAgent,
criadoEm: Date.now(),
ultimaAtividade: Date.now(),
ativo: true,
});
}
return { sucesso: true };
},
return { sucesso: true };
}
});
/**
* Remover subscription de push notification
*/
export const removerPushSubscription = mutation({
args: {
endpoint: v.string(),
},
returns: v.object({ sucesso: v.boolean() }),
handler: async (ctx, args) => {
const subscription = await ctx.db
.query("pushSubscriptions")
.withIndex("by_endpoint", (q) => q.eq("endpoint", args.endpoint))
.first();
args: {
endpoint: v.string()
},
returns: v.object({ sucesso: v.boolean() }),
handler: async (ctx, args) => {
const subscription = await ctx.db
.query('pushSubscriptions')
.withIndex('by_endpoint', (q) => q.eq('endpoint', args.endpoint))
.first();
if (subscription) {
await ctx.db.patch(subscription._id, { ativo: false });
}
if (subscription) {
await ctx.db.patch(subscription._id, { ativo: false });
}
return { sucesso: true };
},
return { sucesso: true };
}
});
/**
* Obter subscriptions ativas de um usuário
*/
export const obterPushSubscriptions = internalQuery({
args: {
usuarioId: v.id("usuarios"),
},
returns: v.array(
v.object({
_id: v.id("pushSubscriptions"),
endpoint: v.string(),
keys: v.object({
p256dh: v.string(),
auth: v.string(),
}),
})
),
handler: async (ctx, args) => {
const subscriptions = await ctx.db
.query("pushSubscriptions")
.withIndex("by_usuario", (q) => q.eq("usuarioId", args.usuarioId).eq("ativo", true))
.collect();
args: {
usuarioId: v.id('usuarios')
},
returns: v.array(
v.object({
_id: v.id('pushSubscriptions'),
endpoint: v.string(),
keys: v.object({
p256dh: v.string(),
auth: v.string()
})
})
),
handler: async (ctx, args) => {
const subscriptions = await ctx.db
.query('pushSubscriptions')
.withIndex('by_usuario', (q) => q.eq('usuarioId', args.usuarioId).eq('ativo', true))
.collect();
return subscriptions.map((sub) => ({
_id: sub._id,
endpoint: sub.endpoint,
keys: sub.keys,
}));
},
return subscriptions.map((sub) => ({
_id: sub._id,
endpoint: sub.endpoint,
keys: sub.keys
}));
}
});
/**
@@ -122,157 +113,156 @@ export const obterPushSubscriptions = internalQuery({
* Esta função será chamada quando uma nova mensagem chegar
*/
export const enviarPushNotification = internalMutation({
args: {
usuarioId: v.id("usuarios"),
titulo: v.string(),
corpo: v.string(),
data: v.optional(
v.object({
conversaId: v.optional(v.id("conversas")),
mensagemId: v.optional(v.id("mensagens")),
tipo: v.optional(v.string()),
})
),
},
returns: v.object({ enviados: v.number(), falhas: v.number() }),
handler: async (ctx, args) => {
// Buscar subscriptions ativas do usuário
const subscriptions = await ctx.runQuery(internal.pushNotifications.obterPushSubscriptions, {
usuarioId: args.usuarioId,
});
args: {
usuarioId: v.id('usuarios'),
titulo: v.string(),
corpo: v.string(),
data: v.optional(
v.object({
conversaId: v.optional(v.id('conversas')),
mensagemId: v.optional(v.id('mensagens')),
tipo: v.optional(v.string())
})
)
},
returns: v.object({ enviados: v.number(), falhas: v.number() }),
handler: async (ctx, args) => {
// Buscar subscriptions ativas do usuário
const subscriptions = await ctx.runQuery(internal.pushNotifications.obterPushSubscriptions, {
usuarioId: args.usuarioId
});
if (subscriptions.length === 0) {
return { enviados: 0, falhas: 0 };
}
if (subscriptions.length === 0) {
return { enviados: 0, falhas: 0 };
}
// Verificar preferências do usuário
const usuario = await ctx.db.get(args.usuarioId);
if (!usuario || usuario.notificacoesAtivadas === false) {
return { enviados: 0, falhas: 0 };
}
// Verificar preferências do usuário
const usuario = await ctx.db.get(args.usuarioId);
if (!usuario || usuario.notificacoesAtivadas === false) {
return { enviados: 0, falhas: 0 };
}
// Se há conversaId, verificar preferências específicas da conversa
const conversaId = args.data?.conversaId;
if (conversaId) {
const preferencias = await ctx.db
.query("preferenciasNotificacaoConversa")
.withIndex("by_usuario_conversa", (q) =>
q.eq("usuarioId", args.usuarioId).eq("conversaId", conversaId)
)
.first();
// Se há conversaId, verificar preferências específicas da conversa
const conversaId = args.data?.conversaId;
if (conversaId) {
const preferencias = await ctx.db
.query('preferenciasNotificacaoConversa')
.withIndex('by_usuario_conversa', (q) =>
q.eq('usuarioId', args.usuarioId).eq('conversaId', conversaId)
)
.first();
if (preferencias) {
// Se silenciado ou push desativado, não enviar
if (preferencias.silenciado || !preferencias.pushAtivado) {
return { enviados: 0, falhas: 0 };
}
if (preferencias) {
// Se silenciado ou push desativado, não enviar
if (preferencias.silenciado || !preferencias.pushAtivado) {
return { enviados: 0, falhas: 0 };
}
// Se apenas menções e não é menção, não enviar
if (preferencias.apenasMencoes && args.data?.tipo !== "mencao") {
return { enviados: 0, falhas: 0 };
}
}
}
// Se apenas menções e não é menção, não enviar
if (preferencias.apenasMencoes && args.data?.tipo !== 'mencao') {
return { enviados: 0, falhas: 0 };
}
}
}
// Agendar envio de push via action (que roda em Node.js)
let enviados = 0;
let falhas = 0;
// Agendar envio de push via action (que roda em Node.js)
let enviados = 0;
let falhas = 0;
// Converter IDs para strings ao passar para a action
// A action espera strings, mas recebemos Ids do Convex
const dataParaAction = args.data
? {
conversaId: args.data.conversaId ? String(args.data.conversaId) : undefined,
mensagemId: args.data.mensagemId ? String(args.data.mensagemId) : undefined,
tipo: args.data.tipo,
}
: undefined;
// Converter IDs para strings ao passar para a action
// A action espera strings, mas recebemos Ids do Convex
const dataParaAction = args.data
? {
conversaId: args.data.conversaId ? String(args.data.conversaId) : undefined,
mensagemId: args.data.mensagemId ? String(args.data.mensagemId) : undefined,
tipo: args.data.tipo
}
: undefined;
for (const subscription of subscriptions) {
try {
await ctx.scheduler.runAfter(0, api.actions.pushNotifications.enviarPush, {
subscriptionId: subscription._id,
titulo: args.titulo,
corpo: args.corpo,
data: dataParaAction,
});
enviados++;
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : String(error);
console.error(`Erro ao agendar push para subscription ${subscription._id}:`, errorMessage);
falhas++;
}
}
for (const subscription of subscriptions) {
try {
await ctx.scheduler.runAfter(0, api.actions.pushNotifications.enviarPush, {
subscriptionId: subscription._id,
titulo: args.titulo,
corpo: args.corpo,
data: dataParaAction
});
enviados++;
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : String(error);
console.error(`Erro ao agendar push para subscription ${subscription._id}:`, errorMessage);
falhas++;
}
}
return { enviados, falhas };
},
return { enviados, falhas };
}
});
/**
* Obter subscription por ID (para actions)
*/
export const getSubscriptionById = internalQuery({
args: {
subscriptionId: v.id("pushSubscriptions"),
},
returns: v.union(
v.object({
_id: v.id("pushSubscriptions"),
endpoint: v.string(),
keys: v.object({
p256dh: v.string(),
auth: v.string(),
}),
ativo: v.boolean(),
}),
v.null()
),
handler: async (ctx, args) => {
const subscription = await ctx.db.get(args.subscriptionId);
if (!subscription) {
return null;
}
args: {
subscriptionId: v.id('pushSubscriptions')
},
returns: v.union(
v.object({
_id: v.id('pushSubscriptions'),
endpoint: v.string(),
keys: v.object({
p256dh: v.string(),
auth: v.string()
}),
ativo: v.boolean()
}),
v.null()
),
handler: async (ctx, args) => {
const subscription = await ctx.db.get(args.subscriptionId);
if (!subscription) {
return null;
}
return {
_id: subscription._id,
endpoint: subscription.endpoint,
keys: subscription.keys,
ativo: subscription.ativo,
};
},
return {
_id: subscription._id,
endpoint: subscription.endpoint,
keys: subscription.keys,
ativo: subscription.ativo
};
}
});
/**
* Marcar subscription como inativa
*/
export const marcarSubscriptionInativa = internalMutation({
args: {
subscriptionId: v.id("pushSubscriptions"),
},
returns: v.null(),
handler: async (ctx, args) => {
await ctx.db.patch(args.subscriptionId, { ativo: false });
return null;
},
args: {
subscriptionId: v.id('pushSubscriptions')
},
returns: v.null(),
handler: async (ctx, args) => {
await ctx.db.patch(args.subscriptionId, { ativo: false });
return null;
}
});
/**
* Verificar se usuário está online (última atividade recente)
*/
export const verificarUsuarioOnline = internalQuery({
args: {
usuarioId: v.id("usuarios"),
},
returns: v.boolean(),
handler: async (ctx, args) => {
const usuario = await ctx.db.get(args.usuarioId);
if (!usuario || !usuario.ultimaAtividade) {
return false;
}
args: {
usuarioId: v.id('usuarios')
},
returns: v.boolean(),
handler: async (ctx, args) => {
const usuario = await ctx.db.get(args.usuarioId);
if (!usuario || !usuario.ultimaAtividade) {
return false;
}
// Considerar online se última atividade foi há menos de 5 minutos
const cincoMinutosAtras = Date.now() - 5 * 60 * 1000;
return usuario.ultimaAtividade >= cincoMinutosAtras;
},
// Considerar online se última atividade foi há menos de 5 minutos
const cincoMinutosAtras = Date.now() - 5 * 60 * 1000;
return usuario.ultimaAtividade >= cincoMinutosAtras;
}
});

File diff suppressed because it is too large Load Diff