refactor: improve layout and backend monitoring functionality - Streamlined the layout component in Svelte for better readability and consistency. - Enhanced the backend monitoring functions by updating argument structures and improving code clarity. - A #10

Merged
killer-cf merged 15 commits from feat-better-auth into master 2025-11-08 22:27:29 +00:00
93 changed files with 17981 additions and 13023 deletions
Showing only changes of commit 5d76c375c2 - Show all commits

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff