后端: - 新增全局设备管理 API(DeviceHandler.GetAllDevices) - 新增登录日志导出功能(LogHandler.ExportLoginLogs, CSV/XLSX) - 新增设置服务(SettingsService)和设置页面 API - 设备管理支持多条件筛选(状态/信任状态/关键词) - 登录日志支持流式导出防 OOM - 操作日志支持按方法/时间范围搜索 - 主题配置服务(ThemeService) - 增强监控健康检查(Prometheus metrics + SLO) - 移除旧 ratelimit.go(已迁移至 robustness) - 修复 SocialAccount NULL 扫描问题 - 新增 API 契约测试、Handler 测试、Settings 测试 前端: - 新增管理员设备管理页面(DevicesPage) - 新增管理员登录日志导出功能 - 新增系统设置页面(SettingsPage) - 设备管理支持筛选和分页 - 增强 HTTP 响应类型 测试: - 业务逻辑测试 68 个(含并发 CONC_001~003) - 规模测试 16 个(P99 百分位统计) - E2E 测试、集成测试、契约测试 - 性能基准测试、鲁棒性测试 全面测试通过(38 个测试包)
258 lines
7.1 KiB
Go
258 lines
7.1 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"regexp"
|
|
|
|
"github.com/user-management-system/internal/domain"
|
|
"github.com/user-management-system/internal/repository"
|
|
)
|
|
|
|
// ThemeService 主题服务
|
|
type ThemeService struct {
|
|
themeRepo *repository.ThemeConfigRepository
|
|
}
|
|
|
|
// NewThemeService 创建主题服务
|
|
func NewThemeService(themeRepo *repository.ThemeConfigRepository) *ThemeService {
|
|
return &ThemeService{themeRepo: themeRepo}
|
|
}
|
|
|
|
// CreateThemeRequest 创建主题请求
|
|
type CreateThemeRequest struct {
|
|
Name string `json:"name" binding:"required"`
|
|
LogoURL string `json:"logo_url"`
|
|
FaviconURL string `json:"favicon_url"`
|
|
PrimaryColor string `json:"primary_color"`
|
|
SecondaryColor string `json:"secondary_color"`
|
|
BackgroundColor string `json:"background_color"`
|
|
TextColor string `json:"text_color"`
|
|
CustomCSS string `json:"custom_css"`
|
|
CustomJS string `json:"custom_js"`
|
|
IsDefault bool `json:"is_default"`
|
|
}
|
|
|
|
// UpdateThemeRequest 更新主题请求
|
|
type UpdateThemeRequest struct {
|
|
LogoURL string `json:"logo_url"`
|
|
FaviconURL string `json:"favicon_url"`
|
|
PrimaryColor string `json:"primary_color"`
|
|
SecondaryColor string `json:"secondary_color"`
|
|
BackgroundColor string `json:"background_color"`
|
|
TextColor string `json:"text_color"`
|
|
CustomCSS string `json:"custom_css"`
|
|
CustomJS string `json:"custom_js"`
|
|
Enabled *bool `json:"enabled"`
|
|
IsDefault *bool `json:"is_default"`
|
|
}
|
|
|
|
// CreateTheme 创建主题
|
|
func (s *ThemeService) CreateTheme(ctx context.Context, req *CreateThemeRequest) (*domain.ThemeConfig, error) {
|
|
// 安全检查:禁止在 CustomCSS/CustomJS 中包含危险模式
|
|
if err := validateCustomCSSJS(req.CustomCSS, req.CustomJS); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// 检查主题名称是否已存在
|
|
existing, err := s.themeRepo.GetByName(ctx, req.Name)
|
|
if err == nil && existing != nil {
|
|
return nil, errors.New("主题名称已存在")
|
|
}
|
|
|
|
theme := &domain.ThemeConfig{
|
|
Name: req.Name,
|
|
LogoURL: req.LogoURL,
|
|
FaviconURL: req.FaviconURL,
|
|
PrimaryColor: req.PrimaryColor,
|
|
SecondaryColor: req.SecondaryColor,
|
|
BackgroundColor: req.BackgroundColor,
|
|
TextColor: req.TextColor,
|
|
CustomCSS: req.CustomCSS,
|
|
CustomJS: req.CustomJS,
|
|
IsDefault: req.IsDefault,
|
|
Enabled: true,
|
|
}
|
|
|
|
// 如果设置为默认,先清除其他默认
|
|
if req.IsDefault {
|
|
if err := s.clearDefaultThemes(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
if err := s.themeRepo.Create(ctx, theme); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return theme, nil
|
|
}
|
|
|
|
// UpdateTheme 更新主题
|
|
func (s *ThemeService) UpdateTheme(ctx context.Context, id int64, req *UpdateThemeRequest) (*domain.ThemeConfig, error) {
|
|
// 安全检查:禁止在 CustomCSS/CustomJS 中包含危险模式
|
|
if err := validateCustomCSSJS(req.CustomCSS, req.CustomJS); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
theme, err := s.themeRepo.GetByID(ctx, id)
|
|
if err != nil {
|
|
return nil, errors.New("主题不存在")
|
|
}
|
|
|
|
if req.LogoURL != "" {
|
|
theme.LogoURL = req.LogoURL
|
|
}
|
|
if req.FaviconURL != "" {
|
|
theme.FaviconURL = req.FaviconURL
|
|
}
|
|
if req.PrimaryColor != "" {
|
|
theme.PrimaryColor = req.PrimaryColor
|
|
}
|
|
if req.SecondaryColor != "" {
|
|
theme.SecondaryColor = req.SecondaryColor
|
|
}
|
|
if req.BackgroundColor != "" {
|
|
theme.BackgroundColor = req.BackgroundColor
|
|
}
|
|
if req.TextColor != "" {
|
|
theme.TextColor = req.TextColor
|
|
}
|
|
if req.CustomCSS != "" {
|
|
theme.CustomCSS = req.CustomCSS
|
|
}
|
|
if req.CustomJS != "" {
|
|
theme.CustomJS = req.CustomJS
|
|
}
|
|
if req.Enabled != nil {
|
|
theme.Enabled = *req.Enabled
|
|
}
|
|
if req.IsDefault != nil && *req.IsDefault {
|
|
if err := s.clearDefaultThemes(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
theme.IsDefault = true
|
|
}
|
|
|
|
if err := s.themeRepo.Update(ctx, theme); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return theme, nil
|
|
}
|
|
|
|
// DeleteTheme 删除主题
|
|
func (s *ThemeService) DeleteTheme(ctx context.Context, id int64) error {
|
|
theme, err := s.themeRepo.GetByID(ctx, id)
|
|
if err != nil {
|
|
return errors.New("主题不存在")
|
|
}
|
|
|
|
if theme.IsDefault {
|
|
return errors.New("不能删除默认主题")
|
|
}
|
|
|
|
return s.themeRepo.Delete(ctx, id)
|
|
}
|
|
|
|
// GetTheme 获取主题
|
|
func (s *ThemeService) GetTheme(ctx context.Context, id int64) (*domain.ThemeConfig, error) {
|
|
return s.themeRepo.GetByID(ctx, id)
|
|
}
|
|
|
|
// ListThemes 获取所有已启用主题
|
|
func (s *ThemeService) ListThemes(ctx context.Context) ([]*domain.ThemeConfig, error) {
|
|
return s.themeRepo.List(ctx)
|
|
}
|
|
|
|
// ListAllThemes 获取所有主题
|
|
func (s *ThemeService) ListAllThemes(ctx context.Context) ([]*domain.ThemeConfig, error) {
|
|
return s.themeRepo.ListAll(ctx)
|
|
}
|
|
|
|
// GetDefaultTheme 获取默认主题
|
|
func (s *ThemeService) GetDefaultTheme(ctx context.Context) (*domain.ThemeConfig, error) {
|
|
return s.themeRepo.GetDefault(ctx)
|
|
}
|
|
|
|
// SetDefaultTheme 设置默认主题
|
|
func (s *ThemeService) SetDefaultTheme(ctx context.Context, id int64) error {
|
|
theme, err := s.themeRepo.GetByID(ctx, id)
|
|
if err != nil {
|
|
return errors.New("主题不存在")
|
|
}
|
|
|
|
if !theme.Enabled {
|
|
return errors.New("不能将禁用的主题设为默认")
|
|
}
|
|
|
|
return s.themeRepo.SetDefault(ctx, id)
|
|
}
|
|
|
|
// GetActiveTheme 获取当前生效的主题
|
|
func (s *ThemeService) GetActiveTheme(ctx context.Context) (*domain.ThemeConfig, error) {
|
|
theme, err := s.themeRepo.GetDefault(ctx)
|
|
if err != nil {
|
|
// 返回默认配置
|
|
return domain.DefaultThemeConfig(), nil
|
|
}
|
|
return theme, nil
|
|
}
|
|
|
|
// clearDefaultThemes 清除所有默认主题标记
|
|
func (s *ThemeService) clearDefaultThemes(ctx context.Context) error {
|
|
themes, err := s.themeRepo.ListAll(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, t := range themes {
|
|
if t.IsDefault {
|
|
t.IsDefault = false
|
|
if err := s.themeRepo.Update(ctx, t); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// validateCustomCSSJS 检查 CustomCSS 和 CustomJS 是否包含危险 XSS 模式
|
|
// 这不是完全净化,而是拒绝明显可造成 XSS 的模式
|
|
func validateCustomCSSJS(css, js string) error {
|
|
// 危险模式列表
|
|
dangerousPatterns := []struct {
|
|
pattern *regexp.Regexp
|
|
message string
|
|
}{
|
|
// Script 标签
|
|
{regexp.MustCompile(`(?i)<script[^>]*>.*?</script>`), "CustomJS 禁止包含 <script> 标签"},
|
|
{regexp.MustCompile(`(?i)javascript\s*:`), "CustomJS 禁止使用 javascript: 协议"},
|
|
// 事件处理器
|
|
{regexp.MustCompile(`(?i)on\w+\s*=`), "CustomJS 禁止使用事件处理器 (如 onerror, onclick)"},
|
|
// Data URL
|
|
{regexp.MustCompile(`(?i)data\s*:\s*text/html`), "禁止使用 data: URL 嵌入 HTML"},
|
|
// CSS expression (IE)
|
|
{regexp.MustCompile(`(?i)expression\s*\(`), "CustomCSS 禁止使用 CSS expression"},
|
|
// CSS 中的 javascript
|
|
{regexp.MustCompile(`(?i)url\s*\(\s*['"]?\s*javascript:`), "CustomCSS 禁止使用 javascript: URL"},
|
|
// 嵌入的 <style> 标签
|
|
{regexp.MustCompile(`(?i)<style[^>]*>.*?</style>`), "CustomCSS 禁止包含 <style> 标签"},
|
|
}
|
|
|
|
// 检查 JS
|
|
for _, p := range dangerousPatterns {
|
|
if p.pattern.MatchString(js) {
|
|
return errors.New(p.message)
|
|
}
|
|
}
|
|
|
|
// 检查 CSS
|
|
for _, p := range dangerousPatterns {
|
|
if p.pattern.MatchString(css) {
|
|
return errors.New(p.message)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|