新增客户端管理功能,包括创建、编辑和删除客户端的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,6 +1,7 @@
package handlers package handlers
import ( import (
"math"
"net/http" "net/http"
"strconv" "strconv"
@@ -12,10 +13,11 @@ import (
type AdminHandler struct { type AdminHandler struct {
adminService *services.AdminService adminService *services.AdminService
clientService *services.ClientService
} }
func NewAdminHandler(adminService *services.AdminService) *AdminHandler { func NewAdminHandler(adminService *services.AdminService, clientService *services.ClientService) *AdminHandler {
return &AdminHandler{adminService: adminService} return &AdminHandler{adminService: adminService, clientService: clientService}
} }
func (h *AdminHandler) ShowAdminLogin(c *gin.Context) { func (h *AdminHandler) ShowAdminLogin(c *gin.Context) {
@@ -63,20 +65,153 @@ func (h *AdminHandler) ListUsers(c *gin.Context) {
}) })
} }
// ListClients 显示客户端列表页面
func (h *AdminHandler) ListClients(c *gin.Context) { func (h *AdminHandler) ListClients(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) page := 1
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10")) pageSize := 10
clients, total, err := h.adminService.ListClients(page, pageSize) // 从查询参数获取分页信息
if pageStr := c.Query("page"); pageStr != "" {
if p, err := strconv.Atoi(pageStr); err == nil && p > 0 {
page = p
}
}
if pageSizeStr := c.Query("page_size"); pageSizeStr != "" {
if ps, err := strconv.Atoi(pageSizeStr); err == nil && ps > 0 {
pageSize = ps
}
}
// 获取客户端列表
clients, total, err := h.clientService.GetClients(page, pageSize)
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) c.HTML(http.StatusInternalServerError, "error.html", gin.H{
"error": err.Error(),
})
return return
} }
c.HTML(http.StatusOK, "admin_clients.html", gin.H{ c.HTML(http.StatusOK, "admin_clients.html", gin.H{
"clients": clients, "clients": clients,
"total": total,
"page": page, "page": page,
"pageSize": pageSize, "pageSize": pageSize,
"total": total,
"lastPage": int(math.Ceil(float64(total) / float64(pageSize))),
}) })
} }
// ShowCreateUser 显示创建用户页面
func (h *AdminHandler) ShowCreateUser(c *gin.Context) {
c.HTML(http.StatusOK, "admin_create_user.html", gin.H{
"title": "创建用户",
})
}
// HandleCreateUser 处理创建用户请求
func (h *AdminHandler) HandleCreateUser(c *gin.Context) {
username := c.PostForm("username")
password := c.PostForm("password")
email := c.PostForm("email")
if username == "" || password == "" || email == "" {
c.HTML(http.StatusBadRequest, "admin_create_user.html", gin.H{
"title": "创建用户",
"error": "用户名、密码和邮箱都不能为空",
})
return
}
_, err := h.adminService.CreateUser(username, password, email)
if err != nil {
c.HTML(http.StatusBadRequest, "admin_create_user.html", gin.H{
"title": "创建用户",
"error": "创建用户失败:" + err.Error(),
})
return
}
c.Redirect(http.StatusFound, "/admin/users")
}
// ShowEditUser 显示编辑用户页面
func (h *AdminHandler) ShowEditUser(c *gin.Context) {
userID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.HTML(http.StatusBadRequest, "error.html", gin.H{
"error": "无效的用户ID",
})
return
}
user, err := h.adminService.GetUser(uint(userID))
if err != nil {
c.HTML(http.StatusNotFound, "error.html", gin.H{
"error": "用户不存在",
})
return
}
c.HTML(http.StatusOK, "admin_edit_user.html", gin.H{
"title": "编辑用户",
"user": user,
})
}
// HandleEditUser 处理编辑用户请求
func (h *AdminHandler) HandleEditUser(c *gin.Context) {
userID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的用户ID"})
return
}
username := c.PostForm("username")
email := c.PostForm("email")
isActiveStr := c.PostForm("is_active")
var isActive *bool
if isActiveStr != "" {
active := isActiveStr == "true"
isActive = &active
}
_, err = h.adminService.UpdateUser(uint(userID), username, email, isActive)
if err != nil {
c.HTML(http.StatusBadRequest, "admin_edit_user.html", gin.H{
"title": "编辑用户",
"error": "更新用户失败:" + err.Error(),
})
return
}
// 如果提供了新密码,则更新密码
newPassword := c.PostForm("password")
if newPassword != "" {
err = h.adminService.UpdateUserPassword(uint(userID), newPassword)
if err != nil {
c.HTML(http.StatusBadRequest, "admin_edit_user.html", gin.H{
"title": "编辑用户",
"error": "更新密码失败:" + err.Error(),
})
return
}
}
c.Redirect(http.StatusFound, "/admin/users")
}
// HandleDeleteUser 处理删除用户请求
func (h *AdminHandler) HandleDeleteUser(c *gin.Context) {
userID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的用户ID"})
return
}
err = h.adminService.DeleteUser(uint(userID))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "删除用户失败:" + err.Error()})
return
}
c.Redirect(http.StatusFound, "/admin/users")
}

