Skip to content

Commit 6e3902c

Browse files
committed
E-Board Vote v2
1 parent 1a1d453 commit 6e3902c

File tree

5 files changed

+199
-45
lines changed

5 files changed

+199
-45
lines changed

eboard.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"net/http"
6+
"slices"
7+
"strings"
8+
9+
"github.com/gin-gonic/gin"
10+
)
11+
12+
var votes map[string]float32
13+
var voters []string
14+
15+
var OPTIONS = []string{"Pass", "Fail", "Abstain"}
16+
17+
func HandleGetEboardVote(c *gin.Context) {
18+
user := getUserData(c)
19+
if !slices.Contains(user.Groups, "eboard") {
20+
c.JSON(http.StatusUnauthorized, gin.H{"error": "You need to be E-Board to access this page"})
21+
return
22+
}
23+
if votes == nil {
24+
votes = make(map[string]float32)
25+
}
26+
fmt.Println(votes)
27+
c.HTML(http.StatusOK, "eboard.tmpl", gin.H{
28+
"Username": user,
29+
"Voted": slices.Contains(voters, user.Username),
30+
"Results": votes,
31+
"Options": OPTIONS,
32+
})
33+
}
34+
35+
func HandlePostEboardVote(c *gin.Context) {
36+
user := getUserData(c)
37+
if !slices.Contains(user.Groups, "eboard") {
38+
c.JSON(http.StatusUnauthorized, gin.H{"error": "You need to be E-Board to access this page"})
39+
return
40+
}
41+
if slices.Contains(voters, user.Username) {
42+
c.JSON(http.StatusBadRequest, gin.H{"error": "You cannot vote again!"})
43+
return
44+
}
45+
i := slices.IndexFunc(user.Groups, func(s string) bool {
46+
return strings.Contains(s, "eboard-")
47+
})
48+
if i == -1 {
49+
c.JSON(http.StatusBadRequest, gin.H{"error": "You have the eboard group but not an eboard-[position] group. What is wrong with you?"})
50+
return
51+
}
52+
//get the eboard position, count the members, and divide one whole vote by the number of members in the position
53+
position := user.Groups[i]
54+
positionMembers := oidcClient.GetOIDCGroup(oidcClient.FindOIDCGroupID(position))
55+
weight := 1.0 / float32(len(positionMembers))
56+
fmt.Println(weight)
57+
//post the vote
58+
option := c.PostForm("option")
59+
if option == "" {
60+
c.JSON(http.StatusBadRequest, gin.H{"error": "You need to pick an option"})
61+
}
62+
votes[option] = votes[option] + weight
63+
voters = append(voters, user.Username)
64+
c.Redirect(http.StatusFound, "/eboard")
65+
}
66+
67+
func HandleManageEboardVote(c *gin.Context) {
68+
user := getUserData(c)
69+
if !slices.Contains(user.Groups, "eboard") {
70+
c.JSON(http.StatusUnauthorized, gin.H{"error": "You need to be E-Board to access this page"})
71+
return
72+
}
73+
if c.PostForm("clear_vote") != "" {
74+
votes = make(map[string]float32)
75+
voters = make([]string, 0)
76+
}
77+
c.Redirect(http.StatusFound, "/eboard")
78+
}

main.go

