feat: 服务管理支持 HTTP/HTTPS 协议选择
- Service 增加 scheme 字段,build_url 按协议拼接地址 - 表单新增协议下拉;启动时自动迁移已有 SQLite 库 - 更新部署说明中的 HTTPS 服务添加示例 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -118,6 +118,7 @@ def create_app() -> Flask:
|
|||||||
|
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
db.create_all()
|
db.create_all()
|
||||||
|
_migrate_schema()
|
||||||
_ensure_default_user()
|
_ensure_default_user()
|
||||||
_ensure_admin_from_env()
|
_ensure_admin_from_env()
|
||||||
|
|
||||||
@@ -254,6 +255,7 @@ def create_app() -> Flask:
|
|||||||
path = (form.path.data or "").strip() or "/"
|
path = (form.path.data or "").strip() or "/"
|
||||||
s = Service(
|
s = Service(
|
||||||
name=form.name.data.strip(),
|
name=form.name.data.strip(),
|
||||||
|
scheme=form.scheme.data,
|
||||||
host=form.host.data.strip(),
|
host=form.host.data.strip(),
|
||||||
port=form.port.data,
|
port=form.port.data,
|
||||||
path=path,
|
path=path,
|
||||||
@@ -288,6 +290,7 @@ def create_app() -> Flask:
|
|||||||
).all()
|
).all()
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
s.name = form.name.data.strip()
|
s.name = form.name.data.strip()
|
||||||
|
s.scheme = form.scheme.data
|
||||||
s.host = form.host.data.strip()
|
s.host = form.host.data.strip()
|
||||||
s.port = form.port.data
|
s.port = form.port.data
|
||||||
s.path = (form.path.data or "").strip() or "/"
|
s.path = (form.path.data or "").strip() or "/"
|
||||||
@@ -331,6 +334,28 @@ def create_app() -> Flask:
|
|||||||
return app
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
def _migrate_schema() -> None:
|
||||||
|
"""SQLite 已有库补列(db.create_all 不会 ALTER 已有表)。"""
|
||||||
|
from sqlalchemy import inspect, text
|
||||||
|
|
||||||
|
try:
|
||||||
|
insp = inspect(db.engine)
|
||||||
|
if "services" not in insp.get_table_names():
|
||||||
|
return
|
||||||
|
cols = {c["name"] for c in insp.get_columns("services")}
|
||||||
|
if "scheme" not in cols:
|
||||||
|
with db.engine.begin() as conn:
|
||||||
|
conn.execute(
|
||||||
|
text(
|
||||||
|
"ALTER TABLE services ADD COLUMN scheme VARCHAR(8) "
|
||||||
|
"NOT NULL DEFAULT 'http'"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
print("[nav] 已为 services 表添加 scheme 列(默认 http)。", flush=True)
|
||||||
|
except Exception as exc:
|
||||||
|
print(f"[nav] 数据库结构迁移跳过或失败: {exc}", flush=True)
|
||||||
|
|
||||||
|
|
||||||
def _first_group_id() -> Optional[int]:
|
def _first_group_id() -> Optional[int]:
|
||||||
g = ServiceGroup.query.order_by(ServiceGroup.sort_order, ServiceGroup.id).first()
|
g = ServiceGroup.query.order_by(ServiceGroup.sort_order, ServiceGroup.id).first()
|
||||||
return g.id if g else None
|
return g.id if g else None
|
||||||
|
|||||||
@@ -21,7 +21,16 @@ class GroupForm(FlaskForm):
|
|||||||
|
|
||||||
class ServiceForm(FlaskForm):
|
class ServiceForm(FlaskForm):
|
||||||
name = StringField("服务名称", validators=[DataRequired(message="请输入服务名称")])
|
name = StringField("服务名称", validators=[DataRequired(message="请输入服务名称")])
|
||||||
host = StringField("内网 IP 或主机名", validators=[DataRequired(message="请输入主机")])
|
scheme = SelectField(
|
||||||
|
"协议",
|
||||||
|
choices=[("http", "HTTP"), ("https", "HTTPS")],
|
||||||
|
default="http",
|
||||||
|
validators=[DataRequired(message="请选择协议")],
|
||||||
|
)
|
||||||
|
host = StringField(
|
||||||
|
"主机或域名",
|
||||||
|
validators=[DataRequired(message="请输入主机或域名")],
|
||||||
|
)
|
||||||
port = IntegerField(
|
port = IntegerField(
|
||||||
"端口",
|
"端口",
|
||||||
validators=[
|
validators=[
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ class Service(db.Model):
|
|||||||
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
name = db.Column(db.String(120), nullable=False)
|
name = db.Column(db.String(120), nullable=False)
|
||||||
|
scheme = db.Column(db.String(8), nullable=False, default="http")
|
||||||
host = db.Column(db.String(255), nullable=False)
|
host = db.Column(db.String(255), nullable=False)
|
||||||
port = db.Column(db.Integer, nullable=False)
|
port = db.Column(db.Integer, nullable=False)
|
||||||
path = db.Column(db.String(512), nullable=False, default="/")
|
path = db.Column(db.String(512), nullable=False, default="/")
|
||||||
@@ -51,4 +52,7 @@ class Service(db.Model):
|
|||||||
p = (self.path or "/").strip()
|
p = (self.path or "/").strip()
|
||||||
if not p.startswith("/"):
|
if not p.startswith("/"):
|
||||||
p = "/" + p
|
p = "/" + p
|
||||||
return f"http://{self.host}:{self.port}{p}"
|
proto = (self.scheme or "http").strip().lower()
|
||||||
|
if proto not in ("http", "https"):
|
||||||
|
proto = "http"
|
||||||
|
return f"{proto}://{self.host}:{self.port}{p}"
|
||||||
|
|||||||
@@ -28,9 +28,16 @@
|
|||||||
<div class="errors">{{ form.name.errors[0] }}</div>
|
<div class="errors">{{ form.name.errors[0] }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
{{ form.scheme.label }}
|
||||||
|
{{ form.scheme() }}
|
||||||
|
{% if form.scheme.errors %}
|
||||||
|
<div class="errors">{{ form.scheme.errors[0] }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
{{ form.host.label }}
|
{{ form.host.label }}
|
||||||
{{ form.host(placeholder="例如 192.168.1.10 或 主机名") }}
|
{{ form.host(placeholder="例如 192.168.1.10 或 api.example.com") }}
|
||||||
{% if form.host.errors %}
|
{% if form.host.errors %}
|
||||||
<div class="errors">{{ form.host.errors[0] }}</div>
|
<div class="errors">{{ form.host.errors[0] }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
+14
-2
@@ -29,7 +29,7 @@
|
|||||||
| 数据库 | SQLite,默认文件名为 `nav_local.db`(与运行当前工作目录有关)。 |
|
| 数据库 | SQLite,默认文件名为 `nav_local.db`(与运行当前工作目录有关)。 |
|
||||||
| 网络监听 | 默认绑定 `0.0.0.0`,便于同局域网手机、电脑访问。 |
|
| 网络监听 | 默认绑定 `0.0.0.0`,便于同局域网手机、电脑访问。 |
|
||||||
|
|
||||||
**说明**:服务访问地址由程序拼接为 `http://{host}:{port}{path}`,当前版本固定为 **HTTP**(内网场景)。若目标服务为 HTTPS,需在后续版本中扩展字段;嵌入 iframe 时浏览器仍按 HTTPS 规则校验混合内容等。
|
**说明**:服务访问地址由程序拼接为 `{scheme}://{host}:{port}{path}`,可在「服务管理」中选择 **HTTP** 或 **HTTPS**。嵌入 iframe 时浏览器仍按混合内容等安全策略校验(例如导航站为 HTTPS、内嵌目标为 HTTP 时可能被浏览器拦截)。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -214,12 +214,24 @@ http://<本机局域网IP>:5070
|
|||||||
|
|
||||||
- 字段含义简要说明:
|
- 字段含义简要说明:
|
||||||
- **服务名称**:左侧显示名称。
|
- **服务名称**:左侧显示名称。
|
||||||
- **内网 IP 或主机名**:如 `192.168.1.10` 或可解析的主机名。
|
- **协议**:**HTTP** 或 **HTTPS**;HTTPS 站点选 HTTPS,端口一般填 `443`(或其它 TLS 端口)。
|
||||||
|
- **主机或域名**:如 `192.168.1.10`、`api.example.com` 等。
|
||||||
- **端口**:1–65535。
|
- **端口**:1–65535。
|
||||||
- **路径**:可选,须以 `/` 开头;留空则按 `/` 处理。
|
- **路径**:可选,须以 `/` 开头;留空则按 `/` 处理。
|
||||||
- **分组**:必选。
|
- **分组**:必选。
|
||||||
- **排序**:同分组内数字越小越靠前。
|
- **排序**:同分组内数字越小越靠前。
|
||||||
|
|
||||||
|
**HTTPS 服务示例:**
|
||||||
|
|
||||||
|
| 字段 | 示例值 |
|
||||||
|
|------|--------|
|
||||||
|
| 协议 | HTTPS |
|
||||||
|
| 主机或域名 | `panel.example.com` |
|
||||||
|
| 端口 | `443` |
|
||||||
|
| 路径 | `/` |
|
||||||
|
|
||||||
|
生成地址:`https://panel.example.com:443/`
|
||||||
|
|
||||||
### 8.5 关于 iframe 打不开的说明
|
### 8.5 关于 iframe 打不开的说明
|
||||||
|
|
||||||
部分网站(尤其银行、部分管理面板)通过 **`X-Frame-Options`** 或 **`Content-Security-Policy`** 禁止被嵌入 iframe,此时右侧区域可能为空白或浏览器控制台报错。这属于 **目标站点安全策略**,与本导航站实现无关。若必须统一入口,只能由目标服务侧放开嵌入策略,或改为新窗口打开(需改代码,非当前默认行为)。
|
部分网站(尤其银行、部分管理面板)通过 **`X-Frame-Options`** 或 **`Content-Security-Policy`** 禁止被嵌入 iframe,此时右侧区域可能为空白或浏览器控制台报错。这属于 **目标站点安全策略**,与本导航站实现无关。若必须统一入口,只能由目标服务侧放开嵌入策略,或改为新窗口打开(需改代码,非当前默认行为)。
|
||||||
|
|||||||
Reference in New Issue
Block a user