feat: cadastro manual de acesso remoto e ajustes de horas

This commit is contained in:
Esdras Renan 2025-10-24 23:52:58 -03:00
parent 8e3cbc7a9a
commit f3a7045691
16 changed files with 1549 additions and 207 deletions

View file

@ -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) {