From 09466af52c3cc43b00af24ed0948448921f9cdd0 Mon Sep 17 00:00:00 2001 From: shaeespring Date: Sun, 5 Oct 2025 23:00:24 -0400 Subject: [PATCH 1/9] gatekeep --- main.go | 37 +++++++++++++++++++++++++++++++++---- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/main.go b/main.go index 3510b22..eb44b44 100644 --- a/main.go +++ b/main.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "html/template" + "log" "net/http" "os" "sort" @@ -297,7 +298,7 @@ func main() { } if rank > 0 && rank <= max_num { vote.Options[opt] = rank - voted[rank - 1] = true + voted[rank-1] = true } else { c.JSON(400, gin.H{"error": fmt.Sprintf("votes must be from 1 - %d", max_num)}) return @@ -513,7 +514,7 @@ func main() { r.Run() } -func canVote(groups []string) bool { +func canVote(groups []string, username string) bool { var active, fallCoop, springCoop bool for _, group := range groups { if group == "active" { @@ -530,10 +531,38 @@ func canVote(groups []string) bool { } } + type Result struct { + Result bool `json:"result"` + } + + // gatekeep + gatekeepURL := "https://conditional.csh.rit.edu/gatekeep/" + username + voteToken := os.Getenv("VOTE_TOKEN") + req, err := http.NewRequest("GET", gatekeepURL, nil) + if err != nil { + log.Fatal(err) + return false + } + req.Header.Add("X-VOTE-TOKEN", voteToken) + + resp, err := http.DefaultClient.Do(req) + if err != nil { + log.Fatal(err) + return false + } + defer resp.Body.Close() + + var result Result + err = json.NewDecoder(resp.Body).Decode(&result) + if err != nil { + log.Fatal(err) + return false + } + gatekeep := result.Result if time.Now().Month() > time.July { - return active && !fallCoop + return active && !fallCoop && gatekeep } else { - return active && !springCoop + return active && !springCoop && gatekeep } } From eff6aab5d68ff64cdcd8767b8414f310d41c0c26 Mon Sep 17 00:00:00 2001 From: shaeespring Date: Sun, 12 Oct 2025 13:30:49 -0400 Subject: [PATCH 2/9] gatekeep implementation and toggle --- database/poll.go | 1 + main.go | 35 ++++++++++++++++++++--------------- templates/create.tmpl | 10 ++++++++++ 3 files changed, 31 insertions(+), 15 deletions(-) diff --git a/database/poll.go b/database/poll.go index 699d95f..44832e8 100644 --- a/database/poll.go +++ b/database/poll.go @@ -17,6 +17,7 @@ type Poll struct { VoteType string `bson:"voteType"` Options []string `bson:"options"` Open bool `bson:"open"` + Gatekeep bool `bson:"gatekeep"` Hidden bool `bson:"hidden"` AllowWriteIns bool `bson:"writeins"` } diff --git a/main.go b/main.go index eb44b44..3376d38 100644 --- a/main.go +++ b/main.go @@ -100,7 +100,7 @@ func main() { r.GET("/create", csh.AuthWrapper(func(c *gin.Context) { cl, _ := c.Get("cshauth") claims := cl.(cshAuth.CSHClaims) - if !canVote(claims.UserInfo.Groups) { + if !canVote(claims.UserInfo.Groups, claims.UserInfo.Username, false) { c.HTML(403, "unauthorized.tmpl", gin.H{ "Username": claims.UserInfo.Username, "FullName": claims.UserInfo.FullName, @@ -117,7 +117,7 @@ func main() { r.POST("/create", csh.AuthWrapper(func(c *gin.Context) { cl, _ := c.Get("cshauth") claims := cl.(cshAuth.CSHClaims) - if !canVote(claims.UserInfo.Groups) { + if !canVote(claims.UserInfo.Groups, claims.UserInfo.Username, false) { c.HTML(403, "unauthorized.tmpl", gin.H{ "Username": claims.UserInfo.Username, "FullName": claims.UserInfo.FullName, @@ -133,6 +133,7 @@ func main() { VoteType: database.POLL_TYPE_SIMPLE, Open: true, Hidden: false, + Gatekeep: c.PostForm("gatekeep") == "true", AllowWriteIns: c.PostForm("allowWriteIn") == "true", } if c.PostForm("rankedChoice") == "true" { @@ -179,7 +180,7 @@ func main() { } // If the user can't vote, just show them results - if !canVote(claims.UserInfo.Groups) { + if !canVote(claims.UserInfo.Groups, claims.UserInfo.Username, poll.Gatekeep) { c.Redirect(302, "/results/"+poll.Id) return } @@ -222,13 +223,6 @@ func main() { r.POST("/poll/:id", csh.AuthWrapper(func(c *gin.Context) { cl, _ := c.Get("cshauth") claims := cl.(cshAuth.CSHClaims) - if !canVote(claims.UserInfo.Groups) { - c.HTML(403, "unauthorized.tmpl", gin.H{ - "Username": claims.UserInfo.Username, - "FullName": claims.UserInfo.FullName, - }) - return - } poll, err := database.GetPoll(c, c.Param("id")) if err != nil { @@ -236,6 +230,14 @@ func main() { return } + if !canVote(claims.UserInfo.Groups, claims.UserInfo.Username, poll.Gatekeep) { + c.HTML(403, "unauthorized.tmpl", gin.H{ + "Username": claims.UserInfo.Username, + "FullName": claims.UserInfo.FullName, + }) + return + } + hasVoted, err := database.HasVoted(c, poll.Id, claims.UserInfo.Username) if err != nil { c.JSON(500, gin.H{"error": err.Error()}) @@ -514,7 +516,7 @@ func main() { r.Run() } -func canVote(groups []string, username string) bool { +func canVote(groups []string, username string, enforce_gatekeep bool) bool { var active, fallCoop, springCoop bool for _, group := range groups { if group == "active" { @@ -532,7 +534,10 @@ func canVote(groups []string, username string) bool { } type Result struct { - Result bool `json:"result"` + Result bool `json:"result"` + H_Meetings int `json:"h_meetings"` + D_Meetings int `json:"d_meetings"` + T_Seminars int `json:"t_seminars"` } // gatekeep @@ -558,11 +563,11 @@ func canVote(groups []string, username string) bool { log.Fatal(err) return false } - gatekeep := result.Result + gatekeep_result := result.Result if time.Now().Month() > time.July { - return active && !fallCoop && gatekeep + return active && !fallCoop && (gatekeep_result || !enforce_gatekeep) } else { - return active && !springCoop && gatekeep + return active && !springCoop && (gatekeep_result || !enforce_gatekeep) } } diff --git a/templates/create.tmpl b/templates/create.tmpl index eaa6c40..1981e11 100644 --- a/templates/create.tmpl +++ b/templates/create.tmpl @@ -72,6 +72,16 @@ /> Ranked Choice Vote +
+ + Gatekeep Required +
+ From 7b057ca2eb73d696e32dd5f1b6756b48f25f05ca Mon Sep 17 00:00:00 2001 From: shaeespring Date: Mon, 13 Oct 2025 21:13:33 -0400 Subject: [PATCH 3/9] fix formatting, go conventions, and code complexity --- database/poll.go | 2 +- main.go | 69 ++++++++++++++++++++++++------------------- templates/create.tmpl | 3 +- 3 files changed, 40 insertions(+), 34 deletions(-) diff --git a/database/poll.go b/database/poll.go index 44832e8..fc7692a 100644 --- a/database/poll.go +++ b/database/poll.go @@ -17,7 +17,7 @@ type Poll struct { VoteType string `bson:"voteType"` Options []string `bson:"options"` Open bool `bson:"open"` - Gatekeep bool `bson:"gatekeep"` + Gatekeep bool `bson:"gatekeep"` Hidden bool `bson:"hidden"` AllowWriteIns bool `bson:"writeins"` } diff --git a/main.go b/main.go index 3376d38..142f199 100644 --- a/main.go +++ b/main.go @@ -20,6 +20,10 @@ import ( "mvdan.cc/xurls/v2" ) +var VOTE_TOKEN string = os.Getenv("VOTE_TOKEN") + +const CONDITIONAL_GATEKEEP_URL string = "https://conditional.csh.rit.edu/gatekeep/" + func inc(x int) string { return strconv.Itoa(x + 1) } @@ -516,7 +520,14 @@ func main() { r.Run() } -func canVote(groups []string, username string, enforce_gatekeep bool) bool { +type Result struct { + Result bool `json:"result"` + H_Meetings int `json:"h_meetings"` + D_Meetings int `json:"d_meetings"` + T_Seminars int `json:"t_seminars"` +} + +func canVote(groups []string, username string, gatekeepEnforcedPoll bool) bool { var active, fallCoop, springCoop bool for _, group := range groups { if group == "active" { @@ -533,42 +544,38 @@ func canVote(groups []string, username string, enforce_gatekeep bool) bool { } } - type Result struct { - Result bool `json:"result"` - H_Meetings int `json:"h_meetings"` - D_Meetings int `json:"d_meetings"` - T_Seminars int `json:"t_seminars"` - } - - // gatekeep - gatekeepURL := "https://conditional.csh.rit.edu/gatekeep/" + username - voteToken := os.Getenv("VOTE_TOKEN") - req, err := http.NewRequest("GET", gatekeepURL, nil) - if err != nil { - log.Fatal(err) - return false - } - req.Header.Add("X-VOTE-TOKEN", voteToken) + passesGatekeep := true + // check if the user passes gatekeep + if gatekeepEnforcedPoll { + endpointURL := CONDITIONAL_GATEKEEP_URL + username + req, err := http.NewRequest("GET", endpointURL, nil) + if err != nil { + log.Fatal(err) + return false + } + req.Header.Add("X-VOTE-TOKEN", VOTE_TOKEN) - resp, err := http.DefaultClient.Do(req) - if err != nil { - log.Fatal(err) - return false - } - defer resp.Body.Close() + resp, err := http.DefaultClient.Do(req) + if err != nil { + log.Fatal(err) + return false + } + defer resp.Body.Close() - var result Result - err = json.NewDecoder(resp.Body).Decode(&result) - if err != nil { - log.Fatal(err) - return false + var result Result + err = json.NewDecoder(resp.Body).Decode(&result) + if err != nil { + log.Fatal(err) + return false + } + passesGatekeep = result.Result } - gatekeep_result := result.Result if time.Now().Month() > time.July { - return active && !fallCoop && (gatekeep_result || !enforce_gatekeep) + return active && !fallCoop && passesGatekeep } else { - return active && !springCoop && (gatekeep_result || !enforce_gatekeep) + return active && !springCoop && passesGatekeep } + } func uniquePolls(polls []*database.Poll) []*database.Poll { diff --git a/templates/create.tmpl b/templates/create.tmpl index 1981e11..dd7d153 100644 --- a/templates/create.tmpl +++ b/templates/create.tmpl @@ -80,8 +80,7 @@ checked /> Gatekeep Required - - + From d8b85c948ad8cc1cd59985a6e52818f2d51d7c50 Mon Sep 17 00:00:00 2001 From: shaeespring Date: Sat, 18 Oct 2025 21:45:06 -0400 Subject: [PATCH 4/9] log -> logger --- main.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/main.go b/main.go index 142f199..079a829 100644 --- a/main.go +++ b/main.go @@ -4,7 +4,6 @@ import ( "encoding/json" "fmt" "html/template" - "log" "net/http" "os" "sort" @@ -14,8 +13,10 @@ import ( cshAuth "github.com/computersciencehouse/csh-auth" "github.com/computersciencehouse/vote/database" + "github.com/computersciencehouse/vote/logging" "github.com/computersciencehouse/vote/sse" "github.com/gin-gonic/gin" + "github.com/sirupsen/logrus" "go.mongodb.org/mongo-driver/bson/primitive" "mvdan.cc/xurls/v2" ) @@ -550,14 +551,14 @@ func canVote(groups []string, username string, gatekeepEnforcedPoll bool) bool { endpointURL := CONDITIONAL_GATEKEEP_URL + username req, err := http.NewRequest("GET", endpointURL, nil) if err != nil { - log.Fatal(err) + logging.Logger.WithFields(logrus.Fields{"method": "canVote"}).Error(err) return false } req.Header.Add("X-VOTE-TOKEN", VOTE_TOKEN) resp, err := http.DefaultClient.Do(req) if err != nil { - log.Fatal(err) + logging.Logger.WithFields(logrus.Fields{"method": "canVote"}).Error(err) return false } defer resp.Body.Close() @@ -565,7 +566,7 @@ func canVote(groups []string, username string, gatekeepEnforcedPoll bool) bool { var result Result err = json.NewDecoder(resp.Body).Decode(&result) if err != nil { - log.Fatal(err) + logging.Logger.WithFields(logrus.Fields{"method": "canVote"}).Error(err) return false } passesGatekeep = result.Result From fbbea77016af546dca099d700bc6f1c100f2b510 Mon Sep 17 00:00:00 2001 From: shaeespring Date: Mon, 3 Nov 2025 09:48:32 -0500 Subject: [PATCH 5/9] this is probably broken --- main.go | 2 +- templates/create.tmpl | 32 ++++++++++++++++++++++++++------ 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/main.go b/main.go index 079a829..b443803 100644 --- a/main.go +++ b/main.go @@ -23,7 +23,7 @@ import ( var VOTE_TOKEN string = os.Getenv("VOTE_TOKEN") -const CONDITIONAL_GATEKEEP_URL string = "https://conditional.csh.rit.edu/gatekeep/" +var CONDITIONAL_GATEKEEP_URL string = os.Getenv("CONDITIONAL_URL") func inc(x int) string { return strconv.Itoa(x + 1) diff --git a/templates/create.tmpl b/templates/create.tmpl index dd7d153..8f07c78 100644 --- a/templates/create.tmpl +++ b/templates/create.tmpl @@ -73,15 +73,25 @@ Ranked Choice Vote
+ + + + - +
From 37e9a2223e4c63a651fd0c3e639fae03ee695733 Mon Sep 17 00:00:00 2001 From: shaeespring Date: Mon, 3 Nov 2025 10:45:07 -0500 Subject: [PATCH 6/9] frontend works, backend impl --- database/poll.go | 1 + main.go | 22 ++++++++++++++++------ templates/create.tmpl | 4 ++-- 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/database/poll.go b/database/poll.go index fc7692a..7a0c22d 100644 --- a/database/poll.go +++ b/database/poll.go @@ -18,6 +18,7 @@ type Poll struct { Options []string `bson:"options"` Open bool `bson:"open"` Gatekeep bool `bson:"gatekeep"` + WaivedUsers []string `bson:"waivedUsers"` Hidden bool `bson:"hidden"` AllowWriteIns bool `bson:"writeins"` } diff --git a/main.go b/main.go index b443803..111d80a 100644 --- a/main.go +++ b/main.go @@ -6,6 +6,7 @@ import ( "html/template" "net/http" "os" + "slices" "sort" "strconv" "strings" @@ -105,7 +106,7 @@ func main() { r.GET("/create", csh.AuthWrapper(func(c *gin.Context) { cl, _ := c.Get("cshauth") claims := cl.(cshAuth.CSHClaims) - if !canVote(claims.UserInfo.Groups, claims.UserInfo.Username, false) { + if !canVote(claims.UserInfo.Groups, claims.UserInfo.Username, false, []string{}) { c.HTML(403, "unauthorized.tmpl", gin.H{ "Username": claims.UserInfo.Username, "FullName": claims.UserInfo.FullName, @@ -122,7 +123,7 @@ func main() { r.POST("/create", csh.AuthWrapper(func(c *gin.Context) { cl, _ := c.Get("cshauth") claims := cl.(cshAuth.CSHClaims) - if !canVote(claims.UserInfo.Groups, claims.UserInfo.Username, false) { + if !canVote(claims.UserInfo.Groups, claims.UserInfo.Username, false, []string{}) { c.HTML(403, "unauthorized.tmpl", gin.H{ "Username": claims.UserInfo.Username, "FullName": claims.UserInfo.FullName, @@ -152,7 +153,7 @@ func main() { poll.Options = []string{"Fail", "Conditional", "Abstain"} case "custom": poll.Options = []string{} - for _, opt := range strings.Split(c.PostForm("customOptions"), ",") { + for opt := range strings.SplitSeq(c.PostForm("customOptions"), ",") { poll.Options = append(poll.Options, strings.TrimSpace(opt)) if !containsString(poll.Options, "Abstain") && (poll.VoteType == database.POLL_TYPE_SIMPLE) { poll.Options = append(poll.Options, "Abstain") @@ -162,6 +163,12 @@ func main() { default: poll.Options = []string{"Pass", "Fail", "Abstain"} } + if poll.Gatekeep { + poll.WaivedUsers = []string{} + for user := range strings.SplitSeq(c.PostForm("waivedUsers"), ",") { + poll.WaivedUsers = append(poll.WaivedUsers, strings.TrimSpace(user)) + } + } pollId, err := database.CreatePoll(c, poll) if err != nil { @@ -185,7 +192,7 @@ func main() { } // If the user can't vote, just show them results - if !canVote(claims.UserInfo.Groups, claims.UserInfo.Username, poll.Gatekeep) { + if !canVote(claims.UserInfo.Groups, claims.UserInfo.Username, poll.Gatekeep, poll.WaivedUsers) { c.Redirect(302, "/results/"+poll.Id) return } @@ -235,7 +242,7 @@ func main() { return } - if !canVote(claims.UserInfo.Groups, claims.UserInfo.Username, poll.Gatekeep) { + if !canVote(claims.UserInfo.Groups, claims.UserInfo.Username, poll.Gatekeep, poll.WaivedUsers) { c.HTML(403, "unauthorized.tmpl", gin.H{ "Username": claims.UserInfo.Username, "FullName": claims.UserInfo.FullName, @@ -528,7 +535,10 @@ type Result struct { T_Seminars int `json:"t_seminars"` } -func canVote(groups []string, username string, gatekeepEnforcedPoll bool) bool { +func canVote(groups []string, username string, gatekeepEnforcedPoll bool, waivedUsers []string) bool { + if slices.Contains(waivedUsers, username) { + return true + } var active, fallCoop, springCoop bool for _, group := range groups { if group == "active" { diff --git a/templates/create.tmpl b/templates/create.tmpl index 8f07c78..1940d28 100644 --- a/templates/create.tmpl +++ b/templates/create.tmpl @@ -83,12 +83,12 @@ /> - From ae4f3edf38b2e8d199359eed49158a03bad5ecea Mon Sep 17 00:00:00 2001 From: shaeespring Date: Wed, 5 Nov 2025 13:34:09 -0500 Subject: [PATCH 8/9] formatting and ReadMe updates --- README.md | 13 ++++++++++--- main.go | 2 +- templates/create.tmpl | 37 ++++++++++++++++++------------------- 3 files changed, 29 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index ae35812..ee28a35 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,19 @@ # vote + because paper ballots are so 2019 Imagine this. You're a somehow still functioning student organization of computer nerds. You've been using paper ballots to vote for the last 40 years. But then, disaster strikes! Global ~~ligma~~ COVID takes over, and if you so much look at a slip of paper, The Virus will take you. Enter vote, a 🚀 blazingly fast 🚀... Wait. This is Go, not Rust. It can't be blazingly fast. Uhh... Enter vote, a reasonably fast voting app with less memory safety than if it was written in Rust. But hey, gotta Pokemon _Go_ to the polls somehow, right? Right...? This is why I'm a software engineer and not a comedian. Anyways, now we can vote online. It's cool, I guess? We have things such as: - - **Server-side rendering**. That's right, this site (should) (mostly) work without JavaScript. - - **Server Sent Events** for real-time vote results - - **~~Limited~~ voting options**. It's now just as good as Google Forms, but a lot less safe! That's what you get when a bored college student does this in their free time + +- **Server-side rendering**. That's right, this site (should) (mostly) work without JavaScript. +- **Server Sent Events** for real-time vote results +- **~~Limited~~ voting options**. It's now just as good as Google Forms, but a lot less safe! That's what you get when a bored college student does this in their free time ## Configuration + You'll need to set up these values in your environment. Ask an RTP for OIDC credentials. A docker-compose file is provided for convenience. Otherwise, I trust you to figure it out! + ``` VOTE_HOST=http://localhost:8080 VOTE_JWT_SECRET= @@ -18,9 +22,12 @@ VOTE_MONGODB_URI= VOTE_OIDC_ID=vote VOTE_OIDC_SECRET= VOTE_STATE= +VOTE_TOKEN= +VOTE_CONDITIONAL_URL=http://conditional.csh.rit.edu/gatekeep/ ``` ## To-Dos + - [x] Custom vote options - [x] Write-in votes - [x] Ranked choice voting diff --git a/main.go b/main.go index 111d80a..ff40017 100644 --- a/main.go +++ b/main.go @@ -24,7 +24,7 @@ import ( var VOTE_TOKEN string = os.Getenv("VOTE_TOKEN") -var CONDITIONAL_GATEKEEP_URL string = os.Getenv("CONDITIONAL_URL") +var CONDITIONAL_GATEKEEP_URL string = os.Getenv("VOTE_CONDITIONAL_URL") func inc(x int) string { return strconv.Itoa(x + 1) diff --git a/templates/create.tmpl b/templates/create.tmpl index eedb617..7021880 100644 --- a/templates/create.tmpl +++ b/templates/create.tmpl @@ -3,7 +3,7 @@ CSH Vote - + @@ -28,7 +28,7 @@ class="form-control" name="shortDescription" placeholder="Short Description" - /> + >
+ >
+ > Allow Write-In Votes
@@ -69,7 +69,7 @@ type="checkbox" name="rankedChoice" value="true" - /> + > Ranked Choice Vote
@@ -80,18 +80,18 @@ value="true" checked onchange="onGatekeepChange()" - /> - - + > + Gatekeep Required +
+ >
- + From 80ccd9d2b0e15ba009f6d9fa4d8eb32792918b7c Mon Sep 17 00:00:00 2001 From: Tyler Allen Date: Thu, 6 Nov 2025 10:28:06 -0500 Subject: [PATCH 9/9] HTTPS not HTTP --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ee28a35..e050819 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ VOTE_OIDC_ID=vote VOTE_OIDC_SECRET= VOTE_STATE= VOTE_TOKEN= -VOTE_CONDITIONAL_URL=http://conditional.csh.rit.edu/gatekeep/ +VOTE_CONDITIONAL_URL=https://conditional.csh.rit.edu/gatekeep/ ``` ## To-Dos