add script about install

This commit is contained in:
dalaolala committed 2026-02-11 15:34:22 +08:00
1 parent d6976f8ee4
commit 657775bf27
7 files changed
+695 -4

No files matched your search

+60
View File
@@ -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
View File
@@ -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"]
+63 -1
View File
@@ -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
---
**注意**: 本项目仍在开发中,部分功能可能不完善。生产环境使用前请充分测试。
**注意**: 本项目仍在开发中,部分功能可能不完善。生产环境使用前请充分测试。
+334
View File
@@ -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;
+20 -3
View File
@@ -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();
+108
View File
@@ -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 "🎉 部署完成!"
+41
View File
@@ -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: