Apply full refactor updates plus pipeline/email UX confirmations
All checks were successful
Build and Push Docker Image / build (push) Successful in 10m33s

This commit is contained in:
Matt
2026-02-14 15:26:42 +01:00
parent e56e143a40
commit b5425e705e
374 changed files with 116737 additions and 111969 deletions

View File

@@ -87,6 +87,7 @@ export const userRouter = router({
updateProfile: protectedProcedure
.input(
z.object({
email: z.string().email().optional(),
name: z.string().min(1).max(255).optional(),
bio: z.string().max(1000).optional(),
phoneNumber: z.string().max(20).optional().nullable(),
@@ -98,7 +99,34 @@ export const userRouter = router({
})
)
.mutation(async ({ ctx, input }) => {
const { bio, expertiseTags, availabilityJson, preferredWorkload, digestFrequency, ...directFields } = input
const {
bio,
expertiseTags,
availabilityJson,
preferredWorkload,
digestFrequency,
email,
...directFields
} = input
const normalizedEmail = email?.toLowerCase().trim()
if (normalizedEmail !== undefined) {
const existing = await ctx.prisma.user.findFirst({
where: {
email: normalizedEmail,
NOT: { id: ctx.user.id },
},
select: { id: true },
})
if (existing) {
throw new TRPCError({
code: 'CONFLICT',
message: 'Another account already uses this email address',
})
}
}
// If bio is provided, merge it into metadataJson
let metadataJson: Prisma.InputJsonValue | undefined
@@ -115,6 +143,7 @@ export const userRouter = router({
where: { id: ctx.user.id },
data: {
...directFields,
...(normalizedEmail !== undefined && { email: normalizedEmail }),
...(metadataJson !== undefined && { metadataJson }),
...(expertiseTags !== undefined && { expertiseTags }),
...(digestFrequency !== undefined && { digestFrequency }),
@@ -258,6 +287,46 @@ export const userRouter = router({
}
}),
/**
* List all invitable user IDs for current filters (not paginated)
*/
listInvitableIds: adminProcedure
.input(
z.object({
role: z.enum(['SUPER_ADMIN', 'PROGRAM_ADMIN', 'AWARD_MASTER', 'JURY_MEMBER', 'MENTOR', 'OBSERVER']).optional(),
roles: z.array(z.enum(['SUPER_ADMIN', 'PROGRAM_ADMIN', 'AWARD_MASTER', 'JURY_MEMBER', 'MENTOR', 'OBSERVER'])).optional(),
search: z.string().optional(),
})
)
.query(async ({ ctx, input }) => {
const where: Record<string, unknown> = {
status: { in: ['NONE', 'INVITED'] },
}
if (input.roles && input.roles.length > 0) {
where.role = { in: input.roles }
} else if (input.role) {
where.role = input.role
}
if (input.search) {
where.OR = [
{ email: { contains: input.search, mode: 'insensitive' } },
{ name: { contains: input.search, mode: 'insensitive' } },
]
}
const users = await ctx.prisma.user.findMany({
where,
select: { id: true },
})
return {
userIds: users.map((u) => u.id),
total: users.length,
}
}),
/**
* Get a single user (admin only)
*/
@@ -347,6 +416,7 @@ export const userRouter = router({
.input(
z.object({
id: z.string(),
email: z.string().email().optional(),
name: z.string().optional().nullable(),
role: z.enum(['SUPER_ADMIN', 'PROGRAM_ADMIN', 'AWARD_MASTER', 'JURY_MEMBER', 'MENTOR', 'OBSERVER']).optional(),
status: z.enum(['NONE', 'INVITED', 'ACTIVE', 'SUSPENDED']).optional(),
@@ -358,6 +428,7 @@ export const userRouter = router({
)
.mutation(async ({ ctx, input }) => {
const { id, ...data } = input
const normalizedEmail = data.email?.toLowerCase().trim()
// Prevent changing super admin role
const targetUser = await ctx.prisma.user.findUniqueOrThrow({
@@ -393,10 +464,32 @@ export const userRouter = router({
})
}
if (normalizedEmail !== undefined) {
const existing = await ctx.prisma.user.findFirst({
where: {
email: normalizedEmail,
NOT: { id },
},
select: { id: true },
})
if (existing) {
throw new TRPCError({
code: 'CONFLICT',
message: 'Another user already uses this email address',
})
}
}
const updateData = {
...data,
...(normalizedEmail !== undefined && { email: normalizedEmail }),
}
const user = await ctx.prisma.$transaction(async (tx) => {
const updated = await tx.user.update({
where: { id },
data,
data: updateData,
})
await logAudit({
@@ -405,7 +498,7 @@ export const userRouter = router({
action: 'UPDATE',
entityType: 'User',
entityId: id,
detailsJson: data,
detailsJson: updateData,
ipAddress: ctx.ip,
userAgent: ctx.userAgent,
})