b8788c239e
MCP Docker / build-and-push (push) Has been cancelled
- Remove telemetry: disabled all phone-home (installation notify, false positives, behavior tracking, upgrade tips) - Version branding: removed 'ce-' prefix, product name changed to '长亭雷池 WAF' - Detection engine enhancement: strict preset enables all 17 modules with aggressive configs - Multi-user RBAC: admin/operator/viewer roles with password + TOTP 2FA - Rate limiting: nginx limit_req per website (RPS, burst, action) - JS Challenge: browser fingerprint verification page with cookie-based bypass - Security fixes: removed InsecureSkipVerify, hardcoded credentials, NO_AUTH backdoor
232 lines
5.9 KiB
Go
232 lines
5.9 KiB
Go
package api
|
|
|
|
import (
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"chaitin.cn/patronus/safeline-2/management/webserver/api/response"
|
|
"chaitin.cn/patronus/safeline-2/management/webserver/model"
|
|
"chaitin.cn/patronus/safeline-2/management/webserver/pkg/database"
|
|
)
|
|
|
|
type CreateUserRequest struct {
|
|
Username string `json:"username" binding:"required,min=3,max=32"`
|
|
Password string `json:"password" binding:"required,min=6,max=64"`
|
|
Role string `json:"role" binding:"required,oneof=admin operator viewer"`
|
|
Comment string `json:"comment"`
|
|
}
|
|
|
|
type UpdateUserRequest struct {
|
|
Role string `json:"role" binding:"omitempty,oneof=admin operator viewer"`
|
|
Comment string `json:"comment"`
|
|
Enabled *bool `json:"enabled"`
|
|
}
|
|
|
|
type ResetPasswordRequest struct {
|
|
Password string `json:"password" binding:"required,min=6,max=64"`
|
|
}
|
|
|
|
type UserInfo struct {
|
|
ID uint `json:"id"`
|
|
Username string `json:"username"`
|
|
Role string `json:"role"`
|
|
Comment string `json:"comment"`
|
|
Enabled bool `json:"enabled"`
|
|
}
|
|
|
|
func GetUsers(c *gin.Context) {
|
|
db := database.GetDB()
|
|
var users []model.User
|
|
db.Find(&users)
|
|
|
|
var result []UserInfo
|
|
for _, u := range users {
|
|
result = append(result, UserInfo{
|
|
ID: u.ID,
|
|
Username: u.Username,
|
|
Role: u.Role,
|
|
Comment: u.Comment,
|
|
Enabled: u.IsEnabled,
|
|
})
|
|
}
|
|
|
|
response.Success(c, gin.H{"data": result})
|
|
}
|
|
|
|
func PostUser(c *gin.Context) {
|
|
var params CreateUserRequest
|
|
if err := c.BindJSON(¶ms); err != nil {
|
|
logger.Error(err)
|
|
response.Error(c, response.ErrorParamNotOK, http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
db := database.GetDB()
|
|
|
|
// Check if username already exists
|
|
var existing model.User
|
|
res := db.Where(&model.User{Username: params.Username}).First(&existing)
|
|
if res.RowsAffected > 0 {
|
|
response.Error(c, response.JSONBody{Err: "username_exists", Msg: "Username already exists"}, http.StatusConflict)
|
|
return
|
|
}
|
|
|
|
user := model.User{
|
|
Username: params.Username,
|
|
Role: params.Role,
|
|
Comment: params.Comment,
|
|
}
|
|
if err := user.SetPassword(params.Password); err != nil {
|
|
logger.Error(err)
|
|
response.Error(c, response.JSONBody{Err: response.ErrInternalError, Msg: "Failed to hash password"}, http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
if err := db.Create(&user).Error; err != nil {
|
|
logger.Error(err)
|
|
response.Error(c, response.JSONBody{Err: response.ErrInternalError, Msg: err.Error()}, http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
response.Success(c, gin.H{
|
|
"id": user.ID,
|
|
"username": user.Username,
|
|
"role": user.Role,
|
|
})
|
|
}
|
|
|
|
func PutUser(c *gin.Context) {
|
|
id, err := strconv.Atoi(c.Param("id"))
|
|
if err != nil {
|
|
response.Error(c, response.ErrorParamNotOK, http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
var params UpdateUserRequest
|
|
if err := c.BindJSON(¶ms); err != nil {
|
|
logger.Error(err)
|
|
response.Error(c, response.ErrorParamNotOK, http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
db := database.GetDB()
|
|
var user model.User
|
|
res := db.First(&user, id)
|
|
if res.RowsAffected == 0 {
|
|
response.Error(c, response.ErrorDataNotExist, http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
// Prevent disabling the last admin
|
|
if params.Enabled != nil && !*params.Enabled && user.Role == "admin" {
|
|
var adminCount int64
|
|
db.Model(&model.User{}).Where("role = ? AND is_enabled = true", "admin").Count(&adminCount)
|
|
if adminCount <= 1 {
|
|
response.Error(c, response.JSONBody{Err: "last_admin", Msg: "Cannot disable the last admin user"}, http.StatusConflict)
|
|
return
|
|
}
|
|
}
|
|
|
|
updates := map[string]interface{}{}
|
|
if params.Role != "" {
|
|
updates["role"] = params.Role
|
|
}
|
|
if params.Comment != "" {
|
|
updates["comment"] = params.Comment
|
|
}
|
|
if params.Enabled != nil {
|
|
updates["is_enabled"] = *params.Enabled
|
|
}
|
|
|
|
db.Model(&user).Updates(updates)
|
|
response.Success(c, nil)
|
|
}
|
|
|
|
func DeleteUser(c *gin.Context) {
|
|
id, err := strconv.Atoi(c.Param("id"))
|
|
if err != nil {
|
|
response.Error(c, response.ErrorParamNotOK, http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
db := database.GetDB()
|
|
var user model.User
|
|
res := db.First(&user, id)
|
|
if res.RowsAffected == 0 {
|
|
response.Error(c, response.ErrorDataNotExist, http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
// Prevent deleting the last admin
|
|
if user.Role == "admin" {
|
|
var adminCount int64
|
|
db.Model(&model.User{}).Where("role = ? AND is_enabled = true", "admin").Count(&adminCount)
|
|
if adminCount <= 1 {
|
|
response.Error(c, response.JSONBody{Err: "last_admin", Msg: "Cannot delete the last admin user"}, http.StatusConflict)
|
|
return
|
|
}
|
|
}
|
|
|
|
// Prevent self-deletion
|
|
sessionUser := c.GetInt("user_id")
|
|
if uint(sessionUser) == user.ID {
|
|
response.Error(c, response.JSONBody{Err: "self_delete", Msg: "Cannot delete your own account"}, http.StatusConflict)
|
|
return
|
|
}
|
|
|
|
db.Delete(&user)
|
|
response.Success(c, nil)
|
|
}
|
|
|
|
func PostResetPassword(c *gin.Context) {
|
|
id, err := strconv.Atoi(c.Param("id"))
|
|
if err != nil {
|
|
response.Error(c, response.ErrorParamNotOK, http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
var params ResetPasswordRequest
|
|
if err := c.BindJSON(¶ms); err != nil {
|
|
logger.Error(err)
|
|
response.Error(c, response.ErrorParamNotOK, http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
db := database.GetDB()
|
|
var user model.User
|
|
res := db.First(&user, id)
|
|
if res.RowsAffected == 0 {
|
|
response.Error(c, response.ErrorDataNotExist, http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
if err := user.SetPassword(params.Password); err != nil {
|
|
logger.Error(err)
|
|
response.Error(c, response.JSONBody{Err: response.ErrInternalError, Msg: "Failed to hash password"}, http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
db.Model(&user).Update("password_hash", user.PasswordHash)
|
|
response.Success(c, nil)
|
|
}
|
|
|
|
// GetUser returns the current authenticated user info
|
|
func GetCurrentUser(c *gin.Context) {
|
|
userId, _ := c.Get("user_id")
|
|
db := database.GetDB()
|
|
var user model.User
|
|
res := db.First(&user, userId)
|
|
if res.RowsAffected == 0 {
|
|
response.Error(c, response.ErrorDataNotExist, http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
response.Success(c, gin.H{
|
|
"id": user.ID,
|
|
"username": user.Username,
|
|
"role": user.Role,
|
|
})
|
|
}
|