Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
2 changes: 2 additions & 0 deletions server/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,9 @@ func (a *App) registerHandlers() {
invoiceRouter.HandleFunc("/pay/{id}", WrapFunc(a.PayInvoiceHandler)).Methods("PUT", "OPTIONS")

notificationRouter.HandleFunc("", WrapFunc(a.ListNotificationsHandler)).Methods("GET", "OPTIONS")
notificationRouter.HandleFunc("/stream", 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
143 changes: 122 additions & 21 deletions server/app/notification_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,79 @@
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"
)

// 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
// @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"))
}

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

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

// 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
// @Success 200 {object} Response
// @Failure 400 {object} Response
// @Failure 401 {object} Response
// @Failure 500 {object} Response
// @Router /notification [put]
func (a *App) SeenNotificationsHandler(req *http.Request) (interface{}, Response) {
userID := req.Context().Value(middlewares.UserIDKey("UserID")).(string)

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 seen",
Data: nil,
}, Ok()
}

// ListNotificationsHandler lists notifications for a user
// Example endpoint: Lists user's notifications
// @Summary Lists user's notifications
Expand Down Expand Up @@ -47,35 +110,73 @@ func (a *App) ListNotificationsHandler(req *http.Request) (interface{}, Response
}, Ok()
}

// 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
// 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
// @Param id path string true "Notification ID"
// @Success 200 {object} Response
// @Failure 400 {object} Response
// @Success 200 {object} []models.Notification
// @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/stream [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
}

err = a.db.UpdateNotification(id, true)
if err != nil {
log.Error().Err(err).Send()
return nil, InternalServerError(errors.New(internalServerErrorMsg))
// 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
}
}
}

return ResponseMsg{
Message: "Notifications are updated",
Data: nil,
}, Ok()
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")
}
}
81 changes: 80 additions & 1 deletion server/docs/docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -1008,6 +1008,81 @@ const docTemplate = `{
"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": {
"description": "Internal Server Error",
"schema": {}
}
}
}
},
"/notification/stream": {
"get": {
"security": [
{
"BearerAuth": []
}
],
"description": "Stream user's notifications",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Notification"
],
"summary": "Stream user's notifications",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/models.Notification"
}
}
},
"401": {
"description": "Unauthorized",
"schema": {}
},
"500": {
"description": "Internal Server Error",
"schema": {}
}
}
}
},
"/notification/{id}": {
Expand Down Expand Up @@ -3279,6 +3354,7 @@ const docTemplate = `{
"type": "object",
"required": [
"msg",
"notified",
"seen",
"type",
"user_id"
Expand All @@ -3290,11 +3366,14 @@ const docTemplate = `{
"msg": {
"type": "string"
},
"notified": {
"type": "boolean"
},
"seen": {
"type": "boolean"
},
"type": {
"description": "to allow redirecting from notifications to the right pages",
"description": "to allow redirecting from notifications to the right pages\nfor example if the type is ` + "`" + `vm` + "`" + ` it will be redirected to the vm page",
"type": "string"
},
"user_id": {
Expand Down
56 changes: 55 additions & 1 deletion server/docs/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -461,15 +461,20 @@ definitions:
type: integer
msg:
type: string
notified:
type: boolean
seen:
type: boolean
type:
description: to allow redirecting from notifications to the right pages
description: |-
to allow redirecting from notifications to the right pages
for example if the type is `vm` it will be redirected to the vm page
type: string
user_id:
type: string
required:
- msg
- notified
- seen
- type
- user_id
Expand Down Expand Up @@ -1296,6 +1301,30 @@ paths:
summary: Lists 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: Set user's notifications as seen
tags:
- Notification
/notification/{id}:
put:
consumes:
Expand Down Expand Up @@ -1327,6 +1356,31 @@ paths:
summary: Set user's notifications as seen
tags:
- Notification
/notification/stream:
get:
consumes:
- application/json
description: Stream user's notifications
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/models.Notification'
type: array
"401":
description: Unauthorized
schema: {}
"500":
description: Internal Server Error
schema: {}
security:
- BearerAuth: []
summary: Stream user's notifications
tags:
- Notification
/region:
get:
consumes:
Expand Down
Loading
Loading