feat: Implement comprehensive member management with user accounts, roles, and password handling for admin and mobile applications.

This commit is contained in:
Timo Knuth
2026-02-27 18:50:17 +01:00
parent 253c3c1c6d
commit 4863d032d9
12 changed files with 148 additions and 115 deletions

View File

@@ -55,11 +55,14 @@ async function createUserDirectly(opts: { email: string; name: string; password:
return { id: userId }
}
const nonEmptyString = (min = 2) =>
z.preprocess((val) => (val === '' ? undefined : val), z.string().min(min).optional())
const MemberInput = z.object({
name: z.string().min(2),
betrieb: z.string().min(2),
sparte: z.string().min(2),
ort: z.string().min(2),
betrieb: z.preprocess((v) => (v === '' ? undefined : v), z.string().optional()),
sparte: z.preprocess((v) => (v === '' ? undefined : v), z.string().optional()),
ort: z.preprocess((v) => (v === '' ? undefined : v), z.string().optional()),
telefon: z.string().optional(),
email: z.string().email(),
status: z.enum(['aktiv', 'ruhend', 'ausgetreten']).default('aktiv'),
@@ -375,7 +378,22 @@ export const membersRouter = router({
* Update member (admin only)
*/
update: adminProcedure
.input(z.object({ id: z.string(), data: MemberInput.partial().extend({ role: z.enum(['member', 'admin']).optional() }) }))
.input(z.object({
id: z.string(),
data: z.object({
name: nonEmptyString(),
betrieb: nonEmptyString(),
sparte: nonEmptyString(),
ort: nonEmptyString(),
telefon: z.string().optional(),
email: z.preprocess((v) => (v === '' ? undefined : v), z.string().email().optional()),
status: z.enum(['aktiv', 'ruhend', 'ausgetreten']).optional(),
istAusbildungsbetrieb: z.boolean().optional(),
seit: z.number().int().min(1900).max(2100).optional(),
role: z.enum(['member', 'admin']).optional(),
password: z.preprocess((val) => (val === '' ? undefined : val), z.string().min(8).optional()),
}),
}))
.mutation(async ({ ctx, input }) => {
const { role, password, ...memberData } = input.data
@@ -524,19 +542,12 @@ export const membersRouter = router({
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,
// Update password hash directly — auth.api.updateUser requires Better Auth admin role
// which our custom UserRole system doesn't set, causing silent failures.
const newHash = await hashPassword(password)
await ctx.prisma.account.update({
where: { id: existingCredAccount.id },
data: { password: newHash },
})
} else {
// No credential account — create one from scratch using Better Auth's own hash format