feat: cadastro manual de acesso remoto e ajustes de horas
This commit is contained in:
parent
8e3cbc7a9a
commit
f3a7045691
16 changed files with 1549 additions and 207 deletions
89
src/app/api/admin/machines/remote-access/route.ts
Normal file
89
src/app/api/admin/machines/remote-access/route.ts
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
import { NextResponse } from "next/server"
|
||||
import { z } from "zod"
|
||||
import { ConvexHttpClient } from "convex/browser"
|
||||
|
||||
import { assertStaffSession } from "@/lib/auth-server"
|
||||
import type { Id } from "@/convex/_generated/dataModel"
|
||||
|
||||
export const runtime = "nodejs"
|
||||
|
||||
const schema = z.object({
|
||||
machineId: z.string().min(1),
|
||||
provider: z.string().optional(),
|
||||
identifier: z.string().optional(),
|
||||
url: z.string().optional(),
|
||||
notes: z.string().optional(),
|
||||
action: z.enum(["save", "clear"]).optional(),
|
||||
})
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const session = await assertStaffSession()
|
||||
if (!session) {
|
||||
return NextResponse.json({ error: "Não autorizado" }, { status: 401 })
|
||||
}
|
||||
if (!session.user.id) {
|
||||
return NextResponse.json({ error: "Sessão inválida" }, { status: 400 })
|
||||
}
|
||||
|
||||
let parsed: z.infer<typeof schema>
|
||||
try {
|
||||
const body = await request.json()
|
||||
parsed = schema.parse(body)
|
||||
} catch {
|
||||
return NextResponse.json({ error: "Payload inválido" }, { status: 400 })
|
||||
}
|
||||
|
||||
const convexUrl = process.env.NEXT_PUBLIC_CONVEX_URL
|
||||
if (!convexUrl) {
|
||||
return NextResponse.json({ error: "Convex não configurado" }, { status: 500 })
|
||||
}
|
||||
|
||||
const client = new ConvexHttpClient(convexUrl)
|
||||
|
||||
try {
|
||||
const action = parsed.action ?? "save"
|
||||
|
||||
if (action === "clear") {
|
||||
const result = (await (client as unknown as { mutation: (name: string, args: Record<string, unknown>) => Promise<unknown> }).mutation("machines:updateRemoteAccess", {
|
||||
machineId: parsed.machineId as Id<"machines">,
|
||||
actorId: session.user.id as Id<"users">,
|
||||
clear: true,
|
||||
})) as { remoteAccess?: unknown } | null
|
||||
return NextResponse.json({ ok: true, remoteAccess: result?.remoteAccess ?? null })
|
||||
}
|
||||
|
||||
const provider = (parsed.provider ?? "").trim()
|
||||
const identifier = (parsed.identifier ?? "").trim()
|
||||
if (!provider || !identifier) {
|
||||
return NextResponse.json({ error: "Informe o provedor e o identificador do acesso remoto." }, { status: 400 })
|
||||
}
|
||||
|
||||
let normalizedUrl: string | undefined
|
||||
const rawUrl = (parsed.url ?? "").trim()
|
||||
if (rawUrl.length > 0) {
|
||||
const candidate = /^https?:\/\//i.test(rawUrl) ? rawUrl : `https://${rawUrl}`
|
||||
try {
|
||||
new URL(candidate)
|
||||
normalizedUrl = candidate
|
||||
} catch {
|
||||
return NextResponse.json({ error: "URL inválida. Informe um endereço iniciado com http:// ou https://." }, { status: 422 })
|
||||
}
|
||||
}
|
||||
|
||||
const notes = (parsed.notes ?? "").trim()
|
||||
|
||||
const result = (await (client as unknown as { mutation: (name: string, args: Record<string, unknown>) => Promise<unknown> }).mutation("machines:updateRemoteAccess", {
|
||||
machineId: parsed.machineId as Id<"machines">,
|
||||
actorId: session.user.id as Id<"users">,
|
||||
provider,
|
||||
identifier,
|
||||
url: normalizedUrl,
|
||||
notes: notes.length ? notes : undefined,
|
||||
})) as { remoteAccess?: unknown } | null
|
||||
|
||||
return NextResponse.json({ ok: true, remoteAccess: result?.remoteAccess ?? null })
|
||||
} catch (error) {
|
||||
console.error("[machines.remote-access]", error)
|
||||
return NextResponse.json({ error: "Falha ao atualizar acesso remoto" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,10 @@
|
|||
import { NextResponse } from "next/server"
|
||||
import { randomBytes } from "crypto"
|
||||
import { ConvexHttpClient } from "convex/browser"
|
||||
|
||||
import { assertAdminSession } from "@/lib/auth-server"
|
||||
import { prisma } from "@/lib/prisma"
|
||||
import { DEFAULT_TENANT_ID } from "@/lib/constants"
|
||||
import type { Id } from "@/convex/_generated/dataModel"
|
||||
import { api } from "@/convex/_generated/api"
|
||||
|
||||
|
|
@ -23,11 +26,51 @@ export async function POST(request: Request) {
|
|||
const client = new ConvexHttpClient(convexUrl)
|
||||
|
||||
try {
|
||||
const tenantId = session.user.tenantId ?? DEFAULT_TENANT_ID
|
||||
|
||||
const company = await prisma.company.findUnique({
|
||||
where: { id: companyId },
|
||||
select: { id: true, tenantId: true, slug: true, name: true, provisioningCode: true },
|
||||
})
|
||||
|
||||
if (!company || company.tenantId !== tenantId) {
|
||||
return NextResponse.json({ error: "Empresa não encontrada" }, { status: 404 })
|
||||
}
|
||||
|
||||
let provisioningCode = company.provisioningCode
|
||||
if (!provisioningCode) {
|
||||
provisioningCode = randomBytes(16).toString("hex")
|
||||
await prisma.company.update({ where: { id: company.id }, data: { provisioningCode } })
|
||||
}
|
||||
|
||||
const ensured = await client.mutation(api.users.ensureUser, {
|
||||
tenantId,
|
||||
email: session.user.email ?? "admin@sistema.dev",
|
||||
name: session.user.name ?? session.user.email ?? "Administrador",
|
||||
avatarUrl: session.user.avatarUrl ?? undefined,
|
||||
role: session.user.role?.toUpperCase?.(),
|
||||
})
|
||||
|
||||
if (!ensured?._id) {
|
||||
throw new Error("Não foi possível identificar o ator no Convex")
|
||||
}
|
||||
|
||||
const ensuredCompany = await client.mutation(api.companies.ensureProvisioned, {
|
||||
tenantId,
|
||||
slug: company.slug ?? `company-${company.id}`,
|
||||
name: company.name,
|
||||
provisioningCode,
|
||||
})
|
||||
|
||||
if (!ensuredCompany?.id) {
|
||||
throw new Error("Falha ao sincronizar empresa no Convex")
|
||||
}
|
||||
|
||||
await client.mutation(api.users.assignCompany, {
|
||||
tenantId: session.user.tenantId ?? "tenant-atlas",
|
||||
tenantId,
|
||||
email,
|
||||
companyId: companyId as Id<"companies">,
|
||||
actorId: (session.user as unknown as { convexUserId?: Id<"users">; id?: Id<"users"> }).convexUserId ?? (session.user.id as unknown as Id<"users">),
|
||||
companyId: ensuredCompany.id as Id<"companies">,
|
||||
actorId: ensured._id,
|
||||
})
|
||||
return NextResponse.json({ ok: true })
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,14 @@
|
|||
import { NextResponse } from "next/server"
|
||||
|
||||
import { hashPassword } from "better-auth/crypto"
|
||||
import { ConvexHttpClient } from "convex/browser"
|
||||
import type { UserRole } from "@prisma/client"
|
||||
|
||||
import { prisma } from "@/lib/prisma"
|
||||
import { DEFAULT_TENANT_ID } from "@/lib/constants"
|
||||
import { assertStaffSession } from "@/lib/auth-server"
|
||||
import { assertAdminSession, assertStaffSession } from "@/lib/auth-server"
|
||||
import { isAdmin } from "@/lib/authz"
|
||||
import { api } from "@/convex/_generated/api"
|
||||
|
||||
export const runtime = "nodejs"
|
||||
|
||||
|
|
@ -16,6 +21,17 @@ function normalizeRole(role?: string | null): AllowedRole {
|
|||
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) {
|
||||
|
|
@ -97,6 +113,111 @@ export async function GET() {
|
|||
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
|
||||
} | 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 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: { email },
|
||||
update: {
|
||||
name,
|
||||
role: userRole,
|
||||
tenantId,
|
||||
},
|
||||
create: {
|
||||
name,
|
||||
email,
|
||||
role: userRole,
|
||||
tenantId,
|
||||
},
|
||||
})
|
||||
|
||||
return [createdAuthUser, createdDomainUser] as const
|
||||
})
|
||||
|
||||
const convexUrl = process.env.NEXT_PUBLIC_CONVEX_URL
|
||||
if (convexUrl) {
|
||||
try {
|
||||
const convex = new ConvexHttpClient(convexUrl)
|
||||
await convex.mutation(api.users.ensureUser, {
|
||||
tenantId,
|
||||
email,
|
||||
name,
|
||||
avatarUrl: authUser.avatarUrl ?? undefined,
|
||||
role: userRole,
|
||||
})
|
||||
} 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(),
|
||||
},
|
||||
temporaryPassword,
|
||||
})
|
||||
}
|
||||
|
||||
export async function DELETE(request: Request) {
|
||||
const session = await assertStaffSession()
|
||||
if (!session) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue