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
166 lines
4.3 KiB
Go
166 lines
4.3 KiB
Go
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
|