feat: user auth.
This commit is contained in:
@@ -2,6 +2,7 @@ package controllers
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"hr_receiver/config"
|
||||||
"hr_receiver/models"
|
"hr_receiver/models"
|
||||||
"hr_receiver/mqtt"
|
"hr_receiver/mqtt"
|
||||||
"hr_receiver/util"
|
"hr_receiver/util"
|
||||||
@@ -82,7 +83,21 @@ func (sc *SystemDebugController) MqttWebSocket(c *gin.Context) {
|
|||||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid token"})
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid token"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if claims.Role != models.UserRoleSuperAdmin {
|
|
||||||
|
var user models.User
|
||||||
|
if err := config.DB.First(&user, claims.UserID).Error; err != nil {
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "user not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !user.IsActive {
|
||||||
|
c.JSON(http.StatusForbidden, gin.H{"error": "user is disabled"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if util.IsTokenRevoked(&user, claims) {
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "token has been revoked"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if user.Role != models.UserRoleSuperAdmin {
|
||||||
c.JSON(http.StatusForbidden, gin.H{"error": "super admin required"})
|
c.JSON(http.StatusForbidden, gin.H{"error": "super admin required"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
+47
-31
@@ -7,6 +7,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
@@ -17,29 +18,31 @@ type UserAdminController struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type userAdminPayload struct {
|
type userAdminPayload struct {
|
||||||
Email *string `json:"email"`
|
Email *string `json:"email"`
|
||||||
FlavorType models.UserFlavorType `json:"flavorType"`
|
FlavorType models.UserFlavorType `json:"flavorType"`
|
||||||
IsActive *bool `json:"isActive"`
|
IsActive *bool `json:"isActive"`
|
||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
Phone *string `json:"phone"`
|
Phone *string `json:"phone"`
|
||||||
RegionIDs []uint32 `json:"regionIds"`
|
RegionIDs []uint32 `json:"regionIds"`
|
||||||
Role models.UserRole `json:"role"`
|
RevokeTokens *bool `json:"revokeTokens"`
|
||||||
Username string `json:"username"`
|
Role models.UserRole `json:"role"`
|
||||||
|
Username string `json:"username"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type userAdminListItem struct {
|
type userAdminListItem struct {
|
||||||
CreatedAt int64 `json:"created_at"`
|
CreatedAt int64 `json:"created_at"`
|
||||||
Email *string `json:"email"`
|
Email *string `json:"email"`
|
||||||
FlavorType models.UserFlavorType `json:"flavorType"`
|
FlavorType models.UserFlavorType `json:"flavorType"`
|
||||||
ID uint `json:"id"`
|
ID uint `json:"id"`
|
||||||
IsActive bool `json:"isActive"`
|
IsActive bool `json:"isActive"`
|
||||||
KindergartenNames []string `json:"kindergartenNames"`
|
KindergartenNames []string `json:"kindergartenNames"`
|
||||||
Phone *string `json:"phone"`
|
Phone *string `json:"phone"`
|
||||||
RegionIDs []uint32 `json:"regionIds"`
|
RegionIDs []uint32 `json:"regionIds"`
|
||||||
Regions []models.UserRegionBinding `json:"regions"`
|
Regions []models.UserRegionBinding `json:"regions"`
|
||||||
Role models.UserRole `json:"role"`
|
Role models.UserRole `json:"role"`
|
||||||
UpdatedAt int64 `json:"updated_at"`
|
TokenInvalidBefore int64 `json:"tokenInvalidBefore"`
|
||||||
Username string `json:"username"`
|
UpdatedAt int64 `json:"updated_at"`
|
||||||
|
Username string `json:"username"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewUserAdminController() *UserAdminController {
|
func NewUserAdminController() *UserAdminController {
|
||||||
@@ -161,6 +164,7 @@ func (uc *UserAdminController) Update(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := uc.DB.Transaction(func(tx *gorm.DB) error {
|
if err := uc.DB.Transaction(func(tx *gorm.DB) error {
|
||||||
|
revokeTokens := payload.RevokeTokens != nil && *payload.RevokeTokens
|
||||||
updateData := map[string]interface{}{
|
updateData := map[string]interface{}{
|
||||||
"username": user.Username,
|
"username": user.Username,
|
||||||
"email": user.Email,
|
"email": user.Email,
|
||||||
@@ -172,6 +176,9 @@ func (uc *UserAdminController) Update(c *gin.Context) {
|
|||||||
if strings.TrimSpace(payload.Password) != "" {
|
if strings.TrimSpace(payload.Password) != "" {
|
||||||
updateData["password"] = user.Password
|
updateData["password"] = user.Password
|
||||||
}
|
}
|
||||||
|
if revokeTokens {
|
||||||
|
updateData["token_invalid_before"] = time.Now().UnixMilli()
|
||||||
|
}
|
||||||
if err := tx.Model(&models.User{}).Where("id = ?", user.ID).Updates(updateData).Error; err != nil {
|
if err := tx.Model(&models.User{}).Where("id = ?", user.ID).Updates(updateData).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -211,6 +218,10 @@ func (uc *UserAdminController) Delete(c *gin.Context) {
|
|||||||
respondUserLookupError(c, err)
|
respondUserLookupError(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if isProtectedAdminAccount(user.Username) {
|
||||||
|
writeError(c, http.StatusBadRequest, "cannot delete protected admin user")
|
||||||
|
return
|
||||||
|
}
|
||||||
currentUserID, _, ok := currentUser(c)
|
currentUserID, _, ok := currentUser(c)
|
||||||
if ok && currentUserID == user.ID {
|
if ok && currentUserID == user.ID {
|
||||||
writeError(c, http.StatusBadRequest, "cannot delete current user")
|
writeError(c, http.StatusBadRequest, "cannot delete current user")
|
||||||
@@ -262,17 +273,18 @@ func (uc *UserAdminController) buildUserAdminItems(users []models.User) ([]userA
|
|||||||
items := make([]userAdminListItem, 0, len(users))
|
items := make([]userAdminListItem, 0, len(users))
|
||||||
for _, user := range users {
|
for _, user := range users {
|
||||||
item := userAdminListItem{
|
item := userAdminListItem{
|
||||||
CreatedAt: user.CreatedAt,
|
CreatedAt: user.CreatedAt,
|
||||||
Email: user.Email,
|
Email: user.Email,
|
||||||
FlavorType: user.FlavorType,
|
FlavorType: user.FlavorType,
|
||||||
ID: user.ID,
|
ID: user.ID,
|
||||||
IsActive: user.IsActive,
|
IsActive: user.IsActive,
|
||||||
Phone: user.Phone,
|
Phone: user.Phone,
|
||||||
RegionIDs: user.RegionIDs(),
|
RegionIDs: user.RegionIDs(),
|
||||||
Regions: user.Regions,
|
Regions: user.Regions,
|
||||||
Role: user.Role,
|
Role: user.Role,
|
||||||
UpdatedAt: user.UpdatedAt,
|
TokenInvalidBefore: user.TokenInvalidBefore,
|
||||||
Username: user.Username,
|
UpdatedAt: user.UpdatedAt,
|
||||||
|
Username: user.Username,
|
||||||
}
|
}
|
||||||
for _, regionID := range item.RegionIDs {
|
for _, regionID := range item.RegionIDs {
|
||||||
if kindergartenName, exists := kindergartenMap[regionID]; exists {
|
if kindergartenName, exists := kindergartenMap[regionID]; exists {
|
||||||
@@ -336,6 +348,10 @@ func normalizeOptionalString(value *string) *string {
|
|||||||
return &trimmed
|
return &trimmed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isProtectedAdminAccount(username string) bool {
|
||||||
|
return strings.EqualFold(strings.TrimSpace(username), "admin")
|
||||||
|
}
|
||||||
|
|
||||||
func respondUserLookupError(c *gin.Context, err error) {
|
func respondUserLookupError(c *gin.Context, err error) {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
writeError(c, http.StatusNotFound, "user not found")
|
writeError(c, http.StatusNotFound, "user not found")
|
||||||
|
|||||||
+18
-22
@@ -35,33 +35,29 @@ func JWTAuth() gin.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
role := claims.Role
|
var user models.User
|
||||||
flavorType := claims.FlavorType
|
if err := config.DB.Preload("Regions").First(&user, claims.UserID).Error; err != nil {
|
||||||
regionIDs := claims.RegionIDs
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "User not found"})
|
||||||
|
c.Abort()
|
||||||
if role == "" || flavorType == "" {
|
return
|
||||||
var user models.User
|
}
|
||||||
if err := config.DB.Preload("Regions").First(&user, claims.UserID).Error; err != nil {
|
if !user.IsActive {
|
||||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "User not found"})
|
c.JSON(http.StatusForbidden, gin.H{"error": "User is disabled"})
|
||||||
c.Abort()
|
c.Abort()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !user.IsActive {
|
if util.IsTokenRevoked(&user, claims) {
|
||||||
c.JSON(http.StatusForbidden, gin.H{"error": "User is disabled"})
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "Token has been revoked"})
|
||||||
c.Abort()
|
c.Abort()
|
||||||
return
|
return
|
||||||
}
|
|
||||||
role = user.Role
|
|
||||||
flavorType = user.FlavorType
|
|
||||||
regionIDs = user.RegionIDs()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 将用户信息存入上下文
|
// 将用户信息存入上下文
|
||||||
c.Set("userID", claims.UserID)
|
c.Set("userID", claims.UserID)
|
||||||
c.Set("username", claims.Username)
|
c.Set("username", claims.Username)
|
||||||
c.Set("role", role)
|
c.Set("role", user.Role)
|
||||||
c.Set("flavorType", flavorType)
|
c.Set("flavorType", user.FlavorType)
|
||||||
c.Set("regionIDs", regionIDs)
|
c.Set("regionIDs", user.RegionIDs())
|
||||||
|
|
||||||
c.Next()
|
c.Next()
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-11
@@ -35,17 +35,18 @@ type UserRegionBinding struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
ID uint `gorm:"primaryKey" json:"id"`
|
ID uint `gorm:"primaryKey" json:"id"`
|
||||||
Username string `gorm:"uniqueIndex;not null" json:"username"`
|
Username string `gorm:"uniqueIndex;not null" json:"username"`
|
||||||
Email *string `gorm:"uniqueIndex;" json:"email"`
|
Email *string `gorm:"uniqueIndex;" json:"email"`
|
||||||
Phone *string `gorm:"uniqueIndex;" json:"phone"`
|
Phone *string `gorm:"uniqueIndex;" json:"phone"`
|
||||||
Password string `gorm:"not null" json:"-"`
|
Password string `gorm:"not null" json:"-"`
|
||||||
Role UserRole `gorm:"type:varchar(32);not null;default:'viewer';index" json:"role"`
|
Role UserRole `gorm:"type:varchar(32);not null;default:'viewer';index" json:"role"`
|
||||||
FlavorType UserFlavorType `gorm:"type:varchar(32);not null;default:'all';index" json:"flavorType"`
|
FlavorType UserFlavorType `gorm:"type:varchar(32);not null;default:'all';index" json:"flavorType"`
|
||||||
IsActive bool `gorm:"not null;default:true;index" json:"isActive"`
|
IsActive bool `gorm:"not null;default:true;index" json:"isActive"`
|
||||||
Regions []UserRegionBinding `gorm:"foreignKey:UserID;constraint:OnDelete:CASCADE" json:"regions"`
|
TokenInvalidBefore int64 `gorm:"not null;default:0" json:"tokenInvalidBefore"`
|
||||||
CreatedAt int64 `gorm:"not null" json:"created_at"`
|
Regions []UserRegionBinding `gorm:"foreignKey:UserID;constraint:OnDelete:CASCADE" json:"regions"`
|
||||||
UpdatedAt int64 `gorm:"not null" json:"updated_at"`
|
CreatedAt int64 `gorm:"not null" json:"created_at"`
|
||||||
|
UpdatedAt int64 `gorm:"not null" json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// HashPassword 密码加密
|
// HashPassword 密码加密
|
||||||
|
|||||||
+20
-5
@@ -20,8 +20,7 @@ type Claims struct {
|
|||||||
|
|
||||||
// GenerateToken 生成JWT Token
|
// GenerateToken 生成JWT Token
|
||||||
func GenerateToken(user *models.User) (string, error) {
|
func GenerateToken(user *models.User) (string, error) {
|
||||||
expirationTime := time.Now().Add(24 * 30 * time.Hour) // Token有效期24小时
|
now := time.Now()
|
||||||
//expirationTime := time.Now().Add(1 * time.Second) // Token有效期24小时
|
|
||||||
|
|
||||||
claims := &Claims{
|
claims := &Claims{
|
||||||
UserID: user.ID,
|
UserID: user.ID,
|
||||||
@@ -30,12 +29,15 @@ func GenerateToken(user *models.User) (string, error) {
|
|||||||
FlavorType: user.FlavorType,
|
FlavorType: user.FlavorType,
|
||||||
RegionIDs: user.RegionIDs(),
|
RegionIDs: user.RegionIDs(),
|
||||||
RegisteredClaims: jwt.RegisteredClaims{
|
RegisteredClaims: jwt.RegisteredClaims{
|
||||||
ExpiresAt: jwt.NewNumericDate(expirationTime),
|
IssuedAt: jwt.NewNumericDate(now),
|
||||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
NotBefore: jwt.NewNumericDate(now),
|
||||||
NotBefore: jwt.NewNumericDate(time.Now()),
|
|
||||||
Issuer: "your-app-name",
|
Issuer: "your-app-name",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
if user.FlavorType != models.UserFlavorHeartRate {
|
||||||
|
expirationTime := now.Add(24 * 30 * time.Hour)
|
||||||
|
claims.ExpiresAt = jwt.NewNumericDate(expirationTime)
|
||||||
|
}
|
||||||
|
|
||||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||||
tokenString, err := token.SignedString([]byte(ApiSecret))
|
tokenString, err := token.SignedString([]byte(ApiSecret))
|
||||||
@@ -61,3 +63,16 @@ func ParseToken(tokenStr string) (*Claims, error) {
|
|||||||
|
|
||||||
return claims, nil
|
return claims, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func IsTokenRevoked(user *models.User, claims *Claims) bool {
|
||||||
|
if user == nil || claims == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if user.TokenInvalidBefore <= 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if claims.IssuedAt == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return claims.IssuedAt.UnixMilli() <= user.TokenInvalidBefore
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user