Skip to content

Commit

Permalink
feat: jwtsign cli + admin login + middleware + manage panel (#3)
Browse files Browse the repository at this point in the history
* feat: jwtsign cli + admin login + more orm models

* feat: jwt validate on admin

* chore: admin middleware

* feat: add manage with real ejudge
  • Loading branch information
Gornak40 authored Apr 23, 2024
1 parent 41f4001 commit 0788784
Show file tree
Hide file tree
Showing 23 changed files with 469 additions and 64 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: build run clean test
.PHONY: build run clean test lint

SERVICE_NAME=crosspawn

Expand Down
4 changes: 2 additions & 2 deletions cmd/crosspawn/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ func main() {

ej := ejudge.NewEjClient(&cfg.EjConfig)

s := controller.NewServer(db, ej)
s := controller.NewServer(db, ej, &cfg.ServerConfig)

r := s.InitRouter(&cfg.GinConfig)
r := s.InitRouter()
if err := r.Run(); err != nil {
logrus.WithError(err).Fatal("failed to init router")
}
Expand Down
49 changes: 49 additions & 0 deletions cmd/jwtsign/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package main

import (
"flag"
"fmt"
"os"
"time"

"github.com/golang-jwt/jwt/v5"
"github.com/joho/godotenv"
"github.com/sirupsen/logrus"
)

const (
defaultUser = "babayka"
defaultDuration = 1 * time.Hour
)

func main() {
user := flag.String("user", defaultUser, "ejudge login for JWT")
duration := flag.Duration("duration", defaultDuration, "duration for JWT")
flag.Parse()

logrus.WithFields(logrus.Fields{
"user": *user,
"duration": *duration,
}).Info("generating JWT")

if err := godotenv.Load(); err != nil {
logrus.WithError(err).Fatal("failed to load .env file")
}

key := os.Getenv("JWT_SECRET")
if key == "" {
logrus.Fatal("JWT_SECRET env var is not set")
}

t := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"iss": "crosspawn",
"sub": *user,
"exp": time.Now().Add(*duration).Unix(),
})
s, err := t.SignedString([]byte(key))
if err != nil {
logrus.WithError(err).Fatal("failed to sign JWT")
}

fmt.Println(s) //nolint:forbidigo // basic functionality
}
7 changes: 4 additions & 3 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
type Config struct {
EjConfig
DBConfig
GinConfig
ServerConfig
}

type EjConfig struct {
Expand All @@ -22,8 +22,9 @@ type DBConfig struct {
SqlitePath string `json:"SQLITE_PATH"`
}

type GinConfig struct {
Secret string `json:"GIN_SECRET"`
type ServerConfig struct {
GinSecret string `json:"GIN_SECRET"`
JWTSecret string `json:"JWT_SECRET"`
}

func NewConfig() (*Config, error) {
Expand Down
82 changes: 82 additions & 0 deletions controller/admin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package controller

import (
"errors"
"fmt"
"net/http"

"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
)

var (
ErrForeignUser = errors.New("foreign user")
)

type adminForm struct {
JWT string `binding:"required" form:"jwt"`
}

// TODO: redirect to /login if admin is set.
func (s *Server) AdminGET(c *gin.Context) {
session := sessions.Default(c)
user := session.Get("user")

c.HTML(http.StatusOK, "admin.html", gin.H{
"Title": "Admin",
"User": user,
})
}

func (s *Server) AdminPOST(c *gin.Context) {
var form adminForm
if err := c.ShouldBind(&form); err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()})

return
}

session := sessions.Default(c)
user := session.Get("user")

if err := s.validateJWT(form.JWT, user.(string)); err != nil { //nolint:forcetypeassert // it's ok
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": err.Error()})

return
}

session.Set("admin", true)
_ = session.Save()

c.Redirect(http.StatusFound, "/manage")
}

func (s *Server) validateJWT(t, user string) error {
claims := jwt.MapClaims{}
_, err := jwt.ParseWithClaims(t, claims, func(token *jwt.Token) (interface{}, error) {
return []byte(s.cfg.JWTSecret), nil
})
if err != nil {
return err
}
if claims["sub"] != user {
return fmt.Errorf("%w: token is not owned by %s", ErrForeignUser, user)
}

return nil
}

