fix: presigned URL signatures, bucket consolidation, login & invite status
All checks were successful
Build and Push Docker Image / build (push) Successful in 9m44s
All checks were successful
Build and Push Docker Image / build (push) Successful in 9m44s
- MinIO: use separate public client for presigned URLs so AWS V4 signature matches the browser's Host header (fixes SignatureDoesNotMatch on all uploads) - Consolidate applicant/partner uploads to mopc-files bucket (removes non-existent mopc-submissions and mopc-partners buckets) - Auth: allow magic links for any non-SUSPENDED user (was ACTIVE-only, blocking first-time CSV-seeded applicants) - Auth: accept invite tokens for any non-SUSPENDED user (was INVITED-only) - Ensure all 14 invite token locations set status to INVITED Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,7 @@ import crypto from 'crypto'
|
||||
import { z } from 'zod'
|
||||
import { TRPCError } from '@trpc/server'
|
||||
import { router, publicProcedure, protectedProcedure } from '../trpc'
|
||||
import { getPresignedUrl, generateObjectKey } from '@/lib/minio'
|
||||
import { getPresignedUrl, generateObjectKey, BUCKET_NAME } from '@/lib/minio'
|
||||
import { generateLogoKey, createStorageProvider, type StorageProviderType } from '@/lib/storage'
|
||||
import { getImageUploadUrl, confirmImageUpload, getImageUrl, deleteImage, type ImageUploadConfig } from '@/server/utils/image-upload'
|
||||
import { sendStyledNotificationEmail, sendTeamMemberInviteEmail } from '@/lib/email'
|
||||
@@ -12,8 +12,9 @@ import { checkRequirementsAndTransition, triggerInProgressOnActivity, transition
|
||||
import { EvaluationConfigSchema, MentoringConfigSchema } from '@/types/competition-configs'
|
||||
import type { Prisma } from '@prisma/client'
|
||||
|
||||
// Bucket for applicant submissions
|
||||
export const SUBMISSIONS_BUCKET = 'mopc-submissions'
|
||||
// All uploads use the single configured bucket (MINIO_BUCKET / mopc-files).
|
||||
// Files are organized by path prefix: {ProjectName}/{RoundName}/... for submissions,
|
||||
// avatars/{userId}/... for profile images, logos/{projectId}/... for project logos.
|
||||
const TEAM_INVITE_TOKEN_EXPIRY_MS = 30 * 24 * 60 * 60 * 1000 // 30 days
|
||||
|
||||
function generateInviteToken(): string {
|
||||
@@ -346,11 +347,11 @@ export const applicantRouter = router({
|
||||
|
||||
const objectKey = generateObjectKey(project.title, input.fileName, roundName)
|
||||
|
||||
const url = await getPresignedUrl(SUBMISSIONS_BUCKET, objectKey, 'PUT', 3600)
|
||||
const url = await getPresignedUrl(BUCKET_NAME, objectKey, 'PUT', 3600)
|
||||
|
||||
return {
|
||||
url,
|
||||
bucket: SUBMISSIONS_BUCKET,
|
||||
bucket: BUCKET_NAME,
|
||||
objectKey,
|
||||
isLate,
|
||||
roundId: input.roundId || null,
|
||||
|
||||
@@ -890,11 +890,11 @@ export const dashboardRouter = router({
|
||||
)
|
||||
|
||||
for (const tm of unactivated) {
|
||||
// Generate invite token for each user
|
||||
// Generate invite token and mark as INVITED
|
||||
const token = generateInviteToken()
|
||||
await ctx.prisma.user.update({
|
||||
where: { id: tm.user.id },
|
||||
data: { inviteToken: token, inviteTokenExpiresAt: expiresAt },
|
||||
data: { inviteToken: token, inviteTokenExpiresAt: expiresAt, status: 'INVITED' },
|
||||
})
|
||||
|
||||
const accountUrl = `/accept-invite?token=${token}`
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import { z } from 'zod'
|
||||
import { router, protectedProcedure, adminProcedure } from '../trpc'
|
||||
import { getPresignedUrl } from '@/lib/minio'
|
||||
import { getPresignedUrl, BUCKET_NAME } from '@/lib/minio'
|
||||
import { logAudit } from '../utils/audit'
|
||||
|
||||
// Bucket for partner logos
|
||||
export const PARTNER_BUCKET = 'mopc-partners'
|
||||
|
||||
export const partnerRouter = router({
|
||||
/**
|
||||
* List all partners (admin view)
|
||||
@@ -270,13 +267,13 @@ export const partnerRouter = router({
|
||||
.mutation(async ({ input }) => {
|
||||
const timestamp = Date.now()
|
||||
const sanitizedName = input.fileName.replace(/[^a-zA-Z0-9.-]/g, '_')
|
||||
const objectKey = `logos/${timestamp}-${sanitizedName}`
|
||||
const objectKey = `partners/${timestamp}-${sanitizedName}`
|
||||
|
||||
const url = await getPresignedUrl(PARTNER_BUCKET, objectKey, 'PUT', 3600)
|
||||
const url = await getPresignedUrl(BUCKET_NAME, objectKey, 'PUT', 3600)
|
||||
|
||||
return {
|
||||
url,
|
||||
bucket: PARTNER_BUCKET,
|
||||
bucket: BUCKET_NAME,
|
||||
objectKey,
|
||||
}
|
||||
}),
|
||||
|
||||
@@ -1834,7 +1834,7 @@ export const projectRouter = router({
|
||||
const token = generateInviteToken()
|
||||
await ctx.prisma.user.update({
|
||||
where: { id: userId },
|
||||
data: { inviteToken: token, inviteTokenExpiresAt: new Date(Date.now() + expiryMs) },
|
||||
data: { inviteToken: token, inviteTokenExpiresAt: new Date(Date.now() + expiryMs), status: 'INVITED' },
|
||||
})
|
||||
tokenMap.set(userId, token)
|
||||
}
|
||||
@@ -2010,7 +2010,7 @@ export const projectRouter = router({
|
||||
const token = generateInviteToken()
|
||||
await ctx.prisma.user.update({
|
||||
where: { id: userId },
|
||||
data: { inviteToken: token, inviteTokenExpiresAt: new Date(Date.now() + expiryMs) },
|
||||
data: { inviteToken: token, inviteTokenExpiresAt: new Date(Date.now() + expiryMs), status: 'INVITED' },
|
||||
})
|
||||
tokenMap.set(userId, token)
|
||||
}
|
||||
@@ -2142,7 +2142,7 @@ export const projectRouter = router({
|
||||
const token = generateInviteToken()
|
||||
await ctx.prisma.user.update({
|
||||
where: { id: userId },
|
||||
data: { inviteToken: token, inviteTokenExpiresAt: new Date(Date.now() + expiryMs) },
|
||||
data: { inviteToken: token, inviteTokenExpiresAt: new Date(Date.now() + expiryMs), status: 'INVITED' },
|
||||
})
|
||||
tokenMap.set(userId, token)
|
||||
}
|
||||
|
||||
@@ -1342,7 +1342,7 @@ export const specialAwardRouter = router({
|
||||
tokenMap.set(user.id, token)
|
||||
await ctx.prisma.user.update({
|
||||
where: { id: user.id },
|
||||
data: { inviteToken: token, inviteTokenExpiresAt: expiresAt },
|
||||
data: { inviteToken: token, inviteTokenExpiresAt: expiresAt, status: 'INVITED' },
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -879,6 +879,7 @@ export const userRouter = router({
|
||||
data: {
|
||||
inviteToken: token,
|
||||
inviteTokenExpiresAt: new Date(Date.now() + expiryMs),
|
||||
status: 'INVITED',
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user