Files
backend/internal/handler/notification_handler.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

178 lines
4.5 KiB
Go

package handler
import (
"strconv"
"github.com/gin-gonic/gin"
"with_you/internal/dto"
"with_you/internal/pkg/cursor"
"with_you/internal/pkg/response"
"with_you/internal/service"
)
// NotificationHandler 通知处理器
type NotificationHandler struct {
notificationService *service.NotificationService
}
// NewNotificationHandler 创建通知处理器
func NewNotificationHandler(notificationService *service.NotificationService) *NotificationHandler {
return &NotificationHandler{
notificationService: notificationService,
}
}
// GetNotifications 获取通知列表
func (h *NotificationHandler) GetNotifications(c *gin.Context) {
userID := c.GetString("user_id")
if userID == "" {
response.Unauthorized(c, "")
return
}
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
unreadOnly := c.Query("unread_only") == "true"
notifications, total, err := h.notificationService.GetByUserID(c.Request.Context(), userID, page, pageSize, unreadOnly)
if err != nil {
response.InternalServerError(c, "failed to get notifications")
return
}
response.Paginated(c, notifications, total, page, pageSize)
}
// MarkAsRead 标记为已读
func (h *NotificationHandler) MarkAsRead(c *gin.Context) {
userID := c.GetString("user_id")
if userID == "" {
response.Unauthorized(c, "")
return
}
id := c.Param("id")
err := h.notificationService.MarkAsReadWithUserID(c.Request.Context(), id, userID)
if err != nil {
response.HandleError(c, err, "failed to mark as read")
return
}
response.SuccessWithMessage(c, "marked as read", nil)
}
// MarkAllAsRead 标记所有为已读
func (h *NotificationHandler) MarkAllAsRead(c *gin.Context) {
userID := c.GetString("user_id")
if userID == "" {
response.Unauthorized(c, "")
return
}
err := h.notificationService.MarkAllAsRead(c.Request.Context(), userID)
if err != nil {
response.InternalServerError(c, "failed to mark all as read")
return
}
response.SuccessWithMessage(c, "all marked as read", nil)
}
// GetUnreadCount 获取未读数量
func (h *NotificationHandler) GetUnreadCount(c *gin.Context) {
userID := c.GetString("user_id")
if userID == "" {
response.Unauthorized(c, "")
return
}
count, err := h.notificationService.GetUnreadCount(c.Request.Context(), userID)
if err != nil {
response.InternalServerError(c, "failed to get unread count")
return
}
response.Success(c, gin.H{"count": count})
}
// DeleteNotification 删除通知
func (h *NotificationHandler) DeleteNotification(c *gin.Context) {
userID := c.GetString("user_id")
if userID == "" {
response.Unauthorized(c, "")
return
}
id := c.Param("id")
err := h.notificationService.DeleteNotification(c.Request.Context(), id, userID)
if err != nil {
response.HandleError(c, err, "failed to delete notification")
return
}
response.Success(c, gin.H{"success": true})
}
// ClearAllNotifications 清空所有通知
func (h *NotificationHandler) ClearAllNotifications(c *gin.Context) {
userID := c.GetString("user_id")
if userID == "" {
response.Unauthorized(c, "")
return
}
err := h.notificationService.ClearAllNotifications(c.Request.Context(), userID)
if err != nil {
response.InternalServerError(c, "failed to clear notifications")
return
}
response.Success(c, gin.H{"success": true})
}
// GetNotificationsByCursor 游标分页获取通知列表
func (h *NotificationHandler) GetNotificationsByCursor(c *gin.Context) {
userID := c.GetString("user_id")
if userID == "" {
response.Unauthorized(c, "")
return
}
// 解析游标分页请求
req := h.parseCursorRequest(c)
unreadOnly := c.Query("unread_only") == "true"
// 调用游标分页服务
result, err := h.notificationService.GetNotificationsByCursor(c.Request.Context(), userID, unreadOnly, req)
if err != nil {
response.InternalServerError(c, "failed to get notifications")
return
}
// 转换为响应结构
notificationResponses := dto.ConvertNotificationsToResponse(result.Items)
// 构建游标分页响应
cursorResp := &dto.NotificationCursorPageResponse{
Items: notificationResponses,
NextCursor: result.NextCursor,
PrevCursor: result.PrevCursor,
HasMore: result.HasMore,
}
response.Success(c, cursorResp)
}
// parseCursorRequest 解析游标分页请求参数
func (h *NotificationHandler) parseCursorRequest(c *gin.Context) *cursor.PageRequest {
cursorStr := c.Query("cursor")
direction := cursor.Direction(c.DefaultQuery("direction", "forward"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
return cursor.NewPageRequest(cursorStr, direction, pageSize)
}