func (s *Server) adminMiddleware(c *gin.Context) {
session := sessions.Default(c)
admin := session.Get("admin")

if admin == nil {
c.Redirect(http.StatusFound, "/admin")
c.Abort()

return
}

c.Next()
}
6 changes: 0 additions & 6 deletions controller/codereview.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,6 @@ func (s *Server) CodereviewGET(c *gin.Context) {
session := sessions.Default(c)
user := session.Get("user")

if user == nil {
c.Redirect(http.StatusUnauthorized, "/login")

return
}

contest := session.Get("contest")
problem := session.Get("problem")

Expand Down
19 changes: 3 additions & 16 deletions controller/index.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,6 @@ func (s *Server) IndexGET(c *gin.Context) {
session := sessions.Default(c)
user := session.Get("user")

if user == nil {
c.Redirect(http.StatusFound, "/login")

return
}

contest := session.Get("contest")
problem := session.Get("problem")

Expand All @@ -38,22 +32,15 @@ func (s *Server) IndexGET(c *gin.Context) {
}

func (s *Server) IndexPOST(c *gin.Context) {
session := sessions.Default(c)
user := session.Get("user")

if user == nil {
c.Redirect(http.StatusFound, "/login")

return
}

var form reviewForm
if err := c.ShouldBind(&form); err != nil {
c.Redirect(http.StatusFound, "/")
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()})

return
}

session := sessions.Default(c)

session.Set("contest", form.Contest)
session.Set("problem", form.Problem)
_ = session.Save()
Expand Down
32 changes: 24 additions & 8 deletions controller/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,30 +14,26 @@ type loginForm struct {

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": session.Get("user"),
"User": user,
})
}

// TODO: add auth.
func (s *Server) authUser(_, _ string) bool {
return true
}

func (s *Server) LoginPOST(c *gin.Context) {
session := sessions.Default(c)

var form loginForm
if err := c.ShouldBind(&form); err != nil {
c.Redirect(http.StatusBadRequest, "/login")
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()})

return
}

if !s.authUser(form.Login, form.Password) {
c.Redirect(http.StatusUnauthorized, "/login")
c.Redirect(http.StatusFound, "/login")

return
}
Expand All @@ -49,7 +45,27 @@ func (s *Server) LoginPOST(c *gin.Context) {

func (s *Server) LogoutPOST(c *gin.Context) {
session := sessions.Default(c)

session.Clear()
_ = session.Save()
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 {
c.Redirect(http.StatusFound, "/login")
c.Abort()

return
}

c.Next()
}
82 changes: 82 additions & 0 deletions controller/manage.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package controller

import (
"net/http"

"github.com/Gornak40/crosspawn/models"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
)

type manageForm struct {
ContestID uint `binding:"required" form:"ejContestID"`
}

func (s *Server) ManageGET(c *gin.Context) {
session := sessions.Default(c)
user := session.Get("user")

var contests []models.Contest
if res := s.db.Find(&contests); res.Error != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": res.Error.Error()})

return
}

c.HTML(http.StatusOK, "manage.html", gin.H{
"Title": "Manage",
"User": user,
"Contests": contests,
})
}

// TODO: add check for acm format.
func (s *Server) ManagePOST(c *gin.Context) {
var form manageForm
if err := c.ShouldBind(&form); err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()})

return
}

contest, err := s.ej.GetContestStatus(form.ContestID)
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()})

return
}

dbContest := models.NewContest(contest)
if res := s.db.Create(dbContest); res.Error != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": res.Error.Error()})

return
}

c.Redirect(http.StatusFound, "/manage")
}

func (s *Server) ManageFlipPOST(c *gin.Context) {
var form manageForm
if err := c.ShouldBind(&form); err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()})

return
}

dbContest := models.Contest{EjudgeID: form.ContestID}
if err := s.db.Where("ejudge_id = ?", form.ContestID).First(&dbContest).Error; err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()})

return
}

dbContest.ReviewActive = !dbContest.ReviewActive
if res := s.db.Save(&dbContest); res.Error != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": res.Error.Error()})

return
}

c.Redirect(http.StatusFound, "/manage")
}
Loading

0 comments on commit 0788784

Please sign in to comment.