feat: expand admin companies and users modules
This commit is contained in:
parent
a043b1203c
commit
2e3b46a7b5
31 changed files with 5626 additions and 2003 deletions
|
|
@ -1,156 +1,2 @@
|
|||
import { NextResponse } from "next/server"
|
||||
|
||||
import { prisma } from "@/lib/prisma"
|
||||
import { DEFAULT_TENANT_ID } from "@/lib/constants"
|
||||
import { assertStaffSession } from "@/lib/auth-server"
|
||||
import { isAdmin } from "@/lib/authz"
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
},
|
||||
},
|
||||
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,
|
||||
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 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 clientes." }, { 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 cliente 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) })
|
||||
}
|
||||
export { runtime } from "../users/route"
|
||||
export { GET, DELETE } from "../users/route"
|
||||
|
|
|
|||
|
|
@ -1,48 +1,99 @@
|
|||
import { NextResponse } from "next/server"
|
||||
import { ZodError } from "zod"
|
||||
import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library"
|
||||
|
||||
import { prisma } from "@/lib/prisma"
|
||||
import { assertStaffSession } from "@/lib/auth-server"
|
||||
import { isAdmin } from "@/lib/authz"
|
||||
import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library"
|
||||
import { removeConvexCompany, syncConvexCompany } from "@/server/companies-sync"
|
||||
import {
|
||||
buildCompanyData,
|
||||
fetchCompanyById,
|
||||
formatZodError,
|
||||
normalizeCompany,
|
||||
sanitizeCompanyInput,
|
||||
} from "@/server/company-service"
|
||||
import type { CompanyFormValues } from "@/lib/schemas/company"
|
||||
|
||||
export const runtime = "nodejs"
|
||||
|
||||
export async function PATCH(request: Request, { params }: { params: Promise<{ id: string }> }) {
|
||||
function mergePayload(base: CompanyFormValues, updates: Record<string, unknown>): Record<string, unknown> {
|
||||
const merged: Record<string, unknown> = { ...base, ...updates }
|
||||
|
||||
if (!("businessHours" in updates)) merged.businessHours = base.businessHours
|
||||
if (!("communicationChannels" in updates)) merged.communicationChannels = base.communicationChannels
|
||||
if (!("clientDomains" in updates)) merged.clientDomains = base.clientDomains
|
||||
if (!("fiscalAddress" in updates)) merged.fiscalAddress = base.fiscalAddress
|
||||
if (!("regulatedEnvironments" in updates)) merged.regulatedEnvironments = base.regulatedEnvironments
|
||||
if (!("contacts" in updates)) merged.contacts = base.contacts
|
||||
if (!("locations" in updates)) merged.locations = base.locations
|
||||
if (!("contracts" in updates)) merged.contracts = base.contracts
|
||||
if (!("sla" in updates)) merged.sla = base.sla
|
||||
if (!("tags" in updates)) merged.tags = base.tags
|
||||
if (!("customFields" in updates)) merged.customFields = base.customFields
|
||||
|
||||
if ("privacyPolicy" in updates) {
|
||||
const incoming = updates.privacyPolicy
|
||||
if (incoming && typeof incoming === "object") {
|
||||
merged.privacyPolicy = {
|
||||
...base.privacyPolicy,
|
||||
...incoming,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
merged.privacyPolicy = base.privacyPolicy
|
||||
}
|
||||
|
||||
merged.tenantId = base.tenantId
|
||||
return merged
|
||||
}
|
||||
|
||||
export async function PATCH(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
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 editar empresas" }, { status: 403 })
|
||||
}
|
||||
const { id } = await params
|
||||
const raw = (await request.json()) as Partial<{
|
||||
name: string
|
||||
slug: string
|
||||
cnpj: string | null
|
||||
domain: string | null
|
||||
phone: string | null
|
||||
description: string | null
|
||||
address: string | null
|
||||
isAvulso: boolean
|
||||
contractedHoursPerMonth: number | string | null
|
||||
}>
|
||||
|
||||
const updates: Record<string, unknown> = {}
|
||||
if (typeof raw.name === "string" && raw.name.trim()) updates.name = raw.name.trim()
|
||||
if (typeof raw.slug === "string" && raw.slug.trim()) updates.slug = raw.slug.trim()
|
||||
if ("cnpj" in raw) updates.cnpj = raw.cnpj ?? null
|
||||
if ("domain" in raw) updates.domain = raw.domain ?? null
|
||||
if ("phone" in raw) updates.phone = raw.phone ?? null
|
||||
if ("description" in raw) updates.description = raw.description ?? null
|
||||
if ("address" in raw) updates.address = raw.address ?? null
|
||||
if ("isAvulso" in raw) updates.isAvulso = Boolean(raw.isAvulso)
|
||||
if ("contractedHoursPerMonth" in raw) {
|
||||
const v = raw.contractedHoursPerMonth
|
||||
updates.contractedHoursPerMonth = typeof v === "number" ? v : v ? Number(v) : null
|
||||
}
|
||||
const { id } = await params
|
||||
|
||||
try {
|
||||
const company = await prisma.company.update({ where: { id }, data: updates })
|
||||
const existing = await fetchCompanyById(id)
|
||||
if (!existing) {
|
||||
return NextResponse.json({ error: "Empresa não encontrada" }, { status: 404 })
|
||||
}
|
||||
|
||||
if (existing.tenantId !== (session.user.tenantId ?? existing.tenantId)) {
|
||||
return NextResponse.json({ error: "Acesso negado" }, { status: 403 })
|
||||
}
|
||||
|
||||
const rawBody = (await request.json()) as Record<string, unknown>
|
||||
const normalized = normalizeCompany(existing)
|
||||
const {
|
||||
id: _ignoreId,
|
||||
provisioningCode: _ignoreCode,
|
||||
createdAt: _createdAt,
|
||||
updatedAt: _updatedAt,
|
||||
...baseForm
|
||||
} = normalized
|
||||
void _ignoreId
|
||||
void _ignoreCode
|
||||
void _createdAt
|
||||
void _updatedAt
|
||||
|
||||
const mergedInput = mergePayload(baseForm, rawBody)
|
||||
const form = sanitizeCompanyInput(mergedInput, existing.tenantId)
|
||||
const createData = buildCompanyData(form, existing.tenantId)
|
||||
const { tenantId: _omitTenant, ...updateData } = createData
|
||||
void _omitTenant
|
||||
|
||||
const company = await prisma.company.update({
|
||||
where: { id },
|
||||
data: updateData,
|
||||
})
|
||||
|
||||
if (company.provisioningCode) {
|
||||
const synced = await syncConvexCompany({
|
||||
|
|
@ -56,17 +107,23 @@ export async function PATCH(request: Request, { params }: { params: Promise<{ id
|
|||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({ company })
|
||||
return NextResponse.json({ company: normalizeCompany(company) })
|
||||
} catch (error) {
|
||||
console.error("Failed to update company", error)
|
||||
if (error instanceof ZodError) {
|
||||
return NextResponse.json({ error: "Dados inválidos", issues: formatZodError(error) }, { status: 422 })
|
||||
}
|
||||
if (error instanceof PrismaClientKnownRequestError && error.code === "P2002") {
|
||||
return NextResponse.json({ error: "Já existe uma empresa com este slug." }, { status: 409 })
|
||||
}
|
||||
console.error("Failed to update company", error)
|
||||
return NextResponse.json({ error: "Falha ao atualizar empresa" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(_: Request, { params }: { params: Promise<{ id: string }> }) {
|
||||
export async function DELETE(
|
||||
_: Request,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const session = await assertStaffSession()
|
||||
if (!session) return NextResponse.json({ error: "Não autorizado" }, { status: 401 })
|
||||
if (!isAdmin(session.user.role)) {
|
||||
|
|
@ -74,10 +131,7 @@ export async function DELETE(_: Request, { params }: { params: Promise<{ id: str
|
|||
}
|
||||
const { id } = await params
|
||||
|
||||
const company = await prisma.company.findUnique({
|
||||
where: { id },
|
||||
select: { id: true, tenantId: true, name: true, slug: true },
|
||||
})
|
||||
const company = await fetchCompanyById(id)
|
||||
|
||||
if (!company) {
|
||||
return NextResponse.json({ error: "Empresa não encontrada" }, { status: 404 })
|
||||
|
|
|
|||
|
|
@ -1,22 +1,31 @@
|
|||
import { NextResponse } from "next/server"
|
||||
import { randomBytes } from "crypto"
|
||||
import { ZodError } from "zod"
|
||||
import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library"
|
||||
|
||||
import { prisma } from "@/lib/prisma"
|
||||
import { assertStaffSession } from "@/lib/auth-server"
|
||||
import { isAdmin } from "@/lib/authz"
|
||||
import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library"
|
||||
import { syncConvexCompany } from "@/server/companies-sync"
|
||||
import {
|
||||
buildCompanyData,
|
||||
fetchCompaniesByTenant,
|
||||
formatZodError,
|
||||
normalizeCompany,
|
||||
sanitizeCompanyInput,
|
||||
} from "@/server/company-service"
|
||||
|
||||
export const runtime = "nodejs"
|
||||
|
||||
const DEFAULT_TENANT_ID = "tenant-atlas"
|
||||
|
||||
export async function GET() {
|
||||
const session = await assertStaffSession()
|
||||
if (!session) return NextResponse.json({ error: "Não autorizado" }, { status: 401 })
|
||||
|
||||
const companies = await prisma.company.findMany({
|
||||
orderBy: { name: "asc" },
|
||||
})
|
||||
return NextResponse.json({ companies })
|
||||
const tenantId = session.user.tenantId ?? DEFAULT_TENANT_ID
|
||||
const companies = await fetchCompaniesByTenant(tenantId)
|
||||
return NextResponse.json({ companies: companies.map(normalizeCompany) })
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
|
|
@ -26,43 +35,16 @@ export async function POST(request: Request) {
|
|||
return NextResponse.json({ error: "Apenas administradores podem criar empresas" }, { status: 403 })
|
||||
}
|
||||
|
||||
const body = (await request.json()) as Partial<{
|
||||
name: string
|
||||
slug: string
|
||||
isAvulso: boolean
|
||||
contractedHoursPerMonth: number | string | null
|
||||
cnpj: string | null
|
||||
domain: string | null
|
||||
phone: string | null
|
||||
description: string | null
|
||||
address: string | null
|
||||
}>
|
||||
const { name, slug, isAvulso, contractedHoursPerMonth, cnpj, domain, phone, description, address } = body ?? {}
|
||||
if (!name || !slug) {
|
||||
return NextResponse.json({ error: "Nome e slug são obrigatórios" }, { status: 400 })
|
||||
}
|
||||
|
||||
try {
|
||||
const rawBody = await request.json()
|
||||
const tenantId = session.user.tenantId ?? DEFAULT_TENANT_ID
|
||||
const form = sanitizeCompanyInput(rawBody, tenantId)
|
||||
const provisioningCode = randomBytes(32).toString("hex")
|
||||
|
||||
const company = await prisma.company.create({
|
||||
data: {
|
||||
tenantId: session.user.tenantId ?? "tenant-atlas",
|
||||
name: String(name),
|
||||
slug: String(slug),
|
||||
...buildCompanyData(form, tenantId),
|
||||
provisioningCode,
|
||||
// Campos opcionais
|
||||
isAvulso: Boolean(isAvulso ?? false),
|
||||
contractedHoursPerMonth:
|
||||
typeof contractedHoursPerMonth === "number"
|
||||
? contractedHoursPerMonth
|
||||
: contractedHoursPerMonth
|
||||
? Number(contractedHoursPerMonth)
|
||||
: null,
|
||||
cnpj: cnpj ? String(cnpj) : null,
|
||||
domain: domain ? String(domain) : null,
|
||||
phone: phone ? String(phone) : null,
|
||||
description: description ? String(description) : null,
|
||||
address: address ? String(address) : null,
|
||||
},
|
||||
})
|
||||
|
||||
|
|
@ -78,16 +60,18 @@ export async function POST(request: Request) {
|
|||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({ company })
|
||||
return NextResponse.json({ company: normalizeCompany(company) })
|
||||
} catch (error) {
|
||||
console.error("Failed to create company", error)
|
||||
if (error instanceof ZodError) {
|
||||
return NextResponse.json({ error: "Dados inválidos", issues: formatZodError(error) }, { status: 422 })
|
||||
}
|
||||
if (error instanceof PrismaClientKnownRequestError && error.code === "P2002") {
|
||||
// Duplicidade de slug por tenant ou provisioningCode único
|
||||
return NextResponse.json(
|
||||
{ error: "Já existe uma empresa com este slug ou código de provisionamento." },
|
||||
{ status: 409 }
|
||||
)
|
||||
}
|
||||
console.error("Failed to create company", error)
|
||||
return NextResponse.json({ error: "Falha ao criar empresa" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,35 +1,19 @@
|
|||
import { NextResponse } from "next/server"
|
||||
import { randomBytes } from "crypto"
|
||||
|
||||
import { hashPassword } from "better-auth/crypto"
|
||||
import { ConvexHttpClient } from "convex/browser"
|
||||
|
||||
import type { UserRole } from "@prisma/client"
|
||||
import { api } from "@/convex/_generated/api"
|
||||
import { prisma } from "@/lib/prisma"
|
||||
import { DEFAULT_TENANT_ID } from "@/lib/constants"
|
||||
import { assertStaffSession } from "@/lib/auth-server"
|
||||
import { ROLE_OPTIONS, type RoleOption, isAdmin } from "@/lib/authz"
|
||||
import { isAdmin } from "@/lib/authz"
|
||||
|
||||
export const runtime = "nodejs"
|
||||
|
||||
function normalizeRole(input: string | null | undefined): RoleOption {
|
||||
const role = (input ?? "agent").toLowerCase() as RoleOption
|
||||
return (ROLE_OPTIONS as readonly string[]).includes(role) ? role : "agent"
|
||||
}
|
||||
const ALLOWED_ROLES = ["MANAGER", "COLLABORATOR"] as const
|
||||
|
||||
const USER_ROLE_OPTIONS: ReadonlyArray<UserRole> = ["ADMIN", "MANAGER", "AGENT", "COLLABORATOR"]
|
||||
type AllowedRole = (typeof ALLOWED_ROLES)[number]
|
||||
|
||||
function mapToUserRole(role: RoleOption): UserRole {
|
||||
const candidate = role.toUpperCase() as UserRole
|
||||
return USER_ROLE_OPTIONS.includes(candidate) ? candidate : "AGENT"
|
||||
}
|
||||
|
||||
function generatePassword(length = 12) {
|
||||
const bytes = randomBytes(length)
|
||||
return Array.from(bytes)
|
||||
.map((byte) => (byte % 36).toString(36))
|
||||
.join("")
|
||||
function normalizeRole(role?: string | null): AllowedRole {
|
||||
const normalized = (role ?? "COLLABORATOR").toUpperCase()
|
||||
return ALLOWED_ROLES.includes(normalized as AllowedRole) ? (normalized as AllowedRole) : "COLLABORATOR"
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
|
|
@ -38,111 +22,135 @@ export async function GET() {
|
|||
return NextResponse.json({ error: "Não autorizado" }, { status: 401 })
|
||||
}
|
||||
|
||||
const users = await prisma.authUser.findMany({
|
||||
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,
|
||||
},
|
||||
},
|
||||
},
|
||||
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,
|
||||
name: true,
|
||||
role: true,
|
||||
tenantId: 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,
|
||||
},
|
||||
})
|
||||
|
||||
return NextResponse.json({ users })
|
||||
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,
|
||||
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) {
|
||||
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 criar usuários" }, { status: 403 })
|
||||
return NextResponse.json({ error: "Apenas administradores podem excluir usuários." }, { status: 403 })
|
||||
}
|
||||
|
||||
const payload = await request.json().catch(() => null)
|
||||
if (!payload || typeof payload !== "object") {
|
||||
return NextResponse.json({ error: "Payload inválido" }, { status: 400 })
|
||||
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 emailInput = typeof payload.email === "string" ? payload.email.trim().toLowerCase() : ""
|
||||
const nameInput = typeof payload.name === "string" ? payload.name.trim() : ""
|
||||
const roleInput = typeof payload.role === "string" ? payload.role : undefined
|
||||
const tenantInput = typeof payload.tenantId === "string" ? payload.tenantId.trim() : undefined
|
||||
const tenantId = session.user.tenantId ?? DEFAULT_TENANT_ID
|
||||
|
||||
if (!emailInput || !emailInput.includes("@")) {
|
||||
return NextResponse.json({ error: "Informe um e-mail válido" }, { status: 400 })
|
||||
}
|
||||
|
||||
const role = normalizeRole(roleInput)
|
||||
const tenantId = tenantInput || session.user.tenantId || DEFAULT_TENANT_ID
|
||||
const userRole = mapToUserRole(role)
|
||||
|
||||
const existing = await prisma.authUser.findUnique({ where: { email: emailInput } })
|
||||
if (existing) {
|
||||
return NextResponse.json({ error: "Já existe um usuário com este e-mail" }, { status: 409 })
|
||||
}
|
||||
|
||||
const password = generatePassword()
|
||||
const hashedPassword = await hashPassword(password)
|
||||
|
||||
const user = await prisma.authUser.create({
|
||||
data: {
|
||||
email: emailInput,
|
||||
name: nameInput || emailInput,
|
||||
role,
|
||||
const users = await prisma.user.findMany({
|
||||
where: {
|
||||
id: { in: ids },
|
||||
tenantId,
|
||||
accounts: {
|
||||
create: {
|
||||
providerId: "credential",
|
||||
accountId: emailInput,
|
||||
password: hashedPassword,
|
||||
},
|
||||
},
|
||||
role: { in: [...ALLOWED_ROLES] },
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
role: true,
|
||||
tenantId: true,
|
||||
createdAt: true,
|
||||
},
|
||||
})
|
||||
|
||||
await prisma.user.upsert({
|
||||
where: { email: user.email },
|
||||
update: {
|
||||
name: user.name ?? user.email,
|
||||
role: userRole,
|
||||
tenantId,
|
||||
},
|
||||
create: {
|
||||
email: user.email,
|
||||
name: user.name ?? user.email,
|
||||
role: userRole,
|
||||
tenantId,
|
||||
},
|
||||
})
|
||||
|
||||
const convexUrl = process.env.NEXT_PUBLIC_CONVEX_URL
|
||||
if (convexUrl) {
|
||||
try {
|
||||
const convex = new ConvexHttpClient(convexUrl)
|
||||
await convex.mutation(api.users.ensureUser, {
|
||||
tenantId,
|
||||
email: emailInput,
|
||||
name: nameInput || emailInput,
|
||||
avatarUrl: undefined,
|
||||
role: userRole,
|
||||
})
|
||||
} catch (error) {
|
||||
console.warn("Falha ao sincronizar usuário no Convex", error)
|
||||
}
|
||||
if (users.length === 0) {
|
||||
return NextResponse.json({ deletedIds: [] })
|
||||
}
|
||||
|
||||
return NextResponse.json({ user, temporaryPassword: password })
|
||||
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) })
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue