195 lines
5.9 KiB
TypeScript
195 lines
5.9 KiB
TypeScript
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(); |