diff --git a/go.mod b/go.mod
index 8fd44f0..374dfd4 100644
--- a/go.mod
+++ b/go.mod
@@ -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
)
diff --git a/go.sum b/go.sum
index 1d76709..e897cf2 100644
--- a/go.sum
+++ b/go.sum
@@ -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=
diff --git a/handlers/admin_handler.go b/handlers/admin_handler.go
new file mode 100644
index 0000000..4fac5b9
--- /dev/null
+++ b/handlers/admin_handler.go
@@ -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,
+ })
+}
diff --git a/handlers/auth_handler.go b/handlers/auth_handler.go
new file mode 100644
index 0000000..57e7917
--- /dev/null
+++ b/handlers/auth_handler.go
@@ -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")
+}
diff --git a/main.go b/main.go
index 10543cd..47cbd33 100644
--- a/main.go
+++ b/main.go
@@ -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)
diff --git a/middleware/admin_auth.go b/middleware/admin_auth.go
new file mode 100644
index 0000000..f28fa41
--- /dev/null
+++ b/middleware/admin_auth.go
@@ -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()
+ }
+}
diff --git a/models/admin.go b/models/admin.go
new file mode 100644
index 0000000..edc6437
--- /dev/null
+++ b/models/admin.go
@@ -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"
+}
diff --git a/models/migration.go b/models/migration.go
index 45d4366..9e14ca9 100644
--- a/models/migration.go
+++ b/models/migration.go
@@ -10,5 +10,6 @@ func AutoMigrate(db *gorm.DB) error {
&User{},
&Client{},
&AuthorizationCode{},
+ &Admin{},
)
}
diff --git a/services/admin.go b/services/admin.go
new file mode 100644
index 0000000..13131b5
--- /dev/null
+++ b/services/admin.go
@@ -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
+}
diff --git a/templates/admin_clients.html b/templates/admin_clients.html
new file mode 100644
index 0000000..e7ba984
--- /dev/null
+++ b/templates/admin_clients.html
@@ -0,0 +1,38 @@
+{{template "header" .}}
+
+
客户端管理
+
+
+
+ | ID |
+ 客户端ID |
+ 名称 |
+ 创建时间 |
+
+
+
+ {{range .clients}}
+
+ | {{.ID}} |
+ {{.ClientID}} |
+ {{.Name}} |
+ {{.CreatedAt.Format "2006-01-02 15:04:05"}} |
+
+ {{end}}
+
+
+
+
+
+{{template "footer" .}}
\ No newline at end of file
diff --git a/templates/admin_dashboard.html b/templates/admin_dashboard.html
new file mode 100644
index 0000000..e15750e
--- /dev/null
+++ b/templates/admin_dashboard.html
@@ -0,0 +1,24 @@
+{{template "header" .}}
+
+
+
+
+
+
用户管理
+
管理系统用户账号
+
查看用户
+
+
+
+
+
+
+
客户端管理
+
管理 OAuth2 客户端
+
查看客户端
+
+
+
+
+
+{{template "footer" .}}
\ No newline at end of file
diff --git a/templates/admin_login.html b/templates/admin_login.html
new file mode 100644
index 0000000..a849818
--- /dev/null
+++ b/templates/admin_login.html
@@ -0,0 +1,29 @@
+{{template "header" .}}
+
+
+
+
+
+
+ {{if .error}}
+
{{.error}}
+ {{end}}
+
+
+
+
+
+
+{{template "footer" .}}
\ No newline at end of file
diff --git a/templates/admin_users.html b/templates/admin_users.html
new file mode 100644
index 0000000..6d0010a
--- /dev/null
+++ b/templates/admin_users.html
@@ -0,0 +1,38 @@
+{{template "header" .}}
+
+
用户管理
+
+
+
+ | ID |
+ 用户名 |
+ 邮箱 |
+ 创建时间 |
+
+
+
+ {{range .users}}
+
+ | {{.ID}} |
+ {{.Username}} |
+ {{.Email}} |
+ {{.CreatedAt.Format "2006-01-02 15:04:05"}} |
+
+ {{end}}
+
+
+
+
+
+{{template "footer" .}}
\ No newline at end of file
diff --git a/templates/footer.html b/templates/footer.html
new file mode 100644
index 0000000..6adcf11
--- /dev/null
+++ b/templates/footer.html
@@ -0,0 +1,5 @@
+{{define "footer"}}
+
+