Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion server/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,8 +158,9 @@ func (a *App) registerHandlers() {
invoiceRouter.HandleFunc("/{id}", WrapFunc(a.GetInvoiceHandler)).Methods("GET", "OPTIONS")
invoiceRouter.HandleFunc("/pay/{id}", WrapFunc(a.PayInvoiceHandler)).Methods("PUT", "OPTIONS")

notificationRouter.HandleFunc("", WrapFunc(a.ListNotificationsHandler)).Methods("GET", "OPTIONS")
notificationRouter.HandleFunc("", a.sseNotificationsHandler).Methods("GET", "OPTIONS")
notificationRouter.HandleFunc("/{id}", WrapFunc(a.UpdateNotificationsHandler)).Methods("PUT", "OPTIONS")
notificationRouter.HandleFunc("", WrapFunc(a.SeenNotificationsHandler)).Methods("PUT", "OPTIONS")

regionRouter.HandleFunc("", WrapFunc(a.ListRegionsHandler)).Methods("GET", "OPTIONS")

Expand Down
125 changes: 95 additions & 30 deletions server/app/notification_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,80 +2,145 @@
package app

import (
"encoding/json"
"errors"
"net/http"
"strconv"
"time"

"github.com/codescalers/cloud4students/middlewares"
"github.com/gorilla/mux"
"github.com/rs/zerolog/log"
"gorm.io/gorm"
)

// ListNotificationsHandler lists notifications for a user
// Example endpoint: Lists user's notifications
// @Summary Lists user's notifications
// @Description Lists user's notifications
// UpdateNotificationsHandler updates notifications for a user
// Example endpoint: Set user's notifications as seen
// @Summary Set user's notifications as seen
// @Description Set user's notifications as seen
// @Tags Notification
// @Accept json
// @Produce json
// @Security BearerAuth
// @Success 200 {object} []models.Notification
// @Param id path string true "Notification ID"
// @Success 200 {object} Response
// @Failure 400 {object} Response
// @Failure 401 {object} Response
// @Failure 404 {object} Response
// @Failure 500 {object} Response
// @Router /notification [get]
func (a *App) ListNotificationsHandler(req *http.Request) (interface{}, Response) {
userID := req.Context().Value(middlewares.UserIDKey("UserID")).(string)

notifications, err := a.db.ListNotifications(userID)
if errors.Is(err, gorm.ErrRecordNotFound) || len(notifications) == 0 {
return ResponseMsg{
Message: "You don't have any notifications yet",
Data: notifications,
}, Ok()
// @Router /notification/{id} [put]
func (a *App) UpdateNotificationsHandler(req *http.Request) (interface{}, Response) {
id, err := strconv.Atoi(mux.Vars(req)["id"])
if err != nil {
log.Error().Err(err).Send()
return nil, BadRequest(errors.New("failed to read notification id"))
}

err = a.db.UpdateNotification(id, true)
if err != nil {
log.Error().Err(err).Send()
return nil, InternalServerError(errors.New(internalServerErrorMsg))
}

return ResponseMsg{
Message: "You have notifications",
Data: notifications,
Message: "Notifications are updated",
Data: nil,
}, Ok()
}

// UpdateNotificationsHandler updates notifications for a user
// SeenNotificationsHandler updates notifications for a user to be seen
// Example endpoint: Set user's notifications as seen
// @Summary Set user's notifications as seen
// @Description Set user's notifications as seen
// @Tags Notification
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path string true "Notification ID"
// @Success 200 {object} Response
// @Failure 400 {object} Response
// @Failure 401 {object} Response
// @Failure 500 {object} Response
// @Router /notification/{id} [put]
func (a *App) UpdateNotificationsHandler(req *http.Request) (interface{}, Response) {
id, err := strconv.Atoi(mux.Vars(req)["id"])
if err != nil {
log.Error().Err(err).Send()
return nil, BadRequest(errors.New("failed to read notification id"))
}
// @Router /notification [put]
func (a *App) SeenNotificationsHandler(req *http.Request) (interface{}, Response) {
userID := req.Context().Value(middlewares.UserIDKey("UserID")).(string)

err = a.db.UpdateNotification(id, true)
err := a.db.UpdateUserNotification(userID, true)
if err != nil {
log.Error().Err(err).Send()
return nil, InternalServerError(errors.New(internalServerErrorMsg))
}

return ResponseMsg{
Message: "Notifications are updated",
Message: "Notifications are seen",
Data: nil,
}, Ok()
}

// sseNotificationsHandler to stream notifications
// Example endpoint: Stream user's notifications
// @Summary Stream user's notifications
// @Description Stream user's notifications
// @Tags Notification
// @Accept json
// @Produce json
// @Security BearerAuth
// @Success 200 {object} []models.Notification
// @Failure 401 {object} Response
// @Failure 500 {object} Response
// @Router /notification [get]
func (a *App) sseNotificationsHandler(w http.ResponseWriter, req *http.Request) {
userID := req.Context().Value(middlewares.UserIDKey("UserID")).(string)

w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")

// Flush the headers immediately
flusher, ok := w.(http.Flusher)
if !ok {
log.Error().Msg("Streaming unsupported")
internalServerError(w)
return
}

// Sending notifications every 5 seconds
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()

for {
select {
case <-ticker.C:
notifications, err := a.db.GetNewNotifications(userID)
if err != nil {
log.Error().Err(err).Send()
internalServerError(w)
return
}

// Send each notification as a separate SSE message
for _, notification := range notifications {
if _, err := w.Write([]byte(notification.Msg)); err != nil {
log.Error().Err(err).Send()
internalServerError(w)
return
}
flusher.Flush() // Ensure the event is sent immediately
}

case <-req.Context().Done():
w.WriteHeader(http.StatusOK)
return
}
}
}

func internalServerError(w http.ResponseWriter) {
w.WriteHeader(http.StatusInternalServerError)
object := struct {
Error string `json:"err"`
}{
Error: "Internal server error",
}

if err := json.NewEncoder(w).Encode(object); err != nil {
log.Error().Err(err).Msg("failed to encode return object")
}
}
44 changes: 40 additions & 4 deletions server/docs/docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -974,7 +974,7 @@ const docTemplate = `{
"BearerAuth": []
}
],
"description": "Lists user's notifications",
"description": "Stream user's notifications",
"consumes": [
"application/json"
],
Expand All @@ -984,7 +984,7 @@ const docTemplate = `{
"tags": [
"Notification"
],
"summary": "Lists user's notifications",
"summary": "Stream user's notifications",
"responses": {
"200": {
"description": "OK",
Expand All @@ -999,8 +999,40 @@ const docTemplate = `{
"description": "Unauthorized",
"schema": {}
},
"404": {
"description": "Not Found",
"500": {
"description": "Internal Server Error",
"schema": {}
}
}
},
"put": {
"security": [
{
"BearerAuth": []
}
],
"description": "Set user's notifications as seen",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Notification"
],
"summary": "Set user's notifications as seen",
"responses": {
"200": {
"description": "OK",
"schema": {}
},
"400": {
"description": "Bad Request",
"schema": {}
},
"401": {
"description": "Unauthorized",
"schema": {}
},
"500": {
Expand Down Expand Up @@ -3279,6 +3311,7 @@ const docTemplate = `{
"type": "object",
"required": [
"msg",
"notified",
"seen",
"type",
"user_id"
Expand All @@ -3290,6 +3323,9 @@ const docTemplate = `{
"msg": {
"type": "string"
},
"notified": {
"type": "boolean"
},
"seen": {
"type": "boolean"
},
Expand Down
32 changes: 28 additions & 4 deletions server/docs/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,8 @@ definitions:
type: integer
msg:
type: string
notified:
type: boolean
seen:
type: boolean
type:
Expand All @@ -470,6 +472,7 @@ definitions:
type: string
required:
- msg
- notified
- seen
- type
- user_id
Expand Down Expand Up @@ -1272,7 +1275,7 @@ paths:
get:
consumes:
- application/json
description: Lists user's notifications
description: Stream user's notifications
produces:
- application/json
responses:
Expand All @@ -1285,15 +1288,36 @@ paths:
"401":
description: Unauthorized
schema: {}
"404":
description: Not Found
"500":
description: Internal Server Error
schema: {}
security:
- BearerAuth: []
summary: Stream user's notifications
tags:
- Notification
put:
consumes:
- application/json
description: Set user's notifications as seen
produces:
- application/json
responses:
"200":
description: OK
schema: {}
"400":
description: Bad Request
schema: {}
"401":
description: Unauthorized
schema: {}
"500":
description: Internal Server Error
schema: {}
security:
- BearerAuth: []
summary: Lists user's notifications
summary: Set user's notifications as seen
tags:
- Notification
/notification/{id}:
Expand Down
25 changes: 21 additions & 4 deletions server/models/notification.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@ const (

// Notification struct holds data of notifications
type Notification struct {
ID int `json:"id" gorm:"primaryKey"`
UserID string `json:"user_id" binding:"required"`
Msg string `json:"msg" binding:"required"`
Seen bool `json:"seen" binding:"required"`
ID int `json:"id" gorm:"primaryKey"`
UserID string `json:"user_id" binding:"required"`
Msg string `json:"msg" binding:"required"`
Seen bool `json:"seen" binding:"required"`
Notified bool `json:"notified" binding:"required"`
// to allow redirecting from notifications to the right pages
Type string `json:"type" binding:"required"`

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What could be the types and how do they behave?

}
Expand All @@ -25,11 +26,27 @@ func (d *DB) ListNotifications(userID string) ([]Notification, error) {
return res, query.Error
}

// GetNewNotifications returns a list of new notifications for a user.
func (d *DB) GetNewNotifications(userID string) ([]Notification, error) {
var res []Notification
query := d.db.Where("user_id = ?", userID).Where("notified = ?", false).Find(&res)
if query.Error != nil {
return nil, query.Error
}

return res, d.db.Model(&Notification{}).Where("user_id = ?", userID).Updates(map[string]interface{}{"notified": true}).Error
}

// UpdateNotification updates seen field for notification
func (d *DB) UpdateNotification(id int, seen bool) error {
return d.db.Model(&Notification{}).Where("id = ?", id).Updates(map[string]interface{}{"seen": seen}).Error
}

// UpdateUserNotification updates seen field for user notifications
func (d *DB) UpdateUserNotification(userID string, seen bool) error {
return d.db.Model(&Notification{}).Where("user_id = ?", userID).Updates(map[string]interface{}{"seen": seen}).Error
}

// CreateNotification adds a new notification for a user
func (d *DB) CreateNotification(n *Notification) error {
return d.db.Create(&n).Error
Expand Down