From a276eb6350f300e5fab9977dd382079fee5086c6 Mon Sep 17 00:00:00 2001 From: Gornak40 Date: Fri, 26 Apr 2024 01:14:48 +0300 Subject: [PATCH] feat: pretty alerts + user auth + jwt exp check middleware --- README.md | 30 ++++++++++++++++---- controller/admin.go | 52 ++++++++++++++++++++++++++------- controller/codereview.go | 6 ++++ controller/index.go | 6 ++-- controller/login.go | 34 +++++++++++++++------- controller/manage.go | 2 ++ internal/alerts/alerts.go | 60 +++++++++++++++++++++++++++------------ templates/admin.html | 1 + templates/header.html | 3 ++ templates/index.html | 1 + templates/login.html | 7 +++++ templates/manage.html | 2 +- 12 files changed, 157 insertions(+), 47 deletions(-) diff --git a/README.md b/README.md index 21e16fb..c8a1878 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ -# crosspawn +# CrossPawn *Cross review microservice developed for the Ejudge ecosystem.* ## Config -Create `config.ini` file in project directory. +Create `config.ini` in project directory. ```ini [ejudge] @@ -22,10 +22,30 @@ POLL_BATCH_SIZE = 50 POLL_DELAY_SECONDS = 10 ``` +## Build + +```bash +make +``` + +## Generate JWT + +You can generate personal token for admin user. + +```bash +./jwtsign.sh --user gorilla --duration 24h +``` + ## Usage -Generate personal admin `jwt` with `./jwtsign.sh` script. +Start poller. -Run poller with `make run-poller` command. +```bash +make run-poller +``` -Run server with `make run-crosspawn` command. +Start server. + +```bash +make run-crossspawn +``` diff --git a/controller/admin.go b/controller/admin.go index 18c84cf..3ba546f 100644 --- a/controller/admin.go +++ b/controller/admin.go @@ -5,6 +5,7 @@ import ( "fmt" "net/http" + "github.com/Gornak40/crosspawn/internal/alerts" "github.com/gin-contrib/sessions" "github.com/gin-gonic/gin" "github.com/golang-jwt/jwt/v5" @@ -23,8 +24,9 @@ func (s *Server) AdminGET(c *gin.Context) { user := session.Get("user") c.HTML(http.StatusOK, "admin.html", gin.H{ - "Title": "Admin", - "User": user, + "Title": "Admin", + "User": user, + "Flashes": alerts.Get(session), }) } @@ -37,23 +39,33 @@ func (s *Server) AdminPOST(c *gin.Context) { } session := sessions.Default(c) - user := session.Get("user") + user, ok := session.Get("user").(string) + + if !ok { + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "user is not authenticated"}) + + return + } - if err := s.validateJWT(form.JWT, user.(string)); err != nil { //nolint:forcetypeassert // it's ok + if err := s.validateJWT(form.JWT, user); err != nil { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": err.Error()}) return } - session.Set("admin", true) + session.Set("jwt", form.JWT) _ = session.Save() + _ = alerts.Add(session, alerts.Alert{ // TODO: add expiration time + Message: "JWT is valid", + Type: alerts.TypeSuccess, + }) c.Redirect(http.StatusFound, "/manage") } -func (s *Server) validateJWT(t, user string) error { +func (s *Server) validateJWT(token, user string) error { claims := jwt.MapClaims{} - _, err := jwt.ParseWithClaims(t, claims, func(_ *jwt.Token) (interface{}, error) { + _, err := jwt.ParseWithClaims(token, claims, func(_ *jwt.Token) (interface{}, error) { return []byte(s.cfg.JWTSecret), nil }) if err != nil { @@ -66,12 +78,32 @@ func (s *Server) validateJWT(t, user string) error { return nil } -// TODO: check jwt here, it can expire. func (s *Server) adminMiddleware(c *gin.Context) { session := sessions.Default(c) - admin := session.Get("admin") + user, ok := session.Get("user").(string) + if !ok { + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "user is not authenticated"}) + + return + } + + token, ok := session.Get("jwt").(string) + if !ok { + _ = alerts.Add(session, alerts.Alert{ + Message: "You should enter JWT", + Type: alerts.TypeWarning, + }) + c.Redirect(http.StatusFound, "/admin") + c.Abort() + + return + } - if admin == nil { + if err := s.validateJWT(token, user); err != nil { + _ = alerts.Add(session, alerts.Alert{ + Message: "Your JWT is expired", + Type: alerts.TypeDanger, + }) c.Redirect(http.StatusFound, "/admin") c.Abort() diff --git a/controller/codereview.go b/controller/codereview.go index d8e08eb..3f91559 100644 --- a/controller/codereview.go +++ b/controller/codereview.go @@ -3,6 +3,7 @@ package controller import ( "net/http" + "github.com/Gornak40/crosspawn/internal/alerts" "github.com/gin-contrib/sessions" "github.com/gin-gonic/gin" ) @@ -15,6 +16,10 @@ func (s *Server) CodereviewGET(c *gin.Context) { problem := session.Get("problem") if contest == nil || problem == nil { + _ = alerts.Add(session, alerts.Alert{ + Message: "Please select contest and problem", + Type: alerts.TypeInfo, + }) c.Redirect(http.StatusFound, "/") return @@ -26,5 +31,6 @@ func (s *Server) CodereviewGET(c *gin.Context) { "User": user, "CodeTitle": contest, "Code": problem, + "Flashes": alerts.Get(session), }) } diff --git a/controller/index.go b/controller/index.go index dba6d10..8c7ec49 100644 --- a/controller/index.go +++ b/controller/index.go @@ -3,6 +3,7 @@ package controller import ( "net/http" + "github.com/Gornak40/crosspawn/internal/alerts" "github.com/gin-contrib/sessions" "github.com/gin-gonic/gin" ) @@ -26,8 +27,9 @@ func (s *Server) IndexGET(c *gin.Context) { } c.HTML(http.StatusOK, "index.html", gin.H{ - "Title": "Home", - "User": user, + "Title": "Home", + "User": user, + "Flashes": alerts.Get(session), }) } diff --git a/controller/login.go b/controller/login.go index ee8234c..758f014 100644 --- a/controller/login.go +++ b/controller/login.go @@ -3,13 +3,16 @@ package controller import ( "net/http" + "github.com/Gornak40/crosspawn/internal/alerts" + "github.com/Gornak40/crosspawn/pkg/ejudge" "github.com/gin-contrib/sessions" "github.com/gin-gonic/gin" ) type loginForm struct { - Login string `binding:"required" form:"ejLogin"` - Password string `binding:"required" form:"ejPassword"` + Login string `binding:"required" form:"ejLogin"` + Password string `binding:"required" form:"ejPassword"` + ContestID uint `binding:"required" form:"ejContest"` } func (s *Server) LoginGET(c *gin.Context) { @@ -17,8 +20,9 @@ func (s *Server) LoginGET(c *gin.Context) { user := session.Get("user") c.HTML(http.StatusOK, "login.html", gin.H{ - "Title": "Login", - "User": user, + "Title": "Login", + "User": user, + "Flashes": alerts.Get(session), }) } @@ -32,14 +36,23 @@ func (s *Server) LoginPOST(c *gin.Context) { return } - if !s.authUser(form.Login, form.Password) { - c.Redirect(http.StatusFound, "/login") + if err := s.ej.AuthUser(ejudge.AuthHeader{ + Login: form.Login, + Password: form.Password, + ContestID: form.ContestID, + }); err != nil { + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": err.Error()}) return } session.Set("user", form.Login) _ = session.Save() + + _ = alerts.Add(session, alerts.Alert{ + Message: "You are logged in", + Type: alerts.TypeSuccess, + }) c.Redirect(http.StatusFound, "/") } @@ -51,16 +64,15 @@ func (s *Server) LogoutPOST(c *gin.Context) { c.Redirect(http.StatusFound, "/") } -// TODO: add auth. -func (s *Server) authUser(_, _ string) bool { - return true -} - func (s *Server) userMiddleware(c *gin.Context) { session := sessions.Default(c) user := session.Get("user") if user == nil { + _ = alerts.Add(session, alerts.Alert{ + Message: "You are not logged in", + Type: alerts.TypeWarning, + }) c.Redirect(http.StatusFound, "/login") c.Abort() diff --git a/controller/manage.go b/controller/manage.go index 00d0101..eb26b29 100644 --- a/controller/manage.go +++ b/controller/manage.go @@ -3,6 +3,7 @@ package controller import ( "net/http" + "github.com/Gornak40/crosspawn/internal/alerts" "github.com/Gornak40/crosspawn/models" "github.com/gin-contrib/sessions" "github.com/gin-gonic/gin" @@ -27,6 +28,7 @@ func (s *Server) ManageGET(c *gin.Context) { "Title": "Manage", "User": user, "Contests": contests, + "Flashes": alerts.Get(session), }) } diff --git a/internal/alerts/alerts.go b/internal/alerts/alerts.go index 1f1c793..88c279c 100644 --- a/internal/alerts/alerts.go +++ b/internal/alerts/alerts.go @@ -1,37 +1,61 @@ package alerts -import "github.com/gin-contrib/sessions" +import ( + "encoding/json" -const flashesGroup = "alerts" + "github.com/gin-contrib/sessions" + "github.com/sirupsen/logrus" +) + +const alertsFlashesGroup = "alerts" -type AlertType int +type AlertType string const ( - AlertSuccess AlertType = iota - AlertWarning - AlertDanger - AlertInfo + TypeSuccess AlertType = "success" + TypeWarning AlertType = "warning" + TypeDanger AlertType = "danger" + TypeInfo AlertType = "info" ) type Alert struct { - Message string - Type AlertType + Message string `json:"message"` + Type AlertType `json:"type"` } -func Add(session sessions.Session, a Alert) { - session.AddFlash(a, flashesGroup) - _ = session.Save() +func Add(session sessions.Session, a Alert) error { + data, err := json.Marshal(a) + if err != nil { + return err + } + session.AddFlash(string(data), alertsFlashesGroup) + + return session.Save() } func Get(session sessions.Session) []Alert { - flashes := session.Flashes(flashesGroup) - res := make([]Alert, 0, len(flashes)) + flashes := session.Flashes(alertsFlashesGroup) + if len(flashes) > 0 { + _ = session.Save() + } + + result := make([]Alert, 0, len(flashes)) for _, f := range flashes { - if flash, ok := f.(Alert); ok { - res = append(res, flash) + s, ok := f.(string) + if !ok { + logrus.Errorf("bad flash: %v", f) + + continue + } + + var a Alert + if err := json.Unmarshal([]byte(s), &a); err != nil { + logrus.WithError(err).Errorf("failed to unmarshal flash: %s", s) + + continue } + result = append(result, a) } - _ = session.Save() - return res + return result } diff --git a/templates/admin.html b/templates/admin.html index 1df0d71..04e902c 100644 --- a/templates/admin.html +++ b/templates/admin.html @@ -6,6 +6,7 @@
+ Admin Panel
diff --git a/templates/header.html b/templates/header.html index 6d56a96..1b89452 100644 --- a/templates/header.html +++ b/templates/header.html @@ -38,6 +38,9 @@
+ {{ range .Flashes }} + {{ .Message }} + {{ end }} {{ .User }} diff --git a/templates/index.html b/templates/index.html index 77723a2..62e152d 100644 --- a/templates/index.html +++ b/templates/index.html @@ -6,6 +6,7 @@
+ Code review
diff --git a/templates/login.html b/templates/login.html index 9ba8bb8..3617d7f 100644 --- a/templates/login.html +++ b/templates/login.html @@ -7,6 +7,7 @@
{{ if .User }} + User Profile
@@ -14,6 +15,7 @@ {{ else }}
+ User Login
@@ -22,6 +24,11 @@
+
+ + +
{{ end }} diff --git a/templates/manage.html b/templates/manage.html index 49cbd7a..e81a0fe 100644 --- a/templates/manage.html +++ b/templates/manage.html @@ -1,6 +1,6 @@ {{ template "header.html" . }} -
+
    {{ range .Contests }}