From 11570f34f64e569c18bddf80016b7e506ac33585 Mon Sep 17 00:00:00 2001 From: NickJ-7010 Date: Mon, 5 Oct 2026 13:46:13 -0400 Subject: [PATCH 1/2] feat: Add SSE support for new polls being created --- README.md | 12 +++++++++++- api.go | 14 ++++++++++++++ main.go | 13 ++++++++++++- sse/broker.go | 34 +++++++++++++++++++++++++++++----- templates/index.tmpl | 41 +++++++++++++++++++++++++++++++++++------ 5 files changed, 101 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index a692756..a7350cb 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,16 @@ Copy `.env.example` to `.env` and fill out the info `DEV_FORCE_IS_EBOARD="true"` will force vote to treat all users as E-Board members `DEV_FORCE_IS_EVALS="true"` will force vote to treat all users as the Evals director +## Running Locally + +This project can be easily run using either `docker` or `podman` + +The run command is the same for both, just replace `podman` with `docker` if you perfer to use docker over podman + +```sh +podman compose up +``` + ## Linting These will be checked by CI @@ -40,7 +50,7 @@ go vet *.go - [ ] Don't let the user fuck it up - [ ] Show E-Board polls with a higher priority -- [x] Move Hide Vote to create instead of after you vote :skull: +- [X] Move Hide Vote to create instead of after you vote :skull: - [X] Display the reason why a user is on the results page of a running poll - [ ] Display minimum time left that a poll is open - [ ] Move routes to their own functions diff --git a/api.go b/api.go index 8fe6c7c..05ba13a 100644 --- a/api.go +++ b/api.go @@ -224,6 +224,20 @@ func CreatePoll(c *gin.Context) { return } + poll.Id = pollId + + if !(poll.Gatekeep && poll.HideForIneligible) { + broker.Notifier <- sse.NotificationEvent{ + EventName: "new-polls-non-gatekeep", + Payload: poll, + } + } + + broker.Notifier <- sse.NotificationEvent{ + EventName: "new-polls", + Payload: poll, + } + c.Redirect(http.StatusFound, "/poll/"+pollId) } diff --git a/main.go b/main.go index e53aad4..1b492ef 100644 --- a/main.go +++ b/main.go @@ -36,6 +36,13 @@ func MakeLinks(s string) template.HTML { return template.HTML(safe) } +// Checks whether the given user has met the gatekeep requirements. +func checkUserGatekeep(username string) bool { + user := &OIDCUser{Username: username} + oidcClient.GetUserGatekeep(user) + return user.Gatekeep +} + var oidcClient = OIDCClient{} var broker *sse.Broker @@ -49,7 +56,11 @@ func main() { "MakeLinks": MakeLinks, }) r.LoadHTMLGlob("templates/*") - broker = sse.NewBroker() + broker = sse.NewBroker( + GetUserData, + IsActive, + checkUserGatekeep, + ) csh := cshAuth.CSHAuth{} csh.Init( diff --git a/sse/broker.go b/sse/broker.go index 464b333..23ec17f 100644 --- a/sse/broker.go +++ b/sse/broker.go @@ -26,8 +26,10 @@ package sse import ( "io" "log" + "net/http" "time" + cshAuth "github.com/computersciencehouse/csh-auth" "github.com/gin-gonic/gin" ) @@ -54,22 +56,44 @@ type ( // Client connections registry clients map[NotifierChan]struct{} + + // Callbacks for cross-package access to main package functions + GetUser func(*gin.Context) cshAuth.CSHUserInfo + IsActive func(cshAuth.CSHUserInfo) bool + CheckUserGatekeep func(username string) bool } ) -func NewBroker() (broker *Broker) { +func NewBroker(getUser func(*gin.Context) cshAuth.CSHUserInfo, isActive func(cshAuth.CSHUserInfo) bool, checkUserGatekeep func(username string) bool) (broker *Broker) { // Instantiate a broker return &Broker{ - Notifier: make(NotifierChan, 1), - newClients: make(chan NotifierChan), - closingClients: make(chan NotifierChan), - clients: make(map[NotifierChan]struct{}), + Notifier: make(NotifierChan, 1), + newClients: make(chan NotifierChan), + closingClients: make(chan NotifierChan), + clients: make(map[NotifierChan]struct{}), + GetUser: getUser, + IsActive: isActive, + CheckUserGatekeep: checkUserGatekeep, } } func (broker *Broker) ServeHTTP(c *gin.Context) { eventName := c.Param("topic") + if eventName == "new-polls" { + user := broker.GetUser(c) + + if broker.IsActive(user) { + if !broker.CheckUserGatekeep(user.Username) { + eventName = "new-polls-non-gatekeep" + } + } else { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized SSE connection"}) + c.Abort() + return + } + } + // Each connection registers its own message channel with the Broker's connections registry messageChan := make(NotifierChan) diff --git a/templates/index.tmpl b/templates/index.tmpl index 640e358..a1f7dc2 100644 --- a/templates/index.tmpl +++ b/templates/index.tmpl @@ -18,18 +18,47 @@ class="list-group-item list-group-item-action p-3" href="/poll/{{ $poll.Id }}" > - - {{ $poll.Title }} - + + {{ $poll.Title }} + - (created by {{ $poll.CreatedBy }}) + (created by {{ $poll.CreatedBy }}) {{ end }} + From 7beb25060d5c7928788285d0dc3fde8ba12005cc Mon Sep 17 00:00:00 2001 From: NickJ-7010 Date: Wed, 7 Oct 2026 15:16:45 -0400 Subject: [PATCH 2/2] fix: Determine SSE eligibility on a per-user per-poll basis based on the poll data --- api.go | 7 ------- main.go | 9 -------- sse/broker.go | 49 ++++++++++++++++++++------------------------ templates/index.tmpl | 1 - 4 files changed, 22 insertions(+), 44 deletions(-) diff --git a/api.go b/api.go index 05ba13a..2f7171f 100644 --- a/api.go +++ b/api.go @@ -226,13 +226,6 @@ func CreatePoll(c *gin.Context) { poll.Id = pollId - if !(poll.Gatekeep && poll.HideForIneligible) { - broker.Notifier <- sse.NotificationEvent{ - EventName: "new-polls-non-gatekeep", - Payload: poll, - } - } - broker.Notifier <- sse.NotificationEvent{ EventName: "new-polls", Payload: poll, diff --git a/main.go b/main.go index 1b492ef..5133cbf 100644 --- a/main.go +++ b/main.go @@ -36,13 +36,6 @@ func MakeLinks(s string) template.HTML { return template.HTML(safe) } -// Checks whether the given user has met the gatekeep requirements. -func checkUserGatekeep(username string) bool { - user := &OIDCUser{Username: username} - oidcClient.GetUserGatekeep(user) - return user.Gatekeep -} - var oidcClient = OIDCClient{} var broker *sse.Broker @@ -58,8 +51,6 @@ func main() { r.LoadHTMLGlob("templates/*") broker = sse.NewBroker( GetUserData, - IsActive, - checkUserGatekeep, ) csh := cshAuth.CSHAuth{} diff --git a/sse/broker.go b/sse/broker.go index 23ec17f..3eff1d0 100644 --- a/sse/broker.go +++ b/sse/broker.go @@ -26,10 +26,11 @@ package sse import ( "io" "log" - "net/http" + "slices" "time" cshAuth "github.com/computersciencehouse/csh-auth" + "github.com/computersciencehouse/vote/database" "github.com/gin-gonic/gin" ) @@ -58,41 +59,25 @@ type ( clients map[NotifierChan]struct{} // Callbacks for cross-package access to main package functions - GetUser func(*gin.Context) cshAuth.CSHUserInfo - IsActive func(cshAuth.CSHUserInfo) bool - CheckUserGatekeep func(username string) bool + GetUser func(*gin.Context) cshAuth.CSHUserInfo } ) -func NewBroker(getUser func(*gin.Context) cshAuth.CSHUserInfo, isActive func(cshAuth.CSHUserInfo) bool, checkUserGatekeep func(username string) bool) (broker *Broker) { +func NewBroker(getUser func(*gin.Context) cshAuth.CSHUserInfo) (broker *Broker) { // Instantiate a broker return &Broker{ - Notifier: make(NotifierChan, 1), - newClients: make(chan NotifierChan), - closingClients: make(chan NotifierChan), - clients: make(map[NotifierChan]struct{}), - GetUser: getUser, - IsActive: isActive, - CheckUserGatekeep: checkUserGatekeep, + Notifier: make(NotifierChan, 1), + newClients: make(chan NotifierChan), + closingClients: make(chan NotifierChan), + clients: make(map[NotifierChan]struct{}), + GetUser: getUser, } } func (broker *Broker) ServeHTTP(c *gin.Context) { eventName := c.Param("topic") - if eventName == "new-polls" { - user := broker.GetUser(c) - - if broker.IsActive(user) { - if !broker.CheckUserGatekeep(user.Username) { - eventName = "new-polls-non-gatekeep" - } - } else { - c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized SSE connection"}) - c.Abort() - return - } - } + username := broker.GetUser(c).Username // Each connection registers its own message channel with the Broker's connections registry messageChan := make(NotifierChan) @@ -110,8 +95,18 @@ func (broker *Broker) ServeHTTP(c *gin.Context) { // Emit Server Sent Events compatible event := <-messageChan - switch eventName { - case event.EventName: + switch event.EventName { + case "new-polls": + payload, ok := event.Payload.(*database.Poll) + + if !ok { + log.Fatal("Event payload is not of type database.Poll") + } + + if !(payload.Gatekeep && payload.HideForIneligible) || slices.Contains(payload.AllowedUsers, username) { + c.SSEvent(event.EventName, event.Payload) + } + case eventName: c.SSEvent(event.EventName, event.Payload) } diff --git a/templates/index.tmpl b/templates/index.tmpl index a1f7dc2..7cbc815 100644 --- a/templates/index.tmpl +++ b/templates/index.tmpl @@ -58,7 +58,6 @@ let eventSource = new EventSource("/stream/new-polls"); eventSource.addEventListener("new-polls", handlePollEvent); - eventSource.addEventListener("new-polls-non-gatekeep", handlePollEvent);