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:
Your Name
2026-08-10 14:17:49 +08:00
commit 71214524f4
27 changed files with 2847 additions and 0 deletions
+57
View File
@@ -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>
+13
View File
@@ -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 %}
+45
View File
@@ -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 %}
+71
View File
@@ -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 %}
+118
View File
@@ -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 %}
+9
View File
@@ -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 %}
+46
View File
@@ -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"]
# ---------------------------------------------------------------
+14
View File
@@ -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 %}
+33
View File
@@ -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 %}
+83
View File
@@ -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 }}.&lt;base&gt;</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 %}
+67
View File
@@ -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 %}
+24
View File
@@ -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 %}
+34
View File
@@ -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 %}
+36
View File
@@ -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 %}
+31
View File
@@ -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 %}