Atualiza portal e admin com bloqueio de máquinas desativadas
This commit is contained in:
parent
e5085962e9
commit
630110bf3a
31 changed files with 1756 additions and 244 deletions
156
src/app/api/admin/clients/route.ts
Normal file
156
src/app/api/admin/clients/route.ts
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
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) })
|
||||
}
|
||||
61
src/app/api/admin/machines/toggle-active/route.ts
Normal file
61
src/app/api/admin/machines/toggle-active/route.ts
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import { NextResponse } from "next/server"
|
||||
import { z } from "zod"
|
||||
import { ConvexHttpClient } from "convex/browser"
|
||||
|
||||
import { assertAuthenticatedSession } from "@/lib/auth-server"
|
||||
import { DEFAULT_TENANT_ID } from "@/lib/constants"
|
||||
import { api } from "@/convex/_generated/api"
|
||||
|
||||
export const runtime = "nodejs"
|
||||
|
||||
const schema = z.object({
|
||||
machineId: z.string().min(1),
|
||||
active: z.boolean(),
|
||||
})
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const session = await assertAuthenticatedSession()
|
||||
if (!session) {
|
||||
return NextResponse.json({ error: "Não autorizado" }, { status: 401 })
|
||||
}
|
||||
|
||||
const convexUrl = process.env.NEXT_PUBLIC_CONVEX_URL
|
||||
if (!convexUrl) {
|
||||
return NextResponse.json({ error: "Convex não configurado" }, { status: 500 })
|
||||
}
|
||||
|
||||
const payload = await request.json().catch(() => null)
|
||||
const parsed = schema.safeParse(payload)
|
||||
if (!parsed.success) {
|
||||
return NextResponse.json({ error: "Payload inválido", details: parsed.error.flatten() }, { status: 400 })
|
||||
}
|
||||
|
||||
const tenantId = session.user.tenantId ?? DEFAULT_TENANT_ID
|
||||
|
||||
try {
|
||||
const convex = new ConvexHttpClient(convexUrl)
|
||||
const ensured = await convex.mutation(api.users.ensureUser, {
|
||||
tenantId,
|
||||
email: session.user.email,
|
||||
name: session.user.name ?? session.user.email,
|
||||
avatarUrl: session.user.avatarUrl ?? undefined,
|
||||
role: session.user.role.toUpperCase(),
|
||||
})
|
||||
const actorId = ensured?._id
|
||||
if (!actorId) {
|
||||
return NextResponse.json({ error: "Falha ao obter ID do usuário no Convex" }, { status: 500 })
|
||||
}
|
||||
|
||||
const client = convex as unknown as { mutation: (name: string, args: unknown) => Promise<unknown> }
|
||||
await client.mutation("machines:toggleActive", {
|
||||
machineId: parsed.data.machineId,
|
||||
actorId,
|
||||
active: parsed.data.active,
|
||||
})
|
||||
|
||||
return NextResponse.json({ ok: true })
|
||||
} catch (error) {
|
||||
console.error("[machines.toggleActive] Falha ao atualizar status", error)
|
||||
return NextResponse.json({ error: "Falha ao atualizar status da máquina" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
37
src/app/api/auth/get-session/route.ts
Normal file
37
src/app/api/auth/get-session/route.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import { NextResponse } from "next/server"
|
||||
|
||||
import { auth } from "@/lib/auth"
|
||||
|
||||
export const runtime = "nodejs"
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const result = await auth.api.getSession({ headers: request.headers, request, asResponse: true })
|
||||
|
||||
if (!result) {
|
||||
return NextResponse.json({ user: null }, { status: 200 })
|
||||
}
|
||||
|
||||
const body = await result.json()
|
||||
const response = NextResponse.json(body, {
|
||||
status: result.status,
|
||||
})
|
||||
|
||||
const headersWithGetSetCookie = result.headers as Headers & { getSetCookie?: () => string[] | undefined }
|
||||
let setCookieHeaders =
|
||||
typeof headersWithGetSetCookie.getSetCookie === "function"
|
||||
? headersWithGetSetCookie.getSetCookie() ?? []
|
||||
: []
|
||||
|
||||
if (setCookieHeaders.length === 0) {
|
||||
const single = result.headers.get("set-cookie")
|
||||
if (single) {
|
||||
setCookieHeaders = [single]
|
||||
}
|
||||
}
|
||||
|
||||
for (const cookie of setCookieHeaders) {
|
||||
response.headers.append("set-cookie", cookie)
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
|
@ -160,6 +160,7 @@ export async function POST(request: Request) {
|
|||
name: collaborator.name ?? collaborator.email,
|
||||
tenantId,
|
||||
companyId: companyRecord.id,
|
||||
role: persona === "manager" ? "MANAGER" : "COLLABORATOR",
|
||||
})
|
||||
|
||||
if (persona) {
|
||||
|
|
|
|||
|
|
@ -22,10 +22,25 @@ const updateSchema = z.object({
|
|||
|
||||
export async function PATCH(request: Request) {
|
||||
const session = await requireAuthenticatedSession()
|
||||
const role = (session.user.role ?? "").toLowerCase()
|
||||
if (role !== "collaborator" && role !== "manager") {
|
||||
const normalizedRole = (session.user.role ?? "").toLowerCase()
|
||||
const persona = (session.user.machinePersona ?? "").toLowerCase()
|
||||
const allowedRoles = new Set(["collaborator", "manager", "admin", "agent"])
|
||||
const isMachinePersonaAllowed = normalizedRole === "machine" && (persona === "collaborator" || persona === "manager")
|
||||
if (!allowedRoles.has(normalizedRole) && !isMachinePersonaAllowed) {
|
||||
return NextResponse.json({ error: "Acesso não autorizado" }, { status: 403 })
|
||||
}
|
||||
const effectiveRole =
|
||||
normalizedRole === "admin"
|
||||
? "ADMIN"
|
||||
: normalizedRole === "agent"
|
||||
? "AGENT"
|
||||
: normalizedRole === "manager"
|
||||
? "MANAGER"
|
||||
: normalizedRole === "collaborator"
|
||||
? "COLLABORATOR"
|
||||
: persona === "manager"
|
||||
? "MANAGER"
|
||||
: "COLLABORATOR"
|
||||
|
||||
let payload: unknown
|
||||
try {
|
||||
|
|
@ -132,14 +147,14 @@ export async function PATCH(request: Request) {
|
|||
update: {
|
||||
name,
|
||||
tenantId,
|
||||
role: role === "manager" ? "MANAGER" : "COLLABORATOR",
|
||||
role: effectiveRole,
|
||||
companyId: companyId ?? undefined,
|
||||
},
|
||||
create: {
|
||||
email: effectiveEmail,
|
||||
name,
|
||||
tenantId,
|
||||
role: role === "manager" ? "MANAGER" : "COLLABORATOR",
|
||||
role: effectiveRole,
|
||||
companyId: companyId ?? undefined,
|
||||
},
|
||||
})
|
||||
|
|
@ -149,6 +164,7 @@ export async function PATCH(request: Request) {
|
|||
name,
|
||||
tenantId,
|
||||
companyId,
|
||||
role: effectiveRole,
|
||||
})
|
||||
|
||||
if (env.NEXT_PUBLIC_CONVEX_URL) {
|
||||
|
|
@ -158,7 +174,7 @@ export async function PATCH(request: Request) {
|
|||
tenantId,
|
||||
email: effectiveEmail,
|
||||
name,
|
||||
role: role === "manager" ? "MANAGER" : "COLLABORATOR",
|
||||
role: effectiveRole,
|
||||
})
|
||||
} catch (error) {
|
||||
console.warn("[portal.profile] Falha ao sincronizar usuário no Convex", error)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue