Files
Nexus/docs/design/plans/2026-05-16-password-presets.md
T
Your Name c9a99f4fb3 fix: 全项目文档对齐 + 代码清理
代码修复:
- web/agent/agent.py: datetime.utcnow() → datetime.now(timezone.utc)
- requirements.txt: 移除 paramiko==3.5.0 (已无活跃引用)
- 删除 server/infrastructure/ssh/pool.py (DEPRECATED, 无引用)

连接池三层对齐 (config.py/.env.example/session.py):
- DB_POOL_SIZE 100→160, DB_MAX_OVERFLOW 100→120
- 基于 MySQL max_connections=400, install.php 公式

文档修复 (21项):
- P0: 硬编码域名/IP替换为配置变量占位符
- P1: 10个过时设计文档加归档标注 (引用旧文件结构)
- P2: step-3-webssh报告 paramiko引用修正
- 6份审查报告连接池参数勘误 100/100→160/120
- ECC安全报告 EC5 datetime.utcnow 标记已修复
- docs/README.md 文档索引重写
- docs/memory/mem_nexus_overview.md 移除硬编码凭证
- docs/project/tech-stack-inventory.md paramiko标记已移除

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 08:19:56 +08:00

9.9 KiB
Raw Blame History

密码预设 实现计划

⚠️ 已归档 — 2026-05-22 本文档引用的文件结构已过时:server/database.py 已拆分为 server/domain/models/PHP 前端 (servers.php/settings.php) 已迁移到 Tailwind CSS v4 + Alpine.js (web/app/servers.html/settings.html);认证已从 X-API-Key 改为 JWT。功能已实现,保留本文档供历史参考。

面向 AI 代理的工作者: 必需子技能:使用 superpowers:executing-plans 逐任务实现此计划。

目标: 提供加密存储的 SSH 密码预设列表,新建服务器时下拉选择自动填充

架构: PasswordPreset 模型复用 Fernet 加密;4 个 REST 端点挂在 servers 路由下;servers.php 加下拉联动;settings.php 加管理面板

技术栈: Python FastAPI + SQLAlchemy + PHP 7.4 + Fernet


任务 1:数据模型 + API 端点

文件: server/database.pyserver/api/servers.py

  • 步骤 1database.py 添加 PasswordPreset 模型

Setting 类之后、init_db() 之前插入:

class PasswordPreset(Base):
    """SSH password preset — encrypted storage"""
    __tablename__ = "password_presets"

    id = Column(Integer, primary_key=True, autoincrement=True)
    name = Column(String(100), nullable=False, comment="预设名称")
    encrypted_pw = Column(String(500), nullable=False, comment="加密后的密码")
    created_at = Column(DateTime, default=datetime.datetime.utcnow)
  • 步骤 2servers.py 添加 4 个预设端点

servers.py 导入 PasswordPreset

from server.database import get_db, Server, SyncLog, PasswordPreset, encrypt_value, decrypt_value

在文件末尾(PushRequest 类之前)添加:

# --- Password Presets ---

class PresetCreate(BaseModel):
    name: str
    password: str


class PresetResponse(BaseModel):
    id: int
    name: str


@router.get("/password-presets", response_model=List[PresetResponse])
def list_presets(db: Session = Depends(get_db)):
    return db.query(PasswordPreset).order_by(PasswordPreset.id).all()


@router.post("/password-presets", response_model=PresetResponse)
def create_preset(data: PresetCreate, db: Session = Depends(get_db)):
    preset = PasswordPreset(name=data.name, encrypted_pw=encrypt_value(data.password))
    db.add(preset)
    db.commit()
    db.refresh(preset)
    return preset


@router.delete("/password-presets/{preset_id}")
def delete_preset(preset_id: int, db: Session = Depends(get_db)):
    preset = db.query(PasswordPreset).filter(PasswordPreset.id == preset_id).first()
    if not preset:
        raise HTTPException(status_code=404, detail="Preset not found")
    db.delete(preset)
    db.commit()
    return {"message": "deleted"}


@router.post("/password-presets/{preset_id}/reveal")
def reveal_preset(preset_id: int, db: Session = Depends(get_db)):
    preset = db.query(PasswordPreset).filter(PasswordPreset.id == preset_id).first()
    if not preset:
        raise HTTPException(status_code=404, detail="Preset not found")
    return {"password": decrypt_value(preset.encrypted_pw)}
  • 步骤 3:验证编译 + Commit
python -m py_compile server/database.py server/api/servers.py && echo "OK"
git add server/database.py server/api/servers.py
git commit -m "feat(presets): PasswordPreset 模型 + 4 个 CRUD/reveal 端点"

任务 2settings.php 密码预设管理面板

文件: web/settings.php

  • 步骤 1:在 settings.php 末尾(</body> 前)添加预设管理 HTML + JS

找到 settings.php 中「修改密码」section 之后,添加新 section

<!-- 密码预设管理 -->
<div class="settings-section">
    <h3><i class="fas fa-key"></i> SSH 密码预设</h3>
    <p style="font-size:13px;color:#64748b;margin-bottom:12px;">保存常用 SSH 密码,新建服务器时快速选择</p>

    <div id="presetList" style="margin-bottom:16px;">加载中...</div>

    <form onsubmit="addPreset(event)" style="display:flex;gap:8px;align-items:flex-end;">
        <div class="form-group" style="flex:1;margin:0;">
            <label>名称</label>
            <input type="text" id="newPresetName" class="form-control" placeholder="如: 生产环境" required>
        </div>
        <div class="form-group" style="flex:1;margin:0;">
            <label>密码</label>
            <input type="password" id="newPresetPw" class="form-control" placeholder="SSH 密码" required>
        </div>
        <button type="submit" class="btn btn-primary"><i class="fas fa-save"></i> 添加</button>
    </form>
