import { NextResponse } from "next/server" import { hashPassword } from "better-auth/crypto" import { ConvexHttpClient } from "convex/browser" import type { UserRole } from "@/lib/prisma" import type { Id } from "@/convex/_generated/dataModel" import { prisma } from "@/lib/prisma" import { DEFAULT_TENANT_ID } from "@/lib/constants" import { assertAdminSession, assertStaffSession } from "@/lib/auth-server" import { isAdmin } from "@/lib/authz" import { api } from "@/convex/_generated/api" export const runtime = "nodejs" const ALLOWED_ROLES = ["MANAGER", "COLLABORATOR"] as const type AllowedRole = (typeof ALLOWED_ROLES)[number] function normalizeRole(role?: string | null): AllowedRole { const normalized = (role ?? "COLLABORATOR").toUpperCase() return ALLOWED_ROLES.includes(normalized as AllowedRole) ? (normalized as AllowedRole) : "COLLABORATOR" } function generatePassword(length = 12) { const charset = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789!@#$%&*?" let password = "" const array = new Uint32Array(length) crypto.getRandomValues(array) for (let index = 0; index < length; index += 1) { password += charset[array[index] % charset.length] } return password } export async function GET() { const session = await assertStaffSession() if (!session) { return NextResponse.json({ error: "Não autorizado" }, { status: 401 }) } const tenantId = session.user.tenantId ?? DEFAULT_TENANT_ID const users = await prisma.user.findMany({ where: { tenantId, role: { in: [...ALLOWED_ROLES] }, }, include: { company: { select: { id: true, name: true, }, }, manager: { select: { id: true, name: true, email: true, }, }, }, orderBy: { createdAt: "desc" }, }) const emails = users.map((user) => user.email) const authUsers = await prisma.authUser.findMany({ where: { email: { in: emails } }, select: { id: true, email: true, updatedAt: true, createdAt: true, }, }) const sessions = await prisma.authSession.findMany({ where: { userId: { in: authUsers.map((authUser) => authUser.id) } }, orderBy: { updatedAt: "desc" }, select: { userId: true, updatedAt: true, }, }) const sessionByUserId = new Map() for (const sessionRow of sessions) { if (!sessionByUserId.has(sessionRow.userId)) { sessionByUserId.set(sessionRow.userId, sessionRow.updatedAt) } } const authByEmail = new Map() for (const authUser of authUsers) { authByEmail.set(authUser.email.toLowerCase(), { id: authUser.id, updatedAt: authUser.updatedAt, createdAt: authUser.createdAt, }) } const items = users.map((user) => { const auth = authByEmail.get(user.email.toLowerCase()) const lastSeenAt = auth ? sessionByUserId.get(auth.id) ?? auth.updatedAt : null return { id: user.id, email: user.email, name: user.name, role: normalizeRole(user.role), companyId: user.companyId, companyName: user.company?.name ?? null, jobTitle: user.jobTitle ?? null, managerId: user.managerId, managerName: user.manager?.name ?? null, managerEmail: user.manager?.email ?? null, tenantId: user.tenantId, createdAt: user.createdAt.toISOString(), updatedAt: user.updatedAt.toISOString(), authUserId: auth?.id ?? null, lastSeenAt: lastSeenAt ? lastSeenAt.toISOString() : null, } }) return NextResponse.json({ items }) } 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 { name?: string email?: string role?: string tenantId?: string jobTitle?: string | null managerId?: string | null } | null if (!body || typeof body !== "object") { return NextResponse.json({ error: "Payload inválido" }, { status: 400 }) } const name = body.name?.trim() ?? "" const email = body.email?.trim().toLowerCase() ?? "" const tenantId = (body.tenantId ?? session.user.tenantId ?? DEFAULT_TENANT_ID).trim() || DEFAULT_TENANT_ID if (!name) { return NextResponse.json({ error: "Informe o nome do usuário" }, { status: 400 }) } if (!email || !email.includes("@")) { return NextResponse.json({ error: "Informe um e-mail válido" }, { status: 400 }) } const rawJobTitle = typeof body.jobTitle === "string" ? body.jobTitle.trim() : null const jobTitle = rawJobTitle ? rawJobTitle : null const rawManagerId = typeof body.managerId === "string" ? body.managerId.trim() : "" const managerId = rawManagerId.length > 0 ? rawManagerId : null let managerRecord: { id: string; email: string; tenantId: string; name: string } | null = null if (managerId) { managerRecord = await prisma.user.findUnique({ where: { id: managerId }, select: { id: true, email: true, tenantId: true, name: true }, }) if (!managerRecord) { return NextResponse.json({ error: "Gestor informado não foi encontrado." }, { status: 400 }) } if (managerRecord.tenantId !== tenantId) { return NextResponse.json({ error: "Gestor pertence a outro cliente." }, { status: 400 }) } } const normalizedRole = normalizeRole(body.role) const authRole = normalizedRole.toLowerCase() const userRole = normalizedRole as UserRole const existingAuth = await prisma.authUser.findUnique({ where: { email } }) if (existingAuth) { return NextResponse.json({ error: "Já existe um usuário com este e-mail" }, { status: 409 }) } const temporaryPassword = generatePassword() const hashedPassword = await hashPassword(temporaryPassword) const [authUser, domainUser] = await prisma.$transaction(async (tx) => { const createdAuthUser = await tx.authUser.create({ data: { email, name, role: authRole, tenantId, accounts: { create: { providerId: "credential", accountId: email, password: hashedPassword, }, }, }, }) const createdDomainUser = await tx.user.upsert({ where: { id: createdAuthUser.id }, update: { name, role: userRole, tenantId, jobTitle, managerId: managerRecord?.id ?? null, }, create: { id: createdAuthUser.id, name, email, role: userRole, tenantId, jobTitle, managerId: managerRecord?.id ?? null, }, }) return [createdAuthUser, createdDomainUser] as const }) const convexUrl = process.env.NEXT_PUBLIC_CONVEX_URL if (convexUrl) { try { const convex = new ConvexHttpClient(convexUrl) let managerConvexId: Id<"users"> | undefined if (managerRecord?.email) { try { const convexManager = await convex.query(api.users.findByEmail, { tenantId, email: managerRecord.email, }) if (convexManager?._id) { managerConvexId = convexManager._id as Id<"users"> } } catch (error) { console.warn("[admin/users] Falha ao localizar gestor no Convex", error) } } await convex.mutation(api.users.ensureUser, { tenantId, email, name, avatarUrl: authUser.avatarUrl ?? undefined, role: userRole, jobTitle: jobTitle ?? undefined, managerId: managerConvexId, }) } catch (error) { console.error("[admin/users] ensureUser failed", error) } } return NextResponse.json({ user: { id: domainUser.id, authUserId: authUser.id, email: domainUser.email, name: domainUser.name, role: authRole, tenantId: domainUser.tenantId, createdAt: domainUser.createdAt.toISOString(), jobTitle, managerId: managerRecord?.id ?? null, managerName: managerRecord?.name ?? null, managerEmail: managerRecord?.email ?? null, }, temporaryPassword, }) } export async function DELETE(request: Request) { const session = await assertStaffSession() if (!session) { return NextResponse.json({ error: "Não autorizado" }, { status: 401 }) } if (!isAdmin(session.user.role)) { return NextResponse.json({ error: "Apenas administradores podem excluir usuários." }, { status: 403 }) } const json = await request.json().catch(() => null) const ids = Array.isArray(json?.ids) ? (json.ids as string[]) : [] if (ids.length === 0) { return NextResponse.json({ error: "Nenhum usuário selecionado." }, { status: 400 }) } const tenantId = session.user.tenantId ?? DEFAULT_TENANT_ID const users = await prisma.user.findMany({ where: { id: { in: ids }, tenantId, role: { in: [...ALLOWED_ROLES] }, }, select: { id: true, email: true, }, }) if (users.length === 0) { return NextResponse.json({ deletedIds: [] }) } const emails = users.map((user) => user.email.toLowerCase()) const authUsers = await prisma.authUser.findMany({ where: { email: { in: emails }, }, select: { id: true, }, }) const authUserIds = authUsers.map((authUser) => authUser.id) await prisma.$transaction(async (tx) => { if (authUserIds.length > 0) { await tx.authSession.deleteMany({ where: { userId: { in: authUserIds } } }) await tx.authAccount.deleteMany({ where: { userId: { in: authUserIds } } }) await tx.authUser.deleteMany({ where: { id: { in: authUserIds } } }) } await tx.user.deleteMany({ where: { id: { in: users.map((user) => user.id) } } }) }) return NextResponse.json({ deletedIds: users.map((user) => user.id) }) }