更新 Go 版本至 1.23.0,添加管理员功能,包括管理员登录、用户和客户端管理,新增相应的模板和中间件,优化数据库模型以支持管理员管理。
This commit is contained in:
12
go.mod
12
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
|
||||
)
|
||||
|
||||
42
go.sum
42
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=
|
||||
|
||||
82
handlers/admin_handler.go
Normal file
82
handlers/admin_handler.go
Normal 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
42
handlers/auth_handler.go
Normal 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
30
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)
|
||||
|
||||
19
middleware/admin_auth.go
Normal file
19
middleware/admin_auth.go
Normal 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
21
models/admin.go
Normal 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"
|
||||
}
|
||||
@@ -10,5 +10,6 @@ func AutoMigrate(db *gorm.DB) error {
|
||||
&User{},
|
||||
&Client{},
|
||||
&AuthorizationCode{},
|
||||
&Admin{},
|
||||
)
|
||||
}
|
||||
|
||||
179
services/admin.go
Normal file
179
services/admin.go
Normal 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
|
||||
}
|
||||
38
templates/admin_clients.html
Normal file
38
templates/admin_clients.html
Normal 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" .}}
|
||||
24
templates/admin_dashboard.html
Normal file
24
templates/admin_dashboard.html
Normal 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" .}}
|
||||
29
templates/admin_login.html
Normal file
29
templates/admin_login.html
Normal 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" .}}
|
||||
38
templates/admin_users.html
Normal file
38
templates/admin_users.html
Normal 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
5
templates/footer.html
Normal 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
27
templates/header.html
Normal 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}}
|
||||
@@ -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
87
templates/signup.html
Normal 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
15
utils/password.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user