diff --git a/frontend/package.json b/frontend/package.json index 1bd74ed..04928a9 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -18,6 +18,7 @@ "vfonts": "^0.0.3" }, "devDependencies": { + "@types/node": "^22.9.0", "@vitejs/plugin-vue": "^5.1.4", "typescript": "^5.6.3", "vite": "^5.4.10", diff --git a/frontend/src/vite-env.d.ts b/frontend/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/frontend/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index f2f2d75..e35b658 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -4,6 +4,7 @@ "useDefineForClassFields": true, "module": "ESNext", "lib": ["ES2020", "DOM", "DOM.Iterable"], + "types": ["node", "vite/client"], "skipLibCheck": true, "moduleResolution": "bundler", "allowImportingTsExtensions": true, diff --git a/scripts/_check_sshd.py b/scripts/_check_sshd.py new file mode 100644 index 0000000..f1433ea --- /dev/null +++ b/scripts/_check_sshd.py @@ -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() diff --git a/scripts/_enable_pubkey.py b/scripts/_enable_pubkey.py new file mode 100644 index 0000000..b51dcb3 --- /dev/null +++ b/scripts/_enable_pubkey.py @@ -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") diff --git a/scripts/_push_key.py b/scripts/_push_key.py new file mode 100644 index 0000000..14f1412 --- /dev/null +++ b/scripts/_push_key.py @@ -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") diff --git a/scripts/_run_deploy.py b/scripts/_run_deploy.py new file mode 100644 index 0000000..a9387f2 --- /dev/null +++ b/scripts/_run_deploy.py @@ -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() diff --git a/scripts/deploy_remote.sh b/scripts/deploy_remote.sh new file mode 100644 index 0000000..b0ca304 --- /dev/null +++ b/scripts/deploy_remote.sh @@ -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" </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 "================================================" diff --git a/scripts/push_ssh_key.py b/scripts/push_ssh_key.py new file mode 100644 index 0000000..211f542 --- /dev/null +++ b/scripts/push_ssh_key.py @@ -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())