Initial commit: MOPC platform with Docker deployment setup
Full Next.js 15 platform with tRPC, Prisma, PostgreSQL, NextAuth. Includes production Dockerfile (multi-stage, port 7600), docker-compose with registry-based image pull, Gitea Actions CI workflow, nginx config for portal.monaco-opc.com, deployment scripts, and DEPLOYMENT.md guide. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
196
src/server/routers/logo.ts
Normal file
196
src/server/routers/logo.ts
Normal file
@@ -0,0 +1,196 @@
|
||||
import { z } from 'zod'
|
||||
import { TRPCError } from '@trpc/server'
|
||||
import { router, adminProcedure } from '../trpc'
|
||||
import {
|
||||
getStorageProviderWithType,
|
||||
createStorageProvider,
|
||||
generateLogoKey,
|
||||
getContentType,
|
||||
isValidImageType,
|
||||
type StorageProviderType,
|
||||
} from '@/lib/storage'
|
||||
|
||||
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 }) => {
|
||||
// Validate content type
|
||||
if (!isValidImageType(input.contentType)) {
|
||||
throw new TRPCError({ code: 'BAD_REQUEST', message: 'Invalid image type. Allowed: JPEG, PNG, GIF, WebP' })
|
||||
}
|
||||
|
||||
// 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' })
|
||||
}
|
||||
|
||||
const key = generateLogoKey(input.projectId, input.fileName)
|
||||
const contentType = getContentType(input.fileName)
|
||||
|
||||
const { provider, providerType } = await getStorageProviderWithType()
|
||||
const uploadUrl = await provider.getUploadUrl(key, contentType)
|
||||
|
||||
return {
|
||||
uploadUrl,
|
||||
key,
|
||||
providerType, // Return so client can pass it back on confirm
|
||||
}
|
||||
}),
|
||||
|
||||
/**
|
||||
* 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 }) => {
|
||||
// Use the provider that was used for upload
|
||||
const provider = createStorageProvider(input.providerType)
|
||||
const exists = await provider.objectExists(input.key)
|
||||
if (!exists) {
|
||||
throw new TRPCError({ code: 'NOT_FOUND', message: 'Upload not found. Please try uploading again.' })
|
||||
}
|
||||
|
||||
// Delete old logo if exists (from its original provider)
|
||||
const currentProject = await ctx.prisma.project.findUnique({
|
||||
where: { id: input.projectId },
|
||||
select: { logoKey: true, logoProvider: true },
|
||||
})
|
||||
|
||||
if (currentProject?.logoKey) {
|
||||
try {
|
||||
const oldProvider = createStorageProvider(
|
||||
(currentProject.logoProvider as StorageProviderType) || 's3'
|
||||
)
|
||||
await oldProvider.deleteObject(currentProject.logoKey)
|
||||
} catch (error) {
|
||||
// Log but don't fail if old logo deletion fails
|
||||
console.warn('Failed to delete old logo:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// Update project with new logo key and provider
|
||||
const project = await ctx.prisma.project.update({
|
||||
where: { id: input.projectId },
|
||||
data: {
|
||||
logoKey: input.key,
|
||||
logoProvider: input.providerType,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
logoKey: true,
|
||||
logoProvider: true,
|
||||
},
|
||||
})
|
||||
|
||||
// Audit log
|
||||
await ctx.prisma.auditLog.create({
|
||||
data: {
|
||||
userId: ctx.user.id,
|
||||
action: 'UPDATE',
|
||||
entityType: 'Project',
|
||||
entityId: input.projectId,
|
||||
detailsJson: {
|
||||
field: 'logoKey',
|
||||
newValue: input.key,
|
||||
provider: input.providerType,
|
||||
},
|
||||
ipAddress: ctx.ip,
|
||||
userAgent: ctx.userAgent,
|
||||
},
|
||||
})
|
||||
|
||||
return project
|
||||
}),
|
||||
|
||||
/**
|
||||
* Get a project's logo URL
|
||||
*/
|
||||
getUrl: adminProcedure
|
||||
.input(z.object({ projectId: z.string() }))
|
||||
.query(async ({ ctx, input }) => {
|
||||
const project = await ctx.prisma.project.findUnique({
|
||||
where: { id: input.projectId },
|
||||
select: { logoKey: true, logoProvider: true },
|
||||
})
|
||||
|
||||
if (!project?.logoKey) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Use the provider that was used when the file was stored
|
||||
const providerType = (project.logoProvider as StorageProviderType) || 's3'
|
||||
const provider = createStorageProvider(providerType)
|
||||
const url = await provider.getDownloadUrl(project.logoKey)
|
||||
|
||||
return url
|
||||
}),
|
||||
|
||||
/**
|
||||
* Delete a project's logo
|
||||
*/
|
||||
delete: adminProcedure
|
||||
.input(z.object({ projectId: z.string() }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const project = await ctx.prisma.project.findUnique({
|
||||
where: { id: input.projectId },
|
||||
select: { logoKey: true, logoProvider: true },
|
||||
})
|
||||
|
||||
if (!project?.logoKey) {
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
// Delete from the provider that was used when the file was stored
|
||||
const providerType = (project.logoProvider as StorageProviderType) || 's3'
|
||||
const provider = createStorageProvider(providerType)
|
||||
try {
|
||||
await provider.deleteObject(project.logoKey)
|
||||
} catch (error) {
|
||||
console.warn('Failed to delete logo from storage:', error)
|
||||
}
|
||||
|
||||
// Update project - clear both key and provider
|
||||
await ctx.prisma.project.update({
|
||||
where: { id: input.projectId },
|
||||
data: {
|
||||
logoKey: null,
|
||||
logoProvider: null,
|
||||
},
|
||||
})
|
||||
|
||||
// Audit log
|
||||
await ctx.prisma.auditLog.create({
|
||||
data: {
|
||||
userId: ctx.user.id,
|
||||
action: 'DELETE',
|
||||
entityType: 'Project',
|
||||
entityId: input.projectId,
|
||||
detailsJson: { field: 'logoKey' },
|
||||
ipAddress: ctx.ip,
|
||||
userAgent: ctx.userAgent,
|
||||
},
|
||||
})
|
||||
|
||||
return { success: true }
|
||||
}),
|
||||
})
|
||||
Reference in New Issue
Block a user