fix: guest and admin logic
This commit is contained in:
+253
-30
@@ -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 = `<h2>${filteredTitle} - Initial Planning</h2>
|
||||
<p><strong>Start Date:</strong> ${startDate ? new Date(startDate).toLocaleDateString() : 'TBD'}</p>
|
||||
<p><strong>End Date:</strong> ${endDate ? new Date(endDate).toLocaleDateString() : 'TBD'}</p>
|
||||
<p><strong>Adult Participants:</strong> ${adultCount || 1}</p>
|
||||
<p><strong>Child Participants:</strong> ${childCount || 0}</p>
|
||||
<h3>Key Items to Plan:</h3>
|
||||
<ul>
|
||||
<li>Accommodations</li>
|
||||
<li>Transportation</li>
|
||||
<li>Activities & Attractions</li>
|
||||
<li>Budget & Expenses</li>
|
||||
<li>Important Contact Numbers</li>
|
||||
<li>Special Requirements & Notes</li>
|
||||
</ul>
|
||||
<p><em>Add your planning notes here...</em></p>`;
|
||||
|
||||
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 };
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user