更新 Go 版本至 1.23.0,添加管理员功能,包括管理员登录、用户和客户端管理,新增相应的模板和中间件,优化数据库模型以支持管理员管理。

This commit is contained in:
2025-04-17 01:47:10 +08:00
parent a3f3cc17cf
commit 83c82f7135
18 changed files with 686 additions and 21 deletions

12
go.mod
View File

@@ -1,12 +1,15 @@
module oidc-oauth2-server
go 1.21
go 1.23.0
toolchain go1.23.8
require (
github.com/gin-contrib/sessions v0.0.5
github.com/gin-gonic/gin v1.9.1
github.com/golang-jwt/jwt/v4 v4.5.0
gopkg.in/yaml.v3 v3.0.1
gorm.io/datatypes v1.2.5
gorm.io/driver/sqlite v1.5.7
gorm.io/gorm v1.25.11
)
@@ -19,7 +22,6 @@ require (
github.com/gorilla/securecookie v1.1.1 // indirect
github.com/gorilla/sessions v1.2.1 // indirect
github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect
gorm.io/datatypes v1.2.5 // indirect
gorm.io/driver/mysql v1.5.6 // indirect
)
@@ -45,10 +47,10 @@ require (
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect
golang.org/x/arch v0.3.0 // indirect
golang.org/x/crypto v0.22.0
golang.org/x/crypto v0.37.0
golang.org/x/net v0.21.0 // indirect
golang.org/x/sys v0.19.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/sys v0.32.0 // indirect
golang.org/x/text v0.24.0 // indirect
google.golang.org/protobuf v1.30.0 // indirect
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
)

42
go.sum
View File

@@ -32,6 +32,10 @@ github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
@@ -44,6 +48,14 @@ github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyC
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA=
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw=
github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
@@ -64,6 +76,8 @@ github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APP
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/microsoft/go-mssqldb v1.7.2 h1:CHkFJiObW7ItKTJfHo1QX7QBBD1iV+mn1eOyRP3b/PA=
github.com/microsoft/go-mssqldb v1.7.2/go.mod h1:kOvZKUdrhhFQmxLZqbwUV0rHkNkZpthMITIb2Ko1IoA=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -91,24 +105,18 @@ github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZ
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
@@ -124,10 +132,12 @@ gorm.io/datatypes v1.2.5 h1:9UogU3jkydFVW1bIVVeoYsTpLRgwDVW3rHfJG6/Ek9I=
gorm.io/datatypes v1.2.5/go.mod h1:I5FUdlKpLb5PMqeMQhm30CQ6jXP8Rj89xkTeCSAaAD4=
gorm.io/driver/mysql v1.5.6 h1:Ld4mkIickM+EliaQZQx3uOJDJHtrd70MxAUqWqlx3Y8=
gorm.io/driver/mysql v1.5.6/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
gorm.io/driver/postgres v1.5.0 h1:u2FXTy14l45qc3UeCJ7QaAXZmZfDDv0YrthvmRq1l0U=
gorm.io/driver/postgres v1.5.0/go.mod h1:FUZXzO+5Uqg5zzwzv4KK49R8lvGIyscBOqYrtI1Ce9A=
gorm.io/driver/sqlite v1.5.7 h1:8NvsrhP0ifM7LX9G4zPB97NwovUakUxc+2V2uuf3Z1I=
gorm.io/driver/sqlite v1.5.7/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4=
gorm.io/gorm v1.25.7-0.20240204074919-46816ad31dde h1:9DShaph9qhkIYw7QF91I/ynrr4cOO2PZra2PFD7Mfeg=
gorm.io/gorm v1.25.7-0.20240204074919-46816ad31dde/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
gorm.io/driver/sqlserver v1.5.4 h1:xA+Y1KDNspv79q43bPyjDMUgHoYHLhXYmdFcYPobg8g=
gorm.io/driver/sqlserver v1.5.4/go.mod h1:+frZ/qYmuna11zHPlh5oc2O6ZA/lS88Keb0XSH1Zh/g=
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
gorm.io/gorm v1.25.11 h1:/Wfyg1B/je1hnDx3sMkX+gAlxrlZpn6X0BXRlwXlvHg=
gorm.io/gorm v1.25.11/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=

82
handlers/admin_handler.go Normal file
View File

