fix: lỗi các thành viên không thể upload ảnh và không có nút xóa ảnh của user
This commit is contained in:
Vendored
+12
@@ -1,6 +1,18 @@
|
|||||||
import 'reflect-metadata';
|
import 'reflect-metadata';
|
||||||
import { OnGatewayConnection } from '@nestjs/websockets';
|
import { OnGatewayConnection } from '@nestjs/websockets';
|
||||||
import { Server, Socket } from 'socket.io';
|
import { Server, Socket } from 'socket.io';
|
||||||
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
|
import { ParticipantRole } from '@prisma/client';
|
||||||
|
import { Reflector } from '@nestjs/core';
|
||||||
|
import { CanActivate, ExecutionContext } from '@nestjs/common';
|
||||||
|
export declare const ROLES_KEY = "roles";
|
||||||
|
export declare const Roles: (...roles: ParticipantRole[]) => import("@nestjs/common").CustomDecorator<string>;
|
||||||
|
export declare class TourRoleGuard implements CanActivate {
|
||||||
|
private reflector;
|
||||||
|
private prisma;
|
||||||
|
constructor(reflector: Reflector, prisma: PrismaService);
|
||||||
|
canActivate(context: ExecutionContext): Promise<boolean>;
|
||||||
|
}
|
||||||
export declare class CommentGateway implements OnGatewayConnection {
|
export declare class CommentGateway implements OnGatewayConnection {
|
||||||
server: Server;
|
server: Server;
|
||||||
handleConnection(client: Socket): void;
|
handleConnection(client: Socket): void;
|
||||||
|
|||||||
Vendored
+154
-19
@@ -48,7 +48,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|||||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||||
};
|
};
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
exports.CommentGateway = void 0;
|
exports.CommentGateway = exports.TourRoleGuard = exports.Roles = exports.ROLES_KEY = void 0;
|
||||||
const dotenv = __importStar(require("dotenv"));
|
const dotenv = __importStar(require("dotenv"));
|
||||||
const path = __importStar(require("path"));
|
const path = __importStar(require("path"));
|
||||||
const envPath = path.resolve(process.cwd(), '..', '.env');
|
const envPath = path.resolve(process.cwd(), '..', '.env');
|
||||||
@@ -62,12 +62,14 @@ const platform_express_1 = require("@nestjs/platform-express");
|
|||||||
const websockets_1 = require("@nestjs/websockets");
|
const websockets_1 = require("@nestjs/websockets");
|
||||||
const socket_io_1 = require("socket.io");
|
const socket_io_1 = require("socket.io");
|
||||||
const prisma_service_1 = require("../prisma/prisma.service");
|
const prisma_service_1 = require("../prisma/prisma.service");
|
||||||
|
const client_1 = require("@prisma/client");
|
||||||
const bcrypt = __importStar(require("bcrypt"));
|
const bcrypt = __importStar(require("bcrypt"));
|
||||||
const admin_guard_1 = require("./auth/admin.guard");
|
const admin_guard_1 = require("./auth/admin.guard");
|
||||||
const jwt_1 = require("@nestjs/jwt");
|
const jwt_1 = require("@nestjs/jwt");
|
||||||
const jwt_auth_guard_1 = require("./auth/jwt-auth.guard");
|
const jwt_auth_guard_1 = require("./auth/jwt-auth.guard");
|
||||||
const jwt_strategy_1 = require("./auth/jwt.strategy");
|
const jwt_strategy_1 = require("./auth/jwt.strategy");
|
||||||
const rbac_middleware_1 = require("./common/rbac.middleware");
|
const core_2 = require("@nestjs/core");
|
||||||
|
const common_2 = require("@nestjs/common");
|
||||||
const UPLOAD_ROOT = path.join(process.cwd(), 'uploads');
|
const UPLOAD_ROOT = path.join(process.cwd(), 'uploads');
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
if (!process.env.DATABASE_URL) {
|
if (!process.env.DATABASE_URL) {
|
||||||
@@ -88,6 +90,41 @@ async function bootstrap() {
|
|||||||
await app.listen(3001);
|
await app.listen(3001);
|
||||||
console.log(`🚀 Server is running on: http://localhost:3001`);
|
console.log(`🚀 Server is running on: http://localhost:3001`);
|
||||||
}
|
}
|
||||||
|
exports.ROLES_KEY = 'roles';
|
||||||
|
const Roles = (...roles) => (0, common_2.SetMetadata)(exports.ROLES_KEY, roles);
|
||||||
|
exports.Roles = Roles;
|
||||||
|
let TourRoleGuard = class TourRoleGuard {
|
||||||
|
constructor(reflector, prisma) {
|
||||||
|
this.reflector = reflector;
|
||||||
|
this.prisma = prisma;
|
||||||
|
}
|
||||||
|
async canActivate(context) {
|
||||||
|
const requiredRoles = this.reflector.getAllAndOverride(exports.ROLES_KEY, [
|
||||||
|
context.getHandler(),
|
||||||
|
context.getClass(),
|
||||||
|
]);
|
||||||
|
const defaultRoles = [client_1.ParticipantRole.OWNER, client_1.ParticipantRole.MANAGER];
|
||||||
|
const rolesToCheck = requiredRoles && requiredRoles.length > 0 ? requiredRoles : defaultRoles;
|
||||||
|
const request = context.switchToHttp().getRequest();
|
||||||
|
const user = request.user;
|
||||||
|
const tourId = request.params.tourId || request.params.id;
|
||||||
|
if (!user || !tourId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const participation = await this.prisma.tourParticipant.findUnique({
|
||||||
|
where: { tourId_userId: { tourId, userId: user.id } },
|
||||||
|
});
|
||||||
|
if (!participation || !rolesToCheck.some(role => participation.role === role)) {
|
||||||
|
throw new common_1.ForbiddenException('Bạn không có quyền thực hiện hành động này trong tour này.');
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
exports.TourRoleGuard = TourRoleGuard;
|
||||||
|
exports.TourRoleGuard = TourRoleGuard = __decorate([
|
||||||
|
(0, common_1.Injectable)(),
|
||||||
|
__metadata("design:paramtypes", [core_2.Reflector, prisma_service_1.PrismaService])
|
||||||
|
], TourRoleGuard);
|
||||||
let AppController = class AppController {
|
let AppController = class AppController {
|
||||||
getHello() {
|
getHello() {
|
||||||
return 'Travel Planning API is running!';
|
return 'Travel Planning API is running!';
|
||||||
@@ -412,11 +449,17 @@ let TourController = class TourController {
|
|||||||
where: { tourId: id }
|
where: { tourId: id }
|
||||||
});
|
});
|
||||||
for (const photo of photos) {
|
for (const photo of photos) {
|
||||||
|
if (photo.imageUrl) {
|
||||||
const displayFilePath = path.join(process.cwd(), photo.imageUrl.replace(/^\//, ''));
|
const displayFilePath = path.join(process.cwd(), photo.imageUrl.replace(/^\//, ''));
|
||||||
if (fs.existsSync(displayFilePath)) {
|
if (fs.existsSync(displayFilePath)) {
|
||||||
fs.unlinkSync(displayFilePath);
|
fs.unlinkSync(displayFilePath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
await this.prisma.photo.updateMany({
|
||||||
|
where: { tourId: id },
|
||||||
|
data: { imageUrl: null }
|
||||||
|
});
|
||||||
await this.prisma.tour.delete({
|
await this.prisma.tour.delete({
|
||||||
where: { id },
|
where: { id },
|
||||||
});
|
});
|
||||||
@@ -694,7 +737,8 @@ __decorate([
|
|||||||
__metadata("design:returntype", Promise)
|
__metadata("design:returntype", Promise)
|
||||||
], TourController.prototype, "createTour", null);
|
], TourController.prototype, "createTour", null);
|
||||||
__decorate([
|
__decorate([
|
||||||
(0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard, rbac_middleware_1.TourRoleGuard),
|
(0, exports.Roles)(client_1.ParticipantRole.OWNER, client_1.ParticipantRole.MANAGER, client_1.ParticipantRole.MEMBER),
|
||||||
|
(0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard, TourRoleGuard),
|
||||||
(0, common_1.Post)(':tourId/locations'),
|
(0, common_1.Post)(':tourId/locations'),
|
||||||
__param(0, (0, common_1.Param)('tourId', common_1.ParseUUIDPipe)),
|
__param(0, (0, common_1.Param)('tourId', common_1.ParseUUIDPipe)),
|
||||||
__param(1, (0, common_1.Body)()),
|
__param(1, (0, common_1.Body)()),
|
||||||
@@ -704,7 +748,8 @@ __decorate([
|
|||||||
__metadata("design:returntype", Promise)
|
__metadata("design:returntype", Promise)
|
||||||
], TourController.prototype, "addLocation", null);
|
], TourController.prototype, "addLocation", null);
|
||||||
__decorate([
|
__decorate([
|
||||||
(0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard, rbac_middleware_1.TourRoleGuard),
|
(0, exports.Roles)(client_1.ParticipantRole.OWNER, client_1.ParticipantRole.MANAGER),
|
||||||
|
(0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard, TourRoleGuard),
|
||||||
(0, common_1.Post)(':tourId/start-point'),
|
(0, common_1.Post)(':tourId/start-point'),
|
||||||
__param(0, (0, common_1.Param)('tourId', common_1.ParseUUIDPipe)),
|
__param(0, (0, common_1.Param)('tourId', common_1.ParseUUIDPipe)),
|
||||||
__param(1, (0, common_1.Body)()),
|
__param(1, (0, common_1.Body)()),
|
||||||
@@ -714,7 +759,8 @@ __decorate([
|
|||||||
__metadata("design:returntype", Promise)
|
__metadata("design:returntype", Promise)
|
||||||
], TourController.prototype, "updateTourStartPoint", null);
|
], TourController.prototype, "updateTourStartPoint", null);
|
||||||
__decorate([
|
__decorate([
|
||||||
(0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard, rbac_middleware_1.TourRoleGuard),
|
(0, exports.Roles)(client_1.ParticipantRole.OWNER, client_1.ParticipantRole.MANAGER),
|
||||||
|
(0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard, TourRoleGuard),
|
||||||
(0, common_1.Post)(':tourId/end-point'),
|
(0, common_1.Post)(':tourId/end-point'),
|
||||||
__param(0, (0, common_1.Param)('tourId', common_1.ParseUUIDPipe)),
|
__param(0, (0, common_1.Param)('tourId', common_1.ParseUUIDPipe)),
|
||||||
__param(1, (0, common_1.Body)()),
|
__param(1, (0, common_1.Body)()),
|
||||||
@@ -724,7 +770,8 @@ __decorate([
|
|||||||
__metadata("design:returntype", Promise)
|
__metadata("design:returntype", Promise)
|
||||||
], TourController.prototype, "updateTourEndPoint", null);
|
], TourController.prototype, "updateTourEndPoint", null);
|
||||||
__decorate([
|
__decorate([
|
||||||
(0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard, rbac_middleware_1.TourRoleGuard),
|
(0, exports.Roles)(client_1.ParticipantRole.OWNER, client_1.ParticipantRole.MANAGER),
|
||||||
|
(0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard, TourRoleGuard),
|
||||||
(0, common_1.Post)(':tourId/legs/batch'),
|
(0, common_1.Post)(':tourId/legs/batch'),
|
||||||
__param(0, (0, common_1.Param)('tourId', common_1.ParseUUIDPipe)),
|
__param(0, (0, common_1.Param)('tourId', common_1.ParseUUIDPipe)),
|
||||||
__param(1, (0, common_1.Body)()),
|
__param(1, (0, common_1.Body)()),
|
||||||
@@ -733,7 +780,8 @@ __decorate([
|
|||||||
__metadata("design:returntype", Promise)
|
__metadata("design:returntype", Promise)
|
||||||
], TourController.prototype, "initializeLegs", null);
|
], TourController.prototype, "initializeLegs", null);
|
||||||
__decorate([
|
__decorate([
|
||||||
(0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard, rbac_middleware_1.TourRoleGuard),
|
(0, exports.Roles)(client_1.ParticipantRole.OWNER, client_1.ParticipantRole.MANAGER),
|
||||||
|
(0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard, TourRoleGuard),
|
||||||
(0, common_1.Post)(':tourId/legs'),
|
(0, common_1.Post)(':tourId/legs'),
|
||||||
__param(0, (0, common_1.Param)('tourId', common_1.ParseUUIDPipe)),
|
__param(0, (0, common_1.Param)('tourId', common_1.ParseUUIDPipe)),
|
||||||
__param(1, (0, common_1.Body)()),
|
__param(1, (0, common_1.Body)()),
|
||||||
@@ -742,7 +790,8 @@ __decorate([
|
|||||||
__metadata("design:returntype", Promise)
|
__metadata("design:returntype", Promise)
|
||||||
], TourController.prototype, "addLeg", null);
|
], TourController.prototype, "addLeg", null);
|
||||||
__decorate([
|
__decorate([
|
||||||
(0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard, rbac_middleware_1.TourRoleGuard),
|
(0, exports.Roles)(client_1.ParticipantRole.OWNER, client_1.ParticipantRole.MANAGER),
|
||||||
|
(0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard, TourRoleGuard),
|
||||||
(0, common_1.Patch)(':id'),
|
(0, common_1.Patch)(':id'),
|
||||||
__param(0, (0, common_1.Param)('id', common_1.ParseUUIDPipe)),
|
__param(0, (0, common_1.Param)('id', common_1.ParseUUIDPipe)),
|
||||||
__param(1, (0, common_1.Body)()),
|
__param(1, (0, common_1.Body)()),
|
||||||
@@ -751,7 +800,8 @@ __decorate([
|
|||||||
__metadata("design:returntype", Promise)
|
__metadata("design:returntype", Promise)
|
||||||
], TourController.prototype, "updateTour", null);
|
], TourController.prototype, "updateTour", null);
|
||||||
__decorate([
|
__decorate([
|
||||||
(0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard, rbac_middleware_1.TourRoleGuard),
|
(0, exports.Roles)(client_1.ParticipantRole.OWNER),
|
||||||
|
(0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard, TourRoleGuard),
|
||||||
(0, common_1.Delete)(':id'),
|
(0, common_1.Delete)(':id'),
|
||||||
__param(0, (0, common_1.Param)('id', common_1.ParseUUIDPipe)),
|
__param(0, (0, common_1.Param)('id', common_1.ParseUUIDPipe)),
|
||||||
__metadata("design:type", Function),
|
__metadata("design:type", Function),
|
||||||
@@ -767,7 +817,8 @@ __decorate([
|
|||||||
__metadata("design:returntype", Promise)
|
__metadata("design:returntype", Promise)
|
||||||
], TourController.prototype, "getPublicTours", null);
|
], TourController.prototype, "getPublicTours", null);
|
||||||
__decorate([
|
__decorate([
|
||||||
(0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard, rbac_middleware_1.TourRoleGuard),
|
(0, exports.Roles)(client_1.ParticipantRole.OWNER, client_1.ParticipantRole.MANAGER, client_1.ParticipantRole.MEMBER, client_1.ParticipantRole.MEMBER_NO_FINANCE, client_1.ParticipantRole.VIEWER_ONLY),
|
||||||
|
(0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard, TourRoleGuard),
|
||||||
(0, common_1.Get)(':id'),
|
(0, common_1.Get)(':id'),
|
||||||
__param(0, (0, common_1.Param)('id', common_1.ParseUUIDPipe)),
|
__param(0, (0, common_1.Param)('id', common_1.ParseUUIDPipe)),
|
||||||
__metadata("design:type", Function),
|
__metadata("design:type", Function),
|
||||||
@@ -775,7 +826,8 @@ __decorate([
|
|||||||
__metadata("design:returntype", Promise)
|
__metadata("design:returntype", Promise)
|
||||||
], TourController.prototype, "getTourDetails", null);
|
], TourController.prototype, "getTourDetails", null);
|
||||||
__decorate([
|
__decorate([
|
||||||
(0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard, rbac_middleware_1.TourRoleGuard),
|
(0, exports.Roles)(client_1.ParticipantRole.OWNER, client_1.ParticipantRole.MANAGER),
|
||||||
|
(0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard, TourRoleGuard),
|
||||||
(0, common_1.Post)(':tourId/members'),
|
(0, common_1.Post)(':tourId/members'),
|
||||||
__param(0, (0, common_1.Param)('tourId', common_1.ParseUUIDPipe)),
|
__param(0, (0, common_1.Param)('tourId', common_1.ParseUUIDPipe)),
|
||||||
__param(1, (0, common_1.Body)()),
|
__param(1, (0, common_1.Body)()),
|
||||||
@@ -785,7 +837,8 @@ __decorate([
|
|||||||
__metadata("design:returntype", Promise)
|
__metadata("design:returntype", Promise)
|
||||||
], TourController.prototype, "addMember", null);
|
], TourController.prototype, "addMember", null);
|
||||||
__decorate([
|
__decorate([
|
||||||
(0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard, rbac_middleware_1.TourRoleGuard),
|
(0, exports.Roles)(client_1.ParticipantRole.OWNER, client_1.ParticipantRole.MANAGER),
|
||||||
|
(0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard, TourRoleGuard),
|
||||||
(0, common_1.Get)(':tourId/join-requests'),
|
(0, common_1.Get)(':tourId/join-requests'),
|
||||||
__param(0, (0, common_1.Param)('tourId', common_1.ParseUUIDPipe)),
|
__param(0, (0, common_1.Param)('tourId', common_1.ParseUUIDPipe)),
|
||||||
__param(1, (0, common_1.Req)()),
|
__param(1, (0, common_1.Req)()),
|
||||||
@@ -794,7 +847,8 @@ __decorate([
|
|||||||
__metadata("design:returntype", Promise)
|
__metadata("design:returntype", Promise)
|
||||||
], TourController.prototype, "getJoinRequests", null);
|
], TourController.prototype, "getJoinRequests", null);
|
||||||
__decorate([
|
__decorate([
|
||||||
(0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard, rbac_middleware_1.TourRoleGuard),
|
(0, exports.Roles)(client_1.ParticipantRole.OWNER, client_1.ParticipantRole.MANAGER, client_1.ParticipantRole.MEMBER, client_1.ParticipantRole.MEMBER_NO_FINANCE, client_1.ParticipantRole.VIEWER_ONLY),
|
||||||
|
(0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard, TourRoleGuard),
|
||||||
(0, common_1.Post)(':tourId/join-requests'),
|
(0, common_1.Post)(':tourId/join-requests'),
|
||||||
__param(0, (0, common_1.Param)('tourId', common_1.ParseUUIDPipe)),
|
__param(0, (0, common_1.Param)('tourId', common_1.ParseUUIDPipe)),
|
||||||
__param(1, (0, common_1.Body)()),
|
__param(1, (0, common_1.Body)()),
|
||||||
@@ -804,7 +858,8 @@ __decorate([
|
|||||||
__metadata("design:returntype", Promise)
|
__metadata("design:returntype", Promise)
|
||||||
], TourController.prototype, "createJoinRequest", null);
|
], TourController.prototype, "createJoinRequest", null);
|
||||||
__decorate([
|
__decorate([
|
||||||
(0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard, rbac_middleware_1.TourRoleGuard),
|
(0, exports.Roles)(client_1.ParticipantRole.OWNER, client_1.ParticipantRole.MANAGER),
|
||||||
|
(0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard, TourRoleGuard),
|
||||||
(0, common_1.Post)(':tourId/join-requests/:requestId/accept'),
|
(0, common_1.Post)(':tourId/join-requests/:requestId/accept'),
|
||||||
__param(0, (0, common_1.Param)('tourId', common_1.ParseUUIDPipe)),
|
__param(0, (0, common_1.Param)('tourId', common_1.ParseUUIDPipe)),
|
||||||
__param(1, (0, common_1.Param)('requestId')),
|
__param(1, (0, common_1.Param)('requestId')),
|
||||||
@@ -814,7 +869,8 @@ __decorate([
|
|||||||
__metadata("design:returntype", Promise)
|
__metadata("design:returntype", Promise)
|
||||||
], TourController.prototype, "acceptJoinRequest", null);
|
], TourController.prototype, "acceptJoinRequest", null);
|
||||||
__decorate([
|
__decorate([
|
||||||
(0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard, rbac_middleware_1.TourRoleGuard),
|
(0, exports.Roles)(client_1.ParticipantRole.OWNER),
|
||||||
|
(0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard, TourRoleGuard),
|
||||||
(0, common_1.Post)(':tourId/join-requests/:requestId/reject'),
|
(0, common_1.Post)(':tourId/join-requests/:requestId/reject'),
|
||||||
__param(0, (0, common_1.Param)('tourId', common_1.ParseUUIDPipe)),
|
__param(0, (0, common_1.Param)('tourId', common_1.ParseUUIDPipe)),
|
||||||
__param(1, (0, common_1.Param)('requestId')),
|
__param(1, (0, common_1.Param)('requestId')),
|
||||||
@@ -824,7 +880,8 @@ __decorate([
|
|||||||
__metadata("design:returntype", Promise)
|
__metadata("design:returntype", Promise)
|
||||||
], TourController.prototype, "rejectJoinRequest", null);
|
], TourController.prototype, "rejectJoinRequest", null);
|
||||||
__decorate([
|
__decorate([
|
||||||
(0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard, rbac_middleware_1.TourRoleGuard),
|
(0, exports.Roles)(client_1.ParticipantRole.OWNER, client_1.ParticipantRole.MANAGER),
|
||||||
|
(0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard, TourRoleGuard),
|
||||||
(0, common_1.Delete)(':tourId/members/:userId'),
|
(0, common_1.Delete)(':tourId/members/:userId'),
|
||||||
__param(0, (0, common_1.Param)('tourId', common_1.ParseUUIDPipe)),
|
__param(0, (0, common_1.Param)('tourId', common_1.ParseUUIDPipe)),
|
||||||
__param(1, (0, common_1.Param)('userId', common_1.ParseUUIDPipe)),
|
__param(1, (0, common_1.Param)('userId', common_1.ParseUUIDPipe)),
|
||||||
@@ -833,7 +890,8 @@ __decorate([
|
|||||||
__metadata("design:returntype", Promise)
|
__metadata("design:returntype", Promise)
|
||||||
], TourController.prototype, "removeMember", null);
|
], TourController.prototype, "removeMember", null);
|
||||||
__decorate([
|
__decorate([
|
||||||
(0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard, rbac_middleware_1.TourRoleGuard),
|
(0, exports.Roles)(client_1.ParticipantRole.OWNER, client_1.ParticipantRole.MANAGER, client_1.ParticipantRole.MEMBER, client_1.ParticipantRole.MEMBER_NO_FINANCE),
|
||||||
|
(0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard, TourRoleGuard),
|
||||||
(0, common_1.Post)(':tourId/photos'),
|
(0, common_1.Post)(':tourId/photos'),
|
||||||
(0, common_1.UseInterceptors)((0, platform_express_1.FilesInterceptor)('images', 10)),
|
(0, common_1.UseInterceptors)((0, platform_express_1.FilesInterceptor)('images', 10)),
|
||||||
__param(0, (0, common_1.Param)('tourId', common_1.ParseUUIDPipe)),
|
__param(0, (0, common_1.Param)('tourId', common_1.ParseUUIDPipe)),
|
||||||
@@ -961,6 +1019,7 @@ __decorate([
|
|||||||
], LegController.prototype, "deleteLeg", null);
|
], LegController.prototype, "deleteLeg", null);
|
||||||
LegController = __decorate([
|
LegController = __decorate([
|
||||||
(0, common_1.Controller)('legs'),
|
(0, common_1.Controller)('legs'),
|
||||||
|
(0, exports.Roles)(client_1.ParticipantRole.OWNER, client_1.ParticipantRole.MANAGER),
|
||||||
(0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard),
|
(0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard),
|
||||||
__metadata("design:paramtypes", [prisma_service_1.PrismaService])
|
__metadata("design:paramtypes", [prisma_service_1.PrismaService])
|
||||||
], LegController);
|
], LegController);
|
||||||
@@ -1063,8 +1122,53 @@ __decorate([
|
|||||||
], RoutingController.prototype, "optimize", null);
|
], RoutingController.prototype, "optimize", null);
|
||||||
RoutingController = __decorate([
|
RoutingController = __decorate([
|
||||||
(0, common_1.Controller)('routing'),
|
(0, common_1.Controller)('routing'),
|
||||||
|
(0, exports.Roles)(client_1.ParticipantRole.OWNER, client_1.ParticipantRole.MANAGER),
|
||||||
__metadata("design:paramtypes", [prisma_service_1.PrismaService])
|
__metadata("design:paramtypes", [prisma_service_1.PrismaService])
|
||||||
], RoutingController);
|
], RoutingController);
|
||||||
|
let PhotoController = class PhotoController {
|
||||||
|
constructor(prisma) {
|
||||||
|
this.prisma = prisma;
|
||||||
|
}
|
||||||
|
async deletePhoto(id, req) {
|
||||||
|
const photo = await this.prisma.photo.findUnique({
|
||||||
|
where: { id },
|
||||||
|
});
|
||||||
|
if (!photo) {
|
||||||
|
throw new common_1.NotFoundException('Không tìm thấy ảnh.');
|
||||||
|
}
|
||||||
|
if (photo.uploaderId !== req.user.id) {
|
||||||
|
throw new common_1.ForbiddenException('Bạn không có quyền xóa ảnh này.');
|
||||||
|
}
|
||||||
|
if (photo.imageUrl) {
|
||||||
|
const displayFilePath = path.join(process.cwd(), photo.imageUrl.replace(/^\//, ''));
|
||||||
|
if (fs.existsSync(displayFilePath)) {
|
||||||
|
fs.unlinkSync(displayFilePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (photo.originalUrl) {
|
||||||
|
const originalFilePath = path.join(process.cwd(), photo.originalUrl.replace(/^\//, ''));
|
||||||
|
if (fs.existsSync(originalFilePath)) {
|
||||||
|
fs.unlinkSync(originalFilePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await this.prisma.photo.delete({ where: { id } });
|
||||||
|
return { message: 'Ảnh đã được xóa thành công.' };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
__decorate([
|
||||||
|
(0, common_1.Delete)(':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)
|
||||||
|
], PhotoController.prototype, "deletePhoto", null);
|
||||||
|
PhotoController = __decorate([
|
||||||
|
(0, common_1.Controller)('photos'),
|
||||||
|
(0, exports.Roles)(client_1.ParticipantRole.OWNER, client_1.ParticipantRole.MANAGER, client_1.ParticipantRole.MEMBER, client_1.ParticipantRole.MEMBER_NO_FINANCE),
|
||||||
|
(0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard),
|
||||||
|
__metadata("design:paramtypes", [prisma_service_1.PrismaService])
|
||||||
|
], PhotoController);
|
||||||
let UserController = class UserController {
|
let UserController = class UserController {
|
||||||
constructor(prisma) {
|
constructor(prisma) {
|
||||||
this.prisma = prisma;
|
this.prisma = prisma;
|
||||||
@@ -1084,6 +1188,15 @@ let UserController = class UserController {
|
|||||||
});
|
});
|
||||||
return users.filter((u) => u.id !== currentUserId);
|
return users.filter((u) => u.id !== currentUserId);
|
||||||
}
|
}
|
||||||
|
async getMyPhotos(req) {
|
||||||
|
return this.prisma.photo.findMany({
|
||||||
|
where: { uploaderId: req.user.id },
|
||||||
|
include: {
|
||||||
|
tour: { select: { title: true } }
|
||||||
|
},
|
||||||
|
orderBy: { capturedAt: 'desc' }
|
||||||
|
});
|
||||||
|
}
|
||||||
async updateUser(id, data) {
|
async updateUser(id, data) {
|
||||||
if (data.password) {
|
if (data.password) {
|
||||||
data.passwordHash = await bcrypt.hash(data.password, 10);
|
data.passwordHash = await bcrypt.hash(data.password, 10);
|
||||||
@@ -1105,6 +1218,16 @@ let UserController = class UserController {
|
|||||||
throw new common_1.BadRequestException('Không thể xóa Quản trị viên cuối cùng');
|
throw new common_1.BadRequestException('Không thể xóa Quản trị viên cuối cùng');
|
||||||
}
|
}
|
||||||
const memberDir = path.join(UPLOAD_ROOT, 'members', id);
|
const memberDir = path.join(UPLOAD_ROOT, 'members', id);
|
||||||
|
const photos = await this.prisma.photo.findMany({
|
||||||
|
where: { uploaderId: id }
|
||||||
|
});
|
||||||
|
for (const photo of photos) {
|
||||||
|
if (photo.imageUrl) {
|
||||||
|
const displayFilePath = path.join(process.cwd(), photo.imageUrl.replace(/^\//, ''));
|
||||||
|
if (fs.existsSync(displayFilePath))
|
||||||
|
fs.unlinkSync(displayFilePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
await this.prisma.photo.deleteMany({ where: { uploaderId: id } });
|
await this.prisma.photo.deleteMany({ where: { uploaderId: id } });
|
||||||
await this.prisma.tourParticipant.deleteMany({ where: { userId: id } });
|
await this.prisma.tourParticipant.deleteMany({ where: { userId: id } });
|
||||||
await this.prisma.user.delete({ where: { id } });
|
await this.prisma.user.delete({ where: { id } });
|
||||||
@@ -1134,6 +1257,15 @@ __decorate([
|
|||||||
__metadata("design:returntype", Promise)
|
__metadata("design:returntype", Promise)
|
||||||
], UserController.prototype, "getAllUsers", null);
|
], UserController.prototype, "getAllUsers", null);
|
||||||
__decorate([
|
__decorate([
|
||||||
|
(0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard),
|
||||||
|
(0, common_1.Get)('me/photos'),
|
||||||
|
__param(0, (0, common_1.Req)()),
|
||||||
|
__metadata("design:type", Function),
|
||||||
|
__metadata("design:paramtypes", [Object]),
|
||||||
|
__metadata("design:returntype", Promise)
|
||||||
|
], UserController.prototype, "getMyPhotos", null);
|
||||||
|
__decorate([
|
||||||
|
(0, exports.Roles)(client_1.ParticipantRole.OWNER, client_1.ParticipantRole.MANAGER),
|
||||||
(0, common_1.Patch)(':id'),
|
(0, common_1.Patch)(':id'),
|
||||||
__param(0, (0, common_1.Param)('id', common_1.ParseUUIDPipe)),
|
__param(0, (0, common_1.Param)('id', common_1.ParseUUIDPipe)),
|
||||||
__param(1, (0, common_1.Body)()),
|
__param(1, (0, common_1.Body)()),
|
||||||
@@ -1142,6 +1274,7 @@ __decorate([
|
|||||||
__metadata("design:returntype", Promise)
|
__metadata("design:returntype", Promise)
|
||||||
], UserController.prototype, "updateUser", null);
|
], UserController.prototype, "updateUser", null);
|
||||||
__decorate([
|
__decorate([
|
||||||
|
(0, exports.Roles)(client_1.ParticipantRole.OWNER),
|
||||||
(0, common_1.Delete)(':id'),
|
(0, common_1.Delete)(':id'),
|
||||||
__param(0, (0, common_1.Param)('id', common_1.ParseUUIDPipe)),
|
__param(0, (0, common_1.Param)('id', common_1.ParseUUIDPipe)),
|
||||||
__metadata("design:type", Function),
|
__metadata("design:type", Function),
|
||||||
@@ -1149,6 +1282,7 @@ __decorate([
|
|||||||
__metadata("design:returntype", Promise)
|
__metadata("design:returntype", Promise)
|
||||||
], UserController.prototype, "deleteUser", null);
|
], UserController.prototype, "deleteUser", null);
|
||||||
__decorate([
|
__decorate([
|
||||||
|
(0, exports.Roles)(client_1.ParticipantRole.OWNER, client_1.ParticipantRole.MANAGER),
|
||||||
(0, common_1.Post)('block/:id'),
|
(0, common_1.Post)('block/:id'),
|
||||||
__param(0, (0, common_1.Param)('id', common_1.ParseUUIDPipe)),
|
__param(0, (0, common_1.Param)('id', common_1.ParseUUIDPipe)),
|
||||||
__metadata("design:type", Function),
|
__metadata("design:type", Function),
|
||||||
@@ -1157,6 +1291,7 @@ __decorate([
|
|||||||
], UserController.prototype, "toggleBlock", null);
|
], UserController.prototype, "toggleBlock", null);
|
||||||
UserController = __decorate([
|
UserController = __decorate([
|
||||||
(0, common_1.Controller)('users'),
|
(0, common_1.Controller)('users'),
|
||||||
|
(0, exports.Roles)(client_1.ParticipantRole.OWNER, client_1.ParticipantRole.MANAGER),
|
||||||
__metadata("design:paramtypes", [prisma_service_1.PrismaService])
|
__metadata("design:paramtypes", [prisma_service_1.PrismaService])
|
||||||
], UserController);
|
], UserController);
|
||||||
let CommentGateway = class CommentGateway {
|
let CommentGateway = class CommentGateway {
|
||||||
@@ -1253,8 +1388,8 @@ AppModule = __decorate([
|
|||||||
signOptions: { expiresIn: '1d' },
|
signOptions: { expiresIn: '1d' },
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
controllers: [AppController, AuthController, PublicTourController, TourController, UserController, RoutingController, LegController, LocationController, CommentController],
|
controllers: [AppController, AuthController, PublicTourController, TourController, UserController, RoutingController, LegController, LocationController, CommentController, PhotoController],
|
||||||
providers: [prisma_service_1.PrismaService, jwt_strategy_1.JwtStrategy, rbac_middleware_1.TourRoleGuard, jwt_auth_guard_1.JwtAuthGuard, admin_guard_1.AdminGuard, CommentGateway],
|
providers: [prisma_service_1.PrismaService, jwt_strategy_1.JwtStrategy, TourRoleGuard, jwt_auth_guard_1.JwtAuthGuard, admin_guard_1.AdminGuard, CommentGateway, core_2.Reflector],
|
||||||
exports: [prisma_service_1.PrismaService]
|
exports: [prisma_service_1.PrismaService]
|
||||||
})
|
})
|
||||||
], AppModule);
|
], AppModule);
|
||||||
|
|||||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
@@ -180,16 +180,16 @@ model Expense {
|
|||||||
|
|
||||||
model Photo {
|
model Photo {
|
||||||
id String @id @default(uuid())
|
id String @id @default(uuid())
|
||||||
tourId String
|
tourId String?
|
||||||
locationId String?
|
locationId String?
|
||||||
uploaderId String
|
uploaderId String
|
||||||
imageUrl String
|
imageUrl String?
|
||||||
originalUrl String?
|
originalUrl String?
|
||||||
capturedAt DateTime @default(now())
|
capturedAt DateTime @default(now())
|
||||||
metadata Json?
|
metadata Json?
|
||||||
privacy PrivacyLevel @default(TOUR_ONLY)
|
privacy PrivacyLevel @default(TOUR_ONLY)
|
||||||
|
|
||||||
tour Tour @relation(fields: [tourId], references: [id], onDelete: Cascade)
|
tour Tour? @relation(fields: [tourId], references: [id], onDelete: SetNull)
|
||||||
location Location? @relation(fields: [locationId], references: [id], onDelete: SetNull)
|
location Location? @relation(fields: [locationId], references: [id], onDelete: SetNull)
|
||||||
uploader User @relation(fields: [uploaderId], references: [id])
|
uploader User @relation(fields: [uploaderId], references: [id])
|
||||||
}
|
}
|
||||||
|
|||||||
+142
-5
@@ -16,12 +16,14 @@ import { diskStorage } from 'multer';
|
|||||||
import { WebSocketGateway, WebSocketServer, SubscribeMessage, OnGatewayConnection, OnGatewayDisconnect } from '@nestjs/websockets';
|
import { WebSocketGateway, WebSocketServer, SubscribeMessage, OnGatewayConnection, OnGatewayDisconnect } from '@nestjs/websockets';
|
||||||
import { Server, Socket } from 'socket.io';
|
import { Server, Socket } from 'socket.io';
|
||||||
import { PrismaService } from '../prisma/prisma.service';
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
|
import { ParticipantRole } from '@prisma/client';
|
||||||
import * as bcrypt from 'bcrypt';
|
import * as bcrypt from 'bcrypt';
|
||||||
import { AdminGuard } from './auth/admin.guard';
|
import { AdminGuard } from './auth/admin.guard';
|
||||||
import { JwtModule, JwtService } from '@nestjs/jwt';
|
import { JwtModule, JwtService } from '@nestjs/jwt';
|
||||||
import { JwtAuthGuard } from './auth/jwt-auth.guard';
|
import { JwtAuthGuard } from './auth/jwt-auth.guard';
|
||||||
import { JwtStrategy } from './auth/jwt.strategy';
|
import { JwtStrategy } from './auth/jwt.strategy';
|
||||||
import { TourRoleGuard } from './common/rbac.middleware';
|
import { Reflector } from '@nestjs/core';
|
||||||
|
import { SetMetadata, CanActivate, ExecutionContext } from '@nestjs/common';
|
||||||
|
|
||||||
// Khai báo vị trí thư mục upload cụ thể
|
// Khai báo vị trí thư mục upload cụ thể
|
||||||
const UPLOAD_ROOT = path.join(process.cwd(), 'uploads');
|
const UPLOAD_ROOT = path.join(process.cwd(), 'uploads');
|
||||||
@@ -56,6 +58,45 @@ async function bootstrap() {
|
|||||||
console.log(`🚀 Server is running on: http://localhost:3001`);
|
console.log(`🚀 Server is running on: http://localhost:3001`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Define ROLES_KEY and Roles decorator
|
||||||
|
export const ROLES_KEY = 'roles';
|
||||||
|
export const Roles = (...roles: ParticipantRole[]) => SetMetadata(ROLES_KEY, roles);
|
||||||
|
|
||||||
|
// Implement TourRoleGuard (assuming it's here or similar to this)
|
||||||
|
// This guard checks if the user is a participant of the tour and has one of the required roles.
|
||||||
|
@Injectable()
|
||||||
|
export class TourRoleGuard implements CanActivate {
|
||||||
|
constructor(private reflector: Reflector, private prisma: PrismaService) {}
|
||||||
|
|
||||||
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||||
|
const requiredRoles = this.reflector.getAllAndOverride<ParticipantRole[]>(ROLES_KEY, [
|
||||||
|
context.getHandler(),
|
||||||
|
context.getClass(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// If no specific roles are required, default to OWNER and MANAGER for editing actions
|
||||||
|
const defaultRoles = [ParticipantRole.OWNER, ParticipantRole.MANAGER];
|
||||||
|
const rolesToCheck = requiredRoles && requiredRoles.length > 0 ? requiredRoles : defaultRoles;
|
||||||
|
|
||||||
|
const request = context.switchToHttp().getRequest();
|
||||||
|
const user = request.user; // User object from JwtAuthGuard
|
||||||
|
// Kiểm tra cả 'tourId' và 'id' để tương thích với các route khác nhau
|
||||||
|
const tourId = request.params.tourId || request.params.id;
|
||||||
|
|
||||||
|
if (!user || !tourId) {
|
||||||
|
return false; // User or tourId not available
|
||||||
|
}
|
||||||
|
|
||||||
|
const participation = await this.prisma.tourParticipant.findUnique({
|
||||||
|
where: { tourId_userId: { tourId, userId: user.id } },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!participation || !rolesToCheck.some(role => participation.role === role)) {
|
||||||
|
throw new ForbiddenException('Bạn không có quyền thực hiện hành động này trong tour này.');
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Controller()
|
@Controller()
|
||||||
class AppController {
|
class AppController {
|
||||||
@@ -202,6 +243,7 @@ class TourController {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Roles(ParticipantRole.OWNER, ParticipantRole.MANAGER, ParticipantRole.MEMBER) // Allow members to add locations
|
||||||
@UseGuards(JwtAuthGuard, TourRoleGuard)
|
@UseGuards(JwtAuthGuard, TourRoleGuard)
|
||||||
@Post(':tourId/locations')
|
@Post(':tourId/locations')
|
||||||
async addLocation(@Param('tourId', ParseUUIDPipe) tourId: string, @Body() body: any, @Req() req: any) {
|
async addLocation(@Param('tourId', ParseUUIDPipe) tourId: string, @Body() body: any, @Req() req: any) {
|
||||||
@@ -244,6 +286,7 @@ class TourController {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Roles(ParticipantRole.OWNER, ParticipantRole.MANAGER) // Only owner/manager can set start point
|
||||||
@UseGuards(JwtAuthGuard, TourRoleGuard)
|
@UseGuards(JwtAuthGuard, TourRoleGuard)
|
||||||
@Post(':tourId/start-point')
|
@Post(':tourId/start-point')
|
||||||
async updateTourStartPoint(@Param('tourId', ParseUUIDPipe) tourId: string, @Body() body: any, @Req() req: any) {
|
async updateTourStartPoint(@Param('tourId', ParseUUIDPipe) tourId: string, @Body() body: any, @Req() req: any) {
|
||||||
@@ -281,6 +324,7 @@ class TourController {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Roles(ParticipantRole.OWNER, ParticipantRole.MANAGER) // Only owner/manager can set end point
|
||||||
@UseGuards(JwtAuthGuard, TourRoleGuard)
|
@UseGuards(JwtAuthGuard, TourRoleGuard)
|
||||||
@Post(':tourId/end-point')
|
@Post(':tourId/end-point')
|
||||||
async updateTourEndPoint(@Param('tourId', ParseUUIDPipe) tourId: string, @Body() body: any, @Req() req: any) {
|
async updateTourEndPoint(@Param('tourId', ParseUUIDPipe) tourId: string, @Body() body: any, @Req() req: any) {
|
||||||
@@ -316,6 +360,7 @@ class TourController {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Roles(ParticipantRole.OWNER, ParticipantRole.MANAGER) // Only owner/manager can initialize legs
|
||||||
@UseGuards(JwtAuthGuard, TourRoleGuard)
|
@UseGuards(JwtAuthGuard, TourRoleGuard)
|
||||||
@Post(':tourId/legs/batch')
|
@Post(':tourId/legs/batch')
|
||||||
async initializeLegs(@Param('tourId', ParseUUIDPipe) tourId: string, @Body() body: { count: number }) {
|
async initializeLegs(@Param('tourId', ParseUUIDPipe) tourId: string, @Body() body: { count: number }) {
|
||||||
@@ -361,6 +406,7 @@ class TourController {
|
|||||||
return allLegs;
|
return allLegs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Roles(ParticipantRole.OWNER, ParticipantRole.MANAGER) // Only owner/manager can add legs
|
||||||
@UseGuards(JwtAuthGuard, TourRoleGuard)
|
@UseGuards(JwtAuthGuard, TourRoleGuard)
|
||||||
@Post(':tourId/legs')
|
@Post(':tourId/legs')
|
||||||
async addLeg(@Param('tourId', ParseUUIDPipe) tourId: string, @Body() body: any) {
|
async addLeg(@Param('tourId', ParseUUIDPipe) tourId: string, @Body() body: any) {
|
||||||
@@ -379,6 +425,7 @@ class TourController {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Roles(ParticipantRole.OWNER, ParticipantRole.MANAGER) // Only owner/manager can update tour details
|
||||||
@UseGuards(JwtAuthGuard, TourRoleGuard)
|
@UseGuards(JwtAuthGuard, TourRoleGuard)
|
||||||
@Patch(':id')
|
@Patch(':id')
|
||||||
async updateTour(@Param('id', ParseUUIDPipe) id: string, @Body() body: any) {
|
async updateTour(@Param('id', ParseUUIDPipe) id: string, @Body() body: any) {
|
||||||
@@ -398,6 +445,7 @@ class TourController {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Roles(ParticipantRole.OWNER) // Only owner can delete tour
|
||||||
@UseGuards(JwtAuthGuard, TourRoleGuard)
|
@UseGuards(JwtAuthGuard, TourRoleGuard)
|
||||||
@Delete(':id')
|
@Delete(':id')
|
||||||
async deleteTour(@Param('id', ParseUUIDPipe) id: string) {
|
async deleteTour(@Param('id', ParseUUIDPipe) id: string) {
|
||||||
@@ -408,19 +456,27 @@ class TourController {
|
|||||||
|
|
||||||
// 2. Xóa các file vật lý (ảnh 2K) trong thư mục uploads/tours
|
// 2. Xóa các file vật lý (ảnh 2K) trong thư mục uploads/tours
|
||||||
for (const photo of photos) {
|
for (const photo of photos) {
|
||||||
|
if (photo.imageUrl) {
|
||||||
const displayFilePath = path.join(process.cwd(), photo.imageUrl.replace(/^\//, ''));
|
const displayFilePath = path.join(process.cwd(), photo.imageUrl.replace(/^\//, ''));
|
||||||
if (fs.existsSync(displayFilePath)) {
|
if (fs.existsSync(displayFilePath)) {
|
||||||
fs.unlinkSync(displayFilePath);
|
fs.unlinkSync(displayFilePath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cập nhật DB: Xóa link ảnh 2K vì file vật lý đã bị xóa, tourId sẽ tự động SetNull
|
||||||
|
await this.prisma.photo.updateMany({
|
||||||
|
where: { tourId: id },
|
||||||
|
data: { imageUrl: null }
|
||||||
|
});
|
||||||
|
|
||||||
// Xóa tour sẽ xóa cascade các Leg, Location, Expense nhờ config onDelete: Cascade trong schema
|
|
||||||
await this.prisma.tour.delete({
|
await this.prisma.tour.delete({
|
||||||
where: { id },
|
where: { id },
|
||||||
});
|
});
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getPublicTours does not need TourRoleGuard as it's for any logged-in user to see their tours
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@Get('explore')
|
@Get('explore')
|
||||||
async getPublicTours(@Req() req: any) {
|
async getPublicTours(@Req() req: any) {
|
||||||
@@ -451,6 +507,7 @@ class TourController {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Roles(ParticipantRole.OWNER, ParticipantRole.MANAGER, ParticipantRole.MEMBER, ParticipantRole.MEMBER_NO_FINANCE, ParticipantRole.VIEWER_ONLY) // Any participant can view tour details
|
||||||
@UseGuards(JwtAuthGuard, TourRoleGuard)
|
@UseGuards(JwtAuthGuard, TourRoleGuard)
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
async getTourDetails(@Param('id', ParseUUIDPipe) id: string) {
|
async getTourDetails(@Param('id', ParseUUIDPipe) id: string) {
|
||||||
@@ -487,6 +544,7 @@ class TourController {
|
|||||||
return tour;
|
return tour;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Roles(ParticipantRole.OWNER, ParticipantRole.MANAGER) // Only owner/manager can add members
|
||||||
@UseGuards(JwtAuthGuard, TourRoleGuard)
|
@UseGuards(JwtAuthGuard, TourRoleGuard)
|
||||||
@Post(':tourId/members')
|
@Post(':tourId/members')
|
||||||
async addMember(@Param('tourId', ParseUUIDPipe) tourId: string, @Body() body: { userId: string; role?: string }, @Req() req: any) {
|
async addMember(@Param('tourId', ParseUUIDPipe) tourId: string, @Body() body: { userId: string; role?: string }, @Req() req: any) {
|
||||||
@@ -537,6 +595,7 @@ class TourController {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Roles(ParticipantRole.OWNER, ParticipantRole.MANAGER) // Only owner/manager can view join requests
|
||||||
@UseGuards(JwtAuthGuard, TourRoleGuard)
|
@UseGuards(JwtAuthGuard, TourRoleGuard)
|
||||||
@Get(':tourId/join-requests')
|
@Get(':tourId/join-requests')
|
||||||
async getJoinRequests(@Param('tourId', ParseUUIDPipe) tourId: string, @Req() req: any) {
|
async getJoinRequests(@Param('tourId', ParseUUIDPipe) tourId: string, @Req() req: any) {
|
||||||
@@ -552,6 +611,7 @@ class TourController {
|
|||||||
return requests;
|
return requests;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Roles(ParticipantRole.OWNER, ParticipantRole.MANAGER, ParticipantRole.MEMBER, ParticipantRole.MEMBER_NO_FINANCE, ParticipantRole.VIEWER_ONLY) // Any participant can create a join request for themselves or others (if they have permission)
|
||||||
@UseGuards(JwtAuthGuard, TourRoleGuard)
|
@UseGuards(JwtAuthGuard, TourRoleGuard)
|
||||||
@Post(':tourId/join-requests')
|
@Post(':tourId/join-requests')
|
||||||
async createJoinRequest(@Param('tourId', ParseUUIDPipe) tourId: string, @Body() body: { userId?: string }, @Req() req: any) {
|
async createJoinRequest(@Param('tourId', ParseUUIDPipe) tourId: string, @Body() body: { userId?: string }, @Req() req: any) {
|
||||||
@@ -587,6 +647,7 @@ class TourController {
|
|||||||
return joinRequest;
|
return joinRequest;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Roles(ParticipantRole.OWNER, ParticipantRole.MANAGER) // Only owner/manager can accept join requests
|
||||||
@UseGuards(JwtAuthGuard, TourRoleGuard)
|
@UseGuards(JwtAuthGuard, TourRoleGuard)
|
||||||
@Post(':tourId/join-requests/:requestId/accept')
|
@Post(':tourId/join-requests/:requestId/accept')
|
||||||
async acceptJoinRequest(@Param('tourId', ParseUUIDPipe) tourId: string, @Param('requestId') requestId: string, @Req() req: any) {
|
async acceptJoinRequest(@Param('tourId', ParseUUIDPipe) tourId: string, @Param('requestId') requestId: string, @Req() req: any) {
|
||||||
@@ -639,6 +700,7 @@ class TourController {
|
|||||||
return { success: true, message: 'Đã chấp nhận yêu cầu tham gia.' };
|
return { success: true, message: 'Đã chấp nhận yêu cầu tham gia.' };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Roles(ParticipantRole.OWNER) // Only owner can reject join requests
|
||||||
@UseGuards(JwtAuthGuard, TourRoleGuard)
|
@UseGuards(JwtAuthGuard, TourRoleGuard)
|
||||||
@Post(':tourId/join-requests/:requestId/reject')
|
@Post(':tourId/join-requests/:requestId/reject')
|
||||||
async rejectJoinRequest(@Param('tourId', ParseUUIDPipe) tourId: string, @Param('requestId') requestId: string, @Req() req: any) {
|
async rejectJoinRequest(@Param('tourId', ParseUUIDPipe) tourId: string, @Param('requestId') requestId: string, @Req() req: any) {
|
||||||
@@ -671,6 +733,7 @@ class TourController {
|
|||||||
return { success: true, message: 'Đã từ chối yêu cầu tham gia.' };
|
return { success: true, message: 'Đã từ chối yêu cầu tham gia.' };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Roles(ParticipantRole.OWNER, ParticipantRole.MANAGER) // Only owner/manager can remove members
|
||||||
@UseGuards(JwtAuthGuard, TourRoleGuard)
|
@UseGuards(JwtAuthGuard, TourRoleGuard)
|
||||||
@Delete(':tourId/members/:userId')
|
@Delete(':tourId/members/:userId')
|
||||||
async removeMember(@Param('tourId', ParseUUIDPipe) tourId: string, @Param('userId', ParseUUIDPipe) userId: string) {
|
async removeMember(@Param('tourId', ParseUUIDPipe) tourId: string, @Param('userId', ParseUUIDPipe) userId: string) {
|
||||||
@@ -686,7 +749,8 @@ class TourController {
|
|||||||
return { message: 'Đã xóa thành viên khỏi tour' };
|
return { message: 'Đã xóa thành viên khỏi tour' };
|
||||||
}
|
}
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard, TourRoleGuard)
|
@Roles(ParticipantRole.OWNER, ParticipantRole.MANAGER, ParticipantRole.MEMBER, ParticipantRole.MEMBER_NO_FINANCE) // All members can upload photos
|
||||||
|
@UseGuards(JwtAuthGuard, TourRoleGuard) // TourRoleGuard will now check for these roles
|
||||||
@Post(':tourId/photos')
|
@Post(':tourId/photos')
|
||||||
@UseInterceptors(FilesInterceptor('images', 10)) // Chuyển sang memory storage để xử lý ảnh trước khi lưu
|
@UseInterceptors(FilesInterceptor('images', 10)) // Chuyển sang memory storage để xử lý ảnh trước khi lưu
|
||||||
async uploadPhotos(@Param('tourId', ParseUUIDPipe) tourId: string, @UploadedFiles() files: any[], @Req() req: any) {
|
async uploadPhotos(@Param('tourId', ParseUUIDPipe) tourId: string, @UploadedFiles() files: any[], @Req() req: any) {
|
||||||
@@ -797,6 +861,7 @@ class LocationController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Controller('legs')
|
@Controller('legs')
|
||||||
|
@Roles(ParticipantRole.OWNER, ParticipantRole.MANAGER) // Default roles for Leg operations
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
class LegController {
|
class LegController {
|
||||||
constructor(private prisma: PrismaService) {}
|
constructor(private prisma: PrismaService) {}
|
||||||
@@ -844,6 +909,7 @@ function calculateDistance(lat1: number, lon1: number, lat2: number, lon2: numbe
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Controller('routing')
|
@Controller('routing')
|
||||||
|
@Roles(ParticipantRole.OWNER, ParticipantRole.MANAGER) // Default roles for Routing operations
|
||||||
class RoutingController {
|
class RoutingController {
|
||||||
constructor(private prisma: PrismaService) {}
|
constructor(private prisma: PrismaService) {}
|
||||||
|
|
||||||
@@ -956,7 +1022,50 @@ class RoutingController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Controller('photos')
|
||||||
|
@Roles(ParticipantRole.OWNER, ParticipantRole.MANAGER, ParticipantRole.MEMBER, ParticipantRole.MEMBER_NO_FINANCE) // All members can delete their own photos
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
class PhotoController {
|
||||||
|
constructor(private prisma: PrismaService) {}
|
||||||
|
|
||||||
|
@Delete(':id')
|
||||||
|
async deletePhoto(@Param('id', ParseUUIDPipe) id: string, @Req() req: any) {
|
||||||
|
const photo = await this.prisma.photo.findUnique({
|
||||||
|
where: { id },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!photo) {
|
||||||
|
throw new NotFoundException('Không tìm thấy ảnh.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chỉ người tải lên mới có quyền xóa ảnh của họ
|
||||||
|
if (photo.uploaderId !== req.user.id) {
|
||||||
|
throw new ForbiddenException('Bạn không có quyền xóa ảnh này.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Xóa file 2K (imageUrl) nếu tồn tại
|
||||||
|
if (photo.imageUrl) {
|
||||||
|
const displayFilePath = path.join(process.cwd(), photo.imageUrl.replace(/^\//, ''));
|
||||||
|
if (fs.existsSync(displayFilePath)) {
|
||||||
|
fs.unlinkSync(displayFilePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Xóa file gốc (originalUrl) nếu tồn tại
|
||||||
|
if (photo.originalUrl) {
|
||||||
|
const originalFilePath = path.join(process.cwd(), photo.originalUrl.replace(/^\//, ''));
|
||||||
|
if (fs.existsSync(originalFilePath)) {
|
||||||
|
fs.unlinkSync(originalFilePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.prisma.photo.delete({ where: { id } });
|
||||||
|
return { message: 'Ảnh đã được xóa thành công.' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Controller('users')
|
@Controller('users')
|
||||||
|
@Roles(ParticipantRole.OWNER, ParticipantRole.MANAGER) // Default roles for User management (Admin/Manager)
|
||||||
class UserController {
|
class UserController {
|
||||||
constructor(private prisma: PrismaService) {}
|
constructor(private prisma: PrismaService) {}
|
||||||
|
|
||||||
@@ -977,6 +1086,20 @@ class UserController {
|
|||||||
return users.filter((u: any) => u.id !== currentUserId);
|
return users.filter((u: any) => u.id !== currentUserId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getMyPhotos does not need TourRoleGuard as it's for the user's own photos
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
@Get('me/photos')
|
||||||
|
async getMyPhotos(@Req() req: any) {
|
||||||
|
return this.prisma.photo.findMany({
|
||||||
|
where: { uploaderId: req.user.id },
|
||||||
|
include: {
|
||||||
|
tour: { select: { title: true } }
|
||||||
|
},
|
||||||
|
orderBy: { capturedAt: 'desc' }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Roles(ParticipantRole.OWNER, ParticipantRole.MANAGER) // Default roles for updating user (Admin/Manager)
|
||||||
@Patch(':id')
|
@Patch(':id')
|
||||||
async updateUser(@Param('id', ParseUUIDPipe) id: string, @Body() data: any) {
|
async updateUser(@Param('id', ParseUUIDPipe) id: string, @Body() data: any) {
|
||||||
if (data.password) {
|
if (data.password) {
|
||||||
@@ -990,6 +1113,7 @@ class UserController {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Roles(ParticipantRole.OWNER) // Only owner can delete user
|
||||||
@Delete(':id')
|
@Delete(':id')
|
||||||
async deleteUser(@Param('id', ParseUUIDPipe) id: string) {
|
async deleteUser(@Param('id', ParseUUIDPipe) id: string) {
|
||||||
const user = await this.prisma.user.findUnique({ where: { id } });
|
const user = await this.prisma.user.findUnique({ where: { id } });
|
||||||
@@ -1002,6 +1126,18 @@ class UserController {
|
|||||||
// 1. Xác định thư mục chứa ảnh gốc của thành viên
|
// 1. Xác định thư mục chứa ảnh gốc của thành viên
|
||||||
const memberDir = path.join(UPLOAD_ROOT, 'members', id);
|
const memberDir = path.join(UPLOAD_ROOT, 'members', id);
|
||||||
|
|
||||||
|
// 2. Tìm tất cả ảnh của user này để dọn dẹp nốt các bản 2K còn lại trong thư mục tours
|
||||||
|
const photos = await this.prisma.photo.findMany({
|
||||||
|
where: { uploaderId: id }
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const photo of photos) {
|
||||||
|
if (photo.imageUrl) {
|
||||||
|
const displayFilePath = path.join(process.cwd(), photo.imageUrl.replace(/^\//, ''));
|
||||||
|
if (fs.existsSync(displayFilePath)) fs.unlinkSync(displayFilePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 2. Xóa các ràng buộc và dữ liệu trong DB
|
// 2. Xóa các ràng buộc và dữ liệu trong DB
|
||||||
await this.prisma.photo.deleteMany({ where: { uploaderId: id } });
|
await this.prisma.photo.deleteMany({ where: { uploaderId: id } });
|
||||||
await this.prisma.tourParticipant.deleteMany({ where: { userId: id } });
|
await this.prisma.tourParticipant.deleteMany({ where: { userId: id } });
|
||||||
@@ -1015,6 +1151,7 @@ class UserController {
|
|||||||
return { message: 'Đã xóa người dùng' };
|
return { message: 'Đã xóa người dùng' };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Roles(ParticipantRole.OWNER, ParticipantRole.MANAGER) // Default roles for blocking user (Admin/Manager)
|
||||||
@Post('block/:id')
|
@Post('block/:id')
|
||||||
async toggleBlock(@Param('id', ParseUUIDPipe) id: string) {
|
async toggleBlock(@Param('id', ParseUUIDPipe) id: string) {
|
||||||
const user = await this.prisma.user.findUnique({ where: { id } });
|
const user = await this.prisma.user.findUnique({ where: { id } });
|
||||||
@@ -1107,8 +1244,8 @@ class CommentController {
|
|||||||
signOptions: { expiresIn: '1d' },
|
signOptions: { expiresIn: '1d' },
|
||||||
}) as any,
|
}) as any,
|
||||||
],
|
],
|
||||||
controllers: [AppController, AuthController, PublicTourController, TourController, UserController, RoutingController, LegController, LocationController, CommentController],
|
controllers: [AppController, AuthController, PublicTourController, TourController, UserController, RoutingController, LegController, LocationController, CommentController, PhotoController],
|
||||||
providers: [PrismaService, JwtStrategy, TourRoleGuard, JwtAuthGuard, AdminGuard, CommentGateway],
|
providers: [PrismaService, JwtStrategy, TourRoleGuard, JwtAuthGuard, AdminGuard, CommentGateway, Reflector],
|
||||||
exports: [PrismaService]
|
exports: [PrismaService]
|
||||||
})
|
})
|
||||||
class AppModule {}
|
class AppModule {}
|
||||||
|
|||||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 1.6 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 2.1 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 378 KiB |
Reference in New Issue
Block a user