Apply full refactor updates plus pipeline/email UX confirmations
All checks were successful
Build and Push Docker Image / build (push) Successful in 10m33s
All checks were successful
Build and Push Docker Image / build (push) Successful in 10m33s
This commit is contained in:
@@ -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,
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user