feat: 登录设备管理与编辑器图片上传

- 新增登录设备列表与会话下线(ListLoginDevices/RevokeLoginDevice),按设备指纹识别当前会话
- Markdown 编辑器新增图片插入弹窗与图片上传(ImageInsertModal)
- 实时总线、账号守卫与用户事件适配
This commit is contained in:
2026-09-26 01:10:10 +08:00
parent 5270fee2b7
commit 31f5360c26
12 changed files with 706 additions and 165 deletions

View File

@@ -191,27 +191,30 @@ func (s *AuthService) TouchDeviceFromRefresh(plain, ip, ua string, userID uint)
return fid
}
func (s *AuthService) revokeSameFingerprintExcept(userID, keepFamily uint, ip, ua string) {
fp := deviceFingerprint(ip, ua)
// revokeAllSessionsExcept 真·单会话:登录时吊销该用户除新会话外的全部
// 有效会话(任何新登录都踢掉所有旧设备);返回吊销行数供上层广播告知。
func (s *AuthService) revokeAllSessionsExcept(userID, keepFamily uint) int {
now := time.Now()
var actives []model.RefreshToken
if err := s.db.Select("id", "family_id", "ip", "user_agent").
if err := s.db.Select("id", "family_id").
Where("user_id = ? AND revoked = ? AND expires_at > ?", userID, false, now).
Find(&actives).Error; err != nil {
return
return 0
}
ids := make([]uint, 0)
for _, t := range actives {
if sessionFamilyID(t) == keepFamily {
continue
}
if deviceFingerprint(t.IP, t.UserAgent) == fp {
ids = append(ids, t.ID)
}
ids = append(ids, t.ID)
}
if len(ids) == 0 {
return
return 0
}
s.db.Model(&model.RefreshToken{}).Where("id IN ?", ids).
res := s.db.Model(&model.RefreshToken{}).Where("id IN ?", ids).
Updates(map[string]any{"revoked": true, "token_cipher": "", "updated_at": now})
if res.Error != nil {
return 0
}
return int(res.RowsAffected)
}