View File

@@ -0,0 +1,75 @@
package handlers
import (
"net/http"
"oidc-oauth2-server/services"
"github.com/gin-gonic/gin"
)
type ClientHandler struct {
clientService *services.ClientService
}
func NewClientHandler(clientService *services.ClientService) *ClientHandler {
return &ClientHandler{clientService: clientService}
}
// RegisterRoutes 注册路由
func (h *ClientHandler) RegisterRoutes(router *gin.Engine) {
api := router.Group("/api")
{
api.POST("/clients", h.CreateClient)
api.PUT("/clients/:id", h.UpdateClient)
api.DELETE("/clients/:id", h.DeleteClient)
}
}
// CreateClient 创建客户端
func (h *ClientHandler) CreateClient(c *gin.Context) {
var req services.ClientRegistrationRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
client, err := h.clientService.RegisterClient(&req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, client)
}
// UpdateClient 更新客户端
func (h *ClientHandler) UpdateClient(c *gin.Context) {
clientID := c.Param("id")
var req services.ClientRegistrationRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
client, err := h.clientService.UpdateClient(clientID, &req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, client)
}
// DeleteClient 删除客户端
func (h *ClientHandler) DeleteClient(c *gin.Context) {
clientID := c.Param("id")
if err := h.clientService.DeleteClient(clientID); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.Status(http.StatusOK)
}

16
main.go
View File

@@ -56,6 +56,9 @@ func main() {
"add": func(a, b int) int { "add": func(a, b int) int {
return a + b return a + b
}, },
"multiply": func(a, b int) int {
return a * b
},
}) })
r.LoadHTMLGlob("templates/*") r.LoadHTMLGlob("templates/*")
@@ -75,7 +78,8 @@ func main() {
oidcHandler := handlers.NewOIDCHandler(config.GlobalConfig.OAuth.IssuerURL, oauthService, authService) oidcHandler := handlers.NewOIDCHandler(config.GlobalConfig.OAuth.IssuerURL, oauthService, authService)
registrationHandler := handlers.NewRegistrationHandler(clientService) registrationHandler := handlers.NewRegistrationHandler(clientService)
tokenHandler := handlers.NewTokenHandler(tokenService) tokenHandler := handlers.NewTokenHandler(tokenService)
adminHandler := handlers.NewAdminHandler(adminService) adminHandler := handlers.NewAdminHandler(adminService, clientService)
clientHandler := handlers.NewClientHandler(clientService)
// 认证路由 // 认证路由
r.GET("/login", authHandler.ShowLogin) r.GET("/login", authHandler.ShowLogin)
@@ -113,6 +117,16 @@ func main() {
authorized.GET("/dashboard", adminHandler.Dashboard) authorized.GET("/dashboard", adminHandler.Dashboard)
authorized.GET("/users", adminHandler.ListUsers) authorized.GET("/users", adminHandler.ListUsers)
authorized.GET("/clients", adminHandler.ListClients) authorized.GET("/clients", adminHandler.ListClients)
// 用户管理路由
authorized.GET("/users/create", adminHandler.ShowCreateUser)
authorized.POST("/users/create", adminHandler.HandleCreateUser)
authorized.GET("/users/:id/edit", adminHandler.ShowEditUser)
authorized.POST("/users/:id/edit", adminHandler.HandleEditUser)
authorized.POST("/users/:id/delete", adminHandler.HandleDeleteUser)
// 客户端管理API路由
clientHandler.RegisterRoutes(r)
} }
} }

View File

