fix/status-review-sync-20260409 #1
@@ -3,7 +3,6 @@ import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest'
|
||||
import {
|
||||
getDeviceFingerprint,
|
||||
clearDeviceFingerprint,
|
||||
type DeviceFingerprint,
|
||||
} from './device-fingerprint'
|
||||
|
||||
describe('device-fingerprint', () => {
|
||||
@@ -99,21 +98,10 @@ describe('device-fingerprint', () => {
|
||||
|
||||
describe('browser detection', () => {
|
||||
it('should detect browser from user agent', () => {
|
||||
// 模拟不同的 User-Agent
|
||||
const testCases = [
|
||||
{ ua: 'Mozilla/5.0 Chrome/120.0', expected: 'Chrome' },
|
||||
{ ua: 'Mozilla/5.0 Firefox/120.0', expected: 'Firefox' },
|
||||
{ ua: 'Mozilla/5.0 Safari/120.0', expected: 'Safari' },
|
||||
{ ua: 'Mozilla/5.0 Edge/120.0', expected: 'Edge' },
|
||||
{ ua: 'Mozilla/5.0 Opera/120.0', expected: 'Opera' },
|
||||
]
|
||||
|
||||
testCases.forEach(({ ua, expected }) => {
|
||||
// 注意:实际测试中 navigator.userAgent 是只读的
|
||||
// 这里主要验证函数能正常工作
|
||||
const fingerprint = getDeviceFingerprint()
|
||||
expect(fingerprint.device_browser).toBeTruthy()
|
||||
})
|
||||
// 注意:实际测试中 navigator.userAgent 是只读的
|
||||
// 这里主要验证函数能正常工作
|
||||
const fingerprint = getDeviceFingerprint()
|
||||
expect(fingerprint.device_browser).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import * as httpIndex from './index'
|
||||
import * as client from './client'
|
||||
import * as authSession from './auth-session'
|
||||
import * as errors from '@/lib/errors'
|
||||
|
||||
describe('lib/http/index', () => {
|
||||
|
||||
@@ -185,6 +185,22 @@ func (h *UserHandler) UpdateUser(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Authorization: only self or admin can update user profile
|
||||
currentUserID := c.GetInt64("user_id")
|
||||
isAdmin := false
|
||||
if roles, ok := c.Get("user_roles"); ok {
|
||||
for _, role := range roles.([]*domain.Role) {
|
||||
if role.Code == "admin" {
|
||||
isAdmin = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if currentUserID != id && !isAdmin {
|
||||
c.JSON(http.StatusForbidden, gin.H{"code": 403, "message": "permission denied"})
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Email *string `json:"email"`
|
||||
Nickname *string `json:"nickname"`
|
||||
|
||||
@@ -10,11 +10,22 @@ import (
|
||||
)
|
||||
|
||||
var corsConfig = config.CORSConfig{
|
||||
AllowedOrigins: []string{"*"},
|
||||
AllowCredentials: true,
|
||||
AllowedOrigins: []string{}, // 默认为空,必须显式配置
|
||||
AllowCredentials: false, // 默认关闭凭证,必须显式启用
|
||||
}
|
||||
|
||||
// init 在包初始化时检测危险的 CORS 配置组合
|
||||
func init() {
|
||||
// 检测危险的通配符 + Credentials 组合
|
||||
for _, origin := range corsConfig.AllowedOrigins {
|
||||
if origin == "*" && corsConfig.AllowCredentials {
|
||||
panic("CORS 配置错误: AllowedOrigins 包含 '*' 且 AllowCredentials 为 true 是危险组合")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func SetCORSConfig(cfg config.CORSConfig) {
|
||||
// 注意:显式配置危险组合时不会panic,但生产环境应避免使用
|
||||
corsConfig = cfg
|
||||
}
|
||||
|
||||
|
||||
@@ -236,10 +236,11 @@ func (r *DeviceRepository) ListAll(ctx context.Context, params *ListDevicesParam
|
||||
if params.IsTrusted != nil {
|
||||
query = query.Where("is_trusted = ?", *params.IsTrusted)
|
||||
}
|
||||
// 按关键词筛选(设备名/IP/位置)
|
||||
// 按关键词筛选(设备名/IP/位置)- 转义 LIKE 特殊字符
|
||||
if params.Keyword != "" {
|
||||
search := "%" + params.Keyword + "%"
|
||||
query = query.Where("device_name LIKE ? OR ip LIKE ? OR location LIKE ?", search, search, search)
|
||||
escapedKeyword := escapeLikePattern(params.Keyword)
|
||||
pattern := "%" + escapedKeyword + "%"
|
||||
query = query.Where("device_name LIKE ? OR ip LIKE ? OR location LIKE ?", pattern, pattern, pattern)
|
||||
}
|
||||
|
||||
// 获取总数
|
||||
|
||||
@@ -783,13 +783,16 @@ func (s *AuthService) RefreshToken(ctx context.Context, refreshToken string) (*L
|
||||
}
|
||||
|
||||
// Token Rotation: 使旧的 refresh token 失效,防止无限刷新
|
||||
// 安全敏感修复:黑名单写入失败时必须 fail closed
|
||||
if s.cache != nil {
|
||||
blacklistKey := tokenBlacklistPrefix + claims.JTI
|
||||
// TTL 设置为 refresh token 的剩余有效期
|
||||
if claims.ExpiresAt != nil {
|
||||
remaining := time.Until(claims.ExpiresAt.Time)
|
||||
if remaining > 0 {
|
||||
_ = s.cache.Set(ctx, blacklistKey, "1", 5*time.Minute, remaining)
|
||||
if err := s.cache.Set(ctx, blacklistKey, "1", 5*time.Minute, remaining); err != nil {
|
||||
return nil, fmt.Errorf("token revocation failed: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user