- 48 个 Go 文件所有注释(行注释/块注释/行尾注释,含 _test.go)翻译为中文 - 保留技术标识符:SECURITY_TODO(n)、unsafe-inline、sqlite/mysql、路由参数等 - 代码、字符串字面量、日志消息保持英文原文,零逻辑改动 - go build/vet 通过,go test -count=1 ./... 全绿
420 lines
12 KiB
Go
420 lines
12 KiB
Go
package handlers
|
||
|
||
import (
|
||
"fmt"
|
||
"net/http"
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
"go_blog/models"
|
||
|
||
"gorm.io/gorm"
|
||
)
|
||
|
||
// userForm 保存提交的用户字段,外加共享创建/编辑表单模板的渲染元数据。
|
||
type userForm struct {
|
||
ID uint
|
||
Username string
|
||
Password string
|
||
DisplayName string
|
||
Email string
|
||
Gender string
|
||
Birthday string // 来自日期输入的 YYYY-MM-DD
|
||
Role string
|
||
Status int
|
||
IsEdit bool
|
||
Action string
|
||
TitleText string
|
||
}
|
||
|
||
// userListView 为 User 附加预渲染的标签/徽章,
|
||
// 使模板绝不调用包装为 interface{} 的结构体上的方法。
|
||
type userListView struct {
|
||
models.User
|
||
RoleLabel string
|
||
RoleBadge string
|
||
StatusLabel string
|
||
StatusBadge string
|
||
}
|
||
|
||
// parseUserForm 从请求中读取用户表单字段。
|
||
func parseUserForm(c *gin.Context) userForm {
|
||
// 状态始终由 <select>(0/1/2/3)发送;缺省视为正常,
|
||
// 但显式的 0(禁用)必须保留。
|
||
status := models.StatusNormal
|
||
if raw := strings.TrimSpace(c.PostForm("status")); raw != "" {
|
||
if n, err := strconv.Atoi(raw); err == nil {
|
||
status = n
|
||
}
|
||
}
|
||
return userForm{
|
||
ID: uintFormID(c.Param("id")),
|
||
Username: strings.TrimSpace(c.PostForm("username")),
|
||
Password: c.PostForm("password"),
|
||
DisplayName: strings.TrimSpace(c.PostForm("display_name")),
|
||
Email: strings.TrimSpace(c.PostForm("email")),
|
||
Gender: strings.TrimSpace(c.PostForm("gender")),
|
||
Birthday: strings.TrimSpace(c.PostForm("birthday")),
|
||
Role: strings.TrimSpace(c.PostForm("role")),
|
||
Status: status,
|
||
}
|
||
}
|
||
|
||
// uintFormID 将路由 :id 解析为 uint(不存在/非法时为 0)。
|
||
func uintFormID(s string) uint {
|
||
if s == "" {
|
||
return 0
|
||
}
|
||
n, err := strconv.ParseUint(s, 10, 64)
|
||
if err != nil {
|
||
return 0
|
||
}
|
||
return uint(n)
|
||
}
|
||
|
||
// applyUserFormToData 将表单值写入模板数据映射,
|
||
// 使渲染时表单被重新填充(初次加载或校验错误)。
|
||
func applyUserFormToData(data gin.H, f userForm) {
|
||
data["FormID"] = f.ID
|
||
data["FormUsername"] = f.Username
|
||
data["FormPassword"] = f.Password
|
||
data["FormDisplayName"] = f.DisplayName
|
||
data["FormEmail"] = f.Email
|
||
data["FormGender"] = f.Gender
|
||
data["FormBirthday"] = f.Birthday
|
||
data["FormRole"] = f.Role
|
||
data["FormStatus"] = f.Status
|
||
data["FormIsEdit"] = f.IsEdit
|
||
data["FormAction"] = f.Action
|
||
data["FormTitleText"] = f.TitleText
|
||
}
|
||
|
||
// renderUserForm 使用给定的表单值和可选的错误消息渲染共享的用户表单模板。
|
||
func renderUserForm(c *gin.Context, f userForm, errMsg string) {
|
||
tr := getTr(c)
|
||
data := DefaultData(c)
|
||
data["Title"] = f.TitleText
|
||
if errMsg != "" {
|
||
data["Error"] = errMsg
|
||
}
|
||
// <select> 元素的角色/状态选项。
|
||
data["RoleAdmin"] = models.RoleAdmin
|
||
data["RoleAuthor"] = models.RoleAuthor
|
||
data["StatusNormal"] = models.StatusNormal
|
||
data["StatusDisabled"] = models.StatusDisabled
|
||
data["StatusLocked"] = models.StatusLocked
|
||
data["StatusUnactivated"] = models.StatusUnactivated
|
||
data["TrGenderMale"] = tr["gender_male"]
|
||
data["TrGenderFemale"] = tr["gender_female"]
|
||
data["TrGenderOther"] = tr["gender_other"]
|
||
applyUserFormToData(data, f)
|
||
c.HTML(http.StatusOK, "user_form", data)
|
||
}
|
||
|
||
// UserListPage 渲染后台用户管理列表。
|
||
func UserListPage(db *gorm.DB) gin.HandlerFunc {
|
||
return func(c *gin.Context) {
|
||
tr := getTr(c)
|
||
var users []models.User
|
||
db.Order("created_at DESC").Find(&users)
|
||
|
||
views := make([]userListView, 0, len(users))
|
||
for _, u := range users {
|
||
v := userListView{User: u}
|
||
switch u.Role {
|
||
case models.RoleAdmin:
|
||
v.RoleLabel = tr["role_admin"]
|
||
v.RoleBadge = "bg-purple-100 text-purple-700"
|
||
default:
|
||
v.RoleLabel = tr["role_author"]
|
||
v.RoleBadge = "bg-blue-100 text-blue-700"
|
||
}
|
||
switch u.Status {
|
||
case models.StatusNormal:
|
||
v.StatusLabel = tr["status_normal"]
|
||
v.StatusBadge = "bg-green-100 text-green-700"
|
||
case models.StatusDisabled:
|
||
v.StatusLabel = tr["status_disabled"]
|
||
v.StatusBadge = "bg-gray-200 text-gray-600"
|
||
case models.StatusLocked:
|
||
v.StatusLabel = tr["status_locked"]
|
||
v.StatusBadge = "bg-yellow-100 text-yellow-700"
|
||
default:
|
||
v.StatusLabel = tr["status_unactivated"]
|
||
v.StatusBadge = "bg-red-100 text-red-700"
|
||
}
|
||
views = append(views, v)
|
||
}
|
||
|
||
data := DefaultData(c)
|
||
data["Title"] = tr["admin_users_title"]
|
||
data["Users"] = views
|
||
if msg := c.Query("saved"); msg == "1" {
|
||
switch c.Query("msg") {
|
||
case "created":
|
||
data["Success"] = tr["user_created"]
|
||
case "updated":
|
||
data["Success"] = tr["user_updated"]
|
||
case "deleted":
|
||
data["Success"] = tr["user_deleted"]
|
||
default:
|
||
data["Success"] = tr["settings_saved"]
|
||
}
|
||
}
|
||
if e := c.Query("error"); e != "" {
|
||
switch e {
|
||
case "self_disable":
|
||
data["Error"] = tr["user_cannot_disable_self"]
|
||
case "last_admin":
|
||
data["Error"] = tr["user_cannot_remove_last_admin"]
|
||
case "exists":
|
||
data["Error"] = tr["user_username_exists"]
|
||
}
|
||
}
|
||
c.HTML(http.StatusOK, "user_list", data)
|
||
}
|
||
}
|
||
|
||
// UserCreatePage 渲染空白用户创建表单。
|
||
func UserCreatePage(db *gorm.DB) gin.HandlerFunc {
|
||
return func(c *gin.Context) {
|
||
tr := getTr(c)
|
||
renderUserForm(c, userForm{
|
||
Action: "/admin/users/new",
|
||
TitleText: tr["user_create_title"],
|
||
Role: models.RoleAuthor,
|
||
Status: models.StatusNormal,
|
||
IsEdit: false,
|
||
}, "")
|
||
}
|
||
}
|
||
|
||
// UserCreate 处理 POST 创建新用户。
|
||
func UserCreate(db *gorm.DB) gin.HandlerFunc {
|
||
return func(c *gin.Context) {
|
||
tr := getTr(c)
|
||
f := parseUserForm(c)
|
||
f.Action = "/admin/users/new"
|
||
f.TitleText = tr["user_create_title"]
|
||
f.IsEdit = false
|
||
|
||
if f.Username == "" {
|
||
renderUserForm(c, f, tr["user_username_required"])
|
||
return
|
||
}
|
||
if f.Password == "" {
|
||
renderUserForm(c, f, tr["user_password_required"])
|
||
return
|
||
}
|
||
// SECURITY (#23):执行平台最小密码长度。
|
||
if !validatePassword(f.Password) {
|
||
renderUserForm(c, f, tr["user_password_short"])
|
||
return
|
||
}
|
||
// SECURITY (#24):拒绝格式非法的邮箱地址。
|
||
if !validateEmail(f.Email) {
|
||
renderUserForm(c, f, tr["user_email_invalid"])
|
||
return
|
||
}
|
||
if f.Role == "" {
|
||
f.Role = models.RoleAuthor
|
||
}
|
||
|
||
// 用户名必须唯一。
|
||
var exists int64
|
||
db.Model(&models.User{}).Where("username = ?", f.Username).Count(&exists)
|
||
if exists > 0 {
|
||
renderUserForm(c, f, tr["user_username_exists"])
|
||
return
|
||
}
|
||
|
||
user := models.User{
|
||
Username: f.Username,
|
||
DisplayName: f.DisplayName,
|
||
Email: f.Email,
|
||
Gender: f.Gender,
|
||
Role: f.Role,
|
||
Status: f.Status,
|
||
}
|
||
if t, err := time.Parse("2006-01-02", f.Birthday); err == nil {
|
||
user.Birthday = &t
|
||
}
|
||
if err := user.SetPassword(f.Password); err != nil {
|
||
renderUserForm(c, f, tr["article_error"])
|
||
return
|
||
}
|
||
if err := db.Create(&user).Error; err != nil {
|
||
renderUserForm(c, f, tr["article_error"])
|
||
return
|
||
}
|
||
c.Redirect(http.StatusFound, "/admin/users?saved=1&msg=created")
|
||
}
|
||
}
|
||
|
||
// UserEditPage 渲染预填现有用户的编辑表单。
|
||
func UserEditPage(db *gorm.DB) gin.HandlerFunc {
|
||
return func(c *gin.Context) {
|
||
tr := getTr(c)
|
||
// SECURITY (#19):路由参数必须先解析为数值 id 再交给 GORM——
|
||
// 原始字符串作为单一条件传给 First() 时会按 SQL WHERE 子句拼接。
|
||
id := uintFormID(c.Param("id"))
|
||
if id == 0 {
|
||
c.Redirect(http.StatusFound, "/admin/users")
|
||
return
|
||
}
|
||
|
||
var user models.User
|
||
if err := db.First(&user, id).Error; err != nil {
|
||
c.Redirect(http.StatusFound, "/admin/users")
|
||
return
|
||
}
|
||
birthday := ""
|
||
if user.Birthday != nil {
|
||
birthday = user.Birthday.Format("2006-01-02")
|
||
}
|
||
renderUserForm(c, userForm{
|
||
ID: user.ID,
|
||
Username: user.Username,
|
||
DisplayName: user.DisplayName,
|
||
Email: user.Email,
|
||
Gender: user.Gender,
|
||
Birthday: birthday,
|
||
Role: user.Role,
|
||
Status: user.Status,
|
||
IsEdit: true,
|
||
Action: fmt.Sprintf("/admin/users/%d/edit", user.ID),
|
||
TitleText: tr["user_edit_title"],
|
||
}, "")
|
||
}
|
||
}
|
||
|
||
// UserUpdate 处理 POST 更新现有用户。
|
||
func UserUpdate(db *gorm.DB) gin.HandlerFunc {
|
||
return func(c *gin.Context) {
|
||
tr := getTr(c)
|
||
f := parseUserForm(c)
|
||
// SECURITY (#19):在触碰 GORM 前拒绝非数值 id(参见 UserEditPage)。
|
||
if f.ID == 0 {
|
||
c.Redirect(http.StatusFound, "/admin/users")
|
||
return
|
||
}
|
||
f.IsEdit = true
|
||
f.Action = fmt.Sprintf("/admin/users/%d/edit", f.ID)
|
||
f.TitleText = tr["user_edit_title"]
|
||
|
||
var user models.User
|
||
if err := db.First(&user, f.ID).Error; err != nil {
|
||
c.Redirect(http.StatusFound, "/admin/users")
|
||
return
|
||
}
|
||
|
||
// 保留原用户名(它是登录键 + 文章外键的来源)。
|
||
f.Username = user.Username
|
||
|
||
// SECURITY (#23/#24):在进行任何其他修改前校验提交的密码/邮箱——
|
||
// 密码重置或个人资料编辑必须遵守与注册相同的规则。
|
||
if f.Password != "" && !validatePassword(f.Password) {
|
||
renderUserForm(c, f, tr["user_password_short"])
|
||
return
|
||
}
|
||
if !validateEmail(f.Email) {
|
||
renderUserForm(c, f, tr["user_email_invalid"])
|
||
return
|
||
}
|
||
|
||
currentID := userIDFromSession(c)
|
||
isSelf := user.ID == currentID
|
||
|
||
// 自我保护:不能禁用/锁定自己的账户。
|
||
if isSelf && f.Status != models.StatusNormal {
|
||
f.DisplayName = user.DisplayName
|
||
f.Email = user.Email
|
||
f.Gender = user.Gender
|
||
f.Role = user.Role
|
||
f.Status = user.Status
|
||
if user.Birthday != nil {
|
||
f.Birthday = user.Birthday.Format("2006-01-02")
|
||
}
|
||
renderUserForm(c, f, tr["user_cannot_disable_self"])
|
||
return
|
||
}
|
||
|
||
// 自我保护:不能降级最后一位管理员。
|
||
if user.Role == models.RoleAdmin && f.Role != models.RoleAdmin {
|
||
var adminCount int64
|
||
db.Model(&models.User{}).Where("role = ?", models.RoleAdmin).Count(&adminCount)
|
||
if adminCount <= 1 {
|
||
c.Redirect(http.StatusFound, "/admin/users?error=last_admin")
|
||
return
|
||
}
|
||
}
|
||
if f.Role == "" {
|
||
f.Role = user.Role
|
||
}
|
||
|
||
user.DisplayName = f.DisplayName
|
||
user.Email = f.Email
|
||
user.Gender = f.Gender
|
||
user.Role = f.Role
|
||
user.Status = f.Status
|
||
if t, err := time.Parse("2006-01-02", f.Birthday); err == nil {
|
||
user.Birthday = &t
|
||
} else {
|
||
user.Birthday = nil
|
||
}
|
||
|
||
// 可选密码重置(留空表示保持当前密码)。
|
||
if f.Password != "" {
|
||
if err := user.SetPassword(f.Password); err != nil {
|
||
renderUserForm(c, f, tr["article_error"])
|
||
return
|
||
}
|
||
}
|
||
|
||
if err := db.Save(&user).Error; err != nil {
|
||
renderUserForm(c, f, tr["article_error"])
|
||
return
|
||
}
|
||
c.Redirect(http.StatusFound, "/admin/users?saved=1&msg=updated")
|
||
}
|
||
}
|
||
|
||
// UserDelete 软删除用户,带自我保护和最后管理员防线。
|
||
func UserDelete(db *gorm.DB) gin.HandlerFunc {
|
||
return func(c *gin.Context) {
|
||
// SECURITY (#19):在触碰 GORM 前拒绝非数值 id(参见 UserEditPage)。
|
||
targetID := uintFormID(c.Param("id"))
|
||
if targetID == 0 {
|
||
c.Redirect(http.StatusFound, "/admin/users")
|
||
return
|
||
}
|
||
currentID := userIDFromSession(c)
|
||
|
||
var user models.User
|
||
if err := db.First(&user, targetID).Error; err != nil {
|
||
c.Redirect(http.StatusFound, "/admin/users")
|
||
return
|
||
}
|
||
|
||
// 不能删除自己。
|
||
if targetID == currentID {
|
||
c.Redirect(http.StatusFound, "/admin/users?error=self_disable")
|
||
return
|
||
}
|
||
// 不能删除最后一位管理员。
|
||
if user.Role == models.RoleAdmin {
|
||
var adminCount int64
|
||
db.Model(&models.User{}).Where("role = ?", models.RoleAdmin).Count(&adminCount)
|
||
if adminCount <= 1 {
|
||
c.Redirect(http.StatusFound, "/admin/users?error=last_admin")
|
||
return
|
||
}
|
||
}
|
||
|
||
db.Delete(&user)
|
||
c.Redirect(http.StatusFound, "/admin/users?saved=1&msg=deleted")
|
||
}
|
||
}
|