674 lines
24 KiB
TypeScript
674 lines
24 KiB
TypeScript
import { z } from 'zod'
|
||
import { router, memberProcedure, adminProcedure } from '../trpc'
|
||
import { auth, getSanitizedHeaders } from '@/lib/auth'
|
||
import { sendInviteEmail, sendAdminCredentialsEmail } from '@/lib/email'
|
||
import crypto from 'node:crypto'
|
||
import { prisma } from '@innungsapp/shared'
|
||
// @ts-ignore — Better Auth exposes its password utilities via `better-auth/crypto`
|
||
import { hashPassword } from 'better-auth/crypto'
|
||
|
||
/**
|
||
* Creates a credential (email+password) account for a user that has no such account yet.
|
||
* Better Auth stores passwords in the `account` table with providerId='credential'.
|
||
* Uses Better Auth's own `hashPassword` to ensure the hash format matches its verifyPassword.
|
||
*/
|
||
async function createCredentialAccount(userId: string, password: string): Promise<void> {
|
||
const hashed = await hashPassword(password)
|
||
await prisma.account.create({
|
||
data: {
|
||
id: crypto.randomUUID(),
|
||
accountId: userId,
|
||
providerId: 'credential',
|
||
userId,
|
||
password: hashed,
|
||
},
|
||
})
|
||
}
|
||
|
||
/**
|
||
* Creates a user + credential account directly in the DB, bypassing Better Auth's admin API.
|
||
* auth.api.createUser requires the calling session to have role='admin' in Better Auth's own
|
||
* user table, which our custom role system doesn't set. This avoids the 403 FORBIDDEN error.
|
||
*/
|
||
async function createUserDirectly(opts: { email: string; name: string; password: string; mustChangePassword?: boolean }) {
|
||
const userId = crypto.randomUUID()
|
||
await prisma.user.create({
|
||
data: {
|
||
id: userId,
|
||
name: opts.name,
|
||
email: opts.email,
|
||
emailVerified: false,
|
||
mustChangePassword: opts.mustChangePassword ?? false,
|
||
},
|
||
})
|
||
// Try better-auth API first (guaranteed correct hash format).
|
||
// Falls back to direct DB write if API fails (e.g. admin permissions not available).
|
||
try {
|
||
const authHeaders = await getSanitizedHeaders()
|
||
await auth.api.updateUser({
|
||
body: { userId, password: opts.password },
|
||
headers: authHeaders,
|
||
})
|
||
} catch {
|
||
await createCredentialAccount(userId, opts.password)
|
||
}
|
||
return { id: userId }
|
||
}
|
||
|
||
const MemberInput = z.object({
|
||
name: z.string().min(2),
|
||
betrieb: z.string().min(2),
|
||
sparte: z.string().min(2),
|
||
ort: z.string().min(2),
|
||
telefon: z.string().optional(),
|
||
email: z.string().email(),
|
||
status: z.enum(['aktiv', 'ruhend', 'ausgetreten']).default('aktiv'),
|
||
istAusbildungsbetrieb: z.boolean().default(false),
|
||
seit: z.number().int().min(1900).max(2100).optional(),
|
||
role: z.enum(['member', 'admin']).optional().default('member'),
|
||
password: z.preprocess((val) => (val === '' ? undefined : val), z.string().min(8).optional()),
|
||
})
|
||
|
||
export const membersRouter = router({
|
||
/**
|
||
* List all members in the user's org
|
||
*/
|
||
list: memberProcedure
|
||
.input(
|
||
z.object({
|
||
search: z.string().optional(),
|
||
status: z.enum(['aktiv', 'ruhend', 'ausgetreten']).optional(),
|
||
ausbildungsbetrieb: z.boolean().optional(),
|
||
})
|
||
)
|
||
.query(async ({ ctx, input }) => {
|
||
const members = await ctx.prisma.member.findMany({
|
||
where: {
|
||
orgId: ctx.orgId,
|
||
...(input.status && { status: input.status }),
|
||
...(input.ausbildungsbetrieb !== undefined && {
|
||
istAusbildungsbetrieb: input.ausbildungsbetrieb,
|
||
}),
|
||
...(input.search && {
|
||
OR: [
|
||
{ name: { contains: input.search } },
|
||
{ betrieb: { contains: input.search } },
|
||
{ ort: { contains: input.search } },
|
||
{ sparte: { contains: input.search } },
|
||
],
|
||
}),
|
||
},
|
||
orderBy: { name: 'asc' },
|
||
})
|
||
return members
|
||
}),
|
||
|
||
byId: memberProcedure
|
||
.input(z.object({ id: z.string() }))
|
||
.query(async ({ ctx, input }) => {
|
||
let member = await ctx.prisma.member.findFirst({
|
||
where: { id: input.id, orgId: ctx.orgId },
|
||
})
|
||
|
||
let role = 'member'
|
||
if (member?.userId) {
|
||
const ur = await ctx.prisma.userRole.findUnique({ where: { orgId_userId: { orgId: ctx.orgId, userId: member.userId } } })
|
||
if (ur && ur.role === 'admin') role = 'admin'
|
||
}
|
||
|
||
if (!member) {
|
||
// Try finding the member by userId (list page uses userId as ID for admins)
|
||
const memberByUserId = await ctx.prisma.member.findFirst({
|
||
where: { userId: input.id, orgId: ctx.orgId },
|
||
})
|
||
if (memberByUserId) {
|
||
const ur2 = await ctx.prisma.userRole.findUnique({
|
||
where: { orgId_userId: { orgId: ctx.orgId, userId: input.id } },
|
||
})
|
||
if (ur2?.role === 'admin') role = 'admin'
|
||
return { ...memberByUserId, role }
|
||
}
|
||
|
||
// Fallback: Check if the ID belongs to a user who is an admin in this org
|
||
const adminRole = await ctx.prisma.userRole.findUnique({
|
||
where: { orgId_userId: { orgId: ctx.orgId, userId: input.id } },
|
||
include: { user: true }
|
||
})
|
||
|
||
if (!adminRole) {
|
||
// Last resort A: find member by ID regardless of org (org mismatch scenario)
|
||
const memberAnyOrg = await ctx.prisma.member.findUnique({ where: { id: input.id } })
|
||
if (memberAnyOrg) {
|
||
const callerHasAccess = await ctx.prisma.userRole.findFirst({
|
||
where: { userId: ctx.session.user.id, orgId: memberAnyOrg.orgId, role: 'admin' }
|
||
})
|
||
if (callerHasAccess) {
|
||
const ur = memberAnyOrg.userId
|
||
? await ctx.prisma.userRole.findUnique({
|
||
where: { orgId_userId: { orgId: memberAnyOrg.orgId, userId: memberAnyOrg.userId } }
|
||
})
|
||
: null
|
||
return { ...memberAnyOrg, role: ur?.role ?? 'member' }
|
||
}
|
||
}
|
||
|
||
// Last resort B: input.id is a userId whose UserRole is in a different org than ctx.orgId
|
||
const roleAnyOrg = await ctx.prisma.userRole.findFirst({
|
||
where: { userId: input.id },
|
||
include: { user: true },
|
||
})
|
||
if (roleAnyOrg) {
|
||
const callerHasAccess = await ctx.prisma.userRole.findFirst({
|
||
where: { userId: ctx.session.user.id, orgId: roleAnyOrg.orgId, role: 'admin' }
|
||
})
|
||
if (callerHasAccess) {
|
||
const memberRecord = await ctx.prisma.member.findFirst({
|
||
where: { userId: input.id, orgId: roleAnyOrg.orgId }
|
||
})
|
||
if (memberRecord) return { ...memberRecord, role: roleAnyOrg.role }
|
||
// Admin without member record — return mock
|
||
return {
|
||
id: roleAnyOrg.userId,
|
||
orgId: roleAnyOrg.orgId,
|
||
userId: roleAnyOrg.userId,
|
||
name: roleAnyOrg.user.name,
|
||
betrieb: 'Administrator',
|
||
sparte: 'Sonderfunktion',
|
||
ort: '',
|
||
telefon: '',
|
||
email: roleAnyOrg.user.email,
|
||
status: 'aktiv',
|
||
istAusbildungsbetrieb: false,
|
||
seit: new Date().getFullYear(),
|
||
avatarUrl: null,
|
||
pushToken: null,
|
||
createdAt: roleAnyOrg.createdAt,
|
||
updatedAt: roleAnyOrg.createdAt,
|
||
role: roleAnyOrg.role,
|
||
} as any
|
||
}
|
||
}
|
||
|
||
throw new Error('Member not found')
|
||
}
|
||
if (adminRole.role !== 'admin') throw new Error('Member not found')
|
||
|
||
// Mock a Member object so the frontend form doesn't crash
|
||
member = {
|
||
id: adminRole.userId, // use userId here to update
|
||
orgId: ctx.orgId,
|
||
userId: adminRole.userId,
|
||
name: adminRole.user.name,
|
||
betrieb: 'Administrator',
|
||
sparte: 'Sonderfunktion',
|
||
ort: '',
|
||
telefon: '',
|
||
email: adminRole.user.email,
|
||
status: 'aktiv',
|
||
istAusbildungsbetrieb: false,
|
||
seit: new Date().getFullYear(),
|
||
avatarUrl: null,
|
||
pushToken: null,
|
||
createdAt: adminRole.createdAt,
|
||
updatedAt: adminRole.createdAt,
|
||
} as any
|
||
role = 'admin'
|
||
}
|
||
|
||
return { ...member, role }
|
||
}),
|
||
|
||
create: adminProcedure.input(MemberInput).mutation(async ({ ctx, input }) => {
|
||
const { role, password, ...rest } = input
|
||
|
||
// 1. Create the member record
|
||
const member = await ctx.prisma.member.create({
|
||
data: { ...rest, orgId: ctx.orgId },
|
||
})
|
||
|
||
// 2. Create a User account if a password was provided OR role is 'admin',
|
||
// so the role is always persisted (no email sent here).
|
||
if (password || role === 'admin') {
|
||
try {
|
||
const authHeaders = await getSanitizedHeaders()
|
||
const existing = await ctx.prisma.user.findUnique({ where: { email: input.email } })
|
||
let userId: string | undefined = existing?.id
|
||
const effectivePassword = password || crypto.randomBytes(8).toString('hex')
|
||
|
||
if (!userId) {
|
||
// Create user + credential account directly via Prisma.
|
||
// auth.api.createUser requires the caller to have role='admin' in Better Auth's own
|
||
// user table (not our custom UserRole table), which causes a 403 FORBIDDEN.
|
||
const newUserRecord = await createUserDirectly({
|
||
name: input.name,
|
||
email: input.email,
|
||
password: effectivePassword,
|
||
mustChangePassword: true,
|
||
})
|
||
userId = newUserRecord.id
|
||
} else if (password) {
|
||
// User exists and a password was explicitly set.
|
||
// Check if they already have a credential account; if not, create one directly.
|
||
const credAccount = await ctx.prisma.account.findFirst({
|
||
where: { userId, providerId: 'credential' }
|
||
})
|
||
if (credAccount) {
|
||
// Credential account exists — update the password via Better Auth
|
||
await auth.api.updateUser({ body: { userId, password, name: input.name }, headers: authHeaders })
|
||
} else {
|
||
// No credential account yet — create one directly in the DB
|
||
await createCredentialAccount(userId, password)
|
||
}
|
||
}
|
||
|
||
if (userId) {
|
||
await ctx.prisma.member.update({ where: { id: member.id }, data: { userId } })
|
||
await ctx.prisma.userRole.upsert({
|
||
where: { orgId_userId: { orgId: ctx.orgId, userId } },
|
||
create: { orgId: ctx.orgId, userId, role: role ?? 'member' },
|
||
update: { role: role ?? 'member' },
|
||
})
|
||
}
|
||
} catch (e) {
|
||
console.error('Failed to create user account during member creation', e)
|
||
}
|
||
}
|
||
|
||
return member
|
||
}),
|
||
|
||
|
||
/**
|
||
* Create member + send invite email (admin only)
|
||
*/
|
||
invite: adminProcedure
|
||
.input(MemberInput)
|
||
.mutation(async ({ ctx, input }) => {
|
||
const { role, password, ...memberData } = input
|
||
const authHeaders = await getSanitizedHeaders()
|
||
|
||
// 1. Create member record
|
||
const member = await ctx.prisma.member.create({
|
||
data: { ...memberData, orgId: ctx.orgId },
|
||
})
|
||
|
||
const org = await ctx.prisma.organization.findUniqueOrThrow({
|
||
where: { id: ctx.orgId },
|
||
})
|
||
|
||
// 2. Create/get User directly via Prisma to avoid auth.api.createUser 403 FORBIDDEN.
|
||
// (Better Auth's admin API requires the caller's session user to have role='admin' in
|
||
// its own user table, which our custom UserRole system doesn't set.)
|
||
let targetUserId: string | undefined
|
||
try {
|
||
const effectivePassword = password || (role === 'admin' ? crypto.randomBytes(6).toString('hex') : undefined)
|
||
|
||
if (effectivePassword) {
|
||
const newUserId = (await createUserDirectly({
|
||
name: input.name,
|
||
email: input.email,
|
||
password: effectivePassword,
|
||
mustChangePassword: !password && role === 'admin',
|
||
})).id
|
||
targetUserId = newUserId
|
||
}
|
||
|
||
if (targetUserId) {
|
||
// link user to member
|
||
await ctx.prisma.member.update({
|
||
where: { id: member.id },
|
||
data: { userId: targetUserId }
|
||
})
|
||
|
||
// if admin, set role
|
||
if (role === 'admin') {
|
||
await ctx.prisma.userRole.upsert({
|
||
where: { orgId_userId: { orgId: ctx.orgId, userId: targetUserId } },
|
||
create: { orgId: ctx.orgId, userId: targetUserId, role: 'admin' },
|
||
update: { role: 'admin' }
|
||
})
|
||
|
||
// Send admin credentials
|
||
await sendAdminCredentialsEmail({
|
||
to: input.email,
|
||
adminName: input.name,
|
||
orgName: org.name,
|
||
password: password!,
|
||
loginUrl: process.env.BETTER_AUTH_URL!
|
||
})
|
||
}
|
||
}
|
||
} catch (e) {
|
||
// User may already exist
|
||
const existingUser = await ctx.prisma.user.findUnique({ where: { email: input.email } })
|
||
if (existingUser) {
|
||
targetUserId = existingUser.id
|
||
await ctx.prisma.member.update({
|
||
where: { id: member.id },
|
||
data: { userId: targetUserId }
|
||
})
|
||
|
||
if (role === 'admin') {
|
||
await ctx.prisma.userRole.upsert({
|
||
where: { orgId_userId: { orgId: ctx.orgId, userId: targetUserId } },
|
||
create: { orgId: ctx.orgId, userId: targetUserId, role: 'admin' },
|
||
update: { role: 'admin' }
|
||
})
|
||
}
|
||
}
|
||
}
|
||
|
||
// 3. Send magic link for members (if not admin or if admin creation failed to send credentials)
|
||
if (role === 'member') {
|
||
await sendInviteEmail({
|
||
to: input.email,
|
||
memberName: input.name,
|
||
orgName: org.name,
|
||
apiUrl: process.env.BETTER_AUTH_URL!,
|
||
})
|
||
}
|
||
|
||
return member
|
||
}),
|
||
|
||
/**
|
||
* Update member (admin only)
|
||
*/
|
||
update: adminProcedure
|
||
.input(z.object({ id: z.string(), data: MemberInput.partial().extend({ role: z.enum(['member', 'admin']).optional() }) }))
|
||
.mutation(async ({ ctx, input }) => {
|
||
const { role, password, ...memberData } = input.data
|
||
|
||
let member = await ctx.prisma.member.findFirst({
|
||
where: { id: input.id, orgId: ctx.orgId },
|
||
})
|
||
|
||
// If not found by member ID, try by userId (list page links use userId for admins)
|
||
if (!member) {
|
||
member = await ctx.prisma.member.findFirst({
|
||
where: { userId: input.id, orgId: ctx.orgId },
|
||
})
|
||
}
|
||
|
||
// For existing members, targetUserId is their associated user ID (can be null).
|
||
// For fallback admins (no member record), input.id is the User ID.
|
||
let targetUserId = member ? member.userId : input.id
|
||
|
||
// If they don't have a User record yet, but we want to update their role to Admin,
|
||
// we need to pre-create their User record so we can attach the UserRole.
|
||
if (member && !targetUserId && role === 'admin') {
|
||
const email = memberData.email || member.email
|
||
const name = memberData.name || member.name
|
||
// Always generate a password – without one Better Auth creates no credential account
|
||
// and the user can never log in with email/password.
|
||
const effectivePassword = password || crypto.randomBytes(8).toString('hex')
|
||
try {
|
||
const user = await ctx.prisma.user.findUnique({ where: { email } })
|
||
if (user) {
|
||
targetUserId = user.id
|
||
// If the existing user has no credential account (e.g. OAuth-only), set a password now
|
||
const credAccount = await ctx.prisma.account.findFirst({
|
||
where: { userId: user.id, providerId: 'credential' }
|
||
})
|
||
if (!credAccount) {
|
||
// No credential account — create one directly (updateUser can't create from scratch)
|
||
await createCredentialAccount(user.id, effectivePassword)
|
||
}
|
||
} else {
|
||
const newUserId = (await createUserDirectly({
|
||
email,
|
||
name,
|
||
password: effectivePassword,
|
||
mustChangePassword: !password,
|
||
})).id
|
||
targetUserId = newUserId
|
||
}
|
||
if (targetUserId) {
|
||
await ctx.prisma.member.update({
|
||
where: { id: member.id },
|
||
data: { userId: targetUserId }
|
||
})
|
||
}
|
||
} catch (e) {
|
||
console.error("Failed to pre-create user for pending invitee admin upgrade", e)
|
||
}
|
||
}
|
||
|
||
if (member) {
|
||
if (Object.keys(memberData).length > 0) {
|
||
await ctx.prisma.member.update({
|
||
where: { id: member.id },
|
||
data: memberData as any,
|
||
})
|
||
}
|
||
} else {
|
||
// Fallback: Creating skeleton member for former pure-admin that is now turning to member
|
||
const existingAdmin = await ctx.prisma.userRole.findUnique({
|
||
where: { orgId_userId: { orgId: ctx.orgId, userId: targetUserId as string } }
|
||
})
|
||
if (!existingAdmin || existingAdmin.role !== 'admin') throw new Error('Member not found')
|
||
|
||
// Since we are creating a member profile, ensure required fields
|
||
const createData = {
|
||
betrieb: memberData.betrieb || 'Administrator',
|
||
sparte: memberData.sparte || 'Sonderfunktion',
|
||
ort: memberData.ort || '',
|
||
telefon: memberData.telefon || '',
|
||
email: memberData.email || 'no-reply@innungsapp.de',
|
||
status: memberData.status || 'aktiv',
|
||
name: memberData.name || 'Unbekannt',
|
||
}
|
||
|
||
const existingMemberByUserId = await ctx.prisma.member.findFirst({
|
||
where: { userId: targetUserId as string, orgId: ctx.orgId }
|
||
})
|
||
|
||
if (existingMemberByUserId) {
|
||
// Member record already exists — just update it with any new data
|
||
await ctx.prisma.member.update({
|
||
where: { id: existingMemberByUserId.id },
|
||
data: createData as any,
|
||
})
|
||
} else {
|
||
await ctx.prisma.member.create({
|
||
data: {
|
||
...createData,
|
||
orgId: ctx.orgId,
|
||
userId: targetUserId
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
if (role && targetUserId) {
|
||
// Update the role in UserRole
|
||
await ctx.prisma.userRole.upsert({
|
||
where: { orgId_userId: { orgId: ctx.orgId, userId: targetUserId } },
|
||
create: { orgId: ctx.orgId, userId: targetUserId, role },
|
||
update: { role }
|
||
})
|
||
}
|
||
|
||
// When promoting to admin, ensure a credential (email+password) account exists
|
||
// so the user can log in to the admin dashboard.
|
||
if (role === 'admin' && targetUserId && !password) {
|
||
const credAccount = await ctx.prisma.account.findFirst({
|
||
where: { userId: targetUserId, providerId: 'credential' },
|
||
})
|
||
if (!credAccount) {
|
||
const generatedPassword = crypto.randomBytes(8).toString('hex')
|
||
await createCredentialAccount(targetUserId, generatedPassword)
|
||
try {
|
||
const targetUser = await ctx.prisma.user.findUnique({ where: { id: targetUserId } })
|
||
const org = await ctx.prisma.organization.findUnique({ where: { id: ctx.orgId } })
|
||
if (targetUser && org) {
|
||
await sendAdminCredentialsEmail({
|
||
to: targetUser.email,
|
||
adminName: targetUser.name,
|
||
orgName: org.name,
|
||
password: generatedPassword,
|
||
loginUrl: process.env.BETTER_AUTH_URL!,
|
||
})
|
||
}
|
||
} catch (e) {
|
||
console.error('Failed to send admin credentials email after auto-credential creation', e)
|
||
}
|
||
}
|
||
}
|
||
|
||
// Update password if provided
|
||
if (password && targetUserId) {
|
||
// Check if the user already has a credential account.
|
||
// auth.api.updateUser can update an existing credential account password, but CANNOT create one.
|
||
const existingCredAccount = await ctx.prisma.account.findFirst({
|
||
where: { userId: targetUserId, providerId: 'credential' }
|
||
})
|
||
if (existingCredAccount) {
|
||
const authHeaders = await getSanitizedHeaders()
|
||
let nameForUpdate = memberData.name
|
||
if (!nameForUpdate) {
|
||
const existingUser = await ctx.prisma.user.findUnique({ where: { id: targetUserId }, select: { name: true } })
|
||
nameForUpdate = existingUser?.name ?? undefined
|
||
}
|
||
await auth.api.updateUser({
|
||
body: {
|
||
userId: targetUserId,
|
||
password,
|
||
...(nameForUpdate && { name: nameForUpdate }),
|
||
},
|
||
headers: authHeaders,
|
||
})
|
||
} else {
|
||
// No credential account — create one from scratch using Better Auth's own hash format
|
||
await createCredentialAccount(targetUserId, password)
|
||
}
|
||
// Admin has set a password → user must change it on next login
|
||
await ctx.prisma.user.update({
|
||
where: { id: targetUserId },
|
||
data: { mustChangePassword: true },
|
||
})
|
||
}
|
||
|
||
// Keep user.name in sync when member name changes
|
||
if (memberData.name && targetUserId) {
|
||
const user = await ctx.prisma.user.findUnique({ where: { id: targetUserId } })
|
||
if (user) {
|
||
await ctx.prisma.user.update({ where: { id: targetUserId }, data: { name: memberData.name } })
|
||
}
|
||
}
|
||
return member || { success: true }
|
||
}),
|
||
|
||
/**
|
||
* Send/resend invite to existing member (admin only)
|
||
*/
|
||
resendInvite: adminProcedure
|
||
.input(z.object({ memberId: z.string() }))
|
||
.mutation(async ({ ctx, input }) => {
|
||
const member = await ctx.prisma.member.findFirstOrThrow({
|
||
where: { id: input.memberId, orgId: ctx.orgId },
|
||
})
|
||
const org = await ctx.prisma.organization.findUniqueOrThrow({
|
||
where: { id: ctx.orgId },
|
||
})
|
||
await sendInviteEmail({
|
||
to: member.email,
|
||
memberName: member.name,
|
||
orgName: org.name,
|
||
apiUrl: process.env.BETTER_AUTH_URL!,
|
||
})
|
||
return { success: true }
|
||
}),
|
||
|
||
/**
|
||
* Get own member profile
|
||
*/
|
||
me: memberProcedure.query(async ({ ctx }) => {
|
||
const member = await ctx.prisma.member.findFirst({
|
||
where: { userId: ctx.session.user.id, orgId: ctx.orgId },
|
||
include: { org: true },
|
||
})
|
||
return member
|
||
}),
|
||
|
||
/**
|
||
* Update own member profile
|
||
*/
|
||
updateMe: memberProcedure
|
||
.input(
|
||
z.object({
|
||
name: z.string().min(2).optional(),
|
||
email: z.string().email().optional(),
|
||
telefon: z.string().optional(),
|
||
ort: z.string().min(2).optional(),
|
||
betrieb: z.string().min(2).optional(),
|
||
sparte: z.string().min(2).optional(),
|
||
istAusbildungsbetrieb: z.boolean().optional(),
|
||
})
|
||
)
|
||
.mutation(async ({ ctx, input }) => {
|
||
const member = await ctx.prisma.member.findFirst({
|
||
where: { userId: ctx.session.user.id, orgId: ctx.orgId },
|
||
})
|
||
if (!member) throw new Error('Member not found')
|
||
|
||
const updated = await ctx.prisma.member.update({
|
||
where: { id: member.id },
|
||
data: input,
|
||
})
|
||
|
||
// Keep user.name in sync when member name changes
|
||
if (input.name) {
|
||
await ctx.prisma.user.update({
|
||
where: { id: ctx.session.user.id },
|
||
data: { name: input.name },
|
||
})
|
||
}
|
||
return updated
|
||
}),
|
||
|
||
/**
|
||
* Delete member (admin only)
|
||
*/
|
||
delete: adminProcedure
|
||
.input(z.object({ id: z.string() }))
|
||
.mutation(async ({ ctx, input }) => {
|
||
const member = await ctx.prisma.member.findFirst({
|
||
where: { id: input.id, orgId: ctx.orgId },
|
||
})
|
||
|
||
if (!member) {
|
||
// Fallback for user-based "pure" admins
|
||
const adminRole = await ctx.prisma.userRole.findUnique({
|
||
where: { orgId_userId: { orgId: ctx.orgId, userId: input.id } }
|
||
})
|
||
if (!adminRole) throw new Error('Member not found')
|
||
|
||
if (adminRole.userId === ctx.session.user.id) {
|
||
throw new Error('Sie können Ihren eigenen Account nicht löschen.')
|
||
}
|
||
|
||
await ctx.prisma.userRole.delete({ where: { id: adminRole.id } })
|
||
return { success: true }
|
||
}
|
||
|
||
if (member.userId === ctx.session.user.id) {
|
||
throw new Error('Sie können Ihren eigenen Account nicht löschen.')
|
||
}
|
||
|
||
// 1. Remove UserRole link if exists
|
||
if (member.userId) {
|
||
await ctx.prisma.userRole.deleteMany({
|
||
where: { orgId: ctx.orgId, userId: member.userId }
|
||
})
|
||
}
|
||
|
||
// 2. Delete member profile
|
||
await ctx.prisma.member.delete({
|
||
where: { id: member.id }
|
||
})
|
||
|
||
return { success: true }
|
||
}),
|
||
})
|