diff --git a/modules/database/database.go b/modules/database/database.go index 4625cc2..af6732c 100644 --- a/modules/database/database.go +++ b/modules/database/database.go @@ -44,4 +44,5 @@ func InitDatabase() { db.AutoMigrate(&structs.RolePermission{}) db.AutoMigrate(&structs.UserApiKey{}) db.AutoMigrate(&structs.EquipmentDocumentation{}) + db.AutoMigrate(&structs.Notification{}) } diff --git a/modules/notification/notification.go b/modules/notification/notification.go new file mode 100644 index 0000000..7ed5e24 --- /dev/null +++ b/modules/notification/notification.go @@ -0,0 +1,127 @@ +package notification + +import ( + "jannex/admin-dashboard-backend/modules/database" + "jannex/admin-dashboard-backend/modules/structs" + "jannex/admin-dashboard-backend/modules/utils" + "jannex/admin-dashboard-backend/socketclients" + "time" + + "github.com/gofiber/fiber/v2" + "github.com/google/uuid" + "github.com/rs/zerolog/log" +) + +func GetTotalNotifications(userId string) int { + var totalNotifications int64 + + database.DB.Model(&structs.Notification{}).Where("user_id = ?", userId).Count(&totalNotifications) + + return int(totalNotifications) +} + +func GetNotifications(userId string) []structs.Notification { + var notifications []structs.Notification + + database.DB.Model(&structs.Notification{}).Where("user_id = ?", userId).Find(¬ifications) + + return notifications +} + +func AddNotification(c *fiber.Ctx, body structs.AddNotificationRequest) error { + log.Info().Msgf("body %v", body) + + var userIds []string + + if len(body.UserIds) == 0 && body.NeededPermission == "" && len(body.RoleIds) == 0 { + return c.SendStatus(fiber.StatusBadRequest) + } + + if len(body.UserIds) > 0 { + // check if user with provided user id in body.UserIds exist in database and add to userIds + database.DB.Model(&structs.User{}).Where("id IN ?", body.UserIds).Select("id").Find(&userIds) + } + + var roleIds []string + + if body.NeededPermission != "" { + // fetch all role_ids from role_permissions and use the role_id to get all users which are in the role from database which have the permission provided in body.NeededPermission and add to userIds + database.DB.Model(&structs.RolePermission{}).Where("permission_id = ?", body.NeededPermission).Select("role_id").Find(&roleIds) + + var userIdsTemp []string + + database.DB.Model(&structs.User{}).Where("role_id IN ?", roleIds).Select("id").Find(&userIdsTemp) + + userIds = append(userIds, userIdsTemp...) + } + + if len(body.RoleIds) > 0 { + var userIdsTemp []string + + database.DB.Model(&structs.User{}).Where("role_id IN ?", body.RoleIds).Select("id").Find(&userIdsTemp) + + userIds = append(userIds, userIdsTemp...) + } + + // no users found + if len(userIds) == 0 { + return c.SendStatus(fiber.StatusUnprocessableEntity) + } + + // remove duplicates + userIds = unique(userIds) + + // add notification to database + for _, userId := range userIds { + notify := structs.Notification{ + Id: uuid.New().String(), + UserId: userId, + Type: body.Type, + Title: body.Title, + CreatedAt: time.Now(), + } + + database.DB.Create(¬ify) + + // send notification to user + socketclients.SendMessageToUser(userId, "", structs.SendSocketMessage{ + Cmd: utils.SentCmdNewNotification, + Body: notify, + }) + } + + log.Info().Msgf("ids %v", userIds) + + return c.SendStatus(fiber.StatusOK) +} + +func unique(slice []string) []string { + keys := make(map[string]bool) + list := []string{} + + for _, entry := range slice { + if _, value := keys[entry]; !value { + keys[entry] = true + list = append(list, entry) + } + } + + return list +} + +func DeleteAllNotifications(userId string) { + database.DB.Delete(&structs.Notification{}, "user_id = ?", userId) + + socketclients.SendMessageToUser(userId, "", structs.SendSocketMessage{ + Cmd: utils.SentCmdAllNotificationsDeleted, + }) +} + +func DeleteOneNotification(userId string, notificationId string) { + database.DB.Delete(&structs.Notification{}, "user_id = ? AND id = ?", userId, notificationId) + + socketclients.SendMessageToUser(userId, "", structs.SendSocketMessage{ + Cmd: utils.SentCmdOneNotificationDeleted, + Body: notificationId, + }) +} diff --git a/modules/structs/notification.go b/modules/structs/notification.go new file mode 100644 index 0000000..2baf79c --- /dev/null +++ b/modules/structs/notification.go @@ -0,0 +1,29 @@ +package structs + +import "time" + +type Notification struct { + Id string + // success = 1, info = 2, warning = 3, error = 4 + Type uint8 + UserId string + Title string + CreatedAt time.Time +} + +// swagger:model AddNotificationRequest +type AddNotificationRequest struct { + Type uint8 + // send notification to specific users + UserIds []string + // send notification to users with specific permission + NeededPermission string + // send notification to users with specific role + RoleIds []string + Title string +} + +// swagger:model NotificationsResponse +type NotificationsResponse struct { + Notifications []Notification +} diff --git a/modules/structs/user.go b/modules/structs/user.go index 5ef82fa..6ce7bab 100644 --- a/modules/structs/user.go +++ b/modules/structs/user.go @@ -64,6 +64,7 @@ type UserInfoResponse struct { Permissions []string AvailableCategoryGroups []string Users []AllUsers + TotalNotifications int } // swagger:model UserProfileResponse diff --git a/modules/utils/globals.go b/modules/utils/globals.go index 2c8d728..17046a3 100644 --- a/modules/utils/globals.go +++ b/modules/utils/globals.go @@ -90,6 +90,9 @@ const ( SentCmdInstallingGlobalPythonPackagesFinished = 38 SentCmdUpdateUsers = 39 SentCmdCheckingForGroupTasksCategoryGroupChanges = 40 + SentCmdNewNotification = 41 + SentCmdAllNotificationsDeleted = 42 + SentCmdOneNotificationDeleted = 43 ) // commands received from web clients @@ -117,6 +120,8 @@ const ( ReceivedCmdGroupTasksInstallPythonPackages = 21 ReceivedCmdGroupTasksInstallGlobalPythonPackages = 22 ReceivedCmdSubscribeToTopic = 23 + ReceivedCmdDeleteAllNotifications = 24 + ReceivedCmdDeleteOneNotification = 25 ) const ( diff --git a/public/swagger/swagger.json b/public/swagger/swagger.json index f327448..55462de 100644 --- a/public/swagger/swagger.json +++ b/public/swagger/swagger.json @@ -416,6 +416,86 @@ } } }, + "/notifications": { + "get": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "notifications" + ], + "summary": "Get notifications", + "operationId": "notificationsGetNotifications", + "parameters": [ + { + "description": "You can create a new api key in your user profile", + "name": "X-Api-Key", + "in": "header" + } + ], + "responses": { + "200": { + "description": "Notifications", + "schema": { + "$ref": "#/definitions/NotificationsResponse" + } + }, + "401": { + "description": "No permissions" + }, + "500": { + "description": "Failed to get notifications" + } + } + }, + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "notifications" + ], + "summary": "Add a new notification", + "operationId": "notificationsAddNotification", + "parameters": [ + { + "description": "You can create a new api key in your user profile", + "name": "X-Api-Key", + "in": "header" + }, + { + "name": "body", + "in": "body", + "schema": { + "$ref": "#/definitions/AddNotificationRequest" + } + } + ], + "responses": { + "200": { + "description": "New notification added successfully" + }, + "400": { + "description": "Invalid request body" + }, + "401": { + "description": "No permissions" + }, + "422": { + "description": "No users found" + }, + "500": { + "description": "Failed to add notification" + } + } + } + }, "/user/auth/login": { "post": { "consumes": [ @@ -686,6 +766,37 @@ } }, "definitions": { + "AddNotificationRequest": { + "type": "object", + "properties": { + "NeededPermission": { + "description": "send notification to users with specific permission", + "type": "string" + }, + "RoleIds": { + "description": "send notification to users with specific role", + "type": "array", + "items": { + "type": "string" + } + }, + "Title": { + "type": "string" + }, + "Type": { + "type": "integer", + "format": "uint8" + }, + "UserIds": { + "description": "send notification to specific users", + "type": "array", + "items": { + "type": "string" + } + } + }, + "x-go-package": "jannex/admin-dashboard-backend/modules/structs" + }, "AdminAreaRolesResponse": { "type": "object", "properties": { @@ -1058,6 +1169,42 @@ "x-go-name": "GroupTaskStepsRequest", "x-go-package": "jannex/admin-dashboard-backend/modules/structs" }, + "Notification": { + "type": "object", + "properties": { + "CreatedAt": { + "type": "string", + "format": "date-time" + }, + "Id": { + "type": "string" + }, + "Title": { + "type": "string" + }, + "Type": { + "description": "success = 1, info = 2, warning = 3, error = 4", + "type": "integer", + "format": "uint8" + }, + "UserId": { + "type": "string" + } + }, + "x-go-package": "jannex/admin-dashboard-backend/modules/structs" + }, + "NotificationsResponse": { + "type": "object", + "properties": { + "Notifications": { + "type": "array", + "items": { + "$ref": "#/definitions/Notification" + } + } + }, + "x-go-package": "jannex/admin-dashboard-backend/modules/structs" + }, "Role": { "type": "object", "properties": { @@ -1206,6 +1353,10 @@ "type": "string" } }, + "TotalNotifications": { + "type": "integer", + "format": "int64" + }, "UserId": { "type": "string" }, diff --git a/routers/router/api/v1/notification/notification.go b/routers/router/api/v1/notification/notification.go new file mode 100644 index 0000000..2bfc139 --- /dev/null +++ b/routers/router/api/v1/notification/notification.go @@ -0,0 +1,73 @@ +package notification + +import ( + "jannex/admin-dashboard-backend/modules/notification" + "jannex/admin-dashboard-backend/modules/structs" + "jannex/admin-dashboard-backend/modules/utils" + + "github.com/gofiber/fiber/v2" +) + +func AddNotification(c *fiber.Ctx) error { + // swagger:operation POST /notifications notifications notificationsAddNotification + // --- + // summary: Add a new notification + // consumes: + // - application/json + // produces: + // - application/json + // parameters: + // - name: X-Api-Key + // in: header + // description: You can create a new api key in your user profile + // - name: body + // in: body + // schema: + // "$ref": "#/definitions/AddNotificationRequest" + // responses: + // '200': + // description: New notification added successfully + // '400': + // description: Invalid request body + // '401': + // description: No permissions + // '422': + // description: No users found + // '500': + // description: Failed to add notification + + var body structs.AddNotificationRequest + + if err := utils.BodyParserHelper(c, &body); err != nil { + return c.SendStatus(fiber.StatusBadRequest) + } + + return notification.AddNotification(c, body) +} + +func GetNotifications(c *fiber.Ctx) error { + // swagger:operation GET /notifications notifications notificationsGetNotifications + // --- + // summary: Get notifications + // consumes: + // - application/json + // produces: + // - application/json + // parameters: + // - name: X-Api-Key + // in: header + // description: You can create a new api key in your user profile + // responses: + // '200': + // description: Notifications + // schema: + // "$ref": "#/definitions/NotificationsResponse" + // '401': + // description: No permissions + // '500': + // description: Failed to get notifications + + return c.JSON(structs.NotificationsResponse{ + Notifications: notification.GetNotifications(c.Locals("userId").(string)), + }) +} diff --git a/routers/router/api/v1/user/user.go b/routers/router/api/v1/user/user.go index dc1f1da..cb179f7 100644 --- a/routers/router/api/v1/user/user.go +++ b/routers/router/api/v1/user/user.go @@ -3,6 +3,7 @@ package user import ( "jannex/admin-dashboard-backend/modules/cache" "jannex/admin-dashboard-backend/modules/database" + "jannex/admin-dashboard-backend/modules/notification" "jannex/admin-dashboard-backend/modules/structs" "jannex/admin-dashboard-backend/modules/utils" "jannex/admin-dashboard-backend/socketclients" @@ -55,5 +56,6 @@ func UserInfo(c *fiber.Ctx) error { Permissions: socketclients.GetPermissionsByRoleId(user.RoleId), AvailableCategoryGroups: categories, Users: socketclients.GetAllUsers(), + TotalNotifications: notification.GetTotalNotifications(user.Id), }) } diff --git a/routers/router/router.go b/routers/router/router.go index 1cb2e59..bc77717 100644 --- a/routers/router/router.go +++ b/routers/router/router.go @@ -10,6 +10,7 @@ import ( "jannex/admin-dashboard-backend/routers/router/api/v1/equipment" "jannex/admin-dashboard-backend/routers/router/api/v1/grouptasks" log "jannex/admin-dashboard-backend/routers/router/api/v1/logger" + "jannex/admin-dashboard-backend/routers/router/api/v1/notification" "jannex/admin-dashboard-backend/routers/router/api/v1/user" "jannex/admin-dashboard-backend/routers/router/api/v1/users" "jannex/admin-dashboard-backend/socketclients" @@ -56,6 +57,10 @@ func SetupRoutes(app *fiber.App) { a := v1.Group("/adminarea") a.Get("/roles", requestAccessValidation, adminarea.GetRoles) + ns := v1.Group("/notifications") + ns.Get("/", requestAccessValidation, notification.GetNotifications) + ns.Post("/", requestAccessValidation, notification.AddNotification) + app.Static("/", config.Cfg.FolderPaths.PublicStatic) } diff --git a/socketclients/socketclients.go b/socketclients/socketclients.go index cc04606..2ec4138 100644 --- a/socketclients/socketclients.go +++ b/socketclients/socketclients.go @@ -51,7 +51,6 @@ func BroadcastMessageToTopicExceptUserSessionId(topic string, ignoreUserSessionI } func hasClientSubscribedToTopic(topic string, clientTopic string) bool { - log.Info().Msgf("hasClientSubscribedToTopic %s %s", topic, clientTopic) return clientTopic == topic || strings.HasPrefix(clientTopic, topic) } diff --git a/socketserver/hub.go b/socketserver/hub.go index 99df748..29aa415 100644 --- a/socketserver/hub.go +++ b/socketserver/hub.go @@ -7,6 +7,7 @@ import ( "jannex/admin-dashboard-backend/modules/database" "jannex/admin-dashboard-backend/modules/grouptasks" "jannex/admin-dashboard-backend/modules/logger" + "jannex/admin-dashboard-backend/modules/notification" "jannex/admin-dashboard-backend/modules/structs" "jannex/admin-dashboard-backend/modules/systempermissions" "jannex/admin-dashboard-backend/modules/utils" @@ -356,6 +357,11 @@ func RunHub() { cache.SubscribeSocketClientToTopic(data.Conn.Locals("sessionId").(string), receivedMessage.Body["topic"].(string)) break + case utils.ReceivedCmdDeleteAllNotifications: + notification.DeleteAllNotifications(data.Conn.Locals("userId").(string)) + break + case utils.ReceivedCmdDeleteOneNotification: + notification.DeleteOneNotification(data.Conn.Locals("userId").(string), receivedMessage.Body["notificationId"].(string)) default: log.Error().Msgf("Received unknown message: %v", receivedMessage) break