feat(ST-902): add setActiveProduct + clearActiveProduct server actions

- actions/active-product.ts: setActiveProductAction validates access via
  productAccessFilter, rejects archived products and demo users
- archiveProductAction: clears active_product_id for all affected users in transaction
- removeProductMemberAction: clears active_product_id for removed member
- leaveProductAction: clears active_product_id for leaving user

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Janpeter Visser 2026-04-27 19:01:20 +02:00
parent 5ab4889b04
commit 35d60cc43b
2 changed files with 75 additions and 4 deletions

51
actions/active-product.ts Normal file
View file

@ -0,0 +1,51 @@
'use server'
import { revalidatePath } from 'next/cache'
import { cookies } from 'next/headers'
import { getIronSession } from 'iron-session'
import { z } from 'zod'
import { prisma } from '@/lib/prisma'
import { SessionData, sessionOptions } from '@/lib/session'
import { productAccessFilter } from '@/lib/product-access'
async function getSession() {
return getIronSession<SessionData>(await cookies(), sessionOptions)
}
const setSchema = z.object({ productId: z.string().min(1) })
export async function setActiveProductAction(productId: string) {
const session = await getSession()
if (!session.userId) return { error: 'Niet ingelogd' }
if (session.isDemo) return { error: 'Niet beschikbaar in demo-modus' }
const parsed = setSchema.safeParse({ productId })
if (!parsed.success) return { error: 'Ongeldig product-id' }
const product = await prisma.product.findFirst({
where: { id: parsed.data.productId, archived: false, ...productAccessFilter(session.userId) },
})
if (!product) return { error: 'Product niet gevonden of niet toegankelijk' }
await prisma.user.update({
where: { id: session.userId },
data: { active_product_id: parsed.data.productId },
})
revalidatePath('/', 'layout')
return { success: true, productId: parsed.data.productId }
}
export async function clearActiveProductAction() {
const session = await getSession()
if (!session.userId) return { error: 'Niet ingelogd' }
if (session.isDemo) return { error: 'Niet beschikbaar in demo-modus' }
await prisma.user.update({
where: { id: session.userId },
data: { active_product_id: null },
})
revalidatePath('/', 'layout')
return { success: true }
}

View file

@ -148,9 +148,16 @@ export async function archiveProductAction(id: string) {
})
if (!product) return { error: 'Product niet gevonden' }
await prisma.product.update({ where: { id }, data: { archived: true } })
await prisma.$transaction([
// Clear active_product_id for all users who had this product active
prisma.user.updateMany({
where: { active_product_id: id },
data: { active_product_id: null },
}),
prisma.product.update({ where: { id }, data: { archived: true } }),
])
revalidatePath('/dashboard')
revalidatePath('/', 'layout')
redirect('/dashboard')
}
@ -215,7 +222,13 @@ export async function removeProductMemberAction(productId: string, memberId: str
const product = await prisma.product.findFirst({ where: { id: productId, user_id: session.userId } })
if (!product) return { error: 'Product niet gevonden of geen eigenaar' }
await prisma.productMember.deleteMany({ where: { product_id: productId, user_id: memberId } })
await prisma.$transaction([
prisma.user.updateMany({
where: { id: memberId, active_product_id: productId },
data: { active_product_id: null },
}),
prisma.productMember.deleteMany({ where: { product_id: productId, user_id: memberId } }),
])
revalidatePath(`/products/${productId}/settings`)
return { success: true }
@ -225,8 +238,15 @@ export async function leaveProductAction(productId: string) {
const session = await getSession()
if (!session.userId) return { error: 'Niet ingelogd' }
await prisma.productMember.deleteMany({ where: { product_id: productId, user_id: session.userId } })
await prisma.$transaction([
prisma.user.updateMany({
where: { id: session.userId, active_product_id: productId },
data: { active_product_id: null },
}),
prisma.productMember.deleteMany({ where: { product_id: productId, user_id: session.userId } }),
])
revalidatePath('/', 'layout')
revalidatePath('/settings')
return { success: true }
}