Some checks failed
Build and Push Docker Image / build (push) Has been cancelled
Security fixes: - Block self-registration via magic link (PrismaAdapter createUser throws) - Magic links only sent to existing ACTIVE users (prevents enumeration) - signIn callback rejects non-existent users (defense-in-depth) - Change schema default role from JURY_MEMBER to APPLICANT - Add authentication to live-voting SSE stream endpoint - Fix false FILE_OPENED/FILE_DOWNLOADED audit events on page load (remove purpose from eagerly pre-fetched URL queries) Bug fixes: - Fix impersonation skeleton screen on applicant dashboard - Fix onboarding redirect loop in auth layout Observer dashboard redesign (Steps 1-6): - Clickable round pipeline with selected round highlighting - Round-type-specific dashboard panels (intake, filtering, evaluation, submission, mentoring, live final, deliberation) - Enhanced activity feed with server-side humanization - Previous round comparison section - New backend queries for round-specific analytics Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
134 lines
3.5 KiB
TypeScript
134 lines
3.5 KiB
TypeScript
import { z } from 'zod'
|
|
import { TRPCError } from '@trpc/server'
|
|
import { router, adminProcedure, protectedProcedure } from '../trpc'
|
|
import { generateLogoKey, type StorageProviderType } from '@/lib/storage'
|
|
import {
|
|
getImageUploadUrl,
|
|
confirmImageUpload,
|
|
getImageUrl,
|
|
deleteImage,
|
|
type ImageUploadConfig,
|
|
} from '../utils/image-upload'
|
|
|
|
type LogoSelect = {
|
|
logoKey: string | null
|
|
logoProvider: string | null
|
|
}
|
|
|
|
const logoConfig: ImageUploadConfig<LogoSelect> = {
|
|
label: 'logo',
|
|
generateKey: generateLogoKey,
|
|
findCurrent: (prisma, entityId) =>
|
|
prisma.project.findUnique({
|
|
where: { id: entityId },
|
|
select: { logoKey: true, logoProvider: true },
|
|
}),
|
|
getImageKey: (record) => record.logoKey,
|
|
getProviderType: (record) =>
|
|
(record.logoProvider as StorageProviderType) || 's3',
|
|
setImage: (prisma, entityId, key, providerType) =>
|
|
prisma.project.update({
|
|
where: { id: entityId },
|
|
data: { logoKey: key, logoProvider: providerType },
|
|
}),
|
|
clearImage: (prisma, entityId) =>
|
|
prisma.project.update({
|
|
where: { id: entityId },
|
|
data: { logoKey: null, logoProvider: null },
|
|
}),
|
|
auditEntityType: 'Project',
|
|
auditFieldName: 'logoKey',
|
|
}
|
|
|
|
export const logoRouter = router({
|
|
/**
|
|
* Get a pre-signed URL for uploading a project logo
|
|
*/
|
|
getUploadUrl: adminProcedure
|
|
.input(
|
|
z.object({
|
|
projectId: z.string(),
|
|
fileName: z.string(),
|
|
contentType: z.string(),
|
|
})
|
|
)
|
|
.mutation(async ({ ctx, input }) => {
|
|
// Verify project exists
|
|
const project = await ctx.prisma.project.findUnique({
|
|
where: { id: input.projectId },
|
|
select: { id: true },
|
|
})
|
|
|
|
if (!project) {
|
|
throw new TRPCError({ code: 'NOT_FOUND', message: 'Project not found' })
|
|
}
|
|
|
|
return getImageUploadUrl(
|
|
input.projectId,
|
|
input.fileName,
|
|
input.contentType,
|
|
generateLogoKey
|
|
)
|
|
}),
|
|
|
|
/**
|
|
* Confirm logo upload and update project
|
|
*/
|
|
confirmUpload: adminProcedure
|
|
.input(
|
|
z.object({
|
|
projectId: z.string(),
|
|
key: z.string(),
|
|
providerType: z.enum(['s3', 'local']),
|
|
})
|
|
)
|
|
.mutation(async ({ ctx, input }) => {
|
|
await confirmImageUpload(
|
|
ctx.prisma,
|
|
logoConfig,
|
|
input.projectId,
|
|
input.key,
|
|
input.providerType,
|
|
{
|
|
userId: ctx.user.id,
|
|
ip: ctx.ip,
|
|
userAgent: ctx.userAgent,
|
|
}
|
|
)
|
|
|
|
// Return the updated project fields to match original API contract
|
|
const project = await ctx.prisma.project.findUnique({
|
|
where: { id: input.projectId },
|
|
select: {
|
|
id: true,
|
|
logoKey: true,
|
|
logoProvider: true,
|
|
},
|
|
})
|
|
|
|
return project
|
|
}),
|
|
|
|
/**
|
|
* Get a project's logo URL (any authenticated user — logos are public display data)
|
|
*/
|
|
getUrl: protectedProcedure
|
|
.input(z.object({ projectId: z.string() }))
|
|
.query(async ({ ctx, input }) => {
|
|
return getImageUrl(ctx.prisma, logoConfig, input.projectId)
|
|
}),
|
|
|
|
/**
|
|
* Delete a project's logo
|
|
*/
|
|
delete: adminProcedure
|
|
.input(z.object({ projectId: z.string() }))
|
|
.mutation(async ({ ctx, input }) => {
|
|
return deleteImage(ctx.prisma, logoConfig, input.projectId, {
|
|
userId: ctx.user.id,
|
|
ip: ctx.ip,
|
|
userAgent: ctx.userAgent,
|
|
})
|
|
}),
|
|
})
|