</div>

<script>
const API_BASE = <?= json_encode(defined('API_BASE_URL') ? API_BASE_URL : 'http://localhost:8600') ?>;
const API_KEY = <?= json_encode(defined('API_KEY') ? API_KEY : '') ?>;

function loadPresets() {
    fetch(API_BASE + '/api/servers/password-presets', { headers: API_KEY ? {'X-API-Key': API_KEY} : {} })
        .then(r => r.json()).then(data => {
            const presets = data.data || data;
            let html = '';
            if (!presets.length) html = '<p style="color:#9ca3af;">暂无预设</p>';
            presets.forEach(p => {
                html += '<div style="display:flex;align-items:center;gap:8px;padding:6px 0;border-bottom:1px solid #f3f4f6;">' +
                    '<span style="flex:1;">' + esc(p.name) + '</span>' +
                    '<button class="btn btn-sm btn-info" onclick="revealPreset(' + p.id + ', this)"><i class="fas fa-eye"></i> 查看</button>' +
                    '<button class="btn btn-sm btn-danger" onclick="deletePreset(' + p.id + ')"><i class="fas fa-trash"></i></button></div>';
            });
            document.getElementById('presetList').innerHTML = html;
        });
}

function addPreset(e) {
    e.preventDefault();
    const name = document.getElementById('newPresetName').value.trim();
    const pw = document.getElementById('newPresetPw').value;
    if (!name || !pw) return;
    fetch(API_BASE + '/api/servers/password-presets', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', 'X-API-Key': API_KEY },
        body: JSON.stringify({ name, password: pw })
    }).then(r => r.json()).then(() => {
        document.getElementById('newPresetName').value = '';
        document.getElementById('newPresetPw').value = '';
        loadPresets();
    });
}

function revealPreset(id, btn) {
    fetch(API_BASE + '/api/servers/password-presets/' + id + '/reveal', {
        method: 'POST', headers: { 'X-API-Key': API_KEY }
    }).then(r => r.json()).then(data => {
        const pw = (data.data || data).password;
        btn.outerHTML = '<code style="background:#fef3c7;padding:2px 8px;border-radius:4px;">' + esc(pw) + '</code>';
        setTimeout(loadPresets, 5000);
    });
}

function deletePreset(id) {
    if (!confirm('确定删除此预设?')) return;
    fetch(API_BASE + '/api/servers/password-presets/' + id, {
        method: 'DELETE', headers: { 'X-API-Key': API_KEY }
    }).then(() => loadPresets());
}

function esc(s) { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
loadPresets();
</script>
  • 步骤 2Commit
git add web/settings.php
git commit -m "feat(presets): settings.php 密码预设管理面板"

任务 3servers.php 预设下拉联动

文件: web/servers.php

  • 步骤 1:在密码字段上方添加预设下拉

passwordField div 内部,密码 input 之前添加:

<div id="presetSelector" style="margin-bottom:8px;">
    <label style="font-size:12px;color:#6b7280;">密码预设</label>
    <select id="presetSelect" class="form-control" onchange="fillPresetPassword()" style="font-size:13px;">
        <option value="">自定义(手动输入)</option>
    </select>
</div>
  • 步骤 2:添加 JS 加载预设列表并联动填充

在现有 <script> 块末尾添加:

function loadPresetOptions() {
    fetch(API_BASE + '/api/servers/password-presets', { headers: API_KEY ? {'X-API-Key': API_KEY} : {} })
        .then(r => r.json()).then(data => {
            const presets = data.data || data;
            const sel = document.getElementById('presetSelect');
            presets.forEach(p => {
                const opt = document.createElement('option');
                opt.value = p.id;
                opt.textContent = p.name;
                sel.appendChild(opt);
            });
        });
}

function fillPresetPassword() {
    const id = document.getElementById('presetSelect').value;
    if (!id) return;
    fetch(API_BASE + '/api/servers/password-presets/' + id + '/reveal', {
        method: 'POST', headers: { 'X-API-Key': API_KEY }
    }).then(r => r.json()).then(data => {
        const pw = (data.data || data).password;
        document.querySelector('[name="password"]').value = pw;
    });
}

loadPresetOptions();
  • 步骤 3:删除旧的 localStorage 逻辑

删除 servers.php 中现有的 localStorage 代码(nexus_default_ssh_pw 相关 4 行),已被预设替代。

  • 步骤 4Commit
git add web/servers.php
git commit -m "feat(presets): servers.php 预设下拉联动填充密码"

任务 4:验证 + 更新 CHANGELOG

  • 步骤 1:全量编译验证
find server -name "*.py" | xargs python -m py_compile && echo "All Python OK"
  • 步骤 2docs/CHANGELOG.md 追加记录
### 密码预设功能
- 新增 PasswordPreset 表,Fernet 加密存储
- 4 个 API 端点(列表/创建/删除/解密查看)
- settings.php 密码预设管理面板
- servers.php 预设下拉自动填充密码
  • 步骤 3Commit
git add docs/CHANGELOG.md
git commit -m "docs: 更新 CHANGELOG — 密码预设功能"

自检

  • 规格全覆盖:模型 ✓ 4 端点 ✓ servers 下拉 ✓ settings 管理 ✓
  • 无占位符/TODO
  • 类型一致:PresetCreate / PresetResponse 与端点签名匹配
  • decrypt_value 导入正确(通过 database 模块)