@@ -0,0 +1,82 @@
package handlers
import (
"net/http"
"strconv"
"oidc-oauth2-server/services"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
)
type AdminHandler struct {
adminService *services.AdminService
}
func NewAdminHandler(adminService *services.AdminService) *AdminHandler {
return &AdminHandler{adminService: adminService}
}
func (h *AdminHandler) ShowAdminLogin(c *gin.Context) {
c.HTML(http.StatusOK, "admin_login.html", gin.H{})
}
func (h *AdminHandler) HandleAdminLogin(c *gin.Context) {
username := c.PostForm("username")
password := c.PostForm("password")
admin, err := h.adminService.Authenticate(username, password)
if err != nil {
c.HTML(http.StatusBadRequest, "admin_login.html", gin.H{
"error": "Invalid credentials",
})
return
}
session := sessions.Default(c)
session.Set("admin_id", admin.ID)
session.Save()
c.Redirect(http.StatusFound, "/admin/dashboard")
}
func (h *AdminHandler) Dashboard(c *gin.Context) {
c.HTML(http.StatusOK, "admin_dashboard.html", gin.H{})
}
func (h *AdminHandler) ListUsers(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10"))
users, total, err := h.adminService.ListUsers(page, pageSize)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.HTML(http.StatusOK, "admin_users.html", gin.H{
"users": users,
"total": total,
"page": page,
"pageSize": pageSize,
})
}
func (h *AdminHandler) ListClients(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10"))
clients, total, err := h.adminService.ListClients(page, pageSize)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.HTML(http.StatusOK, "admin_clients.html", gin.H{
"clients": clients,
"total": total,
"page": page,
"pageSize": pageSize,
})
}

42
handlers/auth_handler.go Normal file
View File

@@ -0,0 +1,42 @@
package handlers
import (
"net/http"
"github.com/gin-gonic/gin"
)
// ShowSignup 显示注册页面
func (h *AuthHandler) ShowSignup(c *gin.Context) {
c.HTML(http.StatusOK, "signup.html", gin.H{
"title": "注册",
})
}
// HandleSignup 处理用户注册
func (h *AuthHandler) HandleSignup(c *gin.Context) {
username := c.PostForm("username")
password := c.PostForm("password")
email := c.PostForm("email")
if username == "" || password == "" || email == "" {
c.HTML(http.StatusBadRequest, "signup.html", gin.H{
"title": "注册",
"error": "用户名、密码和邮箱都不能为空",
})
return
}
// 创建新用户
_, err := h.authService.CreateUser(username, password, email)
if err != nil {
c.HTML(http.StatusBadRequest, "signup.html", gin.H{
"title": "注册",
"error": "注册失败:" + err.Error(),
})
return
}
// 注册成功后重定向到登录页
c.Redirect(http.StatusFound, "/login")
}

30
main.go
View File

