Initial commit: Meshkee CMS API

NestJS backend with Prisma, Docker Compose for Postgres/Redis, and deploy docs for the production VM.
This commit is contained in:
Ali Reza
2026-07-21 17:52:36 +03:30
commit bb59d5e9ba
254 changed files with 37031 additions and 0 deletions
+85
View File
@@ -0,0 +1,85 @@
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import {
DeleteObjectCommand,
GetObjectCommand,
PutObjectCommand,
S3Client,
} from '@aws-sdk/client-s3';
import { StoredObject, UploadObjectInput } from './storage.types';
@Injectable()
export class S3StorageDriver {
private readonly client: S3Client;
private readonly bucket: string;
private readonly publicUrlBase: string;
private readonly storageDisk = 's3';
constructor(private readonly config: ConfigService) {
const endpoint = this.config.getOrThrow<string>('S3_ENDPOINT');
const region = this.config.get<string>('S3_REGION', 'us-east-1');
const forcePathStyle =
this.config.get<string>('S3_FORCE_PATH_STYLE', 'true') === 'true';
this.bucket = this.config.getOrThrow<string>('S3_BUCKET');
this.publicUrlBase = this.config
.getOrThrow<string>('S3_PUBLIC_URL')
.replace(/\/$/, '');
this.client = new S3Client({
endpoint,
region,
forcePathStyle,
credentials: {
accessKeyId: this.config.getOrThrow<string>('S3_ACCESS_KEY_ID'),
secretAccessKey: this.config.getOrThrow<string>('S3_SECRET_ACCESS_KEY'),
},
});
}
async upload(input: UploadObjectInput): Promise<StoredObject> {
const key = input.key.replace(/^\/+/, '');
await this.client.send(
new PutObjectCommand({
Bucket: this.bucket,
Key: key,
Body: input.body,
ContentType: input.contentType,
}),
);
return {
storageDisk: this.storageDisk,
storagePath: key,
publicUrl: `${this.publicUrlBase}/${key}`,
};
}
async getBuffer(storagePath: string): Promise<Buffer> {
const key = storagePath.replace(/^\/+/, '');
const result = await this.client.send(
new GetObjectCommand({
Bucket: this.bucket,
Key: key,
}),
);
if (!result.Body) {
throw new Error(`Empty object body for ${key}`);
}
return Buffer.from(await result.Body.transformToByteArray());
}
async delete(storagePath: string): Promise<void> {
const key = storagePath.replace(/^\/+/, '');
await this.client.send(
new DeleteObjectCommand({
Bucket: this.bucket,
Key: key,
}),
);
}
}
+10
View File
@@ -0,0 +1,10 @@
import { Global, Module } from '@nestjs/common';
import { S3StorageDriver } from './s3-storage.driver';
import { StorageService } from './storage.service';
@Global()
@Module({
providers: [S3StorageDriver, StorageService],
exports: [StorageService],
})
export class StorageModule {}
+28
View File
@@ -0,0 +1,28 @@
import { Injectable } from '@nestjs/common';
import { S3StorageDriver } from './s3-storage.driver';
import { StoredObject, UploadObjectInput } from './storage.types';
@Injectable()
export class StorageService {
constructor(private readonly s3: S3StorageDriver) {}
upload(input: UploadObjectInput): Promise<StoredObject> {
return this.s3.upload(input);
}
getBuffer(storagePath: string, storageDisk: string): Promise<Buffer> {
if (storageDisk !== 's3') {
return Promise.reject(new Error(`Unsupported storage disk: ${storageDisk}`));
}
return this.s3.getBuffer(storagePath);
}
delete(storagePath: string, storageDisk: string): Promise<void> {
if (storageDisk !== 's3') {
return Promise.resolve();
}
return this.s3.delete(storagePath);
}
}
+11
View File
@@ -0,0 +1,11 @@
export interface StoredObject {
storageDisk: string;
storagePath: string;
publicUrl: string;
}
export interface UploadObjectInput {
key: string;
body: Buffer;
contentType: string;
}