Auto-expire revoked invites and allow reactivation
This commit is contained in:
parent
05f5af5ba6
commit
b60f27b2dc
3 changed files with 96 additions and 3 deletions
|
|
@ -68,7 +68,14 @@ async function loadInvites(): Promise<NormalizedInvite[]> {
|
|||
})
|
||||
|
||||
const now = new Date()
|
||||
return invites.map((invite) => normalizeInvite(invite, now))
|
||||
const cutoff = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000)
|
||||
return invites
|
||||
.map((invite) => normalizeInvite(invite, now))
|
||||
.filter((invite) => {
|
||||
if (invite.status !== "revoked") return true
|
||||
if (!invite.revokedAt) return true
|
||||
return new Date(invite.revokedAt) > cutoff
|
||||
})
|
||||
}
|
||||
|
||||
export default async function AdminPage() {
|
||||
|
|
|
|||
|
|
@ -9,10 +9,15 @@ import { env } from "@/lib/env"
|
|||
import { prisma } from "@/lib/prisma"
|
||||
import { computeInviteStatus, normalizeInvite, type NormalizedInvite } from "@/server/invite-utils"
|
||||
|
||||
type RevokePayload = {
|
||||
type InviteAction = "revoke" | "reactivate"
|
||||
|
||||
type InvitePayload = {
|
||||
action?: InviteAction
|
||||
reason?: string
|
||||
}
|
||||
|
||||
const REVOKE_RETENTION_MS = 7 * 24 * 60 * 60 * 1000
|
||||
|
||||
async function syncInvite(invite: NormalizedInvite) {
|
||||
const convexUrl = env.NEXT_PUBLIC_CONVEX_URL
|
||||
if (!convexUrl) return
|
||||
|
|
@ -43,7 +48,8 @@ export async function PATCH(request: Request, context: { params: Promise<{ id: s
|
|||
return NextResponse.json({ error: "Não autorizado" }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = (await request.json().catch(() => null)) as Partial<RevokePayload> | null
|
||||
const body = (await request.json().catch(() => null)) as Partial<InvitePayload> | null
|
||||
const action: InviteAction = body?.action === "reactivate" ? "reactivate" : "revoke"
|
||||
const reason = typeof body?.reason === "string" && body.reason.trim() ? body.reason.trim() : null
|
||||
|
||||
const invite = await prisma.authInvite.findUnique({
|
||||
|
|
@ -62,6 +68,42 @@ export async function PATCH(request: Request, context: { params: Promise<{ id: s
|
|||
return NextResponse.json({ error: "Convite já aceito" }, { status: 400 })
|
||||
}
|
||||
|
||||
if (action === "reactivate") {
|
||||
if (status !== "revoked") {
|
||||
return NextResponse.json({ error: "Convite não está revogado" }, { status: 400 })
|
||||
}
|
||||
if (!invite.revokedAt) {
|
||||
return NextResponse.json({ error: "Convite revogado sem data. Não é possível reativar." }, { status: 400 })
|
||||
}
|
||||
const revokedAtMs = invite.revokedAt.getTime()
|
||||
if (now.getTime() - revokedAtMs > REVOKE_RETENTION_MS) {
|
||||
return NextResponse.json({ error: "Este convite foi revogado há mais de 7 dias" }, { status: 400 })
|
||||
}
|
||||
|
||||
const updated = await prisma.authInvite.update({
|
||||
where: { id: invite.id },
|
||||
data: {
|
||||
status: "pending",
|
||||
revokedAt: null,
|
||||
revokedById: null,
|
||||
revokedReason: null,
|
||||
},
|
||||
})
|
||||
|
||||
const event = await prisma.authInviteEvent.create({
|
||||
data: {
|
||||
inviteId: invite.id,
|
||||
type: "reactivated",
|
||||
payload: Prisma.JsonNull,
|
||||
actorId: session.user.id ?? null,
|
||||
},
|
||||
})
|
||||
|
||||
const normalized = normalizeInvite({ ...updated, events: [...invite.events, event] }, now)
|
||||
await syncInvite(normalized)
|
||||
return NextResponse.json({ invite: normalized })
|
||||
}
|
||||
|
||||
if (status === "revoked") {
|
||||
const normalized = normalizeInvite(invite, now)
|
||||
await syncInvite(normalized)
|
||||
|
|
|
|||
|
|
@ -140,6 +140,13 @@ function extractMachineId(email: string): string | null {
|
|||
return match ? match[1] : null
|
||||
}
|
||||
|
||||
function canReactivateInvite(invite: AdminInvite): boolean {
|
||||
if (invite.status !== "revoked" || !invite.revokedAt) return false
|
||||
const revokedDate = new Date(invite.revokedAt)
|
||||
const limit = Date.now() - 7 * 24 * 60 * 60 * 1000
|
||||
return revokedDate.getTime() > limit
|
||||
}
|
||||
|
||||
export function AdminUsersManager({ initialUsers, initialInvites, roleOptions, defaultTenantId }: Props) {
|
||||
const [users, setUsers] = useState<AdminUser[]>(initialUsers)
|
||||
const [invites, setInvites] = useState<AdminInvite[]>(initialInvites)
|
||||
|
|
@ -152,6 +159,7 @@ export function AdminUsersManager({ initialUsers, initialInvites, roleOptions, d
|
|||
const [expiresInDays, setExpiresInDays] = useState("7")
|
||||
const [lastInviteLink, setLastInviteLink] = useState<string | null>(null)
|
||||
const [revokingId, setRevokingId] = useState<string | null>(null)
|
||||
const [reactivatingId, setReactivatingId] = useState<string | null>(null)
|
||||
const [isPending, startTransition] = useTransition()
|
||||
|
||||
const [linkEmail, setLinkEmail] = useState("")
|
||||
|
|
@ -315,6 +323,31 @@ export function AdminUsersManager({ initialUsers, initialInvites, roleOptions, d
|
|||
}
|
||||
}
|
||||
|
||||
async function handleReactivate(invite: AdminInvite) {
|
||||
if (!canReactivateInvite(invite)) return
|
||||
setReactivatingId(invite.id)
|
||||
try {
|
||||
const response = await fetch(`/api/admin/invites/${invite.id}`, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ action: "reactivate" }),
|
||||
})
|
||||
if (!response.ok) {
|
||||
const data = await response.json().catch(() => ({}))
|
||||
throw new Error(data.error ?? "Falha ao reativar convite")
|
||||
}
|
||||
const data = (await response.json()) as { invite: AdminInvite }
|
||||
const normalized = sanitizeInvite(data.invite)
|
||||
setInvites((previous) => previous.map((item) => (item.id === normalized.id ? normalized : item)))
|
||||
toast.success("Convite reativado")
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Não foi possível reativar"
|
||||
toast.error(message)
|
||||
} finally {
|
||||
setReactivatingId(null)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleAssignCompany(event: React.FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault()
|
||||
const normalizedEmail = linkEmail.trim().toLowerCase()
|
||||
|
|
@ -784,6 +817,17 @@ export function AdminUsersManager({ initialUsers, initialInvites, roleOptions, d
|
|||
{revokingId === invite.id ? "Revogando..." : "Revogar"}
|
||||
</Button>
|
||||
) : null}
|
||||
{invite.status === "revoked" && canReactivateInvite(invite) ? (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="border-amber-400 text-amber-600 hover:bg-amber-50"
|
||||
onClick={() => handleReactivate(invite)}
|
||||
disabled={reactivatingId === invite.id}
|
||||
>
|
||||
{reactivatingId === invite.id ? "Reativando..." : "Reativar"}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue