feat: sistema completo de notificacoes por e-mail
Implementa sistema de notificacoes por e-mail com: - Notificacoes de ciclo de vida (abertura, resolucao, atribuicao, status) - Sistema de avaliacao de chamados com estrelas (1-5) - Deep linking via protocolo raven:// para abrir chamados no desktop - Tokens de acesso seguro para visualizacao sem login - Preferencias de notificacao configuraveis por usuario - Templates HTML responsivos com design tokens da plataforma - API completa para preferencias, tokens e avaliacoes Modelos Prisma: - TicketRating: avaliacoes de chamados - TicketAccessToken: tokens de acesso direto - NotificationPreferences: preferencias por usuario Turbopack como bundler padrao (Next.js 16) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
cb6add1a4a
commit
f2c0298285
23 changed files with 4387 additions and 9 deletions
171
src/server/notification/token-service.ts
Normal file
171
src/server/notification/token-service.ts
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
/**
|
||||
* Serviço de Tokens de Acesso
|
||||
* Gera e valida tokens para acesso direto aos chamados
|
||||
* Sistema de Chamados Raven
|
||||
*/
|
||||
|
||||
import { prisma } from "@/lib/prisma"
|
||||
import { randomBytes } from "crypto"
|
||||
|
||||
// ============================================
|
||||
// Tipos
|
||||
// ============================================
|
||||
|
||||
export interface GenerateTokenOptions {
|
||||
tenantId: string
|
||||
ticketId: string
|
||||
userId: string
|
||||
machineId?: string
|
||||
scope: "view" | "interact" | "rate"
|
||||
expiresInDays?: number
|
||||
}
|
||||
|
||||
export interface ValidatedToken {
|
||||
id: string
|
||||
tenantId: string
|
||||
ticketId: string
|
||||
userId: string
|
||||
machineId: string | null
|
||||
scope: string
|
||||
expiresAt: Date
|
||||
usedAt: Date | null
|
||||
createdAt: Date
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Geração de Token
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Gera um token seguro de acesso ao chamado
|
||||
*/
|
||||
export async function generateAccessToken(options: GenerateTokenOptions): Promise<string> {
|
||||
const {
|
||||
tenantId,
|
||||
ticketId,
|
||||
userId,
|
||||
machineId,
|
||||
scope,
|
||||
expiresInDays = 7,
|
||||
} = options
|
||||
|
||||
// Gera um token aleatório de 32 bytes (64 caracteres hex)
|
||||
const token = randomBytes(32).toString("hex")
|
||||
|
||||
// Calcula a data de expiração
|
||||
const expiresAt = new Date()
|
||||
expiresAt.setDate(expiresAt.getDate() + expiresInDays)
|
||||
|
||||
// Salva o token no banco
|
||||
await prisma.ticketAccessToken.create({
|
||||
data: {
|
||||
tenantId,
|
||||
token,
|
||||
ticketId,
|
||||
userId,
|
||||
machineId,
|
||||
scope,
|
||||
expiresAt,
|
||||
},
|
||||
})
|
||||
|
||||
return token
|
||||
}
|
||||
|
||||
/**
|
||||
* Valida um token de acesso
|
||||
*/
|
||||
export async function validateAccessToken(token: string): Promise<ValidatedToken | null> {
|
||||
const tokenRecord = await prisma.ticketAccessToken.findUnique({
|
||||
where: { token },
|
||||
})
|
||||
|
||||
if (!tokenRecord) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Verifica se o token expirou
|
||||
if (tokenRecord.expiresAt < new Date()) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
id: tokenRecord.id,
|
||||
tenantId: tokenRecord.tenantId,
|
||||
ticketId: tokenRecord.ticketId,
|
||||
userId: tokenRecord.userId,
|
||||
machineId: tokenRecord.machineId,
|
||||
scope: tokenRecord.scope,
|
||||
expiresAt: tokenRecord.expiresAt,
|
||||
usedAt: tokenRecord.usedAt,
|
||||
createdAt: tokenRecord.createdAt,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Marca um token como usado
|
||||
*/
|
||||
export async function markTokenAsUsed(token: string): Promise<void> {
|
||||
await prisma.ticketAccessToken.update({
|
||||
where: { token },
|
||||
data: { usedAt: new Date() },
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalida todos os tokens de um chamado
|
||||
*/
|
||||
export async function invalidateTicketTokens(ticketId: string): Promise<void> {
|
||||
await prisma.ticketAccessToken.updateMany({
|
||||
where: { ticketId },
|
||||
data: { expiresAt: new Date() },
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalida todos os tokens de um usuário
|
||||
*/
|
||||
export async function invalidateUserTokens(userId: string): Promise<void> {
|
||||
await prisma.ticketAccessToken.updateMany({
|
||||
where: { userId },
|
||||
data: { expiresAt: new Date() },
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Limpa tokens expirados (pode ser chamado periodicamente)
|
||||
*/
|
||||
export async function cleanupExpiredTokens(): Promise<number> {
|
||||
const result = await prisma.ticketAccessToken.deleteMany({
|
||||
where: {
|
||||
expiresAt: {
|
||||
lt: new Date(),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return result.count
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica se um token tem o escopo necessário
|
||||
*/
|
||||
export function hasScope(tokenScope: string, requiredScope: "view" | "interact" | "rate"): boolean {
|
||||
const scopeHierarchy: Record<string, number> = {
|
||||
view: 1,
|
||||
interact: 2,
|
||||
rate: 3,
|
||||
}
|
||||
|
||||
const tokenLevel = scopeHierarchy[tokenScope] ?? 0
|
||||
const requiredLevel = scopeHierarchy[requiredScope] ?? 0
|
||||
|
||||
// rate é especial, só pode avaliar
|
||||
if (requiredScope === "rate") {
|
||||
return tokenScope === "rate"
|
||||
}
|
||||
|
||||
// interact permite view
|
||||
// view só permite view
|
||||
return tokenLevel >= requiredLevel
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue