- Aceitar convite: cria User com mesmo ID do AuthUser - Criar usuario admin: usa ID do AuthUser no upsert do User - Garante sincronismo entre tabelas de auth e dominio 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
335 lines
9.8 KiB
TypeScript
335 lines
9.8 KiB
TypeScript
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<string, Date>()
|
|
for (const sessionRow of sessions) {
|
|
if (!sessionByUserId.has(sessionRow.userId)) {
|
|
sessionByUserId.set(sessionRow.userId, sessionRow.updatedAt)
|
|
}
|
|
}
|
|
|
|
const authByEmail = new Map<string, { id: string; updatedAt: Date; createdAt: Date }>()
|
|
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) })
|
|
}
|