package main import ( "encoding/json" "fmt" "net/http" "os" "sort" "strconv" "strings" "time" csh_auth "github.com/computersciencehouse/csh-auth" "github.com/computersciencehouse/vote/database" "github.com/computersciencehouse/vote/sse" "github.com/gin-gonic/gin" "go.mongodb.org/mongo-driver/bson/primitive" ) func main() { r := gin.Default() r.StaticFS("/static", http.Dir("static")) r.LoadHTMLGlob("templates/*") broker := sse.NewBroker() csh := csh_auth.CSHAuth{} csh.Init( os.Getenv("VOTE_OIDC_ID"), os.Getenv("VOTE_OIDC_SECRET"), os.Getenv("VOTE_JWT_SECRET"), os.Getenv("VOTE_STATE"), os.Getenv("VOTE_HOST"), os.Getenv("VOTE_HOST")+"/auth/callback", os.Getenv("VOTE_HOST")+"/auth/login", []string{"profile", "email", "groups"}, ) r.GET("/auth/login", csh.AuthRequest) r.GET("/auth/callback", csh.AuthCallback) r.GET("/auth/logout", csh.AuthLogout) r.GET("/", csh.AuthWrapper(func(c *gin.Context) { cl, _ := c.Get("cshauth") claims := cl.(csh_auth.CSHClaims) // This is intentionally left unprotected // A user may be unable to vote but should still be able to see a list of polls polls, err := database.GetOpenPolls() if err != nil { c.JSON(500, gin.H{"error": err.Error()}) return } sort.Slice(polls, func(i, j int) bool { return polls[i].Id > polls[j].Id }) closedPolls, err := database.GetClosedVotedPolls(claims.UserInfo.Username) if err != nil { c.JSON(500, gin.H{"error": err.Error()}) return } ownedPolls, err := database.GetClosedOwnedPolls(claims.UserInfo.Username) if err != nil { c.JSON(500, gin.H{"error": err.Error()}) return } closedPolls = append(closedPolls, ownedPolls...) sort.Slice(closedPolls, func(i, j int) bool { return closedPolls[i].Id > closedPolls[j].Id }) closedPolls = uniquePolls(closedPolls) c.HTML(200, "index.tmpl", gin.H{ "Polls": polls, "ClosedPolls": closedPolls, "Username": claims.UserInfo.Username, "FullName": claims.UserInfo.FullName, }) })) r.GET("/create", csh.AuthWrapper(func(c *gin.Context) { cl, _ := c.Get("cshauth") claims := cl.(csh_auth.CSHClaims) if !canVote(claims.UserInfo.Groups) { c.HTML(403, "unauthorized.tmpl", gin.H{ "Username": claims.UserInfo.Username, "FullName": claims.UserInfo.FullName, }) return } c.HTML(200, "create.tmpl", gin.H{ "Username": claims.UserInfo.Username, "FullName": claims.UserInfo.FullName, }) })) r.POST("/create", csh.AuthWrapper(func(c *gin.Context) { cl, _ := c.Get("cshauth") claims := cl.(csh_auth.CSHClaims) if !canVote(claims.UserInfo.Groups) { c.HTML(403, "unauthorized.tmpl", gin.H{ "Username": claims.UserInfo.Username, "FullName": claims.UserInfo.FullName, }) return } poll := &database.Poll{ Id: "", CreatedBy: claims.UserInfo.Username, ShortDescription: c.PostForm("shortDescription"), LongDescription: c.PostForm("longDescription"), VoteType: database.POLL_TYPE_SIMPLE, Open: true, Hidden: false, AllowWriteIns: c.PostForm("allowWriteIn") == "true", } if c.PostForm("rankedChoice") == "true" { poll.VoteType = database.POLL_TYPE_RANKED } switch c.PostForm("options") { case "pass-fail-conditional": poll.Options = []string{"Pass", "Fail/Conditional", "Abstain"} case "fail-conditional": poll.Options = []string{"Fail", "Conditional", "Abstain"} case "custom": poll.Options = []string{} for _, opt := range strings.Split(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") } } case "pass-fail": default: poll.Options = []string{"Pass", "Fail", "Abstain"} } pollId, err := database.CreatePoll(poll) if err != nil { c.JSON(500, gin.H{"error": err.Error()}) return } c.Redirect(302, "/poll/"+pollId) })) r.GET("/poll/:id", csh.AuthWrapper(func(c *gin.Context) { cl, _ := c.Get("cshauth") claims := cl.(csh_auth.CSHClaims) // This is intentionally left unprotected // We will check if a user can vote and redirect them to results if not later poll, err := database.GetPoll(c.Param("id")) if err != nil { c.JSON(500, gin.H{"error": err.Error()}) return } // If the user can't vote, just show them results if !canVote(claims.UserInfo.Groups) { c.Redirect(302, "/results/"+poll.Id) return } if !poll.Open { c.Redirect(302, "/results/"+poll.Id) return } hasVoted, err := database.HasVoted(poll.Id, claims.UserInfo.Username) if err != nil { c.JSON(500, gin.H{"error": err.Error()}) return } if hasVoted { c.Redirect(302, "/results/"+poll.Id) return } writeInAdj := 0 if poll.AllowWriteIns { writeInAdj = 1 } c.HTML(200, "poll.tmpl", gin.H{ "Id": poll.Id, "ShortDescription": poll.ShortDescription, "LongDescription": poll.LongDescription, "Options": poll.Options, "PollType": poll.VoteType, "RankedMax": fmt.Sprint(len(poll.Options) + writeInAdj), "AllowWriteIns": poll.AllowWriteIns, "Username": claims.UserInfo.Username, "FullName": claims.UserInfo.FullName, }) })) r.POST("/poll/:id", csh.AuthWrapper(func(c *gin.Context) { cl, _ := c.Get("cshauth") claims := cl.(csh_auth.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.Param("id")) if err != nil { c.JSON(500, gin.H{"error": err.Error()}) return } hasVoted, err := database.HasVoted(poll.Id, claims.UserInfo.Username) if err != nil { c.JSON(500, gin.H{"error": err.Error()}) return } if hasVoted || !poll.Open { c.Redirect(302, "/results/"+poll.Id) return } pId, err := primitive.ObjectIDFromHex(poll.Id) if err != nil { c.JSON(500, gin.H{"error": err.Error()}) return } if poll.VoteType == database.POLL_TYPE_SIMPLE { vote := database.SimpleVote{ Id: "", PollId: pId, UserId: claims.UserInfo.Username, Option: c.PostForm("option"), } if hasOption(poll, c.PostForm("option")) { vote.Option = c.PostForm("option") } else if poll.AllowWriteIns && c.PostForm("option") == "writein" { vote.Option = c.PostForm("writeinOption") } else { c.JSON(400, gin.H{"error": "Invalid Option"}) return } database.CastSimpleVote(&vote) } else if poll.VoteType == database.POLL_TYPE_RANKED { vote := database.RankedVote{ Id: "", PollId: pId, UserId: claims.UserInfo.Username, Options: make(map[string]int), } for _, opt := range poll.Options { if c.PostForm(opt) != "" { rank, err := strconv.Atoi(c.PostForm(opt)) if err != nil { c.JSON(500, gin.H{"error": "error parsing votes"}) } if rank > 0 { vote.Options[opt] = rank } } } if c.PostForm("writeinOption") != "" && c.PostForm("writein") != "" { rank, err := strconv.Atoi(c.PostForm("writein")) if err != nil { c.JSON(500, gin.H{"error": "error parsing votes"}) } if rank > 0 { vote.Options[c.PostForm("writeinOption")] = rank } } database.CastRankedVote(&vote) } else { c.JSON(500, gin.H{"error": "Unknown Poll Type"}) return } if poll, err := database.GetPoll(c.Param("id")); err == nil { if results, err := poll.GetResult(); err == nil { if bytes, err := json.Marshal(results); err == nil { broker.Notifier <- sse.NotificationEvent{ EventName: poll.Id, Payload: string(bytes), } } } } c.Redirect(302, "/results/"+poll.Id) })) r.GET("/results/:id", csh.AuthWrapper(func(c *gin.Context) { cl, _ := c.Get("cshauth") claims := cl.(csh_auth.CSHClaims) // This is intentionally left unprotected // A user may be unable to vote but still interested in the results of a poll poll, err := database.GetPoll(c.Param("id")) if err != nil { c.JSON(500, gin.H{"error": err.Error()}) return } results, err := poll.GetResult() if err != nil { c.JSON(500, gin.H{"error": err.Error()}) return } c.HTML(200, "result.tmpl", gin.H{ "Id": poll.Id, "ShortDescription": poll.ShortDescription, "LongDescription": poll.LongDescription, "Results": results, "IsOpen": poll.Open, "IsOwner": poll.CreatedBy == claims.UserInfo.Username, "Username": claims.UserInfo.Username, "FullName": claims.UserInfo.FullName, }) })) r.POST("/poll/:id/close", csh.AuthWrapper(func(c *gin.Context) { cl, _ := c.Get("cshauth") claims := cl.(csh_auth.CSHClaims) // This is intentionally left unprotected // A user should be able to close their own polls, regardless of if they can vote poll, err := database.GetPoll(c.Param("id")) if err != nil { c.JSON(500, gin.H{"error": err.Error()}) return } if poll.CreatedBy != claims.UserInfo.Username { c.JSON(403, gin.H{"error": "Only the creator can close a poll"}) return } err = poll.Close() if err != nil { c.JSON(500, gin.H{"error": err.Error()}) return } c.Redirect(302, "/results/"+poll.Id) })) r.GET("/stream/:topic", csh.AuthWrapper(broker.ServeHTTP)) go broker.Listen() r.Run() } func canVote(groups []string) bool { var active, fall_coop, spring_coop bool for _, group := range groups { if group == "active" { active = true } if group == "fall_coop" { fall_coop = true } if group == "spring_coop" { spring_coop = true } } if time.Now().Month() > time.July { return active && !fall_coop } else { return active && !spring_coop } } func uniquePolls(polls []*database.Poll) []*database.Poll { var unique []*database.Poll for _, poll := range polls { if !containsPoll(unique, poll) { unique = append(unique, poll) } } return unique } func containsPoll(polls []*database.Poll, poll *database.Poll) bool { for _, p := range polls { if p.Id == poll.Id { return true } } return false } func hasOption(poll *database.Poll, option string) bool { for _, opt := range poll.Options { if opt == option { return true } } return false } func containsString(arr []string, val string) bool { for _, a := range arr { if a == val { return true } } return false }