Files
socks-manager/templates/admins.html
T
Your Name 9890cfc488 feat: add system admin account management with RBAC
- AdminUser model: multi-admin accounts with bcrypt hashing (admin_users table)
- Three roles: superadmin / admin / viewer with granular permission bits
- auth.py: login migrated from env-var single admin to DB-backed accounts;
  seed_initial_admin() auto-creates first superadmin from SM_ADMIN_PASSWORD
- Web UI: /system/admins page (add/edit role/toggle/reset pwd/delete)
  + change-my-password; sidebar entry; permission-guarded routes
- REST API: /api/admins CRUD with protection checks
- Protections: cannot delete/disable self; keep >=1 enabled superadmin;
  disabled accounts fail permission checks immediately
- README: document roles, permission bits, API
2026-09-09 14:33:59 +08:00

204 lines
8.1 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "base.html" %}
{% block title %}管理员账户 · SOCKS Manager{% endblock %}
{% block content %}
<div class="page-header">
<h5><i class="bi bi-person-gear"></i> 管理员账户</h5>
</div>
{% set can_write = current_admin and current_admin.has_permission('admins:write') %}
<!-- 新增管理员 -->
<div class="card mb-3">
<div class="card-header"><span><i class="bi bi-person-plus"></i> 新增管理员</span></div>
<div class="card-body">
<form method="post" action="/system/admins/add" class="row g-2 align-items-end">
<div class="col-md-3">
<label class="form-label small">用户名</label>
<input name="username" class="form-control" placeholder="如: zhangsan" required>
</div>
<div class="col-md-3">
<label class="form-label small">密码</label>
<input name="password" type="password" class="form-control" placeholder="至少 4 位" required>
</div>
<div class="col-md-3">
<label class="form-label small">角色</label>
<select name="role" class="form-select">
{% for r, label in [('superadmin','超级管理员'), ('admin','管理员'), ('viewer','只读用户')] %}
<option value="{{ r }}">{{ label }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-3">
<button type="submit" class="btn btn-primary w-100">
<i class="bi bi-plus-lg"></i> 创建</button>
</div>
</form>
</div>
</div>
<!-- 角色说明 -->
<div class="card mb-3">
<div class="card-header"><span><i class="bi bi-info-circle"></i> 角色权限说明</span></div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-4">
<div class="card" style="background:var(--bg-tertiary)">
<div class="card-body py-2">
<span class="badge badge-danger">超级管理员</span>
<span class="text-muted small">全部权限,含管理员管理</span>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card" style="background:var(--bg-tertiary)">
<div class="card-body py-2">
<span class="badge badge-info">管理员</span>
<span class="text-muted small">用户/实例/系统管理,可查看管理员</span>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card" style="background:var(--bg-tertiary)">
<div class="card-body py-2">
<span class="badge badge-secondary">只读用户</span>
<span class="text-muted small">仅查看,不可修改</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 管理员列表 -->
<div class="card">
<div class="card-header"><span><i class="bi bi-people"></i> 账户列表</span></div>
<div class="card-body p-0">
<table class="table table-dark table-sm">
<thead><tr>
<th>用户名</th><th>角色</th><th>状态</th><th>最近登录</th><th>创建时间</th>
{% if can_write %}<th>操作</th>{% endif %}
</tr></thead>
<tbody>
{% for a in admins %}
<tr>
<td>
{{ a.username }}
{% if current_admin and current_admin.id == a.id %}
<span class="badge badge-info">当前</span>
{% endif %}
</td>
<td>
{% if can_write %}
<form method="post" action="/system/admins/{{ a.id }}/role" class="d-inline">
<select name="role" class="form-select d-inline-block" style="width:auto;padding:.15rem .4rem;font-size:12px"
onchange="this.form.submit()">
{% for r, label in [('superadmin','超级管理员'), ('admin','管理员'), ('viewer','只读用户')] %}
<option value="{{ r }}" {% if a.role == r %}selected{% endif %}>{{ label }}</option>
{% endfor %}
</select>
</form>
{% else %}
<span class="badge {{ 'badge-danger' if a.role=='superadmin' else 'badge-info' if a.role=='admin' else 'badge-secondary' }}">{{ a.role_label }}</span>
{% endif %}
</td>
<td>
{% if a.enabled %}
<span class="badge badge-success">启用</span>
{% else %}
<span class="badge badge-danger">禁用</span>
{% endif %}
</td>
<td class="text-muted">
{% if a.last_login_at %}
{{ a.last_login_at.strftime('%Y-%m-%d %H:%M') }}<br>
<small>{{ a.last_login_ip or '-' }}</small>
{% else %}-{% endif %}
</td>
<td class="text-muted">{{ a.created_at.strftime('%Y-%m-%d') if a.created_at else '-' }}</td>
{% if can_write %}
<td>
{% if not (current_admin and current_admin.id == a.id) %}
<form method="post" action="/system/admins/{{ a.id }}/toggle" class="d-inline">
<button class="btn btn-sm {{ 'btn-outline-warning' if a.enabled else 'btn-outline-success' }}"
title="{{ '禁用' if a.enabled else '启用' }}">
<i class="bi {{ 'bi-pause-circle' if a.enabled else 'bi-play-circle' }}"></i></button>
</form>
<!-- 重置密码(模态框) -->
<button class="btn btn-sm btn-outline-primary" onclick="openPwdModal({{ a.id }}, '{{ a.username }}')">
<i class="bi bi-key"></i> 重置密码</button>
<form method="post" action="/system/admins/{{ a.id }}/delete" class="d-inline"
onsubmit="return confirm('确认删除管理员 {{ a.username }}')">
<button class="btn btn-sm btn-outline-danger"><i class="bi bi-trash"></i></button>
</form>
{% else %}
<span class="text-muted small"></span>
{% endif %}
</td>
{% endif %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<!-- 修改当前密码 -->
<div class="card mt-3">
<div class="card-header"><span><i class="bi bi-shield-check"></i> 修改我的密码</span></div>
<div class="card-body">
<form method="post" action="/system/change-password" class="row g-2 align-items-end">
<div class="col-md-3">
<label class="form-label small">原密码</label>
<input name="old_password" type="password" class="form-control" required>
</div>
<div class="col-md-3">
<label class="form-label small">新密码</label>
<input name="new_password" type="password" class="form-control" required>
</div>
<div class="col-md-3">
<label class="form-label small">确认新密码</label>
<input name="confirm_password" type="password" class="form-control" required>
</div>
<div class="col-md-3">
<button type="submit" class="btn btn-outline-primary w-100">
<i class="bi bi-check-lg"></i> 修改密码</button>
</div>
</form>
</div>
</div>
<!-- 重置密码模态框 -->
<div class="modal fade" id="pwdModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<form method="post" action="" id="pwdForm">
<div class="modal-header">
<h5 class="modal-title">重置密码</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p class="text-muted small" id="pwdTarget"></p>
<label class="form-label small">新密码(至少 4 位)</label>
<input name="password" type="password" class="form-control" required>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">取消</button>
<button type="submit" class="btn btn-primary">确认重置</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}
{% block extra_script %}
<script>
function openPwdModal(id, username) {
document.getElementById('pwdForm').action = '/system/admins/' + id + '/password';
document.getElementById('pwdTarget').textContent = '账户: ' + username;
new bootstrap.Modal(document.getElementById('pwdModal')).show();
}
</script>
{% endblock %}