fix: admin empty thùng rác

This commit is contained in:
2026-06-22 12:28:21 +07:00
parent 860395cb14
commit 01d6d7439f
54 changed files with 992 additions and 361 deletions
+377 -39
View File
@@ -71,6 +71,7 @@ const admin_guard_1 = require("./auth/admin.guard");
const nodemailer = __importStar(require("nodemailer"));
const jwt_1 = require("@nestjs/jwt");
const jwt_auth_guard_1 = require("./auth/jwt-auth.guard");
const jwt_auth_guard_2 = require("./auth/jwt-auth.guard");
const jwt_strategy_1 = require("./auth/jwt.strategy");
const core_2 = require("@nestjs/core");
const common_2 = require("@nestjs/common");
@@ -823,6 +824,34 @@ let TourController = class TourController {
}
}
});
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;
}
@@ -1464,15 +1493,37 @@ let TourController = class TourController {
console.warn('[HEIC] Failed to convert HEIC to JPEG, attempting to process anyway:', e.message);
}
}
await (0, sharp_1.default)(processBuffer)
.rotate()
.resize(2560, 2560, {
fit: 'inside',
withoutEnlargement: true
})
.jpeg({ quality: 85 })
.toFile(displayFilePath);
return this.prisma.photo.create({
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 common_1.BadRequestException(`Lỗi tạo thư mục: ${e.message}`);
}
}
try {
await (0, sharp_1.default)(processBuffer)
.rotate()
.resize(2560, 2560, {
fit: 'inside',
withoutEnlargement: true
})
.jpeg({ quality: 85 })
.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 common_1.BadRequestException(`Lỗi lưu hình ảnh: ${e.message}`);
}
if (!fs.existsSync(displayFilePath)) {
console.error(`[Photo] File verification failed at ${displayFilePath}`);
throw new common_1.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ữ.');
}
const photoRecord = await this.prisma.photo.create({
data: {
tourId: tourId,
uploaderId: uploaderId,
@@ -1485,6 +1536,8 @@ let TourController = class TourController {
}
}
});
console.log(`[Photo] Database record created: ID=${photoRecord.id}, imageUrl=${photoRecord.imageUrl}`);
return photoRecord;
}));
}
async createInvitation(tourId, body, req) {
@@ -1528,17 +1581,31 @@ let TourController = class TourController {
}
async joinByToken(body, req) {
const { token } = body;
console.log('[joinByToken] Request received, token length:', token ? token.length : 0);
if (!token) {
console.error('[joinByToken] No token provided');
throw new common_1.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) + '...');
const totalInvitations = await this.prisma.tourInvitation.count();
console.log('[joinByToken] Total invitations in database:', totalInvitations);
throw new common_1.NotFoundException('Lời mời không hợp lệ hoặc đã bị hủy.');
}
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 common_1.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 } });
}
@@ -1546,10 +1613,12 @@ let TourController = class TourController {
throw new common_1.BadRequestException('Lời mời đã hết hạn.');
}
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 } });
}
@@ -1582,6 +1651,7 @@ let TourController = class TourController {
where: { id: invitation.id }
})
]);
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 };
}
async mergeMember(tourId, body, req) {
@@ -1709,7 +1779,7 @@ __decorate([
], TourController.prototype, "deleteTour", null);
__decorate([
(0, common_1.UseInterceptors)(compress_cache_interceptor_1.CompressCacheInterceptor),
(0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard),
(0, common_1.UseGuards)(jwt_auth_guard_2.JwtAuthGuardNoAnonymous),
(0, common_1.Get)('explore'),
__param(0, (0, common_1.Req)()),
__metadata("design:type", Function),
@@ -2155,11 +2225,15 @@ let PhotoController = class PhotoController {
const originalFilename = `${uniqueSuffix}${originalExtension}`;
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);
if (!fs.existsSync(memberOriginalDir)) {
fs.mkdirSync(memberOriginalDir, { recursive: true });
}
if (!fs.existsSync(tourDisplayPath)) {
fs.mkdirSync(tourDisplayPath, { recursive: true });
}
await fs.promises.writeFile(originalFilePath, file.buffer);
let lat;
let lng;
@@ -3037,6 +3111,23 @@ let PublicPhotoController = class PublicPhotoController {
</html>`;
res.type('text/html').send(htmlContent);
}
async flagPhoto(photoId, body, req) {
const photo = await this.prisma.photo.findUnique({
where: { id: photoId }
});
if (!photo) {
throw new common_1.NotFoundException('Ảnh không tồn tại');
}
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 };
}
};
__decorate([
(0, common_1.Get)(),
@@ -3079,6 +3170,16 @@ __decorate([
__metadata("design:paramtypes", [String, Object, Object]),
__metadata("design:returntype", Promise)
], PublicPhotoController.prototype, "sharePhoto", null);
__decorate([
(0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard),
(0, common_1.Post)(':photoId/flag'),
__param(0, (0, common_1.Param)('photoId', common_1.ParseUUIDPipe)),
__param(1, (0, common_1.Body)()),
__param(2, (0, common_1.Req)()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [String, Object, Object]),
__metadata("design:returntype", Promise)
], PublicPhotoController.prototype, "flagPhoto", null);
PublicPhotoController = __decorate([
(0, common_1.Controller)('public-photos'),
__metadata("design:paramtypes", [prisma_service_1.PrismaService,
@@ -3631,6 +3732,79 @@ AdminModerationController = __decorate([
(0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard, admin_guard_1.AdminGuard),
__metadata("design:paramtypes", [prisma_service_1.PrismaService])
], AdminModerationController);
let AdminPhotosController = class AdminPhotosController {
constructor(prisma) {
this.prisma = prisma;
}
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' }
});
}
async approvePhoto(photoId) {
const photo = await this.prisma.photo.findUnique({
where: { id: photoId }
});
if (!photo) {
throw new common_1.NotFoundException('Ảnh không tồn tại');
}
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 };
}
async deletePhoto(photoId) {
const photo = await this.prisma.photo.findUnique({
where: { id: photoId }
});
if (!photo) {
throw new common_1.NotFoundException('Ảnh không tồn tại');
}
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 };
}
};
__decorate([
(0, common_1.Get)('flagged'),
__metadata("design:type", Function),
__metadata("design:paramtypes", []),
__metadata("design:returntype", Promise)
], AdminPhotosController.prototype, "getFlaggedPhotos", null);
__decorate([
(0, common_1.Post)(':photoId/approve'),
__param(0, (0, common_1.Param)('photoId', common_1.ParseUUIDPipe)),
__metadata("design:type", Function),
__metadata("design:paramtypes", [String]),
__metadata("design:returntype", Promise)
], AdminPhotosController.prototype, "approvePhoto", null);
__decorate([
(0, common_1.Delete)(':photoId'),
__param(0, (0, common_1.Param)('photoId', common_1.ParseUUIDPipe)),
__metadata("design:type", Function),
__metadata("design:paramtypes", [String]),
__metadata("design:returntype", Promise)
], AdminPhotosController.prototype, "deletePhoto", null);
AdminPhotosController = __decorate([
(0, common_1.Controller)('admin/photos'),
(0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard, admin_guard_1.AdminGuard),
__metadata("design:paramtypes", [prisma_service_1.PrismaService])
], AdminPhotosController);
let ReportsController = class ReportsController {
constructor(prisma) {
this.prisma = prisma;
@@ -4347,67 +4521,225 @@ let AdminTrashController = class AdminTrashController {
}
async deletePermanentItems(body) {
const { type, ids } = body;
console.log('[deletePermanent] Request received - type:', type, 'ids count:', ids.length);
if (!type || !ids || !Array.isArray(ids)) {
throw new common_1.BadRequestException('Tham số không hợp lệ.');
}
const deleteResults = { success: 0, failed: 0, errors: [] };
if (type === 'tour') {
for (const tourId of ids) {
const photos = await this.prisma.photo.findMany({ where: { tourId } });
for (const p of photos) {
if (p.imageUrl) {
const displayFilePath = path.join(process.cwd(), p.imageUrl.replace(/^\//, ''));
if (fs.existsSync(displayFilePath)) {
try {
fs.unlinkSync(displayFilePath);
try {
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);
console.log('[deletePermanent] Deleted display file:', displayFilePath);
}
catch (e) {
console.error('[deletePermanent] Error deleting display file:', e);
}
}
}
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);
console.log('[deletePermanent] Deleted original file:', originalFilePath);
}
catch (e) {
console.error('[deletePermanent] Error deleting original file:', e);
}
}
catch (e) { }
}
}
if (p.originalUrl) {
const originalFilePath = path.join(process.cwd(), p.originalUrl.replace(/^\//, ''));
if (fs.existsSync(originalFilePath)) {
try {
fs.unlinkSync(originalFilePath);
}
catch (e) { }
}
}
await this.prisma.tour.delete({ where: { id: tourId } });
console.log('[deletePermanent] Deleted tour from database:', tourId);
deleteResults.success++;
}
catch (e) {
console.error('[deletePermanent] Error deleting tour:', tourId, e);
deleteResults.failed++;
deleteResults.errors.push(`Tour ${tourId}: ${e?.message || 'Unknown error'}`);
}
await this.prisma.tour.delete({ where: { id: tourId } });
}
}
else if (type === 'photo') {
console.log('[deletePermanent] Deleting', ids.length, 'photos');
console.log('[deletePermanent] Photo IDs:', JSON.stringify(ids));
if (!ids || ids.length === 0) {
console.warn('[deletePermanent] No photo IDs provided');
return { success: true, deleted: 0, failed: 0 };
}
for (const photoId of ids) {
const photo = await this.prisma.photo.findUnique({ where: { id: photoId } });
if (photo) {
try {
console.log('[deletePermanent] ========== START Processing photo:', photoId);
const photo = await this.prisma.photo.findUnique({ where: { id: photoId } });
if (!photo) {
console.warn('[deletePermanent] Photo not found in DB:', photoId);
deleteResults.failed++;
deleteResults.errors.push(`Photo ${photoId}: not found in database`);
console.log('[deletePermanent] ========== SKIP (not found):', photoId);
continue;
}
console.log('[deletePermanent] Found photo:', { id: photo.id, imageUrl: photo.imageUrl, isDeleted: photo.isDeleted, tourId: photo.tourId });
console.log('[deletePermanent] Deleting comments for photo:', photoId);
try {
const commentResult = await this.prisma.comment.deleteMany({
where: { photoId }
});
console.log('[deletePermanent] Deleted', commentResult.count, 'comments for photo');
}
catch (e) {
console.warn('[deletePermanent] Error deleting comments (continuing):', e?.message);
}
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);
}
catch (e) { }
}
}
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);
}
catch (e) { }
}
}
await this.prisma.photo.delete({ where: { id: photoId } });
console.log('[deletePermanent] Attempting database delete for photo:', photoId);
const result = await this.prisma.photo.delete({ where: { id: photoId } });
console.log('[deletePermanent] ✓ Successfully deleted photo from database:', photoId);
deleteResults.success++;
console.log('[deletePermanent] ========== SUCCESS:', photoId);
}
catch (e) {
console.error('[deletePermanent] ✗ Error deleting photo:', photoId);
console.error('[deletePermanent] Error details:', e?.message || String(e));
deleteResults.failed++;
deleteResults.errors.push(`Photo ${photoId}: ${e?.message || 'Unknown error'}`);
console.log('[deletePermanent] ========== FAILED:', photoId);
}
}
console.log('[deletePermanent] Photos deletion complete - success:', deleteResults.success, 'failed:', deleteResults.failed);
}
else if (type === 'note') {
await this.prisma.tourNote.deleteMany({
where: { id: { in: ids } }
});
console.log('[deletePermanent] Deleting', ids.length, 'notes');
try {
const result = await this.prisma.tourNote.deleteMany({
where: { id: { in: ids } }
});
console.log('[deletePermanent] Deleted notes from database, count:', result.count);
deleteResults.success = result.count;
}
catch (e) {
console.error('[deletePermanent] Error deleting notes:', e);
deleteResults.failed = ids.length;
deleteResults.errors.push(`Notes deletion: ${e?.message || 'Unknown error'}`);
}
}
return { success: true };
console.log('[deletePermanent] Deletion complete - success:', deleteResults.success, 'failed:', deleteResults.failed);
if (deleteResults.failed > 0) {
console.warn('[deletePermanent] Errors:', deleteResults.errors);
}
return {
success: true,
deleted: deleteResults.success,
failed: deleteResults.failed,
errors: deleteResults.errors.length > 0 ? deleteResults.errors : undefined
};
}
async emptyAllTrash() {
console.log('[emptyAllTrash] Starting to empty all trash');
const deleteResults = { success: 0, failed: 0, errors: [] };
try {
console.log('[emptyAllTrash] Finding all deleted photos');
const deletedPhotos = await this.prisma.photo.findMany({
where: { isDeleted: true }
});
console.log('[emptyAllTrash] Found', deletedPhotos.length, 'deleted photos');
console.log('[emptyAllTrash] Deleting all comments from deleted photos');
const commentCount = await this.prisma.comment.deleteMany({
where: {
photoId: { in: deletedPhotos.map(p => p.id) }
}
});
console.log('[emptyAllTrash] Deleted', commentCount.count, 'comments');
for (const photo of deletedPhotos) {
if (photo.imageUrl) {
const filePath = path.join(process.cwd(), photo.imageUrl.replace(/^\//, ''));
if (fs.existsSync(filePath)) {
try {
fs.unlinkSync(filePath);
console.log('[emptyAllTrash] Deleted display file:', filePath);
}
catch (e) {
console.error('[emptyAllTrash] Error deleting display file:', e);
}
}
}
if (photo.originalUrl) {
const filePath = path.join(process.cwd(), photo.originalUrl.replace(/^\//, ''));
if (fs.existsSync(filePath)) {
try {
fs.unlinkSync(filePath);
console.log('[emptyAllTrash] Deleted original file:', filePath);
}
catch (e) {
console.error('[emptyAllTrash] Error deleting original file:', e);
}
}
}
}
console.log('[emptyAllTrash] Deleting all', deletedPhotos.length, 'photos from database');
const photoResult = await this.prisma.photo.deleteMany({
where: { isDeleted: true }
});
console.log('[emptyAllTrash] Deleted', photoResult.count, 'photos');
deleteResults.success += photoResult.count;
console.log('[emptyAllTrash] Deleting all deleted tours');
const tourResult = await this.prisma.tour.deleteMany({
where: { isDeleted: true }
});
console.log('[emptyAllTrash] Deleted', tourResult.count, 'tours');
deleteResults.success += tourResult.count;
console.log('[emptyAllTrash] Deleting all deleted notes');
const noteResult = await this.prisma.tourNote.deleteMany({
where: { isDeleted: true }
});
console.log('[emptyAllTrash] Deleted', noteResult.count, 'notes');
deleteResults.success += noteResult.count;
console.log('[emptyAllTrash] Successfully emptied all trash - total deleted:', deleteResults.success);
}
catch (e) {
console.error('[emptyAllTrash] Error emptying trash:', e?.message || e);
deleteResults.failed++;
deleteResults.errors.push(`Emptying trash: ${e?.message || 'Unknown error'}`);
}
return {
success: true,
totalDeleted: deleteResults.success,
errors: deleteResults.errors.length > 0 ? deleteResults.errors : undefined
};
}
};
__decorate([
@@ -4437,6 +4769,12 @@ __decorate([
__metadata("design:paramtypes", [Object]),
__metadata("design:returntype", Promise)
], AdminTrashController.prototype, "deletePermanentItems", null);
__decorate([
(0, common_1.Post)('empty-all'),
__metadata("design:type", Function),
__metadata("design:paramtypes", []),
__metadata("design:returntype", Promise)
], AdminTrashController.prototype, "emptyAllTrash", null);
AdminTrashController = __decorate([
(0, common_1.Controller)('admin/trash'),
(0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard, admin_guard_1.AdminGuard),