chore: reorganize project structure and ensure default queues
This commit is contained in:
parent
854887f499
commit
1cccb852a5
201 changed files with 417 additions and 838 deletions
196
src/app/api/invites/[token]/route.ts
Normal file
196
src/app/api/invites/[token]/route.ts
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
import { NextResponse } from "next/server"
|
||||
|
||||
import { hashPassword } from "better-auth/crypto"
|
||||
import { ConvexHttpClient } from "convex/browser"
|
||||
|
||||
// @ts-expect-error Convex generated API lacks types in Next routes
|
||||
import { api } from "@/convex/_generated/api"
|
||||
import { DEFAULT_TENANT_ID } from "@/lib/constants"
|
||||
import { env } from "@/lib/env"
|
||||
import { prisma } from "@/lib/prisma"
|
||||
import {
|
||||
computeInviteStatus,
|
||||
normalizeInvite,
|
||||
normalizeRoleOption,
|
||||
type NormalizedInvite,
|
||||
} from "@/server/invite-utils"
|
||||
|
||||
type AcceptInvitePayload = {
|
||||
name?: string
|
||||
password: string
|
||||
}
|
||||
|
||||
function validatePassword(password: string) {
|
||||
return password.length >= 8
|
||||
}
|
||||
|
||||
async function syncInvite(invite: NormalizedInvite) {
|
||||
const convexUrl = env.NEXT_PUBLIC_CONVEX_URL
|
||||
if (!convexUrl) return
|
||||
const client = new ConvexHttpClient(convexUrl)
|
||||
await client.mutation(api.invites.sync, {
|
||||
tenantId: invite.tenantId,
|
||||
inviteId: invite.id,
|
||||
email: invite.email,
|
||||
name: invite.name ?? undefined,
|
||||
role: invite.role.toUpperCase(),
|
||||
status: invite.status,
|
||||
token: invite.token,
|
||||
expiresAt: Date.parse(invite.expiresAt),
|
||||
createdAt: Date.parse(invite.createdAt),
|
||||
createdById: invite.createdById ?? undefined,
|
||||
acceptedAt: invite.acceptedAt ? Date.parse(invite.acceptedAt) : undefined,
|
||||
acceptedById: invite.acceptedById ?? undefined,
|
||||
revokedAt: invite.revokedAt ? Date.parse(invite.revokedAt) : undefined,
|
||||
revokedById: invite.revokedById ?? undefined,
|
||||
revokedReason: invite.revokedReason ?? undefined,
|
||||
})
|
||||
}
|
||||
|
||||
export async function GET(_request: Request, { params }: { params: { token: string } }) {
|
||||
const invite = await prisma.authInvite.findUnique({
|
||||
where: { token: params.token },
|
||||
include: { events: { orderBy: { createdAt: "asc" } } },
|
||||
})
|
||||
|
||||
if (!invite) {
|
||||
return NextResponse.json({ error: "Convite não encontrado" }, { status: 404 })
|
||||
}
|
||||
|
||||
const now = new Date()
|
||||
const status = computeInviteStatus(invite, now)
|
||||
|
||||
if (status !== invite.status) {
|
||||
await prisma.authInvite.update({ where: { id: invite.id }, data: { status } })
|
||||
const event = await prisma.authInviteEvent.create({
|
||||
data: {
|
||||
inviteId: invite.id,
|
||||
type: status,
|
||||
payload: null,
|
||||
actorId: null,
|
||||
},
|
||||
})
|
||||
invite.status = status
|
||||
invite.events.push(event)
|
||||
}
|
||||
|
||||
const normalized = normalizeInvite(invite, now)
|
||||
await syncInvite(normalized)
|
||||
|
||||
return NextResponse.json({ invite: normalized })
|
||||
}
|
||||
|
||||
export async function POST(request: Request, { params }: { params: { token: string } }) {
|
||||
const payload = (await request.json().catch(() => null)) as Partial<AcceptInvitePayload> | null
|
||||
if (!payload || typeof payload.password !== "string") {
|
||||
return NextResponse.json({ error: "Senha inválida" }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!validatePassword(payload.password)) {
|
||||
return NextResponse.json({ error: "Senha deve conter pelo menos 8 caracteres" }, { status: 400 })
|
||||
}
|
||||
|
||||
const invite = await prisma.authInvite.findUnique({
|
||||
where: { token: params.token },
|
||||
include: { events: { orderBy: { createdAt: "asc" } } },
|
||||
})
|
||||
|
||||
if (!invite) {
|
||||
return NextResponse.json({ error: "Convite não encontrado" }, { status: 404 })
|
||||
}
|
||||
|
||||
const now = new Date()
|
||||
const status = computeInviteStatus(invite, now)
|
||||
|
||||
if (status === "expired") {
|
||||
await prisma.authInvite.update({ where: { id: invite.id }, data: { status: "expired" } })
|
||||
const event = await prisma.authInviteEvent.create({
|
||||
data: {
|
||||
inviteId: invite.id,
|
||||
type: "expired",
|
||||
payload: null,
|
||||
actorId: null,
|
||||
},
|
||||
})
|
||||
invite.status = "expired"
|
||||
invite.events.push(event)
|
||||
const normalizedExpired = normalizeInvite(invite, now)
|
||||
await syncInvite(normalizedExpired)
|
||||
return NextResponse.json({ error: "Convite expirado" }, { status: 410 })
|
||||
}
|
||||
|
||||
if (status === "revoked") {
|
||||
return NextResponse.json({ error: "Convite revogado" }, { status: 410 })
|
||||
}
|
||||
|
||||
if (status === "accepted") {
|
||||
return NextResponse.json({ error: "Convite já utilizado" }, { status: 409 })
|
||||
}
|
||||
|
||||
const existingUser = await prisma.authUser.findUnique({ where: { email: invite.email } })
|
||||
if (existingUser) {
|
||||
return NextResponse.json({ error: "Usuário já registrado" }, { status: 409 })
|
||||
}
|
||||
|
||||
const name = typeof payload.name === "string" && payload.name.trim() ? payload.name.trim() : invite.name || invite.email
|
||||
const tenantId = invite.tenantId || DEFAULT_TENANT_ID
|
||||
const role = normalizeRoleOption(invite.role)
|
||||
|
||||
const hashedPassword = await hashPassword(payload.password)
|
||||
|
||||
const user = await prisma.authUser.create({
|
||||
data: {
|
||||
email: invite.email,
|
||||
name,
|
||||
role,
|
||||
tenantId,
|
||||
accounts: {
|
||||
create: {
|
||||
providerId: "credential",
|
||||
accountId: invite.email,
|
||||
password: hashedPassword,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const updatedInvite = await prisma.authInvite.update({
|
||||
where: { id: invite.id },
|
||||
data: {
|
||||
status: "accepted",
|
||||
acceptedAt: now,
|
||||
acceptedById: user.id,
|
||||
name,
|
||||
},
|
||||
})
|
||||
|
||||
const event = await prisma.authInviteEvent.create({
|
||||
data: {
|
||||
inviteId: invite.id,
|
||||
type: "accepted",
|
||||
payload: { userId: user.id },
|
||||
actorId: user.id,
|
||||
},
|
||||
})
|
||||
|
||||
const normalized = normalizeInvite({ ...updatedInvite, events: [...invite.events, event] }, now)
|
||||
await syncInvite(normalized)
|
||||
|
||||
const convexUrl = env.NEXT_PUBLIC_CONVEX_URL
|
||||
if (convexUrl) {
|
||||
try {
|
||||
const convex = new ConvexHttpClient(convexUrl)
|
||||
await convex.mutation(api.users.ensureUser, {
|
||||
tenantId,
|
||||
email: invite.email,
|
||||
name,
|
||||
avatarUrl: undefined,
|
||||
role: role.toUpperCase(),
|
||||
})
|
||||
} catch (error) {
|
||||
console.warn("Falha ao sincronizar usuário no Convex", error)
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue