chore: reorganize project structure and ensure default queues

This commit is contained in:
Esdras Renan 2025-10-06 22:59:35 -03:00
parent 854887f499
commit 1cccb852a5
201 changed files with 417 additions and 838 deletions

View file

@ -1,205 +0,0 @@
import { NextResponse } from "next/server"
import { randomBytes } from "crypto"
import { ConvexHttpClient } from "convex/browser"
// @ts-expect-error Convex runtime API lacks generated types at build time in Next routes
import { api } from "@/convex/_generated/api"
import { assertAdminSession } from "@/lib/auth-server"
import { DEFAULT_TENANT_ID } from "@/lib/constants"
import { ROLE_OPTIONS, type RoleOption } from "@/lib/authz"
import { env } from "@/lib/env"
import { prisma } from "@/lib/prisma"
import { computeInviteStatus, normalizeInvite, type InviteWithEvents, type NormalizedInvite } from "@/server/invite-utils"
const DEFAULT_EXPIRATION_DAYS = 7
function normalizeRole(input: string | null | undefined): RoleOption {
const role = (input ?? "agent").toLowerCase() as RoleOption
return (ROLE_OPTIONS as readonly string[]).includes(role) ? role : "agent"
}
function generateToken() {
return randomBytes(32).toString("hex")
}
async function syncInviteWithConvex(invite: NormalizedInvite) {
const convexUrl = env.NEXT_PUBLIC_CONVEX_URL
if (!convexUrl) return
const client = new ConvexHttpClient(convexUrl)
await client.mutation(api.invites.sync, {
tenantId: invite.tenantId,
inviteId: invite.id,
email: invite.email,
name: invite.name ?? undefined,
role: invite.role.toUpperCase(),
status: invite.status,
token: invite.token,
expiresAt: Date.parse(invite.expiresAt),
createdAt: Date.parse(invite.createdAt),
createdById: invite.createdById ?? undefined,
acceptedAt: invite.acceptedAt ? Date.parse(invite.acceptedAt) : undefined,
acceptedById: invite.acceptedById ?? undefined,
revokedAt: invite.revokedAt ? Date.parse(invite.revokedAt) : undefined,
revokedById: invite.revokedById ?? undefined,
revokedReason: invite.revokedReason ?? undefined,
})
}
async function appendEvent(inviteId: string, type: string, actorId: string | null, payload: unknown = null) {
return prisma.authInviteEvent.create({
data: {
inviteId,
type,
payload,
actorId,
},
})
}
async function refreshInviteStatus(invite: InviteWithEvents, now: Date) {
const computedStatus = computeInviteStatus(invite, now)
if (computedStatus === invite.status) {
return invite
}
const updated = await prisma.authInvite.update({
where: { id: invite.id },
data: { status: computedStatus },
})
const event = await appendEvent(invite.id, computedStatus, null)
const inviteWithEvents: InviteWithEvents = {
...updated,
events: [...invite.events, event],
}
return inviteWithEvents
}
function buildInvitePayload(invite: InviteWithEvents, now: Date) {
const normalized = normalizeInvite(invite, now)
return normalized
}
export async function GET() {
const session = await assertAdminSession()
if (!session) {
return NextResponse.json({ error: "Não autorizado" }, { status: 401 })
}
const invites = await prisma.authInvite.findMany({
orderBy: { createdAt: "desc" },
include: {
events: {
orderBy: { createdAt: "asc" },
},
},
})
const now = new Date()
const results: NormalizedInvite[] = []
for (const invite of invites) {
const updatedInvite = await refreshInviteStatus(invite, now)
const normalized = buildInvitePayload(updatedInvite, now)
await syncInviteWithConvex(normalized)
results.push(normalized)
}
return NextResponse.json({ invites: results })
}
type CreateInvitePayload = {
email: string
name?: string
role?: RoleOption
tenantId?: string
expiresInDays?: number
}
export async function POST(request: Request) {
const session = await assertAdminSession()
if (!session) {
return NextResponse.json({ error: "Não autorizado" }, { status: 401 })
}
const body = (await request.json().catch(() => null)) as Partial<CreateInvitePayload> | null
if (!body || typeof body !== "object") {
return NextResponse.json({ error: "Payload inválido" }, { status: 400 })
}
const email = typeof body.email === "string" ? body.email.trim().toLowerCase() : ""
if (!email || !email.includes("@")) {
return NextResponse.json({ error: "Informe um e-mail válido" }, { status: 400 })
}
const name = typeof body.name === "string" ? body.name.trim() : undefined
const role = normalizeRole(body.role)
const tenantId = typeof body.tenantId === "string" && body.tenantId.trim() ? body.tenantId.trim() : session.user.tenantId || DEFAULT_TENANT_ID
const expiresInDays = Number.isFinite(body.expiresInDays) ? Math.max(1, Number(body.expiresInDays)) : DEFAULT_EXPIRATION_DAYS
const existing = await prisma.authInvite.findFirst({
where: {
email,
status: { in: ["pending", "accepted"] },
},
include: { events: true },
})
const now = new Date()
if (existing) {
const computed = computeInviteStatus(existing, now)
if (computed === "pending") {
return NextResponse.json({ error: "Já existe um convite pendente para este e-mail" }, { status: 409 })
}
if (computed === "accepted") {
return NextResponse.json({ error: "Este e-mail já possui acesso ativo" }, { status: 409 })
}
if (existing.status !== computed) {
await prisma.authInvite.update({ where: { id: existing.id }, data: { status: computed } })
await appendEvent(existing.id, computed, session.user.id ?? null)
const refreshed = await prisma.authInvite.findUnique({
where: { id: existing.id },
include: { events: true },
})
if (refreshed) {
const normalizedExisting = buildInvitePayload(refreshed, now)
await syncInviteWithConvex(normalizedExisting)
}
}
}
const token = generateToken()
const expiresAt = new Date(Date.now() + expiresInDays * 24 * 60 * 60 * 1000)
const invite = await prisma.authInvite.create({
data: {
email,
name,
role,
tenantId,
token,
status: "pending",
expiresAt,
createdById: session.user.id ?? null,
},
})
const event = await appendEvent(invite.id, "created", session.user.id ?? null, {
role,
tenantId,
expiresAt: expiresAt.toISOString(),
})
const inviteWithEvents: InviteWithEvents = {
...invite,
events: [event],
}
const normalized = buildInvitePayload(inviteWithEvents, now)
await syncInviteWithConvex(normalized)
return NextResponse.json({ invite: normalized })
}