新增客户端管理功能,包括创建、编辑和删除客户端的API,更新用户管理功能,添加用户创建和编辑页面,优化管理员功能,增强用户和客户端的管理体验。

This commit is contained in:
2025-04-17 02:29:16 +08:00
parent d06e45e5d4
commit ff47bb2f6f
11 changed files with 823 additions and 77 deletions

View File

@@ -1,38 +1,281 @@
{{template "header" .}}
<div class="container mt-4">
<h2>客户端管理</h2>
<table class="table">
<thead>
<tr>
<th>ID</th>
<th>客户端ID</th>
<th>名称</th>
<th>创建时间</th>
</tr>
</thead>
<tbody>
{{range .clients}}
<tr>
<td>{{.ID}}</td>
<td>{{.ClientID}}</td>
<td>{{.Name}}</td>
<td>{{.CreatedAt.Format "2006-01-02 15:04:05"}}</td>
</tr>
{{end}}
</tbody>
</table>
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="text-dark">
<i class="bi bi-grid-3x3-gap-fill me-2"></i>客户端管理
</h2>
<button type="button" class="btn btn-primary px-4 py-2 d-flex align-items-center" data-bs-toggle="modal" data-bs-target="#newClientModal">
<i class="bi bi-plus-lg me-2"></i>新建客户端
</button>
</div>
<div class="card shadow-sm">
<div class="card-body p-0">
<table class="table table-hover mb-0">
<thead class="bg-light">
<tr>
<th class="px-4" style="width: 20%">客户端ID</th>
<th class="px-4" style="width: 20%">客户端密钥</th>
<th class="px-4" style="width: 10%">名称</th>
<th class="px-4" style="width: 30%">重定向URI</th>
<th class="px-4" style="width: 12%">创建时间</th>
<th class="px-4 text-end" style="width: 8%">操作</th>
</tr>
</thead>
<tbody>
{{range .clients}}
<tr>
<td class="px-4 align-middle text-truncate" title="{{.ClientID}}">{{.ClientID}}</td>
<td class="px-4 align-middle text-truncate" title="{{.ClientSecret}}">{{.ClientSecret}}</td>
<td class="px-4 align-middle text-truncate" title="{{.ClientName}}">{{.ClientName}}</td>
<td class="px-4 align-middle">
<div class="text-truncate" title="{{range $i, $uri := .RedirectURIs}}{{if $i}}, {{end}}{{$uri}}{{end}}">
{{range $i, $uri := .RedirectURIs}}{{if $i}}, {{end}}{{$uri}}{{end}}
</div>
</td>
<td class="px-4 align-middle">{{.ClientIDIssuedAt.Format "2006-01-02 15:04:05"}}</td>
<td class="px-4 align-middle text-end">
{{$redirectURIsStr := ""}}
{{range $i, $uri := .RedirectURIs}}
{{if $i}}
{{$redirectURIsStr = printf "%s,%s" $redirectURIsStr $uri}}
{{else}}
{{$redirectURIsStr = $uri}}
{{end}}
{{end}}
<div class="btn-group">
<button type="button" class="btn btn-outline-primary btn-sm" data-bs-toggle="modal" data-bs-target="#editClientModal"
data-client-id="{{.ClientID}}"
data-client-name="{{.ClientName}}"
data-redirect-uris="{{$redirectURIsStr}}">
<i class="bi bi-pencil-square"></i>
</button>
<button type="button" class="btn btn-outline-danger btn-sm" onclick="deleteClient('{{.ClientID}}')">
<i class="bi bi-trash"></i>
</button>
</div>
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
</div>
<nav>
<ul class="pagination">
<nav class="mt-4">
<ul class="pagination justify-content-center">
{{if gt .page 1}}
<li class="page-item">
<a class="page-link" href="?page={{subtract .page 1}}&page_size={{.pageSize}}">上一页</a>
<a class="page-link" href="?page={{subtract .page 1}}&page_size={{.pageSize}}">
<i class="bi bi-chevron-left"></i>
</a>
</li>
{{end}}
<li class="page-item">
<a class="page-link" href="?page={{add .page 1}}&page_size={{.pageSize}}">下一页</a>
<a class="page-link" href="?page={{add .page 1}}&page_size={{.pageSize}}">
<i class="bi bi-chevron-right"></i>
</a>
</li>
</ul>
</nav>
</div>
<!-- 新建客户端模态框 -->
<div class="modal fade" id="newClientModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header border-bottom-0">
<h5 class="modal-title">
<i class="bi bi-plus-circle me-2"></i>新建客户端
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body px-4">
<form id="newClientForm">
<div class="mb-4">
<label for="newClientName" class="form-label">名称</label>
<input type="text" class="form-control" id="newClientName" required>
</div>
<div class="mb-4">
<label for="newRedirectURIs" class="form-label">重定向URI</label>
<textarea class="form-control" id="newRedirectURIs" rows="3" required></textarea>
<div class="form-text text-muted">每行一个URI或用逗号分隔多个URI</div>
</div>
</form>
</div>
<div class="modal-footer border-top-0">
<button type="button" class="btn btn-light" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary px-4" onclick="createClient()">保存</button>
</div>
</div>
</div>
</div>
<!-- 编辑客户端模态框 -->
<div class="modal fade" id="editClientModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header border-bottom-0">
<h5 class="modal-title">
<i class="bi bi-pencil-square me-2"></i>编辑客户端
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body px-4">
<form id="editClientForm">
<input type="hidden" id="editClientID">
<div class="mb-4">
<label for="editClientName" class="form-label">名称</label>
<input type="text" class="form-control" id="editClientName" required>
</div>
<div class="mb-4">
<label for="editRedirectURIs" class="form-label">重定向URI</label>
<textarea class="form-control" id="editRedirectURIs" rows="3" required></textarea>
<div class="form-text text-muted">每行一个URI或用逗号分隔多个URI</div>
</div>
</form>
</div>
<div class="modal-footer border-top-0">
<button type="button" class="btn btn-light" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary px-4" onclick="updateClient()">保存</button>
</div>
</div>
</div>
</div>
<style>
.table th {
font-weight: 600;
border-bottom-width: 1px;
}
.table td {
vertical-align: middle;
white-space: nowrap;
max-width: 0;
}
.text-truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 100%;
}
.btn-group .btn {
padding: 0.375rem 0.75rem;
}
.modal-dialog {
max-width: 500px;
}
.form-control:focus {
border-color: #80bdff;
box-shadow: 0 0 0 0.2rem rgba(0,123,255,.25);
}
.card {
border-radius: 0.5rem;
border: none;
}
.page-link {
padding: 0.5rem 0.75rem;
border-radius: 0.25rem;
margin: 0 0.25rem;
}
.page-link:hover {
background-color: #e9ecef;
}
.btn-group .btn i {
font-size: 14px;
}
</style>
<script>
// 创建新客户端
function createClient() {
const clientName = document.getElementById('newClientName').value;
const redirectURIs = document.getElementById('newRedirectURIs').value
.split(/[\n,]/)
.map(uri => uri.trim())
.filter(uri => uri.length > 0);
fetch('/api/clients', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
client_name: clientName,
redirect_uris: redirectURIs,
token_endpoint_auth_method: 'client_secret_basic',
grant_types: ['authorization_code'],
response_types: ['code']
})
})
.then(response => {
if (response.ok) {
location.reload();
} else {
alert('创建失败,请重试');
}
});
}
// 编辑客户端模态框数据填充
document.getElementById('editClientModal').addEventListener('show.bs.modal', function (event) {
const button = event.relatedTarget;
const clientId = button.getAttribute('data-client-id');
const clientName = button.getAttribute('data-client-name');
const redirectURIs = button.getAttribute('data-redirect-uris');
document.getElementById('editClientID').value = clientId;
document.getElementById('editClientName').value = clientName;
document.getElementById('editRedirectURIs').value = redirectURIs.split(',').join('\n');
});
// 更新客户端
function updateClient() {
const clientID = document.getElementById('editClientID').value;
const clientName = document.getElementById('editClientName').value;
const redirectURIs = document.getElementById('editRedirectURIs').value
.split(/[\n,]/)
.map(uri => uri.trim())
.filter(uri => uri.length > 0);
fetch(`/api/clients/${clientID}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
client_name: clientName,
redirect_uris: redirectURIs,
token_endpoint_auth_method: 'client_secret_basic',
grant_types: ['authorization_code'],
response_types: ['code']
})
})
.then(response => {
if (response.ok) {
location.reload();
} else {
alert('更新失败,请重试');
}
});
}
// 删除客户端
function deleteClient(clientID) {
if (!confirm('确定要删除这个客户端吗?')) {
return;
}
fetch(`/api/clients/${clientID}`, {
method: 'DELETE'
})
.then(response => {
if (response.ok) {
location.reload();
} else {
alert('删除失败,请重试');
}
});
}
</script>
{{template "footer" .}}

View File

@@ -0,0 +1,32 @@
<!DOCTYPE html>
<html>
<head>
<title>创建用户 - OIDC OAuth2 管理系统</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css">
</head>
<body>
<div class="container mt-5">
<h2>创建新用户</h2>
{{if .error}}
<div class="alert alert-danger">{{.error}}</div>
{{end}}
<form method="POST" action="/admin/users/create">
<div class="mb-3">
<label for="username" class="form-label">用户名</label>
<input type="text" class="form-control" id="username" name="username" required>
</div>
<div class="mb-3">
<label for="password" class="form-label">密码</label>
<input type="password" class="form-control" id="password" name="password" required>
</div>
<div class="mb-3">
<label for="email" class="form-label">邮箱</label>
<input type="email" class="form-control" id="email" name="email" required>
</div>
<button type="submit" class="btn btn-primary">创建用户</button>
<a href="/admin/users" class="btn btn-secondary">返回</a>
</form>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

View File

@@ -0,0 +1,36 @@
<!DOCTYPE html>
<html>
<head>
<title>编辑用户 - OIDC OAuth2 管理系统</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css">
</head>
<body>
<div class="container mt-5">
<h2>编辑用户</h2>
{{if .error}}
<div class="alert alert-danger">{{.error}}</div>
{{end}}
<form method="POST" action="/admin/users/{{.user.ID}}/edit">
<div class="mb-3">
<label for="username" class="form-label">用户名</label>
<input type="text" class="form-control" id="username" name="username" value="{{.user.Username}}" required>
</div>
<div class="mb-3">
<label for="password" class="form-label">新密码(留空表示不修改)</label>
<input type="password" class="form-control" id="password" name="password">
</div>
<div class="mb-3">
<label for="email" class="form-label">邮箱</label>
<input type="email" class="form-control" id="email" name="email" value="{{.user.Email}}" required>
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="is_active" name="is_active" value="true" {{if .user.IsActive}}checked{{end}}>
<label class="form-check-label" for="is_active">账户激活</label>
</div>
<button type="submit" class="btn btn-primary">保存修改</button>
<a href="/admin/users" class="btn btn-secondary">返回</a>
</form>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

View File

@@ -1,38 +1,110 @@
{{template "header" .}}
<div class="container mt-4">
<h2>用户管理</h2>
<table class="table">
<thead>
<tr>
<th>ID</th>
<th>用户名</th>
<th>邮箱</th>
<th>创建时间</th>
</tr>
</thead>
<tbody>
{{range .users}}
<tr>
<td>{{.ID}}</td>
<td>{{.Username}}</td>
<td>{{.Email}}</td>
<td>{{.CreatedAt.Format "2006-01-02 15:04:05"}}</td>
</tr>
{{end}}
</tbody>
</table>
<nav>
<ul class="pagination">
{{if gt .page 1}}
<li class="page-item">
<a class="page-link" href="?page={{subtract .page 1}}&page_size={{.pageSize}}">上一页</a>
</li>
{{end}}
<li class="page-item">
<a class="page-link" href="?page={{add .page 1}}&page_size={{.pageSize}}">下一页</a>
</li>
</ul>
<!DOCTYPE html>
<html>
<head>
<title>用户管理 - OIDC OAuth2 管理系统</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css">
</head>
<body>
<!-- 导航栏 -->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark mb-4">
<div class="container">
<a class="navbar-brand" href="/">OIDC OAuth2 管理系统</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link {{if eq .active "users"}}active{{end}}" href="/admin/users">用户管理</a>
</li>
<li class="nav-item">
<a class="nav-link {{if eq .active "clients"}}active{{end}}" href="/admin/clients">客户端管理</a>
</li>
</ul>
</div>
</div>
</nav>
</div>
{{template "footer" .}}
<div class="container mt-5">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2>用户管理</h2>
<a href="/admin/users/create" class="btn btn-primary">创建新用户</a>
</div>
{{if .error}}
<div class="alert alert-danger">{{.error}}</div>
{{end}}
<table class="table">
<thead>
<tr>
<th>ID</th>
<th>用户名</th>
<th>邮箱</th>
<th>状态</th>
<th>创建时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{{range .users}}
<tr>
<td>{{.ID}}</td>
<td>{{.Username}}</td>
<td>{{.Email}}</td>
<td>
{{if .IsActive}}
<span class="badge bg-success">激活</span>
{{else}}
<span class="badge bg-danger">禁用</span>
{{end}}
</td>
<td>{{.CreatedAt.Format "2006-01-02 15:04:05"}}</td>
<td>
<a href="/admin/users/{{.ID}}/edit" class="btn btn-sm btn-primary">编辑</a>
<button class="btn btn-sm btn-danger" data-user-id="{{.ID}}" onclick="deleteUser(this.dataset.userId)">删除</button>
</td>
</tr>
{{end}}
</tbody>
</table>
<!-- 分页 -->
<nav>
<ul class="pagination">
{{if gt .page 1}}
<li class="page-item">
<a class="page-link" href="/admin/users?page={{subtract .page 1}}&page_size={{.pageSize}}">上一页</a>
</li>
{{end}}
<li class="page-item active">
<span class="page-link">{{.page}}</span>
</li>
{{if lt (multiply .page .pageSize) .total}}
<li class="page-item">
<a class="page-link" href="/admin/users?page={{add .page 1}}&page_size={{.pageSize}}">下一页</a>
</li>
{{end}}
</ul>
</nav>
</div>
<script>
function deleteUser(id) {
if (confirm('确定要删除这个用户吗?')) {
fetch(`/admin/users/${id}/delete`, {
method: 'POST',
credentials: 'same-origin'
}).then(response => {
if (response.ok) {
window.location.reload();
} else {
alert('删除失败');
}
});
}
}
</script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

View File

@@ -4,6 +4,7 @@
<head>
<title>OIDC OAuth2 管理系统</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css" rel="stylesheet">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">