新增客户端管理功能,包括创建、编辑和删除客户端的API,更新用户管理功能,添加用户创建和编辑页面,优化管理员功能,增强用户和客户端的管理体验。
This commit is contained in:
@@ -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")
|
||||||
|
}
|
||||||
|
|||||||
75
handlers/client_handler.go
Normal file
75
handlers/client_handler.go
Normal 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
16
main.go
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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" .}}
|
||||||
32
templates/admin_create_user.html
Normal file
32
templates/admin_create_user.html
Normal 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>
|
||||||
36
templates/admin_edit_user.html
Normal file
36
templates/admin_edit_user.html
Normal 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>
|
||||||
@@ -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>
|
||||||
@@ -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">
|
||||||
|
|||||||
Reference in New Issue
Block a user