Skip to content

Commit

Permalink
[Backend] Realtime Notification & Check Naver email existence
Browse files Browse the repository at this point in the history
  • Loading branch information
Alfex4936 committed Apr 18, 2024
1 parent 8ca1c76 commit fc6aa00
Show file tree
Hide file tree
Showing 15 changed files with 530 additions and 63 deletions.
37 changes: 17 additions & 20 deletions backend/dto/notification/notification_dto.go
Original file line number Diff line number Diff line change
@@ -1,28 +1,25 @@
package notification

// Notification is the base struct for all notifications.
// It includes a Type field to indicate the specific type of notification.
type Notification struct {
Type string `json:"type"`
}
import "github.com/goccy/go-json"

// BroadcastNotification represents a notification for all
type BroadcastNotification struct {
Notification // Embedding Notification struct to inherit the Type field.
Notice string `json:"notice"`
type NotificationRedis struct {
NotificationId int64 `json:"notificationId" db:"NotificationId"`
UserId string `json:"userId" db:"UserId"`
NotificationType string `json:"type" db:"NotificationType"`
Title string `json:"title" db:"Title"`
Message string `json:"message" db:"Message"`
Metadata json.RawMessage `json:"metadata" db:"Metadata"`
}

// LikeNotification represents a notification for a like event.
type LikeNotification struct {
Notification // Embedding Notification struct to inherit the Type field.
UserID int `json:"userId"`
MarkerID int `json:"markerId"`
type NotificationMarkerMetadata struct {
MarkerID int64 `json:"markerID"`
Latitude float64 `json:"latitude"`
Longitude float64 `json:"longitude"`
Address string `json:"address"`
}

// CommentNotification represents a notification for a comment event.
type CommentNotification struct {
Notification // Embedding Notification struct to inherit the Type field.
UserID int `json:"userId"`
MarkerID int `json:"markerId"`
Comment string `json:"comment"`
type NotificationLikeMetadata struct {
MarkerID int `json:"markerID"`
UserId int `json:"userId"`
LikerId int `json:"likerId"`
}
7 changes: 7 additions & 0 deletions backend/dto/user_dto.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,10 @@ type UserData struct {
UserID int
Username string
}

type UserMarkers struct {
MarkersWithPhotos []MarkerSimpleWithDescrption `json:"markers"`
CurrentPage int `json:"currentPage"`
TotalPages int `json:"totalPages"`
TotalMarkers int `json:"totalMarkers"`
}
13 changes: 10 additions & 3 deletions backend/handlers/auth_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"chulbong-kr/services"
"chulbong-kr/utils"
"database/sql"
"fmt"
"log"
"os"
"strings"
Expand Down Expand Up @@ -176,6 +175,7 @@ func logoutHandler(c *fiber.Ctx) error {
// @Router /auth/send-verification-email [post]
func sendVerificationEmailHandler(c *fiber.Ctx) error {
userEmail := c.FormValue("email")
userEmail = strings.ToLower(userEmail)
_, err := services.GetUserByEmail(userEmail)
if err == nil {
// If GetUserByEmail does not return an error, it means the email is already in use
Expand All @@ -202,15 +202,22 @@ func sendVerificationEmailHandler(c *fiber.Ctx) error {

// Use a goroutine to send the email without blocking
go func(email string) {
if strings.HasSuffix(email, "@naver.com") { // endsWith
exist, _ := services.VerifyNaverEmail(email)
if !exist {
log.Printf("No such email: %s\n", email)
return
}
}
token, err := services.GenerateAndSaveSignUpToken(email)
if err != nil {
fmt.Printf("Failed to generate token: %v\n", err)
log.Printf("Failed to generate token: %v\n", err)
return
}

err = services.SendVerificationEmail(email, token)
if err != nil {
fmt.Printf("Failed to send verification email: %v\n", err)
log.Printf("Failed to send verification email: %v\n", err)
return
}
}(userEmail)
Expand Down
10 changes: 5 additions & 5 deletions backend/handlers/marker_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -410,11 +410,11 @@ func getUserMarkersHandler(c *fiber.Ctx) error {
totalPages := int(math.Ceil(float64(total) / float64(pageSize)))

// Prepare the response
response := fiber.Map{
"markers": markersWithPhotos,
"currentPage": page,
"totalPages": totalPages,
"totalMarkers": total,
response := dto.UserMarkers{
MarkersWithPhotos: markersWithPhotos,
CurrentPage: page,
TotalPages: totalPages,
TotalMarkers: total,
}

// Cache the response for future requests
Expand Down
130 changes: 130 additions & 0 deletions backend/handlers/notification_api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
package handlers

import (
"chulbong-kr/dto/notification"
"chulbong-kr/middlewares"
"chulbong-kr/services"
"log"
"strconv"

"github.com/goccy/go-json"
"github.com/gofiber/contrib/websocket"
"github.com/gofiber/fiber/v2"
"github.com/redis/rueidis"
)

// RegisterNotificationRoutes sets up the routes for Notification handling within the application.
func RegisterNotificationRoutes(api fiber.Router, websocketConfig websocket.Config) {
api.Get("/ws/notification", middlewares.AuthSoftMiddleware, func(c *fiber.Ctx) error {
if websocket.IsWebSocketUpgrade(c) {
return c.Next()
}
return fiber.ErrUpgradeRequired
}, websocket.New(func(c *websocket.Conn) {
var userId string

if id, ok := c.Locals("userID").(int); ok {
// Convert integer userID to string if it exists and is valid
userId = strconv.Itoa(id)
} else {
// anonymous users
userId = c.Query("request-id")
}

if userId == "" {
c.WriteJSON(fiber.Map{"error": "wrong user id"})
c.Close()
return
}

WsNotificationHandler(c, userId)
}, websocketConfig))

api.Post("/notification", middlewares.AdminOnly, PostNotificationHandler)
}

// PostNotificationHandler handles POST requests to send notifications
func PostNotificationHandler(c *fiber.Ctx) error {
var req notification.NotificationRedis
if err := c.BodyParser(&req); err != nil {
log.Printf("Error parsing request: %v", err)
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Bad request"})
}

// Call the PostNotification function to insert the notification into the database and publish to Redis
err := services.PostNotification(req.UserId, req.NotificationType, req.Title, req.Message, req.Metadata)
if err != nil {
log.Printf("Error posting notification: %v", err)
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to post notification"})
}

return c.Status(fiber.StatusOK).JSON(fiber.Map{"message": "Notification posted successfully"})
}

// user id can be anonymous too. it should check auth and if authenticated, use cookie value as userId
func WsNotificationHandler(c *websocket.Conn, userId string) {
// Fetch unviewed notifications at the start of the WebSocket connection
unviewedNotifications, err := services.GetNotifications(userId)
if err != nil {
log.Printf("Error fetching unviewed notifications: %v", err)
return
}
for _, notification := range unviewedNotifications {
jsonData, err := json.Marshal(notification)
if err != nil {
log.Printf("Error marshaling notification: %v", err)
continue
}
if err := c.WriteMessage(websocket.TextMessage, jsonData); err != nil {
log.Printf("Error sending unviewed notification to WebSocket: %v", err)
continue
}

// Mark notifications as viewed based on their type
services.MarkNotificationAsViewed(notification.NotificationId, notification.NotificationType, userId)
}

// Subscribe to Redis on a per-connection basis
cancelSubscription, err := services.SubscribeNotification(userId, func(msg rueidis.PubSubMessage) {
// This function will be called for each message received
if err := c.WriteMessage(websocket.TextMessage, []byte(msg.Message)); err != nil {
log.Printf("Error sending message to WebSocket: %v", err)
} else {
var notification notification.NotificationRedis
json.Unmarshal([]byte(msg.Message), &notification)
services.MarkNotificationAsViewed(notification.NotificationId, notification.NotificationType, userId)
}
})

if err != nil {
log.Printf("Error subscribing to notifications: %v", err)
return
}
defer cancelSubscription() // unsubscribe when the WebSocket closes

// Simple loop to keep the connection open and log any incoming messages which could include pings
for {
messageType, p, err := c.ReadMessage()
if err != nil {
log.Printf("WebSocket read error: %v", err)
break
}
log.Printf("Received message of type %d: %s", messageType, string(p))
}

// Keep-alive loop: handle ping/pong and ensure the connection stays open
// for {
// // if err := c.PingHandler()("ping"); err != nil {
// // log.Printf("Ping failed: %v", err)
// // break // Exit loop if ping fails
// // }
// if err := c.SetReadDeadline(time.Now().Add(time.Second * 300)); err != nil {
// break
// }
// // Wait for a pong response to keep the connection alive
// if _, _, err := c.ReadMessage(); err != nil {
// log.Printf("Error reading pong: %v", err)
// break
// }
// }
}
52 changes: 30 additions & 22 deletions backend/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"runtime"
"runtime/debug"
"strconv"
"strings"
"time"

"github.com/Alfex4936/tzf"
Expand Down Expand Up @@ -118,6 +119,25 @@ func main() {
setUpMiddlewares(app)

// API
websocketConfig := websocket.Config{
// Set the handshake timeout to a reasonable duration to prevent slowloris attacks.
HandshakeTimeout: 5 * time.Second,

Origins: []string{"https://test.k-pullup.com", "https://www.k-pullup.com"},

EnableCompression: true,

RecoverHandler: func(c *websocket.Conn) {
// Custom recover logic. By default, it logs the error and stack trace.
if r := recover(); r != nil {
fmt.Fprintf(os.Stderr, "WebSocket panic: %v\n", r)
debug.PrintStack()
c.WriteMessage(websocket.CloseMessage, []byte{})
c.Close()
}
},
}

app.Get("/ws/:markerID", func(c *fiber.Ctx) error {
// Extract markerID from the parameter
markerID := c.Params("markerID")
Expand Down Expand Up @@ -145,26 +165,9 @@ func main() {
markerID := c.Params("markerID")
reqID := c.Query("request-id")

// Now, the connection is already upgraded to WebSocket, and you've passed the ban check.
// Now, the connection is already upgraded to WebSocket, and passed the ban check.
handlers.HandleChatRoomHandler(c, markerID, reqID)
}, websocket.Config{
// Set the handshake timeout to a reasonable duration to prevent slowloris attacks.
HandshakeTimeout: 5 * time.Second,

Origins: []string{"https://test.k-pullup.com", "https://www.k-pullup.com"},

EnableCompression: true,

RecoverHandler: func(c *websocket.Conn) {
// Custom recover logic. By default, it logs the error and stack trace.
if r := recover(); r != nil {
fmt.Fprintf(os.Stderr, "WebSocket panic: %v\n", r)
debug.PrintStack()
c.WriteMessage(websocket.CloseMessage, []byte{})
c.Close()
}
},
}))
}, websocketConfig))

// HTML
app.Get("/main", func(c *fiber.Ctx) error {
Expand All @@ -180,6 +183,7 @@ func main() {
handlers.RegisterCommentRoutes(api)
handlers.RegisterTossPaymentRoutes(api)
handlers.RegisterReportRoutes(api)
handlers.RegisterNotificationRoutes(api, websocketConfig)

// Cron jobs
services.RunAllCrons()
Expand Down Expand Up @@ -269,9 +273,13 @@ func setUpMiddlewares(app *fiber.App) {

// Enable CORS for all routes
app.Use(cors.New(cors.Config{
AllowOrigins: "http://localhost:5173,https://chulbong-kr.vercel.app,https://www.k-pullup.com", // List allowed origins
AllowMethods: "GET,POST,PUT,DELETE,OPTIONS", // Explicitly list allowed methods
AllowHeaders: "*", // TODO: Allow specific headers
// AllowOrigins: "http://localhost:5173,https://chulbong-kr.vercel.app,https://www.k-pullup.com", // List allowed origins
AllowOriginsFunc: func(origin string) bool {
// Check if the origin is a subdomain of k-pullup.com
return strings.HasSuffix(origin, ".k-pullup.com") || origin == "https://www.k-pullup.com" || origin == "https://chulbong-kr.vercel.app" || origin == "http://localhost:5173"
},
AllowMethods: "GET,POST,PUT,DELETE,OPTIONS", // Explicitly list allowed methods
AllowHeaders: "*", // TODO: Allow specific headers
// ExposeHeaders: "Accept",
AllowCredentials: true,
}))
Expand Down
20 changes: 20 additions & 0 deletions backend/models/notification.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package models

import (
"time"

"github.com/goccy/go-json"
)

// Notification represents the schema of the Notifications table
type Notification struct {
NotificationId int64 `json:"notificationId" db:"NotificationId"`
UserId string `json:"userId" db:"UserId"`
NotificationType string `json:"notifi_type" db:"NotificationType"`
Title string `json:"title" db:"Title"`
Message string `json:"message" db:"Message"`
Metadata json.RawMessage `json:"metadata" db:"Metadata"`
Viewed bool `json:"-" db:"Viewed"`
CreatedAt time.Time `json:"-" db:"CreatedAt"`
UpdatedAt time.Time `json:"-" db:"UpdatedAt"`
}
13 changes: 13 additions & 0 deletions backend/services/marker_interaction_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,19 @@ func AddFavorite(userID, markerID int) error {
return fmt.Errorf("failed to retrieve marker owner: %w", err)
}

// if ownerUserID != userID {
// userIDstr := strconv.Itoa(ownerUserID)
// updateMsg := fmt.Sprintf("누군가 %d 마커에 좋아요를 남겼습니다!", markerID)
// metadata := notification.NotificationLikeMetadata{
// MarkerID: markerID,
// UserId: ownerUserID,
// LikerId: userID,
// }

// rawMetadata, _ := json.Marshal(metadata)
// PostNotification(userIDstr, "Like", "sys", updateMsg, rawMetadata)
// }

// TODO: update when frontend updates
// key := fmt.Sprintf("%d-%d", ownerUserID, markerID)
// PublishLikeEvent(key)
Expand Down
13 changes: 13 additions & 0 deletions backend/services/marker_management_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,19 @@ func CreateMarkerWithPhotos(markerDto *dto.MarkerRequest, userID int, form *mult
if err != nil {
log.Printf("Failed to update address for marker %d: %v", markerID, err)
}

// userIDstr := strconv.Itoa(userID)
// updateMsg := fmt.Sprintf("새로운 철봉이 [ %s ]에 등록되었습니다!", address)
// metadata := notification.NotificationMarkerMetadata{
// MarkerID: markerID,
// Latitude: latitude,
// Longitude: longitude,
// Address: address,
// }

// rawMetadata, _ := json.Marshal(metadata)
// PostNotification(userIDstr, "NewMarker", "k-pullup!", updateMsg, rawMetadata)

// TODO: update when frontend updates
// if address != "" {
// updateMsg := fmt.Sprintf("새로운 철봉이 등록되었습니다! %s", address)
Expand Down
Loading

0 comments on commit fc6aa00

Please sign in to comment.