feat: frps-manager v1.0 - frp 0.70.1 multi-tenant web management system
- Multi-tenant client/proxy management with Flask+SQLite - Frps server control (start/restart/token rotation) - Generate frpc.ini with shared server token + admin API - Dashboard API integration (live client/proxy status) - RBAC: admin/tenant_admin/user roles - Audit logging for all operations - Systemd service files included - Requires legacy INI format (frp 0.70 TOML auth broken)
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}frps-manager{% endblock %}</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar">
|
||||
<div class="nav-inner">
|
||||
<a class="brand" href="{{ url_for('dashboard') }}">frps-manager</a>
|
||||
{% if user %}
|
||||
<ul class="nav-links">
|
||||
<li><a href="{{ url_for('dashboard') }}" class="{{ 'active' if request.endpoint == 'dashboard' else '' }}">仪表盘</a></li>
|
||||
{% if user.role == 'admin' %}
|
||||
<li><a href="{{ url_for('tenants_list') }}" class="{{ 'active' if request.endpoint and request.endpoint.startswith('tenants') else '' }}">租户</a></li>
|
||||
{% endif %}
|
||||
<li><a href="{{ url_for('clients_list') }}" class="{{ 'active' if request.endpoint and request.endpoint.startswith('clients') else '' }}">客户端</a></li>
|
||||
<li><a href="{{ url_for('proxies_list') }}" class="{{ 'active' if request.endpoint and request.endpoint.startswith('proxies') else '' }}">代理</a></li>
|
||||
{% if user.role == 'admin' %}
|
||||
<li><a href="{{ url_for('users_list') }}" class="{{ 'active' if request.endpoint and request.endpoint.startswith('users') else '' }}">用户</a></li>
|
||||
<li><a href="{{ url_for('logs_list') }}" class="{{ 'active' if request.endpoint and request.endpoint.startswith('logs') else '' }}">日志</a></li>
|
||||
{% endif %}
|
||||
<li><a href="{{ url_for('frps_config_view') }}" target="_blank">配置</a></li>
|
||||
</ul>
|
||||
<div class="nav-user">
|
||||
<span class="user-badge">
|
||||
{{ user.username }}
|
||||
<span class="role-tag role-{{ user.role }}">{{ user.role }}</span>
|
||||
</span>
|
||||
<a href="{{ url_for('change_password') }}">改密</a>
|
||||
<a href="{{ url_for('logout') }}">登出</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container">
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
<div class="flash-area">
|
||||
{% for category, message in messages %}
|
||||
<div class="flash flash-{{ category }}">{{ message }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
|
||||
<footer class="footer">
|
||||
frps-manager · frps 0.70+ · {{ now }}
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,13 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}修改密码 · frps-manager{% endblock %}
|
||||
{% block content %}
|
||||
<h1>修改密码</h1>
|
||||
<form method="post" class="form-card">
|
||||
<label>当前密码 <input name="old_password" type="password" required></label>
|
||||
<label>新密码 (至少 6 位) <input name="new_password" type="password" required minlength="6"></label>
|
||||
<div class="form-actions">
|
||||
<button class="btn btn-primary" type="submit">保存</button>
|
||||
<a class="btn" href="{{ url_for('dashboard') }}">取消</a>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,45 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ '编辑' if item else '新建' }}客户端 · frps-manager{% endblock %}
|
||||
{% block content %}
|
||||
<h1>{{ '编辑客户端' if item else '新建客户端 (frpc 注册)' }}</h1>
|
||||
<form method="post" class="form-card">
|
||||
{% if item %}
|
||||
<label>名称 (frpc user) <input value="{{ item.name }}" disabled></label>
|
||||
<label>租户
|
||||
<select name="tenant_id">
|
||||
{% for t in tenants %}
|
||||
<option value="{{ t.id }}" {% if t.id == item.tenant_id %}selected{% endif %}>{{ t.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
{% else %}
|
||||
<label>租户
|
||||
<select name="tenant_id" required>
|
||||
{% for t in tenants %}<option value="{{ t.id }}">{{ t.name }}</option>{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
<label>名称 (frpc user 字段)
|
||||
<input name="name" required pattern="[a-zA-Z0-9_.-]+" title="字母数字下划线短横线点">
|
||||
</label>
|
||||
{% endif %}
|
||||
<label>frps 地址 (文档用)
|
||||
<input name="server_addr" value="{{ item.server_addr or '' if item else '' }}"
|
||||
placeholder="例如 your.frps.host">
|
||||
</label>
|
||||
<label>描述 <input name="description" value="{{ item.description or '' if item else '' }}"></label>
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" name="is_active" {% if item is none or item.is_active %}checked{% endif %}>
|
||||
启用(禁用后 frpc 即使配了相同 token 也会被 dashboard 列为 inactive)
|
||||
</label>
|
||||
<div class="form-actions">
|
||||
<button class="btn btn-primary" type="submit">保存</button>
|
||||
<a class="btn" href="{{ url_for('clients_list') }}">取消</a>
|
||||
</div>
|
||||
{% if item %}
|
||||
<p class="muted small">
|
||||
创建后请到客户端列表页点 <strong>frpc.toml</strong> 按钮下载配置。
|
||||
frpc 端不需要填 token — server token 是共享的,在仪表盘上能看到。
|
||||
</p>
|
||||
{% endif %}
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,71 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}客户端 · frps-manager{% endblock %}
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1>客户端 (frpc)</h1>
|
||||
<a class="btn btn-primary" href="{{ url_for('clients_new') }}">+ 新建客户端</a>
|
||||
</div>
|
||||
|
||||
{% if tenants|length > 1 %}
|
||||
<form method="get" class="filter-bar">
|
||||
<label>租户
|
||||
<select name="tenant_id" onchange="this.form.submit()">
|
||||
<option value="">全部</option>
|
||||
{% for t in tenants %}
|
||||
<option value="{{ t.id }}" {% if filter_tenant_id == t.id %}selected{% endif %}>{{ t.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr><th>名称 (frpc user)</th><th>租户</th><th>在线</th><th>NAT</th><th>版本</th><th>frpc 连接</th><th>状态</th><th>操作</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for c in items %}
|
||||
<tr>
|
||||
<td>
|
||||
<strong>{{ c.name }}</strong>
|
||||
<br><span class="muted small">{{ c.description or '' }}</span>
|
||||
{% if c.server_addr %}<br><span class="muted small">frps: {{ c.server_addr }}</span>{% endif %}
|
||||
</td>
|
||||
<td>{{ c.tenant.name }}</td>
|
||||
<td>
|
||||
{% if c._online %}
|
||||
<span class="badge badge-ok">在线</span>
|
||||
<span class="muted small">{{ c._version }}</span>
|
||||
{% else %}
|
||||
<span class="badge badge-off">离线</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ c._nat }}</td>
|
||||
<td>{{ c._version }}</td>
|
||||
<td>{{ c._conn_count }}</td>
|
||||
<td>{% if c.is_active %}<span class="badge badge-ok">启用</span>{% else %}<span class="badge badge-off">禁用</span>{% endif %}</td>
|
||||
<td class="actions">
|
||||
<a href="{{ url_for('clients_frpc_config', cid=c.id) }}" target="_blank">frpc.toml</a>
|
||||
<a href="{{ url_for('clients_edit', cid=c.id) }}">编辑</a>
|
||||
<form method="post" action="{{ url_for('clients_delete', cid=c.id) }}" style="display:inline"
|
||||
onsubmit="return confirm('删除客户端 {{ c.name }} ?注意:frps 0.70 用共享 token,删除注册不会立即断开已连的 frpc。');">
|
||||
<button class="link-btn link-danger">删除</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="8" class="muted">暂无客户端</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<section style="margin-top: 24px;">
|
||||
<h2>使用流程</h2>
|
||||
<ol class="muted">
|
||||
<li>复制上面任意客户端的 <strong>frpc.toml</strong> 到目标 frpc 主机</li>
|
||||
<li>在该主机的 frpc.toml 末尾追加 <code>[[proxies]]</code> 段定义要暴露的本地服务</li>
|
||||
<li><code>systemctl enable --now frpc</code> 或 <code>frpc -c /etc/frpc.toml</code></li>
|
||||
<li>回到仪表盘或「在线代理」页,应能看到客户端上线</li>
|
||||
</ol>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,118 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}仪表盘 · frps-manager{% endblock %}
|
||||
{% block content %}
|
||||
<h1>仪表盘</h1>
|
||||
|
||||
<section class="stats">
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">frps 版本</div>
|
||||
<div class="stat-value">
|
||||
{% if info.error %}
|
||||
<span class="offline">离线</span>
|
||||
{% else %}
|
||||
{{ info.server.version }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">监听端口</div>
|
||||
<div class="stat-value">
|
||||
{% if info.error %}-{% else %}{{ info.server.bindPort }}{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">在线 frpc 客户端</div>
|
||||
<div class="stat-value">
|
||||
{% if info.error %}-{% else %}{{ info.clients.total }}{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">活跃代理</div>
|
||||
<div class="stat-value">
|
||||
{% if info.error %}-{% else %}{{ info.proxies.total }}{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">租户 / 客户端 / 代理</div>
|
||||
<div class="stat-value">{{ tenant_count }} / {{ client_count }} / {{ proxy_count }}</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% if info.error %}
|
||||
<div class="alert alert-error">
|
||||
无法连接 frps dashboard({{ info.error }})。请确认 frps 服务已启动且 dashboard 配置正确。
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if user.role == 'admin' %}
|
||||
<section class="actions">
|
||||
<h2>服务控制</h2>
|
||||
<form method="post" action="{{ url_for('frps_reload') }}" style="display:inline">
|
||||
<button class="btn">重写 frps.ini 并重启</button>
|
||||
</form>
|
||||
<form method="post" action="{{ url_for('frps_rotate_token') }}" style="display:inline"
|
||||
onsubmit="return confirm('旋转 server token 会让所有 frpc 立刻断连,确定?');">
|
||||
<button class="btn btn-warning">旋转 server token</button>
|
||||
</form>
|
||||
<a class="btn" href="{{ url_for('frps_config_view') }}" target="_blank">查看 frps.ini</a>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Server Token(所有 frpc 共享)</h2>
|
||||
<p class="muted small">frpc 配置 <code>token = ...</code> 字段用此值。轮换会让所有连接断掉。</p>
|
||||
<code class="token-cell">{{ server_token }}</code>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
<section>
|
||||
<h2>在线 frpc 客户端</h2>
|
||||
{% if info.error or not info.clients.get("items") %}
|
||||
<p class="muted">尚无客户端连接</p>
|
||||
{% else %}
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr><th>用户 (frpc [common] user)</th><th>客户端 ID</th><th>版本</th><th>NAT</th><th>连接数</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for c in info.clients.get("items", []) %}
|
||||
<tr>
|
||||
<td><code>{{ c.user or '(unnamed)' }}</code></td>
|
||||
<td><code>{{ c.clientID[:12] }}…</code></td>
|
||||
<td>{{ c.version }}</td>
|
||||
<td>{{ c.nat }}</td>
|
||||
<td>{{ c.conn_count }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>活跃代理(来自 frps dashboard)</h2>
|
||||
{% if info.error or not info.proxies.get("items") %}
|
||||
<p class="muted">尚无活跃代理</p>
|
||||
{% else %}
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr><th>名称</th><th>类型</th><th>状态</th><th>流量 in</th><th>流量 out</th><th>来自 frpc</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for p in info.proxies.get("items", []) %}
|
||||
<tr>
|
||||
<td><code>{{ p.name }}</code></td>
|
||||
<td><span class="tag tag-{{ p.type }}">{{ p.type }}</span></td>
|
||||
<td>
|
||||
{% if p.status.phase == 'online' %}<span class="badge badge-ok">在线</span>
|
||||
{% else %}<span class="badge badge-off">离线</span>{% endif %}
|
||||
</td>
|
||||
<td>{{ p.status.todayTrafficIn|fmt_bytes }}</td>
|
||||
<td>{{ p.status.todayTrafficOut|fmt_bytes }}</td>
|
||||
<td><code>{{ p.clientID[:12] or '-' }}…</code></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,9 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ code }} · frps-manager{% endblock %}
|
||||
{% block content %}
|
||||
<div class="error-card">
|
||||
<h1>{{ code }}</h1>
|
||||
<p class="muted">{{ message }}</p>
|
||||
<a class="btn" href="{{ url_for('dashboard') }}">回首页</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,46 @@
|
||||
# Generated by frps-manager for client: {{ client.name }}
|
||||
# Copy to your frpc host (e.g. /etc/frpc.toml), then:
|
||||
# systemctl enable --now frpc # if you used the example systemd unit
|
||||
# # OR directly:
|
||||
# frpc -c /etc/frpc.toml
|
||||
#
|
||||
# IMPORTANT:
|
||||
# * serverAddr / serverPort / auth.token come from your admin — do not change
|
||||
# unless the admin told you to.
|
||||
# * All clients use the SAME shared token; frpc identifies itself via `user`.
|
||||
# * Append your own [[proxies]] blocks below this line to expose local services.
|
||||
|
||||
serverAddr = "{{ server_addr }}"
|
||||
serverPort = {{ bind_port }}
|
||||
|
||||
[auth]
|
||||
method = "token"
|
||||
token = "{{ server_token }}"
|
||||
|
||||
# Optional transport defaults (uncomment if you want them)
|
||||
# transport.useEncryption = true
|
||||
# transport.useCompression = true
|
||||
|
||||
[webServer]
|
||||
addr = "127.0.0.1"
|
||||
port = 7400
|
||||
user = "admin"
|
||||
password = "admin"
|
||||
|
||||
# ---------------------------------------------------------------
|
||||
# Add your proxies below. Examples:
|
||||
#
|
||||
# [[proxies]]
|
||||
# name = "ssh"
|
||||
# type = "tcp"
|
||||
# localIP = "127.0.0.1"
|
||||
# localPort = 22
|
||||
# remotePort = 6000
|
||||
#
|
||||
# [[proxies]]
|
||||
# name = "web"
|
||||
# type = "http"
|
||||
# localIP = "127.0.0.1"
|
||||
# localPort = 8080
|
||||
# customDomains = ["your.domain.example.com"]
|
||||
# ---------------------------------------------------------------
|
||||
@@ -0,0 +1,14 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}登录 · frps-manager{% endblock %}
|
||||
{% block content %}
|
||||
<div class="login-card">
|
||||
<h1>frps-manager</h1>
|
||||
<p class="muted">frp 内网穿透 Web 管理控制台</p>
|
||||
<form method="post">
|
||||
<label>用户名 <input name="username" autofocus required></label>
|
||||
<label>密码 <input name="password" type="password" required></label>
|
||||
<button type="submit" class="btn btn-primary">登录</button>
|
||||
</form>
|
||||
<p class="muted small">默认管理员 admin / admin(首次登录后请立刻改密)</p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,33 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}审计日志 · frps-manager{% endblock %}
|
||||
{% block content %}
|
||||
<h1>审计日志</h1>
|
||||
<p class="muted small">共 {{ total }} 条</p>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr><th>时间</th><th>用户</th><th>动作</th><th>对象</th><th>详情</th><th>IP</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for e in items %}
|
||||
<tr>
|
||||
<td>{{ e.created_at|fmt_dt }}</td>
|
||||
<td>{{ e.username or '-' }}</td>
|
||||
<td><span class="action-tag">{{ e.action }}</span></td>
|
||||
<td>
|
||||
{{ e.target_type or '-' }}
|
||||
{% if e.target_label %}<br><code>{{ e.target_label }}</code>{% endif %}
|
||||
</td>
|
||||
<td>{{ e.detail or '' }}</td>
|
||||
<td><code>{{ e.ip or '-' }}</code></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% if total > per %}
|
||||
<div class="pagination">
|
||||
{% if page > 1 %}<a href="?page={{ page-1 }}">上一页</a>{% endif %}
|
||||
<span class="muted">第 {{ page }} 页 / 共 {{ ((total + per - 1) // per) }} 页</span>
|
||||
{% if page * per < total %}<a href="?page={{ page+1 }}">下一页</a>{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,83 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}代理 · frps-manager{% endblock %}
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1>代理规则</h1>
|
||||
<a class="btn btn-primary" href="{{ url_for('proxies_new') }}">+ 新建代理</a>
|
||||
</div>
|
||||
|
||||
{% if tenants|length > 1 or clients|length > 1 %}
|
||||
<form method="get" class="filter-bar">
|
||||
{% if tenants|length > 1 %}
|
||||
<label>租户
|
||||
<select name="tenant_id" onchange="this.form.submit()">
|
||||
<option value="">全部</option>
|
||||
{% for t in tenants %}
|
||||
<option value="{{ t.id }}" {% if filter_tenant_id == t.id %}selected{% endif %}>{{ t.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
{% endif %}
|
||||
<label>客户端
|
||||
<select name="client_id" onchange="this.form.submit()">
|
||||
<option value="">全部</option>
|
||||
{% for c in clients %}
|
||||
<option value="{{ c.id }}" {% if filter_client_id == c.id %}selected{% endif %}>{{ c.name }} ({{ c.tenant.name }})</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr><th>名称</th><th>类型</th><th>客户端</th><th>远端</th><th>本地</th><th>在线</th><th>启用</th><th>操作</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for p in items %}
|
||||
<tr>
|
||||
<td>
|
||||
<strong>{{ p.name }}</strong>
|
||||
{% if p.note %}<br><span class="muted small">{{ p.note }}</span>{% endif %}
|
||||
</td>
|
||||
<td><span class="tag tag-{{ p.type }}">{{ p.type }}</span></td>
|
||||
<td>{{ p.client.name }}<br><span class="muted small">{{ p.client.tenant.name }}</span></td>
|
||||
<td>
|
||||
{% if p.type in ('tcp','udp') %}
|
||||
<code>:{{ p.remote_port or '?' }}</code>
|
||||
{% elif p.subdomain %}
|
||||
<code>{{ p.subdomain }}.<base></code>
|
||||
{% elif p.custom_domains %}
|
||||
<code>{{ p.custom_domains.split(',')[0] }}{% if p.custom_domains.count(',') > 0 %} ...{% endif %}</code>
|
||||
{% else %}-{% endif %}
|
||||
</td>
|
||||
<td><code>{{ p.local_ip }}:{{ p.local_port }}</code></td>
|
||||
<td>
|
||||
{% if p.name in online_names %}<span class="badge badge-ok">在线</span>
|
||||
{% else %}<span class="badge badge-off">离线</span>{% endif %}
|
||||
</td>
|
||||
<td>{% if p.is_enabled %}<span class="badge badge-ok">启用</span>{% else %}<span class="badge badge-off">禁用</span>{% endif %}</td>
|
||||
<td class="actions">
|
||||
<a href="{{ url_for('proxies_edit', pid=p.id) }}">编辑</a>
|
||||
<form method="post" action="{{ url_for('proxies_toggle', pid=p.id) }}" style="display:inline">
|
||||
<button class="link-btn">{% if p.is_enabled %}禁用{% else %}启用{% endif %}</button>
|
||||
</form>
|
||||
<form method="post" action="{{ url_for('proxies_delete', pid=p.id) }}" style="display:inline"
|
||||
onsubmit="return confirm('删除代理 {{ p.name }} ?');">
|
||||
<button class="link-btn link-danger">删除</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="8" class="muted">暂无代理</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<section style="margin-top: 24px;">
|
||||
<p class="muted small">
|
||||
修改代理规则后,对应 frpc 主机需要重新拉取 <code>frpc.ini</code> 并执行 <code>frpc reload -c /etc/frpc.ini</code> 让新代理生效。
|
||||
仪表盘上的「在线」状态来自 frps 实时报告,需要 frpc reload 后才会刷新。
|
||||
</p>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,67 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ '编辑' if item else '新建' }}代理 · frps-manager{% endblock %}
|
||||
{% block content %}
|
||||
<h1>{{ '编辑代理' if item else '新建代理规则' }}</h1>
|
||||
<form method="post" class="form-card">
|
||||
{% if item %}
|
||||
<label>名称 <input value="{{ item.name }}" disabled></label>
|
||||
<label>类型 <input value="{{ item.type }}" disabled></label>
|
||||
<label>客户端 <input value="{{ item.client.name }} ({{ item.client.tenant.name }})" disabled></label>
|
||||
{% else %}
|
||||
<label>客户端
|
||||
<select name="client_id" required>
|
||||
{% for c in clients %}<option value="{{ c.id }}" {% if prefill_client == c.id %}selected{% endif %}>{{ c.name }} ({{ c.tenant.name }})</option>{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
<label>名称 <input name="name" required pattern="[a-zA-Z0-9_.-]+"></label>
|
||||
<label>类型
|
||||
<select name="type" required>
|
||||
{% for t in types %}<option value="{{ t }}">{{ t }}</option>{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
{% endif %}
|
||||
|
||||
<fieldset>
|
||||
<legend>本地服务(frpc 那台机器上的)</legend>
|
||||
<label>本地 IP <input name="local_ip" value="{{ item.local_ip if item else '127.0.0.1' }}"></label>
|
||||
<label>本地端口 <input name="local_port" type="number" min="1" max="65535" required value="{{ item.local_port if item else '' }}"></label>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>对外暴露(frps 上的端口 / 域名)</legend>
|
||||
<label>远端端口 (tcp/udp 专用) <input name="remote_port" type="number" min="1" max="65535" value="{{ item.remote_port or '' }}"></label>
|
||||
<label>custom_domains (http/https,逗号分隔) <input name="custom_domains" value="{{ item.custom_domains or '' }}" placeholder="a.example.com,b.example.com"></label>
|
||||
<label>subdomain (http/https) <input name="subdomain" value="{{ item.subdomain or '' }}"></label>
|
||||
<label>locations (http 路径,逗号分隔) <input name="locations" value="{{ item.locations or '' }}" placeholder="/api,/static"></label>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>传输 / 健康检查</legend>
|
||||
<label class="checkbox"><input type="checkbox" name="use_encryption" {% if item and item.use_encryption %}checked{% endif %}> 启用加密</label>
|
||||
<label class="checkbox"><input type="checkbox" name="use_compression" {% if item and item.use_compression %}checked{% endif %}> 启用压缩</label>
|
||||
<label>健康检查类型
|
||||
<select name="health_check_type">
|
||||
<option value="">不启用</option>
|
||||
<option value="tcp" {% if item and item.health_check_type == 'tcp' %}selected{% endif %}>tcp</option>
|
||||
<option value="http" {% if item and item.health_check_type == 'http' %}selected{% endif %}>http</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>健康检查 URL (http 类型) <input name="health_check_url" value="{{ item.health_check_url or '' }}" placeholder="/health"></label>
|
||||
<label>健康检查间隔(秒) <input name="health_check_interval_s" type="number" min="5" value="{{ item.health_check_interval_s if item else 30 }}"></label>
|
||||
<label>健康检查超时(秒) <input name="health_check_timeout_s" type="number" min="1" value="{{ item.health_check_timeout_s if item else 3 }}"></label>
|
||||
</fieldset>
|
||||
|
||||
<label>备注 <textarea name="note" rows="2">{{ item.note or '' if item else '' }}</textarea></label>
|
||||
<label class="checkbox"><input type="checkbox" name="is_enabled" {% if item is none or item.is_enabled %}checked{% endif %}> 启用</label>
|
||||
|
||||
<p class="muted small">
|
||||
保存后,frpc 主机的 <code>frpc.ini</code> 需要重新拉取(点客户端列表的 <strong>frpc.ini</strong> 按钮),
|
||||
然后在 frpc 主机执行 <code>frpc reload -c /etc/frpc.ini</code> 让新代理生效。
|
||||
</p>
|
||||
|
||||
<div class="form-actions">
|
||||
<button class="btn btn-primary" type="submit">保存</button>
|
||||
<a class="btn" href="{{ url_for('proxies_list') }}">取消</a>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,24 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ '编辑' if item else '新建' }}租户 · frps-manager{% endblock %}
|
||||
{% block content %}
|
||||
<h1>{{ '编辑租户' if item else '新建租户' }}</h1>
|
||||
<form method="post" class="form-card">
|
||||
{% if item %}
|
||||
<label>名称 <input value="{{ item.name }}" disabled></label>
|
||||
{% else %}
|
||||
<label>名称 <input name="name" required pattern="[a-zA-Z0-9_-]+" title="字母数字下划线短横线"></label>
|
||||
{% endif %}
|
||||
<label>描述 <input name="description" value="{{ item.description or '' }}"></label>
|
||||
<label>入站带宽 (Mbps, 0=不限) <input name="bandwidth_in_mbps" type="number" min="0" value="{{ item.bandwidth_in_mbps if item else 0 }}"></label>
|
||||
<label>出站带宽 (Mbps, 0=不限) <input name="bandwidth_out_mbps" type="number" min="0" value="{{ item.bandwidth_out_mbps if item else 0 }}"></label>
|
||||
<label>最大并发连接 (0=不限) <input name="max_connections" type="number" min="0" value="{{ item.max_connections if item else 0 }}"></label>
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" name="is_active" {% if item is none or item.is_active %}checked{% endif %}>
|
||||
启用
|
||||
</label>
|
||||
<div class="form-actions">
|
||||
<button class="btn btn-primary" type="submit">保存</button>
|
||||
<a class="btn" href="{{ url_for('tenants_list') }}">取消</a>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,34 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}租户 · frps-manager{% endblock %}
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1>租户</h1>
|
||||
<a class="btn btn-primary" href="{{ url_for('tenants_new') }}">+ 新建租户</a>
|
||||
</div>
|
||||
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr><th>名称</th><th>带宽 in/out</th><th>最大连接</th><th>客户端数</th><th>状态</th><th>操作</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for t in items %}
|
||||
<tr>
|
||||
<td><strong>{{ t.name }}</strong><br><span class="muted small">{{ t.description or '' }}</span></td>
|
||||
<td>{{ t.bandwidth_in_mbps|fmt_mbps }} / {{ t.bandwidth_out_mbps|fmt_mbps }}</td>
|
||||
<td>{{ t.max_connections or 'unlimited' }}</td>
|
||||
<td>{{ t.clients|length }}</td>
|
||||
<td>{% if t.is_active %}<span class="badge badge-ok">启用</span>{% else %}<span class="badge badge-off">禁用</span>{% endif %}</td>
|
||||
<td class="actions">
|
||||
<a href="{{ url_for('tenants_edit', tid=t.id) }}">编辑</a>
|
||||
<form method="post" action="{{ url_for('tenants_delete', tid=t.id) }}" style="display:inline"
|
||||
onsubmit="return confirm('确定删除租户 {{ t.name }} 吗?');">
|
||||
<button class="link-btn link-danger">删除</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="6" class="muted">暂无租户</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,36 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ '编辑' if item else '新建' }}用户 · frps-manager{% endblock %}
|
||||
{% block content %}
|
||||
<h1>{{ '编辑用户' if item else '新建用户' }}</h1>
|
||||
<form method="post" class="form-card">
|
||||
{% if item %}
|
||||
<label>用户名 <input value="{{ item.username }}" disabled></label>
|
||||
{% else %}
|
||||
<label>用户名 <input name="username" required pattern="[a-zA-Z0-9_.-]+"></label>
|
||||
{% endif %}
|
||||
<label>显示名 <input name="display_name" value="{{ item.display_name or '' if item else '' }}"></label>
|
||||
<label>密码 {% if item %}<span class="muted small">(留空表示不修改)</span>{% endif %}
|
||||
<input name="password" type="password" {% if not item %}required{% endif %} minlength="6">
|
||||
</label>
|
||||
<label>角色
|
||||
<select name="role">
|
||||
<option value="user" {% if item and item.role == 'user' %}selected{% endif %}>user (普通用户,只能看自己的租户)</option>
|
||||
<option value="tenant_admin" {% if item and item.role == 'tenant_admin' %}selected{% endif %}>tenant_admin (租户管理员)</option>
|
||||
<option value="admin" {% if item and item.role == 'admin' %}selected{% endif %}>admin (系统管理员)</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>归属租户
|
||||
<select name="tenant_id">
|
||||
<option value="">(不绑定)</option>
|
||||
{% for t in tenants %}
|
||||
<option value="{{ t.id }}" {% if item and item.tenant_id == t.id %}selected{% endif %}>{{ t.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
<label class="checkbox"><input type="checkbox" name="is_active" {% if item is none or item.is_active %}checked{% endif %}> 启用</label>
|
||||
<div class="form-actions">
|
||||
<button class="btn btn-primary" type="submit">保存</button>
|
||||
<a class="btn" href="{{ url_for('users_list') }}">取消</a>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,31 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}用户 · frps-manager{% endblock %}
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1>用户管理</h1>
|
||||
<a class="btn btn-primary" href="{{ url_for('users_new') }}">+ 新建用户</a>
|
||||
</div>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr><th>用户名</th><th>角色</th><th>归属租户</th><th>状态</th><th>最近登录</th><th>操作</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for u in items %}
|
||||
<tr>
|
||||
<td><strong>{{ u.username }}</strong><br><span class="muted small">{{ u.display_name or '' }}</span></td>
|
||||
<td><span class="role-tag role-{{ u.role }}">{{ u.role }}</span></td>
|
||||
<td>{{ u.tenant.name if u.tenant else '-' }}</td>
|
||||
<td>{% if u.is_active %}<span class="badge badge-ok">启用</span>{% else %}<span class="badge badge-off">禁用</span>{% endif %}</td>
|
||||
<td>{{ u.last_login_at|fmt_dt }}</td>
|
||||
<td class="actions">
|
||||
<a href="{{ url_for('users_edit', uid=u.id) }}">编辑</a>
|
||||
<form method="post" action="{{ url_for('users_delete', uid=u.id) }}" style="display:inline"
|
||||
onsubmit="return confirm('删除用户 {{ u.username }} 吗?');">
|
||||
<button class="link-btn link-danger">删除</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user