add script about install
This commit is contained in:
1 parent
d6976f8ee4
commit
657775bf27
7 files changed
+695
-4
No files matched your search
@@ -0,0 +1,60 @@
|
||||
# Node.js
|
||||
node_modules/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Environment files
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage/
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
|
||||
# IDE files
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# OS generated files
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
|
||||
# Git
|
||||
.git/
|
||||
.gitignore
|
||||
|
||||
# Build artifacts
|
||||
frontend/dist/
|
||||
backend/dist/
|
||||
|
||||
# Docker
|
||||
Dockerfile*
|
||||
docker-compose*
|
||||
.dockerignore
|
||||
|
||||
# Temporary files
|
||||
*.tmp
|
||||
*.temp
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
# 多阶段构建Dockerfile
|
||||
FROM node:18-alpine AS frontend-build
|
||||
|
||||
# 设置工作目录
|
||||
WORKDIR /app
|
||||
|
||||
# 复制前端文件
|
||||
COPY frontend/package*.json ./frontend/
|
||||
COPY frontend/ ./frontend/
|
||||
|
||||
# 安装前端依赖并构建
|
||||
RUN cd frontend && npm ci && npm run build
|
||||
|
||||
# 后端构建阶段
|
||||
FROM node:18-alpine AS backend-build
|
||||
|
||||
# 设置工作目录
|
||||
WORKDIR /app
|
||||
|
||||
# 复制后端文件
|
||||
COPY backend/package*.json ./backend/
|
||||
COPY backend/ ./backend/
|
||||
|
||||
# 安装后端依赖
|
||||
RUN cd backend && npm ci --only=production
|
||||
|
||||
# 最终镜像
|
||||
FROM node:18-alpine
|
||||
|
||||
# 安装必要的系统工具
|
||||
RUN apk add --no-cache \
|
||||
openssh-client \
|
||||
&& rm -rf /var/cache/apk/*
|
||||
|
||||
# 创建非root用户
|
||||
RUN addgroup -g 1001 -S nodejs && \
|
||||
adduser -S nodejs -u 1001
|
||||
|
||||
# 设置工作目录
|
||||
WORKDIR /app
|
||||
|
||||
# 从构建阶段复制文件
|
||||
COPY --from=frontend-build /app/frontend/dist ./frontend/dist
|
||||
COPY --from=backend-build /app/backend ./backend
|
||||
RUN rm -f ./backend/.env || true
|
||||
|
||||
# 复制根目录文件
|
||||
COPY package*.json ./
|
||||
COPY install.sql ./backend/
|
||||
|
||||
# 设置权限
|
||||
RUN chown -R nodejs:nodejs /app
|
||||
|
||||
# 切换到非root用户
|
||||
USER nodejs
|
||||
|
||||
# 环境变量
|
||||
ENV NODE_ENV=production
|
||||
ENV PORT=3000
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 3000
|
||||
|
||||
# 健康检查
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||
CMD node -e "require('http').get('http://localhost:3000/api/auth/health', (res) => { process.exit(res.statusCode === 200 ? 0 : 1) })"
|
||||
|
||||
# 启动命令
|
||||
CMD ["node", "backend/server.js"]
|
||||
@@ -193,6 +193,63 @@ GRANT ALL PRIVILEGES ON webssh.* TO 'webssh'@'localhost';
|
||||
FLUSH PRIVILEGES;
|
||||
```
|
||||
|
||||
### 建表SQL(MySQL)
|
||||
```sql
|
||||
-- 用户表
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
email VARCHAR(255) UNIQUE NOT NULL,
|
||||
phone VARCHAR(20),
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- 分组表
|
||||
CREATE TABLE IF NOT EXISTS groups (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT NOT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
CONSTRAINT fk_groups_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
UNIQUE KEY unique_user_group (user_id, name)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- 服务器表
|
||||
CREATE TABLE IF NOT EXISTS servers (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT NOT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
host VARCHAR(255) NOT NULL,
|
||||
port INT DEFAULT 22,
|
||||
username VARCHAR(255) NOT NULL,
|
||||
password_encrypted TEXT,
|
||||
private_key_encrypted TEXT,
|
||||
auth_type ENUM('password', 'key') DEFAULT 'password',
|
||||
group_id INT,
|
||||
group_name VARCHAR(255) DEFAULT 'Default',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
CONSTRAINT fk_servers_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_servers_group FOREIGN KEY (group_id) REFERENCES groups(id) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- 连接日志表
|
||||
CREATE TABLE IF NOT EXISTS connection_logs (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT NOT NULL,
|
||||
server_id INT,
|
||||
host VARCHAR(255) NOT NULL,
|
||||
start_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
end_time TIMESTAMP NULL,
|
||||
duration INT DEFAULT 0,
|
||||
CONSTRAINT fk_logs_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_logs_server FOREIGN KEY (server_id) REFERENCES servers(id) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
```
|
||||
|
||||
#### 方式三:系统初始化
|
||||
```sql
|
||||
-- 如果需要重新初始化系统,使用初始化脚本
|
||||
@@ -250,6 +307,11 @@ ENCRYPTION_KEY=abcdefghijklmnopqrstuvwxyz123456
|
||||
- 生产环境请使用强密码生成器创建安全的密钥
|
||||
- 不要将真实的密钥提交到版本控制系统
|
||||
|
||||
生成一个符合要求的密钥:
|
||||
```bash
|
||||
openssl rand -hex 16 # 输出32字符长度的十六进制字符串
|
||||
```
|
||||
|
||||
### 4.5 数据库初始化验证
|
||||
|
||||
数据库初始化完成后,后端服务会自动创建必要的表结构。如果遇到表结构错误,可以手动执行:
|
||||
@@ -418,4 +480,4 @@ npm run dev
|
||||
|
||||
---
|
||||
|
||||
**注意**: 本项目仍在开发中,部分功能可能不完善。生产环境使用前请充分测试。
|
||||
**注意**: 本项目仍在开发中,部分功能可能不完善。生产环境使用前请充分测试。
|
||||
@@ -0,0 +1,334 @@
|
||||
const express = require('express');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const db = require('../config/database');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// 验证令牌中间件
|
||||
const authenticateToken = (req, res, next) => {
|
||||
const authHeader = req.headers['authorization'];
|
||||
const token = authHeader && authHeader.split(' ')[1];
|
||||
|
||||
if (!token) {
|
||||
return res.status(401).json({ error: '访问令牌缺失' });
|
||||
}
|
||||
|
||||
jwt.verify(token, process.env.JWT_SECRET || 'webssh-secret-key', (err, user) => {
|
||||
if (err) {
|
||||
return res.status(403).json({ error: '令牌无效' });
|
||||
}
|
||||
req.user = user;
|
||||
next();
|
||||
});
|
||||
};
|
||||
|
||||
// 检查是否是管理员
|
||||
const requireAdmin = async (req, res, next) => {
|
||||
try {
|
||||
const [users] = await db.pool.execute(
|
||||
'SELECT role FROM users WHERE id = ?',
|
||||
[req.user.userId]
|
||||
);
|
||||
|
||||
if (users.length === 0) {
|
||||
return res.status(404).json({ error: '用户不存在' });
|
||||
}
|
||||
|
||||
if (users[0].role !== 'admin') {
|
||||
return res.status(403).json({ error: '需要管理员权限' });
|
||||
}
|
||||
|
||||
next();
|
||||
} catch (error) {
|
||||
console.error('检查管理员权限错误:', error);
|
||||
res.status(500).json({ error: '服务器内部错误' });
|
||||
}
|
||||
};
|
||||
|
||||
// 获取所有用户列表(仅管理员)
|
||||
router.get('/', authenticateToken, requireAdmin, async (req, res) => {
|
||||
try {
|
||||
const [users] = await db.pool.execute(`
|
||||
SELECT
|
||||
id, email, phone, role, status,
|
||||
created_at, updated_at
|
||||
FROM users
|
||||
ORDER BY created_at DESC
|
||||
`);
|
||||
|
||||
// 获取每个用户的服务器数量
|
||||
for (let user of users) {
|
||||
const [serverCount] = await db.pool.execute(
|
||||
'SELECT COUNT(*) as count FROM servers WHERE user_id = ?',
|
||||
[user.id]
|
||||
);
|
||||
user.serverCount = serverCount[0].count;
|
||||
}
|
||||
|
||||
res.json({ users });
|
||||
} catch (error) {
|
||||
console.error('获取用户列表错误:', error);
|
||||
res.status(500).json({ error: '服务器内部错误' });
|
||||
}
|
||||
});
|
||||
|
||||
// 创建新用户(仅管理员)
|
||||
router.post('/', authenticateToken, requireAdmin, async (req, res) => {
|
||||
try {
|
||||
const { email, phone, password, role = 'user' } = req.body;
|
||||
|
||||
// 验证输入
|
||||
if (!email || !password) {
|
||||
return res.status(400).json({ error: '邮箱和密码是必填项' });
|
||||
}
|
||||
|
||||
if (password.length < 6) {
|
||||
return res.status(400).json({ error: '密码长度至少6位' });
|
||||
}
|
||||
|
||||
if (!['admin', 'user'].includes(role)) {
|
||||
return res.status(400).json({ error: '角色参数无效' });
|
||||
}
|
||||
|
||||
// 检查用户是否已存在
|
||||
const [existingUsers] = await db.pool.execute(
|
||||
'SELECT id FROM users WHERE email = ?',
|
||||
[email]
|
||||
);
|
||||
|
||||
if (existingUsers.length > 0) {
|
||||
return res.status(400).json({ error: '该邮箱已被注册' });
|
||||
}
|
||||
|
||||
// 加密密码
|
||||
const saltRounds = 12;
|
||||
const passwordHash = await bcrypt.hash(password, saltRounds);
|
||||
|
||||
// 创建用户
|
||||
const [result] = await db.pool.execute(
|
||||
'INSERT INTO users (email, phone, password_hash, role, status) VALUES (?, ?, ?, ?, ?)',
|
||||
[email, phone || null, passwordHash, role, 'active']
|
||||
);
|
||||
|
||||
// 获取新创建的用户详情
|
||||
const [newUser] = await db.pool.execute(
|
||||
'SELECT id, email, phone, role, status, created_at FROM users WHERE id = ?',
|
||||
[result.insertId]
|
||||
);
|
||||
|
||||
res.status(201).json({
|
||||
message: '用户创建成功',
|
||||
user: {
|
||||
...newUser[0],
|
||||
serverCount: 0
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('创建用户错误:', error);
|
||||
res.status(500).json({ error: '服务器内部错误' });
|
||||
}
|
||||
});
|
||||
|
||||
// 更新用户信息(仅管理员)
|
||||
router.put('/:id', authenticateToken, requireAdmin, async (req, res) => {
|
||||
try {
|
||||
const userId = req.params.id;
|
||||
const { email, phone, role, status } = req.body;
|
||||
|
||||
// 检查用户是否存在
|
||||
const [existingUsers] = await db.pool.execute(
|
||||
'SELECT id FROM users WHERE id = ?',
|
||||
[userId]
|
||||
);
|
||||
|
||||
if (existingUsers.length === 0) {
|
||||
return res.status(404).json({ error: '用户不存在' });
|
||||
}
|
||||
|
||||
// 验证角色参数
|
||||
if (role && !['admin', 'user'].includes(role)) {
|
||||
return res.status(400).json({ error: '角色参数无效' });
|
||||
}
|
||||
|
||||
// 验证状态参数
|
||||
if (status && !['active', 'disabled'].includes(status)) {
|
||||
return res.status(400).json({ error: '状态参数无效' });
|
||||
}
|
||||
|
||||
// 检查邮箱是否已被其他用户使用
|
||||
if (email) {
|
||||
const [duplicateUsers] = await db.pool.execute(
|
||||
'SELECT id FROM users WHERE email = ? AND id != ?',
|
||||
[email, userId]
|
||||
);
|
||||
|
||||
if (duplicateUsers.length > 0) {
|
||||
return res.status(400).json({ error: '该邮箱已被其他用户使用' });
|
||||
}
|
||||
}
|
||||
|
||||
// 构建更新字段
|
||||
const updateFields = [];
|
||||
const updateValues = [];
|
||||
|
||||
if (email) {
|
||||
updateFields.push('email = ?');
|
||||
updateValues.push(email);
|
||||
}
|
||||
|
||||
if (phone !== undefined) {
|
||||
updateFields.push('phone = ?');
|
||||
updateValues.push(phone || null);
|
||||
}
|
||||
|
||||
if (role) {
|
||||
updateFields.push('role = ?');
|
||||
updateValues.push(role);
|
||||
}
|
||||
|
||||
if (status) {
|
||||
updateFields.push('status = ?');
|
||||
updateValues.push(status);
|
||||
}
|
||||
|
||||
if (updateFields.length === 0) {
|
||||
return res.status(400).json({ error: '没有提供更新信息' });
|
||||
}
|
||||
|
||||
updateValues.push(userId);
|
||||
|
||||
await db.pool.execute(
|
||||
`UPDATE users SET ${updateFields.join(', ')} WHERE id = ?`,
|
||||
updateValues
|
||||
);
|
||||
|
||||
// 获取更新后的用户详情
|
||||
const [updatedUser] = await db.pool.execute(
|
||||
'SELECT id, email, phone, role, status, created_at FROM users WHERE id = ?',
|
||||
[userId]
|
||||
);
|
||||
|
||||
// 获取服务器数量
|
||||
const [serverCount] = await db.pool.execute(
|
||||
'SELECT COUNT(*) as count FROM servers WHERE user_id = ?',
|
||||
[userId]
|
||||
);
|
||||
|
||||
res.json({
|
||||
message: '用户信息更新成功',
|
||||
user: {
|
||||
...updatedUser[0],
|
||||
serverCount: serverCount[0].count
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('更新用户信息错误:', error);
|
||||
res.status(500).json({ error: '服务器内部错误' });
|
||||
}
|
||||
});
|
||||
|
||||
// 重置用户密码(仅管理员)
|
||||
router.put('/:id/password', authenticateToken, requireAdmin, async (req, res) => {
|
||||
try {
|
||||
const userId = req.params.id;
|
||||
const { newPassword } = req.body;
|
||||
|
||||
if (!newPassword) {
|
||||
return res.status(400).json({ error: '新密码是必填项' });
|
||||
}
|
||||
|
||||
if (newPassword.length < 6) {
|
||||
return res.status(400).json({ error: '新密码长度至少6位' });
|
||||
}
|
||||
|
||||
// 检查用户是否存在
|
||||
const [existingUsers] = await db.pool.execute(
|
||||
'SELECT id FROM users WHERE id = ?',
|
||||
[userId]
|
||||
);
|
||||
|
||||
if (existingUsers.length === 0) {
|
||||
return res.status(404).json({ error: '用户不存在' });
|
||||
}
|
||||
|
||||
// 加密新密码
|
||||
const saltRounds = 12;
|
||||
const newPasswordHash = await bcrypt.hash(newPassword, saltRounds);
|
||||
|
||||
// 更新密码
|
||||
await db.pool.execute(
|
||||
'UPDATE users SET password_hash = ? WHERE id = ?',
|
||||
[newPasswordHash, userId]
|
||||
);
|
||||
|
||||
res.json({ message: '密码重置成功' });
|
||||
} catch (error) {
|
||||
console.error('重置用户密码错误:', error);
|
||||
res.status(500).json({ error: '服务器内部错误' });
|
||||
}
|
||||
});
|
||||
|
||||
// 删除用户(仅管理员)
|
||||
router.delete('/:id', authenticateToken, requireAdmin, async (req, res) => {
|
||||
try {
|
||||
const userId = req.params.id;
|
||||
|
||||
// 检查用户是否存在
|
||||
const [existingUsers] = await db.pool.execute(
|
||||
'SELECT id, role FROM users WHERE id = ?',
|
||||
[userId]
|
||||
);
|
||||
|
||||
if (existingUsers.length === 0) {
|
||||
return res.status(404).json({ error: '用户不存在' });
|
||||
}
|
||||
|
||||
// 不能删除自己
|
||||
if (parseInt(userId) === parseInt(req.user.userId)) {
|
||||
return res.status(400).json({ error: '不能删除自己的账户' });
|
||||
}
|
||||
|
||||
// 获取用户的服务器数量
|
||||
const [serverCount] = await db.pool.execute(
|
||||
'SELECT COUNT(*) as count FROM servers WHERE user_id = ?',
|
||||
[userId]
|
||||
);
|
||||
|
||||
const count = serverCount[0].count;
|
||||
|
||||
// 删除用户(级联删除相关数据)
|
||||
await db.pool.execute(
|
||||
'DELETE FROM users WHERE id = ?',
|
||||
[userId]
|
||||
);
|
||||
|
||||
res.json({
|
||||
message: `用户删除成功,同时删除了 ${count} 个相关服务器`
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('删除用户错误:', error);
|
||||
res.status(500).json({ error: '服务器内部错误' });
|
||||
}
|
||||
});
|
||||
|
||||
// 获取当前用户信息(包含角色和状态)
|
||||
router.get('/me', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const [users] = await db.pool.execute(
|
||||
'SELECT id, email, phone, role, status, created_at FROM users WHERE id = ?',
|
||||
[req.user.userId]
|
||||
);
|
||||
|
||||
if (users.length === 0) {
|
||||
return res.status(404).json({ error: '用户不存在' });
|
||||
}
|
||||
|
||||
res.json({ user: users[0] });
|
||||
} catch (error) {
|
||||
console.error('获取用户信息错误:', error);
|
||||
res.status(500).json({ error: '服务器内部错误' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -9,10 +9,27 @@ const AUTH_TAG_LENGTH = 16;
|
||||
|
||||
class EncryptionService {
|
||||
constructor() {
|
||||
this.encryptionKey = process.env.ENCRYPTION_KEY || 'default-encryption-key-32bytes-long';
|
||||
if (!this.encryptionKey || this.encryptionKey.length !== 32) {
|
||||
let key = (process.env.ENCRYPTION_KEY || '').trim();
|
||||
if (/^[0-9a-fA-F]{64}$/.test(key)) {
|
||||
key = key.substring(0, 32);
|
||||
}
|
||||
if (!key || key.length !== 32) {
|
||||
const raw = (process.env.ENCRYPTION_KEY || '').trim();
|
||||
const preview = raw ? (raw.slice(0, 4) + '...' + raw.slice(-4)) : '(empty)';
|
||||
const len = raw ? raw.length : 0;
|
||||
let fingerprint = '';
|
||||
try { fingerprint = crypto.createHash('sha256').update(raw).digest('hex').slice(0, 16); } catch {}
|
||||
console.error('ENCRYPTION_KEY_RAW=', raw);
|
||||
console.error('Invalid ENCRYPTION_KEY: length=%s, preview=%s, hex64=%s, fingerprint=%s', String(len), preview, /^[0-9a-fA-F]{64}$/.test(raw) ? 'true' : 'false', fingerprint);
|
||||
if (String(process.env.ALLOW_WEAK_ENCRYPTION).toLowerCase() === 'true') {
|
||||
const weak = (raw || '').padEnd(32, '0').slice(0, 32);
|
||||
console.error('ALLOW_WEAK_ENCRYPTION enabled: using fallback key of length 32 to keep service alive');
|
||||
this.encryptionKey = weak;
|
||||
return;
|
||||
}
|
||||
throw new Error('ENCRYPTION_KEY must be exactly 32 bytes long');
|
||||
}
|
||||
this.encryptionKey = key;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -104,4 +121,4 @@ class EncryptionService {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new EncryptionService();
|
||||
module.exports = new EncryptionService();
|
||||
@@ -0,0 +1,108 @@
|
||||
#!/bin/bash
|
||||
|
||||
# WebSSH 部署脚本
|
||||
set -e
|
||||
|
||||
echo "🚀 开始部署 WebSSH..."
|
||||
|
||||
# 检查Docker是否安装
|
||||
if ! command -v docker &> /dev/null; then
|
||||
echo "❌ Docker 未安装,请先安装 Docker"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 检查Docker Compose是否安装(新版Docker内置compose)
|
||||
if ! docker compose version &> /dev/null; then
|
||||
echo "❌ Docker Compose 未安装,请先安装 Docker Compose"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 清理现有环境
|
||||
echo "🧹 清理现有环境..."
|
||||
echo " 停止并删除现有容器..."
|
||||
docker compose down --remove-orphans --volumes --rmi local 2>/dev/null || true
|
||||
|
||||
echo " 清理未使用的镜像、容器和网络..."
|
||||
docker system prune -f 2>/dev/null || true
|
||||
|
||||
# 删除旧的镜像
|
||||
echo " 删除旧的WebSSH镜像..."
|
||||
docker images | grep "webssh" | awk '{print $3}' | xargs -r docker rmi -f 2>/dev/null || true
|
||||
|
||||
# 删除旧的容器
|
||||
echo " 删除旧的WebSSH容器..."
|
||||
docker ps -a | grep "webssh" | awk '{print $1}' | xargs -r docker rm -f 2>/dev/null || true
|
||||
|
||||
# 创建/校验生产环境配置文件
|
||||
echo "📝 创建/校验环境配置文件..."
|
||||
if [ ! -f .env.production ]; then
|
||||
NEW_KEY=$(openssl rand -hex 16)
|
||||
cat > .env.production << EOF
|
||||
# 数据库配置
|
||||
DB_HOST=mysql
|
||||
DB_PORT=3306
|
||||
DB_USER=webssh_user
|
||||
DB_PASSWORD=webssh_password
|
||||
DB_NAME=webssh
|
||||
|
||||
# JWT配置
|
||||
JWT_SECRET=$(openssl rand -base64 32)
|
||||
|
||||
# 服务器配置
|
||||
PORT=3000
|
||||
NODE_ENV=production
|
||||
|
||||
# 加密密钥(长度必须为32字符)
|
||||
ENCRYPTION_KEY=${NEW_KEY}
|
||||
EOF
|
||||
echo "✅ 环境配置文件已创建"
|
||||
else
|
||||
# 读取并校验现有KEY长度
|
||||
CURRENT_KEY=$(grep -E '^ENCRYPTION_KEY=' .env.production | cut -d= -f2 | tr -d '\r\n')
|
||||
if [ -z "$CURRENT_KEY" ] || [ ${#CURRENT_KEY} -ne 32 ]; then
|
||||
echo "⚠️ ENCRYPTION_KEY 无效或缺失,正在重新生成..."
|
||||
NEW_KEY=$(openssl rand -hex 16)
|
||||
if grep -q '^ENCRYPTION_KEY=' .env.production; then
|
||||
awk -v k="$NEW_KEY" 'BEGIN{FS=OFS="="} /^ENCRYPTION_KEY=/ {$2=k} {print}' .env.production > .env.production.tmp && mv .env.production.tmp .env.production
|
||||
else
|
||||
echo "ENCRYPTION_KEY=$NEW_KEY" >> .env.production
|
||||
fi
|
||||
echo "✅ ENCRYPTION_KEY 已修复为有效的32字符值"
|
||||
else
|
||||
echo "✅ ENCRYPTION_KEY 已有效(长度32)"
|
||||
fi
|
||||
fi
|
||||
|
||||
# 构建镜像
|
||||
echo "🔨 构建 Docker 镜像..."
|
||||
docker compose build --no-cache
|
||||
|
||||
# 启动服务
|
||||
echo "🚀 启动服务..."
|
||||
docker compose up -d
|
||||
|
||||
# 等待服务启动
|
||||
echo "⏳ 等待服务启动..."
|
||||
sleep 30
|
||||
|
||||
# 检查服务状态
|
||||
echo "🔍 检查服务状态..."
|
||||
if docker compose ps | grep -q "Up"; then
|
||||
echo "✅ 服务启动成功!"
|
||||
echo ""
|
||||
echo "📊 服务信息:"
|
||||
echo " - WebSSH 应用: http://localhost:3000"
|
||||
echo " - MySQL 数据库: localhost:3306"
|
||||
echo ""
|
||||
echo "🔧 常用命令:"
|
||||
echo " - 查看日志: docker compose logs -f"
|
||||
echo " - 停止服务: docker compose down"
|
||||
echo " - 重启服务: docker compose restart"
|
||||
echo " - 查看状态: docker compose ps"
|
||||
else
|
||||
echo "❌ 服务启动失败,请检查日志"
|
||||
docker compose logs
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "🎉 部署完成!"
|
||||
@@ -0,0 +1,41 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
# WebSSH应用
|
||||
webssh:
|
||||
build: .
|
||||
ports:
|
||||
- "3000:3000"
|
||||
# 环境变量从.env.production文件加载
|
||||
env_file:
|
||||
- .env.production
|
||||
depends_on:
|
||||
mysql:
|
||||
condition: service_healthy
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "node", "-e", "require('http').get('http://localhost:3000/api/auth/health', (res) => { process.exit(res.statusCode === 200 ? 0 : 1) })"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
|
||||
# MySQL数据库
|
||||
mysql:
|
||||
image: mysql:8.0
|
||||
environment:
|
||||
- MYSQL_ROOT_PASSWORD=root_password
|
||||
- MYSQL_DATABASE=webssh
|
||||
- MYSQL_USER=webssh_user
|
||||
- MYSQL_PASSWORD=webssh_password
|
||||
volumes:
|
||||
- mysql_data:/var/lib/mysql
|
||||
- ./install.sql:/docker-entrypoint-initdb.d/install.sql
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
|
||||
timeout: 20s
|
||||
retries: 10
|
||||
|
||||
volumes:
|
||||
mysql_data:
|
||||
Reference in new issue
Block a user