fix: 前端类型修复(@types/node + vite-env.d.ts + ufw SSHD_PORT)
- frontend: 加 @types/node / vite/client 类型声明 - frontend: tsconfig 加 types: [node, vite/client] - scripts: deploy_remote.sh 用 sg docker + dc() 函数避免引号问题 - scripts: deploy_remote.sh ufw 改用 \ 变量
This commit is contained in:
@@ -18,6 +18,7 @@
|
|||||||
"vfonts": "^0.0.3"
|
"vfonts": "^0.0.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/node": "^22.9.0",
|
||||||
"@vitejs/plugin-vue": "^5.1.4",
|
"@vitejs/plugin-vue": "^5.1.4",
|
||||||
"typescript": "^5.6.3",
|
"typescript": "^5.6.3",
|
||||||
"vite": "^5.4.10",
|
"vite": "^5.4.10",
|
||||||
|
|||||||
1
frontend/src/vite-env.d.ts
vendored
Normal file
1
frontend/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
"useDefineForClassFields": true,
|
"useDefineForClassFields": true,
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"types": ["node", "vite/client"],
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"allowImportingTsExtensions": true,
|
"allowImportingTsExtensions": true,
|
||||||
|
|||||||
16
scripts/_check_sshd.py
Normal file
16
scripts/_check_sshd.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import os, sys, paramiko
|
||||||
|
PW = os.environ.get("REMOTE_PASS", "")
|
||||||
|
c = paramiko.SSHClient()
|
||||||
|
c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
c.connect("207.57.129.228", port=19717, username="root", password=PW, timeout=15, allow_agent=False, look_for_keys=False)
|
||||||
|
def run(cmd):
|
||||||
|
si, so, se = c.exec_command(cmd, timeout=15)
|
||||||
|
out = so.read().decode("utf-8", "replace")
|
||||||
|
err = se.read().decode("utf-8", "replace")
|
||||||
|
print(f"$ {cmd}")
|
||||||
|
if out: print(out, end="")
|
||||||
|
if err: print("[err]", err, end="", file=sys.stderr)
|
||||||
|
run("ls -la /root/.ssh/ && echo --- && cat /root/.ssh/authorized_keys | head -1 | cut -c1-100")
|
||||||
|
run("sshd -T 2>/dev/null | grep -iE 'pubkeyauth|permitroot|authentic' | head -20")
|
||||||
|
run("grep -E 'PubkeyAuthentication|PermitRootLogin|PasswordAuthentication|AuthorizedKeysFile' /etc/ssh/sshd_config 2>/dev/null; echo --- && ls -la /etc/ssh/sshd_config.d/ 2>/dev/null")
|
||||||
|
c.close()
|
||||||
37
scripts/_enable_pubkey.py
Normal file
37
scripts/_enable_pubkey.py
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import os, sys, paramiko
|
||||||
|
PW = os.environ.get("REMOTE_PASS", "")
|
||||||
|
c = paramiko.SSHClient()
|
||||||
|
c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
c.connect("207.57.129.228", port=19717, username="root", password=PW, timeout=15, allow_agent=False, look_for_keys=False)
|
||||||
|
def run(cmd, allow_fail=False):
|
||||||
|
print(f"$ {cmd}")
|
||||||
|
si, so, se = c.exec_command(cmd, timeout=20)
|
||||||
|
out = so.read().decode("utf-8", "replace")
|
||||||
|
err = se.read().decode("utf-8", "replace")
|
||||||
|
rc = so.channel.recv_exit_status()
|
||||||
|
if out: print(out, end="")
|
||||||
|
if err: print("[err]", err, end="", file=sys.stderr)
|
||||||
|
print(f" -> rc={rc}")
|
||||||
|
if rc != 0 and not allow_fail:
|
||||||
|
raise SystemExit(f"failed: {cmd}")
|
||||||
|
return out, err, rc
|
||||||
|
|
||||||
|
# 1) 备份
|
||||||
|
run("cp -a /etc/ssh/sshd_config /etc/ssh/sshd_config.bak.$(date +%s)")
|
||||||
|
# 2) 改 PubkeyAuthentication
|
||||||
|
run("sed -i -E 's/^#?\\s*PubkeyAuthentication.*/PubkeyAuthentication yes/' /etc/ssh/sshd_config")
|
||||||
|
# 3) 确认
|
||||||
|
run("grep -n '^[^#]*PubkeyAuthentication' /etc/ssh/sshd_config")
|
||||||
|
# 4) 语法检查
|
||||||
|
run("sshd -t && echo 'sshd config OK'")
|
||||||
|
# 5) 重启(用 service 或 systemctl,Ubuntu 24 用 systemd)
|
||||||
|
# 先试 systemctl,失败回退 service
|
||||||
|
out, _, _ = run("systemctl is-active ssh 2>/dev/null || systemctl is-active sshd 2>/dev/null || echo NONE", allow_fail=True)
|
||||||
|
if "active" in out:
|
||||||
|
run("systemctl restart ssh || systemctl restart sshd")
|
||||||
|
else:
|
||||||
|
run("service ssh restart || service sshd restart")
|
||||||
|
# 6) 再确认 sshd 配置生效
|
||||||
|
run("sshd -T 2>/dev/null | grep -i pubkeyauth")
|
||||||
|
c.close()
|
||||||
|
print("DONE")
|
||||||
54
scripts/_push_key.py
Normal file
54
scripts/_push_key.py
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import os, sys, paramiko
|
||||||
|
HOST = "207.57.129.228"
|
||||||
|
PORT = 19717
|
||||||
|
USER = "root"
|
||||||
|
PW = os.environ.get("REMOTE_PASS", "")
|
||||||
|
PUB = os.path.expanduser("~/.ssh/id_rsa.pub")
|
||||||
|
if not PW:
|
||||||
|
print("REMOTE_PASS not set", file=sys.stderr); sys.exit(2)
|
||||||
|
|
||||||
|
c = paramiko.SSHClient()
|
||||||
|
c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
try:
|
||||||
|
c.connect(HOST, port=PORT, username=USER, password=PW, timeout=15, allow_agent=False, look_for_keys=False)
|
||||||
|
except Exception as e:
|
||||||
|
print("CONNECT FAIL:", e, file=sys.stderr); sys.exit(1)
|
||||||
|
|
||||||
|
def run(cmd, check=False):
|
||||||
|
si, so, se = c.exec_command(cmd, timeout=15)
|
||||||
|
out = so.read().decode("utf-8", "replace")
|
||||||
|
err = se.read().decode("utf-8", "replace")
|
||||||
|
if out: print(out, end="")
|
||||||
|
if err: print("[err]", err, end="", file=sys.stderr)
|
||||||
|
if check and (so.channel.recv_exit_status() != 0):
|
||||||
|
raise SystemExit(f"cmd failed: {cmd}")
|
||||||
|
|
||||||
|
run("mkdir -p /root/.ssh && chmod 700 /root/.ssh")
|
||||||
|
run("touch /root/.ssh/authorized_keys && chmod 600 /root/.ssh/authorized_keys")
|
||||||
|
|
||||||
|
pub = open(PUB, encoding="utf-8").read().strip()
|
||||||
|
marker = "news-deploy-key"
|
||||||
|
if marker not in pub:
|
||||||
|
pub = pub + " " + marker
|
||||||
|
|
||||||
|
# 用 sftp 写文件(避免 shell 转义)
|
||||||
|
sftp = c.open_sftp()
|
||||||
|
ak_path = "/root/.ssh/authorized_keys"
|
||||||
|
existing = ""
|
||||||
|
try:
|
||||||
|
with sftp.open(ak_path, "r") as f:
|
||||||
|
existing = f.read().decode("utf-8", "replace")
|
||||||
|
except IOError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if marker in existing:
|
||||||
|
print("[ok] public key already present, skip")
|
||||||
|
else:
|
||||||
|
with sftp.open(ak_path, "a") as f:
|
||||||
|
f.write(pub + "\n")
|
||||||
|
print("[ok] appended public key to", ak_path)
|
||||||
|
|
||||||
|
sftp.close()
|
||||||
|
run("ls -la /root/.ssh/ && echo '---' && wc -l /root/.ssh/authorized_keys")
|
||||||
|
c.close()
|
||||||
|
print("DONE")
|
||||||
23
scripts/_run_deploy.py
Normal file
23
scripts/_run_deploy.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import os, paramiko
|
||||||
|
HOST, PORT, USER = "207.57.129.228", 19717, "root"
|
||||||
|
PW = os.environ["REMOTE_PASS"]
|
||||||
|
c = paramiko.SSHClient()
|
||||||
|
c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
c.connect(HOST, port=PORT, username=USER, password=PW, timeout=15, allow_agent=False, look_for_keys=False)
|
||||||
|
|
||||||
|
# 1) 推更新后的部署脚本
|
||||||
|
sftp = c.open_sftp()
|
||||||
|
sftp.put("D:/selftools/diary-news/scripts/deploy_remote.sh", "/root/deploy_news.sh")
|
||||||
|
sftp.chmod("/root/deploy_news.sh", 0o755)
|
||||||
|
sftp.close()
|
||||||
|
print("[ok] script pushed")
|
||||||
|
|
||||||
|
# 2) 杀掉旧进程(若有)
|
||||||
|
si, so, se = c.exec_command("pkill -f deploy_news.sh 2>/dev/null; sleep 2; echo done")
|
||||||
|
print(so.read().decode().strip())
|
||||||
|
|
||||||
|
# 3) 后台启动,设 SSHD_PORT=19717
|
||||||
|
si, so, se = c.exec_command("nohup env SSHD_PORT=19717 bash /root/deploy_news.sh > /root/deploy_news.log 2>&1 & echo $!", timeout=10)
|
||||||
|
pid = so.read().decode().strip()
|
||||||
|
print(f"[ok] deploy started, PID={pid}")
|
||||||
|
c.close()
|
||||||
158
scripts/deploy_remote.sh
Normal file
158
scripts/deploy_remote.sh
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# 部署脚本:在远程服务器上跑一次
|
||||||
|
# 用法: bash /root/deploy_news.sh
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# === 配置 ===
|
||||||
|
GITEA_URL="http://124.223.26.33:3000/xiaji/diary-news.git"
|
||||||
|
APP_DIR="/srv/news"
|
||||||
|
DOMAIN="" # 留空走 IP
|
||||||
|
|
||||||
|
log() { echo "[$(date +'%H:%M:%S')] $*"; }
|
||||||
|
fail() { echo "[FAIL] $*" >&2; exit 1; }
|
||||||
|
|
||||||
|
# === 1. 系统初始化 ===
|
||||||
|
log "1/8 系统更新 + 基础包"
|
||||||
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
|
apt-get update -y
|
||||||
|
apt-get install -y --no-install-recommends curl git ufw fail2ban openssl ca-certificates
|
||||||
|
|
||||||
|
# === 2. 创建非 root 用户(后续切过去) ===
|
||||||
|
if ! id news &>/dev/null; then
|
||||||
|
log "2/8 创建 news 用户"
|
||||||
|
adduser --disabled-password --gecos "" news
|
||||||
|
echo "news ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/news
|
||||||
|
fi
|
||||||
|
|
||||||
|
# === 3. Docker ===
|
||||||
|
if ! command -v docker &>/dev/null; then
|
||||||
|
log "3/8 安装 Docker"
|
||||||
|
curl -fsSL https://get.docker.com -o /tmp/get-docker.sh
|
||||||
|
sh /tmp/get-docker.sh
|
||||||
|
usermod -aG docker news
|
||||||
|
fi
|
||||||
|
docker --version
|
||||||
|
|
||||||
|
# === 4. 防火墙 ===
|
||||||
|
log "4/8 防火墙"
|
||||||
|
SSHD_PORT="${SSHD_PORT:-22}" # 自定义 SSH 端口(默认 22)
|
||||||
|
ufw --force reset
|
||||||
|
ufw default deny incoming
|
||||||
|
ufw default allow outgoing
|
||||||
|
ufw allow "${SSHD_PORT}/tcp" comment 'ssh'
|
||||||
|
ufw allow 80/tcp comment 'http'
|
||||||
|
ufw allow 443/tcp comment 'https'
|
||||||
|
ufw --force enable
|
||||||
|
ufw status verbose
|
||||||
|
|
||||||
|
# === 5. 拉代码 ===
|
||||||
|
log "5/8 拉代码到 $APP_DIR"
|
||||||
|
mkdir -p "$APP_DIR"
|
||||||
|
if [ ! -d "$APP_DIR/.git" ]; then
|
||||||
|
sudo -u news git clone "$GITEA_URL" "$APP_DIR"
|
||||||
|
else
|
||||||
|
cd "$APP_DIR" && sudo -u news git pull --rebase
|
||||||
|
fi
|
||||||
|
chown -R news:news "$APP_DIR"
|
||||||
|
|
||||||
|
# === 6. 写 .env(自动生成密码) ===
|
||||||
|
log "6/8 生成 .env"
|
||||||
|
if [ ! -f "$APP_DIR/.env" ]; then
|
||||||
|
cat > "$APP_DIR/.env" <<EOF
|
||||||
|
TZ=Asia/Hong_Kong
|
||||||
|
LOG_LEVEL=INFO
|
||||||
|
|
||||||
|
POSTGRES_USER=news
|
||||||
|
POSTGRES_PASSWORD=$(openssl rand -hex 24)
|
||||||
|
POSTGRES_DB=news
|
||||||
|
POSTGRES_HOST=postgres
|
||||||
|
POSTGRES_PORT=5432
|
||||||
|
|
||||||
|
REDIS_HOST=redis
|
||||||
|
REDIS_PORT=6379
|
||||||
|
REDIS_PASSWORD=$(openssl rand -hex 24)
|
||||||
|
REDIS_DB=0
|
||||||
|
|
||||||
|
JWT_SECRET=$(openssl rand -hex 64)
|
||||||
|
JWT_ALGORITHM=HS256
|
||||||
|
ACCESS_TOKEN_TTL_MIN=60
|
||||||
|
REFRESH_TOKEN_TTL_DAY=14
|
||||||
|
|
||||||
|
TENCENTCLOUD_SECRET_ID=__FILL_ME__
|
||||||
|
TENCENTCLOUD_SECRET_KEY=__FILL_ME__
|
||||||
|
TENCENTCLOUD_REGION=ap-hongkong
|
||||||
|
TENCENT_TMT_ENDPOINT=tmt.tencentcloudapi.com
|
||||||
|
TENCENT_TMT_QUOTA_MONTH=5000000
|
||||||
|
TENCENT_TMT_QUOTA_BUFFER=0.05
|
||||||
|
TENCENT_TMT_MAX_CHARS_PER_REQ=4500
|
||||||
|
|
||||||
|
LOCAL_TRANSLATE_ENABLED=false
|
||||||
|
LOCAL_TRANSLATE_MODEL=nllb-200-distilled-600M
|
||||||
|
LOCAL_TRANSLATE_DEVICE=cpu
|
||||||
|
|
||||||
|
FETCH_GLOBAL_QPS=4
|
||||||
|
FETCH_TIMEOUT=20
|
||||||
|
FETCH_FAIL_PAUSE_THRESHOLD=3
|
||||||
|
FETCH_MAX_RETRIES=2
|
||||||
|
|
||||||
|
DOMAIN=$DOMAIN
|
||||||
|
ACME_EMAIL=
|
||||||
|
EOF
|
||||||
|
chown news:news "$APP_DIR/.env"
|
||||||
|
chmod 600 "$APP_DIR/.env"
|
||||||
|
log "已生成 .env(随机密码已写入,TENCENTCLOUD_SECRET 仍待填)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# === 7. 启动 docker compose ===
|
||||||
|
log "7/8 docker compose up"
|
||||||
|
cd "$APP_DIR"
|
||||||
|
# news 用户需要 docker 组权限,用 sg 临时切换(写成函数避免引号问题)
|
||||||
|
dc() { sg docker -c "docker compose $*"; }
|
||||||
|
|
||||||
|
dc up -d --build
|
||||||
|
|
||||||
|
# 等健康
|
||||||
|
log " 等待 postgres / redis healthy..."
|
||||||
|
for i in {1..30}; do
|
||||||
|
if dc exec -T postgres pg_isready -U news -d news &>/dev/null; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
for i in {1..30}; do
|
||||||
|
if dc exec -T redis redis-cli -a "$(grep ^REDIS_PASSWORD $APP_DIR/.env | cut -d= -f2)" ping 2>/dev/null | grep -q PONG; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
|
||||||
|
# === 8. 初始化 ===
|
||||||
|
log "8/8 数据库迁移 + 用户 + 源"
|
||||||
|
dc exec -T api alembic upgrade head
|
||||||
|
|
||||||
|
# 创建 owner(从 env 或自动生成,避免后台跑卡在 read)
|
||||||
|
if [ -n "${OWNER_PASS:-}" ]; then
|
||||||
|
log " 使用环境变量 OWNER_PASS"
|
||||||
|
else
|
||||||
|
OWNER_PASS="$(openssl rand -hex 12)"
|
||||||
|
log " 自动生成 owner 密码(写入 /root/.owner_pass): $OWNER_PASS"
|
||||||
|
fi
|
||||||
|
dc exec -T api python -m app.scripts.create_user --username owner --password "$OWNER_PASS" || true
|
||||||
|
echo "$OWNER_PASS" > /root/.owner_pass
|
||||||
|
chmod 600 /root/.owner_pass
|
||||||
|
|
||||||
|
# 种子
|
||||||
|
dc exec -T api python -m app.scripts.seed_sources
|
||||||
|
|
||||||
|
# 健康检查
|
||||||
|
log " 健康检查"
|
||||||
|
sleep 3
|
||||||
|
curl -s http://localhost/api/v1/healthz && echo
|
||||||
|
echo
|
||||||
|
echo "================================================"
|
||||||
|
echo " 部署完成!"
|
||||||
|
echo " 访问: http://$(curl -s ifconfig.me)/"
|
||||||
|
echo " 账号: owner"
|
||||||
|
echo " 密码: 写入到 /root/.owner_pass (chmod 600)"
|
||||||
|
echo " 后续: docker compose -f $APP_DIR/docker-compose.yml logs -f"
|
||||||
|
echo "================================================"
|
||||||
79
scripts/push_ssh_key.py
Normal file
79
scripts/push_ssh_key.py
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
"""推公钥到远程服务器 /root/.ssh/authorized_keys。
|
||||||
|
|
||||||
|
用法:python _push_key.py
|
||||||
|
依赖:paramiko
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import paramiko
|
||||||
|
|
||||||
|
HOST = "207.57.129.228"
|
||||||
|
PORT = 19717
|
||||||
|
USER = "root"
|
||||||
|
PASSWORD = os.environ["REMOTE_PASS"] # 由调用方设置
|
||||||
|
PUB_KEY_PATH = os.path.expanduser("~/.ssh/id_rsa.pub")
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
pub = open(PUB_KEY_PATH, encoding="utf-8").read().strip()
|
||||||
|
print(f"公钥({PUB_KEY_PATH})前 60 字符: {pub[:60]}...")
|
||||||
|
|
||||||
|
client = paramiko.SSHClient()
|
||||||
|
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
try:
|
||||||
|
client.connect(
|
||||||
|
HOST, port=PORT, username=USER, password=PASSWORD,
|
||||||
|
timeout=15, allow_agent=False, look_for_keys=False,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"连接失败: {e}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
# 1) 检查 .ssh 目录
|
||||||
|
cmds = [
|
||||||
|
"mkdir -p /root/.ssh && chmod 700 /root/.ssh",
|
||||||
|
f"touch /root/.ssh/authorized_keys && chmod 600 /root/.ssh/authorized_keys",
|
||||||
|
]
|
||||||
|
for c in cmds:
|
||||||
|
_exec(client, c)
|
||||||
|
|
||||||
|
# 2) 检查是否已经存在(去重)
|
||||||
|
stdin, stdout, stderr = client.exec_command("grep -F -c 'news-deploy-key' /root/.ssh/authorized_keys || true")
|
||||||
|
# 用一个独特注释,后续可识别
|
||||||
|
marker = "news-deploy-key"
|
||||||
|
if marker not in pub:
|
||||||
|
pub_with_marker = f"{pub} {marker}"
|
||||||
|
else:
|
||||||
|
pub_with_marker = pub
|
||||||
|
stdin, stdout, stderr = client.exec_command("cat /root/.ssh/authorized_keys | grep -F '" + marker + "' || true")
|
||||||
|
existing = stdout.read().decode().strip()
|
||||||
|
if existing:
|
||||||
|
print(f"已存在,跳过: {existing[:60]}...")
|
||||||
|
else:
|
||||||
|
# 写入(用 heredoc 避免转义)
|
||||||
|
quoted = pub_with_marker.replace("'", "'\\''")
|
||||||
|
cmd = f"echo '{quoted}' >> /root/.ssh/authorized_keys"
|
||||||
|
_exec(client, cmd)
|
||||||
|
print(f"已追加公钥到 /root/.ssh/authorized_keys")
|
||||||
|
|
||||||
|
# 3) 验证权限
|
||||||
|
_exec(client, "ls -la /root/.ssh/ /root/.ssh/authorized_keys")
|
||||||
|
|
||||||
|
# 4) 关闭密码登录(可选项,MVP 保留)
|
||||||
|
client.close()
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def _exec(client: paramiko.SSHClient, cmd: str) -> None:
|
||||||
|
print(f"$ {cmd}")
|
||||||
|
stdin, stdout, stderr = client.exec_command(cmd, timeout=15)
|
||||||
|
out = stdout.read().decode().strip()
|
||||||
|
err = stderr.read().decode().strip()
|
||||||
|
if out:
|
||||||
|
print(out)
|
||||||
|
if err:
|
||||||
|
print(f"[stderr] {err}", file=sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
Reference in New Issue
Block a user