Lines changed: 14 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -95,12 +95,15 @@ func main() {
9595
r.GET("/auth/callback", csh.AuthCallback)
9696
r.GET("/auth/logout", csh.AuthLogout)
9797

98+
r.GET("/eboard", csh.AuthWrapper(HandleGetEboardVote))
99+
r.POST("/eboard", csh.AuthWrapper(HandlePostEboardVote))
100+
r.POST("/eboard/manage", csh.AuthWrapper(HandleManageEboardVote))
101+
98102
// TODO: change ALL the response codes to use http.(actual description)
99103
r.GET("/", csh.AuthWrapper(func(c *gin.Context) {
100-
cl, _ := c.Get("cshauth")
101-
claims := cl.(cshAuth.CSHClaims)
102104
// This is intentionally left unprotected
103105
// A user may be unable to vote but should still be able to see a list of polls
106+
user := getUserData(c)
104107

105108
polls, err := database.GetOpenPolls(c)
106109
if err != nil {
@@ -111,27 +114,11 @@ func main() {
111114
return polls[i].Id > polls[j].Id
112115
})
113116

114-
closedPolls, err := database.GetClosedVotedPolls(c, claims.UserInfo.Username)
115-
if err != nil {
116-
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
117-
return
118-
}
119-
ownedPolls, err := database.GetClosedOwnedPolls(c, claims.UserInfo.Username)
120-
if err != nil {
121-
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
122-
return
123-
}
124-
closedPolls = append(closedPolls, ownedPolls...)
125-
126-
sort.Slice(closedPolls, func(i, j int) bool {
127-
return closedPolls[i].Id > closedPolls[j].Id
128-
})
129-
closedPolls = uniquePolls(closedPolls)
130-
131117
c.HTML(http.StatusOK, "index.tmpl", gin.H{
132118
"Polls": polls,
133-
"Username": claims.UserInfo.Username,
134-
"FullName": claims.UserInfo.FullName,
119+
"Username": user.Username,
120+
"FullName": user.FullName,
121+
"EBoard": slices.Contains(user.Groups, "eboard"),
135122
})
136123
}))
137124

@@ -598,6 +585,12 @@ func canVote(user cshAuth.CSHUserInfo, poll database.Poll, allowedUsers []string
598585
return 0
599586
}
600587

588+
func getUserData(c *gin.Context) cshAuth.CSHUserInfo {
589+
cl, _ := c.Get("cshauth")
590+
user := cl.(cshAuth.CSHClaims).UserInfo
591+
return user
592+
}
593+
601594
func uniquePolls(polls []*database.Poll) []*database.Poll {
602595
var unique []*database.Poll
603596
for _, poll := range polls {

templates/eboard.tmpl

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,52 @@
11
<!DOCTYPE html>
22
<html lang="en">
33
<head>
4-
<title>CSH Vote</title>
5-
<!-- <link rel="stylesheet" href="https://themeswitcher.csh.rit.edu/api/get" /> -->
6-
<link rel="stylesheet" href="https://assets.csh.rit.edu/csh-material-bootstrap/4.6.2/dist/csh-material-bootstrap.min.css" media="screen">
4+
<title>CSH Vote</title>
5+
<link rel="stylesheet"
6+
href="https://assets.csh.rit.edu/csh-material-bootstrap/4.6.2/dist/csh-material-bootstrap.min.css"
7+
media="screen">
78

8-
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.13.1/font/bootstrap-icons.min.css">
9+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.13.1/font/bootstrap-icons.min.css">
910

10-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
11+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
1112
</head>
1213
<body>
1314
{{ template "nav" . }}
15+
<div class="container main p-5">
16+
<h2>E-Board Vote</h2>
17+
<div class="row">
18+
<div class="col-md-8">
19+
{{ if .Voted }}
20+
{{ range $option, $count := .Results }}
21+
<div id="{{ $option }}" style="font-size: 1.25rem; line-height: 1.25">
22+
{{ $option }}: {{ $count }}
23+
</div>
24+
<br/>
25+
{{ end }}
26+
{{ else }}
27+
<form method="POST">
28+
{{ range $i, $option := .Options }}
29+
<div class="form-check">
30+
<input class="form-check-input" type="radio" name="option"
31+
id="{{ $option }}"
32+
value="{{ $option }}" required/>
33+
<label style="font-size: 1.25rem; line-height: 1.25; padding-left: 4px;"
34+
class="form-check-label"
35+
for="{{ $option }}">{{ $option }}</label>
36+
</div>
37+
<br/>
38+
{{ end }}
39+
<button type="submit" class="btn btn-primary">Submit</button>
40+
</form>
41+
{{ end }}
42+
</div>
43+
<div class="col-md-4">
44+
<form action="/eboard/manage" method="POST" onsubmit="location.reload()">
45+
<input class="d-none" name="clear_vote" value="true">
46+
<button type="submit" class="btn btn-warning">Clear Votes</button>
47+
</form>
48+
</div>
49+
</div>
50+
</div>
1451
</body>
15-
</html>
52+
</html>

templates/nav.tmpl

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,27 @@
11
{{ define "nav" }}
2-
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
2+
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
33
<div class="container">
4-
<a class="navbar-brand" href="/">Vote</a>
4+
<a class="navbar-brand" href="/">Vote</a>
55

6-
<button class="navbar-toggler collapsed" type="button" data-toggle="collapse" data-target="#navbar"
7-
aria-controls="navbar" aria-expanded="false" aria-label="Toggle navigation">
8-
<span class="navbar-toggler-icon"></span>
9-
</button>
10-
<div id="navbar" class="collapse navbar-collapse">
11-
<a class="nav-item nav-link text-light" href="/"><i class="bi bi-exclamation-circle-fill"></i> Open Polls</a>
12-
<a class="nav-item nav-link text-light" href="/closed"><i class="bi bi-archive-fill"></i> Closed Polls</a>
13-
<a class="nav-item nav-link text-light" href="/create"><i class="bi bi-plus-circle-fill"></i> Create Poll</a>
14-
<div class="nav navbar-nav ml-auto">
15-
<div class="navbar-user">
16-
<img src="https://profiles.csh.rit.edu/image/{{ .Username }}" />
17-
<span class="text-light">{{ .FullName }}</span>
18-
<a href="/auth/logout" style="color: #c3c3c3;"><i>(logout)</i></a>
19-
</div>
20-
</div>
6+
<button class="navbar-toggler collapsed" type="button" data-toggle="collapse" data-target="#navbar"
7+
aria-controls="navbar" aria-expanded="false" aria-label="Toggle navigation">
8+
<span class="navbar-toggler-icon"></span>
9+
</button>
10+
<div id="navbar" class="collapse navbar-collapse">
11+
<a class="nav-item nav-link text-light" href="/"><i class="bi bi-exclamation-circle-fill"></i> Open Polls</a>
12+
<a class="nav-item nav-link text-light" href="/closed"><i class="bi bi-archive-fill"></i> Closed Polls</a>
13+
<a class="nav-item nav-link text-light" href="/create"><i class="bi bi-plus-circle-fill"></i> Create Poll</a>
14+
{{ if .EBoard }}
15+
<a class="nav-item nav-link text-light" href="/eboard"> E-Board</a>
16+
{{ end }}
17+
<div class="nav navbar-nav ml-auto">
18+
<div class="navbar-user">
19+
<img src="https://profiles.csh.rit.edu/image/{{ .Username }}" />
20+
<span class="text-light">{{ .FullName }}</span>
21+
<a href="/auth/logout" style="color: #c3c3c3;"><i>(logout)</i></a>
22+
</div>
2123
</div>
24+
</div>
2225
</div>
2326

2427
<script>
@@ -31,6 +34,6 @@
3134
});
3235
});
3336
</script>
34-
</nav>
37+
</nav>
3538

