push
This commit is contained in:
@@ -1,7 +1,59 @@
|
||||
import { z } from 'zod'
|
||||
import { router, memberProcedure, adminProcedure } from '../trpc'
|
||||
import { auth } from '@/lib/auth'
|
||||
import { sendInviteEmail } from '@/lib/email'
|
||||
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),
|
||||
@@ -13,6 +65,8 @@ const MemberInput = z.object({
|
||||
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({
|
||||
@@ -49,67 +103,270 @@ export const membersRouter = router({
|
||||
return members
|
||||
}),
|
||||
|
||||
/**
|
||||
* Get a single member by ID
|
||||
*/
|
||||
byId: memberProcedure
|
||||
.input(z.object({ id: z.string() }))
|
||||
.query(async ({ ctx, input }) => {
|
||||
const member = await ctx.prisma.member.findFirst({
|
||||
let member = await ctx.prisma.member.findFirst({
|
||||
where: { id: input.id, orgId: ctx.orgId },
|
||||
})
|
||||
if (!member) throw new Error('Member not found')
|
||||
return member
|
||||
|
||||
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 a new member (admin only)
|
||||
*/
|
||||
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: {
|
||||
...input,
|
||||
orgId: ctx.orgId,
|
||||
},
|
||||
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: { ...input, orgId: ctx.orgId },
|
||||
data: { ...memberData, orgId: ctx.orgId },
|
||||
})
|
||||
|
||||
// 2. Create/get User via better-auth admin
|
||||
try {
|
||||
await auth.api.createUser({
|
||||
body: {
|
||||
name: input.name,
|
||||
email: input.email,
|
||||
role: 'user',
|
||||
password: undefined,
|
||||
},
|
||||
})
|
||||
} catch {
|
||||
// User may already exist — that's ok
|
||||
}
|
||||
|
||||
// 3. Send magic link
|
||||
const org = await ctx.prisma.organization.findUniqueOrThrow({
|
||||
where: { id: ctx.orgId },
|
||||
})
|
||||
await sendInviteEmail({
|
||||
to: input.email,
|
||||
memberName: input.name,
|
||||
orgName: org.name,
|
||||
apiUrl: process.env.BETTER_AUTH_URL!,
|
||||
})
|
||||
|
||||
// 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
|
||||
}),
|
||||
@@ -118,20 +375,188 @@ export const membersRouter = router({
|
||||
* Update member (admin only)
|
||||
*/
|
||||
update: adminProcedure
|
||||
.input(z.object({ id: z.string(), data: MemberInput.partial() }))
|
||||
.input(z.object({ id: z.string(), data: MemberInput.partial().extend({ role: z.enum(['member', 'admin']).optional() }) }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const member = await ctx.prisma.member.updateMany({
|
||||
const { role, password, ...memberData } = input.data
|
||||
|
||||
let member = await ctx.prisma.member.findFirst({
|
||||
where: { id: input.id, orgId: ctx.orgId },
|
||||
data: input.data,
|
||||
})
|
||||
// Keep user.name in sync when member name changes
|
||||
if (input.data.name) {
|
||||
const m = await ctx.prisma.member.findFirst({ where: { id: input.id }, select: { userId: true } })
|
||||
if (m?.userId) {
|
||||
await ctx.prisma.user.update({ where: { id: m.userId }, data: { name: input.data.name } })
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
return member
|
||||
|
||||
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 }
|
||||
}),
|
||||
|
||||
/**
|
||||
@@ -165,4 +590,84 @@ export const membersRouter = router({
|
||||
})
|
||||
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 }
|
||||
}),
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user