diff --git a/.antigravityignore b/.antigravityignore new file mode 100644 index 0000000..3a483d4 --- /dev/null +++ b/.antigravityignore @@ -0,0 +1,13 @@ +node_modules/ +server/node_modules/ + +# Bỏ qua cấu hình hệ thống và Git +.git/ +.idea/ +.vscode/ + +# Bỏ qua các file log và build +*.log +dist/ +build/ +out/ \ No newline at end of file diff --git a/backend/prisma/migrations/20260622012027_add_photo_flagging/migration.sql b/backend/prisma/migrations/20260622012027_add_photo_flagging/migration.sql new file mode 100644 index 0000000..7d21a22 --- /dev/null +++ b/backend/prisma/migrations/20260622012027_add_photo_flagging/migration.sql @@ -0,0 +1,4 @@ +-- AlterTable +ALTER TABLE "Photo" ADD COLUMN "flaggedAt" TIMESTAMP(3), +ADD COLUMN "flaggedReason" TEXT, +ADD COLUMN "isFlagged" BOOLEAN NOT NULL DEFAULT false; diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index d730a24..9996756 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -209,6 +209,9 @@ model Photo { capturedAt DateTime @default(now()) metadata Json? privacy PrivacyLevel @default(TOUR_ONLY) + isFlagged Boolean @default(false) + flaggedReason String? + flaggedAt DateTime? tour Tour? @relation(fields: [tourId], references: [id], onDelete: SetNull) location Location? @relation(fields: [locationId], references: [id], onDelete: SetNull) diff --git a/backend/src/auth/jwt-auth.guard.ts b/backend/src/auth/jwt-auth.guard.ts index 18588a5..51a6517 100644 --- a/backend/src/auth/jwt-auth.guard.ts +++ b/backend/src/auth/jwt-auth.guard.ts @@ -1,5 +1,23 @@ import { Injectable } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; +import { UnauthorizedException } from '@nestjs/common'; @Injectable() -export class JwtAuthGuard extends AuthGuard('jwt') {} \ No newline at end of file +export class JwtAuthGuard extends AuthGuard('jwt') {} + +// Guard that rejects anonymous/guest users - used for dashboard and sensitive endpoints +@Injectable() +export class JwtAuthGuardNoAnonymous extends AuthGuard('jwt') { + handleRequest(err: any, user: any, info: any) { + if (err || !user) { + throw err || new UnauthorizedException('Phiên làm việc không hợp lệ hoặc đã hết hạn'); + } + + // Reject anonymous/temporary users + if (user.isAnonymous) { + throw new UnauthorizedException('Tài khoản khách không có quyền truy cập tính năng này. Vui lòng đăng ký tài khoản để tiếp tục.'); + } + + return user; + } +} \ No newline at end of file diff --git a/backend/src/auth/jwt.strategy.ts b/backend/src/auth/jwt.strategy.ts index b5ea96a..e5b2864 100644 --- a/backend/src/auth/jwt.strategy.ts +++ b/backend/src/auth/jwt.strategy.ts @@ -22,6 +22,8 @@ export class JwtStrategy extends PassportStrategy(Strategy) { throw new UnauthorizedException('Người dùng không tồn tại hoặc phiên làm việc hết hạn'); } + // Note: We allow anonymous users to pass JWT validation + // Individual endpoints decide whether to accept anonymous users based on their guard return user; } } \ No newline at end of file diff --git a/backend/src/main.ts b/backend/src/main.ts index ad585fc..d7c3781 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -23,6 +23,7 @@ import { AdminGuard } from './auth/admin.guard'; import * as nodemailer from 'nodemailer'; import { JwtModule, JwtService } from '@nestjs/jwt'; import { JwtAuthGuard } from './auth/jwt-auth.guard'; +import { JwtAuthGuardNoAnonymous } from './auth/jwt-auth.guard'; import { JwtStrategy } from './auth/jwt.strategy'; import { Reflector } from '@nestjs/core'; import { SetMetadata, CanActivate, ExecutionContext } from '@nestjs/common'; @@ -787,6 +788,37 @@ class TourController { } } }); + + // Auto-create default note template for new tour + const noteContent = `
Start Date: ${startDate ? new Date(startDate).toLocaleDateString() : 'TBD'}
+End Date: ${endDate ? new Date(endDate).toLocaleDateString() : 'TBD'}
+Adult Participants: ${adultCount || 1}
+Child Participants: ${childCount || 0}
+Add your planning notes here...
`; + + try { + await this.prisma.tourNote.create({ + data: { + tourId: tour.id, + userId: req.user.id, + title: `[${filteredTitle}] - Initial Planning`, + content: noteContent + } + }); + } catch (err) { + console.warn(`Failed to auto-create note for tour ${tour.id}:`, err.message); + } + await this.cacheManager.del(`/api/v1/tours/explore`); return tour; } @@ -1117,7 +1149,7 @@ class TourController { } @UseInterceptors(CompressCacheInterceptor) // Áp dụng Interceptor nén và cache - @UseGuards(JwtAuthGuard) + @UseGuards(JwtAuthGuardNoAnonymous) @Get('explore') async getPublicTours(@Req() req: any) { // Trả về tất cả các tour trong hệ thống để hiển thị trên bản đồ cộng đồng @@ -1596,18 +1628,42 @@ class TourController { } } + // Ensure /uploads/tours/ directory exists + const uploadsDir = path.dirname(displayFilePath); + if (!fs.existsSync(uploadsDir)) { + try { + fs.mkdirSync(uploadsDir, { recursive: true }); + console.log(`[Photo] Created directory: ${uploadsDir}`); + } catch (e) { + console.error(`[Photo] Failed to create directory ${uploadsDir}:`, e); + throw new BadRequestException(`Lỗi tạo thư mục: ${e.message}`); + } + } + // Sử dụng Sharp để resize và tối ưu dung lượng ảnh - await sharp(processBuffer) - .rotate() - .resize(2560, 2560, { - fit: 'inside', // Giữ nguyên tỷ lệ, không vượt quá khung 2K - withoutEnlargement: true // Nếu ảnh nhỏ hơn 2K thì giữ nguyên, không làm vỡ ảnh - }) - .jpeg({ quality: 85 }) // Tối ưu chất lượng/dung lượng - .toFile(displayFilePath); + try { + await sharp(processBuffer) + .rotate() + .resize(2560, 2560, { + fit: 'inside', // Giữ nguyên tỷ lệ, không vượt quá khung 2K + withoutEnlargement: true // Nếu ảnh nhỏ hơn 2K thì giữ nguyên, không làm vỡ ảnh + }) + .jpeg({ quality: 85 }) // Tối ưu chất lượng/dung lượng + .toFile(displayFilePath); + console.log(`[Photo] Display image saved successfully: ${displayFilePath}`); + } catch (e) { + console.error(`[Photo] Failed to save display image to ${displayFilePath}:`, e); + throw new BadRequestException(`Lỗi lưu hình ảnh: ${e.message}`); + } + + // Verify file was created + if (!fs.existsSync(displayFilePath)) { + console.error(`[Photo] File verification failed at ${displayFilePath}`); + throw new BadRequestException('Lỗi: Hình ảnh không được lưu thành công. Vui lòng kiểm tra quyền lưu trữ.'); + } // 6. Lưu thông tin vào Database (Lưu cả 2 đường dẫn và metadata GPS) - return this.prisma.photo.create({ + const photoRecord = await this.prisma.photo.create({ data: { tourId: tourId, uploaderId: uploaderId, @@ -1620,6 +1676,8 @@ class TourController { } } }); + console.log(`[Photo] Database record created: ID=${photoRecord.id}, imageUrl=${photoRecord.imageUrl}`); + return photoRecord; })); } @@ -1680,20 +1738,39 @@ class TourController { @Post('join-by-token') async joinByToken(@Body() body: { token: string }, @Req() req: any) { const { token } = body; + console.log('[joinByToken] Request received, token length:', token ? token.length : 0); + if (!token) { + console.error('[joinByToken] No token provided'); throw new BadRequestException('Vui lòng cung cấp token lời mời.'); } + console.log('[joinByToken] Searching for invitation with token:', token.substring(0, 20) + '...'); const invitation = await this.prisma.tourInvitation.findUnique({ where: { token }, - include: { tour: { select: { title: true } } } + include: { tour: { select: { id: true, title: true } } } }); if (!invitation) { + console.error('[joinByToken] Invitation not found for token:', token.substring(0, 20) + '...'); + // Check how many invitations exist in database for debugging + const totalInvitations = await this.prisma.tourInvitation.count(); + console.log('[joinByToken] Total invitations in database:', totalInvitations); throw new NotFoundException('Lời mời không hợp lệ hoặc đã bị hủy.'); } + // Verify the invitation email matches the logged-in user's email + const userEmail = req.user.email; + console.log('[joinByToken] Checking email - User:', userEmail, 'Invitation:', invitation.email); + if (invitation.email && userEmail && invitation.email !== userEmail) { + console.error('[joinByToken] Email mismatch - User email:', userEmail, 'Invitation email:', invitation.email); + throw new BadRequestException('Lời mời này được gửi tới một địa chỉ email khác. Vui lòng đăng nhập bằng tài khoản đúng.'); + } + + console.log('[joinByToken] Invitation found, tourId:', invitation.tourId, 'expired:', invitation.expiredAt < new Date()); + if (invitation.expiredAt < new Date()) { + console.error('[joinByToken] Invitation expired at:', invitation.expiredAt); try { await this.prisma.tourInvitation.delete({ where: { id: invitation.id } }); } catch (e) {} @@ -1701,12 +1778,14 @@ class TourController { } const userId = req.user.id; + console.log('[joinByToken] User attempting to join - userId:', userId, 'tourId:', invitation.tourId); const existingParticipation = await this.prisma.tourParticipant.findUnique({ where: { tourId_userId: { tourId: invitation.tourId, userId } } }); if (existingParticipation) { + console.log('[joinByToken] User is already a participant'); try { await this.prisma.tourInvitation.delete({ where: { id: invitation.id } }); } catch (e) {} @@ -1743,6 +1822,7 @@ class TourController { }) ]); + console.log('[joinByToken] Successfully joined tour:', invitation.tourId, 'userId:', userId); return { success: true, tourId: invitation.tourId, message: `Bạn đã gia nhập hành trình "${invitation.tour.title}"!`, shouldRefresh: true }; } @@ -2112,13 +2192,17 @@ class PhotoController { const displayFilename = `${uniqueSuffix}.jpg`; const memberOriginalDir = path.join(UPLOAD_ROOT, 'members', uploaderId, 'originals'); - const displayFilePath = path.join(UPLOAD_ROOT, 'tours', displayFilename); + const tourDisplayPath = path.join(UPLOAD_ROOT, 'tours'); + const displayFilePath = path.join(tourDisplayPath, displayFilename); const originalFilePath = path.join(memberOriginalDir, originalFilename); - // Đảm bảo thư mục lưu trữ ảnh gốc tồn tại + // Đảm bảo thư mục lưu trữ ảnh gốc và ảnh hiển thị tồn tại if (!fs.existsSync(memberOriginalDir)) { fs.mkdirSync(memberOriginalDir, { recursive: true }); } + if (!fs.existsSync(tourDisplayPath)) { + fs.mkdirSync(tourDisplayPath, { recursive: true }); + } // 1. Lưu ảnh gốc nguyên bản vào thư mục riêng của khách await fs.promises.writeFile(originalFilePath, file.buffer); @@ -3014,6 +3098,33 @@ class PublicPhotoController { res.type('text/html').send(htmlContent); } + + @UseGuards(JwtAuthGuard) + @Post(':photoId/flag') + async flagPhoto( + @Param('photoId', ParseUUIDPipe) photoId: string, + @Body() body: { reason: string }, + @Req() req: any + ) { + const photo = await this.prisma.photo.findUnique({ + where: { id: photoId } + }); + if (!photo) { + throw new NotFoundException('Ảnh không tồn tại'); + } + + // Update photo as flagged + const updatedPhoto = await this.prisma.photo.update({ + where: { id: photoId }, + data: { + isFlagged: true, + flaggedReason: body.reason || 'Không xác định', + flaggedAt: new Date() + } + }); + + return { success: true, message: 'Đã báo cáo ảnh thành công', photo: updatedPhoto }; + } } @UseGuards(JwtAuthGuard) @@ -3536,6 +3647,67 @@ class AdminModerationController { } } +@Controller('admin/photos') +@UseGuards(JwtAuthGuard, AdminGuard) +class AdminPhotosController { + constructor(private prisma: PrismaService) {} + + @Get('flagged') + async getFlaggedPhotos() { + return this.prisma.photo.findMany({ + where: { isFlagged: true }, + include: { + uploader: { select: { id: true, name: true, email: true } }, + tour: { select: { id: true, title: true } } + }, + orderBy: { flaggedAt: 'desc' } + }); + } + + @Post(':photoId/approve') + async approvePhoto(@Param('photoId', ParseUUIDPipe) photoId: string) { + const photo = await this.prisma.photo.findUnique({ + where: { id: photoId } + }); + if (!photo) { + throw new NotFoundException('Ảnh không tồn tại'); + } + + // Unflag the photo + const updatedPhoto = await this.prisma.photo.update({ + where: { id: photoId }, + data: { + isFlagged: false, + flaggedReason: null, + flaggedAt: null + } + }); + + return { success: true, message: 'Đã duyệt ảnh thành công', photo: updatedPhoto }; + } + + @Delete(':photoId') + async deletePhoto(@Param('photoId', ParseUUIDPipe) photoId: string) { + const photo = await this.prisma.photo.findUnique({ + where: { id: photoId } + }); + if (!photo) { + throw new NotFoundException('Ảnh không tồn tại'); + } + + // Soft delete the photo + const deletedPhoto = await this.prisma.photo.update({ + where: { id: photoId }, + data: { + isDeleted: true, + deletedAt: new Date() + } + }); + + return { success: true, message: 'Đã xóa ảnh thành công', photo: deletedPhoto }; + } +} + @Controller('reports') class ReportsController { constructor(private prisma: PrismaService) {} @@ -4141,54 +4313,105 @@ class AdminTrashController { @Post('delete-permanent') async deletePermanentItems(@Body() body: { type: 'tour' | 'photo' | 'note'; ids: string[] }) { const { type, ids } = body; + console.log('[deletePermanent] Request received - type:', type, 'ids count:', ids.length); + if (!type || !ids || !Array.isArray(ids)) { throw new BadRequestException('Tham số không hợp lệ.'); } if (type === 'tour') { for (const tourId of ids) { + console.log('[deletePermanent] Deleting tour:', tourId); const photos = await this.prisma.photo.findMany({ where: { tourId } }); + console.log('[deletePermanent] Found', photos.length, 'photos for tour'); + for (const p of photos) { if (p.imageUrl) { const displayFilePath = path.join(process.cwd(), p.imageUrl.replace(/^\//, '')); + console.log('[deletePermanent] Checking display file:', displayFilePath); if (fs.existsSync(displayFilePath)) { - try { fs.unlinkSync(displayFilePath); } catch (e) {} + try { + fs.unlinkSync(displayFilePath); + console.log('[deletePermanent] Deleted display file:', displayFilePath); + } catch (e) { + console.error('[deletePermanent] Error deleting display file:', e); + } + } else { + console.log('[deletePermanent] Display file not found (OK):', displayFilePath); } } if (p.originalUrl) { const originalFilePath = path.join(process.cwd(), p.originalUrl.replace(/^\//, '')); + console.log('[deletePermanent] Checking original file:', originalFilePath); if (fs.existsSync(originalFilePath)) { - try { fs.unlinkSync(originalFilePath); } catch (e) {} + try { + fs.unlinkSync(originalFilePath); + console.log('[deletePermanent] Deleted original file:', originalFilePath); + } catch (e) { + console.error('[deletePermanent] Error deleting original file:', e); + } + } else { + console.log('[deletePermanent] Original file not found (OK):', originalFilePath); } } } await this.prisma.tour.delete({ where: { id: tourId } }); + console.log('[deletePermanent] Deleted tour from database:', tourId); } } else if (type === 'photo') { + console.log('[deletePermanent] Deleting', ids.length, 'photos'); for (const photoId of ids) { + console.log('[deletePermanent] Processing photo:', photoId); const photo = await this.prisma.photo.findUnique({ where: { id: photoId } }); - if (photo) { - if (photo.imageUrl) { - const displayFilePath = path.join(process.cwd(), photo.imageUrl.replace(/^\//, '')); - if (fs.existsSync(displayFilePath)) { - try { fs.unlinkSync(displayFilePath); } catch (e) {} - } - } - if (photo.originalUrl) { - const originalFilePath = path.join(process.cwd(), photo.originalUrl.replace(/^\//, '')); - if (fs.existsSync(originalFilePath)) { - try { fs.unlinkSync(originalFilePath); } catch (e) {} - } - } - await this.prisma.photo.delete({ where: { id: photoId } }); + if (!photo) { + console.warn('[deletePermanent] Photo not found:', photoId); + continue; } + + console.log('[deletePermanent] Photo found, imageUrl:', photo.imageUrl); + + if (photo.imageUrl) { + const displayFilePath = path.join(process.cwd(), photo.imageUrl.replace(/^\//, '')); + console.log('[deletePermanent] Checking display file:', displayFilePath); + if (fs.existsSync(displayFilePath)) { + try { + fs.unlinkSync(displayFilePath); + console.log('[deletePermanent] Deleted display file'); + } catch (e) { + console.error('[deletePermanent] Error deleting display file:', e); + } + } else { + console.log('[deletePermanent] Display file not found (OK)'); + } + } + + if (photo.originalUrl) { + const originalFilePath = path.join(process.cwd(), photo.originalUrl.replace(/^\//, '')); + console.log('[deletePermanent] Checking original file:', originalFilePath); + if (fs.existsSync(originalFilePath)) { + try { + fs.unlinkSync(originalFilePath); + console.log('[deletePermanent] Deleted original file'); + } catch (e) { + console.error('[deletePermanent] Error deleting original file:', e); + } + } else { + console.log('[deletePermanent] Original file not found (OK)'); + } + } + + const result = await this.prisma.photo.delete({ where: { id: photoId } }); + console.log('[deletePermanent] Deleted photo from database:', photoId); } } else if (type === 'note') { - await this.prisma.tourNote.deleteMany({ + console.log('[deletePermanent] Deleting', ids.length, 'notes'); + const result = await this.prisma.tourNote.deleteMany({ where: { id: { in: ids } } }); + console.log('[deletePermanent] Deleted notes from database, count:', result.count); } + console.log('[deletePermanent] Deletion complete, type:', type); return { success: true }; } } diff --git a/backend/uploads/members/43265bbe-797d-424f-862c-eb61e9c2fe01/originals/1782099792835-465444865.jpg b/backend/uploads/members/43265bbe-797d-424f-862c-eb61e9c2fe01/originals/1782099792835-465444865.jpg new file mode 100644 index 0000000..d308ce5 Binary files /dev/null and b/backend/uploads/members/43265bbe-797d-424f-862c-eb61e9c2fe01/originals/1782099792835-465444865.jpg differ diff --git a/backend/uploads/members/43265bbe-797d-424f-862c-eb61e9c2fe01/originals/1782100326127-171966975.jpg b/backend/uploads/members/43265bbe-797d-424f-862c-eb61e9c2fe01/originals/1782100326127-171966975.jpg new file mode 100644 index 0000000..6e2966e Binary files /dev/null and b/backend/uploads/members/43265bbe-797d-424f-862c-eb61e9c2fe01/originals/1782100326127-171966975.jpg differ diff --git a/backend/uploads/members/43265bbe-797d-424f-862c-eb61e9c2fe01/originals/1782100590684-581575174.jpg b/backend/uploads/members/43265bbe-797d-424f-862c-eb61e9c2fe01/originals/1782100590684-581575174.jpg new file mode 100644 index 0000000..8dd15c6 Binary files /dev/null and b/backend/uploads/members/43265bbe-797d-424f-862c-eb61e9c2fe01/originals/1782100590684-581575174.jpg differ diff --git a/backend/uploads/members/b782b58c-28ed-4725-8110-2e5914be42ef/originals/1782097958694-34402464.jpg b/backend/uploads/members/b782b58c-28ed-4725-8110-2e5914be42ef/originals/1782097958694-34402464.jpg new file mode 100644 index 0000000..202c0ab Binary files /dev/null and b/backend/uploads/members/b782b58c-28ed-4725-8110-2e5914be42ef/originals/1782097958694-34402464.jpg differ diff --git a/backend/uploads/members/b782b58c-28ed-4725-8110-2e5914be42ef/originals/1782098955193-504499361.jpg b/backend/uploads/members/b782b58c-28ed-4725-8110-2e5914be42ef/originals/1782098955193-504499361.jpg new file mode 100644 index 0000000..731ff0d Binary files /dev/null and b/backend/uploads/members/b782b58c-28ed-4725-8110-2e5914be42ef/originals/1782098955193-504499361.jpg differ diff --git a/backend/uploads/members/b782b58c-28ed-4725-8110-2e5914be42ef/originals/1782099003772-651088143.jpg b/backend/uploads/members/b782b58c-28ed-4725-8110-2e5914be42ef/originals/1782099003772-651088143.jpg new file mode 100644 index 0000000..e02da56 Binary files /dev/null and b/backend/uploads/members/b782b58c-28ed-4725-8110-2e5914be42ef/originals/1782099003772-651088143.jpg differ diff --git a/backend/uploads/members/b782b58c-28ed-4725-8110-2e5914be42ef/originals/1782099284022-598478079.jpg b/backend/uploads/members/b782b58c-28ed-4725-8110-2e5914be42ef/originals/1782099284022-598478079.jpg new file mode 100644 index 0000000..930dcc8 Binary files /dev/null and b/backend/uploads/members/b782b58c-28ed-4725-8110-2e5914be42ef/originals/1782099284022-598478079.jpg differ diff --git a/backend/uploads/members/b782b58c-28ed-4725-8110-2e5914be42ef/originals/1782099558534-819985170.jpg b/backend/uploads/members/b782b58c-28ed-4725-8110-2e5914be42ef/originals/1782099558534-819985170.jpg new file mode 100644 index 0000000..12d13e8 Binary files /dev/null and b/backend/uploads/members/b782b58c-28ed-4725-8110-2e5914be42ef/originals/1782099558534-819985170.jpg differ diff --git a/backend/uploads/members/c6828597-682c-4676-8375-523a274d3c7b/originals/1782096253531-758980589.jpg b/backend/uploads/members/c6828597-682c-4676-8375-523a274d3c7b/originals/1782096253531-758980589.jpg new file mode 100644 index 0000000..5b9e042 Binary files /dev/null and b/backend/uploads/members/c6828597-682c-4676-8375-523a274d3c7b/originals/1782096253531-758980589.jpg differ diff --git a/backend/uploads/members/c6828597-682c-4676-8375-523a274d3c7b/originals/1782097908001-770537095.jpg b/backend/uploads/members/c6828597-682c-4676-8375-523a274d3c7b/originals/1782097908001-770537095.jpg new file mode 100644 index 0000000..2785cd9 Binary files /dev/null and b/backend/uploads/members/c6828597-682c-4676-8375-523a274d3c7b/originals/1782097908001-770537095.jpg differ diff --git a/backend/uploads/members/c6828597-682c-4676-8375-523a274d3c7b/originals/1782098598511-366218057.jpg b/backend/uploads/members/c6828597-682c-4676-8375-523a274d3c7b/originals/1782098598511-366218057.jpg new file mode 100644 index 0000000..6b37b09 Binary files /dev/null and b/backend/uploads/members/c6828597-682c-4676-8375-523a274d3c7b/originals/1782098598511-366218057.jpg differ diff --git a/backend/uploads/members/c6828597-682c-4676-8375-523a274d3c7b/originals/1782099024955-859396932.jpg b/backend/uploads/members/c6828597-682c-4676-8375-523a274d3c7b/originals/1782099024955-859396932.jpg new file mode 100644 index 0000000..d909115 Binary files /dev/null and b/backend/uploads/members/c6828597-682c-4676-8375-523a274d3c7b/originals/1782099024955-859396932.jpg differ diff --git a/backend/uploads/members/c6828597-682c-4676-8375-523a274d3c7b/originals/1782099305251-634989764.jpg b/backend/uploads/members/c6828597-682c-4676-8375-523a274d3c7b/originals/1782099305251-634989764.jpg new file mode 100644 index 0000000..d8ccaf4 Binary files /dev/null and b/backend/uploads/members/c6828597-682c-4676-8375-523a274d3c7b/originals/1782099305251-634989764.jpg differ diff --git a/backend/uploads/members/c6828597-682c-4676-8375-523a274d3c7b/originals/1782099673822-87952177.jpg b/backend/uploads/members/c6828597-682c-4676-8375-523a274d3c7b/originals/1782099673822-87952177.jpg new file mode 100644 index 0000000..14c7a3d Binary files /dev/null and b/backend/uploads/members/c6828597-682c-4676-8375-523a274d3c7b/originals/1782099673822-87952177.jpg differ diff --git a/backend/uploads/members/c6828597-682c-4676-8375-523a274d3c7b/originals/1782100420433-736686613.jpg b/backend/uploads/members/c6828597-682c-4676-8375-523a274d3c7b/originals/1782100420433-736686613.jpg new file mode 100644 index 0000000..4a888f8 Binary files /dev/null and b/backend/uploads/members/c6828597-682c-4676-8375-523a274d3c7b/originals/1782100420433-736686613.jpg differ diff --git a/backend/uploads/members/c6828597-682c-4676-8375-523a274d3c7b/originals/1782101005582-952029411.jpg b/backend/uploads/members/c6828597-682c-4676-8375-523a274d3c7b/originals/1782101005582-952029411.jpg new file mode 100644 index 0000000..6fdf28e Binary files /dev/null and b/backend/uploads/members/c6828597-682c-4676-8375-523a274d3c7b/originals/1782101005582-952029411.jpg differ diff --git a/backend/uploads/members/cdac7a4e-fd1b-4765-adae-e1273871f461/originals/1782100955639-135095862.jpg b/backend/uploads/members/cdac7a4e-fd1b-4765-adae-e1273871f461/originals/1782100955639-135095862.jpg new file mode 100644 index 0000000..2f3794b Binary files /dev/null and b/backend/uploads/members/cdac7a4e-fd1b-4765-adae-e1273871f461/originals/1782100955639-135095862.jpg differ diff --git a/backend/uploads/members/cdac7a4e-fd1b-4765-adae-e1273871f461/originals/1782101476154-635865207.jpg b/backend/uploads/members/cdac7a4e-fd1b-4765-adae-e1273871f461/originals/1782101476154-635865207.jpg new file mode 100644 index 0000000..175bf6e Binary files /dev/null and b/backend/uploads/members/cdac7a4e-fd1b-4765-adae-e1273871f461/originals/1782101476154-635865207.jpg differ diff --git a/backend/uploads/tours/1782096253531-758980589.jpg b/backend/uploads/tours/1782096253531-758980589.jpg new file mode 100644 index 0000000..b084436 Binary files /dev/null and b/backend/uploads/tours/1782096253531-758980589.jpg differ diff --git a/backend/uploads/tours/1782097908001-770537095.jpg b/backend/uploads/tours/1782097908001-770537095.jpg new file mode 100644 index 0000000..ffc2b9c Binary files /dev/null and b/backend/uploads/tours/1782097908001-770537095.jpg differ diff --git a/backend/uploads/tours/1782097958694-34402464.jpg b/backend/uploads/tours/1782097958694-34402464.jpg new file mode 100644 index 0000000..3bcf349 Binary files /dev/null and b/backend/uploads/tours/1782097958694-34402464.jpg differ diff --git a/backend/uploads/tours/1782098598511-366218057.jpg b/backend/uploads/tours/1782098598511-366218057.jpg new file mode 100644 index 0000000..dbe45b1 Binary files /dev/null and b/backend/uploads/tours/1782098598511-366218057.jpg differ diff --git a/backend/uploads/tours/1782098955193-504499361.jpg b/backend/uploads/tours/1782098955193-504499361.jpg new file mode 100644 index 0000000..a757dbf Binary files /dev/null and b/backend/uploads/tours/1782098955193-504499361.jpg differ diff --git a/backend/uploads/tours/1782099003772-651088143.jpg b/backend/uploads/tours/1782099003772-651088143.jpg new file mode 100644 index 0000000..b4e9975 Binary files /dev/null and b/backend/uploads/tours/1782099003772-651088143.jpg differ diff --git a/backend/uploads/tours/1782099024955-859396932.jpg b/backend/uploads/tours/1782099024955-859396932.jpg new file mode 100644 index 0000000..151bb18 Binary files /dev/null and b/backend/uploads/tours/1782099024955-859396932.jpg differ diff --git a/backend/uploads/tours/1782099284022-598478079.jpg b/backend/uploads/tours/1782099284022-598478079.jpg new file mode 100644 index 0000000..1ac3fb7 Binary files /dev/null and b/backend/uploads/tours/1782099284022-598478079.jpg differ diff --git a/backend/uploads/tours/1782099305251-634989764.jpg b/backend/uploads/tours/1782099305251-634989764.jpg new file mode 100644 index 0000000..e668e17 Binary files /dev/null and b/backend/uploads/tours/1782099305251-634989764.jpg differ diff --git a/backend/uploads/tours/1782099558534-819985170.jpg b/backend/uploads/tours/1782099558534-819985170.jpg new file mode 100644 index 0000000..c9212eb Binary files /dev/null and b/backend/uploads/tours/1782099558534-819985170.jpg differ diff --git a/backend/uploads/tours/1782099673822-87952177.jpg b/backend/uploads/tours/1782099673822-87952177.jpg new file mode 100644 index 0000000..bdae9ee Binary files /dev/null and b/backend/uploads/tours/1782099673822-87952177.jpg differ diff --git a/backend/uploads/tours/1782099792835-465444865.jpg b/backend/uploads/tours/1782099792835-465444865.jpg new file mode 100644 index 0000000..3f09313 Binary files /dev/null and b/backend/uploads/tours/1782099792835-465444865.jpg differ diff --git a/backend/uploads/tours/1782100326127-171966975.jpg b/backend/uploads/tours/1782100326127-171966975.jpg new file mode 100644 index 0000000..0837130 Binary files /dev/null and b/backend/uploads/tours/1782100326127-171966975.jpg differ diff --git a/backend/uploads/tours/1782100420433-736686613.jpg b/backend/uploads/tours/1782100420433-736686613.jpg new file mode 100644 index 0000000..3998813 Binary files /dev/null and b/backend/uploads/tours/1782100420433-736686613.jpg differ diff --git a/backend/uploads/tours/1782100590684-581575174.jpg b/backend/uploads/tours/1782100590684-581575174.jpg new file mode 100644 index 0000000..4cbeb28 Binary files /dev/null and b/backend/uploads/tours/1782100590684-581575174.jpg differ diff --git a/backend/uploads/tours/1782100955639-135095862.jpg b/backend/uploads/tours/1782100955639-135095862.jpg new file mode 100644 index 0000000..3651f65 Binary files /dev/null and b/backend/uploads/tours/1782100955639-135095862.jpg differ diff --git a/backend/uploads/tours/1782101005582-952029411.jpg b/backend/uploads/tours/1782101005582-952029411.jpg new file mode 100644 index 0000000..5799dbd Binary files /dev/null and b/backend/uploads/tours/1782101005582-952029411.jpg differ diff --git a/backend/uploads/tours/1782101476154-635865207.jpg b/backend/uploads/tours/1782101476154-635865207.jpg new file mode 100644 index 0000000..770a74c Binary files /dev/null and b/backend/uploads/tours/1782101476154-635865207.jpg differ diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index ac1a0f9..842cd4a 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -1,5 +1,3 @@ -version: '3.8' - services: postgres: image: postgres:15-alpine @@ -32,6 +30,8 @@ services: sh -c "npx prisma migrate deploy --schema=prisma/schema.prisma && node dist/src/main.js" ports: - "3001:3001" + volumes: + - ./backend/uploads:/usr/src/app/uploads environment: DATABASE_URL: "postgresql://postgres:password@postgres:5432/travel_db?schema=public" REDIS_URL: "redis://redis:6379" diff --git a/frontend/nginx.conf b/frontend/nginx.conf index 45d0002..0da7adc 100644 --- a/frontend/nginx.conf +++ b/frontend/nginx.conf @@ -1,6 +1,6 @@ server { listen 80; - server_name localhost; + server_name yotrip.labz.io.vn localhost; client_max_body_size 50M; location / { @@ -9,7 +9,7 @@ server { try_files $uri $uri/ /index.html; } - # Proxy requests to backend for deployment on same server/domain (optional but good practice) + # Proxy API requests to backend location /api/v1/ { proxy_pass http://backend:3001; proxy_http_version 1.1; @@ -19,6 +19,14 @@ server { proxy_cache_bypass $http_upgrade; } + # Serve uploaded files from backend + location /uploads/ { + proxy_pass http://backend:3001; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_cache_bypass $http_upgrade; + } + # Proxy WebSocket connection location /socket.io/ { proxy_pass http://backend:3001; diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 347528a..b5795b9 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -7,6 +7,7 @@ import { MyPhotosPage } from './pages/MyPhotosPage'; import { MyNotePage } from './pages/MyNotePage'; import { JoinTourPage } from './pages/JoinTourPage'; import { MemberDashboard } from './pages/MemberDashboard'; +import { AdminDashboard } from './pages/AdminDashboard'; import { ShareJourneyPage } from './pages/ShareJourneyPage'; import { ConfirmProvider } from './hooks/useConfirm'; import { NotificationProvider } from './hooks/useNotification'; @@ -20,7 +21,7 @@ function App() { const [user, setUser] = useState+ Đăng nhập để tham gia chuyến du lịch này +
+ + {error && ( +Bạn không có quyền truy cập trang admin này.
+Danh sách các chuyến đi bạn tham gia (với vai trò chủ sở hữu, quản lý hoặc thành viên).
Quản lý các mối quan hệ bạn bè, gia đình, duyệt các yêu cầu kết nối từ thành viên khác.
+{t('manageRelations')}
Bạn chưa kết nối với ai. Hãy chuyển sang tìm kiếm để gửi lời mời.
+{t('noConnections')}