Sửa lỗi tạo docker
This commit is contained in:
@@ -0,0 +1,7 @@
|
|||||||
|
node_modules
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
uploads/*
|
||||||
|
!uploads/.gitkeep
|
||||||
|
.env
|
||||||
|
*.log
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# Cấu hình Server
|
||||||
|
PORT=3000
|
||||||
|
NODE_ENV=production
|
||||||
|
JWT_SECRET=generate_a_random_long_string_here
|
||||||
|
SYSTEM_HOST=https://your-domain.com
|
||||||
|
ADDITIONAL_ALLOWED_ORIGINS=http://localhost:5000
|
||||||
|
|
||||||
|
# Cấu hình MongoDB
|
||||||
|
MONGO_USERNAME=admin
|
||||||
|
MONGO_PASSWORD=secure_password_here
|
||||||
|
MONGODB_URI=mongodb://admin:secure_password_here@mongo:27017/3dtours?authSource=admin
|
||||||
|
|
||||||
|
# Cấu hình Redis
|
||||||
|
REDIS_HOST=redis
|
||||||
|
REDIS_PORT=6379
|
||||||
|
UPLOAD_DIR=/app/uploads
|
||||||
@@ -146,11 +146,3 @@ Tài liệu này tổng hợp toàn bộ cấu trúc hệ thống phục vụ qu
|
|||||||
- `verifyReferer`: Chặn truy cập trực tiếp từ trình duyệt/site khác.
|
- `verifyReferer`: Chặn truy cập trực tiếp từ trình duyệt/site khác.
|
||||||
- `setNoCacheHeaders`: Chặn lưu cache các tài sản nhạy cảm.
|
- `setNoCacheHeaders`: Chặn lưu cache các tài sản nhạy cảm.
|
||||||
- `quotaMiddleware.js`: Kiểm tra dung lượng lưu trữ dựa trên Role người dùng.
|
- `quotaMiddleware.js`: Kiểm tra dung lượng lưu trữ dựa trên Role người dùng.
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Ghi chú cho Refactor
|
|
||||||
- Cần chuẩn hóa các đường dẫn tuyệt đối (hiện đang fix cứng `/home/locpham/...`).
|
|
||||||
- Chuyển đổi các logic xử lý file đồng bộ (`fs.unlinkSync`) sang bất đồng bộ để tối ưu I/O.
|
|
||||||
- Tách nhỏ `apiRoutes.js` thành các route con (admin, scenes, users, assets).
|
|
||||||
- Bổ sung Unit Test cho logic tính toán tọa độ Hotspot ngược.
|
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
FROM node:18-slim
|
||||||
|
|
||||||
|
# Cài đặt các công cụ biên dịch và thư viện cần thiết cho các module native (sharp, bcrypt)
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
python3 \
|
||||||
|
make \
|
||||||
|
g++ \
|
||||||
|
libvips-dev \
|
||||||
|
perl \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm install --omit=dev
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
CMD ["node", "backend/server.js"]
|
||||||
@@ -2,9 +2,6 @@ const mongoose = require('mongoose');
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const dotenv = require('dotenv');
|
const dotenv = require('dotenv');
|
||||||
|
|
||||||
// Tự động tìm và nạp file .env nằm cùng thư mục với folder config (tức là trong backend/.env)
|
|
||||||
dotenv.config({ path: path.join(__dirname, '../.env') });
|
|
||||||
|
|
||||||
const connectDB = async () => {
|
const connectDB = async () => {
|
||||||
try {
|
try {
|
||||||
const dbURI = process.env.MONGODB_URI;
|
const dbURI = process.env.MONGODB_URI;
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
"bullmq": "^5.8.0",
|
"bullmq": "^5.8.0",
|
||||||
"cors": "^2.8.6",
|
"cors": "^2.8.6",
|
||||||
"dotenv": "^17.4.2",
|
"dotenv": "^17.4.2",
|
||||||
"exiftool-vendored": "^26.4.0",
|
"exiftool-vendored": "^29.2.0",
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
"express-fileupload": "^1.5.2",
|
"express-fileupload": "^1.5.2",
|
||||||
"jsonwebtoken": "^9.0.3",
|
"jsonwebtoken": "^9.0.3",
|
||||||
|
|||||||
@@ -4,6 +4,20 @@ const User = require('../models/User');
|
|||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @route GET /api/auth/init-status
|
||||||
|
* @desc Check if the system has at least one admin
|
||||||
|
* @access Public
|
||||||
|
*/
|
||||||
|
router.get('/init-status', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const userCount = await User.countDocuments();
|
||||||
|
res.json({ initialized: userCount > 0 });
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ message: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @route POST /api/auth/register
|
* @route POST /api/auth/register
|
||||||
* @desc Register a new user
|
* @desc Register a new user
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
const { Queue } = require('bullmq');
|
const { Queue } = require('bullmq');
|
||||||
const IORedis = require('ioredis');
|
const IORedis = require('ioredis');
|
||||||
|
|
||||||
// Cấu hình kết nối Redis (Mặc định localhost:6379)
|
// Cấu hình kết nối Redis sử dụng biến môi trường từ Docker
|
||||||
const connection = new IORedis({
|
const connection = new IORedis({
|
||||||
|
host: process.env.REDIS_HOST || '127.0.0.1',
|
||||||
|
port: process.env.REDIS_PORT || 6379,
|
||||||
maxRetriesPerRequest: null
|
maxRetriesPerRequest: null
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -17,10 +17,17 @@ const { imageQueue } = require('./imageQueue');
|
|||||||
const { deleteSceneCascade, propagateScenePrivacy } = require('../utils/sceneHelper');
|
const { deleteSceneCascade, propagateScenePrivacy } = require('../utils/sceneHelper');
|
||||||
|
|
||||||
const uploadDir = process.env.UPLOAD_DIR ? path.resolve(process.env.UPLOAD_DIR) : path.join(__dirname, '../uploads');
|
const uploadDir = process.env.UPLOAD_DIR ? path.resolve(process.env.UPLOAD_DIR) : path.join(__dirname, '../uploads');
|
||||||
const tempDir = path.join(uploadDir, 'temp');
|
|
||||||
|
|
||||||
const storage = multer.diskStorage({
|
const storage = multer.diskStorage({
|
||||||
destination: (req, file, cb) => cb(null, tempDir),
|
destination: (req, file, cb) => {
|
||||||
|
// req.user đã được populate bởi protect middleware
|
||||||
|
const userId = req.user._id.toString();
|
||||||
|
const userTempDir = path.join(uploadDir, userId, 'temp');
|
||||||
|
if (!fs.existsSync(userTempDir)) {
|
||||||
|
fs.mkdirSync(userTempDir, { recursive: true });
|
||||||
|
}
|
||||||
|
cb(null, userTempDir);
|
||||||
|
},
|
||||||
filename: (req, file, cb) => cb(null, `${Date.now()}_${crypto.randomBytes(4).toString('hex')}${path.extname(file.originalname)}`)
|
filename: (req, file, cb) => cb(null, `${Date.now()}_${crypto.randomBytes(4).toString('hex')}${path.extname(file.originalname)}`)
|
||||||
});
|
});
|
||||||
const upload = multer({ storage });
|
const upload = multer({ storage });
|
||||||
@@ -54,8 +61,16 @@ router.post('/', protect, uploadSinglePanorama, checkQuota, async (req, res) =>
|
|||||||
const latitude = Number(lat) || 0;
|
const latitude = Number(lat) || 0;
|
||||||
const longitude = Number(lng) || 0;
|
const longitude = Number(lng) || 0;
|
||||||
const tempFilePath = req.file.path;
|
const tempFilePath = req.file.path;
|
||||||
|
|
||||||
|
// Tạo thư mục lưu trữ chính cho User nếu chưa có
|
||||||
|
const userId = req.user._id.toString();
|
||||||
|
const userUploadDir = path.join(uploadDir, userId);
|
||||||
|
if (!fs.existsSync(userUploadDir)) {
|
||||||
|
fs.mkdirSync(userUploadDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
const processedFileName = `processed_${req.file.filename}.jpg`;
|
const processedFileName = `processed_${req.file.filename}.jpg`;
|
||||||
const processedFilePath = path.join(uploadDir, processedFileName);
|
const processedFilePath = path.join(userUploadDir, processedFileName);
|
||||||
|
|
||||||
const asset = new Asset({
|
const asset = new Asset({
|
||||||
filePath: tempFilePath,
|
filePath: tempFilePath,
|
||||||
@@ -143,7 +158,6 @@ router.get('/', optionalAuth, async (req, res) => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[SceneRoutes] GET /api/scenes - Final Query for user ${req.user?._id || 'Guest'}:`, JSON.stringify(finalQuery));
|
|
||||||
const scenes = await Scene.find(finalQuery)
|
const scenes = await Scene.find(finalQuery)
|
||||||
.populate('createdBy', 'username')
|
.populate('createdBy', 'username')
|
||||||
.populate('tourId') // Nạp thông tin Tour để Frontend nhận diện
|
.populate('tourId') // Nạp thông tin Tour để Frontend nhận diện
|
||||||
@@ -154,6 +168,70 @@ router.get('/', optionalAuth, async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// @route GET /api/share/:id
|
||||||
|
// @desc Trang trung gian hỗ trợ Open Graph (Facebook, Zalo,...)
|
||||||
|
router.get('/share/:id', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const scene = await Scene.findById(req.params.id).populate('tourId');
|
||||||
|
if (!scene) return res.status(404).send('Không tìm thấy cảnh 3D');
|
||||||
|
|
||||||
|
const tour = scene.tourId;
|
||||||
|
const title = tour ? tour.name : (scene.name || 'Virtual Tour 3D');
|
||||||
|
const description = tour ? tour.description : (scene.description || 'Khám phá không gian 360 độ chân thực');
|
||||||
|
|
||||||
|
// Xác định token (ưu tiên query token, sau đó là token của tour/scene nếu có)
|
||||||
|
const token = req.query.token || scene.shareToken || (tour && tour.shareToken) || '';
|
||||||
|
|
||||||
|
// Xử lý Protocol/Host để tạo URL tuyệt đối
|
||||||
|
const protocol = req.headers['x-forwarded-proto'] || req.protocol;
|
||||||
|
const host = req.get('host');
|
||||||
|
const baseUrl = `${protocol}://${host}`;
|
||||||
|
|
||||||
|
// URL ảnh thumbnail gọi sang Asset API với cờ watermark
|
||||||
|
const imageUrl = `${baseUrl}/api/assets/view/${scene.assetId}?watermark=true${token ? '&token=' + token : ''}`;
|
||||||
|
|
||||||
|
// URL thực tế của ứng dụng để redirect người dùng
|
||||||
|
const appUrl = `${baseUrl}/?sceneId=${scene._id}${token ? '&token=' + token : ''}`;
|
||||||
|
|
||||||
|
res.send(`
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="vi">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>${title}</title>
|
||||||
|
|
||||||
|
<!-- Open Graph / Facebook -->
|
||||||
|
<meta property="og:type" content="website">
|
||||||
|
<meta property="og:url" content="${appUrl}">
|
||||||
|
<meta property="og:title" content="${title}">
|
||||||
|
<meta property="og:description" content="${description}">
|
||||||
|
<meta property="og:image" content="${imageUrl}">
|
||||||
|
<meta property="og:image:width" content="1200">
|
||||||
|
<meta property="og:image:height" content="630">
|
||||||
|
|
||||||
|
<!-- Twitter -->
|
||||||
|
<meta property="twitter:card" content="summary_large_image">
|
||||||
|
<meta property="twitter:title" content="${title}">
|
||||||
|
<meta property="twitter:description" content="${description}">
|
||||||
|
<meta property="twitter:image" content="${imageUrl}">
|
||||||
|
|
||||||
|
<!-- Chuyển hướng người dùng về trang chủ để mở viewer -->
|
||||||
|
<script type="text/javascript">
|
||||||
|
window.location.href = "${appUrl}";
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body style="font-family: sans-serif; text-align: center; padding-top: 50px; background: #1a1a1a; color: #fff;">
|
||||||
|
<h2>${title}</h2>
|
||||||
|
<p>Đang tải không gian 3D, vui lòng đợi...</p>
|
||||||
|
</body>
|
||||||
|
</html>`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[Share Error]", error);
|
||||||
|
res.status(500).send('Lỗi máy chủ');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// @route GET /api/scenes/:id
|
// @route GET /api/scenes/:id
|
||||||
router.get('/:id', optionalAuth, async (req, res) => {
|
router.get('/:id', optionalAuth, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
@@ -294,8 +372,14 @@ router.put('/:id', protect, uploadSinglePanorama, async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (req.file) {
|
if (req.file) {
|
||||||
|
const userId = req.user._id.toString();
|
||||||
|
const userUploadDir = path.join(uploadDir, userId);
|
||||||
|
if (!fs.existsSync(userUploadDir)) {
|
||||||
|
fs.mkdirSync(userUploadDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
const processedFileName = `processed_${req.file.filename}.jpg`;
|
const processedFileName = `processed_${req.file.filename}.jpg`;
|
||||||
const processedFilePath = path.join(uploadDir, processedFileName);
|
const processedFilePath = path.join(userUploadDir, processedFileName);
|
||||||
await resizeTo8K(req.file.path, processedFilePath);
|
await resizeTo8K(req.file.path, processedFilePath);
|
||||||
await injectGPSCoordinates(processedFilePath, scene.gps.lat, scene.gps.lng);
|
await injectGPSCoordinates(processedFilePath, scene.gps.lat, scene.gps.lng);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
const mongoose = require('mongoose');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
require('dotenv').config({ path: path.resolve(__dirname, '../../.env') }); // Load .env for UPLOAD_DIR
|
||||||
|
|
||||||
|
const connectDB = require('../config/db');
|
||||||
|
const Asset = require('../models/Asset');
|
||||||
|
const User = require('../models/User'); // Required for Asset model's 'uploadedBy' reference
|
||||||
|
|
||||||
|
// Xác định thư mục uploads gốc
|
||||||
|
const uploadDir = process.env.UPLOAD_DIR ? path.resolve(process.env.UPLOAD_DIR) : path.join(__dirname, '../../uploads');
|
||||||
|
|
||||||
|
const migrateAssetsToUserFolders = async () => {
|
||||||
|
try {
|
||||||
|
console.log('--- Bắt đầu quy trình di chuyển Assets vào thư mục người dùng ---');
|
||||||
|
await connectDB();
|
||||||
|
|
||||||
|
const allAssets = await Asset.find({});
|
||||||
|
console.log(`Tìm thấy ${allAssets.length} Assets cần kiểm tra.`);
|
||||||
|
|
||||||
|
let movedCount = 0;
|
||||||
|
let updatedDbCount = 0;
|
||||||
|
let skippedCount = 0;
|
||||||
|
let errorCount = 0;
|
||||||
|
|
||||||
|
for (const asset of allAssets) {
|
||||||
|
const currentFilePath = asset.filePath;
|
||||||
|
const userId = asset.uploadedBy ? asset.uploadedBy.toString() : null;
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
console.warn(`[WARN] Asset ${asset._id}: Không có thông tin người tải lên. Bỏ qua.`);
|
||||||
|
skippedCount++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kiểm tra xem đường dẫn hiện tại đã có userId trong cấu trúc chưa
|
||||||
|
// Ví dụ: uploads/654321abcdef/processed_123.jpg
|
||||||
|
const relativePathSegments = path.relative(uploadDir, currentFilePath).split(path.sep);
|
||||||
|
if (relativePathSegments.length > 1 && relativePathSegments[0] === userId) {
|
||||||
|
console.log(`[SKIP] Asset ${asset._id}: Đường dẫn đã ở đúng định dạng người dùng (${currentFilePath}).`);
|
||||||
|
skippedCount++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileName = path.basename(currentFilePath);
|
||||||
|
const userUploadSubDir = path.join(uploadDir, userId); // e.g., /app/uploads/654321abcdef
|
||||||
|
const newFilePath = path.join(userUploadSubDir, fileName); // e.g., /app/uploads/654321abcdef/processed_123.jpg
|
||||||
|
|
||||||
|
if (!fs.existsSync(userUploadSubDir)) {
|
||||||
|
console.log(`[MKDIR] Tạo thư mục: ${userUploadSubDir}`);
|
||||||
|
fs.mkdirSync(userUploadSubDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Kiểm tra sự tồn tại của tệp tin vật lý trước khi di chuyển
|
||||||
|
if (fs.existsSync(currentFilePath)) {
|
||||||
|
fs.renameSync(currentFilePath, newFilePath);
|
||||||
|
movedCount++;
|
||||||
|
console.log(`[MOVE] Asset ${asset._id}: Di chuyển từ ${currentFilePath} sang ${newFilePath}`);
|
||||||
|
} else {
|
||||||
|
console.warn(`[WARN] Tệp tin vật lý không tồn tại: ${currentFilePath} cho Asset ${asset._id}.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cập nhật đường dẫn trong bản ghi Asset trong cơ sở dữ liệu
|
||||||
|
asset.filePath = newFilePath;
|
||||||
|
await asset.save();
|
||||||
|
updatedDbCount++;
|
||||||
|
|
||||||
|
} catch (fileError) {
|
||||||
|
console.error(`[ERROR] Lỗi khi xử lý file cho Asset ${asset._id} (${currentFilePath}): ${fileError.message}`);
|
||||||
|
errorCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('--- Hoàn tất quy trình di chuyển Assets ---');
|
||||||
|
console.log(`Tổng Assets kiểm tra: ${allAssets.length}`);
|
||||||
|
console.log(`Assets đã di chuyển file: ${movedCount}`);
|
||||||
|
console.log(`Assets đã cập nhật DB: ${updatedDbCount}`);
|
||||||
|
console.log(`Assets đã bỏ qua (đã đúng định dạng hoặc không có userId): ${skippedCount}`);
|
||||||
|
console.log(`Assets gặp lỗi: ${errorCount}`);
|
||||||
|
|
||||||
|
} catch (dbError) {
|
||||||
|
console.error('Lỗi kết nối hoặc truy vấn Database:', dbError.message);
|
||||||
|
process.exit(1);
|
||||||
|
} finally {
|
||||||
|
if (mongoose.connection) {
|
||||||
|
await mongoose.connection.close();
|
||||||
|
}
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
migrateAssetsToUserFolders();
|
||||||
+4
-1
@@ -1,9 +1,12 @@
|
|||||||
|
const crypto = require('crypto');
|
||||||
|
// Đảm bảo crypto có sẵn toàn cục cho các thư viện cũ hoặc plugin mongoose
|
||||||
|
global.crypto = crypto;
|
||||||
|
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const cors = require('cors');
|
const cors = require('cors');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const dotenv = require('dotenv');
|
const dotenv = require('dotenv');
|
||||||
const connectDB = require('./config/db');
|
const connectDB = require('./config/db');
|
||||||
|
|
||||||
// Cấu hình môi trường
|
// Cấu hình môi trường
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
services:
|
||||||
|
mongo:
|
||||||
|
image: mongo:4.4 # Sử dụng phiên bản MongoDB cụ thể để đảm bảo tính ổn định
|
||||||
|
container_name: 3dtours_mongo
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- "27017:27017" # Mở cổng MongoDB ra ngoài (có thể bỏ nếu chỉ dùng nội bộ Docker)
|
||||||
|
volumes:
|
||||||
|
- mongo_data:/data/db # Lưu trữ dữ liệu MongoDB bền vững
|
||||||
|
environment:
|
||||||
|
MONGO_INITDB_ROOT_USERNAME: ${MONGO_USERNAME}
|
||||||
|
MONGO_INITDB_ROOT_PASSWORD: ${MONGO_PASSWORD}
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:6-alpine # Sử dụng phiên bản Redis nhẹ
|
||||||
|
container_name: 3dtours_redis
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- "6379:6379" # Mở cổng Redis ra ngoài (có thể bỏ nếu chỉ dùng nội bộ Docker)
|
||||||
|
volumes:
|
||||||
|
- redis_data:/data # Lưu trữ dữ liệu Redis bền vững (tùy chọn)
|
||||||
|
|
||||||
|
app:
|
||||||
|
build:
|
||||||
|
context: ./backend # Đường dẫn image trên Gitea Registry của bạn
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: 3dtours_app
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- "${PORT}:${PORT}" # Khớp cổng máy host với cổng bên trong container (ví dụ: 3000:3000)
|
||||||
|
volumes:
|
||||||
|
- uploads:/app/uploads # Lưu trữ các tệp ảnh panorama đã tải lên
|
||||||
|
- ./frontend:/frontend # Gắn thư mục frontend vào container để Node.js truy cập được qua ../frontend
|
||||||
|
environment:
|
||||||
|
# Biến môi trường cho ứng dụng Node.js
|
||||||
|
PORT: ${PORT}
|
||||||
|
MONGODB_URI: ${MONGODB_URI}
|
||||||
|
JWT_SECRET: ${JWT_SECRET}
|
||||||
|
REDIS_HOST: ${REDIS_HOST}
|
||||||
|
REDIS_PORT: ${REDIS_PORT}
|
||||||
|
UPLOAD_DIR: ${UPLOAD_DIR}
|
||||||
|
NODE_ENV: ${NODE_ENV}
|
||||||
|
SYSTEM_HOST: ${SYSTEM_HOST}
|
||||||
|
ADDITIONAL_ALLOWED_ORIGINS: ${ADDITIONAL_ALLOWED_ORIGINS}
|
||||||
|
depends_on:
|
||||||
|
- mongo # Đảm bảo MongoDB khởi động trước
|
||||||
|
- redis # Đảm bảo Redis khởi động trước
|
||||||
|
command: node server.js
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
mongo_data:
|
||||||
|
redis_data:
|
||||||
|
uploads:
|
||||||
+39
-6
@@ -22,8 +22,6 @@ let sharedEmailsData = []; // [email]
|
|||||||
// Initialize when DOM is ready
|
// Initialize when DOM is ready
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
try {
|
try {
|
||||||
console.log("--- Bắt đầu khởi tạo Frontend ---");
|
|
||||||
|
|
||||||
// 0. Kiểm tra tham số URL để truy cập trực tiếp
|
// 0. Kiểm tra tham số URL để truy cập trực tiếp
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
let urlSceneId = urlParams.get('sceneId');
|
let urlSceneId = urlParams.get('sceneId');
|
||||||
@@ -39,14 +37,15 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
fetchSystemSettings();
|
fetchSystemSettings();
|
||||||
|
|
||||||
if (document.getElementById('map')) {
|
if (document.getElementById('map')) {
|
||||||
console.log("1. Đang khởi tạo bản đồ Leaflet...");
|
|
||||||
initMap();
|
initMap();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Chạy tuần tự để tránh xung đột luồng xử lý
|
// Chạy tuần tự để tránh xung đột luồng xử lý
|
||||||
checkAuthStatus(); // 2. Kiểm tra đăng nhập
|
checkAuthStatus(); // 2. Kiểm tra đăng nhập
|
||||||
|
|
||||||
// 3. Xử lý logic vào thẳng Scene hoặc khôi phục trang
|
// 2.1. Kiểm tra xem server đã có Admin chưa (dành cho cài đặt mới)
|
||||||
|
checkSystemInitialization();
|
||||||
|
|
||||||
if (urlSceneId) {
|
if (urlSceneId) {
|
||||||
console.log(`[Direct Access] Opening scene ${urlSceneId} from URL`);
|
console.log(`[Direct Access] Opening scene ${urlSceneId} from URL`);
|
||||||
openScene(urlSceneId, urlToken ? 'shared' : null, urlToken);
|
openScene(urlSceneId, urlToken ? 'shared' : null, urlToken);
|
||||||
@@ -489,6 +488,42 @@ function checkAuthStatus() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kiểm tra trạng thái khởi tạo của hệ thống
|
||||||
|
*/
|
||||||
|
async function checkSystemInitialization() {
|
||||||
|
const token = localStorage.getItem('jwt');
|
||||||
|
if (token) return; // Nếu đã đăng nhập thì bỏ qua
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API_BASE_URL}/auth/init-status`);
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
if (data && data.initialized === false) {
|
||||||
|
showAdminSetupWizard();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Không thể kiểm tra trạng thái khởi tạo hệ thống:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hiển thị giao diện thiết lập Admin tối cao
|
||||||
|
*/
|
||||||
|
function showAdminSetupWizard() {
|
||||||
|
// Mở dropdown và chuyển sang tab đăng ký
|
||||||
|
const dropdown = document.getElementById('user-dropdown');
|
||||||
|
if (dropdown) dropdown.classList.add('show');
|
||||||
|
|
||||||
|
switchAuthMode('register');
|
||||||
|
|
||||||
|
// Tùy chỉnh giao diện cho chế độ thiết lập
|
||||||
|
const regBtn = document.querySelector('#register-section .auth-submit-btn');
|
||||||
|
if (regBtn) regBtn.innerText = 'Thiết lập Admin tối cao';
|
||||||
|
|
||||||
|
showNotification("Hệ thống mới: Vui lòng đăng ký tài khoản Admin đầu tiên để quản trị server.", "warning");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles user login
|
* Handles user login
|
||||||
*/
|
*/
|
||||||
@@ -1008,8 +1043,6 @@ async function loadScenes(urlToken = null) {
|
|||||||
const tourName = (scene.tourId && typeof scene.tourId === 'object') ? scene.tourId.name : sceneName;
|
const tourName = (scene.tourId && typeof scene.tourId === 'object') ? scene.tourId.name : sceneName;
|
||||||
const tourDescription = (scene.tourId && typeof scene.tourId === 'object') ? scene.tourId.description : scene.description;
|
const tourDescription = (scene.tourId && typeof scene.tourId === 'object') ? scene.tourId.description : scene.description;
|
||||||
|
|
||||||
console.log(`[Frontend] Đang thêm marker cho Tour: ${tourName} (Scene đại diện: ${sceneName})`);
|
|
||||||
|
|
||||||
const isProcessing = scene.status === 'processing';
|
const isProcessing = scene.status === 'processing';
|
||||||
if (isProcessing) foundProcessing++;
|
if (isProcessing) foundProcessing++;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user