file service module added
This commit is contained in:
Generated
+26
@@ -17,7 +17,9 @@
|
|||||||
"better-auth": "^1.3.8",
|
"better-auth": "^1.3.8",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.2",
|
"class-validator": "^0.14.2",
|
||||||
|
"cloudinary": "^2.8.0",
|
||||||
"jose": "3.16.0",
|
"jose": "3.16.0",
|
||||||
|
"multer": "^2.0.2",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"rxjs": "^7.8.1"
|
"rxjs": "^7.8.1"
|
||||||
},
|
},
|
||||||
@@ -5165,6 +5167,19 @@
|
|||||||
"node": ">=0.8"
|
"node": ">=0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/cloudinary": {
|
||||||
|
"version": "2.8.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/cloudinary/-/cloudinary-2.8.0.tgz",
|
||||||
|
"integrity": "sha512-s7frvR0HnQXeJsQSIsbLa/I09IMb1lOnVLEDH5b5E53WTiCYgrNNOBGV/i/nLHwrcEOUkqjfSwP1+enXWNYmdw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"lodash": "^4.17.21",
|
||||||
|
"q": "^1.5.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=9"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/co": {
|
"node_modules/co": {
|
||||||
"version": "4.6.0",
|
"version": "4.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
|
||||||
@@ -9304,6 +9319,17 @@
|
|||||||
"node": ">=16.0.0"
|
"node": ">=16.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/q": {
|
||||||
|
"version": "1.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz",
|
||||||
|
"integrity": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==",
|
||||||
|
"deprecated": "You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other.\n\n(For a CapTP with native promises, see @endo/eventual-send and @endo/captp)",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.6.0",
|
||||||
|
"teleport": ">=0.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/qs": {
|
"node_modules/qs": {
|
||||||
"version": "6.14.0",
|
"version": "6.14.0",
|
||||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
|
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
|
||||||
|
|||||||
@@ -28,7 +28,9 @@
|
|||||||
"better-auth": "^1.3.8",
|
"better-auth": "^1.3.8",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.2",
|
"class-validator": "^0.14.2",
|
||||||
|
"cloudinary": "^2.8.0",
|
||||||
"jose": "3.16.0",
|
"jose": "3.16.0",
|
||||||
|
"multer": "^2.0.2",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"rxjs": "^7.8.1"
|
"rxjs": "^7.8.1"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "File" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"originalName" TEXT NOT NULL,
|
||||||
|
"storagePath" TEXT NOT NULL,
|
||||||
|
"folder" TEXT NOT NULL,
|
||||||
|
"version" INTEGER NOT NULL DEFAULT 1,
|
||||||
|
"mimeType" TEXT NOT NULL,
|
||||||
|
"size" INTEGER NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "File_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "File_folder_originalName_idx" ON "File"("folder", "originalName");
|
||||||
@@ -119,3 +119,17 @@ model MenuItem {
|
|||||||
|
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model File {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
originalName String
|
||||||
|
storagePath String
|
||||||
|
folder String
|
||||||
|
version Int @default(1)
|
||||||
|
mimeType String
|
||||||
|
size Int
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
@@index([folder, originalName])
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { PrismaModule } from './prisma/prisma.module';
|
|||||||
import { CategoryModule } from './category/category.module';
|
import { CategoryModule } from './category/category.module';
|
||||||
import { RestaurantModule } from './restaurant/restaurant.module';
|
import { RestaurantModule } from './restaurant/restaurant.module';
|
||||||
import { ItemModule } from './item/item.module';
|
import { ItemModule } from './item/item.module';
|
||||||
|
import { FileModule } from './file/file.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -15,6 +16,7 @@ import { ItemModule } from './item/item.module';
|
|||||||
CategoryModule,
|
CategoryModule,
|
||||||
RestaurantModule,
|
RestaurantModule,
|
||||||
ItemModule,
|
ItemModule,
|
||||||
|
FileModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule {}
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
// src/file/file.controller.ts
|
||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
Post,
|
||||||
|
Body,
|
||||||
|
UploadedFile,
|
||||||
|
UseInterceptors,
|
||||||
|
UseGuards,
|
||||||
|
Delete,
|
||||||
|
Param,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { FileInterceptor } from '@nestjs/platform-express';
|
||||||
|
import { FileService } from './file.service';
|
||||||
|
import { AuthGuard } from '@thallesp/nestjs-better-auth';
|
||||||
|
|
||||||
|
@Controller('files')
|
||||||
|
@UseGuards(AuthGuard)
|
||||||
|
export class FileController {
|
||||||
|
constructor(private readonly fileService: FileService) {}
|
||||||
|
|
||||||
|
@Post('upload')
|
||||||
|
@UseInterceptors(FileInterceptor('file'))
|
||||||
|
async upload(@UploadedFile() file: any, @Body('folder') folder: string) {
|
||||||
|
return this.fileService.uploadFile(file, folder ?? 'general');
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete(':id')
|
||||||
|
async deleteFile(@Param('id') id: string) {
|
||||||
|
return this.fileService.deleteFile(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
// src/file/file.module.ts
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { FileService } from './file.service';
|
||||||
|
import { FileController } from './file.controller';
|
||||||
|
import { PrismaService } from 'src/prisma/prisma.service';
|
||||||
|
import { LocalStorageProvider } from './storage-providers/local-storage.provider';
|
||||||
|
import { CloudinaryStorageProvider } from './storage-providers/cloudinary-storage.provider';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
controllers: [FileController],
|
||||||
|
providers: [
|
||||||
|
FileService,
|
||||||
|
PrismaService,
|
||||||
|
{
|
||||||
|
provide: 'StorageProvider',
|
||||||
|
useFactory: () => {
|
||||||
|
switch (process.env.FILE_DRIVER) {
|
||||||
|
case 'cloudinary':
|
||||||
|
return new CloudinaryStorageProvider();
|
||||||
|
default:
|
||||||
|
return new LocalStorageProvider();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class FileModule {}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
// src/file/file.service.ts
|
||||||
|
import { Injectable, Inject } from '@nestjs/common';
|
||||||
|
import { PrismaService } from 'src/prisma/prisma.service';
|
||||||
|
import type { IStorageProvider } from './storage-providers/storage-provider.interface';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class FileService {
|
||||||
|
constructor(
|
||||||
|
private prisma: PrismaService,
|
||||||
|
|
||||||
|
@Inject('StorageProvider')
|
||||||
|
private readonly storageProvider: IStorageProvider,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async uploadFile(file: any, folder: string) {
|
||||||
|
const lastVersion = await this.prisma.file.findFirst({
|
||||||
|
where: { folder, originalName: file.originalname },
|
||||||
|
orderBy: { version: 'desc' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const nextVersion = lastVersion ? lastVersion.version + 1 : 1;
|
||||||
|
|
||||||
|
const storagePath = await this.storageProvider.upload(file, folder);
|
||||||
|
|
||||||
|
return this.prisma.file.create({
|
||||||
|
data: {
|
||||||
|
originalName: file.originalname,
|
||||||
|
storagePath,
|
||||||
|
folder,
|
||||||
|
mimeType: file.mimetype,
|
||||||
|
size: file.size,
|
||||||
|
version: nextVersion,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteFile(id: string) {
|
||||||
|
const file = await this.prisma.file.findUnique({ where: { id } });
|
||||||
|
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
await this.storageProvider.delete(file.storagePath);
|
||||||
|
|
||||||
|
await this.prisma.file.delete({ where: { id } });
|
||||||
|
}
|
||||||
|
|
||||||
|
async getFileVersions(originalName: string, folder: string) {
|
||||||
|
return this.prisma.file.findMany({
|
||||||
|
where: { folder, originalName },
|
||||||
|
orderBy: { version: 'asc' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
// src/file/storage-providers/cloudinary-storage.provider.ts
|
||||||
|
import { IStorageProvider } from './storage-provider.interface';
|
||||||
|
import { v2 as cloudinary } from 'cloudinary';
|
||||||
|
|
||||||
|
cloudinary.config({
|
||||||
|
cloud_name: process.env.CLOUDINARY_CLOUD_NAME,
|
||||||
|
api_key: process.env.CLOUDINARY_API_KEY,
|
||||||
|
api_secret: process.env.CLOUDINARY_API_SECRET,
|
||||||
|
});
|
||||||
|
|
||||||
|
export class CloudinaryStorageProvider implements IStorageProvider {
|
||||||
|
async upload(file: any, folder: string): Promise<string> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
cloudinary.uploader
|
||||||
|
.upload_stream({ folder }, (err, result) => {
|
||||||
|
if (err) return reject(err);
|
||||||
|
if (!result) return reject(new Error('Result is undefined'));
|
||||||
|
resolve(result.secure_url);
|
||||||
|
})
|
||||||
|
.end(file.buffer);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(fileUrl: string): Promise<void> {
|
||||||
|
const segments = fileUrl.split('/');
|
||||||
|
const publicIdWithExt = segments.pop();
|
||||||
|
const folder = segments.pop();
|
||||||
|
|
||||||
|
if (!publicIdWithExt || !folder) return;
|
||||||
|
|
||||||
|
const publicId = `${folder}/${publicIdWithExt.split('.')[0]}`;
|
||||||
|
|
||||||
|
await cloudinary.uploader.destroy(publicId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { IStorageProvider } from './storage-provider.interface';
|
||||||
|
import { promises as fs } from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
|
export class LocalStorageProvider implements IStorageProvider {
|
||||||
|
private baseFolder = path.join(process.cwd(), 'uploads');
|
||||||
|
|
||||||
|
async upload(file: any, folder: string): Promise<string> {
|
||||||
|
const targetFolder = path.join(this.baseFolder, folder);
|
||||||
|
|
||||||
|
await fs.mkdir(targetFolder, { recursive: true });
|
||||||
|
|
||||||
|
const filePath = path.join(targetFolder, file.originalname);
|
||||||
|
await fs.writeFile(filePath, file.buffer);
|
||||||
|
|
||||||
|
return `/uploads/${folder}/${file.originalname}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(filePath: string): Promise<void> {
|
||||||
|
const fullPath = path.join(process.cwd(), filePath);
|
||||||
|
await fs.unlink(fullPath).catch(() => {});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export interface IStorageProvider {
|
||||||
|
upload(file: any, folder: string): Promise<string>;
|
||||||
|
delete(filePath: string): Promise<void>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import dotenv from 'dotenv';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
dotenv.config({ path: path.resolve(__dirname, '../../../.env') });
|
||||||
|
|
||||||
|
const requiredEnv = [
|
||||||
|
'BETTER_AUTH_SECRET',
|
||||||
|
'BETTER_AUTH_URL',
|
||||||
|
'TRUSTED_ORIGINS',
|
||||||
|
'GOOGLE_CLIENT_ID',
|
||||||
|
'GOOGLE_CLIENT_SECRET',
|
||||||
|
'FILE_DRIVER',
|
||||||
|
'DATABASE_URL',
|
||||||
|
'CLOUDINARY_CLOUD_NAME',
|
||||||
|
'CLOUDINARY_API_KEY',
|
||||||
|
'CLOUDINARY_API_SECRET',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
for (const varName of requiredEnv) {
|
||||||
|
if (!process.env[varName]) {
|
||||||
|
throw new Error(`Missing required environment variable: ${varName}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const config = {
|
||||||
|
betterAuthSecret: process.env.BETTER_AUTH_SECRET!,
|
||||||
|
betterAuthUrl: process.env.BETTER_AUTH_URL!,
|
||||||
|
trustedOrigins: process.env.TRUSTED_ORIGINS!.split(','),
|
||||||
|
googleClientId: process.env.GOOGLE_CLIENT_ID!,
|
||||||
|
googleClientSecret: process.env.GOOGLE_CLIENT_SECRET!,
|
||||||
|
fileDriver: process.env.FILE_DRIVER,
|
||||||
|
cloudinary: {
|
||||||
|
cloudName: process.env.CLOUDINARY_CLOUD_NAME!,
|
||||||
|
apiKey: process.env.CLOUDINARY_API_KEY!,
|
||||||
|
apiSecret: process.env.CLOUDINARY_API_SECRET!,
|
||||||
|
},
|
||||||
|
databaseUrl: process.env.DATABASE_URL!,
|
||||||
|
};
|
||||||
@@ -13,7 +13,7 @@ import { UserService } from './user.service';
|
|||||||
export class UserController {
|
export class UserController {
|
||||||
constructor(private readonly userService: UserService) {}
|
constructor(private readonly userService: UserService) {}
|
||||||
|
|
||||||
// @AllowAnonymous()
|
@Roles(['admin'])
|
||||||
@Get()
|
@Get()
|
||||||
async getAllUsers() {
|
async getAllUsers() {
|
||||||
return this.userService.getAllUsers();
|
return this.userService.getAllUsers();
|
||||||
@@ -25,16 +25,19 @@ export class UserController {
|
|||||||
return this.userService.getUserById(id);
|
return this.userService.getUserById(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Roles(['admin'])
|
||||||
@Delete(':id')
|
@Delete(':id')
|
||||||
async deleteUser(@Param('id') id: string) {
|
async deleteUser(@Param('id') id: string) {
|
||||||
return this.userService.deleteUser(id);
|
return this.userService.deleteUser(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Roles(['admin'])
|
||||||
@Get(':id/sessions')
|
@Get(':id/sessions')
|
||||||
async getSessionsByUser(@Param('id') id: string) {
|
async getSessionsByUser(@Param('id') id: string) {
|
||||||
return this.userService.getSessionsByUserId(id);
|
return this.userService.getSessionsByUserId(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Roles(['admin'])
|
||||||
@Delete(':id/sessions/:sessionId')
|
@Delete(':id/sessions/:sessionId')
|
||||||
async removeSession(
|
async removeSession(
|
||||||
@Param('id') userId: string,
|
@Param('id') userId: string,
|
||||||
@@ -43,6 +46,7 @@ export class UserController {
|
|||||||
return this.userService.removeSession(userId, sessionId);
|
return this.userService.removeSession(userId, sessionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Roles(['admin'])
|
||||||
@Delete(':id/sessions')
|
@Delete(':id/sessions')
|
||||||
async removeAllSessions(@Param('id') id: string) {
|
async removeAllSessions(@Param('id') id: string) {
|
||||||
return this.userService.removeAllSessions(id);
|
return this.userService.removeAllSessions(id);
|
||||||
|
|||||||
Reference in New Issue
Block a user