fix: thêm thành viên ngoài hệ thống vào Tour
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
const { PrismaClient } = require('@prisma/client');
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
const tourId = '84d84c21-c12b-4a18-9655-043c85f5c0c5';
|
||||
const ownerUserId = '5b2053bb-f523-4a11-817c-f47fef7322bb'; // owner@travel.com
|
||||
|
||||
// Check if owner@travel.com is already a participant
|
||||
const existing = await prisma.tourParticipant.findFirst({
|
||||
where: { tourId, userId: ownerUserId }
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
await prisma.tourParticipant.update({
|
||||
where: { id: existing.id },
|
||||
data: { role: 'OWNER' }
|
||||
});
|
||||
console.log('Updated existing participant to OWNER');
|
||||
} else {
|
||||
// Demote current owner to MEMBER or just keep them
|
||||
const result = await prisma.tourParticipant.create({
|
||||
data: {
|
||||
tourId,
|
||||
userId: ownerUserId,
|
||||
role: 'OWNER'
|
||||
}
|
||||
});
|
||||
console.log('Created new OWNER participant:', result);
|
||||
}
|
||||
|
||||
// Update tour createdById to ownerUserId
|
||||
await prisma.tour.update({
|
||||
where: { id: tourId },
|
||||
data: { createdById: ownerUserId }
|
||||
});
|
||||
console.log('Updated tour creator to owner@travel.com');
|
||||
}
|
||||
|
||||
main().catch(console.error).finally(() => prisma.$disconnect());
|
||||
Vendored
+77
-15
@@ -555,7 +555,7 @@ let TourController = class TourController {
|
||||
this.cacheManager = cacheManager;
|
||||
}
|
||||
async createTour(body, req) {
|
||||
const { title, description, startDate, endDate, adultCount, childCount, childDiscount, tags } = body;
|
||||
const { title, description, startDate, endDate, adultCount, childCount, childDiscount, tags, members } = body;
|
||||
const tour = await this.prisma.tour.create({
|
||||
data: {
|
||||
title,
|
||||
@@ -568,10 +568,19 @@ let TourController = class TourController {
|
||||
childDiscount: childDiscount || 0,
|
||||
createdById: req.user.id,
|
||||
participants: {
|
||||
create: {
|
||||
userId: req.user.id,
|
||||
role: 'OWNER'
|
||||
}
|
||||
create: [
|
||||
{
|
||||
userId: req.user.id,
|
||||
role: 'OWNER'
|
||||
},
|
||||
...(members && Array.isArray(members)
|
||||
? members.map((m) => ({
|
||||
userId: m.userId || null,
|
||||
displayName: m.displayName || null,
|
||||
role: m.role || 'MEMBER'
|
||||
}))
|
||||
: [])
|
||||
]
|
||||
},
|
||||
legs: {
|
||||
create: {
|
||||
@@ -610,6 +619,13 @@ let TourController = class TourController {
|
||||
}
|
||||
}).then(async (loc) => {
|
||||
if (body.expenseAmount && Number(body.expenseAmount) > 0) {
|
||||
let paidById = body.paidById || null;
|
||||
if (paidById) {
|
||||
const userExists = await this.prisma.user.findUnique({ where: { id: paidById } });
|
||||
if (!userExists) {
|
||||
paidById = null;
|
||||
}
|
||||
}
|
||||
await this.prisma.expense.create({
|
||||
data: {
|
||||
amount: Number(body.expenseAmount),
|
||||
@@ -618,7 +634,7 @@ let TourController = class TourController {
|
||||
legId: loc.legId,
|
||||
description: body.expenseDescription || `Chi phí tại ${loc.name}`,
|
||||
note: body.expenseNote || null,
|
||||
paidById: body.paidById || null,
|
||||
paidById,
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -794,7 +810,9 @@ let TourController = class TourController {
|
||||
if (!tour)
|
||||
throw new common_1.NotFoundException('Không tìm thấy tour');
|
||||
for (const participant of tour.participants) {
|
||||
await this.cacheManager.del(`user-role:${participant.userId}:${id}`);
|
||||
if (participant.userId) {
|
||||
await this.cacheManager.del(`user-role:${participant.userId}:${id}`);
|
||||
}
|
||||
}
|
||||
await this.cacheManager.del(`res-to-tour:${id}`);
|
||||
for (const leg of tour.legs) {
|
||||
@@ -898,6 +916,25 @@ let TourController = class TourController {
|
||||
async addMember(tourId, body, req) {
|
||||
const validRoles = ['OWNER', 'MANAGER', 'MEMBER', 'MEMBER_NO_FINANCE', 'VIEWER_ONLY'];
|
||||
const role = validRoles.includes(body.role) ? body.role : 'MEMBER';
|
||||
if (body.displayName) {
|
||||
const participation = await this.prisma.tourParticipant.create({
|
||||
data: {
|
||||
tourId,
|
||||
role,
|
||||
displayName: body.displayName,
|
||||
},
|
||||
include: { user: { select: { id: true, name: true, email: true } } },
|
||||
});
|
||||
await Promise.all([
|
||||
this.cacheManager.del(tourId),
|
||||
this.cacheManager.del(`/api/v1/tours/${tourId}`),
|
||||
this.cacheManager.del(`/api/v1/tours/${tourId}/public`),
|
||||
]);
|
||||
return participation;
|
||||
}
|
||||
if (!body.userId) {
|
||||
throw new common_1.BadRequestException('Vui lòng cung cấp userId hoặc displayName');
|
||||
}
|
||||
await this.cacheManager.del(`user-role:${body.userId}:${tourId}`);
|
||||
const participation = await this.prisma.tourParticipant.findUnique({
|
||||
where: { tourId_userId: { tourId, userId: body.userId } },
|
||||
@@ -941,7 +978,18 @@ let TourController = class TourController {
|
||||
});
|
||||
}
|
||||
async updateMemberCounts(tourId, userId, body, req) {
|
||||
const isSelf = req.user.id === userId;
|
||||
const participant = await this.prisma.tourParticipant.findFirst({
|
||||
where: {
|
||||
OR: [
|
||||
{ id: userId },
|
||||
{ tourId, userId }
|
||||
]
|
||||
}
|
||||
});
|
||||
if (!participant) {
|
||||
throw new common_1.NotFoundException('Không tìm thấy thành viên trong tour.');
|
||||
}
|
||||
const isSelf = req.user.id === participant.userId;
|
||||
const requesterParticipation = await this.prisma.tourParticipant.findUnique({
|
||||
where: { tourId_userId: { tourId, userId: req.user.id } },
|
||||
});
|
||||
@@ -966,14 +1014,16 @@ let TourController = class TourController {
|
||||
data.role = body.role;
|
||||
}
|
||||
}
|
||||
if (participant.userId) {
|
||||
await this.cacheManager.del(`user-role:${participant.userId}:${tourId}`);
|
||||
}
|
||||
await Promise.all([
|
||||
this.cacheManager.del(`user-role:${userId}:${tourId}`),
|
||||
this.cacheManager.del(tourId),
|
||||
this.cacheManager.del(`/api/v1/tours/${tourId}`),
|
||||
this.cacheManager.del(`/api/v1/tours/${tourId}/public`),
|
||||
]);
|
||||
return this.prisma.tourParticipant.update({
|
||||
where: { tourId_userId: { tourId, userId } },
|
||||
where: { id: participant.id },
|
||||
data,
|
||||
include: { user: { select: { id: true, name: true, email: true } } },
|
||||
});
|
||||
@@ -1090,15 +1140,27 @@ let TourController = class TourController {
|
||||
return { success: true, message: 'Đã từ chối yêu cầu tham gia.' };
|
||||
}
|
||||
async removeMember(tourId, userId) {
|
||||
const participation = await this.prisma.tourParticipant.findUnique({
|
||||
where: { tourId_userId: { tourId, userId } },
|
||||
const participant = await this.prisma.tourParticipant.findFirst({
|
||||
where: {
|
||||
OR: [
|
||||
{ id: userId },
|
||||
{ tourId, userId }
|
||||
]
|
||||
}
|
||||
});
|
||||
if (!participation) {
|
||||
if (!participant) {
|
||||
throw new common_1.NotFoundException('Thành viên này không có trong tour');
|
||||
}
|
||||
await this.cacheManager.del(`user-role:${userId}:${tourId}`);
|
||||
if (participant.userId) {
|
||||
await this.cacheManager.del(`user-role:${participant.userId}:${tourId}`);
|
||||
}
|
||||
await Promise.all([
|
||||
this.cacheManager.del(tourId),
|
||||
this.cacheManager.del(`/api/v1/tours/${tourId}`),
|
||||
this.cacheManager.del(`/api/v1/tours/${tourId}/public`),
|
||||
]);
|
||||
await this.prisma.tourParticipant.delete({
|
||||
where: { tourId_userId: { tourId, userId } },
|
||||
where: { id: participant.id },
|
||||
});
|
||||
return { message: 'Đã xóa thành viên khỏi tour' };
|
||||
}
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- The primary key for the `TourParticipant` table will be changed. If it partially fails, the table could be left without primary key constraint.
|
||||
- A unique constraint covering the columns `[tourId,userId]` on the table `TourParticipant` will be added. If there are existing duplicate values, this will fail.
|
||||
- The required column `id` was added to the `TourParticipant` table with a prisma-level default value. This is not possible if the table is not empty. Please add this column as optional, then populate it before making it required.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "TourParticipant" DROP CONSTRAINT "TourParticipant_pkey",
|
||||
ADD COLUMN "displayName" TEXT,
|
||||
ADD COLUMN "id" TEXT;
|
||||
|
||||
UPDATE "TourParticipant" SET "id" = md5(random()::text);
|
||||
|
||||
ALTER TABLE "TourParticipant" ALTER COLUMN "id" SET NOT NULL,
|
||||
ALTER COLUMN "userId" DROP NOT NULL;
|
||||
|
||||
ALTER TABLE "TourParticipant" ADD CONSTRAINT "TourParticipant_pkey" PRIMARY KEY ("id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "TourParticipant_tourId_userId_key" ON "TourParticipant"("tourId", "userId");
|
||||
+81
-16
@@ -531,7 +531,7 @@ class TourController {
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Post()
|
||||
async createTour(@Body() body: any, @Req() req: any) {
|
||||
const { title, description, startDate, endDate, adultCount, childCount, childDiscount, tags } = body;
|
||||
const { title, description, startDate, endDate, adultCount, childCount, childDiscount, tags, members } = body;
|
||||
const tour = await this.prisma.tour.create({
|
||||
data: {
|
||||
title,
|
||||
@@ -544,10 +544,19 @@ class TourController {
|
||||
childDiscount: childDiscount || 0,
|
||||
createdById: req.user.id,
|
||||
participants: {
|
||||
create: {
|
||||
userId: req.user.id,
|
||||
role: 'OWNER'
|
||||
}
|
||||
create: [
|
||||
{
|
||||
userId: req.user.id,
|
||||
role: 'OWNER'
|
||||
},
|
||||
...(members && Array.isArray(members)
|
||||
? members.map((m: any) => ({
|
||||
userId: m.userId || null,
|
||||
displayName: m.displayName || null,
|
||||
role: m.role || 'MEMBER'
|
||||
}))
|
||||
: [])
|
||||
]
|
||||
},
|
||||
legs: {
|
||||
create: {
|
||||
@@ -594,6 +603,13 @@ class TourController {
|
||||
}
|
||||
}).then(async (loc) => {
|
||||
if (body.expenseAmount && Number(body.expenseAmount) > 0) {
|
||||
let paidById = body.paidById || null;
|
||||
if (paidById) {
|
||||
const userExists = await this.prisma.user.findUnique({ where: { id: paidById } });
|
||||
if (!userExists) {
|
||||
paidById = null;
|
||||
}
|
||||
}
|
||||
await this.prisma.expense.create({
|
||||
data: {
|
||||
amount: Number(body.expenseAmount),
|
||||
@@ -602,7 +618,7 @@ class TourController {
|
||||
legId: loc.legId,
|
||||
description: body.expenseDescription || `Chi phí tại ${loc.name}`,
|
||||
note: body.expenseNote || null,
|
||||
paidById: body.paidById || null,
|
||||
paidById,
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -832,7 +848,9 @@ class TourController {
|
||||
// --- BẮT ĐẦU DỌN DẸP CACHE ---
|
||||
// a. Xóa cache vai trò của tất cả thành viên trong tour này
|
||||
for (const participant of tour.participants) {
|
||||
await this.cacheManager.del(`user-role:${participant.userId}:${id}`);
|
||||
if (participant.userId) {
|
||||
await this.cacheManager.del(`user-role:${participant.userId}:${id}`);
|
||||
}
|
||||
}
|
||||
|
||||
// b. Xóa cache mapping tài nguyên (Leg, Location, Photo) về Tour này
|
||||
@@ -961,10 +979,31 @@ class TourController {
|
||||
@Roles(ParticipantRole.OWNER, ParticipantRole.MANAGER) // Only owner/manager can add members
|
||||
@UseGuards(JwtAuthGuard, TourRoleGuard)
|
||||
@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; displayName?: string; role?: string }, @Req() req: any) {
|
||||
const validRoles = ['OWNER', 'MANAGER', 'MEMBER', 'MEMBER_NO_FINANCE', 'VIEWER_ONLY'] as const;
|
||||
const role = validRoles.includes(body.role as any) ? body.role as 'OWNER' | 'MANAGER' | 'MEMBER' | 'MEMBER_NO_FINANCE' | 'VIEWER_ONLY' : 'MEMBER';
|
||||
|
||||
if (body.displayName) {
|
||||
const participation = await this.prisma.tourParticipant.create({
|
||||
data: {
|
||||
tourId,
|
||||
role,
|
||||
displayName: body.displayName,
|
||||
},
|
||||
include: { user: { select: { id: true, name: true, email: true } } },
|
||||
});
|
||||
await Promise.all([
|
||||
this.cacheManager.del(tourId),
|
||||
this.cacheManager.del(`/api/v1/tours/${tourId}`),
|
||||
this.cacheManager.del(`/api/v1/tours/${tourId}/public`),
|
||||
]);
|
||||
return participation;
|
||||
}
|
||||
|
||||
if (!body.userId) {
|
||||
throw new BadRequestException('Vui lòng cung cấp userId hoặc displayName');
|
||||
}
|
||||
|
||||
// Xóa cache khi thay đổi quyền hạn hoặc thêm thành viên mới
|
||||
await this.cacheManager.del(`user-role:${body.userId}:${tourId}`);
|
||||
|
||||
@@ -1021,7 +1060,19 @@ class TourController {
|
||||
@Body() body: { adultCount?: number; childCount?: number; role?: string },
|
||||
@Req() req: any
|
||||
) {
|
||||
const isSelf = req.user.id === userId;
|
||||
const participant = await this.prisma.tourParticipant.findFirst({
|
||||
where: {
|
||||
OR: [
|
||||
{ id: userId },
|
||||
{ tourId, userId }
|
||||
]
|
||||
}
|
||||
});
|
||||
if (!participant) {
|
||||
throw new NotFoundException('Không tìm thấy thành viên trong tour.');
|
||||
}
|
||||
|
||||
const isSelf = req.user.id === participant.userId;
|
||||
|
||||
// Tìm quyền hạn của người gửi yêu cầu trong tour này
|
||||
const requesterParticipation = await this.prisma.tourParticipant.findUnique({
|
||||
@@ -1050,15 +1101,17 @@ class TourController {
|
||||
}
|
||||
|
||||
// Xóa cache liên quan
|
||||
if (participant.userId) {
|
||||
await this.cacheManager.del(`user-role:${participant.userId}:${tourId}`);
|
||||
}
|
||||
await Promise.all([
|
||||
this.cacheManager.del(`user-role:${userId}:${tourId}`),
|
||||
this.cacheManager.del(tourId),
|
||||
this.cacheManager.del(`/api/v1/tours/${tourId}`),
|
||||
this.cacheManager.del(`/api/v1/tours/${tourId}/public`),
|
||||
]);
|
||||
|
||||
return this.prisma.tourParticipant.update({
|
||||
where: { tourId_userId: { tourId, userId } },
|
||||
where: { id: participant.id },
|
||||
data,
|
||||
include: { user: { select: { id: true, name: true, email: true } } },
|
||||
});
|
||||
@@ -1208,17 +1261,29 @@ class TourController {
|
||||
@UseGuards(JwtAuthGuard, TourRoleGuard)
|
||||
@Delete(':tourId/members/:userId')
|
||||
async removeMember(@Param('tourId', ParseUUIDPipe) tourId: string, @Param('userId', ParseUUIDPipe) userId: string) {
|
||||
const participation = await this.prisma.tourParticipant.findUnique({
|
||||
where: { tourId_userId: { tourId, userId } },
|
||||
const participant = await this.prisma.tourParticipant.findFirst({
|
||||
where: {
|
||||
OR: [
|
||||
{ id: userId },
|
||||
{ tourId, userId }
|
||||
]
|
||||
}
|
||||
});
|
||||
if (!participation) {
|
||||
if (!participant) {
|
||||
throw new NotFoundException('Thành viên này không có trong tour');
|
||||
}
|
||||
|
||||
await this.cacheManager.del(`user-role:${userId}:${tourId}`);
|
||||
if (participant.userId) {
|
||||
await this.cacheManager.del(`user-role:${participant.userId}:${tourId}`);
|
||||
}
|
||||
await Promise.all([
|
||||
this.cacheManager.del(tourId),
|
||||
this.cacheManager.del(`/api/v1/tours/${tourId}`),
|
||||
this.cacheManager.del(`/api/v1/tours/${tourId}/public`),
|
||||
]);
|
||||
|
||||
await this.prisma.tourParticipant.delete({
|
||||
where: { tourId_userId: { tourId, userId } },
|
||||
where: { id: participant.id },
|
||||
});
|
||||
return { message: 'Đã xóa thành viên khỏi tour' };
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
async function test() {
|
||||
const loginRes = await fetch('http://localhost:3001/api/v1/auth/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email: 'owner@travel.com', password: '123456' })
|
||||
});
|
||||
const loginData = await loginRes.json();
|
||||
|
||||
if (loginRes.ok) {
|
||||
const token = loginData.access_token;
|
||||
const tourId = '84d84c21-c12b-4a18-9655-043c85f5c0c5'; // Tour ID from seed
|
||||
const res = await fetch(`http://localhost:3001/api/v1/tours/${tourId}/members`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({ displayName: 'Offline Member X', role: 'MEMBER' })
|
||||
});
|
||||
console.log('Add status:', res.status);
|
||||
const data = await res.json();
|
||||
console.log('Add response:', data);
|
||||
} else {
|
||||
console.error('Login failed:', loginData);
|
||||
}
|
||||
}
|
||||
|
||||
test().catch(console.error);
|
||||
@@ -0,0 +1,14 @@
|
||||
const { PrismaClient } = require('@prisma/client');
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
const users = await prisma.user.findMany();
|
||||
console.log('--- Users in DB ---');
|
||||
console.log(users);
|
||||
|
||||
const participants = await prisma.tourParticipant.findMany();
|
||||
console.log('--- Tour Participants in DB ---');
|
||||
console.log(participants);
|
||||
}
|
||||
|
||||
main().catch(console.error).finally(() => prisma.$disconnect());
|
||||
@@ -0,0 +1,17 @@
|
||||
const { PrismaClient } = require('@prisma/client');
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
const tours = await prisma.tour.findMany({
|
||||
include: {
|
||||
participants: {
|
||||
include: {
|
||||
user: { select: { email: true, name: true } }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
console.log(JSON.stringify(tours, null, 2));
|
||||
}
|
||||
|
||||
main().finally(() => prisma.$disconnect());
|
||||
@@ -0,0 +1,36 @@
|
||||
const { PrismaClient } = require('@prisma/client');
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
const tourId = '84d84c21-c12b-4a18-9655-043c85f5c0c5'; // Use existing tour ID from seed
|
||||
|
||||
console.log('Inserting first manual member...');
|
||||
try {
|
||||
const p1 = await prisma.tourParticipant.create({
|
||||
data: {
|
||||
tourId,
|
||||
role: 'MEMBER',
|
||||
displayName: 'Manual Member 1'
|
||||
}
|
||||
});
|
||||
console.log('Inserted:', p1);
|
||||
} catch (err) {
|
||||
console.error('Failed to insert first:', err);
|
||||
}
|
||||
|
||||
console.log('Inserting second manual member...');
|
||||
try {
|
||||
const p2 = await prisma.tourParticipant.create({
|
||||
data: {
|
||||
tourId,
|
||||
role: 'MEMBER',
|
||||
displayName: 'Manual Member 2'
|
||||
}
|
||||
});
|
||||
console.log('Inserted:', p2);
|
||||
} catch (err) {
|
||||
console.error('Failed to insert second:', err);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(console.error).finally(() => prisma.$disconnect());
|
||||
@@ -0,0 +1,22 @@
|
||||
async function test() {
|
||||
const loginRes = await fetch('http://localhost:3001/api/v1/auth/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email: 'photomember@travel.com', password: '123456' })
|
||||
});
|
||||
const loginData = await loginRes.json();
|
||||
|
||||
if (loginRes.ok) {
|
||||
const token = loginData.access_token;
|
||||
const usersRes = await fetch('http://localhost:3001/api/v1/users?q=owner@travel.com', {
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
});
|
||||
console.log('Users status:', usersRes.status);
|
||||
const usersData = await usersRes.json();
|
||||
console.log('Users data:', usersData);
|
||||
} else {
|
||||
console.error('Login failed:', loginData);
|
||||
}
|
||||
}
|
||||
|
||||
test().catch(console.error);
|
||||
Reference in New Issue
Block a user