新增后台管理

This commit is contained in:
2026-06-03 23:29:21 +08:00
parent b1548baccf
commit 9221a53617
15 changed files with 1299 additions and 57 deletions
+112 -4
View File
@@ -14,17 +14,19 @@ import (
"gorm.io/gorm"
)
func newHTTPServer(cfg webConfig, store *store) *http.Server {
func newHTTPServer(cfg webConfig, store *store, sessions *sessionManager, mqttStatus mqttStatusProvider) *http.Server {
return &http.Server{
Addr: net.JoinHostPort(cfg.Host, strconv.Itoa(cfg.Port)),
Handler: newRouter(cfg, store),
Handler: newRouter(cfg, store, sessions, mqttStatus),
}
}
func newRouter(cfg webConfig, store *store) *gin.Engine {
func newRouter(cfg webConfig, store *store, sessions *sessionManager, mqttStatus mqttStatusProvider) *gin.Engine {
r := gin.New()
r.Use(gin.Logger(), gin.Recovery())
registerAPIRoutes(r.Group("/api"), store)
api := r.Group("/api")
registerAPIRoutes(api, store)
registerAdminRoutes(api.Group("/admin"), store, sessions, mqttStatus)
registerStaticRoutes(r, cfg.StaticDir)
return r
}
@@ -86,6 +88,112 @@ func registerAPIRoutes(r gin.IRouter, store *store) {
})
}
func registerAdminRoutes(r gin.IRouter, store *store, sessions *sessionManager, mqttStatus mqttStatusProvider) {
type loginRequest struct {
Username string `json:"username"`
Password string `json:"password"`
}
type createUserRequest struct {
Username string `json:"username"`
Password string `json:"password"`
}
type updatePasswordRequest struct {
Password string `json:"password"`
}
userDTO := func(user userRecord) gin.H {
return gin.H{"id": user.ID, "username": user.Username, "role": user.Role, "created_at": user.CreatedAt, "updated_at": user.UpdatedAt}
}
r.POST("/login", func(c *gin.Context) {
var req loginRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid login request"})
return
}
user, err := store.GetUserByUsername(req.Username)
if err != nil || user.Role != adminRole || !verifyPassword(user.PasswordHash, req.Password) {
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid username or password"})
return
}
cookie, err := sessions.newCookie(*user)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
http.SetCookie(c.Writer, cookie)
c.JSON(http.StatusOK, gin.H{"user": adminUserResponse(*user)})
})
r.POST("/logout", func(c *gin.Context) {
http.SetCookie(c.Writer, sessions.clearCookie())
c.JSON(http.StatusOK, gin.H{"status": "ok"})
})
protected := r.Group("")
protected.Use(requireAdmin(sessions))
protected.GET("/me", func(c *gin.Context) {
claims := c.MustGet("admin_claims").(*sessionClaims)
c.JSON(http.StatusOK, gin.H{"user": adminUserDTO{Username: claims.Username, Role: claims.Role}})
})
protected.GET("/mqtt/status", func(c *gin.Context) {
if mqttStatus == nil {
c.JSON(http.StatusOK, adminMqttStatus{Running: false})
return
}
c.JSON(http.StatusOK, mqttStatus.Status())
})
protected.GET("/users", func(c *gin.Context) {
users, err := store.ListUsers()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
items := make([]gin.H, 0, len(users))
for _, user := range users {
items = append(items, userDTO(user))
}
c.JSON(http.StatusOK, gin.H{"items": items})
})
protected.POST("/users", func(c *gin.Context) {
var req createUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid create user request"})
return
}
user, err := store.CreateAdminUser(req.Username, req.Password)
if errors.Is(err, errUserAlreadyExists) {
c.JSON(http.StatusConflict, gin.H{"error": "username already exists"})
return
}
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, gin.H{"user": userDTO(*user)})
})
protected.PUT("/users/:id/password", func(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil || id == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid user id"})
return
}
var req updatePasswordRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid password request"})
return
}
user, err := store.UpdateUserPassword(id, req.Password)
if errors.Is(err, gorm.ErrRecordNotFound) {
c.JSON(http.StatusNotFound, gin.H{"error": "user not found"})
return
}
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"user": userDTO(*user)})
})
}
func registerNodeInfoRoutes(r gin.IRouter, store *store, path string) {
r.GET(path, func(c *gin.Context) {
opts, ok := parseListOptions(c)