9890cfc488
- 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
204 lines
8.1 KiB
HTML
204 lines
8.1 KiB
HTML
{% 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 %}
|