@@ -4,13 +4,16 @@ import (
"time" "time"
"gorm.io/datatypes" "gorm.io/datatypes"
"gorm.io/gorm"
) )
// Client 表示 OAuth2 客户端
type Client struct { type Client struct {
gorm.Model ID uint `json:"id"`
ClientID string `gorm:"uniqueIndex;not null"` ClientID string `json:"client_id" gorm:"unique"`
ClientSecret string `gorm:"not null"` ClientSecret string `json:"client_secret,omitempty"`
Name string `json:"name"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
RedirectURIs datatypes.JSON `gorm:"type:json"` RedirectURIs datatypes.JSON `gorm:"type:json"`
TokenEndpointAuthMethod string `gorm:"not null"` TokenEndpointAuthMethod string `gorm:"not null"`
GrantTypes datatypes.JSON `gorm:"type:json"` GrantTypes datatypes.JSON `gorm:"type:json"`
@@ -25,8 +28,19 @@ type Client struct {
SoftwareID string `gorm:"type:varchar(255)"` SoftwareID string `gorm:"type:varchar(255)"`
SoftwareVersion string `gorm:"type:varchar(255)"` SoftwareVersion string `gorm:"type:varchar(255)"`
IsActive bool `gorm:"default:true"` IsActive bool `gorm:"default:true"`
CreatedAt time.Time }
UpdatedAt time.Time
// CreateClientRequest 创建客户端的请求结构
type CreateClientRequest struct {
ClientID string `json:"client_id" binding:"required"`
ClientSecret string `json:"client_secret" binding:"required"`
Name string `json:"name" binding:"required"`
}
// UpdateClientRequest 更新客户端的请求结构
type UpdateClientRequest struct {
ClientID string `json:"client_id" binding:"required"`
Name string `json:"name" binding:"required"`
} }
func (c *Client) TableName() string { func (c *Client) TableName() string {

View File

@@ -177,3 +177,72 @@ func (s *AdminService) ListClients(page, pageSize int) ([]models.Client, int64,
err := s.db.Offset((page - 1) * pageSize).Limit(pageSize).Find(&clients).Error err := s.db.Offset((page - 1) * pageSize).Limit(pageSize).Find(&clients).Error
return clients, total, err return clients, total, err
} }
// CreateUser 创建新用户
func (s *AdminService) CreateUser(username, password, email string) (*models.User, error) {
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return nil, err
}
user := &models.User{
Username: username,
Password: string(hashedPassword),
Email: email,
IsActive: true,
}
if err := s.db.Create(user).Error; err != nil {
return nil, err
}
return user, nil
}
// UpdateUser 更新用户信息
func (s *AdminService) UpdateUser(id uint, username, email string, isActive *bool) (*models.User, error) {
user := &models.User{}
if err := s.db.First(user, id).Error; err != nil {
return nil, err
}
if username != "" {
user.Username = username
}
if email != "" {
user.Email = email
}
if isActive != nil {
user.IsActive = *isActive
}
if err := s.db.Save(user).Error; err != nil {
return nil, err
}
return user, nil
}
// UpdateUserPassword 更新用户密码
func (s *AdminService) UpdateUserPassword(id uint, password string) error {
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return err
}
return s.db.Model(&models.User{}).Where("id = ?", id).Update("password", string(hashedPassword)).Error
}
// DeleteUser 删除用户
func (s *AdminService) DeleteUser(id uint) error {
return s.db.Delete(&models.User{}, id).Error
}
// GetUser 获取单个用户信息
func (s *AdminService) GetUser(id uint) (*models.User, error) {
user := &models.User{}
if err := s.db.First(user, id).Error; err != nil {
return nil, err
}
return user, nil
}

View File

@@ -209,3 +209,58 @@ func generateSecureToken(length int) string {
} }
return base64.RawURLEncoding.EncodeToString(b) return base64.RawURLEncoding.EncodeToString(b)
} }
// GetClients 获取客户端列表(分页)
func (s *ClientService) GetClients(page, pageSize int) ([]ClientResponse, int64, error) {
var clients []models.Client
var total int64
// 获取总数
if err := s.db.Model(&models.Client{}).Count(&total).Error; err != nil {
return nil, 0, err
}
// 获取分页数据
offset := (page - 1) * pageSize
if err := s.db.Offset(offset).Limit(pageSize).Find(&clients).Error; err != nil {
return nil, 0, err
}
// 转换为响应格式
responses := make([]ClientResponse, len(clients))
for i, client := range clients {
// 解析 JSON 字段
var redirectURIs, grantTypes, responseTypes, scopes, contacts []string
json.Unmarshal(client.RedirectURIs, &redirectURIs)
json.Unmarshal(client.GrantTypes, &grantTypes)
json.Unmarshal(client.ResponseTypes, &responseTypes)
json.Unmarshal(client.Scopes, &scopes)
json.Unmarshal(client.Contacts, &contacts)
responses[i] = ClientResponse{
ClientID: client.ClientID,
ClientSecret: client.ClientSecret,
ClientIDIssuedAt: client.CreatedAt,
RedirectURIs: redirectURIs,
TokenEndpointAuthMethod: client.TokenEndpointAuthMethod,
GrantTypes: grantTypes,
ResponseTypes: responseTypes,
ClientName: client.ClientName,
ClientURI: client.ClientURI,
LogoURI: client.LogoURI,
Scope: func() string {
if len(scopes) > 0 {
return scopes[0]
}
return ""
}(),
Contacts: contacts,
TosURI: client.TosURI,
PolicyURI: client.PolicyURI,
SoftwareID: client.SoftwareID,
SoftwareVersion: client.SoftwareVersion,
}
}
return responses, total, nil
}

View File

@@ -1,38 +1,281 @@
{{template "header" .}} {{template "header" .}}
<div class="container mt-4"> <div class="container mt-4">
<h2>客户端管理</h2> <div class="d-flex justify-content-between align-items-center mb-4">
<table class="table"> <h2 class="text-dark">
<thead> <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> <tr>
<th>ID</th> <th class="px-4" style="width: 20%">客户端ID</th>
<th>客户端ID</th> <th class="px-4" style="width: 20%">客户端密钥</th>
<th>名称</th> <th class="px-4" style="width: 10%">名称</th>
<th>创建时间</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> </tr>
</thead> </thead>
<tbody> <tbody>
{{range .clients}} {{range .clients}}
<tr> <tr>
<td>{{.ID}}</td> <td class="px-4 align-middle text-truncate" title="{{.ClientID}}">{{.ClientID}}</td>
<td>{{.ClientID}}</td> <td class="px-4 align-middle text-truncate" title="{{.ClientSecret}}">{{.ClientSecret}}</td>
<td>{{.Name}}</td> <td class="px-4 align-middle text-truncate" title="{{.ClientName}}">{{.ClientName}}</td>
<td>{{.CreatedAt.Format "2006-01-02 15:04:05"}}</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> </tr>
{{end}} {{end}}
</tbody> </tbody>
</table> </table>
</div>
</div>
<nav> <nav class="mt-4">
<ul class="pagination"> <ul class="pagination justify-content-center">
{{if gt .page 1}} {{if gt .page 1}}
<li class="page-item"> <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> </li>
{{end}} {{end}}
<li class="page-item"> <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> </li>
</ul> </ul>
</nav> </nav>
</div> </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" .}} {{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,13 +1,49 @@
{{template "header" .}} <!DOCTYPE html>
<div class="container mt-4"> <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 class="container mt-5">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2>用户管理</h2> <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"> <table class="table">
<thead> <thead>
<tr> <tr>
<th>ID</th> <th>ID</th>
<th>用户名</th> <th>用户名</th>
<th>邮箱</th> <th>邮箱</th>
<th>状态</th>
<th>创建时间</th> <th>创建时间</th>
<th>操作</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -16,23 +52,59 @@
<td>{{.ID}}</td> <td>{{.ID}}</td>
<td>{{.Username}}</td> <td>{{.Username}}</td>
<td>{{.Email}}</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>{{.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> </tr>
{{end}} {{end}}
</tbody> </tbody>
</table> </table>
<!-- 分页 -->
<nav> <nav>
<ul class="pagination"> <ul class="pagination">
{{if gt .page 1}} {{if gt .page 1}}
<li class="page-item"> <li class="page-item">
<a class="page-link" href="?page={{subtract .page 1}}&page_size={{.pageSize}}">上一页</a> <a class="page-link" href="/admin/users?page={{subtract .page 1}}&page_size={{.pageSize}}">上一页</a>
</li> </li>
{{end}} {{end}}
<li class="page-item"> <li class="page-item active">
<a class="page-link" href="?page={{add .page 1}}&page_size={{.pageSize}}">下一页</a> <span class="page-link">{{.page}}</span>
</li> </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> </ul>
</nav> </nav>
</div> </div>
{{template "footer" .}}
<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> <head>
<title>OIDC OAuth2 管理系统</title> <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@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> </head>
<body> <body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark"> <nav class="navbar navbar-expand-lg navbar-dark bg-dark">