3639
{{ end }}

users.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package main
22

33
import (
44
"encoding/json"
5+
"fmt"
56
"io"
67
"net/http"
78
"net/url"
@@ -29,6 +30,8 @@ type OIDCUser struct {
2930
SlackUID string `json:"slackuid"`
3031
}
3132

33+
var groupCache map[string]string
34+
3235
func (client *OIDCClient) setupOidcClient(oidcClientId, oidcClientSecret string) {
3336
client.oidcClientId = oidcClientId
3437
client.oidcClientSecret = oidcClientSecret
@@ -37,6 +40,7 @@ func (client *OIDCClient) setupOidcClient(oidcClientId, oidcClientSecret string)
3740
logging.Logger.WithFields(logrus.Fields{"method": "setupOidcClient"}).Error(err)
3841
return
3942
}
43+
groupCache = make(map[string]string)
4044
client.providerBase = parse.Scheme + "://" + parse.Host
4145
exp := client.getAccessToken()
4246
ticker := time.NewTicker(time.Duration(exp) * time.Second)
@@ -94,6 +98,45 @@ func (client *OIDCClient) GetEBoard() []OIDCUser {
9498
return client.GetOIDCGroup("47dd1a94-853c-426d-b181-6d0714074892")
9599
}
96100

101+
func (client *OIDCClient) FindOIDCGroupID(name string) string {
102+
if groupCache[name] != "" {
103+
return groupCache[name]
104+
}
105+
htclient := &http.Client{}
106+
//active
107+
req, err := http.NewRequest("GET", client.providerBase+"/auth/admin/realms/csh/groups?exact=true&search="+name, nil)
108+
if err != nil {
109+
logging.Logger.WithFields(logrus.Fields{"method": "FindOIDCGroupID"}).Error(err)
110+
return ""
111+
}
112+
req.Header.Add("Authorization", "Bearer "+client.accessToken)
113+
resp, err := htclient.Do(req)
114+
if err != nil {
115+
logging.Logger.WithFields(logrus.Fields{"method": "FindOIDCGroupID"}).Error(err)
116+
return ""
117+
}
118+
defer resp.Body.Close()
119+
ret := make([]map[string]any, 0)
120+
err = json.NewDecoder(resp.Body).Decode(&ret)
121+
if err != nil {
122+
logging.Logger.WithFields(logrus.Fields{"method": "FindOIDCGroupID"}).Error(err)
123+
return ""
124+
}
125+
//Example:
126+
//[{"id":"47dd1a94-853c-426d-b181-6d0714074892","name":"eboard","path":"/eboard","subGroups":[{"id":"66b9578a-2b58-46a6-8040-59388e57e830","name":"eboard-opcomm","path":"/eboard/eboard-opcomm","subGroups":[]}]}]
127+
//it returns as an array for SOME reason, so we cut to the group we want
128+
group := ret[0]
129+
// and now we have SUBgroups, so we do this fucked parse
130+
subGroups := group["subGroups"].([]any)
131+
fmt.Println(subGroups)
132+
subGroup := subGroups[0].(map[string]interface{})
133+
fmt.Println(subGroup)
134+
gid := subGroup["id"].(string)
135+
groupCache[name] = gid
136+
return gid
137+
138+
}
139+
97140
func (client *OIDCClient) GetOIDCGroup(groupID string) []OIDCUser {
98141
htclient := &http.Client{}
99142
//active

0 commit comments

Comments
 (0)