diff --git a/Makefile b/Makefile index f29a34f..5757889 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: build run clean test +.PHONY: build run clean test lint SERVICE_NAME=crosspawn diff --git a/cmd/crosspawn/main.go b/cmd/crosspawn/main.go index 93ef56e..8a84595 100644 --- a/cmd/crosspawn/main.go +++ b/cmd/crosspawn/main.go @@ -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") } diff --git a/cmd/jwtsign/main.go b/cmd/jwtsign/main.go new file mode 100644 index 0000000..a0c3dc2 --- /dev/null +++ b/cmd/jwtsign/main.go @@ -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 +} diff --git a/config/config.go b/config/config.go index 02efe61..cb16bd7 100644 --- a/config/config.go +++ b/config/config.go @@ -9,7 +9,7 @@ import ( type Config struct { EjConfig DBConfig - GinConfig + ServerConfig } type EjConfig struct { @@ -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) { diff --git a/controller/admin.go b/controller/admin.go new file mode 100644 index 0000000..b77ac4f --- /dev/null +++ b/controller/admin.go @@ -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() +} diff --git a/controller/codereview.go b/controller/codereview.go index aa99ba5..d8e08eb 100644 --- a/controller/codereview.go +++ b/controller/codereview.go @@ -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") diff --git a/controller/index.go b/controller/index.go index b8d8948..dba6d10 100644 --- a/controller/index.go +++ b/controller/index.go @@ -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") @@ -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() diff --git a/controller/login.go b/controller/login.go index 87f2e3f..ee8234c 100644 --- a/controller/login.go +++ b/controller/login.go @@ -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 } @@ -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() +} diff --git a/controller/manage.go b/controller/manage.go new file mode 100644 index 0000000..89c0f3e --- /dev/null +++ b/controller/manage.go @@ -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") +} diff --git a/controller/server.go b/controller/server.go index b0085b5..50249ad 100644 --- a/controller/server.go +++ b/controller/server.go @@ -12,33 +12,50 @@ import ( const sessionName = "crosspawn" type Server struct { - db *gorm.DB - ej *ejudge.EjClient + db *gorm.DB + ej *ejudge.EjClient + cfg *config.ServerConfig } -func NewServer(db *gorm.DB, ej *ejudge.EjClient) *Server { +func NewServer(db *gorm.DB, ej *ejudge.EjClient, cfg *config.ServerConfig) *Server { return &Server{ - db: db, - ej: ej, + db: db, + ej: ej, + cfg: cfg, } } -func (s *Server) InitRouter(cfg *config.GinConfig) *gin.Engine { +func (s *Server) InitRouter() *gin.Engine { r := gin.Default() - store := cookie.NewStore([]byte(cfg.Secret)) + store := cookie.NewStore([]byte(s.cfg.GinSecret)) r.Use(sessions.Sessions(sessionName, store)) r.LoadHTMLGlob("./templates/*") r.StaticFile("/favicon.ico", "./static/img/favicon.ico") - r.GET("/", s.IndexGET) - r.GET("/codereview", s.CodereviewGET) - r.GET("/login", s.LoginGET) + { + r.GET("/login", s.LoginGET) + r.POST("/login", s.LoginPOST) + } + + ua := r.Group("/", s.userMiddleware) + { + ua.GET("/", s.IndexGET) + ua.GET("/codereview", s.CodereviewGET) + ua.GET("/admin", s.AdminGET) - r.POST("/login", s.LoginPOST) - r.POST("/logout", s.LogoutPOST) - r.POST("/", s.IndexPOST) + ua.POST("/logout", s.LogoutPOST) + ua.POST("/", s.IndexPOST) + ua.POST("/admin", s.AdminPOST) + } + + aa := ua.Group("/manage", s.adminMiddleware) + { + aa.GET("/", s.ManageGET) + aa.POST("/", s.ManagePOST) + aa.POST("/flip", s.ManageFlipPOST) + } return r } diff --git a/go.mod b/go.mod index d73fe9f..f2429dd 100644 --- a/go.mod +++ b/go.mod @@ -2,14 +2,22 @@ module github.com/Gornak40/crosspawn go 1.21.9 +require ( + github.com/gin-contrib/sessions v1.0.0 + github.com/gin-gonic/gin v1.9.1 + github.com/golang-jwt/jwt/v5 v5.2.1 + github.com/joho/godotenv v1.5.1 + github.com/sirupsen/logrus v1.9.3 + gorm.io/driver/sqlite v1.5.5 + gorm.io/gorm v1.25.9 +) + require ( github.com/bytedance/sonic v1.11.3 // indirect github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect github.com/chenzhuoyu/iasm v0.9.1 // indirect github.com/gabriel-vasile/mimetype v1.4.3 // indirect - github.com/gin-contrib/sessions v1.0.0 // indirect github.com/gin-contrib/sse v0.1.0 // indirect - github.com/gin-gonic/gin v1.9.1 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.19.0 // indirect @@ -19,7 +27,6 @@ require ( github.com/gorilla/sessions v1.2.2 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect - github.com/joho/godotenv v1.5.1 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.7 // indirect github.com/leodido/go-urn v1.4.0 // indirect @@ -28,7 +35,6 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml/v2 v2.2.1 // indirect - github.com/sirupsen/logrus v1.9.3 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect golang.org/x/arch v0.7.0 // indirect @@ -38,6 +44,4 @@ require ( golang.org/x/text v0.14.0 // indirect google.golang.org/protobuf v1.33.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - gorm.io/driver/sqlite v1.5.5 // indirect - gorm.io/gorm v1.25.9 // indirect ) diff --git a/go.sum b/go.sum index add96d1..a8dfd14 100644 --- a/go.sum +++ b/go.sum @@ -10,6 +10,7 @@ github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLI github.com/chenzhuoyu/iasm v0.9.1 h1:tUHQJXo3NhBqw6s33wkGn9SP3bvrWLdlVIJ3hQBL7P0= github.com/chenzhuoyu/iasm v0.9.1/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= @@ -19,6 +20,8 @@ github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= @@ -27,9 +30,13 @@ github.com/go-playground/validator/v10 v10.19.0 h1:ol+5Fu+cSq9JD7SoSqe04GMI92cbn github.com/go-playground/validator/v10 v10.19.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o= github.com/gorilla/context v1.1.2/go.mod h1:KDPwT9i/MeWHiLl90fuTgrt4/wPcv75vFAZLaOOcbxM= github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= @@ -61,6 +68,7 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/pelletier/go-toml/v2 v2.2.1 h1:9TA9+T8+8CUCO2+WYnDLCgrYi9+omqKXyjDtosvtEhg= github.com/pelletier/go-toml/v2 v2.2.1/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= @@ -74,6 +82,7 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= @@ -93,9 +102,11 @@ golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/internal/alerts/alerts.go b/internal/alerts/alerts.go index fa637c0..1f1c793 100644 --- a/internal/alerts/alerts.go +++ b/internal/alerts/alerts.go @@ -1,5 +1,9 @@ package alerts +import "github.com/gin-contrib/sessions" + +const flashesGroup = "alerts" + type AlertType int const ( @@ -13,3 +17,21 @@ type Alert struct { Message string Type AlertType } + +func Add(session sessions.Session, a Alert) { + session.AddFlash(a, flashesGroup) + _ = session.Save() +} + +func Get(session sessions.Session) []Alert { + flashes := session.Flashes(flashesGroup) + res := make([]Alert, 0, len(flashes)) + for _, f := range flashes { + if flash, ok := f.(Alert); ok { + res = append(res, flash) + } + } + _ = session.Save() + + return res +} diff --git a/jwtsign.sh b/jwtsign.sh new file mode 100755 index 0000000..cd3969d --- /dev/null +++ b/jwtsign.sh @@ -0,0 +1,2 @@ +#!/usr/bin/bash +go build -o ./bin ./cmd/jwtsign && ./bin/jwtsign $@ diff --git a/models/contest.go b/models/contest.go new file mode 100644 index 0000000..586202d --- /dev/null +++ b/models/contest.go @@ -0,0 +1,33 @@ +package models + +import ( + "strings" + "time" + + "github.com/Gornak40/crosspawn/pkg/ejudge" + "gorm.io/gorm" +) + +type Contest struct { + gorm.Model + + EjudgeID uint `gorm:"unique;not null"` + EjudgeName string `gorm:"not null;type:varchar(128)"` + EjudgeProblemsList string `gorm:"not null;type:varchar(128)"` + + ReviewActive bool `gorm:"not null;default:true"` + LastUpdateTime time.Time `gorm:"not null;default:unixepoch"` +} + +func NewContest(contest *ejudge.EjContest) *Contest { + problemsList := make([]string, 0, len(contest.Problems)) + for _, p := range contest.Problems { + problemsList = append(problemsList, p.ShortName) + } + + return &Contest{ + EjudgeID: contest.Contest.ID, + EjudgeName: contest.Contest.Name, + EjudgeProblemsList: strings.Join(problemsList, " "), + } +} diff --git a/models/run.go b/models/run.go new file mode 100644 index 0000000..ee8215f --- /dev/null +++ b/models/run.go @@ -0,0 +1,15 @@ +package models + +import "gorm.io/gorm" + +type Run struct { + gorm.Model + + EjudgeID uint `gorm:"unique;not null"` + EjudgeContestID uint `gorm:"not null"` + EjudgeUserID uint `gorm:"not null"` + EjudgeSource string `gorm:"not null"` + EjudgeContest Contest `gorm:"foreignKey:EjudgeContestID;not null"` + + ReviewCount uint `gorm:"not null"` +} diff --git a/models/setup.go b/models/setup.go index 8538c0c..43c1b64 100644 --- a/models/setup.go +++ b/models/setup.go @@ -12,7 +12,7 @@ func ConnectDatabase(cfg *config.DBConfig) (*gorm.DB, error) { return nil, err } - if err := db.AutoMigrate(&User{}); err != nil { + if err := db.AutoMigrate(new(User), new(Contest), new(Run)); err != nil { return nil, err } diff --git a/models/user.go b/models/user.go index eb129d8..bf34591 100644 --- a/models/user.go +++ b/models/user.go @@ -4,6 +4,11 @@ import "gorm.io/gorm" type User struct { gorm.Model - Login string `gorm:"unique;not null"` - Password string `gorm:"not null"` + + EjudgeID uint `gorm:"unique;not null"` + EjudgeLogin string `gorm:"unique;not null;type:varchar(128)"` + EjudgePassword string `gorm:"not null;type:varchar(128)"` + + ReviewApproveCount uint `gorm:"not null"` + ReviewRejectCount uint `gorm:"not null"` } diff --git a/pkg/ejudge/contest.go b/pkg/ejudge/contest.go index 4d2635a..6232d50 100644 --- a/pkg/ejudge/contest.go +++ b/pkg/ejudge/contest.go @@ -8,16 +8,20 @@ import ( ) type EjContest struct { + Contest struct { + ID uint `json:"id"` + Name string `json:"name"` + } `json:"contest"` Problems []struct { - ID int `json:"id"` + ID uint `json:"id"` ShortName string `json:"short_name"` LongName string `json:"long_name"` } `json:"problems"` } -func (ej *EjClient) GetContestStatus(id int) (*EjContest, error) { +func (ej *EjClient) GetContestStatus(id uint) (*EjContest, error) { params := url.Values{ - "contest_id": {strconv.Itoa(id)}, + "contest_id": {strconv.Itoa(int(id))}, } answer, err := ej.shoot(context.TODO(), "ej/api/v1/client/contest-status-json", params) if err != nil { diff --git a/templates/admin.html b/templates/admin.html new file mode 100644 index 0000000..1df0d71 --- /dev/null +++ b/templates/admin.html @@ -0,0 +1,21 @@ +{{ template "header.html". }} + +