Initial commit: Port Nimara CRM (Layers 0-4)
Some checks failed
Build & Push Docker Images / build-and-push (push) Has been cancelled
Build & Push Docker Images / deploy (push) Has been cancelled
Build & Push Docker Images / lint (push) Has been cancelled

Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-26 11:52:51 +01:00
commit 67d7e6e3d5
572 changed files with 86496 additions and 0 deletions

63
src/lib/minio/index.ts Normal file
View File

@@ -0,0 +1,63 @@
import { Client } from 'minio';
import { env } from '@/lib/env';
import { logger } from '@/lib/logger';
export const minioClient = new Client({
endPoint: env.MINIO_ENDPOINT,
port: env.MINIO_PORT,
useSSL: env.MINIO_USE_SSL,
accessKey: env.MINIO_ACCESS_KEY,
secretKey: env.MINIO_SECRET_KEY,
});
const BUCKET = env.MINIO_BUCKET;
/**
* Ensures the configured bucket exists, creating it if not.
* Should be called once at application startup.
*/
export async function ensureBucket(): Promise<void> {
try {
const exists = await minioClient.bucketExists(BUCKET);
if (!exists) {
await minioClient.makeBucket(BUCKET);
logger.info({ bucket: BUCKET }, 'MinIO bucket created');
} else {
logger.debug({ bucket: BUCKET }, 'MinIO bucket exists');
}
} catch (err) {
logger.error({ err, bucket: BUCKET }, 'Failed to ensure MinIO bucket');
throw err;
}
}
/**
* Generates a pre-signed GET URL for an object.
*
* Default expiry is 15 minutes (900 seconds) per SECURITY-GUIDELINES.md §7.1.
*/
export async function getPresignedUrl(
objectKey: string,
expirySeconds = 900,
): Promise<string> {
return minioClient.presignedGetObject(BUCKET, objectKey, expirySeconds);
}
/**
* Constructs a storage path from typed components.
*
* Format: `{portSlug}/{entity}/{entityId}/{fileId}.{extension}`
*
* No user-supplied input should ever be used as path components — only UUIDs
* and controlled slugs (SECURITY-GUIDELINES.md §3.4, §7.1).
*/
export function buildStoragePath(
portSlug: string,
entity: string,
entityId: string,
fileId: string,
extension: string,
): string {
return `${portSlug}/${entity}/${entityId}/${fileId}.${extension}`;
}