import 'reflect-metadata'; import { NestFactory } from '@nestjs/core'; import { Module, Controller, Get, Post, Body, Param, Patch, Delete, ParseUUIDPipe, NotFoundException, BadRequestException, UnauthorizedException, UseGuards } from '@nestjs/common'; import { PrismaService } from './prisma.service.js'; import 'dotenv/config'; import * as bcrypt from 'bcrypt'; import { AdminGuard } from './admin.guard.js'; import { JwtModule, JwtService } from '@nestjs/jwt'; import { JwtAuthGuard } from './jwt-auth.guard.js'; import { JwtStrategy } from './jwt.strategy.js'; @Controller() class AppController { @Get() getHello(): string { return 'Travel Planning API is running!'; } } @Controller('v1/auth') class AuthController { constructor(private prisma: PrismaService, private jwtService: JwtService) {} @Get('status') async getStatus() { const userCount = await this.prisma.user.count(); console.log(`[Status Check] Users found: ${userCount}`); return { isInitialSetup: userCount === 0 }; } @Post('login') async login(@Body() body: any) { const { email, password } = body; const user = await this.prisma.user.findUnique({ where: { email } }); if (!user || !(await bcrypt.compare(password, user.passwordHash))) { throw new UnauthorizedException('Email hoặc mật khẩu không chính xác'); } // Chặn người dùng đã bị khóa đăng nhập if (user.isBlocked) { throw new UnauthorizedException('Tài khoản của bạn đã bị khóa. Vui lòng liên hệ quản trị viên.'); } const payload = { email: user.email, sub: user.id }; return { access_token: this.jwtService.sign(payload), user: { id: user.id, email: user.email, name: user.name, isAdmin: user.isAdmin, }, }; } @Post('signup') async signup(@Body() body: any) { const { email, password, name } = body; // 1. Kiểm tra email tồn tại const existingUser = await this.prisma.user.findUnique({ where: { email } }); if (existingUser) throw new BadRequestException('Email đã được sử dụng'); // 2. Logic "First User is Admin" const userCount = await this.prisma.user.count(); const shouldBeAdmin = userCount === 0; // 3. Hash mật khẩu const passwordHash = await bcrypt.hash(password, 10); return this.prisma.user.create({ // Bỏ createdTours: {} vì đây là quan hệ 1-n, Prisma sẽ tự hiểu là mảng rỗng data: { email, passwordHash, name, isAdmin: shouldBeAdmin }, select: { id: true, email: true, name: true, isAdmin: true } }); } } @Controller('v1/tours') class TourController { constructor(private prisma: PrismaService) {} @Get('explore') async getPublicTours() { // Lấy các tour có ít nhất 1 ảnh và thông tin vị trí từ chặng đầu tiên return this.prisma.tour.findMany({ take: 20, include: { photos: { take: 1 }, legs: { take: 1, include: { locations: { take: 1 } } } } }); } @Get(':id') async getTourDetails(@Param('id', ParseUUIDPipe) id: string) { const tour = await this.prisma.tour.findUnique({ where: { id }, include: { participants: true, photos: true, legs: { orderBy: { sequence: 'asc' }, include: { locations: { orderBy: { plannedStart: 'asc' } }, expenses: true, }, }, }, }); if (!tour) throw new NotFoundException(`Không tìm thấy Tour với ID ${id}`); return tour; } } @Controller('v1/users') @UseGuards(JwtAuthGuard, AdminGuard) class UserController { constructor(private prisma: PrismaService) {} @Get() async getAllUsers() { return this.prisma.user.findMany({ select: { id: true, email: true, name: true, isAdmin: true, isBlocked: true, createdAt: true } }); } @Patch(':id') async updateUser(@Param('id', ParseUUIDPipe) id: string, @Body() data: any) { // Nếu đổi mật khẩu thì cần hash if (data.password) { data.passwordHash = await bcrypt.hash(data.password, 10); delete data.password; } return this.prisma.user.update({ where: { id }, data, select: { id: true, email: true, name: true, isAdmin: true, isBlocked: true } }); } @Delete(':id') async deleteUser(@Param('id', ParseUUIDPipe) id: string) { // Không cho phép tự xóa chính mình hoặc xóa admin cuối cùng (logic đơn giản) const user = await this.prisma.user.findUnique({ where: { id } }); if (user?.isAdmin) { const adminCount = await this.prisma.user.count({ where: { isAdmin: true } }); if (adminCount <= 1) throw new BadRequestException('Không thể xóa Quản trị viên cuối cùng'); } await this.prisma.user.delete({ where: { id } }); return { success: true }; } @Post('block/:id') async toggleBlock(@Param('id', ParseUUIDPipe) id: string) { const user = await this.prisma.user.findUnique({ where: { id } }); if (!user) throw new NotFoundException('Người dùng không tồn tại'); return this.prisma.user.update({ where: { id }, data: { isBlocked: !user.isBlocked } }); } } @Module({ imports: [ JwtModule.register({ secret: process.env.JWT_SECRET || 'super-secret', signOptions: { expiresIn: '1d' }, }), ], controllers: [AppController, AuthController, TourController, UserController], providers: [PrismaService, JwtStrategy], exports: [PrismaService] }) class AppModule {} async function bootstrap() { const app = await NestFactory.create(AppModule); app.enableCors(); // Cho phép Frontend gọi API app.setGlobalPrefix('api'); // Tất cả API sẽ bắt đầu bằng /api/... const port = process.env.PORT || 3001; await app.listen(port); console.log(`🚀 Server is running on: http://localhost:${port}`); } bootstrap();