feat: ProductMember — team management for product backlogs
- Add ProductMember model (many-to-many User ↔ Product) - Add productAccessFilter helper (owner OR member OR clause) - Replace all ownership checks across actions and API routes - Add addProductMemberAction / removeProductMemberAction / leaveProductAction - Add TeamManager component in product settings (owner adds/removes Developers) - Add LeaveProductButton in user settings (member leaves a product team) - Regenerate Prisma Client after schema migration Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
fc12e3cc64
commit
357b1e32e8
18 changed files with 370 additions and 82 deletions
|
|
@ -7,6 +7,7 @@ import { getIronSession } from 'iron-session'
|
|||
import { z } from 'zod'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { SessionData, sessionOptions } from '@/lib/session'
|
||||
import { Role } from '@prisma/client'
|
||||
|
||||
const productSchema = z.object({
|
||||
name: z.string().min(1, 'Naam is verplicht').max(100, 'Naam mag maximaal 100 tekens bevatten'),
|
||||
|
|
@ -137,3 +138,64 @@ export async function restoreProductAction(id: string) {
|
|||
revalidatePath('/dashboard')
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
export async function addProductMemberAction(_prevState: unknown, formData: FormData) {
|
||||
const session = await getSession()
|
||||
if (!session.userId) return { error: 'Niet ingelogd' }
|
||||
if (session.isDemo) return { error: 'Niet beschikbaar in demo-modus' }
|
||||
|
||||
const productId = formData.get('productId') as string
|
||||
const username = (formData.get('username') as string)?.trim().toLowerCase()
|
||||
|
||||
if (!username) return { error: 'Gebruikersnaam is verplicht' }
|
||||
|
||||
const product = await prisma.product.findFirst({ where: { id: productId, user_id: session.userId } })
|
||||
if (!product) return { error: 'Product niet gevonden of geen eigenaar' }
|
||||
|
||||
const targetUser = await prisma.user.findUnique({
|
||||
where: { username },
|
||||
include: { roles: true },
|
||||
})
|
||||
if (!targetUser) return { error: 'Gebruiker niet gevonden' }
|
||||
if (targetUser.is_demo) return { error: 'Demo-gebruiker kan niet worden toegevoegd' }
|
||||
if (targetUser.id === session.userId) return { error: 'Je bent al eigenaar van dit product' }
|
||||
|
||||
const hasDeveloperRole = targetUser.roles.some(r => r.role === Role.DEVELOPER)
|
||||
if (!hasDeveloperRole) return { error: `Gebruiker "${username}" heeft geen Developer-rol` }
|
||||
|
||||
const existing = await prisma.productMember.findUnique({
|
||||
where: { product_id_user_id: { product_id: productId, user_id: targetUser.id } },
|
||||
})
|
||||
if (existing) return { error: 'Gebruiker is al lid van dit product' }
|
||||
|
||||
await prisma.productMember.create({
|
||||
data: { product_id: productId, user_id: targetUser.id },
|
||||
})
|
||||
|
||||
revalidatePath(`/products/${productId}/settings`)
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
export async function removeProductMemberAction(productId: string, memberId: string) {
|
||||
const session = await getSession()
|
||||
if (!session.userId) return { error: 'Niet ingelogd' }
|
||||
if (session.isDemo) return { error: 'Niet beschikbaar in demo-modus' }
|
||||
|
||||
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 } })
|
||||
|
||||
revalidatePath(`/products/${productId}/settings`)
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
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 } })
|
||||
|
||||
revalidatePath('/settings')
|
||||
return { success: true }
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue