diff --git a/internal/data/events.go b/internal/data/events.go index a7be2784..c5544fc2 100644 --- a/internal/data/events.go +++ b/internal/data/events.go @@ -42,6 +42,7 @@ const ( ConfigPrefix = "wag-config-" AuthenticationPrefix = "wag-config-authentication-" NodeEvents = "wag/node/" + NodeErrors = "wag/node/errors" ) var ( @@ -66,7 +67,7 @@ func RegisterEventListener[T any](path string, isPrefix bool, f func(key string, key, err := generateRandomBytes(16) if err != nil { - return "", nil + return "", err } ctx, cancel := context.WithCancel(context.Background()) diff --git a/ui/clustering.go b/ui/clustering.go index fc809771..a417227c 100644 --- a/ui/clustering.go +++ b/ui/clustering.go @@ -32,7 +32,7 @@ func clusterMembersUI(w http.ResponseWriter, r *http.Request) { CurrentNode string }{ Page: Page{ - Notification: getUpdate(), + Description: "Clustering Management Page", Title: "Clustering", User: u.Username, @@ -187,7 +187,7 @@ func clusterEventsUI(w http.ResponseWriter, r *http.Request) { Errors []data.EventError }{ Page: Page{ - Notification: getUpdate(), + Description: "Clustering Management Page", Title: "Clustering", User: u.Username, diff --git a/ui/daignostics.go b/ui/daignostics.go index a9fb57be..42938213 100644 --- a/ui/daignostics.go +++ b/ui/daignostics.go @@ -40,7 +40,7 @@ func firewallDiagnositicsUI(w http.ResponseWriter, r *http.Request) { XDPState string }{ Page: Page{ - Notification: getUpdate(), + Description: "Firewall state page", Title: "Firewall", User: u.Username, @@ -75,7 +75,7 @@ func wgDiagnositicsUI(w http.ResponseWriter, r *http.Request) { } d := Page{ - Notification: getUpdate(), + Description: "Wireguard Devices", Title: "wg", User: u.Username, diff --git a/ui/dashboard.go b/ui/dashboard.go index 750050ee..624afa02 100644 --- a/ui/dashboard.go +++ b/ui/dashboard.go @@ -93,7 +93,7 @@ func populateDashboard(w http.ResponseWriter, r *http.Request) { d := Dashboard{ Page: Page{ - Notification: getUpdate(), + Description: "Dashboard", Title: "Dashboard", User: u.Username, diff --git a/ui/devices.go b/ui/devices.go index 61c3f383..62105134 100644 --- a/ui/devices.go +++ b/ui/devices.go @@ -21,7 +21,7 @@ func devicesMgmtUI(w http.ResponseWriter, r *http.Request) { } d := Page{ - Notification: getUpdate(), + Description: "Devices Management Page", Title: "Devices", User: u.Username, diff --git a/ui/groups.go b/ui/groups.go index e3f06a86..ae4e97e1 100644 --- a/ui/groups.go +++ b/ui/groups.go @@ -21,7 +21,7 @@ func groupsUI(w http.ResponseWriter, r *http.Request) { } d := Page{ - Notification: getUpdate(), + Description: "Groups", Title: "Groups", User: u.Username, diff --git a/ui/notifications.go b/ui/notifications.go index 0ce7e8f2..a5992cf8 100644 --- a/ui/notifications.go +++ b/ui/notifications.go @@ -4,11 +4,14 @@ import ( "encoding/json" "log" "net/http" + "sort" "strings" + "sync" "time" "github.com/NHAS/wag/internal/data" "github.com/gorilla/websocket" + "golang.org/x/exp/maps" ) var upgrader = websocket.Upgrader{ @@ -21,19 +24,55 @@ type Acknowledgement struct { ID string } -func notificationsWS(w http.ResponseWriter, r *http.Request) { - // Upgrade HTTP connection to WebSocket connection - conn, err := upgrader.Upgrade(w, r, nil) - if err != nil { - log.Println(err) - return - } - defer conn.Close() +func notificationsWS(notifications <-chan Notification) func(w http.ResponseWriter, r *http.Request) { - go func() { + var mapLck sync.RWMutex + servingConnections := map[string]chan<- Notification{} + go func() { + for notification := range notifications { + for key := range servingConnections { + go func(key string, notification Notification) { + servingConnections[key] <- notification + }(key, notification) + } + } }() + return func(w http.ResponseWriter, r *http.Request) { + // Upgrade HTTP connection to WebSocket connection + conn, err := upgrader.Upgrade(w, r, nil) + if err != nil { + log.Println(err) + return + } + + connectionChan := make(chan Notification) + defer func() { + mapLck.Lock() + delete(servingConnections, r.RemoteAddr) + mapLck.Unlock() + + close(connectionChan) + conn.Close() + }() + + mapLck.Lock() + servingConnections[r.RemoteAddr] = connectionChan + mapLck.Unlock() + + for notf := range connectionChan { + conn.SetWriteDeadline(time.Now().Add(10 * time.Second)) + + err := conn.WriteJSON(notf) + if err != nil { + return + } + + conn.SetWriteDeadline(time.Time{}) + } + + } } type githubResponse struct { @@ -45,43 +84,78 @@ type githubResponse struct { } type Notification struct { - ID string - Heading string - Message []string - Url string + ID string + Heading string + Message []string + Url string + Time time.Time + Color string + OpenNewTab bool } var ( - mostRecentUpdate *Notification - lastChecked time.Time + notifications = map[string]Notification{} ) -func getUpdate() Notification { +func getNotifications() []Notification { - should, err := data.ShouldCheckUpdates() - if err != nil || !should { - return Notification{} - } + notfs := maps.Values(notifications) + sort.Slice(notfs, func(i, j int) bool { + return notfs[i].Time.After(notfs[j].Time) + }) - if time.Now().After(lastChecked.Add(15*time.Minute)) || mostRecentUpdate == nil { - resp, err := http.Get("https://api.github.com/repos/NHAS/wag/releases/latest") - if err != nil { - return Notification{} - } - defer resp.Body.Close() + return notfs +} - var gr githubResponse - err = json.NewDecoder(resp.Body).Decode(&gr) - if err != nil { - return Notification{} +func startUpdateChecker(notifications chan<- Notification) { + go func() { + + for { + resp, err := http.Get("https://api.github.com/repos/NHAS/wag/releases/latest") + if err != nil { + log.Println("unable to fetch updates: ", err) + return + } + defer resp.Body.Close() + + var gr githubResponse + err = json.NewDecoder(resp.Body).Decode(&gr) + if err != nil { + log.Println("unable to parse update json: ", err) + return + } + + notifications <- Notification{ + Heading: gr.TagName, + Message: strings.Split(gr.Body, "\r\n"), + Url: gr.Url, + Time: time.Now(), + OpenNewTab: true, + Color: "#0bb329", + } + + <-time.After(15 * time.Minute) } + }() +} + +func recieveErrorNotifications(notifications chan<- Notification) func(key string, current, previous data.EventError, et data.EventType) error { - mostRecentUpdate = &Notification{ - Heading: gr.TagName, - Message: strings.Split(gr.Body, "\r\n"), - Url: gr.Url, + return func(key string, current, previous data.EventError, et data.EventType) error { + if et == data.CREATED { + + msg := Notification{ + ID: current.ErrorID, + Heading: "Node Error", + Message: []string{"Node " + current.NodeID, current.Error}, + Url: "/cluster/events/", + Time: time.Now(), + OpenNewTab: false, + Color: "#db0b3c", + } + + notifications <- msg } + return nil } - - return *mostRecentUpdate } diff --git a/ui/policies.go b/ui/policies.go index 26f196b5..b47ee008 100644 --- a/ui/policies.go +++ b/ui/policies.go @@ -20,7 +20,7 @@ func policiesUI(w http.ResponseWriter, r *http.Request) { return } d := Page{ - Notification: getUpdate(), + Description: "Firewall rules", Title: "Rules", User: u.Username, diff --git a/ui/registration.go b/ui/registration.go index 6a883b25..93d9aaa3 100644 --- a/ui/registration.go +++ b/ui/registration.go @@ -21,7 +21,7 @@ func registrationUI(w http.ResponseWriter, r *http.Request) { } d := Page{ - Notification: getUpdate(), + Description: "Registration Tokens Management Page", Title: "Registration", User: u.Username, diff --git a/ui/settings.go b/ui/settings.go index d08c712d..626a41e0 100644 --- a/ui/settings.go +++ b/ui/settings.go @@ -22,7 +22,7 @@ func adminUsersUI(w http.ResponseWriter, r *http.Request) { } d := Page{ - Notification: getUpdate(), + Description: "Wag settings", Title: "Settings - Admin Users", User: u.Username, @@ -93,7 +93,7 @@ func generalSettingsUI(w http.ResponseWriter, r *http.Request) { MFAMethods []authenticators.Authenticator }{ Page: Page{ - Notification: getUpdate(), + Description: "Wag settings", Title: "Settings - General", User: u.Username, diff --git a/ui/src/js/notifications.js b/ui/src/js/notifications.js index 41e44329..b64c8dbf 100644 --- a/ui/src/js/notifications.js +++ b/ui/src/js/notifications.js @@ -1,14 +1,39 @@ + + + + + const httpsEnabled = window.location.protocol == "https:"; const url = (httpsEnabled ? 'wss://' : 'ws://') + window.location.host + "/notifications"; -var socket = new WebSocket(url) +let socket = new WebSocket(url) +const alertBadge = document.getElementById("numNotifications"); const dropDownlist = document.getElementById("notificationsDropDown"); -socket.onmessage = function (e) { - const msg = JSON.parse(e.data) +if (dropDownlist.querySelectorAll(".notification").length > 0) { + alertBadge.textContent = dropDownlist.querySelectorAll(".notification").length + alertBadge.hidden = false +} +socket.onmessage = function (e) { + const msg = JSON.parse(e.data) + Toastify({ + text: msg.Message.join('\n'), + className: "info", + destination: msg.Url, + newWindow: msg.OpenNewTab, + position: "right", + gravity: "top", + offset: { + y: 30, + }, + stopOnFocus: true, + style: { + background: msg.Color, + } + }).showToast(); } diff --git a/ui/structs.go b/ui/structs.go index 7b495d41..ca4601bf 100644 --- a/ui/structs.go +++ b/ui/structs.go @@ -1,7 +1,6 @@ package ui type Page struct { - Notification Description string Title string User string diff --git a/ui/templates/menus.html b/ui/templates/menus.html index 8f9bed58..ca825826 100755 --- a/ui/templates/menus.html +++ b/ui/templates/menus.html @@ -35,6 +35,7 @@ + @@ -226,6 +227,20 @@ Tools: + + {{range $val := notifications}} + + + {{$val.Time}} + {{$val.Heading}} + {{range $val := $val.Message}} + {{$val}} + {{end}} + + + {{end}} diff --git a/ui/ui_webserver.go b/ui/ui_webserver.go index 114474ac..bea42ceb 100644 --- a/ui/ui_webserver.go +++ b/ui/ui_webserver.go @@ -71,6 +71,9 @@ func render(w http.ResponseWriter, r *http.Request, model interface{}, content . return template.HTML(fmt.Sprintf("", functionalityName)) }, + "notifications": func() []Notification { + return getNotifications() + }, } if !config.Values.ManagementUI.Debug { @@ -221,7 +224,10 @@ func StartWebServer(errs chan<- error) error { } serverID = data.GetServerID() - data.RegisterClusterHealthListener(watchClusterHealth) + _, err = data.RegisterClusterHealthListener(watchClusterHealth) + if err != nil { + return err + } log.SetOutput(io.MultiWriter(os.Stdout, LogQueue)) @@ -316,7 +322,14 @@ func StartWebServer(errs chan<- error) error { protectedRoutes.HandleFunc("/settings/management_users", adminUsersUI) protectedRoutes.HandleFunc("/settings/management_users/data", adminUsersData) - protectedRoutes.HandleFunc("/notifications", notificationsWS) + notifications := make(chan Notification, 1) + protectedRoutes.HandleFunc("/notifications", notificationsWS(notifications)) + data.RegisterEventListener(data.NodeErrors, true, recieveErrorNotifications(notifications)) + + should, err := data.ShouldCheckUpdates() + if err == nil && should { + startUpdateChecker(notifications) + } protectedRoutes.HandleFunc("/change_password", changePassword) @@ -374,7 +387,7 @@ func changePassword(w http.ResponseWriter, r *http.Request) { d := ChangePassword{ Page: Page{ - Notification: getUpdate(), + Description: "Change password page", Title: "Change password", User: u.Username, diff --git a/ui/users.go b/ui/users.go index 2484819f..b6889e8d 100644 --- a/ui/users.go +++ b/ui/users.go @@ -23,7 +23,7 @@ func usersUI(w http.ResponseWriter, r *http.Request) { } d := Page{ - Notification: getUpdate(), + Description: "Users Management Page", Title: "Users", User: u.Username,
{{$val}}