feat: use generic Response type and rewrite swagger documentation. close #1

This commit is contained in:
Paul Pan 2023-12-22 15:19:13 +08:00
parent 9485dbbce4
commit eb6f5d0aca
Signed by: Paul
GPG Key ID: D639BDF5BA578AF4
23 changed files with 281 additions and 240 deletions

View File

@ -31,7 +31,7 @@ dep:
swagger: swagger:
go install github.com/swaggo/swag/cmd/swag@latest go install github.com/swaggo/swag/cmd/swag@latest
$(GOBIN)/swag init -g internal/web/router/api.go -o internal/web/router/docs $(GOBIN)/swag init -g internal/web/router/api.go -d .,./internal/e,./internal/model --pdl 1 -o internal/web/router/docs
fmt: fmt:
go fmt ./... go fmt ./...

View File

@ -10,7 +10,7 @@ import (
var _ Handler = (*handler)(nil) var _ Handler = (*handler)(nil)
type Handler interface { type Handler interface {
randomString(c *gin.Context) RandomString(c *gin.Context)
} }
type handler struct { type handler struct {
@ -21,5 +21,5 @@ func RouteRegister(rg *gin.RouterGroup, i *do.Injector) {
app := &handler{} app := &handler{}
app.log = do.MustInvoke[log.Service](i).GetLogger("api.debug") app.log = do.MustInvoke[log.Service](i).GetLogger("api.debug")
rg.GET("/random", app.randomString) rg.GET("/random", app.RandomString)
} }

View File

@ -7,14 +7,14 @@ import (
"go.uber.org/zap" "go.uber.org/zap"
) )
// randomString // RandomString
// @Summary random string // @Summary generate random string
// @Description generate random string with length = 32 // @Description Generate random string with length = 32.
// @Tags debug // @Tags debug
// @Produce json // @Produce json
// @Response 200 {object} e.Response "random string" // @Response 200 {object} e.Response[string] "random string"
// @Router /debug/random [get] // @Router /debug/random [get]
func (h *handler) randomString(c *gin.Context) { func (h *handler) RandomString(c *gin.Context) {
str := utils.RandomString(32) str := utils.RandomString(32)
h.log.Info("random string", zap.String("str", str)) h.log.Info("random string", zap.String("str", str))
e.Pong(c, e.Success, str) e.Pong(c, e.Success, str)

View File

@ -13,55 +13,61 @@ type createVersionRequest struct {
} }
// CreateVersion // CreateVersion
// @Summary create a problem version // @Summary [admin] create a problem version
// @Description create a problem version // @Description Create a problem version associated with `pid`.
// @Tags problem // @Tags problem,admin
// @Accept application/x-www-form-urlencoded // @Accept application/x-www-form-urlencoded
// @Produce json // @Produce json
// @Param pid formData int true "problem id" // @Param pid formData int true "problem id"
// @Param storage_key formData string true "storage key" // @Param storage_key formData string true "storage key, zip file containing problem data"
// @Response 200 {object} e.Response "" // @Response 200 {object} e.Response[any] "nothing"
// @Security Authentication // @Security Authentication
// @Router /v1/problem/create_version [post] // @Router /v1/problem/create_version [post]
func (h *handler) CreateVersion(c *gin.Context) { func (h *handler) CreateVersion(c *gin.Context) {
claim, exist := c.Get("claim") claim, exist := c.Get("claim")
if !exist { if !exist {
e.Pong(c, e.UserUnauthenticated, nil) e.Pong[any](c, e.UserUnauthenticated, nil)
return return
} }
// uid := claim.(*model.Claim).UID
role := claim.(*model.Claim).Role
req := new(createVersionRequest) req := new(createVersionRequest)
if err := c.ShouldBind(req); err != nil { if err := c.ShouldBind(req); err != nil {
e.Pong(c, e.InvalidParameter, err.Error()) e.Pong(c, e.InvalidParameter, err.Error())
return return
} }
// guest can not submit // only admin can create problem version
role := claim.(*model.Claim).Role
if role < model.RoleAdmin { if role < model.RoleAdmin {
e.Pong(c, e.UserUnauthorized, nil) e.Pong[any](c, e.UserUnauthorized, nil)
return return
} }
// TODO: check pid exist // make sure problem exists
_, status := h.problemService.Query(req.ProblemID, false, false)
if status != e.Success {
e.Pong[any](c, status, nil)
return
}
// create problem version
createVersionData := &problem.CreateVersionData{ createVersionData := &problem.CreateVersionData{
ProblemID: req.ProblemID, ProblemID: req.ProblemID,
StorageKey: req.StorageKey, StorageKey: req.StorageKey,
} }
pv, status := h.problemService.CreateVersion(createVersionData) pv, status := h.problemService.CreateVersion(createVersionData)
if status != e.Success { if status != e.Success {
e.Pong(c, status, nil) e.Pong[any](c, status, nil)
return return
} }
// enqueue task: runner build problem
payload := &model.ProblemBuildPayload{ payload := &model.ProblemBuildPayload{
ProblemVersionID: pv.ID, ProblemVersionID: pv.ID,
StorageKey: pv.StorageKey, StorageKey: pv.StorageKey,
} }
_, status = h.taskService.ProblemBuild(payload) _, status = h.taskService.ProblemBuild(payload)
e.Pong(c, status, nil) e.Pong[any](c, status, nil)
// TODO: if failed, delete problem version
} }

View File

@ -10,18 +10,22 @@ type detailsRequest struct {
Pid uint `form:"pid"` Pid uint `form:"pid"`
} }
type problemDetailsResponse struct {
Problem *model.Problem `json:"problem"`
Context interface{} `json:"context"`
}
// Details // Details
// @Summary get details of a problem // @Summary get details of a problem
// @Description get details of a problem // @Description Get details of a problem.
// @Tags problem // @Tags problem
// @Accept application/x-www-form-urlencoded // @Accept application/x-www-form-urlencoded
// @Produce json // @Produce json
// @Param pid formData int true "problem id" // @Param pid formData int true "problem id"
// @Response 200 {object} e.Response "problem details" // @Response 200 {object} e.Response[problemDetailsResponse] "problem details"
// @Router /v1/problem/details [post] // @Router /v1/problem/details [post]
func (h *handler) Details(c *gin.Context) { func (h *handler) Details(c *gin.Context) {
req := new(detailsRequest) req := new(detailsRequest)
if err := c.ShouldBind(req); err != nil { if err := c.ShouldBind(req); err != nil {
e.Pong(c, e.InvalidParameter, err.Error()) e.Pong(c, e.InvalidParameter, err.Error())
return return
@ -32,17 +36,18 @@ func (h *handler) Details(c *gin.Context) {
p, status := h.problemService.Query(req.Pid, true, shouldEnable) p, status := h.problemService.Query(req.Pid, true, shouldEnable)
if status != e.Success { if status != e.Success {
e.Pong(c, status, nil) e.Pong[any](c, status, nil)
return return
} }
pv, status := h.problemService.QueryLatestVersion(req.Pid) pv, status := h.problemService.QueryLatestVersion(req.Pid)
if status != e.Success { if status != e.Success {
e.Pong(c, status, nil) e.Pong[any](c, status, nil)
return return
} }
e.Pong(c, e.Success, gin.H{
"problem": p, e.Pong(c, e.Success, problemDetailsResponse{
"context": pv.Context.Get(), Problem: p,
Context: pv.Context.Get(),
}) })
} }

View File

@ -2,6 +2,7 @@ package problem
import ( import (
"git.0x7f.app/WOJ/woj-server/internal/e" "git.0x7f.app/WOJ/woj-server/internal/e"
_ "git.0x7f.app/WOJ/woj-server/internal/model" // swag requires this
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@ -10,17 +11,16 @@ type searchRequest struct {
} }
// Search // Search
// @Summary get detail of a problem // @Summary search for problems
// @Description get detail of a problem // @Description Search for problems based on keywords. If the keyword is empty, return all problems.
// @Tags problem // @Tags problem
// @Accept application/x-www-form-urlencoded // @Accept application/x-www-form-urlencoded
// @Produce json // @Produce json
// @Param search formData string false "word search" // @Param search formData string false "keyword"
// @Response 200 {object} e.Response "problemset" // @Response 200 {object} e.Response[[]model.Problem] "problems found"
// @Router /v1/problem/search [post] // @Router /v1/problem/search [post]
func (h *handler) Search(c *gin.Context) { func (h *handler) Search(c *gin.Context) {
req := new(searchRequest) req := new(searchRequest)
if err := c.ShouldBind(req); err != nil { if err := c.ShouldBind(req); err != nil {
e.Pong(c, e.InvalidParameter, err.Error()) e.Pong(c, e.InvalidParameter, err.Error())
return return

View File

@ -4,6 +4,7 @@ import (
"git.0x7f.app/WOJ/woj-server/internal/e" "git.0x7f.app/WOJ/woj-server/internal/e"
"git.0x7f.app/WOJ/woj-server/internal/model" "git.0x7f.app/WOJ/woj-server/internal/model"
"git.0x7f.app/WOJ/woj-server/internal/service/problem" "git.0x7f.app/WOJ/woj-server/internal/service/problem"
"git.0x7f.app/WOJ/woj-server/pkg/utils"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@ -15,29 +16,31 @@ type updateRequest struct {
} }
// Update // Update
// @Summary create or update a problem // @Summary [admin] create or update a problem
// @Description create or update a problem // @Description Create or update a problem.
// @Tags problem // @Tags problem,admin
// @Accept application/x-www-form-urlencoded // @Accept application/x-www-form-urlencoded
// @Produce json // @Produce json
// @Param pid formData int false "problem id, 0 for create" // @Param pid formData int false "problem id, 0 for create"
// @Param title formData string true "title" // @Param title formData string true "title"
// @Param statement formData string true "statement" // @Param statement formData string true "statement"
// @Param is_enabled formData bool false "is enabled" // @Param is_enabled formData bool false "is enabled"
// @Response 200 {object} e.Response "problem info without provider information" // @Response 200 {object} e.Response[model.Problem] "problem info without provider information"
// @Security Authentication // @Security Authentication
// @Router /v1/problem/update [post] // @Router /v1/problem/update [post]
func (h *handler) Update(c *gin.Context) { func (h *handler) Update(c *gin.Context) {
claim, exist := c.Get("claim") claim, exist := c.Get("claim")
if !exist { if !exist {
e.Pong(c, e.UserUnauthenticated, nil) e.Pong[any](c, e.UserUnauthenticated, nil)
return return
} }
uid := claim.(*model.Claim).UID uid := claim.(*model.Claim).UID
role := claim.(*model.Claim).Role role := claim.(*model.Claim).Role
// only admin can modify problem
if role < model.RoleAdmin { if role < model.RoleAdmin {
e.Pong(c, e.UserUnauthorized, nil) e.Pong[any](c, e.UserUnauthorized, nil)
return return
} }
@ -48,6 +51,7 @@ func (h *handler) Update(c *gin.Context) {
} }
if req.Pid == 0 { if req.Pid == 0 {
// create problem
createData := &problem.CreateData{ createData := &problem.CreateData{
Title: req.Title, Title: req.Title,
Statement: req.Statement, Statement: req.Statement,
@ -58,18 +62,24 @@ func (h *handler) Update(c *gin.Context) {
e.Pong(c, status, p) e.Pong(c, status, p)
return return
} else { } else {
// update problem
// check if problem exists
p, status := h.problemService.Query(req.Pid, true, false) p, status := h.problemService.Query(req.Pid, true, false)
if status != e.Success { if status != e.Success {
e.Pong(c, status, nil) e.Pong[any](c, status, nil)
return
}
if p.ProviderID != uid {
e.Pong(c, e.UserUnauthorized, nil)
return return
} }
p.Title = req.Title // check if user is the provider of the problem
p.Statement = req.Statement if p.ProviderID != uid {
e.Pong[any](c, e.UserUnauthorized, nil)
return
}
// update problem
p.Title = utils.If(req.Title != "", req.Title, p.Title)
p.Statement = utils.If(req.Statement != "", req.Statement, p.Statement)
p.IsEnabled = req.IsEnabled p.IsEnabled = req.IsEnabled
p, status = h.problemService.Update(p) p, status = h.problemService.Update(p)

View File

@ -8,37 +8,40 @@ import (
"time" "time"
) )
type uploadResponse struct {
Key string `json:"key"`
URL string `json:"url"`
}
// Upload // Upload
// @Summary get upload url // @Summary [admin] get upload url
// @Description get upload url // @Description Retrieve a pre-signed upload URL from the object storage
// @Tags problem // @Tags problem,admin
// @Produce json // @Produce json
// @Response 200 {object} e.Response "upload url and key" // @Response 200 {object} e.Response[uploadResponse] "upload url and key"
// @Security Authentication // @Security Authentication
// @Router /v1/problem/upload [post] // @Router /v1/problem/upload [post]
func (h *handler) Upload(c *gin.Context) { func (h *handler) Upload(c *gin.Context) {
claim, exist := c.Get("claim") claim, exist := c.Get("claim")
if !exist { if !exist {
e.Pong(c, e.UserUnauthenticated, nil) e.Pong[any](c, e.UserUnauthenticated, nil)
return return
} }
// only admin can upload
role := claim.(*model.Claim).Role role := claim.(*model.Claim).Role
if role < model.RoleAdmin { if role < model.RoleAdmin {
e.Pong(c, e.UserUnauthorized, nil) e.Pong[any](c, e.UserUnauthorized, nil)
return return
} }
// generate random key
key := utils.RandomString(16) key := utils.RandomString(16)
url, status := h.storageService.Upload(key, time.Second*60*60) url, status := h.storageService.Upload(key, time.Second*60*60)
if status != e.Success { if status != e.Success {
e.Pong(c, status, nil) e.Pong[any](c, status, nil)
return return
} }
e.Pong(c, e.Success, gin.H{ e.Pong(c, e.Success, uploadResponse{Key: key, URL: url})
"key": key,
"url": url,
})
} }

View File

@ -3,6 +3,7 @@ package status
import ( import (
"git.0x7f.app/WOJ/woj-server/internal/misc/log" "git.0x7f.app/WOJ/woj-server/internal/misc/log"
"git.0x7f.app/WOJ/woj-server/internal/service/status" "git.0x7f.app/WOJ/woj-server/internal/service/status"
"git.0x7f.app/WOJ/woj-server/internal/service/submission"
"git.0x7f.app/WOJ/woj-server/internal/web/jwt" "git.0x7f.app/WOJ/woj-server/internal/web/jwt"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/samber/do" "github.com/samber/do"
@ -13,22 +14,26 @@ var _ Handler = (*handler)(nil)
type Handler interface { type Handler interface {
Query(c *gin.Context) Query(c *gin.Context)
QueryBySubmissionID(c *gin.Context)
QueryByProblemVersion(c *gin.Context) QueryByProblemVersion(c *gin.Context)
} }
type handler struct { type handler struct {
log *zap.Logger log *zap.Logger
statusService status.Service statusService status.Service
jwtService jwt.Service submissionService submission.Service
jwtService jwt.Service
} }
func RouteRegister(rg *gin.RouterGroup, i *do.Injector) { func RouteRegister(rg *gin.RouterGroup, i *do.Injector) {
app := &handler{ app := &handler{
log: do.MustInvoke[log.Service](i).GetLogger("api.status"), log: do.MustInvoke[log.Service](i).GetLogger("api.status"),
statusService: do.MustInvoke[status.Service](i), submissionService: do.MustInvoke[submission.Service](i),
jwtService: do.MustInvoke[jwt.Service](i), statusService: do.MustInvoke[status.Service](i),
jwtService: do.MustInvoke[jwt.Service](i),
} }
rg.POST("/query", app.Query) rg.POST("/query", app.jwtService.Handler(true), app.Query)
rg.POST("/query/problem_version", app.jwtService.Handler(true), app.QueryByProblemVersion) rg.POST("/query/submission", app.jwtService.Handler(true), app.QueryBySubmissionID)
rg.POST("/query/version", app.jwtService.Handler(true), app.QueryByProblemVersion)
} }

View File

@ -2,33 +2,74 @@ package status
import ( import (
"git.0x7f.app/WOJ/woj-server/internal/e" "git.0x7f.app/WOJ/woj-server/internal/e"
"git.0x7f.app/WOJ/woj-server/internal/model"
"git.0x7f.app/WOJ/woj-server/pkg/utils"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
type queryRequest struct { type queryRequest struct {
SubmissionID uint `form:"sid" binding:"required"` Pid uint `form:"pid"`
Uid uint `form:"uid"`
Offset int `form:"offset"`
Limit int `form:"limit" binding:"required"`
}
type queryResponse struct {
Submission model.Submission `json:"submission"`
Point int32 `json:"point"`
} }
// Query // Query
// @Summary query submissions by via submission id // @Summary query status via problem id or user id
// @Description query submissions by via submission id // @Description Batch query judgement status based on either the question or user.
// @Tags status // @Tags status
// @Accept application/x-www-form-urlencoded // @Accept application/x-www-form-urlencoded
// @Produce json // @Produce json
// @Param sid formData uint true "submission id" // @Param pid formData uint false "problem id"
// @Response 200 {object} e.Response "model.status" // @Param uid formData uint false "user id"
// @Param offset formData int false "start position"
// @Param limit formData int true "limit number of records"
// @Response 200 {object} e.Response[[]queryResponse] "queryResponse"
// @Router /v1/status/query [post] // @Router /v1/status/query [post]
func (h *handler) Query(c *gin.Context) { func (h *handler) Query(c *gin.Context) {
// TODO: add permission check claim, exist := c.Get("claim")
if !exist {
e.Pong[any](c, e.UserUnauthenticated, nil)
return
}
req := new(queryRequest) req := new(queryRequest)
if err := c.ShouldBind(req); err != nil { if err := c.ShouldBind(req); err != nil {
e.Pong(c, e.InvalidParameter, err.Error()) e.Pong(c, e.InvalidParameter, err.Error())
return return
} }
status, eStatus := h.statusService.Query(req.SubmissionID, true) if req.Pid == 0 && req.Uid == 0 {
e.Pong[any](c, e.InvalidParameter, nil)
return
}
e.Pong(c, eStatus, status) submissions, status := h.submissionService.Query(req.Pid, req.Uid, req.Offset, req.Limit)
uid := claim.(*model.Claim).UID
role := claim.(*model.Claim).Role
var response []*queryResponse
for _, submission := range submissions {
cur, _ := h.statusService.Query(submission.ID, false)
point := utils.If(cur == nil, -1, cur.Point)
resp := &queryResponse{
Submission: *submission,
Point: point,
}
if role < model.RoleAdmin || uid != submission.UserID {
// strip out code
resp.Submission.Code = ""
}
response = append(response, resp)
}
e.Pong(c, status, response)
} }

View File

@ -0,0 +1,50 @@
package status
import (
"git.0x7f.app/WOJ/woj-server/internal/e"
"git.0x7f.app/WOJ/woj-server/internal/model"
"github.com/gin-gonic/gin"
)
type queryOneRequest struct {
SubmissionID uint `form:"sid" binding:"required"`
}
// QueryBySubmissionID
// @Summary query status via submission id
// @Description Query the detailed results of the judgement based on the submission ID.
// @Tags status
// @Accept application/x-www-form-urlencoded
// @Produce json
// @Param sid formData uint true "submission id"
// @Response 200 {object} e.Response[model.Status] "submission status"
// @Router /v1/status/query/submission [post]
func (h *handler) QueryBySubmissionID(c *gin.Context) {
claim, exist := c.Get("claim")
if !exist {
e.Pong[any](c, e.UserUnauthenticated, nil)
return
}
req := new(queryOneRequest)
if err := c.ShouldBind(req); err != nil {
e.Pong(c, e.InvalidParameter, err.Error())
return
}
// query status
submitStatus, status := h.statusService.Query(req.SubmissionID, true)
// check permission
role := claim.(*model.Claim).Role
if role >= model.RoleAdmin || submitStatus.Submission.UserID == claim.(*model.Claim).UID {
// full status
e.Pong(c, status, submitStatus)
return
} else {
// strip out code
submitStatus.Submission.Code = ""
e.Pong(c, status, submitStatus)
return
}
}

View File

@ -13,39 +13,37 @@ type queryByVersionRequest struct {
} }
// QueryByProblemVersion // QueryByProblemVersion
// @Summary query submissions by problem version (admin only) // @Summary [admin] query status by problem version
// @Description query submissions by problem version (admin only) // @Description Retrieve all judgement results corresponding to the problem version.
// @Tags status // @Tags status,admin
// @Accept application/x-www-form-urlencoded // @Accept application/x-www-form-urlencoded
// @Produce json // @Produce json
// @Param pvid formData uint true "problem version id" // @Param pvid formData uint true "problem version"
// @Param offset formData int false "start position" // @Param offset formData int false "start position"
// @Param limit formData int true "limit number of records" // @Param limit formData int true "max number of results"
// @Response 200 {object} e.Response "[]*model.status" // @Response 200 {object} e.Response[[]model.Status] "submission status array"
// @Security Authentication // @Security Authentication
// @Router /v1/status/query/problem_version [post] // @Router /v1/status/query/version [post]
func (h *handler) QueryByProblemVersion(c *gin.Context) { func (h *handler) QueryByProblemVersion(c *gin.Context) {
claim, exist := c.Get("claim") claim, exist := c.Get("claim")
if !exist { if !exist {
e.Pong(c, e.UserUnauthenticated, nil) e.Pong[any](c, e.UserUnauthenticated, nil)
return return
} }
role := claim.(*model.Claim).Role
req := new(queryByVersionRequest) req := new(queryByVersionRequest)
if err := c.ShouldBind(req); err != nil { if err := c.ShouldBind(req); err != nil {
e.Pong(c, e.InvalidParameter, err.Error()) e.Pong(c, e.InvalidParameter, err.Error())
return return
} }
// check permission
role := claim.(*model.Claim).Role
if role < model.RoleAdmin { if role < model.RoleAdmin {
e.Pong(c, e.UserUnauthorized, nil) e.Pong[any](c, e.UserUnauthorized, nil)
return return
} }
statuses, eStatus := h.statusService.QueryByVersion(req.ProblemVersionID, req.Offset, req.Limit) submitStatus, status := h.statusService.QueryByVersion(req.ProblemVersionID, req.Offset, req.Limit)
e.Pong(c, status, submitStatus)
e.Pong(c, eStatus, statuses)
} }

View File

@ -14,21 +14,21 @@ type createRequest struct {
} }
// Create // Create
// @Summary create a submission // @Summary submit for judgement
// @Description create a submission // @Description Submit the code for judgement.
// @Tags submission // @Tags submission
// @Accept application/x-www-form-urlencoded // @Accept application/x-www-form-urlencoded
// @Produce json // @Produce json
// @Param pid formData int true "problem id" // @Param pid formData int true "problem id"
// @Param language formData string true "language" // @Param language formData string true "language"
// @Param code formData string true "code" // @Param code formData string true "code"
// @Response 200 {object} e.Response "" // @Response 200 {object} e.Response[uint] "submission id"
// @Security Authentication // @Security Authentication
// @Router /v1/submission/create [post] // @Router /v1/submission/create [post]
func (h *handler) Create(c *gin.Context) { func (h *handler) Create(c *gin.Context) {
claim, exist := c.Get("claim") claim, exist := c.Get("claim")
if !exist { if !exist {
e.Pong(c, e.UserUnauthenticated, nil) e.Pong[any](c, e.UserUnauthenticated, nil)
return return
} }
@ -37,7 +37,7 @@ func (h *handler) Create(c *gin.Context) {
// guest can not submit // guest can not submit
if role < model.RoleGeneral { if role < model.RoleGeneral {
e.Pong(c, e.UserUnauthorized, nil) e.Pong[any](c, e.UserUnauthorized, nil)
return return
} }
@ -47,30 +47,33 @@ func (h *handler) Create(c *gin.Context) {
return return
} }
// create submission
createData := &submission.CreateData{ createData := &submission.CreateData{
ProblemID: req.Pid, ProblemID: req.Pid,
UserID: uid, UserID: uid,
Language: req.Language, Language: req.Language,
Code: req.Code, Code: req.Code,
} }
s, status := h.submissionService.Create(createData) res, status := h.submissionService.Create(createData)
if status != e.Success { if status != e.Success {
e.Pong(c, status, nil) e.Pong[any](c, status, nil)
return return
} }
// query latest version
pv, status := h.problemService.QueryLatestVersion(req.Pid) pv, status := h.problemService.QueryLatestVersion(req.Pid)
if status != e.Success { if status != e.Success {
e.Pong(c, status, nil) e.Pong[any](c, status, nil)
return return
} }
// submit judge
payload := &model.SubmitJudgePayload{ payload := &model.SubmitJudgePayload{
ProblemVersionID: pv.ID, ProblemVersionID: pv.ID,
StorageKey: pv.StorageKey, StorageKey: pv.StorageKey,
Submission: *s, Submission: *res,
} }
_, status = h.taskService.SubmitJudge(payload) _, status = h.taskService.SubmitJudge(payload)
e.Pong(c, status, nil) e.Pong[any](c, status, res.ID)
} }

View File

@ -16,7 +16,6 @@ var _ Handler = (*handler)(nil)
type Handler interface { type Handler interface {
Create(c *gin.Context) Create(c *gin.Context)
Query(c *gin.Context)
Rejudge(c *gin.Context) Rejudge(c *gin.Context)
} }
@ -40,6 +39,5 @@ func RouteRegister(rg *gin.RouterGroup, i *do.Injector) {
} }
rg.POST("/create", app.jwtService.Handler(true), app.Create) rg.POST("/create", app.jwtService.Handler(true), app.Create)
rg.POST("/query", app.Query)
rg.POST("/rejudge", app.jwtService.Handler(true), app.Rejudge) rg.POST("/rejudge", app.jwtService.Handler(true), app.Rejudge)
} }

View File

@ -1,72 +0,0 @@
package submission
import (
"git.0x7f.app/WOJ/woj-server/internal/e"
"git.0x7f.app/WOJ/woj-server/internal/model"
"github.com/gin-gonic/gin"
)
type queryRequest struct {
Pid uint `form:"pid"`
Uid uint `form:"uid"`
Offset int `form:"offset"`
Limit int `form:"limit" binding:"required"`
}
type queryResponse struct {
Submission model.Submission `json:"submission"`
Point int32 `json:"point"`
}
// Query
// @Summary Query submissions
// @Description Query submissions
// @Tags submission
// @Accept application/x-www-form-urlencoded
// @Produce json
// @Param pid formData uint false "problem id"
// @Param uid formData uint false "user id"
// @Param offset formData int false "start position"
// @Param limit formData int true "limit number of records"
// @Response 200 {object} e.Response "queryResponse"
// @Router /v1/submission/query [post]
func (h *handler) Query(c *gin.Context) {
req := new(queryRequest)
if err := c.ShouldBind(req); err != nil {
e.Pong(c, e.InvalidParameter, err.Error())
return
}
if req.Pid == 0 && req.Uid == 0 {
e.Pong(c, e.InvalidParameter, nil)
return
}
submissions, status := h.submissionService.Query(req.Pid, req.Uid, req.Offset, req.Limit)
var response []*queryResponse
for _, submission := range submissions {
currentStatus, _ := h.statusService.Query(submission.ID, false)
var currentPoint int32
if currentStatus == nil {
currentPoint = -1
} else {
currentPoint = currentStatus.Point
}
newResponse := &queryResponse{
Submission: *submission,
Point: currentPoint,
}
// TODO: only show code when user is admin or the code is submitted by the user
newResponse.Submission.Code = ""
response = append(response, newResponse)
}
e.Pong(c, status, response)
}

View File

@ -11,53 +11,55 @@ type rejudgeRequest struct {
} }
// Rejudge // Rejudge
// @Summary rejudge a submission // @Summary [admin] rejudge a specific submission
// @Description rejudge a submission // @Description rejudge a specific submission
// @Tags submission // @Tags submission,admin
// @Accept application/x-www-form-urlencoded // @Accept application/x-www-form-urlencoded
// @Produce json // @Produce json
// @Param sid formData int true "submission id" // @Param sid formData int true "submission id"
// @Response 200 {object} e.Response "" // @Response 200 {object} e.Response[any] "nothing"
// @Security Authentication // @Security Authentication
// @Router /v1/submission/rejudge [post] // @Router /v1/submission/rejudge [post]
func (h *handler) Rejudge(c *gin.Context) { func (h *handler) Rejudge(c *gin.Context) {
claim, exist := c.Get("claim") claim, exist := c.Get("claim")
if !exist { if !exist {
e.Pong(c, e.UserUnauthenticated, nil) e.Pong[any](c, e.UserUnauthenticated, nil)
return return
} }
role := claim.(*model.Claim).Role
req := new(rejudgeRequest) req := new(rejudgeRequest)
if err := c.ShouldBind(req); err != nil { if err := c.ShouldBind(req); err != nil {
e.Pong(c, e.InvalidParameter, err.Error()) e.Pong(c, e.InvalidParameter, err.Error())
return return
} }
// only admin can rejudge // only admin can rejudge
role := claim.(*model.Claim).Role
if role < model.RoleAdmin { if role < model.RoleAdmin {
e.Pong(c, e.UserUnauthorized, nil) e.Pong[any](c, e.UserUnauthorized, nil)
return return
} }
// query submission
s, status := h.submissionService.QueryBySid(req.Sid, false) s, status := h.submissionService.QueryBySid(req.Sid, false)
if status != e.Success { if status != e.Success {
e.Pong(c, status, nil) e.Pong[any](c, status, nil)
return return
} }
// query latest problem version
pv, status := h.problemService.QueryLatestVersion(s.ProblemID) pv, status := h.problemService.QueryLatestVersion(s.ProblemID)
if status != e.Success { if status != e.Success {
e.Pong(c, status, nil) e.Pong[any](c, status, nil)
return return
} }
// submit judge
_, status = h.taskService.SubmitJudge(&model.SubmitJudgePayload{ _, status = h.taskService.SubmitJudge(&model.SubmitJudgePayload{
ProblemVersionID: pv.ID, ProblemVersionID: pv.ID,
StorageKey: pv.StorageKey, StorageKey: pv.StorageKey,
Submission: *s, Submission: *s,
}) })
e.Pong(c, status, nil) e.Pong[any](c, status, nil)
} }

View File

@ -22,16 +22,16 @@ type createRequest struct {
// @Param username formData string true "username" // @Param username formData string true "username"
// @Param nickname formData string true "nickname" // @Param nickname formData string true "nickname"
// @Param password formData string true "password" // @Param password formData string true "password"
// @Response 200 {object} e.Response "jwt token" // @Response 200 {object} e.Response[string] "jwt token"
// @Router /v1/user/create [post] // @Router /v1/user/create [post]
func (h *handler) Create(c *gin.Context) { func (h *handler) Create(c *gin.Context) {
req := new(createRequest) req := new(createRequest)
if err := c.ShouldBind(req); err != nil { if err := c.ShouldBind(req); err != nil {
e.Pong(c, e.InvalidParameter, err.Error()) e.Pong(c, e.InvalidParameter, err.Error())
return return
} }
// create user
createData := &user.CreateData{ createData := &user.CreateData{
UserName: req.UserName, UserName: req.UserName,
Password: req.Password, Password: req.Password,
@ -39,16 +39,18 @@ func (h *handler) Create(c *gin.Context) {
} }
u, status := h.userService.Create(createData) u, status := h.userService.Create(createData)
if status != e.Success { if status != e.Success {
e.Pong(c, status, nil) e.Pong[any](c, status, nil)
return return
} }
// update version in cache
version, status := h.userService.IncrVersion(u.ID) version, status := h.userService.IncrVersion(u.ID)
if status != e.Success { if status != e.Success {
e.Pong(c, status, nil) e.Pong[any](c, status, nil)
return return
} }
// sign jwt token
claim := &model.Claim{ claim := &model.Claim{
UID: u.ID, UID: u.ID,
Role: u.Role, Role: u.Role,

View File

@ -12,6 +12,11 @@ type loginRequest struct {
Password string `form:"password" binding:"required"` Password string `form:"password" binding:"required"`
} }
type loginResponse struct {
Token string `json:"token"`
NickName string `json:"nickname"`
}
// Login // Login
// @Summary login // @Summary login
// @Description login and return token // @Description login and return token
@ -20,13 +25,12 @@ type loginRequest struct {
// @Produce json // @Produce json
// @Param username formData string true "username" // @Param username formData string true "username"
// @Param password formData string true "password" // @Param password formData string true "password"
// @Response 200 {object} e.Response "jwt token and user nickname" // @Response 200 {object} e.Response[loginResponse] "jwt token and user's nickname"
// @Router /v1/user/login [post] // @Router /v1/user/login [post]
func (h *handler) Login(c *gin.Context) { func (h *handler) Login(c *gin.Context) {
req := new(loginRequest) req := new(loginRequest)
if err := c.ShouldBind(req); err != nil { if err := c.ShouldBind(req); err != nil {
e.Pong(c, e.InvalidParameter, nil) e.Pong(c, e.InvalidParameter, err.Error())
return return
} }
@ -37,14 +41,14 @@ func (h *handler) Login(c *gin.Context) {
} }
u, status := h.userService.Login(loginData) u, status := h.userService.Login(loginData)
if status != e.Success { if status != e.Success {
e.Pong(c, status, nil) e.Pong[any](c, status, nil)
return return
} }
// sign and return token // sign and return token
version, status := h.userService.IncrVersion(u.ID) version, status := h.userService.IncrVersion(u.ID)
if status != e.Success { if status != e.Success {
e.Pong(c, status, nil) e.Pong[any](c, status, nil)
return return
} }
claim := &model.Claim{ claim := &model.Claim{
@ -53,8 +57,5 @@ func (h *handler) Login(c *gin.Context) {
Version: version, Version: version,
} }
token, status := h.jwtService.SignClaim(claim) token, status := h.jwtService.SignClaim(claim)
e.Pong(c, status, gin.H{ e.Pong(c, status, loginResponse{Token: token, NickName: u.NickName})
"token": token,
"nickname": u.NickName,
})
} }

View File

@ -12,16 +12,16 @@ import (
// @Tags user // @Tags user
// @Accept application/x-www-form-urlencoded // @Accept application/x-www-form-urlencoded
// @Produce json // @Produce json
// @Response 200 {object} e.Response "nil" // @Response 200 {object} e.Response[any] "nothing"
// @Security Authentication // @Security Authentication
// @Router /v1/user/logout [post] // @Router /v1/user/logout [post]
func (h *handler) Logout(c *gin.Context) { func (h *handler) Logout(c *gin.Context) {
claim, exist := c.Get("claim") claim, exist := c.Get("claim")
if !exist { if !exist {
e.Pong(c, e.UserUnauthenticated, nil) e.Pong[any](c, e.UserUnauthenticated, nil)
return return
} }
_, status := h.userService.IncrVersion(claim.(*model.Claim).UID) _, status := h.userService.IncrVersion(claim.(*model.Claim).UID)
e.Pong(c, status, nil) e.Pong[any](c, status, nil)
} }

View File

@ -3,6 +3,7 @@ package user
import ( import (
"git.0x7f.app/WOJ/woj-server/internal/e" "git.0x7f.app/WOJ/woj-server/internal/e"
"git.0x7f.app/WOJ/woj-server/internal/model" "git.0x7f.app/WOJ/woj-server/internal/model"
"git.0x7f.app/WOJ/woj-server/pkg/utils"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@ -17,15 +18,13 @@ type profileRequest struct {
// @Accept application/x-www-form-urlencoded // @Accept application/x-www-form-urlencoded
// @Produce json // @Produce json
// @Param uid formData int false "user id" // @Param uid formData int false "user id"
// @Response 200 {object} e.Response "user info" // @Response 200 {object} e.Response[model.User] "user info"
// @Security Authentication // @Security Authentication
// @Router /v1/user/profile [post] // @Router /v1/user/profile [post]
func (h *handler) Profile(c *gin.Context) { func (h *handler) Profile(c *gin.Context) {
// TODO: create a new struct for profile (user info & solve info)
claim, exist := c.Get("claim") claim, exist := c.Get("claim")
if !exist { if !exist {
e.Pong(c, e.UserUnauthenticated, nil) e.Pong[any](c, e.UserUnauthenticated, nil)
return return
} }
@ -33,22 +32,21 @@ func (h *handler) Profile(c *gin.Context) {
role := claim.(*model.Claim).Role role := claim.(*model.Claim).Role
req := new(profileRequest) req := new(profileRequest)
if err := c.ShouldBind(req); err != nil { if err := c.ShouldBind(req); err != nil {
e.Pong(c, e.InvalidParameter, nil) e.Pong(c, e.InvalidParameter, err.Error())
return return
} }
if req.UID == 0 { user, status := h.userService.Profile(utils.If(req.UID == 0, uid, req.UID))
req.UID = uid if status != e.Success {
} else if req.UID != uid && role < model.RoleGeneral { e.Pong[any](c, status, nil)
e.Pong(c, e.UserUnauthorized, nil)
return return
} }
user, status := h.userService.Profile(req.UID) if role < model.RoleAdmin && user.ID != uid {
e.Pong[any](c, e.UserUnauthorized, nil)
// TODO: >= admin can see is_enable return
}
e.Pong(c, status, user) e.Pong(c, status, user)
} }

View File

@ -6,30 +6,21 @@ import (
"net/http" "net/http"
) )
type Response struct { type Response[T any] struct {
Code int `json:"code"` Code int `json:"code"`
Msg string `json:"msg"` Msg string `json:"msg"`
Body interface{} `json:"body"` Body T `json:"body"`
} }
func Wrap(status Status, body interface{}) interface{} { func wrap[T any](status Status, body T) Response[interface{}] {
return Response{ return Response[interface{}]{
Code: int(status), Code: int(status),
Msg: status.String(), Msg: status.String(),
Body: utils.If(status == Success, body, nil), Body: utils.If[interface{}](status == Success, body, nil),
} }
} }
func Pong(c *gin.Context, status Status, body interface{}) { func Pong[T any](c *gin.Context, status Status, body T) {
c.Set("err", status) c.Set("err", status)
c.JSON(http.StatusOK, Wrap(status, body)) c.JSON(http.StatusOK, wrap(status, body))
}
type Endpoint func(*gin.Context) (Status, interface{})
func PongWrapper(handler Endpoint) func(*gin.Context) {
return func(c *gin.Context) {
status, body := handler(c)
Pong(c, status, body)
}
} }

View File

@ -37,7 +37,7 @@ func (s *service) Handler(forced bool) gin.HandlerFunc {
c.Set("claim", claim) c.Set("claim", claim)
} }
if forced && status != e.Success { if forced && status != e.Success {
e.Pong(c, status, nil) e.Pong[any](c, status, nil)
c.Abort() c.Abort()
} else { } else {
c.Next() c.Next()

View File

@ -11,8 +11,8 @@ import (
"github.com/samber/do" "github.com/samber/do"
) )
// @title OJ Server API Documentation // @title WOJ Server API Documentation
// @version 1.0 // @version 1.1.0
// @BasePath /api // @BasePath /api
// @securityDefinitions.apikey Authentication // @securityDefinitions.apikey Authentication
// @in header // @in header