@@ -10,8 +10,10 @@ import (
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"html/template"
"oidc-oauth2-server/config"
"oidc-oauth2-server/handlers"
"oidc-oauth2-server/middleware"
"oidc-oauth2-server/models"
"oidc-oauth2-server/services"
)
@@ -41,11 +43,20 @@ func main() {
}
clientService := services.NewClientService(db)
tokenService := services.NewTokenService(db, oauthService.GetKeyManager())
adminService := services.NewAdminService(db)
// 设置 Gin 路由
r := gin.Default()
// 设置模板目录
r.SetFuncMap(template.FuncMap{
"subtract": func(a, b int) int {
return a - b
},
"add": func(a, b int) int {
return a + b
},
})
r.LoadHTMLGlob("templates/*")
// 设置 session 中间件
@@ -64,10 +75,13 @@ func main() {
oidcHandler := handlers.NewOIDCHandler(config.GlobalConfig.OAuth.IssuerURL, oauthService, authService)
registrationHandler := handlers.NewRegistrationHandler(clientService)
tokenHandler := handlers.NewTokenHandler(tokenService)
adminHandler := handlers.NewAdminHandler(adminService)
// 认证路由
r.GET("/login", authHandler.ShowLogin)
r.POST("/login", authHandler.HandleLogin)
r.GET("/signup", authHandler.ShowSignup)
r.POST("/signup", authHandler.HandleSignup)
// OIDC 端点
r.GET("/.well-known/openid-configuration", oidcHandler.OpenIDConfiguration)
@@ -86,6 +100,22 @@ func main() {
r.POST("/revoke", tokenHandler.Revoke)
r.POST("/introspect", tokenHandler.Introspect)
// 管理后台路由
admin := r.Group("/admin")
{
admin.GET("/login", adminHandler.ShowAdminLogin)
admin.POST("/login", adminHandler.HandleAdminLogin)
// 需要管理员认证的路由
authorized := admin.Group("/")
authorized.Use(middleware.AdminAuthRequired())
{
authorized.GET("/dashboard", adminHandler.Dashboard)
authorized.GET("/users", adminHandler.ListUsers)
authorized.GET("/clients", adminHandler.ListClients)
}
}
// 启动服务器
addr := fmt.Sprintf("%s:%d", config.GlobalConfig.Server.Host, config.GlobalConfig.Server.Port)
log.Printf("Starting server on %s", addr)

19
middleware/admin_auth.go Normal file
View File

@@ -0,0 +1,19 @@
package middleware
import (
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
)
func AdminAuthRequired() gin.HandlerFunc {
return func(c *gin.Context) {
session := sessions.Default(c)
adminID := session.Get("admin_id")
if adminID == nil {
c.Redirect(302, "/admin/login")
c.Abort()
return
}
c.Next()
}
}

21
models/admin.go Normal file
View File

@@ -0,0 +1,21 @@
package models
import (
"time"
"gorm.io/gorm"
)
type Admin struct {
gorm.Model
Username string `gorm:"uniqueIndex;not null"`
Password string `gorm:"not null"`
Email string `gorm:"uniqueIndex"`
LastLogin time.Time
IsActive bool `gorm:"default:true"`
Role string `gorm:"type:varchar(20);default:'admin'"`
}
func (a *Admin) TableName() string {
return "admins"
}

View File

@@ -10,5 +10,6 @@ func AutoMigrate(db *gorm.DB) error {
&User{},
&Client{},
&AuthorizationCode{},
&Admin{},
)
}

179
services/admin.go Normal file
View File

@@ -0,0 +1,179 @@
package services
import (
"errors"
"time"
"oidc-oauth2-server/models"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
)
type AdminService struct {
db *gorm.DB
}
func NewAdminService(db *gorm.DB) *AdminService {
return &AdminService{db: db}
}
type AdminCreateRequest struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
Email string `json:"email" binding:"required,email"`
Role string `json:"role"`
}
type AdminUpdateRequest struct {
Password string `json:"password"`
Email string `json:"email" binding:"omitempty,email"`
IsActive *bool `json:"is_active"`
Role string `json:"role"`
}
type AdminResponse struct {
ID uint `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
LastLogin time.Time `json:"last_login"`
IsActive bool `json:"is_active"`
Role string `json:"role"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
func (s *AdminService) Create(req *AdminCreateRequest) (*AdminResponse, error) {
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
if err != nil {
return nil, err
}
admin := &models.Admin{
Username: req.Username,
Password: string(hashedPassword),
Email: req.Email,
Role: req.Role,
}
if err := s.db.Create(admin).Error; err != nil {
return nil, err
}
return s.toResponse(admin), nil
}
func (s *AdminService) Update(id uint, req *AdminUpdateRequest) (*AdminResponse, error) {
admin := &models.Admin{}
if err := s.db.First(admin, id).Error; err != nil {
return nil, err
}
if req.Password != "" {
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
if err != nil {
return nil, err
}
admin.Password = string(hashedPassword)
}
if req.Email != "" {
admin.Email = req.Email
}
if req.IsActive != nil {
admin.IsActive = *req.IsActive
}
if req.Role != "" {
admin.Role = req.Role
}
if err := s.db.Save(admin).Error; err != nil {
return nil, err
}
return s.toResponse(admin), nil
}
func (s *AdminService) Delete(id uint) error {
return s.db.Delete(&models.Admin{}, id).Error
}
func (s *AdminService) Get(id uint) (*AdminResponse, error) {
admin := &models.Admin{}
if err := s.db.First(admin, id).Error; err != nil {
return nil, err
}
return s.toResponse(admin), nil
}
func (s *AdminService) List(page, pageSize int) ([]AdminResponse, int64, error) {
var admins []models.Admin
var total int64
query := s.db.Model(&models.Admin{})
query.Count(&total)
if err := query.Offset((page - 1) * pageSize).Limit(pageSize).Find(&admins).Error; err != nil {
return nil, 0, err
}
responses := make([]AdminResponse, len(admins))
for i, admin := range admins {
responses[i] = *s.toResponse(&admin)
}
return responses, total, nil
}
func (s *AdminService) Authenticate(username, password string) (*AdminResponse, error) {
admin := &models.Admin{}
if err := s.db.Where("username = ?", username).First(admin).Error; err != nil {
return nil, errors.New("invalid credentials")
}
if err := bcrypt.CompareHashAndPassword([]byte(admin.Password), []byte(password)); err != nil {
return nil, errors.New("invalid credentials")
}
admin.LastLogin = time.Now()
if err := s.db.Save(admin).Error; err != nil {
return nil, err
}
return s.toResponse(admin), nil
}
func (s *AdminService) toResponse(admin *models.Admin) *AdminResponse {
return &AdminResponse{
ID: admin.ID,
Username: admin.Username,
Email: admin.Email,
LastLogin: admin.LastLogin,
IsActive: admin.IsActive,
Role: admin.Role,
CreatedAt: admin.CreatedAt,
UpdatedAt: admin.UpdatedAt,
}
}
func (s *AdminService) ListUsers(page, pageSize int) ([]models.User, int64, error) {
var users []models.User
var total int64
s.db.Model(&models.User{}).Count(&total)
err := s.db.Offset((page - 1) * pageSize).Limit(pageSize).Find(&users).Error
return users, total, err
}
func (s *AdminService) ListClients(page, pageSize int) ([]models.Client, int64, error) {
var clients []models.Client
var total int64
s.db.Model(&models.Client{}).Count(&total)
err := s.db.Offset((page - 1) * pageSize).Limit(pageSize).Find(&clients).Error
return clients, total, err
}

View File

@@ -0,0 +1,38 @@
{{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>
<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>
</nav>
</div>
{{template "footer" .}}

View File

@@ -0,0 +1,24 @@
{{template "header" .}}
<div class="container mt-4">
<div class="row">
<div class="col-md-4">
<div class="card">
<div class="card-body">
<h5 class="card-title">用户管理</h5>
<p class="card-text">管理系统用户账号</p>
<a href="/admin/users" class="btn btn-primary">查看用户</a>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-body">
<h5 class="card-title">客户端管理</h5>
<p class="card-text">管理 OAuth2 客户端</p>
<a href="/admin/clients" class="btn btn-primary">查看客户端</a>
</div>
</div>
</div>
</div>
</div>
{{template "footer" .}}

View File

@@ -0,0 +1,29 @@
{{template "header" .}}
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h3 class="text-center">管理员登录</h3>
</div>
<div class="card-body">
{{if .error}}
<div class="alert alert-danger">{{.error}}</div>
{{end}}
<form method="POST" action="/admin/login">
<div class="form-group mb-3">
<label for="username">用户名</label>
<input type="text" class="form-control" id="username" name="username" required>
</div>
<div class="form-group mb-3">
<label for="password">密码</label>
<input type="password" class="form-control" id="password" name="password" required>
</div>
<button type="submit" class="btn btn-primary w-100">登录</button>
</form>
</div>
</div>
</div>
</div>
</div>
{{template "footer" .}}

View File

@@ -0,0 +1,38 @@
{{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>
</nav>
</div>
{{template "footer" .}}

5
templates/footer.html Normal file
View File

@@ -0,0 +1,5 @@
{{define "footer"}}
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
{{end}}

27
templates/header.html Normal file
View File

@@ -0,0 +1,27 @@
{{define "header"}}
<!DOCTYPE html>
<html>
<head>
<title>OIDC OAuth2 管理系统</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container">
<a class="navbar-brand" href="/admin/dashboard">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" href="/admin/users">用户管理</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/admin/clients">客户端管理</a>
</li>
</ul>
</div>
</div>
</nav>
{{end}}

View File

@@ -55,6 +55,19 @@
color: #dc3545;
margin-bottom: 15px;
}
.signup-link {
text-align: center;
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid #eee;
}
.signup-link a {
color: #007bff;
text-decoration: none;
}
.signup-link a:hover {
text-decoration: underline;
}
</style>
</head>
<body>
@@ -82,6 +95,9 @@
<button type="submit">登录</button>
</form>
<div class="signup-link">
还没有账号?<a href="/signup">立即注册</a>
</div>
</div>
</body>
</html>

87
templates/signup.html Normal file
View File

@@ -0,0 +1,87 @@
<!DOCTYPE html>
<html>
<head>
<title>{{.title}}</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body {
font-family: Arial, sans-serif;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
background-color: #f5f5f5;
}
.signup-container {
background: white;
padding: 2rem;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
width: 100%;
max-width: 400px;
}
.form-group {
margin-bottom: 1rem;
}
label {
display: block;
margin-bottom: 0.5rem;
}
input {
width: 100%;
padding: 0.5rem;
border: 1px solid #ddd;
border-radius: 4px;
box-sizing: border-box;
}
button {
width: 100%;
padding: 0.75rem;
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background-color: #0056b3;
}
.error {
color: red;
margin-bottom: 1rem;
}
.login-link {
text-align: center;
margin-top: 1rem;
}
</style>
</head>
<body>
<div class="signup-container">
<h2>注册</h2>
{{if .error}}
<div class="error">{{.error}}</div>
{{end}}
<form method="POST" action="/signup">
<div class="form-group">
<label for="username">用户名</label>
<input type="text" id="username" name="username" required>
</div>
<div class="form-group">
<label for="email">邮箱</label>
<input type="email" id="email" name="email" required>
</div>
<div class="form-group">
<label for="password">密码</label>
<input type="password" id="password" name="password" required>
</div>
<button type="submit">注册</button>
</form>
<div class="login-link">
已有账号?<a href="/login">立即登录</a>
</div>
</div>
</body>
</html>

15
utils/password.go Normal file
View File

@@ -0,0 +1,15 @@
package utils
import (
"golang.org/x/crypto/bcrypt"
)
func HashPassword(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14)
return string(bytes), err
}
func CheckPasswordHash(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}