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" .}} +
+

客户端管理

+ + + + + + + + + + + {{range .clients}} + + + + + + + {{end}} + +
ID客户端ID名称创建时间
{{.ID}}{{.ClientID}}{{.Name}}{{.CreatedAt.Format "2006-01-02 15:04:05"}}
+ + +
+{{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" .}} +
+

用户管理

+ + + + + + + + + + + {{range .users}} + + + + + + + {{end}} + +
ID用户名邮箱创建时间
{{.ID}}{{.Username}}{{.Email}}{{.CreatedAt.Format "2006-01-02 15:04:05"}}
+ + +
+{{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"}} + + + +{{end}} \ No newline at end of file diff --git a/templates/header.html b/templates/header.html new file mode 100644 index 0000000..a250bc8 --- /dev/null +++ b/templates/header.html @@ -0,0 +1,27 @@ +{{define "header"}} + + + + OIDC OAuth2 管理系统 + + + + +{{end}} \ No newline at end of file diff --git a/templates/login.html b/templates/login.html index d1c483b..0fa90b7 100644 --- a/templates/login.html +++ b/templates/login.html @@ -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; + } @@ -82,6 +95,9 @@ + \ No newline at end of file diff --git a/templates/signup.html b/templates/signup.html new file mode 100644 index 0000000..430795e --- /dev/null +++ b/templates/signup.html @@ -0,0 +1,87 @@ + + + + {{.title}} + + + + + +
+

注册

+ {{if .error}} +
{{.error}}
+ {{end}} +
+
+ + +
+
+ + +
+
+ + +
+ +
+ +
+ + \ No newline at end of file diff --git a/utils/password.go b/utils/password.go new file mode 100644 index 0000000..1de1146 --- /dev/null +++ b/utils/password.go @@ -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 +}