diff --git a/Containerfile b/Containerfile
index be7dc8d..3780ff6 100644
--- a/Containerfile
+++ b/Containerfile
@@ -1,4 +1,4 @@
-FROM docker.io/golang:1.25-alpine AS build
+FROM docker.io/golang:1.26.2-alpine AS build
WORKDIR /src/
RUN apk add git
diff --git a/README.md b/README.md
index 2925e45..723dc94 100644
--- a/README.md
+++ b/README.md
@@ -37,10 +37,11 @@ Counters also has support for Docker Compose, a extended version of docker that
(This is a really cool thing! If you use docker often, check it out!)
```
- docker compose up
+ docker compose up --build
```
## TODO
+- [] Add Swagger
- [] Add API Integration
- [] Add Logging (similar to Votes "Actions")
- [] Time Zone support
diff --git a/api.go b/api.go
index 5491f03..5608db7 100644
--- a/api.go
+++ b/api.go
@@ -3,37 +3,31 @@ package main
import (
"net/http"
"sort"
- "time"
"github.com/WeatherGod3218/counters/database"
"github.com/gin-gonic/gin"
+ cshAuth "github.com/computersciencehouse/csh-auth/v2"
"go.mongodb.org/mongo-driver/v2/bson"
)
-func translateTime(inputTime string) time.Time {
- timeZone, _ := time.LoadLocation("America/New_York")
-
- currentTime := time.Now()
+func GetHomePage(c *gin.Context) {
+ userAny, exists := c.Get("cshauth")
- timeConverted, err := time.ParseInLocation("2006-01-02T15:04", inputTime, timeZone)
- if err != nil {
- timeConverted = currentTime
+ if !exists {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "Unable to load CSH context!"})
+ return
}
-
- if timeConverted.After(currentTime) {
- timeConverted = currentTime
+ user, ok := userAny.(*cshAuth.Claims)
+ if !ok {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "Unable to load CSH context!"})
+ return
}
- return timeConverted
-}
-
-func GetHomePage(c *gin.Context) {
- user := GetUserData(c)
counters, err := database.GetAllCounters(c)
if err != nil {
- c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "Unable to load counters!"})
return
}
@@ -51,7 +45,17 @@ func GetHomePage(c *gin.Context) {
}
func GetCreatePage(c *gin.Context) {
- user := GetUserData(c)
+ userAny, exists := c.Get("cshauth")
+
+ if !exists {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "Unable to load CSH context!"})
+ return
+ }
+ user, ok := userAny.(*cshAuth.Claims)
+ if !ok {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "Unable to load CSH context!"})
+ return
+ }
c.HTML(http.StatusOK, "create.tmpl", gin.H{
"Username": user.Username,
@@ -62,7 +66,17 @@ func GetCreatePage(c *gin.Context) {
}
func GetResetPage(c *gin.Context) {
- user := GetUserData(c)
+ userAny, exists := c.Get("cshauth")
+
+ if !exists {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "Unable to load CSH context!"})
+ return
+ }
+ user, ok := userAny.(*cshAuth.Claims)
+ if !ok {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "Unable to load CSH context!"})
+ return
+ }
idToUse := c.Param("id")
c.HTML(http.StatusOK, "reset.tmpl", gin.H{
@@ -75,8 +89,17 @@ func GetResetPage(c *gin.Context) {
}
func LoadCounter(c *gin.Context) {
+ userAny, exists := c.Get("cshauth")
+ if !exists {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "Unable to load CSH context!"})
+ return
+ }
+ user, ok := userAny.(*cshAuth.Claims)
+ if !ok {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "Unable to load CSH context!"})
+ return
+ }
idToUse := c.Param("id")
- user := GetUserData(c)
counter, err := database.GetCounterFromId(c, idToUse)
if err != nil {
@@ -106,8 +129,19 @@ func LoadCounter(c *gin.Context) {
}
func ResetCounter(c *gin.Context) {
- resetTime := translateTime(c.PostForm("reset-time"))
- user := GetUserData(c)
+ userAny, exists := c.Get("cshauth")
+
+ if !exists {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "Unable to load CSH context!"})
+ return
+ }
+ user, ok := userAny.(*cshAuth.Claims)
+ if !ok {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "Unable to load CSH context!"})
+ return
+ }
+
+ resetTime := TranslateTime(c.PostForm("reset-time"))
reset := &database.Reset{
Id: bson.NewObjectID(),
@@ -128,8 +162,19 @@ func ResetCounter(c *gin.Context) {
}
func CreateCounter(c *gin.Context) {
- resetTime := translateTime(c.PostForm("reset-time"))
- user := GetUserData(c)
+ userAny, exists := c.Get("cshauth")
+
+ if !exists {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "Unable to load CSH context!"})
+ return
+ }
+ user, ok := userAny.(*cshAuth.Claims)
+ if !ok {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "Unable to load CSH context!"})
+ return
+ }
+
+ resetTime := TranslateTime(c.PostForm("reset-time"))
reset := &database.Reset{
Id: bson.NewObjectID(),
diff --git a/go.mod b/go.mod
index 4496206..997d4f9 100644
--- a/go.mod
+++ b/go.mod
@@ -3,7 +3,7 @@ module github.com/WeatherGod3218/counters
go 1.25.7
require (
- github.com/computersciencehouse/csh-auth/v2 v2.0.1
+ github.com/computersciencehouse/csh-auth/v2 v2.0.2
github.com/gin-gonic/gin v1.12.0
github.com/sirupsen/logrus v1.9.4
go.mongodb.org/mongo-driver/v2 v2.6.0
diff --git a/go.sum b/go.sum
index 454c9e7..297248e 100644
--- a/go.sum
+++ b/go.sum
@@ -18,6 +18,8 @@ github.com/computersciencehouse/csh-auth/v2 v2.0.0 h1:uRP8Ti/pb1SaSSBvhk3dLVpHag
github.com/computersciencehouse/csh-auth/v2 v2.0.0/go.mod h1:JelmcPFBLpo8wnmErF4Zoa4UJKi0psgUDpcuTq6pWbY=
github.com/computersciencehouse/csh-auth/v2 v2.0.1 h1:rtGa9cBDZJZZJ8aeGefwAM/rzPMxWNAGDwuVDZI80ro=
github.com/computersciencehouse/csh-auth/v2 v2.0.1/go.mod h1:uL/2UDr9GJlAQORsn+rmFc18YkBsZPymYJbwb+uTRig=
+github.com/computersciencehouse/csh-auth/v2 v2.0.2 h1:xlxv2w2cmiWriVNhwA8Io1FWI54sDmv7c12gkVAcK30=
+github.com/computersciencehouse/csh-auth/v2 v2.0.2/go.mod h1:uL/2UDr9GJlAQORsn+rmFc18YkBsZPymYJbwb+uTRig=
github.com/coreos/go-oidc v2.5.0+incompatible h1:6W0vGJR3Tu0r0PwfmjOrRZSlfxeEln8dsejt3ZWIvwo=
github.com/coreos/go-oidc v2.5.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
github.com/coreos/go-oidc/v3 v3.18.0 h1:V9orjXynvu5wiC9SemFTWnG4F45v403aIcjWo0d41+A=
diff --git a/main.go b/main.go
index e6f8529..19f88d7 100644
--- a/main.go
+++ b/main.go
@@ -15,20 +15,18 @@ import (
var DEV_FORCE_IS_EBOARD bool = os.Getenv("DEV_FORCE_IS_EBOARD") == "true"
-var oidcClient = OIDCClient{}
-
func main() {
database.Client = database.Connect()
+ hostUrl := os.Getenv("SERVER_HOST")
auth, err := cshAuth.Init(
os.Getenv("AUTH_OIDC_ID"),
os.Getenv("AUTH_OIDC_SECRET"),
- os.Getenv("SERVER_HOST"),
- os.Getenv("SERVER_HOST")+"/auth/login",
- os.Getenv("SERVER_HOST")+"/auth/callback",
+ hostUrl,
+ hostUrl+"/auth/login",
+ hostUrl+"/auth/callback",
[]string{"profile", "email", "groups"},
)
- oidcClient.setupOidcClient(os.Getenv("AUTH_OIDC_ID"), os.Getenv("AUTH_OIDC_SECRET"))
if err != nil {
logging.Logger.WithFields(logrus.Fields{"error": err, "module": "main", "method": "main"}).Fatal("error initializing csh-auth")
@@ -39,11 +37,11 @@ func main() {
router.StaticFS("/static", http.Dir("static"))
router.LoadHTMLGlob("templates/*")
- router.GET("/auth/login", auth.HandleLogin) // This endpoint should match the path for loginURL
- router.GET("/auth/callback", auth.HandleCallback) // This endpoint should match the path for callbackURL
+ router.GET("/auth/login", auth.HandleLogin)
+ router.GET("/auth/callback", auth.HandleCallback)
router.GET("/auth/logout", auth.HandleLogout)
- router.Use(auth.HeaderMiddleware())
+ router.Use(auth.CookieMiddleware())
router.GET("/", GetHomePage)
router.GET("/counters/:id", LoadCounter)
diff --git a/templates/header.tmpl b/templates/header.tmpl
index a4b60b5..04c44f1 100644
--- a/templates/header.tmpl
+++ b/templates/header.tmpl
@@ -36,7 +36,6 @@
diff --git a/users.go b/users.go
deleted file mode 100644
index c55a212..0000000
--- a/users.go
+++ /dev/null
@@ -1,223 +0,0 @@
-package main
-
-import (
- "encoding/json"
- "fmt"
- "io"
- "net/http"
- "net/url"
- "slices"
- "strings"
- "time"
-
- "github.com/WeatherGod3218/counters/logging"
- cshAuth "github.com/computersciencehouse/csh-auth/v2"
- "github.com/gin-gonic/gin"
- "github.com/sirupsen/logrus"
-)
-
-type OIDCClient struct {
- oidcClientId string
- oidcClientSecret string
-
- accessToken string
- providerBase string
- quit chan struct{}
-}
-
-type OIDCUser struct {
- Uuid string `json:"id"`
- Username string `json:"username"`
- Gatekeep bool `json:"result"`
- SlackUID string `json:"slackuid"`
-}
-
-var groupCache map[string]string
-
-func (client *OIDCClient) setupOidcClient(oidcClientId, oidcClientSecret string) {
- client.oidcClientId = oidcClientId
- client.oidcClientSecret = oidcClientSecret
- parse, err := url.Parse(cshAuth.ProviderURI)
- if err != nil {
- logging.Logger.WithFields(logrus.Fields{"method": "setupOidcClient"}).Error(err)
- return
- }
- groupCache = make(map[string]string)
- client.providerBase = parse.Scheme + "://" + parse.Host
- exp := client.getAccessToken()
- ticker := time.NewTicker(time.Duration(exp) * time.Second)
- // this will async get the token
- go func() {
- for {
- select {
- case <-ticker.C:
- exp = client.getAccessToken()
- ticker.Reset(time.Duration(exp) * time.Second)
- case <-client.quit:
- ticker.Stop()
- return
- }
- }
- }()
-}
-
-func (client *OIDCClient) getAccessToken() int {
- htclient := http.DefaultClient
- //request body
- authData := url.Values{}
- authData.Set("client_id", client.oidcClientId)
- authData.Set("client_secret", client.oidcClientSecret)
- authData.Set("grant_type", "client_credentials")
- resp, err := htclient.PostForm(cshAuth.ProviderURI+"/protocol/openid-connect/token", authData)
- if err != nil {
- logging.Logger.WithFields(logrus.Fields{"method": "getAccessToken"}).Error(err)
- return 0
- }
- defer resp.Body.Close()
- if resp.StatusCode != http.StatusOK {
- logging.Logger.WithFields(logrus.Fields{"method": "getAccessToken", "statusCode": resp.StatusCode}).Error(resp.Status)
- return 0
- }
- respData := make(map[string]interface{})
- err = json.NewDecoder(resp.Body).Decode(&respData)
- if err != nil {
- logging.Logger.WithFields(logrus.Fields{"method": "getAccessToken"}).Error(err)
- return 0
- }
- if respData["error"] != nil {
- logging.Logger.WithFields(logrus.Fields{"method": "getAccessToken"}).Error(respData)
- return 0
- }
- client.accessToken = respData["access_token"].(string)
- return int(respData["expires_in"].(float64))
-}
-
-func (client *OIDCClient) GetActiveUsers() []OIDCUser {
- return client.GetOIDCGroup("a97a191e-5668-43f5-bc0c-6eefc2b958a7")
-}
-
-func (client *OIDCClient) GetEBoard() []OIDCUser {
- return client.GetOIDCGroup("47dd1a94-853c-426d-b181-6d0714074892")
-}
-
-func (client *OIDCClient) FindOIDCGroupID(name string) string {
- if groupCache[name] != "" {
- return groupCache[name]
- }
- htclient := &http.Client{}
- //active
- req, err := http.NewRequest("GET", client.providerBase+"/auth/admin/realms/csh/groups?exact=true&search="+name, nil)
- if err != nil {
- logging.Logger.WithFields(logrus.Fields{"method": "FindOIDCGroupID"}).Error(err)
- return ""
- }
- req.Header.Add("Authorization", "Bearer "+client.accessToken)
- resp, err := htclient.Do(req)
- if err != nil {
- logging.Logger.WithFields(logrus.Fields{"method": "FindOIDCGroupID"}).Error(err)
- return ""
- }
- defer resp.Body.Close()
- ret := make([]map[string]any, 0)
- err = json.NewDecoder(resp.Body).Decode(&ret)
- if err != nil {
- logging.Logger.WithFields(logrus.Fields{"method": "FindOIDCGroupID"}).Error(err)
- return ""
- }
- //Example:
- //[{"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":[]}]}]
- //it returns as an array for SOME reason, so we cut to the group we want
- group := ret[0]
- // and now we have SUBgroups, so we do this fucked parse
- subGroups := group["subGroups"].([]any)
- fmt.Println(subGroups)
- subGroup := subGroups[0].(map[string]interface{})
- fmt.Println(subGroup)
- gid := subGroup["id"].(string)
- groupCache[name] = gid
- return gid
-
-}
-
-func (client *OIDCClient) GetOIDCGroup(groupID string) []OIDCUser {
- htclient := &http.Client{}
- //active
- req, err := http.NewRequest("GET", client.providerBase+"/auth/admin/realms/csh/groups/"+groupID+"/members", nil)
- if err != nil {
- logging.Logger.WithFields(logrus.Fields{"method": "GetOIDCGroup"}).Error(err)
- return nil
- }
- req.Header.Add("Authorization", "Bearer "+client.accessToken)
- resp, err := htclient.Do(req)
- if err != nil {
- logging.Logger.WithFields(logrus.Fields{"method": "GetOIDCGroup"}).Error(err)
- return nil
- }
- defer resp.Body.Close()
- ret := make([]OIDCUser, 0)
- err = json.NewDecoder(resp.Body).Decode(&ret)
- if err != nil {
- logging.Logger.WithFields(logrus.Fields{"method": "GetOIDCGroup"}).Error(err)
- return nil
- }
- return ret
-}
-
-func (client *OIDCClient) GetUserInfo(user *OIDCUser) {
- htclient := &http.Client{}
- arg := ""
- if len(user.Uuid) == 0 {
- arg = "?username=" + user.Username
- }
- req, err := http.NewRequest("GET", client.providerBase+"/auth/admin/realms/csh/users/"+user.Uuid+arg, nil)
- // also "users/{user-id}/groups"
- if err != nil {
- logging.Logger.WithFields(logrus.Fields{"method": "GetUserInfo"}).Error(err)
- return
- }
- req.Header.Add("Authorization", "Bearer "+client.accessToken)
- resp, err := htclient.Do(req)
- if err != nil {
- logging.Logger.WithFields(logrus.Fields{"method": "GetUserInfo"}).Error(err)
- return
- }
- defer resp.Body.Close()
- b, _ := io.ReadAll(resp.Body)
- if strings.Contains(string(b), "error") {
- logging.Logger.WithFields(logrus.Fields{"method": "GetUserInfo"}).Error(string(b))
- return
- }
- if len(arg) > 0 {
- userData := make([]map[string]any, 0)
- err = json.Unmarshal(b, &userData)
- // userdata attributes are a KV pair of string:[]any, this casts attributes, finds the specific attribute, casts it to a list of any, and then pulls the first field since there will only ever be one
- userAttributes := userData[0]["attributes"].(map[string]any)
- if slackIDRaw, exists := userAttributes["slackuid"]; exists {
- user.SlackUID = slackIDRaw.([]any)[0].(string)
- } else {
- logging.Logger.WithFields(logrus.Fields{"method": "GetUserInfo"}).Error("User " + user.Username + " does not have a SlackUID. Skipping messaging.")
- }
- } else {
- err = json.Unmarshal(b, &user)
- }
- if err != nil {
- logging.Logger.WithFields(logrus.Fields{"method": "GetUserInfo"}).Error(err)
- }
-}
-
-// GetUserData Retreives information about a specific CSH user account
-func GetUserData(c *gin.Context) cshAuth.UserInfo {
- cl, _ := c.Get("cshauth")
- user := cl.(*cshAuth.Claims).UserInfo
- return user
-}
-
-// IsEboard determines if the current user is on eboard, allowing for a dev mode override
-func IsEboard(user cshAuth.UserInfo) bool {
- return DEV_FORCE_IS_EBOARD || slices.Contains(user.Groups, "eboard")
-}
-
-// IsActiveRTP Determines whether the user is an active RTP, based on user groups from OIDC
-func IsActiveRTP(user cshAuth.UserInfo) bool {
- return slices.Contains(user.Groups, "active-rtp")
-}
diff --git a/util.go b/util.go
new file mode 100644
index 0000000..ccf0544
--- /dev/null
+++ b/util.go
@@ -0,0 +1,32 @@
+package main
+
+import (
+ "slices"
+ "time"
+
+ cshAuth "github.com/computersciencehouse/csh-auth/v2"
+)
+
+func IsEboard(user *cshAuth.Claims) bool {
+ return slices.Contains(user.Groups, "eboard")
+}
+
+func IsActiveRTP(user *cshAuth.Claims) bool {
+ return slices.Contains(user.Groups, "active-rtp")
+}
+
+func TranslateTime(inputTime string) time.Time {
+ timeZone, _ := time.LoadLocation("America/New_York")
+
+ currentTime := time.Now()
+
+ timeConverted, err := time.ParseInLocation("2006-01-02T15:04", inputTime, timeZone)
+ if err != nil {
+ timeConverted = currentTime
+ }
+
+ if timeConverted.After(currentTime) {
+ timeConverted = currentTime
+ }
+ return timeConverted
+}