Add partnership request API with media uploads and admin listing.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
b661877d77
commit
70c889ab4b
@@ -0,0 +1,37 @@
|
|||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "PartnershipRequestStatus" AS ENUM ('pending', 'reviewed', 'accepted', 'rejected');
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "PartnershipRequest" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"userId" TEXT NOT NULL,
|
||||||
|
"businessNameFa" TEXT NOT NULL,
|
||||||
|
"businessNameEn" TEXT NOT NULL DEFAULT '',
|
||||||
|
"categoryIds" INTEGER[],
|
||||||
|
"description" TEXT NOT NULL,
|
||||||
|
"imageUrls" TEXT[],
|
||||||
|
"cellNumber" TEXT NOT NULL,
|
||||||
|
"landline" TEXT,
|
||||||
|
"instagram" TEXT,
|
||||||
|
"city" TEXT NOT NULL,
|
||||||
|
"province" TEXT NOT NULL,
|
||||||
|
"address" TEXT NOT NULL,
|
||||||
|
"postalCode" TEXT NOT NULL,
|
||||||
|
"status" "PartnershipRequestStatus" NOT NULL DEFAULT 'pending',
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "PartnershipRequest_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "PartnershipRequest_userId_idx" ON "PartnershipRequest"("userId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "PartnershipRequest_status_idx" ON "PartnershipRequest"("status");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "PartnershipRequest_createdAt_idx" ON "PartnershipRequest"("createdAt");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "PartnershipRequest" ADD CONSTRAINT "PartnershipRequest_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
+38
-4
@@ -45,6 +45,13 @@ enum SmsOtpPurpose {
|
|||||||
storeRegister
|
storeRegister
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum PartnershipRequestStatus {
|
||||||
|
pending
|
||||||
|
reviewed
|
||||||
|
accepted
|
||||||
|
rejected
|
||||||
|
}
|
||||||
|
|
||||||
model User {
|
model User {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
firstName String
|
firstName String
|
||||||
@@ -61,10 +68,11 @@ model User {
|
|||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
refreshTokens RefreshToken[]
|
refreshTokens RefreshToken[]
|
||||||
addresses UserAddress[]
|
addresses UserAddress[]
|
||||||
ownedStore Store?
|
ownedStore Store?
|
||||||
orders Order[] @relation("CustomerOrders")
|
orders Order[] @relation("CustomerOrders")
|
||||||
|
partnershipRequests PartnershipRequest[]
|
||||||
|
|
||||||
@@index([role])
|
@@index([role])
|
||||||
@@index([disabled])
|
@@index([disabled])
|
||||||
@@ -324,6 +332,32 @@ model OrderStoreFulfillment {
|
|||||||
@@index([status])
|
@@index([status])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Producer / seller partnership applications from the storefront.
|
||||||
|
model PartnershipRequest {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
userId String
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
businessNameFa String
|
||||||
|
businessNameEn String @default("")
|
||||||
|
categoryIds Int[]
|
||||||
|
description String
|
||||||
|
imageUrls String[]
|
||||||
|
cellNumber String
|
||||||
|
landline String?
|
||||||
|
instagram String?
|
||||||
|
city String
|
||||||
|
province String
|
||||||
|
address String
|
||||||
|
postalCode String
|
||||||
|
status PartnershipRequestStatus @default(pending)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
@@index([userId])
|
||||||
|
@@index([status])
|
||||||
|
@@index([createdAt])
|
||||||
|
}
|
||||||
|
|
||||||
/// Homepage / storefront image slots (hero slides, banners, category thumbs, …)
|
/// Homepage / storefront image slots (hero slides, banners, category thumbs, …)
|
||||||
model WebsiteSlotItem {
|
model WebsiteSlotItem {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { CategoriesModule } from './categories/categories.module';
|
|||||||
import { CustomersModule } from './customers/customers.module';
|
import { CustomersModule } from './customers/customers.module';
|
||||||
import { MediaModule } from './media/media.module';
|
import { MediaModule } from './media/media.module';
|
||||||
import { OrdersModule } from './orders/orders.module';
|
import { OrdersModule } from './orders/orders.module';
|
||||||
|
import { PartnershipRequestsModule } from './partnership-requests/partnership-requests.module';
|
||||||
import { PrismaModule } from './prisma/prisma.module';
|
import { PrismaModule } from './prisma/prisma.module';
|
||||||
import { ProductsModule } from './products/products.module';
|
import { ProductsModule } from './products/products.module';
|
||||||
import { ProfileModule } from './profile/profile.module';
|
import { ProfileModule } from './profile/profile.module';
|
||||||
@@ -36,6 +37,7 @@ import { WebsiteSlotsModule } from './website-slots/website-slots.module';
|
|||||||
BlogsModule,
|
BlogsModule,
|
||||||
CustomersModule,
|
CustomersModule,
|
||||||
OrdersModule,
|
OrdersModule,
|
||||||
|
PartnershipRequestsModule,
|
||||||
ProfileModule,
|
ProfileModule,
|
||||||
WebsiteSlotsModule,
|
WebsiteSlotsModule,
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export const MEDIA_KINDS = [
|
|||||||
'store',
|
'store',
|
||||||
'avatar',
|
'avatar',
|
||||||
'website',
|
'website',
|
||||||
|
'partnership',
|
||||||
'temp',
|
'temp',
|
||||||
] as const;
|
] as const;
|
||||||
export type MediaKind = (typeof MEDIA_KINDS)[number];
|
export type MediaKind = (typeof MEDIA_KINDS)[number];
|
||||||
|
|||||||
@@ -8,5 +8,6 @@ import { MediaService } from './media.service';
|
|||||||
imports: [ConfigModule, StorageModule],
|
imports: [ConfigModule, StorageModule],
|
||||||
controllers: [MediaController],
|
controllers: [MediaController],
|
||||||
providers: [MediaService],
|
providers: [MediaService],
|
||||||
|
exports: [MediaService],
|
||||||
})
|
})
|
||||||
export class MediaModule {}
|
export class MediaModule {}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { extname } from 'path';
|
|||||||
import {
|
import {
|
||||||
blogMediaKey,
|
blogMediaKey,
|
||||||
brandMediaKey,
|
brandMediaKey,
|
||||||
|
partnershipMediaKey,
|
||||||
productGalleryKey,
|
productGalleryKey,
|
||||||
productMainKey,
|
productMainKey,
|
||||||
storeMediaKey,
|
storeMediaKey,
|
||||||
@@ -88,6 +89,8 @@ export class MediaService {
|
|||||||
return userMediaKey(fileName);
|
return userMediaKey(fileName);
|
||||||
case 'website':
|
case 'website':
|
||||||
return websiteMediaKey(fileName);
|
return websiteMediaKey(fileName);
|
||||||
|
case 'partnership':
|
||||||
|
return partnershipMediaKey(fileName);
|
||||||
case 'temp':
|
case 'temp':
|
||||||
return tempMediaKey(fileName);
|
return tempMediaKey(fileName);
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import { Type } from 'class-transformer';
|
||||||
|
import {
|
||||||
|
ArrayMaxSize,
|
||||||
|
ArrayMinSize,
|
||||||
|
IsArray,
|
||||||
|
IsInt,
|
||||||
|
IsOptional,
|
||||||
|
IsString,
|
||||||
|
MaxLength,
|
||||||
|
MinLength,
|
||||||
|
} from 'class-validator';
|
||||||
|
|
||||||
|
export class CreatePartnershipRequestDto {
|
||||||
|
@IsString()
|
||||||
|
@MinLength(2, { message: 'نام کسبوکار فارسی الزامی است' })
|
||||||
|
@MaxLength(120)
|
||||||
|
businessNameFa!: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(120)
|
||||||
|
businessNameEn?: string;
|
||||||
|
|
||||||
|
@IsArray()
|
||||||
|
@ArrayMinSize(1, { message: 'حداقل یک دستهبندی انتخاب کنید' })
|
||||||
|
@ArrayMaxSize(20)
|
||||||
|
@IsInt({ each: true })
|
||||||
|
@Type(() => Number)
|
||||||
|
categoryIds!: number[];
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@MinLength(20, { message: 'توضیحات باید حداقل ۲۰ کاراکتر باشد' })
|
||||||
|
@MaxLength(4000)
|
||||||
|
description!: string;
|
||||||
|
|
||||||
|
@IsArray()
|
||||||
|
@ArrayMaxSize(8)
|
||||||
|
@IsString({ each: true })
|
||||||
|
imageUrls!: string[];
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(20)
|
||||||
|
landline?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(80)
|
||||||
|
instagram?: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@MinLength(1, { message: 'شهر الزامی است' })
|
||||||
|
@MaxLength(80)
|
||||||
|
city!: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@MinLength(1, { message: 'استان الزامی است' })
|
||||||
|
@MaxLength(80)
|
||||||
|
province!: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@MinLength(5, { message: 'آدرس الزامی است' })
|
||||||
|
@MaxLength(500)
|
||||||
|
address!: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@MinLength(5, { message: 'کد پستی الزامی است' })
|
||||||
|
@MaxLength(20)
|
||||||
|
postalCode!: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { Type } from 'class-transformer';
|
||||||
|
import { IsInt, IsOptional, Max, Min } from 'class-validator';
|
||||||
|
|
||||||
|
export class ListPartnershipRequestsQueryDto {
|
||||||
|
@IsOptional()
|
||||||
|
@Type(() => Number)
|
||||||
|
@IsInt()
|
||||||
|
@Min(1)
|
||||||
|
page?: number = 1;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@Type(() => Number)
|
||||||
|
@IsInt()
|
||||||
|
@Min(1)
|
||||||
|
@Max(50)
|
||||||
|
limit?: number = 10;
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
Param,
|
||||||
|
Post,
|
||||||
|
Query,
|
||||||
|
UploadedFile,
|
||||||
|
UseGuards,
|
||||||
|
UseInterceptors,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { FileInterceptor } from '@nestjs/platform-express';
|
||||||
|
import { UserRole } from '@prisma/client';
|
||||||
|
import { memoryStorage } from 'multer';
|
||||||
|
import { CurrentUser } from '../auth/decorators/current-user.decorator';
|
||||||
|
import { Roles } from '../auth/decorators/roles.decorator';
|
||||||
|
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
|
||||||
|
import { RolesGuard } from '../auth/guards/roles.guard';
|
||||||
|
import type { AuthUser } from '../auth/auth.types';
|
||||||
|
import { MediaService } from '../media/media.service';
|
||||||
|
import { CreatePartnershipRequestDto } from './dto/create-partnership-request.dto';
|
||||||
|
import { ListPartnershipRequestsQueryDto } from './dto/list-partnership-requests.dto';
|
||||||
|
import { PartnershipRequestsService } from './partnership-requests.service';
|
||||||
|
|
||||||
|
@Controller('partnership-requests')
|
||||||
|
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||||
|
export class PartnershipRequestsController {
|
||||||
|
constructor(
|
||||||
|
private readonly partnershipRequests: PartnershipRequestsService,
|
||||||
|
private readonly media: MediaService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
create(
|
||||||
|
@CurrentUser() user: AuthUser,
|
||||||
|
@Body() dto: CreatePartnershipRequestDto,
|
||||||
|
) {
|
||||||
|
return this.partnershipRequests.create(user, dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('images')
|
||||||
|
@UseInterceptors(
|
||||||
|
FileInterceptor('file', {
|
||||||
|
storage: memoryStorage(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
uploadImage(@UploadedFile() file: Express.Multer.File) {
|
||||||
|
return this.media.upload('partnership', file);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@Roles(UserRole.STORE_ADMIN, UserRole.SUPER_ADMIN)
|
||||||
|
list(
|
||||||
|
@CurrentUser() user: AuthUser,
|
||||||
|
@Query() query: ListPartnershipRequestsQueryDto,
|
||||||
|
) {
|
||||||
|
return this.partnershipRequests.list(user, query);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(':id')
|
||||||
|
@Roles(UserRole.STORE_ADMIN, UserRole.SUPER_ADMIN)
|
||||||
|
findOne(@CurrentUser() user: AuthUser, @Param('id') id: string) {
|
||||||
|
return this.partnershipRequests.findOne(user, id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { MediaModule } from '../media/media.module';
|
||||||
|
import { PartnershipRequestsController } from './partnership-requests.controller';
|
||||||
|
import { PartnershipRequestsService } from './partnership-requests.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [MediaModule],
|
||||||
|
controllers: [PartnershipRequestsController],
|
||||||
|
providers: [PartnershipRequestsService],
|
||||||
|
})
|
||||||
|
export class PartnershipRequestsModule {}
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
import {
|
||||||
|
BadRequestException,
|
||||||
|
Injectable,
|
||||||
|
NotFoundException,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { UserRole } from '@prisma/client';
|
||||||
|
import type { AuthUser } from '../auth/auth.types';
|
||||||
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
|
import { CreatePartnershipRequestDto } from './dto/create-partnership-request.dto';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PartnershipRequestsService {
|
||||||
|
constructor(private readonly prisma: PrismaService) {}
|
||||||
|
|
||||||
|
async create(actor: AuthUser, dto: CreatePartnershipRequestDto) {
|
||||||
|
const user = await this.prisma.user.findUnique({
|
||||||
|
where: { id: actor.id },
|
||||||
|
select: { id: true, cellNumber: true, disabled: true },
|
||||||
|
});
|
||||||
|
if (!user || user.disabled) {
|
||||||
|
throw new NotFoundException('کاربر یافت نشد');
|
||||||
|
}
|
||||||
|
|
||||||
|
const uniqueCategoryIds = [...new Set(dto.categoryIds)];
|
||||||
|
if (!uniqueCategoryIds.length) {
|
||||||
|
throw new BadRequestException('حداقل یک دستهبندی انتخاب کنید');
|
||||||
|
}
|
||||||
|
|
||||||
|
const categories = await this.prisma.category.findMany({
|
||||||
|
where: { id: { in: uniqueCategoryIds } },
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
if (categories.length !== uniqueCategoryIds.length) {
|
||||||
|
throw new BadRequestException('یکی از دستهبندیها نامعتبر است');
|
||||||
|
}
|
||||||
|
|
||||||
|
const request = await this.prisma.partnershipRequest.create({
|
||||||
|
data: {
|
||||||
|
userId: user.id,
|
||||||
|
businessNameFa: dto.businessNameFa.trim(),
|
||||||
|
businessNameEn: (dto.businessNameEn ?? '').trim(),
|
||||||
|
categoryIds: uniqueCategoryIds,
|
||||||
|
description: dto.description.trim(),
|
||||||
|
imageUrls: dto.imageUrls,
|
||||||
|
cellNumber: user.cellNumber,
|
||||||
|
landline: dto.landline?.trim() || null,
|
||||||
|
instagram: dto.instagram?.trim() || null,
|
||||||
|
city: dto.city.trim(),
|
||||||
|
province: dto.province.trim(),
|
||||||
|
address: dto.address.trim(),
|
||||||
|
postalCode: dto.postalCode.trim(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
async list(
|
||||||
|
actor: AuthUser,
|
||||||
|
query: { page?: number; limit?: number } = {},
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
actor.role !== UserRole.STORE_ADMIN &&
|
||||||
|
actor.role !== UserRole.SUPER_ADMIN
|
||||||
|
) {
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
const page = Math.max(1, query.page ?? 1);
|
||||||
|
const limit = Math.min(50, Math.max(1, query.limit ?? 10));
|
||||||
|
const skip = (page - 1) * limit;
|
||||||
|
|
||||||
|
const [total, items] = await Promise.all([
|
||||||
|
this.prisma.partnershipRequest.count(),
|
||||||
|
this.prisma.partnershipRequest.findMany({
|
||||||
|
orderBy: { createdAt: 'desc' },
|
||||||
|
skip,
|
||||||
|
take: limit,
|
||||||
|
include: {
|
||||||
|
user: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
firstName: true,
|
||||||
|
lastName: true,
|
||||||
|
cellNumber: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const categoryIds = [...new Set(items.flatMap((item) => item.categoryIds))];
|
||||||
|
const categories = categoryIds.length
|
||||||
|
? await this.prisma.category.findMany({
|
||||||
|
where: { id: { in: categoryIds } },
|
||||||
|
select: { id: true, nameFa: true, nameEn: true },
|
||||||
|
})
|
||||||
|
: [];
|
||||||
|
const categoryMap = new Map(categories.map((c) => [c.id, c]));
|
||||||
|
|
||||||
|
return {
|
||||||
|
items: items.map((item) => ({
|
||||||
|
...item,
|
||||||
|
categories: item.categoryIds
|
||||||
|
.map((id) => categoryMap.get(id))
|
||||||
|
.filter(Boolean),
|
||||||
|
})),
|
||||||
|
total,
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
totalPages: Math.max(1, Math.ceil(total / limit) || 1),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async findOne(actor: AuthUser, id: string) {
|
||||||
|
if (
|
||||||
|
actor.role !== UserRole.STORE_ADMIN &&
|
||||||
|
actor.role !== UserRole.SUPER_ADMIN
|
||||||
|
) {
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
const item = await this.prisma.partnershipRequest.findUnique({
|
||||||
|
where: { id },
|
||||||
|
include: {
|
||||||
|
user: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
firstName: true,
|
||||||
|
lastName: true,
|
||||||
|
cellNumber: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!item) throw new NotFoundException('درخواست یافت نشد');
|
||||||
|
|
||||||
|
const categories = item.categoryIds.length
|
||||||
|
? await this.prisma.category.findMany({
|
||||||
|
where: { id: { in: item.categoryIds } },
|
||||||
|
select: { id: true, nameFa: true, nameEn: true },
|
||||||
|
})
|
||||||
|
: [];
|
||||||
|
const categoryMap = new Map(categories.map((c) => [c.id, c]));
|
||||||
|
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
categories: item.categoryIds
|
||||||
|
.map((cid) => categoryMap.get(cid))
|
||||||
|
.filter(Boolean),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
* brands/{uuid}{ext}
|
* brands/{uuid}{ext}
|
||||||
* users/{uuid}{ext}
|
* users/{uuid}{ext}
|
||||||
* stores/{uuid}{ext}
|
* stores/{uuid}{ext}
|
||||||
|
* partnership/{uuid}{ext}
|
||||||
* temp/{uuid}{ext}
|
* temp/{uuid}{ext}
|
||||||
*/
|
*/
|
||||||
const ROOT = 'mana-collection';
|
const ROOT = 'mana-collection';
|
||||||
@@ -40,6 +41,10 @@ export function websiteMediaKey(fileName: string): string {
|
|||||||
return `${ROOT}/website/${fileName}`;
|
return `${ROOT}/website/${fileName}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function partnershipMediaKey(fileName: string): string {
|
||||||
|
return `${ROOT}/partnership/${fileName}`;
|
||||||
|
}
|
||||||
|
|
||||||
export function tempMediaKey(fileName: string): string {
|
export function tempMediaKey(fileName: string): string {
|
||||||
return `${ROOT}/temp/${fileName}`;
|
return `${ROOT}/temp/${fileName}`;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user