Files
MOPC-Portal/src/lib/minio.ts

118 lines
3.3 KiB
TypeScript
Raw Normal View History

import * as Minio from 'minio'
// MinIO client singleton
const globalForMinio = globalThis as unknown as {
minio: Minio.Client | undefined
}
// Internal endpoint for server-to-server communication
const MINIO_ENDPOINT = process.env.MINIO_ENDPOINT || 'http://localhost:9000'
// Public endpoint for browser-accessible URLs (pre-signed URLs)
// If not set, falls back to internal endpoint
export const MINIO_PUBLIC_ENDPOINT = process.env.MINIO_PUBLIC_ENDPOINT || MINIO_ENDPOINT
function createMinioClient(): Minio.Client {
const url = new URL(MINIO_ENDPOINT)
Comprehensive platform review: security fixes, query optimization, UI improvements, and code cleanup Security (Critical/High): - Fix path traversal bypass in local storage provider (path.resolve + prefix check) - Fix timing-unsafe HMAC comparison (crypto.timingSafeEqual) - Add auth + ownership checks to email API routes (verify-credentials, change-password) - Remove hardcoded secret key fallback in local storage provider - Add production credential check for MinIO (fail loudly if not set) - Remove DB error details from health check response - Add stricter rate limiting on application submissions (5/hour) - Add rate limiting on email availability check (anti-enumeration) - Change getAIAssignmentJobStatus to adminProcedure - Block dangerous file extensions on upload - Reduce project list max perPage from 5000 to 200 Query Optimization: - Optimize analytics getProjectRankings with select instead of full includes - Fix N+1 in mentor.getSuggestions (batch findMany instead of loop) - Use _count for files instead of fetching full file records in project list - Switch to bulk notifications in assignment and user bulk operations - Batch filtering upserts (25 per transaction instead of all at once) UI/UX: - Replace Inter font with Montserrat in public layout (brand consistency) - Use Logo component in public layout instead of placeholder - Create branded 404 and error pages - Make admin rounds table responsive with mobile card layout - Fix notification bell paths to be role-aware - Replace hardcoded slate colors with semantic tokens in admin sidebar - Force light mode (dark mode untested) - Adjust CardTitle default size - Improve muted-foreground contrast for accessibility (A11Y) - Move profile form state initialization to useEffect Code Quality: - Extract shared toProjectWithRelations to anonymization.ts (removed 3 duplicates) - Remove dead code: getObjectInfo, isValidImageSize, unused batch tag functions, debug logs - Remove unused twilio dependency - Remove redundant email index from schema - Add actual storage object deletion when file records are deleted - Wrap evaluation submit + assignment update in - Add comprehensive platform review document Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-05 20:31:08 +01:00
const accessKey = process.env.MINIO_ACCESS_KEY
const secretKey = process.env.MINIO_SECRET_KEY
if (process.env.NODE_ENV === 'production' && (!accessKey || !secretKey)) {
throw new Error('MINIO_ACCESS_KEY and MINIO_SECRET_KEY environment variables are required in production')
}
return new Minio.Client({
endPoint: url.hostname,
port: url.port ? parseInt(url.port) : (url.protocol === 'https:' ? 443 : 80),
useSSL: url.protocol === 'https:',
Comprehensive platform review: security fixes, query optimization, UI improvements, and code cleanup Security (Critical/High): - Fix path traversal bypass in local storage provider (path.resolve + prefix check) - Fix timing-unsafe HMAC comparison (crypto.timingSafeEqual) - Add auth + ownership checks to email API routes (verify-credentials, change-password) - Remove hardcoded secret key fallback in local storage provider - Add production credential check for MinIO (fail loudly if not set) - Remove DB error details from health check response - Add stricter rate limiting on application submissions (5/hour) - Add rate limiting on email availability check (anti-enumeration) - Change getAIAssignmentJobStatus to adminProcedure - Block dangerous file extensions on upload - Reduce project list max perPage from 5000 to 200 Query Optimization: - Optimize analytics getProjectRankings with select instead of full includes - Fix N+1 in mentor.getSuggestions (batch findMany instead of loop) - Use _count for files instead of fetching full file records in project list - Switch to bulk notifications in assignment and user bulk operations - Batch filtering upserts (25 per transaction instead of all at once) UI/UX: - Replace Inter font with Montserrat in public layout (brand consistency) - Use Logo component in public layout instead of placeholder - Create branded 404 and error pages - Make admin rounds table responsive with mobile card layout - Fix notification bell paths to be role-aware - Replace hardcoded slate colors with semantic tokens in admin sidebar - Force light mode (dark mode untested) - Adjust CardTitle default size - Improve muted-foreground contrast for accessibility (A11Y) - Move profile form state initialization to useEffect Code Quality: - Extract shared toProjectWithRelations to anonymization.ts (removed 3 duplicates) - Remove dead code: getObjectInfo, isValidImageSize, unused batch tag functions, debug logs - Remove unused twilio dependency - Remove redundant email index from schema - Add actual storage object deletion when file records are deleted - Wrap evaluation submit + assignment update in - Add comprehensive platform review document Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-05 20:31:08 +01:00
accessKey: accessKey || 'minioadmin',
secretKey: secretKey || 'minioadmin',
})
}
export const minio = globalForMinio.minio ?? createMinioClient()
if (process.env.NODE_ENV !== 'production') globalForMinio.minio = minio
// Default bucket name
export const BUCKET_NAME = process.env.MINIO_BUCKET || 'mopc-files'
/**
* Replace internal endpoint with public endpoint in a URL
*/
function replaceEndpoint(url: string): string {
if (MINIO_PUBLIC_ENDPOINT === MINIO_ENDPOINT) {
return url
}
try {
const internalUrl = new URL(MINIO_ENDPOINT)
const publicUrl = new URL(MINIO_PUBLIC_ENDPOINT)
return url.replace(
`${internalUrl.protocol}//${internalUrl.host}`,
`${publicUrl.protocol}//${publicUrl.host}`
)
} catch {
return url
}
}
// =============================================================================
// Helper Functions
// =============================================================================
/**
* Generate a pre-signed URL for file download or upload
* Uses MINIO_PUBLIC_ENDPOINT for browser-accessible URLs
*/
export async function getPresignedUrl(
bucket: string,
objectKey: string,
method: 'GET' | 'PUT' = 'GET',
expirySeconds: number = 900 // 15 minutes default
): Promise<string> {
let url: string
if (method === 'GET') {
url = await minio.presignedGetObject(bucket, objectKey, expirySeconds)
} else {
url = await minio.presignedPutObject(bucket, objectKey, expirySeconds)
}
// Replace internal endpoint with public endpoint for browser access
return replaceEndpoint(url)
}
/**
* Check if a bucket exists, create if not
*/
export async function ensureBucket(bucket: string): Promise<void> {
const exists = await minio.bucketExists(bucket)
if (!exists) {
await minio.makeBucket(bucket)
console.log(`Created MinIO bucket: ${bucket}`)
}
}
/**
* Delete an object from MinIO
*/
export async function deleteObject(
bucket: string,
objectKey: string
): Promise<void> {
await minio.removeObject(bucket, objectKey)
}
/**
* Generate a unique object key for a project file
*/
export function generateObjectKey(
projectId: string,
fileName: string
): string {
const timestamp = Date.now()
const sanitizedName = fileName.replace(/[^a-zA-Z0-9.-]/g, '_')
return `projects/${projectId}/${timestamp}-${sanitizedName}`
}