mirror of
https://gitcode.com/JianFeeeee/TrulyMEM-TrueHumanMEM.git
synced 2026-10-15 12:15:32 +00:00
feat: HTTPS/SSL 开关 — 设置页切换证书路径,重启生效
This commit is contained in:
130
core/web_api.py
130
core/web_api.py
@ -82,31 +82,69 @@ _last_cleanup = time.time()
|
|||||||
|
|
||||||
|
|
||||||
# Web 服务配置(仅 SECRET_KEY 保留在 json 文件,用户信息在数据库)
|
# Web 服务配置(仅 SECRET_KEY 保留在 json 文件,用户信息在数据库)
|
||||||
def load_secret_key():
|
def _find_config_path():
|
||||||
|
"""查找已有的 web_config.json,或返回默认路径"""
|
||||||
import json
|
import json
|
||||||
defaults = {"SECRET_KEY": "trulymem-secret-key-2026"}
|
|
||||||
search_paths = [
|
search_paths = [
|
||||||
os.path.join(os.path.dirname(os.path.abspath(__file__)), 'web_config.json'),
|
os.path.join(os.path.dirname(os.path.abspath(__file__)), 'web_config.json'),
|
||||||
os.path.join(os.getcwd(), 'web_config.json'),
|
os.path.join(os.getcwd(), 'web_config.json'),
|
||||||
os.path.join(os.path.expanduser("~"), ".trulymem", 'web_config.json'),
|
os.path.join(os.path.expanduser("~"), ".trulymem", 'web_config.json'),
|
||||||
]
|
]
|
||||||
for config_path in search_paths:
|
for p in search_paths:
|
||||||
if os.path.exists(config_path):
|
if os.path.exists(p):
|
||||||
try:
|
return p
|
||||||
with open(config_path, 'r', encoding='utf-8') as f:
|
return search_paths[0]
|
||||||
file_config = json.load(f)
|
|
||||||
if "SECRET_KEY" in file_config:
|
|
||||||
defaults["SECRET_KEY"] = file_config["SECRET_KEY"]
|
def load_web_config():
|
||||||
break
|
"""加载完整 web_config.json"""
|
||||||
except Exception:
|
import json
|
||||||
continue
|
defaults = {
|
||||||
|
"SECRET_KEY": "trulymem-secret-key-2026",
|
||||||
|
"ssl_enabled": False,
|
||||||
|
"ssl_cert_path": "",
|
||||||
|
"ssl_key_path": "",
|
||||||
|
}
|
||||||
|
config_path = _find_config_path()
|
||||||
|
if os.path.exists(config_path):
|
||||||
|
try:
|
||||||
|
with open(config_path, 'r', encoding='utf-8') as f:
|
||||||
|
file_config = json.load(f)
|
||||||
|
for k in defaults:
|
||||||
|
if k in file_config:
|
||||||
|
defaults[k] = file_config[k]
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
return defaults
|
return defaults
|
||||||
|
|
||||||
WEB_CONFIG = load_secret_key()
|
|
||||||
|
def save_web_config(updates: dict) -> bool:
|
||||||
|
"""更新并保存 web_config.json"""
|
||||||
|
import json
|
||||||
|
config_path = _find_config_path()
|
||||||
|
# 读取已有配置
|
||||||
|
current = {}
|
||||||
|
if os.path.exists(config_path):
|
||||||
|
try:
|
||||||
|
with open(config_path, 'r', encoding='utf-8') as f:
|
||||||
|
current = json.load(f)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
current.update(updates)
|
||||||
|
try:
|
||||||
|
os.makedirs(os.path.dirname(config_path), exist_ok=True)
|
||||||
|
with open(config_path, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(current, f, ensure_ascii=False, indent=2)
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
WEB_CONFIG = load_web_config()
|
||||||
|
|
||||||
_ui_dir = os.path.join(os.path.dirname(__file__), '..', 'ui')
|
_ui_dir = os.path.join(os.path.dirname(__file__), '..', 'ui')
|
||||||
app = Flask(__name__, static_folder=os.path.join(_ui_dir, 'static'), static_url_path='', template_folder=os.path.join(_ui_dir, 'templates'))
|
app = Flask(__name__, static_folder=os.path.join(_ui_dir, 'static'), static_url_path='', template_folder=os.path.join(_ui_dir, 'templates'))
|
||||||
app.secret_key = WEB_CONFIG["SECRET_KEY"]
|
app.secret_key = WEB_CONFIG.get("SECRET_KEY", "trulymem-secret-key-2026")
|
||||||
app.permanent_session_lifetime = timedelta(days=7)
|
app.permanent_session_lifetime = timedelta(days=7)
|
||||||
CORS(app, supports_credentials=True) # 启用跨域支持,支持 session cookies
|
CORS(app, supports_credentials=True) # 启用跨域支持,支持 session cookies
|
||||||
|
|
||||||
@ -559,6 +597,49 @@ def web_settings_config():
|
|||||||
return jsonify({"success": False, "error": "没有需要更新的配置"}), 400
|
return jsonify({"success": False, "error": "没有需要更新的配置"}), 400
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/ssl/config', methods=['GET', 'POST'])
|
||||||
|
@api_login_required
|
||||||
|
def web_ssl_config():
|
||||||
|
"""获取/更新 SSL 配置(服务器级别,写入 web_config.json,重启后生效)"""
|
||||||
|
if request.method == 'GET':
|
||||||
|
cfg = load_web_config()
|
||||||
|
return jsonify({
|
||||||
|
"success": True,
|
||||||
|
"ssl_enabled": cfg.get('ssl_enabled', False),
|
||||||
|
"ssl_cert_path": cfg.get('ssl_cert_path', ''),
|
||||||
|
"ssl_key_path": cfg.get('ssl_key_path', ''),
|
||||||
|
})
|
||||||
|
|
||||||
|
data = request.get_json() or {}
|
||||||
|
updates = {}
|
||||||
|
|
||||||
|
# 提取有用的字段
|
||||||
|
for key in ('ssl_enabled', 'ssl_cert_path', 'ssl_key_path'):
|
||||||
|
if key in data:
|
||||||
|
updates[key] = data[key]
|
||||||
|
|
||||||
|
if not updates:
|
||||||
|
return jsonify({"success": False, "error": "没有需要更新的字段"}), 400
|
||||||
|
|
||||||
|
# 如果启用 SSL,验证证书路径
|
||||||
|
if updates.get('ssl_enabled'):
|
||||||
|
cert_path = updates.get('ssl_cert_path') or load_web_config().get('ssl_cert_path', '')
|
||||||
|
key_path = updates.get('ssl_key_path') or load_web_config().get('ssl_key_path', '')
|
||||||
|
if not os.path.exists(cert_path):
|
||||||
|
return jsonify({"success": False, "error": f"证书文件不存在: {cert_path}"}), 400
|
||||||
|
if not os.path.exists(key_path):
|
||||||
|
return jsonify({"success": False, "error": f"密钥文件不存在: {key_path}"}), 400
|
||||||
|
|
||||||
|
ok = save_web_config(updates)
|
||||||
|
if ok:
|
||||||
|
return jsonify({
|
||||||
|
"success": True,
|
||||||
|
"message": "SSL 配置已保存,重启服务后生效",
|
||||||
|
**updates
|
||||||
|
})
|
||||||
|
return jsonify({"success": False, "error": "写入配置文件失败"}), 500
|
||||||
|
|
||||||
|
|
||||||
@app.errorhandler(404)
|
@app.errorhandler(404)
|
||||||
def not_found(e):
|
def not_found(e):
|
||||||
"""404 处理"""
|
"""404 处理"""
|
||||||
@ -829,8 +910,25 @@ def run_web_server(port: int = 4096, host: str = '0.0.0.0') -> None:
|
|||||||
global _http_server
|
global _http_server
|
||||||
try:
|
try:
|
||||||
from werkzeug.serving import make_server
|
from werkzeug.serving import make_server
|
||||||
_http_server = make_server(host, port, app, threaded=True)
|
import ssl
|
||||||
print(f"Web API 服务启动在 http://{host}:{port}")
|
|
||||||
|
# 尝试加载 SSL 配置
|
||||||
|
cfg = load_web_config()
|
||||||
|
ssl_enabled = cfg.get('ssl_enabled', False)
|
||||||
|
ssl_context = None
|
||||||
|
if ssl_enabled:
|
||||||
|
cert_path = cfg.get('ssl_cert_path', '')
|
||||||
|
key_path = cfg.get('ssl_key_path', '')
|
||||||
|
if cert_path and os.path.exists(cert_path) and key_path and os.path.exists(key_path):
|
||||||
|
ssl_context = (cert_path, key_path)
|
||||||
|
print(f"🔒 HTTPS 已启用:cert={cert_path}")
|
||||||
|
else:
|
||||||
|
print(f"⚠️ SSL 已启用但证书路径无效:cert={cert_path}, key={key_path}")
|
||||||
|
print(" 回退到 HTTP")
|
||||||
|
|
||||||
|
_http_server = make_server(host, port, app, threaded=True, ssl_context=ssl_context)
|
||||||
|
proto = "https" if ssl_context else "http"
|
||||||
|
print(f"Web API 服务启动在 {proto}://{host}:{port}")
|
||||||
_http_server.serve_forever()
|
_http_server.serve_forever()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Web 服务启动失败: {e}")
|
print(f"Web 服务启动失败: {e}")
|
||||||
|
|||||||
@ -263,6 +263,34 @@
|
|||||||
<button type="submit" class="btn" id="changePwdBtn">更 新 密 码</button>
|
<button type="submit" class="btn" id="changePwdBtn">更 新 密 码</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<!-- HTTPS 配置 -->
|
||||||
|
<div class="section-title"><i class="fas fa-lock"></i> HTTPS / SSL</div>
|
||||||
|
<div class="toggle-row">
|
||||||
|
<div>
|
||||||
|
<div class="toggle-label">启用 HTTPS</div>
|
||||||
|
<div class="toggle-desc">开启后使用 HTTPS 协议,需提供有效的证书和密钥文件</div>
|
||||||
|
</div>
|
||||||
|
<label class="toggle-switch">
|
||||||
|
<input type="checkbox" id="enableSslToggle">
|
||||||
|
<span class="toggle-slider"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div id="sslCertFields" style="margin-top:12px;">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="ssl_cert_path">SSL 证书路径</label>
|
||||||
|
<input type="text" id="ssl_cert_path" placeholder="/etc/letsencrypt/live/example.com/fullchain.pem">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="ssl_key_path">SSL 密钥路径</label>
|
||||||
|
<input type="text" id="ssl_key_path" placeholder="/etc/letsencrypt/live/example.com/privkey.pem">
|
||||||
|
</div>
|
||||||
|
<button class="btn" id="saveSslConfigBtn">保 存 HTTPS 配 置</button>
|
||||||
|
<div id="sslStatus" style="text-align:center;font-size:12px;color:#666688;margin-top:5px;"></div>
|
||||||
|
<div style="text-align:center;font-size:11px;color:#555577;margin-top:5px;">
|
||||||
|
<i class="fas fa-info-circle"></i> 修改后需重启服务生效:<code>systemctl restart trulymem-web</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- TUI 服务控制 -->
|
<!-- TUI 服务控制 -->
|
||||||
<div class="section-title"><i class="fas fa-terminal"></i> TUI 终端服务</div>
|
<div class="section-title"><i class="fas fa-terminal"></i> TUI 终端服务</div>
|
||||||
<div class="toggle-row">
|
<div class="toggle-row">
|
||||||
@ -370,6 +398,21 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 加载 SSL 配置
|
||||||
|
try {
|
||||||
|
const sslResp = await fetch('/api/ssl/config');
|
||||||
|
const sslData = await sslResp.json();
|
||||||
|
if (sslData.success) {
|
||||||
|
document.getElementById('enableSslToggle').checked = sslData.ssl_enabled === true;
|
||||||
|
document.getElementById('ssl_cert_path').value = sslData.ssl_cert_path || '';
|
||||||
|
document.getElementById('ssl_key_path').value = sslData.ssl_key_path || '';
|
||||||
|
toggleSslFields(sslData.ssl_enabled === true);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// SSL 端点可能不支持
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 加载用户列表
|
// 加载用户列表
|
||||||
async function loadUsers() {
|
async function loadUsers() {
|
||||||
try {
|
try {
|
||||||
@ -616,6 +659,58 @@
|
|||||||
loadApiConfig();
|
loadApiConfig();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// SSL 开关 — 显示/隐藏证书路径
|
||||||
|
const sslToggle = document.getElementById('enableSslToggle');
|
||||||
|
const sslCertFields = document.getElementById('sslCertFields');
|
||||||
|
function toggleSslFields(enabled) {
|
||||||
|
sslCertFields.style.display = enabled ? '' : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
sslToggle.addEventListener('change', function() {
|
||||||
|
toggleSslFields(this.checked);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 保存 SSL 配置
|
||||||
|
document.getElementById('saveSslConfigBtn').addEventListener('click', async function() {
|
||||||
|
const enabled = sslToggle.checked;
|
||||||
|
const certPath = document.getElementById('ssl_cert_path').value.trim();
|
||||||
|
const keyPath = document.getElementById('ssl_key_path').value.trim();
|
||||||
|
|
||||||
|
if (enabled && (!certPath || !keyPath)) {
|
||||||
|
document.getElementById('sslStatus').innerHTML = '<i class="fas fa-exclamation-triangle" style="color:#ff6b6b"></i> 启用 HTTPS 必须填写证书和密钥路径';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const btn = this;
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.textContent = '保存中...';
|
||||||
|
document.getElementById('sslStatus').innerHTML = '<i class="fas fa-spinner fa-spin" style="color:#4488ff"></i> 保存中...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resp = await fetch('/api/ssl/config', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
ssl_enabled: enabled,
|
||||||
|
ssl_cert_path: certPath,
|
||||||
|
ssl_key_path: keyPath
|
||||||
|
})
|
||||||
|
});
|
||||||
|
const data = await resp.json();
|
||||||
|
if (data.success) {
|
||||||
|
document.getElementById('sslStatus').innerHTML = '<i class="fas fa-check-circle" style="color:#22c55e"></i> ' + (data.message || '配置已保存');
|
||||||
|
showSuccess('HTTPS 配置已保存');
|
||||||
|
} else {
|
||||||
|
document.getElementById('sslStatus').innerHTML = '<i class="fas fa-exclamation-triangle" style="color:#ff6b6b"></i> ' + (data.error || '保存失败');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
document.getElementById('sslStatus').innerHTML = '<i class="fas fa-exclamation-triangle" style="color:#ff6b6b"></i> 网络错误';
|
||||||
|
} finally {
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.textContent = '保 存 HTTPS 配 置';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// TUI 开关
|
// TUI 开关
|
||||||
document.getElementById('enableTuiToggle').addEventListener('change', async function() {
|
document.getElementById('enableTuiToggle').addEventListener('change', async function() {
|
||||||
const enable = this.checked;
|
const enable = this.checked;
|
||||||
|
|||||||
Reference in New Issue
Block a user