feat: tính năng chia sẻ khẩn cấp

This commit is contained in:
2026-06-21 12:52:47 +07:00
parent 8d79cd76f6
commit 403c169ddd
21 changed files with 2939 additions and 132 deletions
+457 -18
View File
@@ -420,6 +420,19 @@ let AuthController = class AuthController {
},
};
}
async promoteAdmin(body, req) {
const { secretKey } = body;
const adminSecret = this.configService.get('ADMIN_SECRET_KEY') || 'yotrip_secret_admin_key';
if (secretKey !== adminSecret) {
throw new common_1.BadRequestException('Mã Secret Key không hợp lệ.');
}
const updated = await this.prisma.user.update({
where: { id: req.user.id },
data: { isAdmin: true },
select: { id: true, email: true, name: true, isAdmin: true }
});
return { success: true, user: updated };
}
async createGuestUser() {
const user = await this.prisma.user.create({
data: {
@@ -601,6 +614,15 @@ __decorate([
__metadata("design:paramtypes", [Object]),
__metadata("design:returntype", Promise)
], AuthController.prototype, "convertGuestToOfficial", null);
__decorate([
(0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard),
(0, common_1.Post)('promote-admin'),
__param(0, (0, common_1.Body)()),
__param(1, (0, common_1.Req)()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object, Object]),
__metadata("design:returntype", Promise)
], AuthController.prototype, "promoteAdmin", null);
__decorate([
(0, common_1.Post)('create-guest'),
__metadata("design:type", Function),
@@ -720,10 +742,12 @@ let TourController = class TourController {
}
async createTour(body, req) {
const { title, description, startDate, endDate, adultCount, childCount, childDiscount, tags, members } = body;
const filteredTitle = await filterText(this.prisma, title || '');
const filteredDesc = await filterText(this.prisma, description || '');
const tour = await this.prisma.tour.create({
data: {
title,
description,
title: filteredTitle,
description: filteredDesc,
startDate: startDate ? new Date(startDate) : null,
endDate: endDate ? new Date(endDate) : null,
tags: tags || [],
@@ -939,18 +963,23 @@ let TourController = class TourController {
});
}
async updateTour(id, body) {
const updateData = {
startDate: body.startDate ? new Date(body.startDate) : undefined,
tags: body.tags,
endDate: body.endDate ? new Date(body.endDate) : undefined,
adultCount: body.adultCount !== undefined ? Number(body.adultCount) : undefined,
childCount: body.childCount !== undefined ? Number(body.childCount) : undefined,
childDiscount: body.childDiscount !== undefined ? Number(body.childDiscount) : undefined,
};
if (body.title !== undefined) {
updateData.title = await filterText(this.prisma, body.title);
}
if (body.description !== undefined) {
updateData.description = await filterText(this.prisma, body.description);
}
return this.prisma.tour.update({
where: { id },
data: {
title: body.title,
description: body.description,
startDate: body.startDate ? new Date(body.startDate) : undefined,
tags: body.tags,
endDate: body.endDate ? new Date(body.endDate) : undefined,
adultCount: body.adultCount !== undefined ? Number(body.adultCount) : undefined,
childCount: body.childCount !== undefined ? Number(body.childCount) : undefined,
childDiscount: body.childDiscount !== undefined ? Number(body.childDiscount) : undefined,
},
data: updateData,
}).then(async (tour) => {
await Promise.all([
this.cacheManager.del(id),
@@ -2445,9 +2474,10 @@ let CommentController = class CommentController {
});
}
async addComment(locationId, body, req) {
const filteredContent = await filterText(this.prisma, body.content);
const comment = await this.prisma.comment.create({
data: {
content: body.content,
content: filteredContent,
locationId,
userId: req.user.id
},
@@ -2848,9 +2878,10 @@ let PublicPhotoController = class PublicPhotoController {
if (!photo) {
throw new common_1.NotFoundException('Không tìm thấy ảnh.');
}
const filteredContent = await filterText(this.prisma, content.trim());
const comment = await this.prisma.comment.create({
data: {
content: content.trim(),
content: filteredContent,
photoId,
userId: req.user.id
},
@@ -2863,6 +2894,26 @@ let PublicPhotoController = class PublicPhotoController {
this.commentGateway.notifyNewPhotoComment(photoId, comment);
return comment;
}
async deletePhotoComment(id, req) {
const comment = await this.prisma.comment.findUnique({
where: { id },
include: { photo: true }
});
if (!comment) {
throw new common_1.NotFoundException('Không tìm thấy bình luận.');
}
const isAuthorized = req.user.isAdmin ||
comment.userId === req.user.id ||
(comment.photo && comment.photo.uploaderId === req.user.id);
if (!isAuthorized) {
throw new common_1.ForbiddenException('Bạn không có quyền xóa bình luận này.');
}
await this.prisma.comment.delete({ where: { id } });
if (comment.photoId) {
this.commentGateway.server.to(`photo_${comment.photoId}`).emit('photoCommentDeleted', { id, photoId: comment.photoId });
}
return { success: true, message: 'Đã xóa bình luận thành công.' };
}
async sharePhoto(photoId, req, res) {
const photo = await this.prisma.photo.findUnique({
where: { id: photoId }
@@ -2977,6 +3028,15 @@ __decorate([
__metadata("design:paramtypes", [String, Object, Object]),
__metadata("design:returntype", Promise)
], PublicPhotoController.prototype, "addPhotoComment", null);
__decorate([
(0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard),
(0, common_1.Delete)('comments/:id'),
__param(0, (0, common_1.Param)('id', common_1.ParseUUIDPipe)),
__param(1, (0, common_1.Req)()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [String, Object]),
__metadata("design:returntype", Promise)
], PublicPhotoController.prototype, "deletePhotoComment", null);
__decorate([
(0, common_1.Get)(':photoId/share'),
__param(0, (0, common_1.Param)('photoId', common_1.ParseUUIDPipe)),
@@ -3224,11 +3284,12 @@ let DirectMessageController = class DirectMessageController {
if (!conn) {
throw new common_1.ForbiddenException('Bạn chỉ có thể nhắn tin với người đã kết nối.');
}
const filteredContent = content ? await filterText(this.prisma, content) : '';
const message = await this.prisma.directMessage.create({
data: {
senderId,
receiverId,
content: content || '',
content: filteredContent,
attachmentUrl,
latitude,
longitude
@@ -3330,11 +3391,12 @@ let TourMessageController = class TourMessageController {
if (!participant) {
throw new common_1.ForbiddenException('Bạn không phải là thành viên của hành trình này.');
}
const filteredContent = content ? await filterText(this.prisma, content) : '';
const tourMessage = await this.prisma.tourMessage.create({
data: {
tourId,
senderId,
content: content || '',
content: filteredContent,
attachmentUrl,
latitude,
longitude
@@ -3365,13 +3427,14 @@ let TourMessageController = class TourMessageController {
});
for (const p of otherParticipants) {
if (p.userId) {
const filteredContent = content ? await filterText(this.prisma, content) : '';
const isTagged = hasTags && taggedUserIds.includes(p.userId);
console.log(`[sendTourMessage] Emitting tourMessageNotification to user_${p.userId}, isTagged: ${isTagged}`);
this.commentGateway.server.to(`user_${p.userId}`).emit('tourMessageNotification', {
tourId,
senderName: req.user.name || 'Thành viên',
tourTitle: tour?.title || 'Hành trình',
content: isTagged ? `@Bạn: ${content || ''}` : content || (attachmentUrl ? '[Hình ảnh]' : '[Vị trí]'),
content: isTagged ? `@Bạn: ${filteredContent || ''}` : filteredContent || (attachmentUrl ? '[Hình ảnh]' : '[Vị trí]'),
isTagged
});
}
@@ -3402,6 +3465,382 @@ TourMessageController = __decorate([
__metadata("design:paramtypes", [prisma_service_1.PrismaService,
CommentGateway])
], TourMessageController);
async function filterText(prisma, text) {
if (!text)
return text;
try {
const filters = await prisma.wordFilter.findMany();
let result = text;
for (const filter of filters) {
const escapedWord = filter.word.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
const regex = new RegExp(escapedWord, 'gi');
result = result.replace(regex, filter.replacement);
}
return result;
}
catch (err) {
console.error('Lỗi khi lọc từ cấm:', err);
return text;
}
}
let ModerationController = class ModerationController {
constructor(prisma) {
this.prisma = prisma;
}
async getSettings() {
let settings = await this.prisma.moderationSetting.findFirst();
if (!settings) {
settings = await this.prisma.moderationSetting.create({
data: { blockNsfw: false, blurFaces: false }
});
}
return settings;
}
};
__decorate([
(0, common_1.Get)('settings'),
__metadata("design:type", Function),
__metadata("design:paramtypes", []),
__metadata("design:returntype", Promise)
], ModerationController.prototype, "getSettings", null);
ModerationController = __decorate([
(0, common_1.Controller)('moderation'),
__metadata("design:paramtypes", [prisma_service_1.PrismaService])
], ModerationController);
let AdminModerationController = class AdminModerationController {
constructor(prisma) {
this.prisma = prisma;
}
async getSettings() {
let settings = await this.prisma.moderationSetting.findFirst();
if (!settings) {
settings = await this.prisma.moderationSetting.create({
data: { blockNsfw: false, blurFaces: false }
});
}
return settings;
}
async updateSettings(body) {
let settings = await this.prisma.moderationSetting.findFirst();
if (!settings) {
return this.prisma.moderationSetting.create({
data: {
blockNsfw: body.blockNsfw ?? false,
blurFaces: body.blurFaces ?? false
}
});
}
return this.prisma.moderationSetting.update({
where: { id: settings.id },
data: {
blockNsfw: body.blockNsfw,
blurFaces: body.blurFaces
}
});
}
async getWordFilters() {
return this.prisma.wordFilter.findMany({
orderBy: { createdAt: 'desc' }
});
}
async addWordFilter(body) {
const { word, replacement } = body;
if (!word || replacement === undefined) {
throw new common_1.BadRequestException('Từ khóa và từ thay thế không được để trống.');
}
return this.prisma.wordFilter.create({
data: {
word: word.trim(),
replacement: replacement.trim()
}
});
}
async deleteWordFilter(id) {
await this.prisma.wordFilter.delete({ where: { id } });
return { success: true };
}
};
__decorate([
(0, common_1.Get)(),
__metadata("design:type", Function),
__metadata("design:paramtypes", []),
__metadata("design:returntype", Promise)
], AdminModerationController.prototype, "getSettings", null);
__decorate([
(0, common_1.Post)(),
__param(0, (0, common_1.Body)()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object]),
__metadata("design:returntype", Promise)
], AdminModerationController.prototype, "updateSettings", null);
__decorate([
(0, common_1.Get)('word-filters'),
__metadata("design:type", Function),
__metadata("design:paramtypes", []),
__metadata("design:returntype", Promise)
], AdminModerationController.prototype, "getWordFilters", null);
__decorate([
(0, common_1.Post)('word-filters'),
__param(0, (0, common_1.Body)()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object]),
__metadata("design:returntype", Promise)
], AdminModerationController.prototype, "addWordFilter", null);
__decorate([
(0, common_1.Delete)('word-filters/:id'),
__param(0, (0, common_1.Param)('id', common_1.ParseUUIDPipe)),
__metadata("design:type", Function),
__metadata("design:paramtypes", [String]),
__metadata("design:returntype", Promise)
], AdminModerationController.prototype, "deleteWordFilter", null);
AdminModerationController = __decorate([
(0, common_1.Controller)('admin/moderation'),
(0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard, admin_guard_1.AdminGuard),
__metadata("design:paramtypes", [prisma_service_1.PrismaService])
], AdminModerationController);
let TrustedUsersController = class TrustedUsersController {
constructor(prisma) {
this.prisma = prisma;
}
async getTrustedUsers() {
const ratings = await this.prisma.tourRating.groupBy({
by: ['targetUserId'],
_avg: { averageScore: true },
_count: { id: true }
});
const users = await this.prisma.user.findMany({
where: { id: { in: ratings.map(r => r.targetUserId) } },
select: { id: true, name: true, avatar: true }
});
const result = ratings.map(r => {
const u = users.find(user => user.id === r.targetUserId);
return {
id: r.targetUserId,
name: u?.name || 'Ẩn danh',
avatar: u?.avatar || null,
averageScore: Math.round((r._avg.averageScore || 0) * 10) / 10,
ratingCount: r._count.id
};
});
return result.sort((a, b) => b.averageScore - a.averageScore || b.ratingCount - a.ratingCount);
}
};
__decorate([
(0, common_1.Get)(),
__metadata("design:type", Function),
__metadata("design:paramtypes", []),
__metadata("design:returntype", Promise)
], TrustedUsersController.prototype, "getTrustedUsers", null);
TrustedUsersController = __decorate([
(0, common_1.Controller)('users/trusted'),
__metadata("design:paramtypes", [prisma_service_1.PrismaService])
], TrustedUsersController);
let TourRatingController = class TourRatingController {
constructor(prisma) {
this.prisma = prisma;
}
async getTourRatings(tourId) {
return this.prisma.tourRating.findMany({
where: { tourId },
include: {
raterUser: { select: { id: true, name: true, avatar: true } },
targetUser: { select: { id: true, name: true, avatar: true } }
},
orderBy: { createdAt: 'desc' }
});
}
async createTourRating(tourId, body, req) {
const raterUserId = req.user.id;
const { targetUserId, honesty, transparency, enthusiasm, cheerfulness, seriousness, planning, survival, comment } = body;
if (!targetUserId || honesty < 1 || honesty > 5 || transparency < 1 || transparency > 5 || enthusiasm < 1 || enthusiasm > 5 || cheerfulness < 1 || cheerfulness > 5 || seriousness < 1 || seriousness > 5 || planning < 1 || planning > 5 || survival < 1 || survival > 5) {
throw new common_1.BadRequestException('Dữ liệu đánh giá không hợp lệ.');
}
const participant = await this.prisma.tourParticipant.findFirst({
where: { tourId, userId: raterUserId }
});
if (!participant) {
throw new common_1.ForbiddenException('Bạn không phải là thành viên của tour này.');
}
const targetParticipant = await this.prisma.tourParticipant.findFirst({
where: {
tourId,
userId: targetUserId,
role: { in: ['OWNER', 'MANAGER'] }
}
});
if (!targetParticipant) {
throw new common_1.BadRequestException('Chỉ có thể đánh giá người tạo tour hoặc người quản lý.');
}
const averageScore = (honesty + transparency + enthusiasm + cheerfulness + seriousness + planning + survival) / 7;
const existingRating = await this.prisma.tourRating.findUnique({
where: {
tourId_targetUserId_raterUserId: {
tourId,
targetUserId,
raterUserId
}
}
});
if (existingRating) {
return this.prisma.tourRating.update({
where: { id: existingRating.id },
data: {
honesty,
transparency,
enthusiasm,
cheerfulness,
seriousness,
planning,
survival,
averageScore,
comment,
createdAt: new Date()
}
});
}
return this.prisma.tourRating.create({
data: {
tourId,
targetUserId,
raterUserId,
honesty,
transparency,
enthusiasm,
cheerfulness,
seriousness,
planning,
survival,
averageScore,
comment
}
});
}
};
__decorate([
(0, common_1.Get)(),
__param(0, (0, common_1.Param)('tourId', common_1.ParseUUIDPipe)),
__metadata("design:type", Function),
__metadata("design:paramtypes", [String]),
__metadata("design:returntype", Promise)
], TourRatingController.prototype, "getTourRatings", null);
__decorate([
(0, common_1.Post)(),
__param(0, (0, common_1.Param)('tourId', 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)
], TourRatingController.prototype, "createTourRating", null);
TourRatingController = __decorate([
(0, common_1.Controller)('tours/:tourId/ratings'),
(0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard),
__metadata("design:paramtypes", [prisma_service_1.PrismaService])
], TourRatingController);
let PublicTourShareController = class PublicTourShareController {
constructor(prisma) {
this.prisma = prisma;
}
async getSharedJourney(token) {
const tourShare = await this.prisma.tourShare.findUnique({
where: { token, isEnabled: true },
include: {
tour: {
include: {
creator: { select: { id: true, name: true, phone: true } },
participants: {
select: {
id: true,
role: true,
displayName: true,
user: { select: { id: true, name: true, phone: true } }
}
},
legs: {
include: {
locations: {
orderBy: { plannedStart: 'asc' }
}
},
orderBy: { sequence: 'asc' }
}
}
}
}
});
if (!tourShare || !tourShare.tour) {
throw new common_1.NotFoundException('Không tìm thấy hành trình chia sẻ hoặc liên kết đã bị vô hiệu hóa.');
}
return {
tour: tourShare.tour,
isEnabled: tourShare.isEnabled
};
}
};
__decorate([
(0, common_1.Get)('share/:token'),
__param(0, (0, common_1.Param)('token')),
__metadata("design:type", Function),
__metadata("design:paramtypes", [String]),
__metadata("design:returntype", Promise)
], PublicTourShareController.prototype, "getSharedJourney", null);
PublicTourShareController = __decorate([
(0, common_1.Controller)('tours'),
__metadata("design:paramtypes", [prisma_service_1.PrismaService])
], PublicTourShareController);
let TourShareController = class TourShareController {
constructor(prisma) {
this.prisma = prisma;
}
async getShareStatus(tourId) {
let share = await this.prisma.tourShare.findUnique({
where: { tourId }
});
if (!share) {
share = await this.prisma.tourShare.create({
data: { tourId, isEnabled: false }
});
}
return share;
}
async toggleShare(tourId, body) {
let share = await this.prisma.tourShare.findUnique({
where: { tourId }
});
if (!share) {
return this.prisma.tourShare.create({
data: {
tourId,
isEnabled: body.isEnabled
}
});
}
return this.prisma.tourShare.update({
where: { id: share.id },
data: { isEnabled: body.isEnabled }
});
}
};
__decorate([
(0, common_1.Get)(),
__param(0, (0, common_1.Param)('tourId', common_1.ParseUUIDPipe)),
__metadata("design:type", Function),
__metadata("design:paramtypes", [String]),
__metadata("design:returntype", Promise)
], TourShareController.prototype, "getShareStatus", null);
__decorate([
(0, common_1.Post)(),
__param(0, (0, common_1.Param)('tourId', common_1.ParseUUIDPipe)),
__param(1, (0, common_1.Body)()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [String, Object]),
__metadata("design:returntype", Promise)
], TourShareController.prototype, "toggleShare", null);
TourShareController = __decorate([
(0, common_1.Controller)('tours/:tourId/share'),
(0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard),
__metadata("design:paramtypes", [prisma_service_1.PrismaService])
], TourShareController);
let AppModule = class AppModule {
};
AppModule = __decorate([
@@ -3426,7 +3865,7 @@ AppModule = __decorate([
signOptions: { expiresIn: '1d' },
}),
],
controllers: [AppController, AuthController, PublicTourController, TourController, UserController, RoutingController, LegController, LocationController, CommentController, PhotoController, PublicPhotoController, AdminOtpController, AdminController, ConnectionController, DirectMessageController, UploadController, TourMessageController],
controllers: [AppController, AuthController, PublicTourController, TourController, UserController, RoutingController, LegController, LocationController, CommentController, PhotoController, PublicPhotoController, AdminOtpController, AdminController, ConnectionController, DirectMessageController, UploadController, TourMessageController, ModerationController, AdminModerationController, TrustedUsersController, TourRatingController, PublicTourShareController, TourShareController],
providers: [prisma_service_1.PrismaService, jwt_strategy_1.JwtStrategy, TourRoleGuard, jwt_auth_guard_1.JwtAuthGuard, admin_guard_1.AdminGuard, CommentGateway, core_2.Reflector, EmailService, config_1.ConfigService],
exports: [prisma_service_1.PrismaService]
})