Files
backend/internal/service/notification_service.go
lafay b2b55ea52d
All checks were successful
Build Backend / build (push) Successful in 1m56s
Build Backend / build-docker (push) Successful in 1m15s
feat: enhance security with IP banning, ownership checks, and SSRF protection
Add comprehensive security improvements across the application:

- **IP-based login protection**: Implement IP ban system tracking login failures, auto-banning after threshold exceeded
- **Ownership verification**: Add userID parameter to Delete/Update operations for posts and comments to prevent unauthorized modifications
- **SSRF protection**: Add URL and resolved host validation for image URLs in chat and OpenAI client
- **SQL injection prevention**: Add EscapeLikeWildcard utility to escape special characters in LIKE queries
- **HTTP security**: Configure server timeouts and add security headers middleware
- **Rate limiting**: Refactor to support configurable duration and per-endpoint rate limits for auth routes
- **Error handling**: Standardize error responses using HandleError and proper error types
2026-04-30 12:26:25 +08:00

166 lines
4.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package service
import (
"context"
"time"
"with_you/internal/cache"
apperrors "with_you/internal/errors"
"with_you/internal/model"
"with_you/internal/pkg/cursor"
"with_you/internal/repository"
)
// 缓存TTL常量
const (
NotificationUnreadCountTTL = 30 * time.Second // 通知未读数缓存30秒
NotificationNullTTL = 5 * time.Second
NotificationCacheJitter = 0.1
)
// NotificationService 通知服务
type NotificationService struct {
notificationRepo repository.NotificationRepository
cache cache.Cache
}
// NewNotificationService 创建通知服务
func NewNotificationService(notificationRepo repository.NotificationRepository, cacheBackend cache.Cache) *NotificationService {
return &NotificationService{
notificationRepo: notificationRepo,
cache: cacheBackend,
}
}
// Create 创建通知
func (s *NotificationService) Create(ctx context.Context, userID string, notificationType model.NotificationType, title, content string) (*model.Notification, error) {
notification := &model.Notification{
UserID: userID,
Type: notificationType,
Title: title,
Content: content,
IsRead: false,
}
err := s.notificationRepo.Create(notification)
if err != nil {
return nil, err
}
// 失效未读数缓存
cache.InvalidateUnreadSystem(s.cache, userID)
return notification, nil
}
// GetByUserID 获取用户通知
func (s *NotificationService) GetByUserID(ctx context.Context, userID string, page, pageSize int, unreadOnly bool) ([]*model.Notification, int64, error) {
return s.notificationRepo.GetByUserID(userID, page, pageSize, unreadOnly)
}
// MarkAsReadWithUserID 标记为已读带用户ID验证所有权
func (s *NotificationService) MarkAsReadWithUserID(ctx context.Context, id, userID string) error {
notification, err := s.notificationRepo.GetByID(id)
if err != nil {
return err
}
if notification.UserID != userID {
return apperrors.ErrForbidden
}
err = s.notificationRepo.MarkAsRead(id)
if err != nil {
return err
}
cache.InvalidateUnreadSystem(s.cache, userID)
return nil
}
// MarkAllAsRead 标记所有为已读
func (s *NotificationService) MarkAllAsRead(ctx context.Context, userID string) error {
err := s.notificationRepo.MarkAllAsRead(userID)
if err != nil {
return err
}
// 失效未读数缓存
cache.InvalidateUnreadSystem(s.cache, userID)
return nil
}
// GetUnreadCount 获取未读数量(带缓存)
func (s *NotificationService) GetUnreadCount(ctx context.Context, userID string) (int64, error) {
cacheSettings := cache.GetSettings()
unreadTTL := cacheSettings.UnreadCountTTL
if unreadTTL <= 0 {
unreadTTL = NotificationUnreadCountTTL
}
nullTTL := cacheSettings.NullTTL
if nullTTL <= 0 {
nullTTL = NotificationNullTTL
}
jitter := cacheSettings.JitterRatio
if jitter <= 0 {
jitter = NotificationCacheJitter
}
// 生成缓存键
cacheKey := cache.UnreadSystemKey(userID)
return cache.GetOrLoadTyped[int64](
s.cache,
cacheKey,
unreadTTL,
jitter,
nullTTL,
func() (int64, error) {
return s.notificationRepo.GetUnreadCount(userID)
},
)
}
// DeleteNotification 删除通知(带用户验证)
func (s *NotificationService) DeleteNotification(ctx context.Context, id, userID string) error {
// 先检查通知是否属于该用户
notification, err := s.notificationRepo.GetByID(id)
if err != nil {
return err
}
if notification.UserID != userID {
return ErrUnauthorizedNotification
}
err = s.notificationRepo.Delete(id)
if err != nil {
return err
}
// 失效未读数缓存
cache.InvalidateUnreadSystem(s.cache, userID)
return nil
}
// ClearAllNotifications 清空所有通知
func (s *NotificationService) ClearAllNotifications(ctx context.Context, userID string) error {
err := s.notificationRepo.DeleteAllByUserID(userID)
if err != nil {
return err
}
// 失效未读数缓存
cache.InvalidateUnreadSystem(s.cache, userID)
return nil
}
// GetNotificationsByCursor 游标分页获取用户通知
func (s *NotificationService) GetNotificationsByCursor(ctx context.Context, userID string, unreadOnly bool, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.Notification], error) {
return s.notificationRepo.GetNotificationsByCursor(ctx, userID, unreadOnly, req)
}
// 错误定义
var ErrUnauthorizedNotification = apperrors.ErrUnauthorizedNotification