feat: expand backend admin marketplace and scaling
فشلت بعض الفحوصات
/ deploy (push) Failing after 1m22s

هذا الالتزام موجود في:
2026-05-14 16:17:12 +03:00
الأصل 0e76a4a9fc
التزام 5bd5e19a89
158 ملفات معدلة مع 19563 إضافات و3315 حذوفات

عرض الملف

@@ -1,12 +1,34 @@
import { Body, Controller, Delete, Get, Param, Patch, Post, Query, UseGuards } from '@nestjs/common';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import {
Body,
Controller,
Delete,
Get,
HttpCode,
HttpStatus,
Param,
Patch,
Post,
Query,
UploadedFiles,
UseGuards,
UseInterceptors,
} from '@nestjs/common';
import { FileFieldsInterceptor } from '@nestjs/platform-express';
import { ApiBearerAuth, ApiBody, ApiConsumes, ApiTags } from '@nestjs/swagger';
import { CurrentUser } from '../../common/decorators/current-user.decorator';
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
import { SuperAdminPermissionsGuard } from '../../common/guards/superadmin-permissions.guard';
import { SuperAdminJwtAuthGuard } from '../../common/guards/super-admin-jwt-auth.guard';
import { JwtPayload } from '../../common/interfaces/jwt-payload.interface';
import { SuperAdminPermissions } from '../../common/decorators/superadmin-permissions.decorator';
import { AdminPostQueryDto } from './dto/admin-post-query.dto';
import { CreateReelDto } from './dto/create-reel.dto';
import { CreatePostDto } from './dto/create-post.dto';
import { PostQueryDto } from './dto/post-query.dto';
import { ReelQueryDto } from './dto/reel-query.dto';
import { UpdatePostDto } from './dto/update-post.dto';
import { PostsService } from './posts.service';
import { SUPERADMIN_PERMISSIONS } from '../superadmin/superadmin-permissions';
@ApiTags('Posts')
@Controller('posts')
@@ -15,9 +37,58 @@ export class PostsController {
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@UseInterceptors(
FileFieldsInterceptor([
{ name: 'imageFiles', maxCount: 10 },
{ name: 'videoFile', maxCount: 1 },
{ name: 'audioFile', maxCount: 1 },
]),
)
@ApiConsumes('multipart/form-data')
@ApiBody({
schema: {
type: 'object',
properties: {
content: { type: 'string', example: 'First post #music' },
visibility: { type: 'string', enum: ['public', 'followers', 'private'] },
imageUrls: { type: 'array', items: { type: 'string' } },
taggedUserIds: { type: 'array', items: { type: 'string' } },
mentionUsernames: { type: 'array', items: { type: 'string' } },
location: { type: 'string', example: 'Riyadh, Saudi Arabia' },
latitude: { type: 'number', example: 24.7136 },
longitude: { type: 'number', example: 46.6753 },
videoUrl: { type: 'string', example: 'https://cdn.example.com/video.mp4' },
audioUrl: { type: 'string', example: 'https://cdn.example.com/audio.mp3' },
durationSeconds: { type: 'number', example: 54 },
thumbnailUrl: { type: 'string', example: 'https://cdn.example.com/cover.jpg' },
style: { type: 'string', example: 'Sharqi' },
maqam: { type: 'string', example: 'Hijaz' },
rhythmSignature: { type: 'string', example: '6/8' },
waveformPeaks: { type: 'array', items: { type: 'number' } },
imageFiles: { type: 'array', items: { type: 'string', format: 'binary' } },
videoFile: { type: 'string', format: 'binary' },
audioFile: { type: 'string', format: 'binary' },
},
},
})
@Post()
async create(@CurrentUser() user: JwtPayload, @Body() dto: CreatePostDto) {
return this.postsService.create(user.sub, dto);
async create(
@CurrentUser() user: JwtPayload,
@Body() dto: CreatePostDto,
@UploadedFiles()
files?: {
imageFiles?: Array<{ mimetype?: string; size: number; buffer: Buffer; originalname?: string }>;
videoFile?: Array<{ mimetype?: string; size: number; buffer: Buffer; originalname?: string }>;
audioFile?: Array<{ mimetype?: string; size: number; buffer: Buffer; originalname?: string }>;
},
) {
return this.postsService.create(
user.sub,
dto,
files?.imageFiles ?? [],
files?.videoFile?.[0],
files?.audioFile?.[0],
);
}
@ApiBearerAuth()
@@ -27,6 +98,58 @@ export class PostsController {
return this.postsService.findUserPosts(userId, query);
}
@ApiBearerAuth()
@UseGuards(SuperAdminJwtAuthGuard, SuperAdminPermissionsGuard)
@SuperAdminPermissions(SUPERADMIN_PERMISSIONS.CONTENT_MODERATE)
@Get('admin/moderation')
async findPlatformPosts(@Query() query: AdminPostQueryDto) {
return this.postsService.findPlatformPosts(query);
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@UseInterceptors(
FileFieldsInterceptor([
{ name: 'videoFile', maxCount: 1 },
]),
)
@ApiConsumes('multipart/form-data')
@ApiBody({
schema: {
type: 'object',
properties: {
content: { type: 'string', example: 'New reel from oud session #reel' },
visibility: { type: 'string', enum: ['public', 'followers', 'private'] },
videoUrl: { type: 'string', example: 'https://cdn.example.com/reel.mp4' },
durationSeconds: { type: 'number', example: 42 },
thumbnailUrl: { type: 'string', example: 'https://cdn.example.com/reel-cover.jpg' },
style: { type: 'string', example: 'Sharqi' },
maqam: { type: 'string', example: 'Hijaz' },
rhythmSignature: { type: 'string', example: '6/8' },
mentionUsernames: { type: 'array', items: { type: 'string' } },
videoFile: { type: 'string', format: 'binary' },
},
},
})
@Post('reels')
async createReel(
@CurrentUser() user: JwtPayload,
@Body() dto: CreateReelDto,
@UploadedFiles()
files?: {
videoFile?: Array<{ mimetype?: string; size: number; buffer: Buffer; originalname?: string }>;
},
) {
return this.postsService.createReel(user.sub, dto, files?.videoFile?.[0]);
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Get('reels')
async findReels(@Query() query: ReelQueryDto) {
return this.postsService.findReels(query);
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Get(':postId')
@@ -36,13 +159,60 @@ export class PostsController {
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@UseInterceptors(
FileFieldsInterceptor([
{ name: 'imageFiles', maxCount: 10 },
{ name: 'videoFile', maxCount: 1 },
{ name: 'audioFile', maxCount: 1 },
]),
)
@ApiConsumes('multipart/form-data')
@ApiBody({
schema: {
type: 'object',
properties: {
content: { type: 'string', example: 'Updated content' },
visibility: { type: 'string', enum: ['public', 'followers', 'private'] },
imageUrls: { type: 'array', items: { type: 'string' } },
taggedUserIds: { type: 'array', items: { type: 'string' } },
mentionUsernames: { type: 'array', items: { type: 'string' } },
location: { type: 'string', example: 'Jeddah, Saudi Arabia' },
latitude: { type: 'number', example: 21.5433 },
longitude: { type: 'number', example: 39.1728 },
videoUrl: { type: 'string', example: 'https://cdn.example.com/video.mp4' },
audioUrl: { type: 'string', example: 'https://cdn.example.com/audio.mp3' },
durationSeconds: { type: 'number', example: 54 },
thumbnailUrl: { type: 'string', example: 'https://cdn.example.com/cover.jpg' },
style: { type: 'string', example: 'Sharqi' },
maqam: { type: 'string', example: 'Hijaz' },
rhythmSignature: { type: 'string', example: '6/8' },
waveformPeaks: { type: 'array', items: { type: 'number' } },
imageFiles: { type: 'array', items: { type: 'string', format: 'binary' } },
videoFile: { type: 'string', format: 'binary' },
audioFile: { type: 'string', format: 'binary' },
},
},
})
@Patch(':postId')
async update(
@CurrentUser() user: JwtPayload,
@Param('postId') postId: string,
@Body() dto: UpdatePostDto,
@UploadedFiles()
files?: {
imageFiles?: Array<{ mimetype?: string; size: number; buffer: Buffer; originalname?: string }>;
videoFile?: Array<{ mimetype?: string; size: number; buffer: Buffer; originalname?: string }>;
audioFile?: Array<{ mimetype?: string; size: number; buffer: Buffer; originalname?: string }>;
},
) {
return this.postsService.update(user.sub, postId, dto);
return this.postsService.update(
user.sub,
postId,
dto,
files?.imageFiles ?? [],
files?.videoFile?.[0],
files?.audioFile?.[0],
);
}
@ApiBearerAuth()
@@ -52,4 +222,37 @@ export class PostsController {
await this.postsService.remove(user.sub, postId);
return { success: true };
}
@ApiBearerAuth()
@UseGuards(SuperAdminJwtAuthGuard, SuperAdminPermissionsGuard)
@SuperAdminPermissions(SUPERADMIN_PERMISSIONS.CONTENT_MODERATE)
@Delete('admin/:postId')
async removeBySuperAdmin(@CurrentUser() user: JwtPayload, @Param('postId') postId: string) {
await this.postsService.removeBySuperAdmin(user.email ?? user.sub, postId);
return { success: true };
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@HttpCode(HttpStatus.OK)
@Post(':postId/view')
async registerView(@CurrentUser() user: JwtPayload, @Param('postId') postId: string) {
return this.postsService.registerView(user.sub, postId);
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@HttpCode(HttpStatus.OK)
@Post(':postId/play')
async registerPlay(@CurrentUser() user: JwtPayload, @Param('postId') postId: string) {
return this.postsService.registerPlay(user.sub, postId);
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@HttpCode(HttpStatus.OK)
@Post(':postId/share')
async registerShare(@CurrentUser() user: JwtPayload, @Param('postId') postId: string) {
return this.postsService.registerShare(user.sub, postId);
}
}