Skip to content

Commit

Permalink
feat: pretty alerts + user auth + jwt exp check middleware
Browse files Browse the repository at this point in the history
  • Loading branch information
Gornak40 committed Apr 25, 2024
1 parent ca25e3e commit a276eb6
Show file tree
Hide file tree
Showing 12 changed files with 157 additions and 47 deletions.
30 changes: 25 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -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]
Expand All @@ -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
```
52 changes: 42 additions & 10 deletions controller/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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),
})
}

Expand All @@ -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 {
Expand All @@ -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()

Expand Down
6 changes: 6 additions & 0 deletions controller/codereview.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand All @@ -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
Expand All @@ -26,5 +31,6 @@ func (s *Server) CodereviewGET(c *gin.Context) {
"User": user,
"CodeTitle": contest,
"Code": problem,
"Flashes": alerts.Get(session),
})
}
6 changes: 4 additions & 2 deletions controller/index.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand All @@ -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),
})
}

Expand Down
34 changes: 23 additions & 11 deletions controller/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,26 @@ 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) {
session := sessions.Default(c)
user := session.Get("user")

c.HTML(http.StatusOK, "login.html", gin.H{
"Title": "Login",
"User": user,
"Title": "Login",
"User": user,
"Flashes": alerts.Get(session),
})
}

Expand All @@ -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, "/")
}

Expand All @@ -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()

Expand Down
2 changes: 2 additions & 0 deletions controller/manage.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -27,6 +28,7 @@ func (s *Server) ManageGET(c *gin.Context) {
"Title": "Manage",
"User": user,
"Contests": contests,
"Flashes": alerts.Get(session),
})
}

Expand Down
60 changes: 42 additions & 18 deletions internal/alerts/alerts.go
Original file line number Diff line number Diff line change
@@ -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
}
1 change: 1 addition & 0 deletions templates/admin.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
<div class="card">
<div class="card-body">
<form action="/admin" method="POST">
<legend>Admin Panel</legend>
<div class="mb-3">
<label for="jwt" class="form-label">JWT</label>
<input type="password" class="form-control" id="jwt" name="jwt" required>
Expand Down
3 changes: 3 additions & 0 deletions templates/header.html
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@
</li>
</ul>
</div>
{{ range .Flashes }}
<span class="badge rounded-pill text-bg-{{ .Type }} me-3">{{ .Message }}</span>
{{ end }}
<span class="navbar-text me-3">
<a href="/login"><i class="bi bi-person-fill"></i></a>
{{ .User }}
Expand Down
1 change: 1 addition & 0 deletions templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
<div class="card">
<div class="card-body">
<form action="/" method="POST">
<legend>Code review</legend>
<div class="mb-3">
<label for="ejContest" class="form-label">Ejudge contest ID</label>
<input type="number" class="form-control" id="ejContest" name="ejContest" required>
Expand Down
Loading

0 comments on commit a276eb6

Please sign in to comment.