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

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