Files
SafeLine/management/webserver/api/user.go
T
Binbim b8788c239e
MCP Docker / build-and-push (push) Has been cancelled
feat: SafeLine CE 9.3.9 with pro features
- 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
2026-07-04 07:59:04 +08:00

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(&params); 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(&params); 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(&params); 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,
})
}