2026-01-30 13:41:32 +01:00
|
|
|
import { z } from 'zod'
|
|
|
|
|
import { TRPCError } from '@trpc/server'
|
Add profile settings page, mentor management, and S3 email logos
- Add universal /settings/profile page accessible to all roles with
avatar upload, bio, phone, password change, and account deletion
- Expand updateProfile endpoint to accept bio (metadataJson), phone,
and notification preference
- Add deleteAccount endpoint with password confirmation
- Add Profile Settings link to all nav components (admin, jury, mentor,
observer)
- Add /admin/mentors list page and /admin/mentors/[id] detail page for
mentor management
- Add Mentors nav item to admin sidebar
- Update email logo URLs to S3 (s3.monaco-opc.com/public/)
- Add ocean.png background image to email wrapper
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 19:57:12 +01:00
|
|
|
import type { Prisma } from '@prisma/client'
|
2026-02-24 17:44:55 +01:00
|
|
|
import { UserRole } from '@prisma/client'
|
2026-01-30 13:41:32 +01:00
|
|
|
import { router, protectedProcedure, adminProcedure, superAdminProcedure, publicProcedure } from '../trpc'
|
|
|
|
|
import { sendInvitationEmail, sendMagicLinkEmail } from '@/lib/email'
|
|
|
|
|
import { hashPassword, validatePassword } from '@/lib/password'
|
2026-02-02 13:19:28 +01:00
|
|
|
import { attachAvatarUrls } from '@/server/utils/avatar-url'
|
2026-02-05 21:09:06 +01:00
|
|
|
import { logAudit } from '@/server/utils/audit'
|
2026-03-03 19:14:41 +01:00
|
|
|
import { generateInviteToken, getInviteExpiryHours, getInviteExpiryMs } from '@/server/utils/invite'
|
2026-01-31 14:13:16 +01:00
|
|
|
|
2026-01-30 13:41:32 +01:00
|
|
|
export const userRouter = router({
|
|
|
|
|
/**
|
|
|
|
|
* Get current user profile
|
|
|
|
|
*/
|
|
|
|
|
me: protectedProcedure.query(async ({ ctx }) => {
|
2026-02-14 13:30:55 +01:00
|
|
|
const user = await ctx.prisma.user.findUnique({
|
2026-01-30 13:41:32 +01:00
|
|
|
where: { id: ctx.user.id },
|
|
|
|
|
select: {
|
|
|
|
|
id: true,
|
|
|
|
|
email: true,
|
|
|
|
|
name: true,
|
|
|
|
|
role: true,
|
|
|
|
|
status: true,
|
|
|
|
|
expertiseTags: true,
|
Add profile settings page, mentor management, and S3 email logos
- Add universal /settings/profile page accessible to all roles with
avatar upload, bio, phone, password change, and account deletion
- Expand updateProfile endpoint to accept bio (metadataJson), phone,
and notification preference
- Add deleteAccount endpoint with password confirmation
- Add Profile Settings link to all nav components (admin, jury, mentor,
observer)
- Add /admin/mentors list page and /admin/mentors/[id] detail page for
mentor management
- Add Mentors nav item to admin sidebar
- Update email logo URLs to S3 (s3.monaco-opc.com/public/)
- Add ocean.png background image to email wrapper
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 19:57:12 +01:00
|
|
|
metadataJson: true,
|
|
|
|
|
phoneNumber: true,
|
2026-02-04 14:15:06 +01:00
|
|
|
country: true,
|
feat: applicant onboarding, bulk invite, team management enhancements
- Add nationality/institution fields to User model with migration
- Applicant onboarding wizard (name, photo, nationality, country, institution, bio, project logo, preferences)
- Project logo upload from applicant context with team membership verification
- APPLICANT redirects in set-password, onboarding, and auth layout
- Mask evaluation round names as "Evaluation Round 1/2/..." for applicants
- Extend inviteTeamMember with nationality/country/institution/sendInvite fields
- Admin getApplicants query with search/filter/pagination
- Admin bulkInviteApplicants mutation with token generation and emails
- Applicants tab on Members page with bulk select and floating invite bar
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 10:11:11 +01:00
|
|
|
nationality: true,
|
|
|
|
|
institution: true,
|
2026-02-04 15:27:28 +01:00
|
|
|
bio: true,
|
Add profile settings page, mentor management, and S3 email logos
- Add universal /settings/profile page accessible to all roles with
avatar upload, bio, phone, password change, and account deletion
- Expand updateProfile endpoint to accept bio (metadataJson), phone,
and notification preference
- Add deleteAccount endpoint with password confirmation
- Add Profile Settings link to all nav components (admin, jury, mentor,
observer)
- Add /admin/mentors list page and /admin/mentors/[id] detail page for
mentor management
- Add Mentors nav item to admin sidebar
- Update email logo URLs to S3 (s3.monaco-opc.com/public/)
- Add ocean.png background image to email wrapper
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 19:57:12 +01:00
|
|
|
notificationPreference: true,
|
|
|
|
|
profileImageKey: true,
|
Implement 15 platform features: digest, availability, templates, comparison, live voting SSE, file versioning, mentorship, messaging, analytics, drafts, webhooks, peer review, audit enhancements, i18n
Features implemented:
- F1: Email digest notifications with cron endpoint and per-user frequency
- F2: Jury availability windows and workload preferences in smart assignment
- F3: Round templates with save-from-round and CRUD management
- F4: Side-by-side project comparison view for jury members
- F5: Real-time voting dashboard with Server-Sent Events (SSE)
- F6: Live voting UX: QR codes, audience voting, tie-breaking, score animations
- F7: File versioning, inline preview, bulk download with presigned URLs
- F8: Mentor dashboard: milestones, private notes, activity tracking
- F9: Communication hub with broadcasts, templates, and recipient targeting
- F10: Advanced analytics: cross-round comparison, juror consistency, diversity metrics, PDF export
- F11: Applicant draft saving with magic link resume and cron cleanup
- F12: Webhook integration layer with HMAC signing, retry, and delivery logs
- F13: Peer review discussions with anonymized scores and threaded comments
- F14: Audit log enhancements: before/after diffs, session grouping, anomaly detection, retention
- F15: i18n foundation with next-intl (EN/FR), cookie-based locale, language switcher
Schema: 12 new models, field additions to User, Project, ProjectFile, LiveVotingSession, LiveVote, MentorAssignment, AuditLog, Program
New routers: roundTemplate, message, webhook (registered in _app.ts)
New services: email-digest, webhook-dispatcher
New cron endpoints: /api/cron/digest, /api/cron/draft-cleanup, /api/cron/audit-cleanup
New API routes: /api/live-voting/stream (SSE), /api/files/bulk-download
All features are admin-configurable via SystemSettings or per-model settingsJson fields.
Docker build verified successfully.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-05 23:31:41 +01:00
|
|
|
digestFrequency: true,
|
|
|
|
|
availabilityJson: true,
|
|
|
|
|
preferredWorkload: true,
|
2026-01-30 13:41:32 +01:00
|
|
|
createdAt: true,
|
|
|
|
|
lastLoginAt: true,
|
|
|
|
|
},
|
|
|
|
|
})
|
2026-02-14 13:30:55 +01:00
|
|
|
|
|
|
|
|
if (!user) {
|
|
|
|
|
throw new TRPCError({
|
|
|
|
|
code: 'UNAUTHORIZED',
|
|
|
|
|
message: 'User session is stale. Please log out and log back in.',
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return user
|
2026-01-30 13:41:32 +01:00
|
|
|
}),
|
|
|
|
|
|
2026-01-31 14:13:16 +01:00
|
|
|
/**
|
|
|
|
|
* Validate an invitation token (public, no auth required)
|
|
|
|
|
*/
|
|
|
|
|
validateInviteToken: publicProcedure
|
|
|
|
|
.input(z.object({ token: z.string().min(1) }))
|
|
|
|
|
.query(async ({ ctx, input }) => {
|
|
|
|
|
const user = await ctx.prisma.user.findUnique({
|
|
|
|
|
where: { inviteToken: input.token },
|
|
|
|
|
select: { id: true, name: true, email: true, role: true, status: true, inviteTokenExpiresAt: true },
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if (!user) {
|
|
|
|
|
return { valid: false, error: 'INVALID_TOKEN' as const }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (user.status !== 'INVITED') {
|
|
|
|
|
return { valid: false, error: 'ALREADY_ACCEPTED' as const }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (user.inviteTokenExpiresAt && user.inviteTokenExpiresAt < new Date()) {
|
|
|
|
|
return { valid: false, error: 'EXPIRED_TOKEN' as const }
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-03 19:14:41 +01:00
|
|
|
// Check if user belongs to a team (was invited as team member)
|
|
|
|
|
const teamMembership = await ctx.prisma.teamMember.findFirst({
|
|
|
|
|
where: { userId: user.id },
|
|
|
|
|
select: {
|
|
|
|
|
role: true,
|
|
|
|
|
project: { select: { title: true, teamName: true } },
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
2026-01-31 14:13:16 +01:00
|
|
|
return {
|
|
|
|
|
valid: true,
|
|
|
|
|
user: { name: user.name, email: user.email, role: user.role },
|
2026-03-03 19:14:41 +01:00
|
|
|
team: teamMembership
|
|
|
|
|
? { projectTitle: teamMembership.project.title, teamName: teamMembership.project.teamName }
|
|
|
|
|
: null,
|
2026-01-31 14:13:16 +01:00
|
|
|
}
|
|
|
|
|
}),
|
|
|
|
|
|
2026-01-30 13:41:32 +01:00
|
|
|
/**
|
|
|
|
|
* Update current user profile
|
|
|
|
|
*/
|
|
|
|
|
updateProfile: protectedProcedure
|
|
|
|
|
.input(
|
|
|
|
|
z.object({
|
2026-02-14 15:26:42 +01:00
|
|
|
email: z.string().email().optional(),
|
2026-01-30 13:41:32 +01:00
|
|
|
name: z.string().min(1).max(255).optional(),
|
Add profile settings page, mentor management, and S3 email logos
- Add universal /settings/profile page accessible to all roles with
avatar upload, bio, phone, password change, and account deletion
- Expand updateProfile endpoint to accept bio (metadataJson), phone,
and notification preference
- Add deleteAccount endpoint with password confirmation
- Add Profile Settings link to all nav components (admin, jury, mentor,
observer)
- Add /admin/mentors list page and /admin/mentors/[id] detail page for
mentor management
- Add Mentors nav item to admin sidebar
- Update email logo URLs to S3 (s3.monaco-opc.com/public/)
- Add ocean.png background image to email wrapper
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 19:57:12 +01:00
|
|
|
bio: z.string().max(1000).optional(),
|
|
|
|
|
phoneNumber: z.string().max(20).optional().nullable(),
|
feat: applicant onboarding, bulk invite, team management enhancements
- Add nationality/institution fields to User model with migration
- Applicant onboarding wizard (name, photo, nationality, country, institution, bio, project logo, preferences)
- Project logo upload from applicant context with team membership verification
- APPLICANT redirects in set-password, onboarding, and auth layout
- Mask evaluation round names as "Evaluation Round 1/2/..." for applicants
- Extend inviteTeamMember with nationality/country/institution/sendInvite fields
- Admin getApplicants query with search/filter/pagination
- Admin bulkInviteApplicants mutation with token generation and emails
- Applicants tab on Members page with bulk select and floating invite bar
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 10:11:11 +01:00
|
|
|
nationality: z.string().max(100).optional().nullable(),
|
|
|
|
|
institution: z.string().max(255).optional().nullable(),
|
|
|
|
|
country: z.string().max(100).optional(),
|
Add profile settings page, mentor management, and S3 email logos
- Add universal /settings/profile page accessible to all roles with
avatar upload, bio, phone, password change, and account deletion
- Expand updateProfile endpoint to accept bio (metadataJson), phone,
and notification preference
- Add deleteAccount endpoint with password confirmation
- Add Profile Settings link to all nav components (admin, jury, mentor,
observer)
- Add /admin/mentors list page and /admin/mentors/[id] detail page for
mentor management
- Add Mentors nav item to admin sidebar
- Update email logo URLs to S3 (s3.monaco-opc.com/public/)
- Add ocean.png background image to email wrapper
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 19:57:12 +01:00
|
|
|
notificationPreference: z.enum(['EMAIL', 'WHATSAPP', 'BOTH', 'NONE']).optional(),
|
2026-02-05 13:45:34 +01:00
|
|
|
expertiseTags: z.array(z.string()).max(15).optional(),
|
Implement 15 platform features: digest, availability, templates, comparison, live voting SSE, file versioning, mentorship, messaging, analytics, drafts, webhooks, peer review, audit enhancements, i18n
Features implemented:
- F1: Email digest notifications with cron endpoint and per-user frequency
- F2: Jury availability windows and workload preferences in smart assignment
- F3: Round templates with save-from-round and CRUD management
- F4: Side-by-side project comparison view for jury members
- F5: Real-time voting dashboard with Server-Sent Events (SSE)
- F6: Live voting UX: QR codes, audience voting, tie-breaking, score animations
- F7: File versioning, inline preview, bulk download with presigned URLs
- F8: Mentor dashboard: milestones, private notes, activity tracking
- F9: Communication hub with broadcasts, templates, and recipient targeting
- F10: Advanced analytics: cross-round comparison, juror consistency, diversity metrics, PDF export
- F11: Applicant draft saving with magic link resume and cron cleanup
- F12: Webhook integration layer with HMAC signing, retry, and delivery logs
- F13: Peer review discussions with anonymized scores and threaded comments
- F14: Audit log enhancements: before/after diffs, session grouping, anomaly detection, retention
- F15: i18n foundation with next-intl (EN/FR), cookie-based locale, language switcher
Schema: 12 new models, field additions to User, Project, ProjectFile, LiveVotingSession, LiveVote, MentorAssignment, AuditLog, Program
New routers: roundTemplate, message, webhook (registered in _app.ts)
New services: email-digest, webhook-dispatcher
New cron endpoints: /api/cron/digest, /api/cron/draft-cleanup, /api/cron/audit-cleanup
New API routes: /api/live-voting/stream (SSE), /api/files/bulk-download
All features are admin-configurable via SystemSettings or per-model settingsJson fields.
Docker build verified successfully.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-05 23:31:41 +01:00
|
|
|
digestFrequency: z.enum(['none', 'daily', 'weekly']).optional(),
|
|
|
|
|
availabilityJson: z.any().optional(),
|
|
|
|
|
preferredWorkload: z.number().int().min(1).max(100).optional().nullable(),
|
2026-01-30 13:41:32 +01:00
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
.mutation(async ({ ctx, input }) => {
|
2026-02-14 15:26:42 +01:00
|
|
|
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',
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
Add profile settings page, mentor management, and S3 email logos
- Add universal /settings/profile page accessible to all roles with
avatar upload, bio, phone, password change, and account deletion
- Expand updateProfile endpoint to accept bio (metadataJson), phone,
and notification preference
- Add deleteAccount endpoint with password confirmation
- Add Profile Settings link to all nav components (admin, jury, mentor,
observer)
- Add /admin/mentors list page and /admin/mentors/[id] detail page for
mentor management
- Add Mentors nav item to admin sidebar
- Update email logo URLs to S3 (s3.monaco-opc.com/public/)
- Add ocean.png background image to email wrapper
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 19:57:12 +01:00
|
|
|
|
|
|
|
|
// If bio is provided, merge it into metadataJson
|
|
|
|
|
let metadataJson: Prisma.InputJsonValue | undefined
|
|
|
|
|
if (bio !== undefined) {
|
|
|
|
|
const currentUser = await ctx.prisma.user.findUniqueOrThrow({
|
|
|
|
|
where: { id: ctx.user.id },
|
|
|
|
|
select: { metadataJson: true },
|
|
|
|
|
})
|
|
|
|
|
const currentMeta = (currentUser.metadataJson as Record<string, string>) || {}
|
|
|
|
|
metadataJson = { ...currentMeta, bio } as Prisma.InputJsonValue
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-30 13:41:32 +01:00
|
|
|
return ctx.prisma.user.update({
|
|
|
|
|
where: { id: ctx.user.id },
|
Add profile settings page, mentor management, and S3 email logos
- Add universal /settings/profile page accessible to all roles with
avatar upload, bio, phone, password change, and account deletion
- Expand updateProfile endpoint to accept bio (metadataJson), phone,
and notification preference
- Add deleteAccount endpoint with password confirmation
- Add Profile Settings link to all nav components (admin, jury, mentor,
observer)
- Add /admin/mentors list page and /admin/mentors/[id] detail page for
mentor management
- Add Mentors nav item to admin sidebar
- Update email logo URLs to S3 (s3.monaco-opc.com/public/)
- Add ocean.png background image to email wrapper
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 19:57:12 +01:00
|
|
|
data: {
|
|
|
|
|
...directFields,
|
2026-02-14 15:26:42 +01:00
|
|
|
...(normalizedEmail !== undefined && { email: normalizedEmail }),
|
Add profile settings page, mentor management, and S3 email logos
- Add universal /settings/profile page accessible to all roles with
avatar upload, bio, phone, password change, and account deletion
- Expand updateProfile endpoint to accept bio (metadataJson), phone,
and notification preference
- Add deleteAccount endpoint with password confirmation
- Add Profile Settings link to all nav components (admin, jury, mentor,
observer)
- Add /admin/mentors list page and /admin/mentors/[id] detail page for
mentor management
- Add Mentors nav item to admin sidebar
- Update email logo URLs to S3 (s3.monaco-opc.com/public/)
- Add ocean.png background image to email wrapper
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 19:57:12 +01:00
|
|
|
...(metadataJson !== undefined && { metadataJson }),
|
2026-02-05 13:45:34 +01:00
|
|
|
...(expertiseTags !== undefined && { expertiseTags }),
|
Implement 15 platform features: digest, availability, templates, comparison, live voting SSE, file versioning, mentorship, messaging, analytics, drafts, webhooks, peer review, audit enhancements, i18n
Features implemented:
- F1: Email digest notifications with cron endpoint and per-user frequency
- F2: Jury availability windows and workload preferences in smart assignment
- F3: Round templates with save-from-round and CRUD management
- F4: Side-by-side project comparison view for jury members
- F5: Real-time voting dashboard with Server-Sent Events (SSE)
- F6: Live voting UX: QR codes, audience voting, tie-breaking, score animations
- F7: File versioning, inline preview, bulk download with presigned URLs
- F8: Mentor dashboard: milestones, private notes, activity tracking
- F9: Communication hub with broadcasts, templates, and recipient targeting
- F10: Advanced analytics: cross-round comparison, juror consistency, diversity metrics, PDF export
- F11: Applicant draft saving with magic link resume and cron cleanup
- F12: Webhook integration layer with HMAC signing, retry, and delivery logs
- F13: Peer review discussions with anonymized scores and threaded comments
- F14: Audit log enhancements: before/after diffs, session grouping, anomaly detection, retention
- F15: i18n foundation with next-intl (EN/FR), cookie-based locale, language switcher
Schema: 12 new models, field additions to User, Project, ProjectFile, LiveVotingSession, LiveVote, MentorAssignment, AuditLog, Program
New routers: roundTemplate, message, webhook (registered in _app.ts)
New services: email-digest, webhook-dispatcher
New cron endpoints: /api/cron/digest, /api/cron/draft-cleanup, /api/cron/audit-cleanup
New API routes: /api/live-voting/stream (SSE), /api/files/bulk-download
All features are admin-configurable via SystemSettings or per-model settingsJson fields.
Docker build verified successfully.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-05 23:31:41 +01:00
|
|
|
...(digestFrequency !== undefined && { digestFrequency }),
|
|
|
|
|
...(availabilityJson !== undefined && { availabilityJson: availabilityJson as Prisma.InputJsonValue }),
|
|
|
|
|
...(preferredWorkload !== undefined && { preferredWorkload }),
|
Add profile settings page, mentor management, and S3 email logos
- Add universal /settings/profile page accessible to all roles with
avatar upload, bio, phone, password change, and account deletion
- Expand updateProfile endpoint to accept bio (metadataJson), phone,
and notification preference
- Add deleteAccount endpoint with password confirmation
- Add Profile Settings link to all nav components (admin, jury, mentor,
observer)
- Add /admin/mentors list page and /admin/mentors/[id] detail page for
mentor management
- Add Mentors nav item to admin sidebar
- Update email logo URLs to S3 (s3.monaco-opc.com/public/)
- Add ocean.png background image to email wrapper
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 19:57:12 +01:00
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Delete own account (requires password confirmation)
|
|
|
|
|
*/
|
|
|
|
|
deleteAccount: protectedProcedure
|
|
|
|
|
.input(
|
|
|
|
|
z.object({
|
|
|
|
|
password: z.string().min(1),
|
|
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
.mutation(async ({ ctx, input }) => {
|
|
|
|
|
// Get current user with password hash
|
|
|
|
|
const user = await ctx.prisma.user.findUniqueOrThrow({
|
|
|
|
|
where: { id: ctx.user.id },
|
|
|
|
|
select: { id: true, email: true, passwordHash: true, role: true },
|
2026-01-30 13:41:32 +01:00
|
|
|
})
|
Add profile settings page, mentor management, and S3 email logos
- Add universal /settings/profile page accessible to all roles with
avatar upload, bio, phone, password change, and account deletion
- Expand updateProfile endpoint to accept bio (metadataJson), phone,
and notification preference
- Add deleteAccount endpoint with password confirmation
- Add Profile Settings link to all nav components (admin, jury, mentor,
observer)
- Add /admin/mentors list page and /admin/mentors/[id] detail page for
mentor management
- Add Mentors nav item to admin sidebar
- Update email logo URLs to S3 (s3.monaco-opc.com/public/)
- Add ocean.png background image to email wrapper
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 19:57:12 +01:00
|
|
|
|
|
|
|
|
// Prevent super admins from self-deleting
|
|
|
|
|
if (user.role === 'SUPER_ADMIN') {
|
|
|
|
|
throw new TRPCError({
|
|
|
|
|
code: 'FORBIDDEN',
|
|
|
|
|
message: 'Super admins cannot delete their own account',
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!user.passwordHash) {
|
|
|
|
|
throw new TRPCError({
|
|
|
|
|
code: 'BAD_REQUEST',
|
|
|
|
|
message: 'No password set. Please set a password first.',
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Verify password
|
|
|
|
|
const { verifyPassword } = await import('@/lib/password')
|
|
|
|
|
const isValid = await verifyPassword(input.password, user.passwordHash)
|
|
|
|
|
if (!isValid) {
|
|
|
|
|
throw new TRPCError({
|
|
|
|
|
code: 'UNAUTHORIZED',
|
|
|
|
|
message: 'Password is incorrect',
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
Round detail overhaul, file requirements, project management, audit log fix
- Redesign round detail page with 6 tabs (overview, projects, filtering, assignments, config, documents)
- Add jury group assignment selector in round stats bar
- Add FileRequirementsEditor component replacing SubmissionWindowManager
- Add FilteringDashboard component for AI-powered project screening
- Add project removal from rounds (single + bulk) with cascading to subsequent rounds
- Add project add/remove UI in ProjectStatesTable with confirmation dialogs
- Fix logAudit inside $transaction pattern across all 12 router files
(PostgreSQL aborted-transaction state caused silent operation failures)
- Fix special awards creation, deletion, status update, and winner assignment
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 07:49:39 +01:00
|
|
|
// Delete user
|
|
|
|
|
await ctx.prisma.user.delete({
|
|
|
|
|
where: { id: ctx.user.id },
|
|
|
|
|
})
|
Add profile settings page, mentor management, and S3 email logos
- Add universal /settings/profile page accessible to all roles with
avatar upload, bio, phone, password change, and account deletion
- Expand updateProfile endpoint to accept bio (metadataJson), phone,
and notification preference
- Add deleteAccount endpoint with password confirmation
- Add Profile Settings link to all nav components (admin, jury, mentor,
observer)
- Add /admin/mentors list page and /admin/mentors/[id] detail page for
mentor management
- Add Mentors nav item to admin sidebar
- Update email logo URLs to S3 (s3.monaco-opc.com/public/)
- Add ocean.png background image to email wrapper
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 19:57:12 +01:00
|
|
|
|
Round detail overhaul, file requirements, project management, audit log fix
- Redesign round detail page with 6 tabs (overview, projects, filtering, assignments, config, documents)
- Add jury group assignment selector in round stats bar
- Add FileRequirementsEditor component replacing SubmissionWindowManager
- Add FilteringDashboard component for AI-powered project screening
- Add project removal from rounds (single + bulk) with cascading to subsequent rounds
- Add project add/remove UI in ProjectStatesTable with confirmation dialogs
- Fix logAudit inside $transaction pattern across all 12 router files
(PostgreSQL aborted-transaction state caused silent operation failures)
- Fix special awards creation, deletion, status update, and winner assignment
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 07:49:39 +01:00
|
|
|
// Audit outside transaction so failures don't roll back the deletion
|
|
|
|
|
await logAudit({
|
|
|
|
|
prisma: ctx.prisma,
|
|
|
|
|
userId: ctx.user.id,
|
|
|
|
|
action: 'DELETE_OWN_ACCOUNT',
|
|
|
|
|
entityType: 'User',
|
|
|
|
|
entityId: ctx.user.id,
|
|
|
|
|
detailsJson: { email: user.email },
|
|
|
|
|
ipAddress: ctx.ip,
|
|
|
|
|
userAgent: ctx.userAgent,
|
Add profile settings page, mentor management, and S3 email logos
- Add universal /settings/profile page accessible to all roles with
avatar upload, bio, phone, password change, and account deletion
- Expand updateProfile endpoint to accept bio (metadataJson), phone,
and notification preference
- Add deleteAccount endpoint with password confirmation
- Add Profile Settings link to all nav components (admin, jury, mentor,
observer)
- Add /admin/mentors list page and /admin/mentors/[id] detail page for
mentor management
- Add Mentors nav item to admin sidebar
- Update email logo URLs to S3 (s3.monaco-opc.com/public/)
- Add ocean.png background image to email wrapper
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 19:57:12 +01:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return { success: true }
|
2026-01-30 13:41:32 +01:00
|
|
|
}),
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* List all users (admin only)
|
|
|
|
|
*/
|
|
|
|
|
list: adminProcedure
|
|
|
|
|
.input(
|
|
|
|
|
z.object({
|
Fix pipeline config crashes, settings UX, invite roles, seed expertise tags
- Fix critical crash when clicking Edit on INTAKE stage configs: normalize
DB fileRequirements shape (type/required → acceptedMimeTypes/isRequired),
add null guard in getActiveCategoriesFromMimeTypes
- Fix config summary display for all stage types to handle seed data key
mismatches (votingEnabled→juryVotingEnabled, minAssignmentsPerJuror→
minLoadPerJuror, deterministic.rules→rules, etc.)
- Add AWARD_MASTER role to invite page dropdown and user router validations
- Restructure settings sidebar: Tags and Webhooks as direct links instead
of nested tabs, remove redundant Quick Links section
- Seed 38 expertise tags across 7 categories (Marine Science, Technology,
Policy, Conservation, Business, Education, Engineering)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 11:40:44 +01:00
|
|
|
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(),
|
Inline filtering results, select-all across pages, country flags, settings RBAC, and inline role changes
- Round detail: add skeleton loading for filtering stats, inline results table
with expandable rows, pagination, override/reinstate, CSV export, and tooltip
on AI summaries button (removes need for separate results page)
- Projects: add select-all-across-pages with Gmail-style banner, show country
flags with tooltip instead of country codes (table + card views), add listAllIds
backend endpoint
- Settings: allow PROGRAM_ADMIN access to settings page, restrict infrastructure
tabs (AI, Email, Storage, Security, Webhooks) to SUPER_ADMIN only
- Members: add inline role change via dropdown submenu in user actions, enforce
role hierarchy (only super admins can modify admin/super-admin roles) in both
backend and UI
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 23:07:38 +01:00
|
|
|
status: z.enum(['NONE', 'INVITED', 'ACTIVE', 'SUSPENDED']).optional(),
|
2026-01-30 13:41:32 +01:00
|
|
|
search: z.string().optional(),
|
|
|
|
|
page: z.number().int().min(1).default(1),
|
|
|
|
|
perPage: z.number().int().min(1).max(100).default(20),
|
|
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
.query(async ({ ctx, input }) => {
|
Implement Prototype 1 improvements: unified members, project filters, audit expansion, filtering rounds, special awards
- Unified Member Management: merge /admin/users and /admin/mentors into /admin/members with role tabs, search, pagination
- Project List Filters: add search, multi-status filter, round/category/country selects, boolean toggles, URL persistence
- Audit Log Expansion: track logins, round state changes, evaluation submissions, file access, role changes via shared logAudit utility
- Founding Date Field: add foundedAt to Project model with CSV import support
- Filtering Round System: configurable rules (field-based, document check, AI screening), execution engine, results review with override/reinstate
- Special Awards System: named awards with eligibility criteria, dedicated jury, PICK_WINNER/RANKED/SCORED voting modes, AI eligibility
- Dashboard resilience: wrap heavy queries in try-catch to prevent error boundary on transient DB failures
- Reusable pagination component extracted to src/components/shared/pagination.tsx
- Old /admin/users and /admin/mentors routes redirect to /admin/members
- Prisma migration for all schema additions (additive, no data loss)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-02 16:58:29 +01:00
|
|
|
const { role, roles, status, search, page, perPage } = input
|
2026-01-30 13:41:32 +01:00
|
|
|
const skip = (page - 1) * perPage
|
|
|
|
|
|
|
|
|
|
const where: Record<string, unknown> = {}
|
|
|
|
|
|
Implement Prototype 1 improvements: unified members, project filters, audit expansion, filtering rounds, special awards
- Unified Member Management: merge /admin/users and /admin/mentors into /admin/members with role tabs, search, pagination
- Project List Filters: add search, multi-status filter, round/category/country selects, boolean toggles, URL persistence
- Audit Log Expansion: track logins, round state changes, evaluation submissions, file access, role changes via shared logAudit utility
- Founding Date Field: add foundedAt to Project model with CSV import support
- Filtering Round System: configurable rules (field-based, document check, AI screening), execution engine, results review with override/reinstate
- Special Awards System: named awards with eligibility criteria, dedicated jury, PICK_WINNER/RANKED/SCORED voting modes, AI eligibility
- Dashboard resilience: wrap heavy queries in try-catch to prevent error boundary on transient DB failures
- Reusable pagination component extracted to src/components/shared/pagination.tsx
- Old /admin/users and /admin/mentors routes redirect to /admin/members
- Prisma migration for all schema additions (additive, no data loss)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-02 16:58:29 +01:00
|
|
|
if (roles && roles.length > 0) {
|
|
|
|
|
where.role = { in: roles }
|
|
|
|
|
} else if (role) {
|
|
|
|
|
where.role = role
|
|
|
|
|
}
|
2026-01-30 13:41:32 +01:00
|
|
|
if (status) where.status = status
|
|
|
|
|
if (search) {
|
|
|
|
|
where.OR = [
|
|
|
|
|
{ email: { contains: search, mode: 'insensitive' } },
|
|
|
|
|
{ name: { contains: search, mode: 'insensitive' } },
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const [users, total] = await Promise.all([
|
|
|
|
|
ctx.prisma.user.findMany({
|
|
|
|
|
where,
|
|
|
|
|
skip,
|
|
|
|
|
take: perPage,
|
|
|
|
|
orderBy: { createdAt: 'desc' },
|
|
|
|
|
select: {
|
|
|
|
|
id: true,
|
|
|
|
|
email: true,
|
|
|
|
|
name: true,
|
|
|
|
|
role: true,
|
2026-02-24 17:44:55 +01:00
|
|
|
roles: true,
|
2026-01-30 13:41:32 +01:00
|
|
|
status: true,
|
|
|
|
|
expertiseTags: true,
|
|
|
|
|
maxAssignments: true,
|
Implement 15 platform features: digest, availability, templates, comparison, live voting SSE, file versioning, mentorship, messaging, analytics, drafts, webhooks, peer review, audit enhancements, i18n
Features implemented:
- F1: Email digest notifications with cron endpoint and per-user frequency
- F2: Jury availability windows and workload preferences in smart assignment
- F3: Round templates with save-from-round and CRUD management
- F4: Side-by-side project comparison view for jury members
- F5: Real-time voting dashboard with Server-Sent Events (SSE)
- F6: Live voting UX: QR codes, audience voting, tie-breaking, score animations
- F7: File versioning, inline preview, bulk download with presigned URLs
- F8: Mentor dashboard: milestones, private notes, activity tracking
- F9: Communication hub with broadcasts, templates, and recipient targeting
- F10: Advanced analytics: cross-round comparison, juror consistency, diversity metrics, PDF export
- F11: Applicant draft saving with magic link resume and cron cleanup
- F12: Webhook integration layer with HMAC signing, retry, and delivery logs
- F13: Peer review discussions with anonymized scores and threaded comments
- F14: Audit log enhancements: before/after diffs, session grouping, anomaly detection, retention
- F15: i18n foundation with next-intl (EN/FR), cookie-based locale, language switcher
Schema: 12 new models, field additions to User, Project, ProjectFile, LiveVotingSession, LiveVote, MentorAssignment, AuditLog, Program
New routers: roundTemplate, message, webhook (registered in _app.ts)
New services: email-digest, webhook-dispatcher
New cron endpoints: /api/cron/digest, /api/cron/draft-cleanup, /api/cron/audit-cleanup
New API routes: /api/live-voting/stream (SSE), /api/files/bulk-download
All features are admin-configurable via SystemSettings or per-model settingsJson fields.
Docker build verified successfully.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-05 23:31:41 +01:00
|
|
|
availabilityJson: true,
|
|
|
|
|
preferredWorkload: true,
|
2026-02-02 13:19:28 +01:00
|
|
|
profileImageKey: true,
|
|
|
|
|
profileImageProvider: true,
|
2026-01-30 13:41:32 +01:00
|
|
|
createdAt: true,
|
|
|
|
|
lastLoginAt: true,
|
|
|
|
|
_count: {
|
Implement Prototype 1 improvements: unified members, project filters, audit expansion, filtering rounds, special awards
- Unified Member Management: merge /admin/users and /admin/mentors into /admin/members with role tabs, search, pagination
- Project List Filters: add search, multi-status filter, round/category/country selects, boolean toggles, URL persistence
- Audit Log Expansion: track logins, round state changes, evaluation submissions, file access, role changes via shared logAudit utility
- Founding Date Field: add foundedAt to Project model with CSV import support
- Filtering Round System: configurable rules (field-based, document check, AI screening), execution engine, results review with override/reinstate
- Special Awards System: named awards with eligibility criteria, dedicated jury, PICK_WINNER/RANKED/SCORED voting modes, AI eligibility
- Dashboard resilience: wrap heavy queries in try-catch to prevent error boundary on transient DB failures
- Reusable pagination component extracted to src/components/shared/pagination.tsx
- Old /admin/users and /admin/mentors routes redirect to /admin/members
- Prisma migration for all schema additions (additive, no data loss)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-02 16:58:29 +01:00
|
|
|
select: { assignments: true, mentorAssignments: true },
|
2026-01-30 13:41:32 +01:00
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
ctx.prisma.user.count({ where }),
|
|
|
|
|
])
|
|
|
|
|
|
2026-02-02 13:19:28 +01:00
|
|
|
const usersWithAvatars = await attachAvatarUrls(users)
|
|
|
|
|
|
2026-01-30 13:41:32 +01:00
|
|
|
return {
|
2026-02-02 13:19:28 +01:00
|
|
|
users: usersWithAvatars,
|
2026-01-30 13:41:32 +01:00
|
|
|
total,
|
|
|
|
|
page,
|
|
|
|
|
perPage,
|
|
|
|
|
totalPages: Math.ceil(total / perPage),
|
|
|
|
|
}
|
|
|
|
|
}),
|
|
|
|
|
|
2026-02-14 15:26:42 +01:00
|
|
|
/**
|
|
|
|
|
* 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,
|
|
|
|
|
}
|
|
|
|
|
}),
|
|
|
|
|
|
2026-01-30 13:41:32 +01:00
|
|
|
/**
|
|
|
|
|
* Get a single user (admin only)
|
|
|
|
|
*/
|
|
|
|
|
get: adminProcedure
|
|
|
|
|
.input(z.object({ id: z.string() }))
|
|
|
|
|
.query(async ({ ctx, input }) => {
|
2026-02-05 20:31:08 +01:00
|
|
|
const user = await ctx.prisma.user.findUniqueOrThrow({
|
|
|
|
|
where: { id: input.id },
|
|
|
|
|
include: {
|
|
|
|
|
_count: {
|
|
|
|
|
select: { assignments: true, mentorAssignments: true },
|
2026-01-30 13:41:32 +01:00
|
|
|
},
|
2026-02-05 20:31:08 +01:00
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
return user
|
2026-01-30 13:41:32 +01:00
|
|
|
}),
|
|
|
|
|
|
2026-02-20 14:23:10 +01:00
|
|
|
/**
|
|
|
|
|
* Resolve a batch of user IDs to names (admin only).
|
|
|
|
|
* Returns a map of id → name for displaying in audit logs, etc.
|
|
|
|
|
*/
|
|
|
|
|
resolveNames: adminProcedure
|
|
|
|
|
.input(z.object({ ids: z.array(z.string()).max(50) }))
|
|
|
|
|
.query(async ({ ctx, input }) => {
|
|
|
|
|
if (input.ids.length === 0) return {}
|
|
|
|
|
const users = await ctx.prisma.user.findMany({
|
|
|
|
|
where: { id: { in: input.ids } },
|
|
|
|
|
select: { id: true, name: true, email: true },
|
|
|
|
|
})
|
|
|
|
|
const map: Record<string, string> = {}
|
|
|
|
|
for (const u of users) {
|
|
|
|
|
map[u.id] = u.name || u.email
|
|
|
|
|
}
|
|
|
|
|
return map
|
|
|
|
|
}),
|
|
|
|
|
|
2026-01-30 13:41:32 +01:00
|
|
|
/**
|
|
|
|
|
* Create/invite a new user (admin only)
|
|
|
|
|
*/
|
|
|
|
|
create: adminProcedure
|
|
|
|
|
.input(
|
|
|
|
|
z.object({
|
|
|
|
|
email: z.string().email(),
|
|
|
|
|
name: z.string().optional(),
|
Fix pipeline config crashes, settings UX, invite roles, seed expertise tags
- Fix critical crash when clicking Edit on INTAKE stage configs: normalize
DB fileRequirements shape (type/required → acceptedMimeTypes/isRequired),
add null guard in getActiveCategoriesFromMimeTypes
- Fix config summary display for all stage types to handle seed data key
mismatches (votingEnabled→juryVotingEnabled, minAssignmentsPerJuror→
minLoadPerJuror, deterministic.rules→rules, etc.)
- Add AWARD_MASTER role to invite page dropdown and user router validations
- Restructure settings sidebar: Tags and Webhooks as direct links instead
of nested tabs, remove redundant Quick Links section
- Seed 38 expertise tags across 7 categories (Marine Science, Technology,
Policy, Conservation, Business, Education, Engineering)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 11:40:44 +01:00
|
|
|
role: z.enum(['SUPER_ADMIN', 'PROGRAM_ADMIN', 'AWARD_MASTER', 'JURY_MEMBER', 'MENTOR', 'OBSERVER']).default('JURY_MEMBER'),
|
2026-01-30 13:41:32 +01:00
|
|
|
expertiseTags: z.array(z.string()).optional(),
|
|
|
|
|
maxAssignments: z.number().int().min(1).max(100).optional(),
|
|
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
.mutation(async ({ ctx, input }) => {
|
|
|
|
|
// Check if user already exists
|
|
|
|
|
const existing = await ctx.prisma.user.findUnique({
|
|
|
|
|
where: { email: input.email },
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if (existing) {
|
|
|
|
|
throw new TRPCError({
|
|
|
|
|
code: 'CONFLICT',
|
|
|
|
|
message: 'A user with this email already exists',
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-08 23:01:33 +01:00
|
|
|
// Prevent non-super-admins from creating super admins or program admins
|
|
|
|
|
if (input.role === 'SUPER_ADMIN' && ctx.user.role !== 'SUPER_ADMIN') {
|
|
|
|
|
throw new TRPCError({
|
|
|
|
|
code: 'FORBIDDEN',
|
|
|
|
|
message: 'Only super admins can create super admins',
|
|
|
|
|
})
|
|
|
|
|
}
|
2026-01-30 13:41:32 +01:00
|
|
|
if (input.role === 'PROGRAM_ADMIN' && ctx.user.role !== 'SUPER_ADMIN') {
|
|
|
|
|
throw new TRPCError({
|
|
|
|
|
code: 'FORBIDDEN',
|
|
|
|
|
message: 'Only super admins can create program admins',
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
Round detail overhaul, file requirements, project management, audit log fix
- Redesign round detail page with 6 tabs (overview, projects, filtering, assignments, config, documents)
- Add jury group assignment selector in round stats bar
- Add FileRequirementsEditor component replacing SubmissionWindowManager
- Add FilteringDashboard component for AI-powered project screening
- Add project removal from rounds (single + bulk) with cascading to subsequent rounds
- Add project add/remove UI in ProjectStatesTable with confirmation dialogs
- Fix logAudit inside $transaction pattern across all 12 router files
(PostgreSQL aborted-transaction state caused silent operation failures)
- Fix special awards creation, deletion, status update, and winner assignment
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 07:49:39 +01:00
|
|
|
const user = await ctx.prisma.user.create({
|
|
|
|
|
data: {
|
|
|
|
|
...input,
|
|
|
|
|
status: 'INVITED',
|
|
|
|
|
},
|
|
|
|
|
})
|
2026-02-05 21:09:06 +01:00
|
|
|
|
Round detail overhaul, file requirements, project management, audit log fix
- Redesign round detail page with 6 tabs (overview, projects, filtering, assignments, config, documents)
- Add jury group assignment selector in round stats bar
- Add FileRequirementsEditor component replacing SubmissionWindowManager
- Add FilteringDashboard component for AI-powered project screening
- Add project removal from rounds (single + bulk) with cascading to subsequent rounds
- Add project add/remove UI in ProjectStatesTable with confirmation dialogs
- Fix logAudit inside $transaction pattern across all 12 router files
(PostgreSQL aborted-transaction state caused silent operation failures)
- Fix special awards creation, deletion, status update, and winner assignment
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 07:49:39 +01:00
|
|
|
// Audit outside transaction so failures don't roll back the user creation
|
|
|
|
|
await logAudit({
|
|
|
|
|
prisma: ctx.prisma,
|
|
|
|
|
userId: ctx.user.id,
|
|
|
|
|
action: 'CREATE',
|
|
|
|
|
entityType: 'User',
|
|
|
|
|
entityId: user.id,
|
|
|
|
|
detailsJson: { email: input.email, role: input.role },
|
|
|
|
|
ipAddress: ctx.ip,
|
|
|
|
|
userAgent: ctx.userAgent,
|
2026-01-30 13:41:32 +01:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return user
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Update a user (admin only)
|
|
|
|
|
*/
|
|
|
|
|
update: adminProcedure
|
|
|
|
|
.input(
|
|
|
|
|
z.object({
|
|
|
|
|
id: z.string(),
|
2026-02-14 15:26:42 +01:00
|
|
|
email: z.string().email().optional(),
|
2026-01-30 13:41:32 +01:00
|
|
|
name: z.string().optional().nullable(),
|
Fix pipeline config crashes, settings UX, invite roles, seed expertise tags
- Fix critical crash when clicking Edit on INTAKE stage configs: normalize
DB fileRequirements shape (type/required → acceptedMimeTypes/isRequired),
add null guard in getActiveCategoriesFromMimeTypes
- Fix config summary display for all stage types to handle seed data key
mismatches (votingEnabled→juryVotingEnabled, minAssignmentsPerJuror→
minLoadPerJuror, deterministic.rules→rules, etc.)
- Add AWARD_MASTER role to invite page dropdown and user router validations
- Restructure settings sidebar: Tags and Webhooks as direct links instead
of nested tabs, remove redundant Quick Links section
- Seed 38 expertise tags across 7 categories (Marine Science, Technology,
Policy, Conservation, Business, Education, Engineering)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 11:40:44 +01:00
|
|
|
role: z.enum(['SUPER_ADMIN', 'PROGRAM_ADMIN', 'AWARD_MASTER', 'JURY_MEMBER', 'MENTOR', 'OBSERVER']).optional(),
|
Inline filtering results, select-all across pages, country flags, settings RBAC, and inline role changes
- Round detail: add skeleton loading for filtering stats, inline results table
with expandable rows, pagination, override/reinstate, CSV export, and tooltip
on AI summaries button (removes need for separate results page)
- Projects: add select-all-across-pages with Gmail-style banner, show country
flags with tooltip instead of country codes (table + card views), add listAllIds
backend endpoint
- Settings: allow PROGRAM_ADMIN access to settings page, restrict infrastructure
tabs (AI, Email, Storage, Security, Webhooks) to SUPER_ADMIN only
- Members: add inline role change via dropdown submenu in user actions, enforce
role hierarchy (only super admins can modify admin/super-admin roles) in both
backend and UI
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 23:07:38 +01:00
|
|
|
status: z.enum(['NONE', 'INVITED', 'ACTIVE', 'SUSPENDED']).optional(),
|
2026-01-30 13:41:32 +01:00
|
|
|
expertiseTags: z.array(z.string()).optional(),
|
|
|
|
|
maxAssignments: z.number().int().min(1).max(100).optional().nullable(),
|
Implement 15 platform features: digest, availability, templates, comparison, live voting SSE, file versioning, mentorship, messaging, analytics, drafts, webhooks, peer review, audit enhancements, i18n
Features implemented:
- F1: Email digest notifications with cron endpoint and per-user frequency
- F2: Jury availability windows and workload preferences in smart assignment
- F3: Round templates with save-from-round and CRUD management
- F4: Side-by-side project comparison view for jury members
- F5: Real-time voting dashboard with Server-Sent Events (SSE)
- F6: Live voting UX: QR codes, audience voting, tie-breaking, score animations
- F7: File versioning, inline preview, bulk download with presigned URLs
- F8: Mentor dashboard: milestones, private notes, activity tracking
- F9: Communication hub with broadcasts, templates, and recipient targeting
- F10: Advanced analytics: cross-round comparison, juror consistency, diversity metrics, PDF export
- F11: Applicant draft saving with magic link resume and cron cleanup
- F12: Webhook integration layer with HMAC signing, retry, and delivery logs
- F13: Peer review discussions with anonymized scores and threaded comments
- F14: Audit log enhancements: before/after diffs, session grouping, anomaly detection, retention
- F15: i18n foundation with next-intl (EN/FR), cookie-based locale, language switcher
Schema: 12 new models, field additions to User, Project, ProjectFile, LiveVotingSession, LiveVote, MentorAssignment, AuditLog, Program
New routers: roundTemplate, message, webhook (registered in _app.ts)
New services: email-digest, webhook-dispatcher
New cron endpoints: /api/cron/digest, /api/cron/draft-cleanup, /api/cron/audit-cleanup
New API routes: /api/live-voting/stream (SSE), /api/files/bulk-download
All features are admin-configurable via SystemSettings or per-model settingsJson fields.
Docker build verified successfully.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-05 23:31:41 +01:00
|
|
|
availabilityJson: z.any().optional(),
|
|
|
|
|
preferredWorkload: z.number().int().min(1).max(100).optional().nullable(),
|
2026-01-30 13:41:32 +01:00
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
.mutation(async ({ ctx, input }) => {
|
|
|
|
|
const { id, ...data } = input
|
2026-02-14 15:26:42 +01:00
|
|
|
const normalizedEmail = data.email?.toLowerCase().trim()
|
2026-01-30 13:41:32 +01:00
|
|
|
|
|
|
|
|
// Prevent changing super admin role
|
|
|
|
|
const targetUser = await ctx.prisma.user.findUniqueOrThrow({
|
|
|
|
|
where: { id },
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if (targetUser.role === 'SUPER_ADMIN' && ctx.user.role !== 'SUPER_ADMIN') {
|
|
|
|
|
throw new TRPCError({
|
|
|
|
|
code: 'FORBIDDEN',
|
|
|
|
|
message: 'Cannot modify super admin',
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
Inline filtering results, select-all across pages, country flags, settings RBAC, and inline role changes
- Round detail: add skeleton loading for filtering stats, inline results table
with expandable rows, pagination, override/reinstate, CSV export, and tooltip
on AI summaries button (removes need for separate results page)
- Projects: add select-all-across-pages with Gmail-style banner, show country
flags with tooltip instead of country codes (table + card views), add listAllIds
backend endpoint
- Settings: allow PROGRAM_ADMIN access to settings page, restrict infrastructure
tabs (AI, Email, Storage, Security, Webhooks) to SUPER_ADMIN only
- Members: add inline role change via dropdown submenu in user actions, enforce
role hierarchy (only super admins can modify admin/super-admin roles) in both
backend and UI
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 23:07:38 +01:00
|
|
|
// Prevent non-super-admins from changing admin roles
|
|
|
|
|
if (data.role && targetUser.role === 'PROGRAM_ADMIN' && ctx.user.role !== 'SUPER_ADMIN') {
|
|
|
|
|
throw new TRPCError({
|
|
|
|
|
code: 'FORBIDDEN',
|
|
|
|
|
message: 'Only super admins can change admin roles',
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-08 23:01:33 +01:00
|
|
|
// Prevent non-super-admins from assigning super admin or admin role
|
|
|
|
|
if (data.role === 'SUPER_ADMIN' && ctx.user.role !== 'SUPER_ADMIN') {
|
|
|
|
|
throw new TRPCError({
|
|
|
|
|
code: 'FORBIDDEN',
|
|
|
|
|
message: 'Only super admins can assign super admin role',
|
|
|
|
|
})
|
|
|
|
|
}
|
2026-01-30 13:41:32 +01:00
|
|
|
if (data.role === 'PROGRAM_ADMIN' && ctx.user.role !== 'SUPER_ADMIN') {
|
|
|
|
|
throw new TRPCError({
|
|
|
|
|
code: 'FORBIDDEN',
|
|
|
|
|
message: 'Only super admins can assign admin role',
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-14 15:26:42 +01:00
|
|
|
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 }),
|
|
|
|
|
}
|
|
|
|
|
|
Round detail overhaul, file requirements, project management, audit log fix
- Redesign round detail page with 6 tabs (overview, projects, filtering, assignments, config, documents)
- Add jury group assignment selector in round stats bar
- Add FileRequirementsEditor component replacing SubmissionWindowManager
- Add FilteringDashboard component for AI-powered project screening
- Add project removal from rounds (single + bulk) with cascading to subsequent rounds
- Add project add/remove UI in ProjectStatesTable with confirmation dialogs
- Fix logAudit inside $transaction pattern across all 12 router files
(PostgreSQL aborted-transaction state caused silent operation failures)
- Fix special awards creation, deletion, status update, and winner assignment
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 07:49:39 +01:00
|
|
|
const user = await ctx.prisma.user.update({
|
|
|
|
|
where: { id },
|
|
|
|
|
data: updateData,
|
|
|
|
|
})
|
2026-01-30 13:41:32 +01:00
|
|
|
|
Round detail overhaul, file requirements, project management, audit log fix
- Redesign round detail page with 6 tabs (overview, projects, filtering, assignments, config, documents)
- Add jury group assignment selector in round stats bar
- Add FileRequirementsEditor component replacing SubmissionWindowManager
- Add FilteringDashboard component for AI-powered project screening
- Add project removal from rounds (single + bulk) with cascading to subsequent rounds
- Add project add/remove UI in ProjectStatesTable with confirmation dialogs
- Fix logAudit inside $transaction pattern across all 12 router files
(PostgreSQL aborted-transaction state caused silent operation failures)
- Fix special awards creation, deletion, status update, and winner assignment
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 07:49:39 +01:00
|
|
|
// Audit outside transaction so failures don't roll back the update
|
|
|
|
|
await logAudit({
|
|
|
|
|
prisma: ctx.prisma,
|
|
|
|
|
userId: ctx.user.id,
|
|
|
|
|
action: 'UPDATE',
|
|
|
|
|
entityType: 'User',
|
|
|
|
|
entityId: id,
|
|
|
|
|
detailsJson: updateData,
|
|
|
|
|
ipAddress: ctx.ip,
|
|
|
|
|
userAgent: ctx.userAgent,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Track role change specifically
|
|
|
|
|
if (data.role && data.role !== targetUser.role) {
|
2026-02-05 21:09:06 +01:00
|
|
|
await logAudit({
|
Round detail overhaul, file requirements, project management, audit log fix
- Redesign round detail page with 6 tabs (overview, projects, filtering, assignments, config, documents)
- Add jury group assignment selector in round stats bar
- Add FileRequirementsEditor component replacing SubmissionWindowManager
- Add FilteringDashboard component for AI-powered project screening
- Add project removal from rounds (single + bulk) with cascading to subsequent rounds
- Add project add/remove UI in ProjectStatesTable with confirmation dialogs
- Fix logAudit inside $transaction pattern across all 12 router files
(PostgreSQL aborted-transaction state caused silent operation failures)
- Fix special awards creation, deletion, status update, and winner assignment
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 07:49:39 +01:00
|
|
|
prisma: ctx.prisma,
|
2026-01-30 13:41:32 +01:00
|
|
|
userId: ctx.user.id,
|
Round detail overhaul, file requirements, project management, audit log fix
- Redesign round detail page with 6 tabs (overview, projects, filtering, assignments, config, documents)
- Add jury group assignment selector in round stats bar
- Add FileRequirementsEditor component replacing SubmissionWindowManager
- Add FilteringDashboard component for AI-powered project screening
- Add project removal from rounds (single + bulk) with cascading to subsequent rounds
- Add project add/remove UI in ProjectStatesTable with confirmation dialogs
- Fix logAudit inside $transaction pattern across all 12 router files
(PostgreSQL aborted-transaction state caused silent operation failures)
- Fix special awards creation, deletion, status update, and winner assignment
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 07:49:39 +01:00
|
|
|
action: 'ROLE_CHANGED',
|
2026-01-30 13:41:32 +01:00
|
|
|
entityType: 'User',
|
|
|
|
|
entityId: id,
|
Round detail overhaul, file requirements, project management, audit log fix
- Redesign round detail page with 6 tabs (overview, projects, filtering, assignments, config, documents)
- Add jury group assignment selector in round stats bar
- Add FileRequirementsEditor component replacing SubmissionWindowManager
- Add FilteringDashboard component for AI-powered project screening
- Add project removal from rounds (single + bulk) with cascading to subsequent rounds
- Add project add/remove UI in ProjectStatesTable with confirmation dialogs
- Fix logAudit inside $transaction pattern across all 12 router files
(PostgreSQL aborted-transaction state caused silent operation failures)
- Fix special awards creation, deletion, status update, and winner assignment
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 07:49:39 +01:00
|
|
|
detailsJson: { previousRole: targetUser.role, newRole: data.role },
|
2026-01-30 13:41:32 +01:00
|
|
|
ipAddress: ctx.ip,
|
|
|
|
|
userAgent: ctx.userAgent,
|
2026-02-05 21:09:06 +01:00
|
|
|
})
|
Round detail overhaul, file requirements, project management, audit log fix
- Redesign round detail page with 6 tabs (overview, projects, filtering, assignments, config, documents)
- Add jury group assignment selector in round stats bar
- Add FileRequirementsEditor component replacing SubmissionWindowManager
- Add FilteringDashboard component for AI-powered project screening
- Add project removal from rounds (single + bulk) with cascading to subsequent rounds
- Add project add/remove UI in ProjectStatesTable with confirmation dialogs
- Fix logAudit inside $transaction pattern across all 12 router files
(PostgreSQL aborted-transaction state caused silent operation failures)
- Fix special awards creation, deletion, status update, and winner assignment
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 07:49:39 +01:00
|
|
|
}
|
Implement Prototype 1 improvements: unified members, project filters, audit expansion, filtering rounds, special awards
- Unified Member Management: merge /admin/users and /admin/mentors into /admin/members with role tabs, search, pagination
- Project List Filters: add search, multi-status filter, round/category/country selects, boolean toggles, URL persistence
- Audit Log Expansion: track logins, round state changes, evaluation submissions, file access, role changes via shared logAudit utility
- Founding Date Field: add foundedAt to Project model with CSV import support
- Filtering Round System: configurable rules (field-based, document check, AI screening), execution engine, results review with override/reinstate
- Special Awards System: named awards with eligibility criteria, dedicated jury, PICK_WINNER/RANKED/SCORED voting modes, AI eligibility
- Dashboard resilience: wrap heavy queries in try-catch to prevent error boundary on transient DB failures
- Reusable pagination component extracted to src/components/shared/pagination.tsx
- Old /admin/users and /admin/mentors routes redirect to /admin/members
- Prisma migration for all schema additions (additive, no data loss)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-02 16:58:29 +01:00
|
|
|
|
2026-01-30 13:41:32 +01:00
|
|
|
return user
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Delete a user (super admin only)
|
|
|
|
|
*/
|
|
|
|
|
delete: superAdminProcedure
|
|
|
|
|
.input(z.object({ id: z.string() }))
|
|
|
|
|
.mutation(async ({ ctx, input }) => {
|
|
|
|
|
// Prevent self-deletion
|
|
|
|
|
if (input.id === ctx.user.id) {
|
|
|
|
|
throw new TRPCError({
|
|
|
|
|
code: 'BAD_REQUEST',
|
|
|
|
|
message: 'Cannot delete yourself',
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
Round detail overhaul, file requirements, project management, audit log fix
- Redesign round detail page with 6 tabs (overview, projects, filtering, assignments, config, documents)
- Add jury group assignment selector in round stats bar
- Add FileRequirementsEditor component replacing SubmissionWindowManager
- Add FilteringDashboard component for AI-powered project screening
- Add project removal from rounds (single + bulk) with cascading to subsequent rounds
- Add project add/remove UI in ProjectStatesTable with confirmation dialogs
- Fix logAudit inside $transaction pattern across all 12 router files
(PostgreSQL aborted-transaction state caused silent operation failures)
- Fix special awards creation, deletion, status update, and winner assignment
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 07:49:39 +01:00
|
|
|
// Fetch user data before deletion for the audit log
|
|
|
|
|
const target = await ctx.prisma.user.findUniqueOrThrow({
|
|
|
|
|
where: { id: input.id },
|
|
|
|
|
select: { email: true },
|
|
|
|
|
})
|
2026-01-30 13:41:32 +01:00
|
|
|
|
Round detail overhaul, file requirements, project management, audit log fix
- Redesign round detail page with 6 tabs (overview, projects, filtering, assignments, config, documents)
- Add jury group assignment selector in round stats bar
- Add FileRequirementsEditor component replacing SubmissionWindowManager
- Add FilteringDashboard component for AI-powered project screening
- Add project removal from rounds (single + bulk) with cascading to subsequent rounds
- Add project add/remove UI in ProjectStatesTable with confirmation dialogs
- Fix logAudit inside $transaction pattern across all 12 router files
(PostgreSQL aborted-transaction state caused silent operation failures)
- Fix special awards creation, deletion, status update, and winner assignment
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 07:49:39 +01:00
|
|
|
const user = await ctx.prisma.user.delete({
|
|
|
|
|
where: { id: input.id },
|
|
|
|
|
})
|
2026-02-05 21:09:06 +01:00
|
|
|
|
Round detail overhaul, file requirements, project management, audit log fix
- Redesign round detail page with 6 tabs (overview, projects, filtering, assignments, config, documents)
- Add jury group assignment selector in round stats bar
- Add FileRequirementsEditor component replacing SubmissionWindowManager
- Add FilteringDashboard component for AI-powered project screening
- Add project removal from rounds (single + bulk) with cascading to subsequent rounds
- Add project add/remove UI in ProjectStatesTable with confirmation dialogs
- Fix logAudit inside $transaction pattern across all 12 router files
(PostgreSQL aborted-transaction state caused silent operation failures)
- Fix special awards creation, deletion, status update, and winner assignment
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 07:49:39 +01:00
|
|
|
// Audit outside transaction so failures don't roll back the deletion
|
|
|
|
|
await logAudit({
|
|
|
|
|
prisma: ctx.prisma,
|
|
|
|
|
userId: ctx.user.id,
|
|
|
|
|
action: 'DELETE',
|
|
|
|
|
entityType: 'User',
|
|
|
|
|
entityId: input.id,
|
|
|
|
|
detailsJson: { email: target.email },
|
|
|
|
|
ipAddress: ctx.ip,
|
|
|
|
|
userAgent: ctx.userAgent,
|
2026-01-30 13:41:32 +01:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return user
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Bulk import users (admin only)
|
2026-02-04 14:15:06 +01:00
|
|
|
* Optionally pre-assign projects to jury members during invitation
|
2026-01-30 13:41:32 +01:00
|
|
|
*/
|
|
|
|
|
bulkCreate: adminProcedure
|
|
|
|
|
.input(
|
|
|
|
|
z.object({
|
|
|
|
|
users: z.array(
|
|
|
|
|
z.object({
|
|
|
|
|
email: z.string().email(),
|
|
|
|
|
name: z.string().optional(),
|
Fix pipeline config crashes, settings UX, invite roles, seed expertise tags
- Fix critical crash when clicking Edit on INTAKE stage configs: normalize
DB fileRequirements shape (type/required → acceptedMimeTypes/isRequired),
add null guard in getActiveCategoriesFromMimeTypes
- Fix config summary display for all stage types to handle seed data key
mismatches (votingEnabled→juryVotingEnabled, minAssignmentsPerJuror→
minLoadPerJuror, deterministic.rules→rules, etc.)
- Add AWARD_MASTER role to invite page dropdown and user router validations
- Restructure settings sidebar: Tags and Webhooks as direct links instead
of nested tabs, remove redundant Quick Links section
- Seed 38 expertise tags across 7 categories (Marine Science, Technology,
Policy, Conservation, Business, Education, Engineering)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 11:40:44 +01:00
|
|
|
role: z.enum(['SUPER_ADMIN', 'PROGRAM_ADMIN', 'AWARD_MASTER', 'JURY_MEMBER', 'MENTOR', 'OBSERVER']).default('JURY_MEMBER'),
|
2026-01-30 13:41:32 +01:00
|
|
|
expertiseTags: z.array(z.string()).optional(),
|
2026-02-04 14:15:06 +01:00
|
|
|
// Optional pre-assignments for jury members
|
|
|
|
|
assignments: z
|
|
|
|
|
.array(
|
|
|
|
|
z.object({
|
|
|
|
|
projectId: z.string(),
|
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
|
|
|
roundId: z.string(),
|
|
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
.optional(),
|
|
|
|
|
// Competition architecture: optional jury group memberships
|
|
|
|
|
juryGroupIds: z.array(z.string()).optional(),
|
|
|
|
|
juryGroupRole: z.enum(['CHAIR', 'MEMBER', 'OBSERVER']).default('MEMBER'),
|
|
|
|
|
// Competition architecture: optional assignment intents
|
|
|
|
|
assignmentIntents: z
|
|
|
|
|
.array(
|
|
|
|
|
z.object({
|
|
|
|
|
roundId: z.string(),
|
|
|
|
|
projectId: z.string(),
|
2026-02-04 14:15:06 +01:00
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
.optional(),
|
2026-01-30 13:41:32 +01:00
|
|
|
})
|
|
|
|
|
),
|
2026-02-11 13:20:52 +01:00
|
|
|
sendInvitation: z.boolean().default(true),
|
2026-01-30 13:41:32 +01:00
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
.mutation(async ({ ctx, input }) => {
|
2026-02-08 23:01:33 +01:00
|
|
|
// Prevent non-super-admins from creating super admins or program admins
|
|
|
|
|
const hasSuperAdminRole = input.users.some((u) => u.role === 'SUPER_ADMIN')
|
|
|
|
|
if (hasSuperAdminRole && ctx.user.role !== 'SUPER_ADMIN') {
|
|
|
|
|
throw new TRPCError({
|
|
|
|
|
code: 'FORBIDDEN',
|
|
|
|
|
message: 'Only super admins can create super admins',
|
|
|
|
|
})
|
|
|
|
|
}
|
Add dynamic apply wizard customization with admin settings UI
- Create wizard config types, utilities, and defaults (wizard-config.ts)
- Add admin apply settings page with drag-and-drop step ordering, dropdown
option management, feature toggles, welcome message customization, and
custom field builder with select/multiselect options editor
- Build dynamic apply wizard component with animated step transitions,
mobile-first responsive design, and config-driven form validation
- Update step components to accept dynamic config (categories, ocean issues,
field visibility, feature flags)
- Replace hardcoded enum validation with string-based validation for
admin-configurable dropdown values, with safe enum casting at storage layer
- Add wizard template system (model, router, admin UI) with built-in
MOPC Classic preset
- Add program wizard config CRUD procedures to program router
- Update application router getConfig to return wizardConfig, submit handler
to store custom field data in metadataJson
- Add edition-based apply page, project pool page, and supporting routers
- Fix CSS (invalid sm:fixed-none), Enter key handler (skip textarea),
safe area insets for notched phones, buildStepsArray field visibility
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 13:18:20 +01:00
|
|
|
const hasAdminRole = input.users.some((u) => u.role === 'PROGRAM_ADMIN')
|
|
|
|
|
if (hasAdminRole && ctx.user.role !== 'SUPER_ADMIN') {
|
|
|
|
|
throw new TRPCError({
|
|
|
|
|
code: 'FORBIDDEN',
|
|
|
|
|
message: 'Only super admins can create program admins',
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-30 13:41:32 +01:00
|
|
|
// Deduplicate input by email (keep first occurrence)
|
|
|
|
|
const seenEmails = new Set<string>()
|
|
|
|
|
const uniqueUsers = input.users.filter((u) => {
|
|
|
|
|
const email = u.email.toLowerCase()
|
|
|
|
|
if (seenEmails.has(email)) return false
|
|
|
|
|
seenEmails.add(email)
|
|
|
|
|
return true
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Get existing emails from database
|
|
|
|
|
const existingUsers = await ctx.prisma.user.findMany({
|
|
|
|
|
where: { email: { in: uniqueUsers.map((u) => u.email.toLowerCase()) } },
|
|
|
|
|
select: { email: true },
|
|
|
|
|
})
|
|
|
|
|
const existingEmails = new Set(existingUsers.map((u) => u.email.toLowerCase()))
|
|
|
|
|
|
|
|
|
|
// Filter out existing users
|
|
|
|
|
const newUsers = uniqueUsers.filter((u) => !existingEmails.has(u.email.toLowerCase()))
|
|
|
|
|
|
|
|
|
|
const duplicatesInInput = input.users.length - uniqueUsers.length
|
|
|
|
|
const skipped = existingEmails.size + duplicatesInInput
|
|
|
|
|
|
|
|
|
|
if (newUsers.length === 0) {
|
|
|
|
|
return { created: 0, skipped }
|
|
|
|
|
}
|
|
|
|
|
|
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
|
|
|
const emailToAssignments = new Map<string, Array<{ projectId: string; roundId: string }>>()
|
|
|
|
|
const emailToJuryGroupIds = new Map<string, { ids: string[]; role: 'CHAIR' | 'MEMBER' | 'OBSERVER' }>()
|
|
|
|
|
const emailToIntents = new Map<string, Array<{ roundId: string; projectId: string }>>()
|
2026-02-04 14:15:06 +01:00
|
|
|
for (const u of newUsers) {
|
|
|
|
|
if (u.assignments && u.assignments.length > 0) {
|
|
|
|
|
emailToAssignments.set(u.email.toLowerCase(), u.assignments)
|
|
|
|
|
}
|
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
|
|
|
if (u.juryGroupIds && u.juryGroupIds.length > 0) {
|
|
|
|
|
emailToJuryGroupIds.set(u.email.toLowerCase(), { ids: u.juryGroupIds, role: u.juryGroupRole })
|
|
|
|
|
}
|
|
|
|
|
if (u.assignmentIntents && u.assignmentIntents.length > 0) {
|
|
|
|
|
emailToIntents.set(u.email.toLowerCase(), u.assignmentIntents)
|
|
|
|
|
}
|
2026-02-04 14:15:06 +01:00
|
|
|
}
|
|
|
|
|
|
2026-01-30 13:41:32 +01:00
|
|
|
const created = await ctx.prisma.user.createMany({
|
|
|
|
|
data: newUsers.map((u) => ({
|
|
|
|
|
email: u.email.toLowerCase(),
|
2026-02-04 14:15:06 +01:00
|
|
|
name: u.name,
|
|
|
|
|
role: u.role,
|
|
|
|
|
expertiseTags: u.expertiseTags,
|
2026-02-11 13:20:52 +01:00
|
|
|
status: input.sendInvitation ? 'INVITED' : 'NONE',
|
2026-01-30 13:41:32 +01:00
|
|
|
})),
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Audit log
|
2026-02-05 21:09:06 +01:00
|
|
|
await logAudit({
|
|
|
|
|
prisma: ctx.prisma,
|
|
|
|
|
userId: ctx.user.id,
|
|
|
|
|
action: 'BULK_CREATE',
|
|
|
|
|
entityType: 'User',
|
|
|
|
|
detailsJson: { count: created.count, skipped, duplicatesInInput },
|
|
|
|
|
ipAddress: ctx.ip,
|
|
|
|
|
userAgent: ctx.userAgent,
|
2026-01-30 13:41:32 +01:00
|
|
|
})
|
|
|
|
|
|
2026-02-11 13:20:52 +01:00
|
|
|
// Fetch newly created users for assignments and optional invitation emails
|
2026-01-31 14:13:16 +01:00
|
|
|
const createdUsers = await ctx.prisma.user.findMany({
|
|
|
|
|
where: { email: { in: newUsers.map((u) => u.email.toLowerCase()) } },
|
|
|
|
|
select: { id: true, email: true, name: true, role: true },
|
|
|
|
|
})
|
|
|
|
|
|
Admin platform audit: fix bugs, harden backend, add auto-refresh, clean dead code
Phase 1 — Critical bugs:
- Fix deliberation participant selection (wire jury group query)
- Fix reports "By Round" tab (inline content instead of 404 route)
- Fix messages "Sent History" (add message.sent procedure, wire tab)
- Add missing fields to competition award form (criteriaText, maxRankedPicks)
- Wire LiveControlPanel buttons (cursor, voting, scores)
- Fix ResultLockControls empty snapshot (fetch actual data before lock)
- Fix SubmissionWindowManager losing fields on edit
Phase 2 — Backend fixes:
- Remove write-in-query from specialAward.get
- Fix award eligibility job overwriting manual shortlist overrides
- Fix filtering startJob deleting all prior results (defer cleanup to post-success)
- Tighten access control: protectedProcedure → adminProcedure on 8 procedures
- Add audit logging to deliberation mutations
- Add FINALIST/SEMIFINALIST delete guard on project.delete/bulkDelete
Phase 3 — Auto-refresh:
- Add refetchInterval to 15+ admin pages/components (10s–30s)
- Fix AI job polling: derive speed from job status for all viewers
Phase 4 — Dead code cleanup:
- Delete unused command-palette, pdf-report, admin-page-transition
- Remove dead subItems sidebar code, unused GripVertical import
- Replace redundant isGenerating state with mutation.isPending
- Add Role column to jury members table
- Remove misleading manual mentor assignment stub
Phase 5 — UX improvements:
- Fix rounds page single-competition assumption (add selector)
- Remove raw UUID fallback in deliberation config
- Fix programs page "Stage" → "Round" terminology
Phase 6 — Backend hardening:
- Complete logAudit calls (add prisma, ipAddress, userAgent)
- Batch analytics queries (fix N+1 in getCrossRoundComparison, getYearOverYear)
- Batch user.bulkCreate writes (assignments, jury memberships, intents)
- Remove any casts from deliberation service (typed PrismaClient + TransactionClient)
- Fix stale DeliberationStatus enum values blocking build
40 files changed, 1010 insertions(+), 612 deletions(-)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 08:20:13 +01:00
|
|
|
// Create pre-assignments for users who have them (batched)
|
|
|
|
|
const assignmentData: Array<{ userId: string; projectId: string; roundId: string; method: 'MANUAL'; createdBy: string }> = []
|
2026-02-04 14:15:06 +01:00
|
|
|
for (const user of createdUsers) {
|
|
|
|
|
const assignments = emailToAssignments.get(user.email.toLowerCase())
|
|
|
|
|
if (assignments && assignments.length > 0) {
|
|
|
|
|
for (const assignment of assignments) {
|
Admin platform audit: fix bugs, harden backend, add auto-refresh, clean dead code
Phase 1 — Critical bugs:
- Fix deliberation participant selection (wire jury group query)
- Fix reports "By Round" tab (inline content instead of 404 route)
- Fix messages "Sent History" (add message.sent procedure, wire tab)
- Add missing fields to competition award form (criteriaText, maxRankedPicks)
- Wire LiveControlPanel buttons (cursor, voting, scores)
- Fix ResultLockControls empty snapshot (fetch actual data before lock)
- Fix SubmissionWindowManager losing fields on edit
Phase 2 — Backend fixes:
- Remove write-in-query from specialAward.get
- Fix award eligibility job overwriting manual shortlist overrides
- Fix filtering startJob deleting all prior results (defer cleanup to post-success)
- Tighten access control: protectedProcedure → adminProcedure on 8 procedures
- Add audit logging to deliberation mutations
- Add FINALIST/SEMIFINALIST delete guard on project.delete/bulkDelete
Phase 3 — Auto-refresh:
- Add refetchInterval to 15+ admin pages/components (10s–30s)
- Fix AI job polling: derive speed from job status for all viewers
Phase 4 — Dead code cleanup:
- Delete unused command-palette, pdf-report, admin-page-transition
- Remove dead subItems sidebar code, unused GripVertical import
- Replace redundant isGenerating state with mutation.isPending
- Add Role column to jury members table
- Remove misleading manual mentor assignment stub
Phase 5 — UX improvements:
- Fix rounds page single-competition assumption (add selector)
- Remove raw UUID fallback in deliberation config
- Fix programs page "Stage" → "Round" terminology
Phase 6 — Backend hardening:
- Complete logAudit calls (add prisma, ipAddress, userAgent)
- Batch analytics queries (fix N+1 in getCrossRoundComparison, getYearOverYear)
- Batch user.bulkCreate writes (assignments, jury memberships, intents)
- Remove any casts from deliberation service (typed PrismaClient + TransactionClient)
- Fix stale DeliberationStatus enum values blocking build
40 files changed, 1010 insertions(+), 612 deletions(-)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 08:20:13 +01:00
|
|
|
assignmentData.push({
|
|
|
|
|
userId: user.id,
|
|
|
|
|
projectId: assignment.projectId,
|
|
|
|
|
roundId: assignment.roundId,
|
|
|
|
|
method: 'MANUAL',
|
|
|
|
|
createdBy: ctx.user.id,
|
|
|
|
|
})
|
2026-02-04 14:15:06 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
Admin platform audit: fix bugs, harden backend, add auto-refresh, clean dead code
Phase 1 — Critical bugs:
- Fix deliberation participant selection (wire jury group query)
- Fix reports "By Round" tab (inline content instead of 404 route)
- Fix messages "Sent History" (add message.sent procedure, wire tab)
- Add missing fields to competition award form (criteriaText, maxRankedPicks)
- Wire LiveControlPanel buttons (cursor, voting, scores)
- Fix ResultLockControls empty snapshot (fetch actual data before lock)
- Fix SubmissionWindowManager losing fields on edit
Phase 2 — Backend fixes:
- Remove write-in-query from specialAward.get
- Fix award eligibility job overwriting manual shortlist overrides
- Fix filtering startJob deleting all prior results (defer cleanup to post-success)
- Tighten access control: protectedProcedure → adminProcedure on 8 procedures
- Add audit logging to deliberation mutations
- Add FINALIST/SEMIFINALIST delete guard on project.delete/bulkDelete
Phase 3 — Auto-refresh:
- Add refetchInterval to 15+ admin pages/components (10s–30s)
- Fix AI job polling: derive speed from job status for all viewers
Phase 4 — Dead code cleanup:
- Delete unused command-palette, pdf-report, admin-page-transition
- Remove dead subItems sidebar code, unused GripVertical import
- Replace redundant isGenerating state with mutation.isPending
- Add Role column to jury members table
- Remove misleading manual mentor assignment stub
Phase 5 — UX improvements:
- Fix rounds page single-competition assumption (add selector)
- Remove raw UUID fallback in deliberation config
- Fix programs page "Stage" → "Round" terminology
Phase 6 — Backend hardening:
- Complete logAudit calls (add prisma, ipAddress, userAgent)
- Batch analytics queries (fix N+1 in getCrossRoundComparison, getYearOverYear)
- Batch user.bulkCreate writes (assignments, jury memberships, intents)
- Remove any casts from deliberation service (typed PrismaClient + TransactionClient)
- Fix stale DeliberationStatus enum values blocking build
40 files changed, 1010 insertions(+), 612 deletions(-)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 08:20:13 +01:00
|
|
|
let assignmentsCreated = 0
|
|
|
|
|
if (assignmentData.length > 0) {
|
|
|
|
|
const result = await ctx.prisma.assignment.createMany({
|
|
|
|
|
data: assignmentData,
|
|
|
|
|
skipDuplicates: true,
|
|
|
|
|
})
|
|
|
|
|
assignmentsCreated = result.count
|
|
|
|
|
}
|
2026-02-04 14:15:06 +01:00
|
|
|
|
|
|
|
|
// Audit log for assignments if any were created
|
|
|
|
|
if (assignmentsCreated > 0) {
|
2026-02-05 21:09:06 +01:00
|
|
|
await logAudit({
|
|
|
|
|
prisma: ctx.prisma,
|
|
|
|
|
userId: ctx.user.id,
|
|
|
|
|
action: 'BULK_ASSIGN',
|
|
|
|
|
entityType: 'Assignment',
|
|
|
|
|
detailsJson: { count: assignmentsCreated, context: 'invitation_pre_assignment' },
|
|
|
|
|
ipAddress: ctx.ip,
|
|
|
|
|
userAgent: ctx.userAgent,
|
2026-02-04 14:15:06 +01:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
Admin platform audit: fix bugs, harden backend, add auto-refresh, clean dead code
Phase 1 — Critical bugs:
- Fix deliberation participant selection (wire jury group query)
- Fix reports "By Round" tab (inline content instead of 404 route)
- Fix messages "Sent History" (add message.sent procedure, wire tab)
- Add missing fields to competition award form (criteriaText, maxRankedPicks)
- Wire LiveControlPanel buttons (cursor, voting, scores)
- Fix ResultLockControls empty snapshot (fetch actual data before lock)
- Fix SubmissionWindowManager losing fields on edit
Phase 2 — Backend fixes:
- Remove write-in-query from specialAward.get
- Fix award eligibility job overwriting manual shortlist overrides
- Fix filtering startJob deleting all prior results (defer cleanup to post-success)
- Tighten access control: protectedProcedure → adminProcedure on 8 procedures
- Add audit logging to deliberation mutations
- Add FINALIST/SEMIFINALIST delete guard on project.delete/bulkDelete
Phase 3 — Auto-refresh:
- Add refetchInterval to 15+ admin pages/components (10s–30s)
- Fix AI job polling: derive speed from job status for all viewers
Phase 4 — Dead code cleanup:
- Delete unused command-palette, pdf-report, admin-page-transition
- Remove dead subItems sidebar code, unused GripVertical import
- Replace redundant isGenerating state with mutation.isPending
- Add Role column to jury members table
- Remove misleading manual mentor assignment stub
Phase 5 — UX improvements:
- Fix rounds page single-competition assumption (add selector)
- Remove raw UUID fallback in deliberation config
- Fix programs page "Stage" → "Round" terminology
Phase 6 — Backend hardening:
- Complete logAudit calls (add prisma, ipAddress, userAgent)
- Batch analytics queries (fix N+1 in getCrossRoundComparison, getYearOverYear)
- Batch user.bulkCreate writes (assignments, jury memberships, intents)
- Remove any casts from deliberation service (typed PrismaClient + TransactionClient)
- Fix stale DeliberationStatus enum values blocking build
40 files changed, 1010 insertions(+), 612 deletions(-)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 08:20:13 +01:00
|
|
|
// Create JuryGroupMember records for users with juryGroupIds (batched)
|
|
|
|
|
const juryGroupMemberData: Array<{ juryGroupId: string; userId: string; role: 'CHAIR' | 'MEMBER' | 'OBSERVER' }> = []
|
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
|
|
|
for (const user of createdUsers) {
|
|
|
|
|
const groupInfo = emailToJuryGroupIds.get(user.email.toLowerCase())
|
|
|
|
|
if (groupInfo) {
|
|
|
|
|
for (const groupId of groupInfo.ids) {
|
Admin platform audit: fix bugs, harden backend, add auto-refresh, clean dead code
Phase 1 — Critical bugs:
- Fix deliberation participant selection (wire jury group query)
- Fix reports "By Round" tab (inline content instead of 404 route)
- Fix messages "Sent History" (add message.sent procedure, wire tab)
- Add missing fields to competition award form (criteriaText, maxRankedPicks)
- Wire LiveControlPanel buttons (cursor, voting, scores)
- Fix ResultLockControls empty snapshot (fetch actual data before lock)
- Fix SubmissionWindowManager losing fields on edit
Phase 2 — Backend fixes:
- Remove write-in-query from specialAward.get
- Fix award eligibility job overwriting manual shortlist overrides
- Fix filtering startJob deleting all prior results (defer cleanup to post-success)
- Tighten access control: protectedProcedure → adminProcedure on 8 procedures
- Add audit logging to deliberation mutations
- Add FINALIST/SEMIFINALIST delete guard on project.delete/bulkDelete
Phase 3 — Auto-refresh:
- Add refetchInterval to 15+ admin pages/components (10s–30s)
- Fix AI job polling: derive speed from job status for all viewers
Phase 4 — Dead code cleanup:
- Delete unused command-palette, pdf-report, admin-page-transition
- Remove dead subItems sidebar code, unused GripVertical import
- Replace redundant isGenerating state with mutation.isPending
- Add Role column to jury members table
- Remove misleading manual mentor assignment stub
Phase 5 — UX improvements:
- Fix rounds page single-competition assumption (add selector)
- Remove raw UUID fallback in deliberation config
- Fix programs page "Stage" → "Round" terminology
Phase 6 — Backend hardening:
- Complete logAudit calls (add prisma, ipAddress, userAgent)
- Batch analytics queries (fix N+1 in getCrossRoundComparison, getYearOverYear)
- Batch user.bulkCreate writes (assignments, jury memberships, intents)
- Remove any casts from deliberation service (typed PrismaClient + TransactionClient)
- Fix stale DeliberationStatus enum values blocking build
40 files changed, 1010 insertions(+), 612 deletions(-)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 08:20:13 +01:00
|
|
|
juryGroupMemberData.push({
|
|
|
|
|
juryGroupId: groupId,
|
|
|
|
|
userId: user.id,
|
|
|
|
|
role: groupInfo.role,
|
|
|
|
|
})
|
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
|
|
|
}
|
|
|
|
|
}
|
Admin platform audit: fix bugs, harden backend, add auto-refresh, clean dead code
Phase 1 — Critical bugs:
- Fix deliberation participant selection (wire jury group query)
- Fix reports "By Round" tab (inline content instead of 404 route)
- Fix messages "Sent History" (add message.sent procedure, wire tab)
- Add missing fields to competition award form (criteriaText, maxRankedPicks)
- Wire LiveControlPanel buttons (cursor, voting, scores)
- Fix ResultLockControls empty snapshot (fetch actual data before lock)
- Fix SubmissionWindowManager losing fields on edit
Phase 2 — Backend fixes:
- Remove write-in-query from specialAward.get
- Fix award eligibility job overwriting manual shortlist overrides
- Fix filtering startJob deleting all prior results (defer cleanup to post-success)
- Tighten access control: protectedProcedure → adminProcedure on 8 procedures
- Add audit logging to deliberation mutations
- Add FINALIST/SEMIFINALIST delete guard on project.delete/bulkDelete
Phase 3 — Auto-refresh:
- Add refetchInterval to 15+ admin pages/components (10s–30s)
- Fix AI job polling: derive speed from job status for all viewers
Phase 4 — Dead code cleanup:
- Delete unused command-palette, pdf-report, admin-page-transition
- Remove dead subItems sidebar code, unused GripVertical import
- Replace redundant isGenerating state with mutation.isPending
- Add Role column to jury members table
- Remove misleading manual mentor assignment stub
Phase 5 — UX improvements:
- Fix rounds page single-competition assumption (add selector)
- Remove raw UUID fallback in deliberation config
- Fix programs page "Stage" → "Round" terminology
Phase 6 — Backend hardening:
- Complete logAudit calls (add prisma, ipAddress, userAgent)
- Batch analytics queries (fix N+1 in getCrossRoundComparison, getYearOverYear)
- Batch user.bulkCreate writes (assignments, jury memberships, intents)
- Remove any casts from deliberation service (typed PrismaClient + TransactionClient)
- Fix stale DeliberationStatus enum values blocking build
40 files changed, 1010 insertions(+), 612 deletions(-)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 08:20:13 +01:00
|
|
|
}
|
|
|
|
|
let juryGroupMembershipsCreated = 0
|
|
|
|
|
if (juryGroupMemberData.length > 0) {
|
|
|
|
|
const result = await ctx.prisma.juryGroupMember.createMany({
|
|
|
|
|
data: juryGroupMemberData,
|
|
|
|
|
skipDuplicates: true,
|
|
|
|
|
})
|
|
|
|
|
juryGroupMembershipsCreated = result.count
|
|
|
|
|
}
|
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
|
|
|
|
Admin platform audit: fix bugs, harden backend, add auto-refresh, clean dead code
Phase 1 — Critical bugs:
- Fix deliberation participant selection (wire jury group query)
- Fix reports "By Round" tab (inline content instead of 404 route)
- Fix messages "Sent History" (add message.sent procedure, wire tab)
- Add missing fields to competition award form (criteriaText, maxRankedPicks)
- Wire LiveControlPanel buttons (cursor, voting, scores)
- Fix ResultLockControls empty snapshot (fetch actual data before lock)
- Fix SubmissionWindowManager losing fields on edit
Phase 2 — Backend fixes:
- Remove write-in-query from specialAward.get
- Fix award eligibility job overwriting manual shortlist overrides
- Fix filtering startJob deleting all prior results (defer cleanup to post-success)
- Tighten access control: protectedProcedure → adminProcedure on 8 procedures
- Add audit logging to deliberation mutations
- Add FINALIST/SEMIFINALIST delete guard on project.delete/bulkDelete
Phase 3 — Auto-refresh:
- Add refetchInterval to 15+ admin pages/components (10s–30s)
- Fix AI job polling: derive speed from job status for all viewers
Phase 4 — Dead code cleanup:
- Delete unused command-palette, pdf-report, admin-page-transition
- Remove dead subItems sidebar code, unused GripVertical import
- Replace redundant isGenerating state with mutation.isPending
- Add Role column to jury members table
- Remove misleading manual mentor assignment stub
Phase 5 — UX improvements:
- Fix rounds page single-competition assumption (add selector)
- Remove raw UUID fallback in deliberation config
- Fix programs page "Stage" → "Round" terminology
Phase 6 — Backend hardening:
- Complete logAudit calls (add prisma, ipAddress, userAgent)
- Batch analytics queries (fix N+1 in getCrossRoundComparison, getYearOverYear)
- Batch user.bulkCreate writes (assignments, jury memberships, intents)
- Remove any casts from deliberation service (typed PrismaClient + TransactionClient)
- Fix stale DeliberationStatus enum values blocking build
40 files changed, 1010 insertions(+), 612 deletions(-)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 08:20:13 +01:00
|
|
|
// Create AssignmentIntents for users who have them
|
|
|
|
|
let assignmentIntentsCreated = 0
|
|
|
|
|
const allIntentUsers = createdUsers.filter(
|
|
|
|
|
(u) => emailToIntents.has(u.email.toLowerCase())
|
|
|
|
|
)
|
|
|
|
|
if (allIntentUsers.length > 0) {
|
|
|
|
|
// Batch-fetch all relevant rounds to avoid N+1 lookups
|
|
|
|
|
const allIntentRoundIds = new Set<string>()
|
|
|
|
|
for (const u of allIntentUsers) {
|
|
|
|
|
for (const intent of emailToIntents.get(u.email.toLowerCase())!) {
|
|
|
|
|
allIntentRoundIds.add(intent.roundId)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const rounds = await ctx.prisma.round.findMany({
|
|
|
|
|
where: { id: { in: [...allIntentRoundIds] } },
|
|
|
|
|
select: { id: true, juryGroupId: true },
|
|
|
|
|
})
|
|
|
|
|
const roundJuryGroupMap = new Map(rounds.map((r) => [r.id, r.juryGroupId]))
|
|
|
|
|
|
|
|
|
|
// Batch-fetch all matching JuryGroupMembers
|
|
|
|
|
const memberLookups = allIntentUsers.flatMap((u) => {
|
|
|
|
|
const intents = emailToIntents.get(u.email.toLowerCase())!
|
|
|
|
|
return intents
|
|
|
|
|
.map((intent) => {
|
|
|
|
|
const juryGroupId = roundJuryGroupMap.get(intent.roundId)
|
|
|
|
|
return juryGroupId ? { juryGroupId, userId: u.id } : null
|
|
|
|
|
})
|
|
|
|
|
.filter((x): x is { juryGroupId: string; userId: string } => x !== null)
|
|
|
|
|
})
|
|
|
|
|
const members = memberLookups.length > 0
|
|
|
|
|
? await ctx.prisma.juryGroupMember.findMany({
|
|
|
|
|
where: {
|
|
|
|
|
OR: memberLookups.map((l) => ({
|
|
|
|
|
juryGroupId: l.juryGroupId,
|
|
|
|
|
userId: l.userId,
|
|
|
|
|
})),
|
|
|
|
|
},
|
|
|
|
|
select: { id: true, juryGroupId: true, userId: true },
|
|
|
|
|
})
|
|
|
|
|
: []
|
|
|
|
|
const memberMap = new Map(
|
|
|
|
|
members.map((m) => [`${m.juryGroupId}:${m.userId}`, m.id])
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// Batch-create all intents
|
|
|
|
|
const intentData: Array<{
|
|
|
|
|
juryGroupMemberId: string
|
|
|
|
|
roundId: string
|
|
|
|
|
projectId: string
|
|
|
|
|
source: 'INVITE'
|
|
|
|
|
status: 'INTENT_PENDING'
|
|
|
|
|
}> = []
|
|
|
|
|
for (const user of allIntentUsers) {
|
|
|
|
|
const intents = emailToIntents.get(user.email.toLowerCase())!
|
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
|
|
|
for (const intent of intents) {
|
Admin platform audit: fix bugs, harden backend, add auto-refresh, clean dead code
Phase 1 — Critical bugs:
- Fix deliberation participant selection (wire jury group query)
- Fix reports "By Round" tab (inline content instead of 404 route)
- Fix messages "Sent History" (add message.sent procedure, wire tab)
- Add missing fields to competition award form (criteriaText, maxRankedPicks)
- Wire LiveControlPanel buttons (cursor, voting, scores)
- Fix ResultLockControls empty snapshot (fetch actual data before lock)
- Fix SubmissionWindowManager losing fields on edit
Phase 2 — Backend fixes:
- Remove write-in-query from specialAward.get
- Fix award eligibility job overwriting manual shortlist overrides
- Fix filtering startJob deleting all prior results (defer cleanup to post-success)
- Tighten access control: protectedProcedure → adminProcedure on 8 procedures
- Add audit logging to deliberation mutations
- Add FINALIST/SEMIFINALIST delete guard on project.delete/bulkDelete
Phase 3 — Auto-refresh:
- Add refetchInterval to 15+ admin pages/components (10s–30s)
- Fix AI job polling: derive speed from job status for all viewers
Phase 4 — Dead code cleanup:
- Delete unused command-palette, pdf-report, admin-page-transition
- Remove dead subItems sidebar code, unused GripVertical import
- Replace redundant isGenerating state with mutation.isPending
- Add Role column to jury members table
- Remove misleading manual mentor assignment stub
Phase 5 — UX improvements:
- Fix rounds page single-competition assumption (add selector)
- Remove raw UUID fallback in deliberation config
- Fix programs page "Stage" → "Round" terminology
Phase 6 — Backend hardening:
- Complete logAudit calls (add prisma, ipAddress, userAgent)
- Batch analytics queries (fix N+1 in getCrossRoundComparison, getYearOverYear)
- Batch user.bulkCreate writes (assignments, jury memberships, intents)
- Remove any casts from deliberation service (typed PrismaClient + TransactionClient)
- Fix stale DeliberationStatus enum values blocking build
40 files changed, 1010 insertions(+), 612 deletions(-)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 08:20:13 +01:00
|
|
|
const juryGroupId = roundJuryGroupMap.get(intent.roundId)
|
|
|
|
|
if (!juryGroupId) continue
|
|
|
|
|
const memberId = memberMap.get(`${juryGroupId}:${user.id}`)
|
|
|
|
|
if (!memberId) continue
|
|
|
|
|
intentData.push({
|
|
|
|
|
juryGroupMemberId: memberId,
|
|
|
|
|
roundId: intent.roundId,
|
|
|
|
|
projectId: intent.projectId,
|
|
|
|
|
source: 'INVITE',
|
|
|
|
|
status: 'INTENT_PENDING',
|
|
|
|
|
})
|
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
|
|
|
}
|
|
|
|
|
}
|
Admin platform audit: fix bugs, harden backend, add auto-refresh, clean dead code
Phase 1 — Critical bugs:
- Fix deliberation participant selection (wire jury group query)
- Fix reports "By Round" tab (inline content instead of 404 route)
- Fix messages "Sent History" (add message.sent procedure, wire tab)
- Add missing fields to competition award form (criteriaText, maxRankedPicks)
- Wire LiveControlPanel buttons (cursor, voting, scores)
- Fix ResultLockControls empty snapshot (fetch actual data before lock)
- Fix SubmissionWindowManager losing fields on edit
Phase 2 — Backend fixes:
- Remove write-in-query from specialAward.get
- Fix award eligibility job overwriting manual shortlist overrides
- Fix filtering startJob deleting all prior results (defer cleanup to post-success)
- Tighten access control: protectedProcedure → adminProcedure on 8 procedures
- Add audit logging to deliberation mutations
- Add FINALIST/SEMIFINALIST delete guard on project.delete/bulkDelete
Phase 3 — Auto-refresh:
- Add refetchInterval to 15+ admin pages/components (10s–30s)
- Fix AI job polling: derive speed from job status for all viewers
Phase 4 — Dead code cleanup:
- Delete unused command-palette, pdf-report, admin-page-transition
- Remove dead subItems sidebar code, unused GripVertical import
- Replace redundant isGenerating state with mutation.isPending
- Add Role column to jury members table
- Remove misleading manual mentor assignment stub
Phase 5 — UX improvements:
- Fix rounds page single-competition assumption (add selector)
- Remove raw UUID fallback in deliberation config
- Fix programs page "Stage" → "Round" terminology
Phase 6 — Backend hardening:
- Complete logAudit calls (add prisma, ipAddress, userAgent)
- Batch analytics queries (fix N+1 in getCrossRoundComparison, getYearOverYear)
- Batch user.bulkCreate writes (assignments, jury memberships, intents)
- Remove any casts from deliberation service (typed PrismaClient + TransactionClient)
- Fix stale DeliberationStatus enum values blocking build
40 files changed, 1010 insertions(+), 612 deletions(-)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 08:20:13 +01:00
|
|
|
if (intentData.length > 0) {
|
|
|
|
|
const result = await ctx.prisma.assignmentIntent.createMany({
|
|
|
|
|
data: intentData,
|
|
|
|
|
skipDuplicates: true,
|
|
|
|
|
})
|
|
|
|
|
assignmentIntentsCreated = result.count
|
|
|
|
|
}
|
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (juryGroupMembershipsCreated > 0) {
|
|
|
|
|
await logAudit({
|
|
|
|
|
prisma: ctx.prisma,
|
|
|
|
|
userId: ctx.user.id,
|
|
|
|
|
action: 'BULK_CREATE',
|
|
|
|
|
entityType: 'JuryGroupMember',
|
|
|
|
|
detailsJson: { count: juryGroupMembershipsCreated, context: 'invitation_jury_group_binding' },
|
|
|
|
|
ipAddress: ctx.ip,
|
|
|
|
|
userAgent: ctx.userAgent,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-11 13:20:52 +01:00
|
|
|
// Send invitation emails if requested
|
2026-01-31 14:13:16 +01:00
|
|
|
let emailsSent = 0
|
|
|
|
|
const emailErrors: string[] = []
|
|
|
|
|
|
2026-02-11 13:20:52 +01:00
|
|
|
if (input.sendInvitation) {
|
2026-02-23 14:27:58 +01:00
|
|
|
const baseUrl = process.env.NEXTAUTH_URL || 'https://portal.monaco-opc.com'
|
2026-02-17 22:05:58 +01:00
|
|
|
const expiryHours = await getInviteExpiryHours(ctx.prisma)
|
|
|
|
|
const expiryMs = expiryHours * 60 * 60 * 1000
|
2026-02-11 13:20:52 +01:00
|
|
|
|
|
|
|
|
for (const user of createdUsers) {
|
|
|
|
|
try {
|
|
|
|
|
const token = generateInviteToken()
|
|
|
|
|
await ctx.prisma.user.update({
|
|
|
|
|
where: { id: user.id },
|
|
|
|
|
data: {
|
|
|
|
|
inviteToken: token,
|
2026-02-17 22:05:58 +01:00
|
|
|
inviteTokenExpiresAt: new Date(Date.now() + expiryMs),
|
2026-02-11 13:20:52 +01:00
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const inviteUrl = `${baseUrl}/accept-invite?token=${token}`
|
2026-02-17 22:05:58 +01:00
|
|
|
await sendInvitationEmail(user.email, user.name, inviteUrl, user.role, expiryHours)
|
2026-02-11 13:20:52 +01:00
|
|
|
|
|
|
|
|
await ctx.prisma.notificationLog.create({
|
|
|
|
|
data: {
|
|
|
|
|
userId: user.id,
|
|
|
|
|
channel: 'EMAIL',
|
|
|
|
|
provider: 'SMTP',
|
|
|
|
|
type: 'JURY_INVITATION',
|
|
|
|
|
status: 'SENT',
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
emailsSent++
|
|
|
|
|
} catch (e) {
|
|
|
|
|
emailErrors.push(user.email)
|
|
|
|
|
await ctx.prisma.notificationLog.create({
|
|
|
|
|
data: {
|
|
|
|
|
userId: user.id,
|
|
|
|
|
channel: 'EMAIL',
|
|
|
|
|
provider: 'SMTP',
|
|
|
|
|
type: 'JURY_INVITATION',
|
|
|
|
|
status: 'FAILED',
|
|
|
|
|
errorMsg: e instanceof Error ? e.message : 'Unknown error',
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
}
|
2026-01-31 14:13:16 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
|
|
|
return { created: created.count, skipped, emailsSent, emailErrors, assignmentsCreated, juryGroupMembershipsCreated, assignmentIntentsCreated, invitationSent: input.sendInvitation }
|
2026-01-30 13:41:32 +01:00
|
|
|
}),
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get jury members for assignment
|
|
|
|
|
*/
|
|
|
|
|
getJuryMembers: adminProcedure
|
|
|
|
|
.input(
|
|
|
|
|
z.object({
|
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
|
|
|
roundId: z.string().optional(),
|
2026-01-30 13:41:32 +01:00
|
|
|
search: z.string().optional(),
|
|
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
.query(async ({ ctx, input }) => {
|
|
|
|
|
const where: Record<string, unknown> = {
|
2026-02-24 17:44:55 +01:00
|
|
|
roles: { has: 'JURY_MEMBER' },
|
2026-01-30 13:41:32 +01:00
|
|
|
status: 'ACTIVE',
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
email: true,
|
|
|
|
|
name: true,
|
|
|
|
|
expertiseTags: true,
|
|
|
|
|
maxAssignments: true,
|
Implement 15 platform features: digest, availability, templates, comparison, live voting SSE, file versioning, mentorship, messaging, analytics, drafts, webhooks, peer review, audit enhancements, i18n
Features implemented:
- F1: Email digest notifications with cron endpoint and per-user frequency
- F2: Jury availability windows and workload preferences in smart assignment
- F3: Round templates with save-from-round and CRUD management
- F4: Side-by-side project comparison view for jury members
- F5: Real-time voting dashboard with Server-Sent Events (SSE)
- F6: Live voting UX: QR codes, audience voting, tie-breaking, score animations
- F7: File versioning, inline preview, bulk download with presigned URLs
- F8: Mentor dashboard: milestones, private notes, activity tracking
- F9: Communication hub with broadcasts, templates, and recipient targeting
- F10: Advanced analytics: cross-round comparison, juror consistency, diversity metrics, PDF export
- F11: Applicant draft saving with magic link resume and cron cleanup
- F12: Webhook integration layer with HMAC signing, retry, and delivery logs
- F13: Peer review discussions with anonymized scores and threaded comments
- F14: Audit log enhancements: before/after diffs, session grouping, anomaly detection, retention
- F15: i18n foundation with next-intl (EN/FR), cookie-based locale, language switcher
Schema: 12 new models, field additions to User, Project, ProjectFile, LiveVotingSession, LiveVote, MentorAssignment, AuditLog, Program
New routers: roundTemplate, message, webhook (registered in _app.ts)
New services: email-digest, webhook-dispatcher
New cron endpoints: /api/cron/digest, /api/cron/draft-cleanup, /api/cron/audit-cleanup
New API routes: /api/live-voting/stream (SSE), /api/files/bulk-download
All features are admin-configurable via SystemSettings or per-model settingsJson fields.
Docker build verified successfully.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-05 23:31:41 +01:00
|
|
|
availabilityJson: true,
|
|
|
|
|
preferredWorkload: true,
|
2026-02-02 13:19:28 +01:00
|
|
|
profileImageKey: true,
|
|
|
|
|
profileImageProvider: true,
|
2026-01-30 13:41:32 +01:00
|
|
|
_count: {
|
|
|
|
|
select: {
|
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
|
|
|
assignments: input.roundId
|
|
|
|
|
? { where: { roundId: input.roundId } }
|
2026-01-30 13:41:32 +01:00
|
|
|
: true,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
orderBy: { name: 'asc' },
|
|
|
|
|
})
|
|
|
|
|
|
2026-02-02 13:19:28 +01:00
|
|
|
const mapped = users.map((u) => ({
|
2026-01-30 13:41:32 +01:00
|
|
|
...u,
|
|
|
|
|
currentAssignments: u._count.assignments,
|
|
|
|
|
availableSlots:
|
|
|
|
|
u.maxAssignments !== null
|
|
|
|
|
? Math.max(0, u.maxAssignments - u._count.assignments)
|
|
|
|
|
: null,
|
|
|
|
|
}))
|
2026-02-02 13:19:28 +01:00
|
|
|
|
|
|
|
|
return attachAvatarUrls(mapped)
|
2026-01-30 13:41:32 +01:00
|
|
|
}),
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Send invitation email to a user
|
|
|
|
|
*/
|
|
|
|
|
sendInvitation: adminProcedure
|
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
|
|
|
.input(z.object({
|
|
|
|
|
userId: z.string(),
|
|
|
|
|
juryGroupId: z.string().optional(),
|
|
|
|
|
}))
|
2026-01-30 13:41:32 +01:00
|
|
|
.mutation(async ({ ctx, input }) => {
|
|
|
|
|
const user = await ctx.prisma.user.findUniqueOrThrow({
|
|
|
|
|
where: { id: input.userId },
|
|
|
|
|
})
|
|
|
|
|
|
Inline filtering results, select-all across pages, country flags, settings RBAC, and inline role changes
- Round detail: add skeleton loading for filtering stats, inline results table
with expandable rows, pagination, override/reinstate, CSV export, and tooltip
on AI summaries button (removes need for separate results page)
- Projects: add select-all-across-pages with Gmail-style banner, show country
flags with tooltip instead of country codes (table + card views), add listAllIds
backend endpoint
- Settings: allow PROGRAM_ADMIN access to settings page, restrict infrastructure
tabs (AI, Email, Storage, Security, Webhooks) to SUPER_ADMIN only
- Members: add inline role change via dropdown submenu in user actions, enforce
role hierarchy (only super admins can modify admin/super-admin roles) in both
backend and UI
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 23:07:38 +01:00
|
|
|
if (user.status !== 'NONE' && user.status !== 'INVITED') {
|
2026-01-30 13:41:32 +01:00
|
|
|
throw new TRPCError({
|
|
|
|
|
code: 'BAD_REQUEST',
|
|
|
|
|
message: 'User has already accepted their invitation',
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
|
|
|
// Bind to jury group if specified (upsert to be idempotent)
|
|
|
|
|
if (input.juryGroupId) {
|
|
|
|
|
await ctx.prisma.juryGroupMember.upsert({
|
|
|
|
|
where: {
|
|
|
|
|
juryGroupId_userId: {
|
|
|
|
|
juryGroupId: input.juryGroupId,
|
|
|
|
|
userId: user.id,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
create: {
|
|
|
|
|
juryGroupId: input.juryGroupId,
|
|
|
|
|
userId: user.id,
|
|
|
|
|
role: 'MEMBER',
|
|
|
|
|
},
|
|
|
|
|
update: {}, // No-op if already exists
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
Inline filtering results, select-all across pages, country flags, settings RBAC, and inline role changes
- Round detail: add skeleton loading for filtering stats, inline results table
with expandable rows, pagination, override/reinstate, CSV export, and tooltip
on AI summaries button (removes need for separate results page)
- Projects: add select-all-across-pages with Gmail-style banner, show country
flags with tooltip instead of country codes (table + card views), add listAllIds
backend endpoint
- Settings: allow PROGRAM_ADMIN access to settings page, restrict infrastructure
tabs (AI, Email, Storage, Security, Webhooks) to SUPER_ADMIN only
- Members: add inline role change via dropdown submenu in user actions, enforce
role hierarchy (only super admins can modify admin/super-admin roles) in both
backend and UI
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 23:07:38 +01:00
|
|
|
// Generate invite token, set status to INVITED, and store on user
|
2026-01-31 14:13:16 +01:00
|
|
|
const token = generateInviteToken()
|
2026-02-17 22:05:58 +01:00
|
|
|
const expiryHours = await getInviteExpiryHours(ctx.prisma)
|
2026-01-31 14:13:16 +01:00
|
|
|
await ctx.prisma.user.update({
|
|
|
|
|
where: { id: user.id },
|
|
|
|
|
data: {
|
Inline filtering results, select-all across pages, country flags, settings RBAC, and inline role changes
- Round detail: add skeleton loading for filtering stats, inline results table
with expandable rows, pagination, override/reinstate, CSV export, and tooltip
on AI summaries button (removes need for separate results page)
- Projects: add select-all-across-pages with Gmail-style banner, show country
flags with tooltip instead of country codes (table + card views), add listAllIds
backend endpoint
- Settings: allow PROGRAM_ADMIN access to settings page, restrict infrastructure
tabs (AI, Email, Storage, Security, Webhooks) to SUPER_ADMIN only
- Members: add inline role change via dropdown submenu in user actions, enforce
role hierarchy (only super admins can modify admin/super-admin roles) in both
backend and UI
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 23:07:38 +01:00
|
|
|
status: 'INVITED',
|
2026-01-31 14:13:16 +01:00
|
|
|
inviteToken: token,
|
2026-02-17 22:05:58 +01:00
|
|
|
inviteTokenExpiresAt: new Date(Date.now() + expiryHours * 60 * 60 * 1000),
|
2026-01-31 14:13:16 +01:00
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
2026-02-23 14:27:58 +01:00
|
|
|
const baseUrl = process.env.NEXTAUTH_URL || 'https://portal.monaco-opc.com'
|
2026-01-31 14:13:16 +01:00
|
|
|
const inviteUrl = `${baseUrl}/accept-invite?token=${token}`
|
2026-01-30 13:41:32 +01:00
|
|
|
|
|
|
|
|
// Send invitation email
|
2026-02-17 22:05:58 +01:00
|
|
|
await sendInvitationEmail(user.email, user.name, inviteUrl, user.role, expiryHours)
|
2026-01-30 13:41:32 +01:00
|
|
|
|
|
|
|
|
// Log notification
|
|
|
|
|
await ctx.prisma.notificationLog.create({
|
|
|
|
|
data: {
|
|
|
|
|
userId: user.id,
|
|
|
|
|
channel: 'EMAIL',
|
|
|
|
|
provider: 'SMTP',
|
|
|
|
|
type: 'JURY_INVITATION',
|
|
|
|
|
status: 'SENT',
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Audit log
|
2026-02-05 21:09:06 +01:00
|
|
|
await logAudit({
|
|
|
|
|
prisma: ctx.prisma,
|
|
|
|
|
userId: ctx.user.id,
|
|
|
|
|
action: 'SEND_INVITATION',
|
|
|
|
|
entityType: 'User',
|
|
|
|
|
entityId: user.id,
|
|
|
|
|
detailsJson: { email: user.email },
|
|
|
|
|
ipAddress: ctx.ip,
|
|
|
|
|
userAgent: ctx.userAgent,
|
2026-01-30 13:41:32 +01:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return { success: true, email: user.email }
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Send invitation emails to multiple users
|
|
|
|
|
*/
|
|
|
|
|
bulkSendInvitations: adminProcedure
|
|
|
|
|
.input(z.object({ userIds: z.array(z.string()) }))
|
|
|
|
|
.mutation(async ({ ctx, input }) => {
|
|
|
|
|
const users = await ctx.prisma.user.findMany({
|
|
|
|
|
where: {
|
|
|
|
|
id: { in: input.userIds },
|
Inline filtering results, select-all across pages, country flags, settings RBAC, and inline role changes
- Round detail: add skeleton loading for filtering stats, inline results table
with expandable rows, pagination, override/reinstate, CSV export, and tooltip
on AI summaries button (removes need for separate results page)
- Projects: add select-all-across-pages with Gmail-style banner, show country
flags with tooltip instead of country codes (table + card views), add listAllIds
backend endpoint
- Settings: allow PROGRAM_ADMIN access to settings page, restrict infrastructure
tabs (AI, Email, Storage, Security, Webhooks) to SUPER_ADMIN only
- Members: add inline role change via dropdown submenu in user actions, enforce
role hierarchy (only super admins can modify admin/super-admin roles) in both
backend and UI
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 23:07:38 +01:00
|
|
|
status: { in: ['NONE', 'INVITED'] },
|
2026-01-30 13:41:32 +01:00
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if (users.length === 0) {
|
|
|
|
|
return { sent: 0, skipped: input.userIds.length }
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-23 14:27:58 +01:00
|
|
|
const baseUrl = process.env.NEXTAUTH_URL || 'https://portal.monaco-opc.com'
|
2026-02-17 22:05:58 +01:00
|
|
|
const expiryHours = await getInviteExpiryHours(ctx.prisma)
|
|
|
|
|
const expiryMs = expiryHours * 60 * 60 * 1000
|
2026-01-30 13:41:32 +01:00
|
|
|
let sent = 0
|
|
|
|
|
const errors: string[] = []
|
|
|
|
|
|
|
|
|
|
for (const user of users) {
|
|
|
|
|
try {
|
Inline filtering results, select-all across pages, country flags, settings RBAC, and inline role changes
- Round detail: add skeleton loading for filtering stats, inline results table
with expandable rows, pagination, override/reinstate, CSV export, and tooltip
on AI summaries button (removes need for separate results page)
- Projects: add select-all-across-pages with Gmail-style banner, show country
flags with tooltip instead of country codes (table + card views), add listAllIds
backend endpoint
- Settings: allow PROGRAM_ADMIN access to settings page, restrict infrastructure
tabs (AI, Email, Storage, Security, Webhooks) to SUPER_ADMIN only
- Members: add inline role change via dropdown submenu in user actions, enforce
role hierarchy (only super admins can modify admin/super-admin roles) in both
backend and UI
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 23:07:38 +01:00
|
|
|
// Generate invite token for each user and set status to INVITED
|
2026-01-31 14:13:16 +01:00
|
|
|
const token = generateInviteToken()
|
|
|
|
|
await ctx.prisma.user.update({
|
|
|
|
|
where: { id: user.id },
|
|
|
|
|
data: {
|
Inline filtering results, select-all across pages, country flags, settings RBAC, and inline role changes
- Round detail: add skeleton loading for filtering stats, inline results table
with expandable rows, pagination, override/reinstate, CSV export, and tooltip
on AI summaries button (removes need for separate results page)
- Projects: add select-all-across-pages with Gmail-style banner, show country
flags with tooltip instead of country codes (table + card views), add listAllIds
backend endpoint
- Settings: allow PROGRAM_ADMIN access to settings page, restrict infrastructure
tabs (AI, Email, Storage, Security, Webhooks) to SUPER_ADMIN only
- Members: add inline role change via dropdown submenu in user actions, enforce
role hierarchy (only super admins can modify admin/super-admin roles) in both
backend and UI
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 23:07:38 +01:00
|
|
|
status: 'INVITED',
|
2026-01-31 14:13:16 +01:00
|
|
|
inviteToken: token,
|
2026-02-17 22:05:58 +01:00
|
|
|
inviteTokenExpiresAt: new Date(Date.now() + expiryMs),
|
2026-01-31 14:13:16 +01:00
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const inviteUrl = `${baseUrl}/accept-invite?token=${token}`
|
2026-02-17 22:05:58 +01:00
|
|
|
await sendInvitationEmail(user.email, user.name, inviteUrl, user.role, expiryHours)
|
2026-01-30 13:41:32 +01:00
|
|
|
|
|
|
|
|
await ctx.prisma.notificationLog.create({
|
|
|
|
|
data: {
|
|
|
|
|
userId: user.id,
|
|
|
|
|
channel: 'EMAIL',
|
|
|
|
|
provider: 'SMTP',
|
|
|
|
|
type: 'JURY_INVITATION',
|
|
|
|
|
status: 'SENT',
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
sent++
|
|
|
|
|
} catch (e) {
|
|
|
|
|
errors.push(user.email)
|
|
|
|
|
await ctx.prisma.notificationLog.create({
|
|
|
|
|
data: {
|
|
|
|
|
userId: user.id,
|
|
|
|
|
channel: 'EMAIL',
|
|
|
|
|
provider: 'SMTP',
|
|
|
|
|
type: 'JURY_INVITATION',
|
|
|
|
|
status: 'FAILED',
|
|
|
|
|
errorMsg: e instanceof Error ? e.message : 'Unknown error',
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Audit log
|
2026-02-05 21:09:06 +01:00
|
|
|
await logAudit({
|
|
|
|
|
prisma: ctx.prisma,
|
|
|
|
|
userId: ctx.user.id,
|
|
|
|
|
action: 'BULK_SEND_INVITATIONS',
|
|
|
|
|
entityType: 'User',
|
|
|
|
|
detailsJson: { sent, errors },
|
|
|
|
|
ipAddress: ctx.ip,
|
|
|
|
|
userAgent: ctx.userAgent,
|
2026-01-30 13:41:32 +01:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return { sent, skipped: input.userIds.length - users.length, errors }
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Complete onboarding for current user
|
|
|
|
|
*/
|
|
|
|
|
completeOnboarding: protectedProcedure
|
|
|
|
|
.input(
|
|
|
|
|
z.object({
|
|
|
|
|
name: z.string().min(1).max(255),
|
|
|
|
|
phoneNumber: z.string().optional(),
|
2026-02-04 14:15:06 +01:00
|
|
|
country: z.string().optional(),
|
feat: applicant onboarding, bulk invite, team management enhancements
- Add nationality/institution fields to User model with migration
- Applicant onboarding wizard (name, photo, nationality, country, institution, bio, project logo, preferences)
- Project logo upload from applicant context with team membership verification
- APPLICANT redirects in set-password, onboarding, and auth layout
- Mask evaluation round names as "Evaluation Round 1/2/..." for applicants
- Extend inviteTeamMember with nationality/country/institution/sendInvite fields
- Admin getApplicants query with search/filter/pagination
- Admin bulkInviteApplicants mutation with token generation and emails
- Applicants tab on Members page with bulk select and floating invite bar
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 10:11:11 +01:00
|
|
|
nationality: z.string().optional(),
|
|
|
|
|
institution: z.string().optional(),
|
2026-02-04 15:27:28 +01:00
|
|
|
bio: z.string().max(500).optional(),
|
2026-01-30 13:41:32 +01:00
|
|
|
expertiseTags: z.array(z.string()).optional(),
|
|
|
|
|
notificationPreference: z.enum(['EMAIL', 'WHATSAPP', 'BOTH', 'NONE']).optional(),
|
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
|
|
|
// Competition architecture: jury self-service preferences
|
|
|
|
|
juryPreferences: z
|
|
|
|
|
.array(
|
|
|
|
|
z.object({
|
|
|
|
|
juryGroupMemberId: z.string(),
|
|
|
|
|
selfServiceCap: z.number().int().positive().optional(),
|
|
|
|
|
selfServiceRatio: z.number().min(0).max(1).optional(),
|
|
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
.optional(),
|
2026-01-30 13:41:32 +01:00
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
.mutation(async ({ ctx, input }) => {
|
2026-02-04 01:15:21 +01:00
|
|
|
// Get existing user to preserve admin-set tags
|
|
|
|
|
const existingUser = await ctx.prisma.user.findUniqueOrThrow({
|
|
|
|
|
where: { id: ctx.user.id },
|
|
|
|
|
select: { expertiseTags: true },
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Merge admin-set tags with user-selected tags (preserving order: admin first, then user)
|
|
|
|
|
const adminTags = existingUser.expertiseTags || []
|
|
|
|
|
const userTags = input.expertiseTags || []
|
|
|
|
|
const mergedTags = [...new Set([...adminTags, ...userTags])]
|
|
|
|
|
|
2026-02-05 21:09:06 +01:00
|
|
|
const user = await ctx.prisma.$transaction(async (tx) => {
|
|
|
|
|
const updated = await tx.user.update({
|
|
|
|
|
where: { id: ctx.user.id },
|
|
|
|
|
data: {
|
|
|
|
|
name: input.name,
|
|
|
|
|
phoneNumber: input.phoneNumber,
|
|
|
|
|
country: input.country,
|
feat: applicant onboarding, bulk invite, team management enhancements
- Add nationality/institution fields to User model with migration
- Applicant onboarding wizard (name, photo, nationality, country, institution, bio, project logo, preferences)
- Project logo upload from applicant context with team membership verification
- APPLICANT redirects in set-password, onboarding, and auth layout
- Mask evaluation round names as "Evaluation Round 1/2/..." for applicants
- Extend inviteTeamMember with nationality/country/institution/sendInvite fields
- Admin getApplicants query with search/filter/pagination
- Admin bulkInviteApplicants mutation with token generation and emails
- Applicants tab on Members page with bulk select and floating invite bar
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 10:11:11 +01:00
|
|
|
nationality: input.nationality,
|
|
|
|
|
institution: input.institution,
|
2026-02-05 21:09:06 +01:00
|
|
|
bio: input.bio,
|
|
|
|
|
expertiseTags: mergedTags,
|
|
|
|
|
notificationPreference: input.notificationPreference || 'EMAIL',
|
|
|
|
|
onboardingCompletedAt: new Date(),
|
|
|
|
|
status: 'ACTIVE', // Activate user after onboarding
|
|
|
|
|
},
|
|
|
|
|
})
|
2026-01-30 13:41:32 +01:00
|
|
|
|
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
|
|
|
// Process jury self-service preferences
|
|
|
|
|
if (input.juryPreferences && input.juryPreferences.length > 0) {
|
|
|
|
|
for (const pref of input.juryPreferences) {
|
|
|
|
|
// Security: verify this member belongs to the current user
|
|
|
|
|
const member = await tx.juryGroupMember.findUnique({
|
|
|
|
|
where: { id: pref.juryGroupMemberId },
|
|
|
|
|
})
|
|
|
|
|
if (!member || member.userId !== ctx.user.id) continue
|
|
|
|
|
|
2026-02-17 12:33:20 +01:00
|
|
|
await tx.juryGroupMember.update({
|
|
|
|
|
where: { id: pref.juryGroupMemberId },
|
|
|
|
|
data: {
|
|
|
|
|
selfServiceCap: pref.selfServiceCap != null ? Math.min(pref.selfServiceCap, 50) : undefined,
|
|
|
|
|
selfServiceRatio: pref.selfServiceRatio,
|
|
|
|
|
},
|
|
|
|
|
})
|
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-05 21:09:06 +01:00
|
|
|
return updated
|
2026-01-30 13:41:32 +01:00
|
|
|
})
|
|
|
|
|
|
Round detail overhaul, file requirements, project management, audit log fix
- Redesign round detail page with 6 tabs (overview, projects, filtering, assignments, config, documents)
- Add jury group assignment selector in round stats bar
- Add FileRequirementsEditor component replacing SubmissionWindowManager
- Add FilteringDashboard component for AI-powered project screening
- Add project removal from rounds (single + bulk) with cascading to subsequent rounds
- Add project add/remove UI in ProjectStatesTable with confirmation dialogs
- Fix logAudit inside $transaction pattern across all 12 router files
(PostgreSQL aborted-transaction state caused silent operation failures)
- Fix special awards creation, deletion, status update, and winner assignment
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 07:49:39 +01:00
|
|
|
// Audit outside transaction so failures don't roll back the onboarding
|
|
|
|
|
await logAudit({
|
|
|
|
|
prisma: ctx.prisma,
|
|
|
|
|
userId: ctx.user.id,
|
|
|
|
|
action: 'COMPLETE_ONBOARDING',
|
|
|
|
|
entityType: 'User',
|
|
|
|
|
entityId: ctx.user.id,
|
|
|
|
|
detailsJson: { name: input.name, juryPreferencesCount: input.juryPreferences?.length ?? 0 },
|
|
|
|
|
ipAddress: ctx.ip,
|
|
|
|
|
userAgent: ctx.userAgent,
|
|
|
|
|
})
|
|
|
|
|
|
2026-01-30 13:41:32 +01:00
|
|
|
return user
|
|
|
|
|
}),
|
|
|
|
|
|
2026-02-17 12:33:20 +01:00
|
|
|
/**
|
|
|
|
|
* Update jury preferences outside of onboarding (e.g., when a new round opens).
|
|
|
|
|
*/
|
|
|
|
|
updateJuryPreferences: protectedProcedure
|
|
|
|
|
.input(
|
|
|
|
|
z.object({
|
|
|
|
|
preferences: z.array(
|
|
|
|
|
z.object({
|
|
|
|
|
juryGroupMemberId: z.string(),
|
|
|
|
|
selfServiceCap: z.number().int().min(1).max(50),
|
|
|
|
|
selfServiceRatio: z.number().min(0).max(1),
|
|
|
|
|
})
|
|
|
|
|
),
|
|
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
.mutation(async ({ ctx, input }) => {
|
|
|
|
|
for (const pref of input.preferences) {
|
|
|
|
|
const member = await ctx.prisma.juryGroupMember.findUnique({
|
|
|
|
|
where: { id: pref.juryGroupMemberId },
|
|
|
|
|
})
|
|
|
|
|
if (!member || member.userId !== ctx.user.id) continue
|
|
|
|
|
|
|
|
|
|
await ctx.prisma.juryGroupMember.update({
|
|
|
|
|
where: { id: pref.juryGroupMemberId },
|
|
|
|
|
data: {
|
|
|
|
|
selfServiceCap: pref.selfServiceCap,
|
|
|
|
|
selfServiceRatio: pref.selfServiceRatio,
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
return { success: true }
|
|
|
|
|
}),
|
|
|
|
|
|
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
|
|
|
/**
|
|
|
|
|
* Get onboarding context for the current user.
|
2026-02-17 12:33:20 +01:00
|
|
|
* Returns jury group memberships for self-service preferences.
|
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
|
|
|
*/
|
|
|
|
|
getOnboardingContext: protectedProcedure.query(async ({ ctx }) => {
|
|
|
|
|
const memberships = await ctx.prisma.juryGroupMember.findMany({
|
|
|
|
|
where: { userId: ctx.user.id },
|
|
|
|
|
include: {
|
|
|
|
|
juryGroup: {
|
|
|
|
|
select: {
|
|
|
|
|
id: true,
|
|
|
|
|
name: true,
|
|
|
|
|
defaultMaxAssignments: true,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return {
|
2026-02-17 12:33:20 +01:00
|
|
|
hasSelfServiceOptions: memberships.length > 0,
|
|
|
|
|
memberships: memberships.map((m) => ({
|
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
|
|
|
juryGroupMemberId: m.id,
|
|
|
|
|
juryGroupName: m.juryGroup.name,
|
|
|
|
|
currentCap: m.maxAssignmentsOverride ?? m.juryGroup.defaultMaxAssignments,
|
|
|
|
|
selfServiceCap: m.selfServiceCap,
|
|
|
|
|
selfServiceRatio: m.selfServiceRatio,
|
2026-02-17 12:33:20 +01:00
|
|
|
preferredStartupRatio: m.preferredStartupRatio,
|
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
|
|
|
})),
|
|
|
|
|
}
|
|
|
|
|
}),
|
|
|
|
|
|
2026-01-30 13:41:32 +01:00
|
|
|
/**
|
|
|
|
|
* Check if current user needs onboarding
|
|
|
|
|
*/
|
|
|
|
|
needsOnboarding: protectedProcedure.query(async ({ ctx }) => {
|
|
|
|
|
const user = await ctx.prisma.user.findUniqueOrThrow({
|
|
|
|
|
where: { id: ctx.user.id },
|
|
|
|
|
select: { onboardingCompletedAt: true, role: true },
|
|
|
|
|
})
|
|
|
|
|
|
2026-02-04 14:15:06 +01:00
|
|
|
// Jury members, mentors, and admins need onboarding
|
|
|
|
|
const rolesRequiringOnboarding = ['JURY_MEMBER', 'MENTOR', 'PROGRAM_ADMIN', 'SUPER_ADMIN']
|
2026-02-04 01:15:21 +01:00
|
|
|
if (!rolesRequiringOnboarding.includes(user.role)) {
|
2026-01-30 13:41:32 +01:00
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return user.onboardingCompletedAt === null
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Check if current user needs to set a password
|
|
|
|
|
*/
|
|
|
|
|
needsPasswordSetup: protectedProcedure.query(async ({ ctx }) => {
|
|
|
|
|
const user = await ctx.prisma.user.findUniqueOrThrow({
|
|
|
|
|
where: { id: ctx.user.id },
|
|
|
|
|
select: { mustSetPassword: true, passwordHash: true },
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return user.mustSetPassword || user.passwordHash === null
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Set password for current user
|
|
|
|
|
*/
|
|
|
|
|
setPassword: protectedProcedure
|
|
|
|
|
.input(
|
|
|
|
|
z.object({
|
|
|
|
|
password: z.string().min(8),
|
|
|
|
|
confirmPassword: z.string().min(8),
|
|
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
.mutation(async ({ ctx, input }) => {
|
|
|
|
|
// Validate passwords match
|
|
|
|
|
if (input.password !== input.confirmPassword) {
|
|
|
|
|
throw new TRPCError({
|
|
|
|
|
code: 'BAD_REQUEST',
|
|
|
|
|
message: 'Passwords do not match',
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Validate password requirements
|
|
|
|
|
const validation = validatePassword(input.password)
|
|
|
|
|
if (!validation.valid) {
|
|
|
|
|
throw new TRPCError({
|
|
|
|
|
code: 'BAD_REQUEST',
|
|
|
|
|
message: validation.errors.join('. '),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Hash the password
|
|
|
|
|
const passwordHash = await hashPassword(input.password)
|
|
|
|
|
|
Round detail overhaul, file requirements, project management, audit log fix
- Redesign round detail page with 6 tabs (overview, projects, filtering, assignments, config, documents)
- Add jury group assignment selector in round stats bar
- Add FileRequirementsEditor component replacing SubmissionWindowManager
- Add FilteringDashboard component for AI-powered project screening
- Add project removal from rounds (single + bulk) with cascading to subsequent rounds
- Add project add/remove UI in ProjectStatesTable with confirmation dialogs
- Fix logAudit inside $transaction pattern across all 12 router files
(PostgreSQL aborted-transaction state caused silent operation failures)
- Fix special awards creation, deletion, status update, and winner assignment
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 07:49:39 +01:00
|
|
|
// Update user with new password
|
|
|
|
|
const user = await ctx.prisma.user.update({
|
|
|
|
|
where: { id: ctx.user.id },
|
|
|
|
|
data: {
|
|
|
|
|
passwordHash,
|
|
|
|
|
passwordSetAt: new Date(),
|
|
|
|
|
mustSetPassword: false,
|
|
|
|
|
},
|
|
|
|
|
})
|
2026-02-05 21:09:06 +01:00
|
|
|
|
Round detail overhaul, file requirements, project management, audit log fix
- Redesign round detail page with 6 tabs (overview, projects, filtering, assignments, config, documents)
- Add jury group assignment selector in round stats bar
- Add FileRequirementsEditor component replacing SubmissionWindowManager
- Add FilteringDashboard component for AI-powered project screening
- Add project removal from rounds (single + bulk) with cascading to subsequent rounds
- Add project add/remove UI in ProjectStatesTable with confirmation dialogs
- Fix logAudit inside $transaction pattern across all 12 router files
(PostgreSQL aborted-transaction state caused silent operation failures)
- Fix special awards creation, deletion, status update, and winner assignment
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 07:49:39 +01:00
|
|
|
// Audit outside transaction so failures don't roll back the password set
|
|
|
|
|
await logAudit({
|
|
|
|
|
prisma: ctx.prisma,
|
|
|
|
|
userId: ctx.user.id,
|
|
|
|
|
action: 'PASSWORD_SET',
|
|
|
|
|
entityType: 'User',
|
|
|
|
|
entityId: ctx.user.id,
|
|
|
|
|
detailsJson: { timestamp: new Date().toISOString() },
|
|
|
|
|
ipAddress: ctx.ip,
|
|
|
|
|
userAgent: ctx.userAgent,
|
2026-01-30 13:41:32 +01:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return { success: true, email: user.email }
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Change password for current user (requires current password)
|
|
|
|
|
*/
|
|
|
|
|
changePassword: protectedProcedure
|
|
|
|
|
.input(
|
|
|
|
|
z.object({
|
|
|
|
|
currentPassword: z.string().min(1),
|
|
|
|
|
newPassword: z.string().min(8),
|
|
|
|
|
confirmNewPassword: z.string().min(8),
|
|
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
.mutation(async ({ ctx, input }) => {
|
|
|
|
|
// Get current user with password hash
|
|
|
|
|
const user = await ctx.prisma.user.findUniqueOrThrow({
|
|
|
|
|
where: { id: ctx.user.id },
|
|
|
|
|
select: { passwordHash: true },
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if (!user.passwordHash) {
|
|
|
|
|
throw new TRPCError({
|
|
|
|
|
code: 'BAD_REQUEST',
|
|
|
|
|
message: 'No password set. Please use magic link to sign in.',
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Verify current password
|
|
|
|
|
const { verifyPassword } = await import('@/lib/password')
|
|
|
|
|
const isValid = await verifyPassword(input.currentPassword, user.passwordHash)
|
|
|
|
|
if (!isValid) {
|
|
|
|
|
throw new TRPCError({
|
|
|
|
|
code: 'UNAUTHORIZED',
|
|
|
|
|
message: 'Current password is incorrect',
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Validate new passwords match
|
|
|
|
|
if (input.newPassword !== input.confirmNewPassword) {
|
|
|
|
|
throw new TRPCError({
|
|
|
|
|
code: 'BAD_REQUEST',
|
|
|
|
|
message: 'New passwords do not match',
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Validate new password requirements
|
|
|
|
|
const validation = validatePassword(input.newPassword)
|
|
|
|
|
if (!validation.valid) {
|
|
|
|
|
throw new TRPCError({
|
|
|
|
|
code: 'BAD_REQUEST',
|
|
|
|
|
message: validation.errors.join('. '),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Hash the new password
|
|
|
|
|
const passwordHash = await hashPassword(input.newPassword)
|
|
|
|
|
|
Round detail overhaul, file requirements, project management, audit log fix
- Redesign round detail page with 6 tabs (overview, projects, filtering, assignments, config, documents)
- Add jury group assignment selector in round stats bar
- Add FileRequirementsEditor component replacing SubmissionWindowManager
- Add FilteringDashboard component for AI-powered project screening
- Add project removal from rounds (single + bulk) with cascading to subsequent rounds
- Add project add/remove UI in ProjectStatesTable with confirmation dialogs
- Fix logAudit inside $transaction pattern across all 12 router files
(PostgreSQL aborted-transaction state caused silent operation failures)
- Fix special awards creation, deletion, status update, and winner assignment
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 07:49:39 +01:00
|
|
|
// Update user with new password
|
|
|
|
|
await ctx.prisma.user.update({
|
|
|
|
|
where: { id: ctx.user.id },
|
|
|
|
|
data: {
|
|
|
|
|
passwordHash,
|
|
|
|
|
passwordSetAt: new Date(),
|
|
|
|
|
},
|
|
|
|
|
})
|
2026-01-30 13:41:32 +01:00
|
|
|
|
Round detail overhaul, file requirements, project management, audit log fix
- Redesign round detail page with 6 tabs (overview, projects, filtering, assignments, config, documents)
- Add jury group assignment selector in round stats bar
- Add FileRequirementsEditor component replacing SubmissionWindowManager
- Add FilteringDashboard component for AI-powered project screening
- Add project removal from rounds (single + bulk) with cascading to subsequent rounds
- Add project add/remove UI in ProjectStatesTable with confirmation dialogs
- Fix logAudit inside $transaction pattern across all 12 router files
(PostgreSQL aborted-transaction state caused silent operation failures)
- Fix special awards creation, deletion, status update, and winner assignment
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 07:49:39 +01:00
|
|
|
// Audit outside transaction so failures don't roll back the password change
|
|
|
|
|
await logAudit({
|
|
|
|
|
prisma: ctx.prisma,
|
|
|
|
|
userId: ctx.user.id,
|
|
|
|
|
action: 'PASSWORD_CHANGED',
|
|
|
|
|
entityType: 'User',
|
|
|
|
|
entityId: ctx.user.id,
|
|
|
|
|
detailsJson: { timestamp: new Date().toISOString() },
|
|
|
|
|
ipAddress: ctx.ip,
|
|
|
|
|
userAgent: ctx.userAgent,
|
2026-01-30 13:41:32 +01:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return { success: true }
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Request password reset (public - no auth required)
|
|
|
|
|
* Sends a magic link and marks user for password reset
|
|
|
|
|
*/
|
|
|
|
|
requestPasswordReset: publicProcedure
|
|
|
|
|
.input(z.object({ email: z.string().email() }))
|
|
|
|
|
.mutation(async ({ ctx, input }) => {
|
|
|
|
|
// Find user by email
|
|
|
|
|
const user = await ctx.prisma.user.findUnique({
|
|
|
|
|
where: { email: input.email },
|
|
|
|
|
select: { id: true, email: true, status: true },
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Always return success to prevent email enumeration
|
|
|
|
|
if (!user || user.status === 'SUSPENDED') {
|
|
|
|
|
return { success: true, message: 'If an account exists with this email, a password reset link will be sent.' }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Mark user for password reset
|
|
|
|
|
await ctx.prisma.user.update({
|
|
|
|
|
where: { id: user.id },
|
|
|
|
|
data: { mustSetPassword: true },
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Generate a callback URL for the magic link
|
2026-02-23 14:27:58 +01:00
|
|
|
const baseUrl = process.env.NEXTAUTH_URL || 'https://portal.monaco-opc.com'
|
2026-01-30 13:41:32 +01:00
|
|
|
const callbackUrl = `${baseUrl}/set-password`
|
|
|
|
|
|
|
|
|
|
// We don't send the email here - the user will use the magic link form
|
|
|
|
|
// This just marks them for password reset
|
|
|
|
|
// The actual email is sent through NextAuth's email provider
|
|
|
|
|
|
|
|
|
|
// Audit log (without user ID since this is public)
|
2026-02-05 21:09:06 +01:00
|
|
|
await logAudit({
|
|
|
|
|
prisma: ctx.prisma,
|
|
|
|
|
userId: null, // No authenticated user
|
|
|
|
|
action: 'REQUEST_PASSWORD_RESET',
|
|
|
|
|
entityType: 'User',
|
|
|
|
|
entityId: user.id,
|
|
|
|
|
detailsJson: { email: input.email, timestamp: new Date().toISOString() },
|
|
|
|
|
ipAddress: ctx.ip,
|
|
|
|
|
userAgent: ctx.userAgent,
|
2026-01-30 13:41:32 +01:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return { success: true, message: 'If an account exists with this email, a password reset link will be sent.' }
|
|
|
|
|
}),
|
Implement 15 platform features: digest, availability, templates, comparison, live voting SSE, file versioning, mentorship, messaging, analytics, drafts, webhooks, peer review, audit enhancements, i18n
Features implemented:
- F1: Email digest notifications with cron endpoint and per-user frequency
- F2: Jury availability windows and workload preferences in smart assignment
- F3: Round templates with save-from-round and CRUD management
- F4: Side-by-side project comparison view for jury members
- F5: Real-time voting dashboard with Server-Sent Events (SSE)
- F6: Live voting UX: QR codes, audience voting, tie-breaking, score animations
- F7: File versioning, inline preview, bulk download with presigned URLs
- F8: Mentor dashboard: milestones, private notes, activity tracking
- F9: Communication hub with broadcasts, templates, and recipient targeting
- F10: Advanced analytics: cross-round comparison, juror consistency, diversity metrics, PDF export
- F11: Applicant draft saving with magic link resume and cron cleanup
- F12: Webhook integration layer with HMAC signing, retry, and delivery logs
- F13: Peer review discussions with anonymized scores and threaded comments
- F14: Audit log enhancements: before/after diffs, session grouping, anomaly detection, retention
- F15: i18n foundation with next-intl (EN/FR), cookie-based locale, language switcher
Schema: 12 new models, field additions to User, Project, ProjectFile, LiveVotingSession, LiveVote, MentorAssignment, AuditLog, Program
New routers: roundTemplate, message, webhook (registered in _app.ts)
New services: email-digest, webhook-dispatcher
New cron endpoints: /api/cron/digest, /api/cron/draft-cleanup, /api/cron/audit-cleanup
New API routes: /api/live-voting/stream (SSE), /api/files/bulk-download
All features are admin-configurable via SystemSettings or per-model settingsJson fields.
Docker build verified successfully.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-05 23:31:41 +01:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get current user's digest settings along with global digest config
|
|
|
|
|
*/
|
|
|
|
|
getDigestSettings: protectedProcedure.query(async ({ ctx }) => {
|
|
|
|
|
const [user, digestEnabled, digestSections] = await Promise.all([
|
|
|
|
|
ctx.prisma.user.findUniqueOrThrow({
|
|
|
|
|
where: { id: ctx.user.id },
|
|
|
|
|
select: { digestFrequency: true },
|
|
|
|
|
}),
|
|
|
|
|
ctx.prisma.systemSettings.findUnique({
|
|
|
|
|
where: { key: 'digest_enabled' },
|
|
|
|
|
select: { value: true },
|
|
|
|
|
}),
|
|
|
|
|
ctx.prisma.systemSettings.findUnique({
|
|
|
|
|
where: { key: 'digest_sections' },
|
|
|
|
|
select: { value: true },
|
|
|
|
|
}),
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
digestFrequency: user.digestFrequency,
|
|
|
|
|
globalDigestEnabled: digestEnabled?.value === 'true',
|
|
|
|
|
globalDigestSections: digestSections?.value ? JSON.parse(digestSections.value) : [],
|
|
|
|
|
}
|
|
|
|
|
}),
|
2026-02-24 17:44:55 +01:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Update a user's roles array (admin only)
|
|
|
|
|
* Also updates the primary role to the highest privilege role in the array.
|
|
|
|
|
*/
|
|
|
|
|
updateRoles: adminProcedure
|
|
|
|
|
.input(z.object({
|
|
|
|
|
userId: z.string(),
|
|
|
|
|
roles: z.array(z.nativeEnum(UserRole)).min(1),
|
|
|
|
|
}))
|
|
|
|
|
.mutation(async ({ ctx, input }) => {
|
|
|
|
|
// Guard: only SUPER_ADMIN can grant SUPER_ADMIN
|
|
|
|
|
if (input.roles.includes('SUPER_ADMIN') && ctx.user.role !== 'SUPER_ADMIN') {
|
|
|
|
|
throw new TRPCError({ code: 'FORBIDDEN', message: 'Only super admins can grant super admin role' })
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Set primary role to highest privilege role
|
|
|
|
|
const rolePriority: UserRole[] = ['SUPER_ADMIN', 'PROGRAM_ADMIN', 'JURY_MEMBER', 'MENTOR', 'OBSERVER', 'AWARD_MASTER', 'APPLICANT', 'AUDIENCE']
|
|
|
|
|
const primaryRole = rolePriority.find(r => input.roles.includes(r)) || input.roles[0]
|
|
|
|
|
|
|
|
|
|
return ctx.prisma.user.update({
|
|
|
|
|
where: { id: input.userId },
|
|
|
|
|
data: { roles: input.roles, role: primaryRole },
|
|
|
|
|
})
|
|
|
|
|
}),
|
feat: applicant onboarding, bulk invite, team management enhancements
- Add nationality/institution fields to User model with migration
- Applicant onboarding wizard (name, photo, nationality, country, institution, bio, project logo, preferences)
- Project logo upload from applicant context with team membership verification
- APPLICANT redirects in set-password, onboarding, and auth layout
- Mask evaluation round names as "Evaluation Round 1/2/..." for applicants
- Extend inviteTeamMember with nationality/country/institution/sendInvite fields
- Admin getApplicants query with search/filter/pagination
- Admin bulkInviteApplicants mutation with token generation and emails
- Applicants tab on Members page with bulk select and floating invite bar
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 10:11:11 +01:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* List applicant users with project info for admin bulk-invite page.
|
|
|
|
|
*/
|
|
|
|
|
getApplicants: adminProcedure
|
|
|
|
|
.input(
|
|
|
|
|
z.object({
|
|
|
|
|
search: z.string().optional(),
|
|
|
|
|
roundId: z.string().optional(),
|
|
|
|
|
status: z.enum(['NONE', 'INVITED', 'ACTIVE', 'SUSPENDED']).optional(),
|
|
|
|
|
page: z.number().int().positive().default(1),
|
|
|
|
|
perPage: z.number().int().positive().max(100).default(20),
|
|
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
.query(async ({ ctx, input }) => {
|
|
|
|
|
const where: Prisma.UserWhereInput = {
|
|
|
|
|
role: 'APPLICANT',
|
|
|
|
|
...(input.status && { status: input.status }),
|
|
|
|
|
...(input.search && {
|
|
|
|
|
OR: [
|
|
|
|
|
{ name: { contains: input.search, mode: 'insensitive' as const } },
|
|
|
|
|
{ email: { contains: input.search, mode: 'insensitive' as const } },
|
|
|
|
|
{ teamMemberships: { some: { project: { title: { contains: input.search, mode: 'insensitive' as const } } } } },
|
|
|
|
|
],
|
|
|
|
|
}),
|
|
|
|
|
...(input.roundId && {
|
|
|
|
|
teamMemberships: { some: { project: { projectRoundStates: { some: { roundId: input.roundId } } } } },
|
|
|
|
|
}),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const [users, total] = await Promise.all([
|
|
|
|
|
ctx.prisma.user.findMany({
|
|
|
|
|
where,
|
|
|
|
|
select: {
|
|
|
|
|
id: true,
|
|
|
|
|
email: true,
|
|
|
|
|
name: true,
|
|
|
|
|
status: true,
|
|
|
|
|
nationality: true,
|
|
|
|
|
institution: true,
|
|
|
|
|
lastLoginAt: true,
|
|
|
|
|
onboardingCompletedAt: true,
|
|
|
|
|
teamMemberships: {
|
|
|
|
|
take: 1,
|
|
|
|
|
select: {
|
|
|
|
|
role: true,
|
|
|
|
|
project: { select: { id: true, title: true } },
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
submittedProjects: {
|
|
|
|
|
take: 1,
|
|
|
|
|
select: { id: true, title: true },
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
orderBy: { name: 'asc' },
|
|
|
|
|
skip: (input.page - 1) * input.perPage,
|
|
|
|
|
take: input.perPage,
|
|
|
|
|
}),
|
|
|
|
|
ctx.prisma.user.count({ where }),
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
users: users.map((u) => {
|
|
|
|
|
const project = u.submittedProjects[0] || u.teamMemberships[0]?.project || null
|
|
|
|
|
return {
|
|
|
|
|
id: u.id,
|
|
|
|
|
email: u.email,
|
|
|
|
|
name: u.name,
|
|
|
|
|
status: u.status,
|
|
|
|
|
nationality: u.nationality,
|
|
|
|
|
institution: u.institution,
|
|
|
|
|
lastLoginAt: u.lastLoginAt,
|
|
|
|
|
onboardingCompleted: !!u.onboardingCompletedAt,
|
|
|
|
|
projectName: project?.title ?? null,
|
|
|
|
|
projectId: project?.id ?? null,
|
|
|
|
|
}
|
|
|
|
|
}),
|
|
|
|
|
total,
|
|
|
|
|
totalPages: Math.ceil(total / input.perPage),
|
|
|
|
|
perPage: input.perPage,
|
|
|
|
|
}
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Bulk invite applicant users — generates tokens, sets INVITED, sends emails.
|
|
|
|
|
*/
|
|
|
|
|
bulkInviteApplicants: adminProcedure
|
|
|
|
|
.input(z.object({ userIds: z.array(z.string()).min(1).max(500) }))
|
|
|
|
|
.mutation(async ({ ctx, input }) => {
|
|
|
|
|
const users = await ctx.prisma.user.findMany({
|
|
|
|
|
where: {
|
|
|
|
|
id: { in: input.userIds },
|
|
|
|
|
role: 'APPLICANT',
|
|
|
|
|
status: { in: ['NONE', 'INVITED'] },
|
|
|
|
|
},
|
|
|
|
|
select: { id: true, email: true, name: true, status: true },
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const expiryMs = await getInviteExpiryMs(ctx.prisma)
|
|
|
|
|
let sent = 0
|
|
|
|
|
let skipped = 0
|
|
|
|
|
const failed: string[] = []
|
|
|
|
|
const baseUrl = process.env.NEXTAUTH_URL || 'https://portal.monaco-opc.com'
|
|
|
|
|
|
|
|
|
|
for (const user of users) {
|
|
|
|
|
try {
|
|
|
|
|
const token = generateInviteToken()
|
|
|
|
|
await ctx.prisma.user.update({
|
|
|
|
|
where: { id: user.id },
|
|
|
|
|
data: {
|
|
|
|
|
status: 'INVITED',
|
|
|
|
|
inviteToken: token,
|
|
|
|
|
inviteTokenExpiresAt: new Date(Date.now() + expiryMs),
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const inviteUrl = `${baseUrl}/accept-invite?token=${token}`
|
|
|
|
|
await sendInvitationEmail(user.email, user.name || 'Applicant', inviteUrl, 'APPLICANT')
|
|
|
|
|
sent++
|
|
|
|
|
} catch (error) {
|
|
|
|
|
failed.push(user.email)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
skipped = input.userIds.length - users.length
|
|
|
|
|
|
|
|
|
|
await logAudit({
|
|
|
|
|
prisma: ctx.prisma,
|
|
|
|
|
userId: ctx.user.id,
|
|
|
|
|
action: 'BULK_INVITE_APPLICANTS',
|
|
|
|
|
entityType: 'User',
|
|
|
|
|
entityId: 'bulk',
|
|
|
|
|
detailsJson: { sent, skipped, failed: failed.length, total: input.userIds.length },
|
|
|
|
|
ipAddress: ctx.ip,
|
|
|
|
|
userAgent: ctx.userAgent,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return { sent, skipped, failed }
|
|
|
|
|
}),
|
2026-01-30 13:41:32 +01:00
|
|
|
})
|