From eb6f5d0aca08dc80c47fbe755ee371842baa52bf Mon Sep 17 00:00:00 2001 From: Paul Pan Date: Fri, 22 Dec 2023 15:19:13 +0800 Subject: [PATCH] feat: use generic Response type and rewrite swagger documentation. close #1 --- Makefile | 2 +- internal/api/debug/handler.go | 4 +- internal/api/debug/random.go | 10 +-- .../{createVersion.go => create_version.go} | 36 ++++++---- internal/api/problem/details.go | 21 +++--- internal/api/problem/search.go | 10 +-- internal/api/problem/update.go | 36 ++++++---- internal/api/problem/upload.go | 27 +++---- internal/api/status/handler.go | 21 +++--- internal/api/status/query.go | 59 ++++++++++++--- internal/api/status/query_one.go | 50 +++++++++++++ .../{queryByVersion.go => query_version.go} | 30 ++++---- internal/api/submission/create.go | 23 +++--- internal/api/submission/handler.go | 2 - internal/api/submission/query.go | 72 ------------------- internal/api/submission/rejudge.go | 26 +++---- internal/api/user/create.go | 10 +-- internal/api/user/login.go | 19 ++--- internal/api/user/logout.go | 6 +- internal/api/user/profile.go | 24 +++---- internal/e/resp.go | 27 +++---- internal/web/jwt/middleware.go | 2 +- internal/web/router/api.go | 4 +- 23 files changed, 281 insertions(+), 240 deletions(-) rename internal/api/problem/{createVersion.go => create_version.go} (62%) create mode 100644 internal/api/status/query_one.go rename internal/api/status/{queryByVersion.go => query_version.go} (52%) delete mode 100644 internal/api/submission/query.go diff --git a/Makefile b/Makefile index 288fb5c..ce25cb9 100644 --- a/Makefile +++ b/Makefile @@ -31,7 +31,7 @@ dep: swagger: 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: go fmt ./... diff --git a/internal/api/debug/handler.go b/internal/api/debug/handler.go index 50796fd..5d930e2 100644 --- a/internal/api/debug/handler.go +++ b/internal/api/debug/handler.go @@ -10,7 +10,7 @@ import ( var _ Handler = (*handler)(nil) type Handler interface { - randomString(c *gin.Context) + RandomString(c *gin.Context) } type handler struct { @@ -21,5 +21,5 @@ func RouteRegister(rg *gin.RouterGroup, i *do.Injector) { app := &handler{} app.log = do.MustInvoke[log.Service](i).GetLogger("api.debug") - rg.GET("/random", app.randomString) + rg.GET("/random", app.RandomString) } diff --git a/internal/api/debug/random.go b/internal/api/debug/random.go index d2221e4..f174930 100644 --- a/internal/api/debug/random.go +++ b/internal/api/debug/random.go @@ -7,14 +7,14 @@ import ( "go.uber.org/zap" ) -// randomString -// @Summary random string -// @Description generate random string with length = 32 +// RandomString +// @Summary generate random string +// @Description Generate random string with length = 32. // @Tags debug // @Produce json -// @Response 200 {object} e.Response "random string" +// @Response 200 {object} e.Response[string] "random string" // @Router /debug/random [get] -func (h *handler) randomString(c *gin.Context) { +func (h *handler) RandomString(c *gin.Context) { str := utils.RandomString(32) h.log.Info("random string", zap.String("str", str)) e.Pong(c, e.Success, str) diff --git a/internal/api/problem/createVersion.go b/internal/api/problem/create_version.go similarity index 62% rename from internal/api/problem/createVersion.go rename to internal/api/problem/create_version.go index 4302f17..6ab028e 100644 --- a/internal/api/problem/createVersion.go +++ b/internal/api/problem/create_version.go @@ -13,55 +13,61 @@ type createVersionRequest struct { } // CreateVersion -// @Summary create a problem version -// @Description create a problem version -// @Tags problem +// @Summary [admin] create a problem version +// @Description Create a problem version associated with `pid`. +// @Tags problem,admin // @Accept application/x-www-form-urlencoded // @Produce json // @Param pid formData int true "problem id" -// @Param storage_key formData string true "storage key" -// @Response 200 {object} e.Response "" +// @Param storage_key formData string true "storage key, zip file containing problem data" +// @Response 200 {object} e.Response[any] "nothing" // @Security Authentication // @Router /v1/problem/create_version [post] func (h *handler) CreateVersion(c *gin.Context) { claim, exist := c.Get("claim") if !exist { - e.Pong(c, e.UserUnauthenticated, nil) + e.Pong[any](c, e.UserUnauthenticated, nil) return } - // uid := claim.(*model.Claim).UID - - role := claim.(*model.Claim).Role req := new(createVersionRequest) - if err := c.ShouldBind(req); err != nil { e.Pong(c, e.InvalidParameter, err.Error()) return } - // guest can not submit + // only admin can create problem version + role := claim.(*model.Claim).Role if role < model.RoleAdmin { - e.Pong(c, e.UserUnauthorized, nil) + e.Pong[any](c, e.UserUnauthorized, nil) 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{ ProblemID: req.ProblemID, StorageKey: req.StorageKey, } pv, status := h.problemService.CreateVersion(createVersionData) if status != e.Success { - e.Pong(c, status, nil) + e.Pong[any](c, status, nil) return } + // enqueue task: runner build problem payload := &model.ProblemBuildPayload{ ProblemVersionID: pv.ID, StorageKey: pv.StorageKey, } _, status = h.taskService.ProblemBuild(payload) - e.Pong(c, status, nil) + e.Pong[any](c, status, nil) + + // TODO: if failed, delete problem version } diff --git a/internal/api/problem/details.go b/internal/api/problem/details.go index 201d2ff..337e287 100644 --- a/internal/api/problem/details.go +++ b/internal/api/problem/details.go @@ -10,18 +10,22 @@ type detailsRequest struct { Pid uint `form:"pid"` } +type problemDetailsResponse struct { + Problem *model.Problem `json:"problem"` + Context interface{} `json:"context"` +} + // Details // @Summary get details of a problem -// @Description get details of a problem +// @Description Get details of a problem. // @Tags problem // @Accept application/x-www-form-urlencoded // @Produce json // @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] func (h *handler) Details(c *gin.Context) { req := new(detailsRequest) - if err := c.ShouldBind(req); err != nil { e.Pong(c, e.InvalidParameter, err.Error()) return @@ -32,17 +36,18 @@ func (h *handler) Details(c *gin.Context) { p, status := h.problemService.Query(req.Pid, true, shouldEnable) if status != e.Success { - e.Pong(c, status, nil) + e.Pong[any](c, status, nil) return } pv, status := h.problemService.QueryLatestVersion(req.Pid) if status != e.Success { - e.Pong(c, status, nil) + e.Pong[any](c, status, nil) return } - e.Pong(c, e.Success, gin.H{ - "problem": p, - "context": pv.Context.Get(), + + e.Pong(c, e.Success, problemDetailsResponse{ + Problem: p, + Context: pv.Context.Get(), }) } diff --git a/internal/api/problem/search.go b/internal/api/problem/search.go index d473de3..1b64f69 100644 --- a/internal/api/problem/search.go +++ b/internal/api/problem/search.go @@ -2,6 +2,7 @@ package problem import ( "git.0x7f.app/WOJ/woj-server/internal/e" + _ "git.0x7f.app/WOJ/woj-server/internal/model" // swag requires this "github.com/gin-gonic/gin" ) @@ -10,17 +11,16 @@ type searchRequest struct { } // Search -// @Summary get detail of a problem -// @Description get detail of a problem +// @Summary search for problems +// @Description Search for problems based on keywords. If the keyword is empty, return all problems. // @Tags problem // @Accept application/x-www-form-urlencoded // @Produce json -// @Param search formData string false "word search" -// @Response 200 {object} e.Response "problemset" +// @Param search formData string false "keyword" +// @Response 200 {object} e.Response[[]model.Problem] "problems found" // @Router /v1/problem/search [post] func (h *handler) Search(c *gin.Context) { req := new(searchRequest) - if err := c.ShouldBind(req); err != nil { e.Pong(c, e.InvalidParameter, err.Error()) return diff --git a/internal/api/problem/update.go b/internal/api/problem/update.go index 4fd36c8..8909108 100644 --- a/internal/api/problem/update.go +++ b/internal/api/problem/update.go @@ -4,6 +4,7 @@ import ( "git.0x7f.app/WOJ/woj-server/internal/e" "git.0x7f.app/WOJ/woj-server/internal/model" "git.0x7f.app/WOJ/woj-server/internal/service/problem" + "git.0x7f.app/WOJ/woj-server/pkg/utils" "github.com/gin-gonic/gin" ) @@ -15,29 +16,31 @@ type updateRequest struct { } // Update -// @Summary create or update a problem -// @Description create or update a problem -// @Tags problem +// @Summary [admin] create or update a problem +// @Description Create or update a problem. +// @Tags problem,admin // @Accept application/x-www-form-urlencoded // @Produce json // @Param pid formData int false "problem id, 0 for create" // @Param title formData string true "title" // @Param statement formData string true "statement" // @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 // @Router /v1/problem/update [post] func (h *handler) Update(c *gin.Context) { claim, exist := c.Get("claim") if !exist { - e.Pong(c, e.UserUnauthenticated, nil) + e.Pong[any](c, e.UserUnauthenticated, nil) return } uid := claim.(*model.Claim).UID role := claim.(*model.Claim).Role + + // only admin can modify problem if role < model.RoleAdmin { - e.Pong(c, e.UserUnauthorized, nil) + e.Pong[any](c, e.UserUnauthorized, nil) return } @@ -48,6 +51,7 @@ func (h *handler) Update(c *gin.Context) { } if req.Pid == 0 { + // create problem createData := &problem.CreateData{ Title: req.Title, Statement: req.Statement, @@ -58,18 +62,24 @@ func (h *handler) Update(c *gin.Context) { e.Pong(c, status, p) return } else { + // update problem + + // check if problem exists p, status := h.problemService.Query(req.Pid, true, false) if status != e.Success { - e.Pong(c, status, nil) - return - } - if p.ProviderID != uid { - e.Pong(c, e.UserUnauthorized, nil) + e.Pong[any](c, status, nil) return } - p.Title = req.Title - p.Statement = req.Statement + // check if user is the provider of the problem + 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, status = h.problemService.Update(p) diff --git a/internal/api/problem/upload.go b/internal/api/problem/upload.go index 746ba02..20d2333 100644 --- a/internal/api/problem/upload.go +++ b/internal/api/problem/upload.go @@ -8,37 +8,40 @@ import ( "time" ) +type uploadResponse struct { + Key string `json:"key"` + URL string `json:"url"` +} + // Upload -// @Summary get upload url -// @Description get upload url -// @Tags problem +// @Summary [admin] get upload url +// @Description Retrieve a pre-signed upload URL from the object storage +// @Tags problem,admin // @Produce json -// @Response 200 {object} e.Response "upload url and key" +// @Response 200 {object} e.Response[uploadResponse] "upload url and key" // @Security Authentication // @Router /v1/problem/upload [post] func (h *handler) Upload(c *gin.Context) { claim, exist := c.Get("claim") if !exist { - e.Pong(c, e.UserUnauthenticated, nil) + e.Pong[any](c, e.UserUnauthenticated, nil) return } + // only admin can upload role := claim.(*model.Claim).Role if role < model.RoleAdmin { - e.Pong(c, e.UserUnauthorized, nil) + e.Pong[any](c, e.UserUnauthorized, nil) return } + // generate random key key := utils.RandomString(16) url, status := h.storageService.Upload(key, time.Second*60*60) - if status != e.Success { - e.Pong(c, status, nil) + e.Pong[any](c, status, nil) return } - e.Pong(c, e.Success, gin.H{ - "key": key, - "url": url, - }) + e.Pong(c, e.Success, uploadResponse{Key: key, URL: url}) } diff --git a/internal/api/status/handler.go b/internal/api/status/handler.go index eb31276..42b9aff 100644 --- a/internal/api/status/handler.go +++ b/internal/api/status/handler.go @@ -3,6 +3,7 @@ package status import ( "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/submission" "git.0x7f.app/WOJ/woj-server/internal/web/jwt" "github.com/gin-gonic/gin" "github.com/samber/do" @@ -13,22 +14,26 @@ var _ Handler = (*handler)(nil) type Handler interface { Query(c *gin.Context) + QueryBySubmissionID(c *gin.Context) QueryByProblemVersion(c *gin.Context) } type handler struct { - log *zap.Logger - statusService status.Service - jwtService jwt.Service + log *zap.Logger + statusService status.Service + submissionService submission.Service + jwtService jwt.Service } func RouteRegister(rg *gin.RouterGroup, i *do.Injector) { app := &handler{ - log: do.MustInvoke[log.Service](i).GetLogger("api.status"), - statusService: do.MustInvoke[status.Service](i), - jwtService: do.MustInvoke[jwt.Service](i), + log: do.MustInvoke[log.Service](i).GetLogger("api.status"), + submissionService: do.MustInvoke[submission.Service](i), + statusService: do.MustInvoke[status.Service](i), + jwtService: do.MustInvoke[jwt.Service](i), } - rg.POST("/query", app.Query) - rg.POST("/query/problem_version", app.jwtService.Handler(true), app.QueryByProblemVersion) + rg.POST("/query", app.jwtService.Handler(true), app.Query) + rg.POST("/query/submission", app.jwtService.Handler(true), app.QueryBySubmissionID) + rg.POST("/query/version", app.jwtService.Handler(true), app.QueryByProblemVersion) } diff --git a/internal/api/status/query.go b/internal/api/status/query.go index ebea0d1..a686269 100644 --- a/internal/api/status/query.go +++ b/internal/api/status/query.go @@ -2,33 +2,74 @@ package status import ( "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" ) 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 -// @Summary query submissions by via submission id -// @Description query submissions by via submission id +// @Summary query status via problem id or user id +// @Description Batch query judgement status based on either the question or user. // @Tags status // @Accept application/x-www-form-urlencoded // @Produce json -// @Param sid formData uint true "submission id" -// @Response 200 {object} e.Response "model.status" +// @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] "queryResponse" // @Router /v1/status/query [post] 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) - if err := c.ShouldBind(req); err != nil { e.Pong(c, e.InvalidParameter, err.Error()) 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) } diff --git a/internal/api/status/query_one.go b/internal/api/status/query_one.go new file mode 100644 index 0000000..b0e9834 --- /dev/null +++ b/internal/api/status/query_one.go @@ -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 + } +} diff --git a/internal/api/status/queryByVersion.go b/internal/api/status/query_version.go similarity index 52% rename from internal/api/status/queryByVersion.go rename to internal/api/status/query_version.go index c6a4b3b..73e1aab 100644 --- a/internal/api/status/queryByVersion.go +++ b/internal/api/status/query_version.go @@ -13,39 +13,37 @@ type queryByVersionRequest struct { } // QueryByProblemVersion -// @Summary query submissions by problem version (admin only) -// @Description query submissions by problem version (admin only) -// @Tags status +// @Summary [admin] query status by problem version +// @Description Retrieve all judgement results corresponding to the problem version. +// @Tags status,admin // @Accept application/x-www-form-urlencoded // @Produce json -// @Param pvid formData uint true "problem version id" -// @Param offset formData int false "start position" -// @Param limit formData int true "limit number of records" -// @Response 200 {object} e.Response "[]*model.status" +// @Param pvid formData uint true "problem version" +// @Param offset formData int false "start position" +// @Param limit formData int true "max number of results" +// @Response 200 {object} e.Response[[]model.Status] "submission status array" // @Security Authentication -// @Router /v1/status/query/problem_version [post] +// @Router /v1/status/query/version [post] func (h *handler) QueryByProblemVersion(c *gin.Context) { claim, exist := c.Get("claim") if !exist { - e.Pong(c, e.UserUnauthenticated, nil) + e.Pong[any](c, e.UserUnauthenticated, nil) return } - role := claim.(*model.Claim).Role - req := new(queryByVersionRequest) - if err := c.ShouldBind(req); err != nil { e.Pong(c, e.InvalidParameter, err.Error()) return } + // check permission + role := claim.(*model.Claim).Role if role < model.RoleAdmin { - e.Pong(c, e.UserUnauthorized, nil) + e.Pong[any](c, e.UserUnauthorized, nil) return } - statuses, eStatus := h.statusService.QueryByVersion(req.ProblemVersionID, req.Offset, req.Limit) - - e.Pong(c, eStatus, statuses) + submitStatus, status := h.statusService.QueryByVersion(req.ProblemVersionID, req.Offset, req.Limit) + e.Pong(c, status, submitStatus) } diff --git a/internal/api/submission/create.go b/internal/api/submission/create.go index e4cabd5..70dcb8a 100644 --- a/internal/api/submission/create.go +++ b/internal/api/submission/create.go @@ -14,21 +14,21 @@ type createRequest struct { } // Create -// @Summary create a submission -// @Description create a submission +// @Summary submit for judgement +// @Description Submit the code for judgement. // @Tags submission // @Accept application/x-www-form-urlencoded // @Produce json // @Param pid formData int true "problem id" // @Param language formData string true "language" // @Param code formData string true "code" -// @Response 200 {object} e.Response "" +// @Response 200 {object} e.Response[uint] "submission id" // @Security Authentication // @Router /v1/submission/create [post] func (h *handler) Create(c *gin.Context) { claim, exist := c.Get("claim") if !exist { - e.Pong(c, e.UserUnauthenticated, nil) + e.Pong[any](c, e.UserUnauthenticated, nil) return } @@ -37,7 +37,7 @@ func (h *handler) Create(c *gin.Context) { // guest can not submit if role < model.RoleGeneral { - e.Pong(c, e.UserUnauthorized, nil) + e.Pong[any](c, e.UserUnauthorized, nil) return } @@ -47,30 +47,33 @@ func (h *handler) Create(c *gin.Context) { return } + // create submission createData := &submission.CreateData{ ProblemID: req.Pid, UserID: uid, Language: req.Language, Code: req.Code, } - s, status := h.submissionService.Create(createData) + res, status := h.submissionService.Create(createData) if status != e.Success { - e.Pong(c, status, nil) + e.Pong[any](c, status, nil) return } + // query latest version pv, status := h.problemService.QueryLatestVersion(req.Pid) if status != e.Success { - e.Pong(c, status, nil) + e.Pong[any](c, status, nil) return } + // submit judge payload := &model.SubmitJudgePayload{ ProblemVersionID: pv.ID, StorageKey: pv.StorageKey, - Submission: *s, + Submission: *res, } _, status = h.taskService.SubmitJudge(payload) - e.Pong(c, status, nil) + e.Pong[any](c, status, res.ID) } diff --git a/internal/api/submission/handler.go b/internal/api/submission/handler.go index 6cc1de3..68e2860 100644 --- a/internal/api/submission/handler.go +++ b/internal/api/submission/handler.go @@ -16,7 +16,6 @@ var _ Handler = (*handler)(nil) type Handler interface { Create(c *gin.Context) - Query(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("/query", app.Query) rg.POST("/rejudge", app.jwtService.Handler(true), app.Rejudge) } diff --git a/internal/api/submission/query.go b/internal/api/submission/query.go deleted file mode 100644 index a44d569..0000000 --- a/internal/api/submission/query.go +++ /dev/null @@ -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) -} diff --git a/internal/api/submission/rejudge.go b/internal/api/submission/rejudge.go index ef43dd9..08a7df5 100644 --- a/internal/api/submission/rejudge.go +++ b/internal/api/submission/rejudge.go @@ -11,53 +11,55 @@ type rejudgeRequest struct { } // Rejudge -// @Summary rejudge a submission -// @Description rejudge a submission -// @Tags submission +// @Summary [admin] rejudge a specific submission +// @Description rejudge a specific submission +// @Tags submission,admin // @Accept application/x-www-form-urlencoded // @Produce json -// @Param sid formData int true "submission id" -// @Response 200 {object} e.Response "" +// @Param sid formData int true "submission id" +// @Response 200 {object} e.Response[any] "nothing" // @Security Authentication // @Router /v1/submission/rejudge [post] func (h *handler) Rejudge(c *gin.Context) { claim, exist := c.Get("claim") if !exist { - e.Pong(c, e.UserUnauthenticated, nil) + e.Pong[any](c, e.UserUnauthenticated, nil) return } - role := claim.(*model.Claim).Role req := new(rejudgeRequest) - if err := c.ShouldBind(req); err != nil { e.Pong(c, e.InvalidParameter, err.Error()) return } // only admin can rejudge + role := claim.(*model.Claim).Role if role < model.RoleAdmin { - e.Pong(c, e.UserUnauthorized, nil) + e.Pong[any](c, e.UserUnauthorized, nil) return } + // query submission s, status := h.submissionService.QueryBySid(req.Sid, false) if status != e.Success { - e.Pong(c, status, nil) + e.Pong[any](c, status, nil) return } + // query latest problem version pv, status := h.problemService.QueryLatestVersion(s.ProblemID) if status != e.Success { - e.Pong(c, status, nil) + e.Pong[any](c, status, nil) return } + // submit judge _, status = h.taskService.SubmitJudge(&model.SubmitJudgePayload{ ProblemVersionID: pv.ID, StorageKey: pv.StorageKey, Submission: *s, }) - e.Pong(c, status, nil) + e.Pong[any](c, status, nil) } diff --git a/internal/api/user/create.go b/internal/api/user/create.go index 7ec9599..e8d5148 100644 --- a/internal/api/user/create.go +++ b/internal/api/user/create.go @@ -22,16 +22,16 @@ type createRequest struct { // @Param username formData string true "username" // @Param nickname formData string true "nickname" // @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] func (h *handler) Create(c *gin.Context) { req := new(createRequest) - if err := c.ShouldBind(req); err != nil { e.Pong(c, e.InvalidParameter, err.Error()) return } + // create user createData := &user.CreateData{ UserName: req.UserName, Password: req.Password, @@ -39,16 +39,18 @@ func (h *handler) Create(c *gin.Context) { } u, status := h.userService.Create(createData) if status != e.Success { - e.Pong(c, status, nil) + e.Pong[any](c, status, nil) return } + // update version in cache version, status := h.userService.IncrVersion(u.ID) if status != e.Success { - e.Pong(c, status, nil) + e.Pong[any](c, status, nil) return } + // sign jwt token claim := &model.Claim{ UID: u.ID, Role: u.Role, diff --git a/internal/api/user/login.go b/internal/api/user/login.go index c6a67ff..42c6ed8 100644 --- a/internal/api/user/login.go +++ b/internal/api/user/login.go @@ -12,6 +12,11 @@ type loginRequest struct { Password string `form:"password" binding:"required"` } +type loginResponse struct { + Token string `json:"token"` + NickName string `json:"nickname"` +} + // Login // @Summary login // @Description login and return token @@ -20,13 +25,12 @@ type loginRequest struct { // @Produce json // @Param username formData string true "username" // @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] func (h *handler) Login(c *gin.Context) { req := new(loginRequest) - if err := c.ShouldBind(req); err != nil { - e.Pong(c, e.InvalidParameter, nil) + e.Pong(c, e.InvalidParameter, err.Error()) return } @@ -37,14 +41,14 @@ func (h *handler) Login(c *gin.Context) { } u, status := h.userService.Login(loginData) if status != e.Success { - e.Pong(c, status, nil) + e.Pong[any](c, status, nil) return } // sign and return token version, status := h.userService.IncrVersion(u.ID) if status != e.Success { - e.Pong(c, status, nil) + e.Pong[any](c, status, nil) return } claim := &model.Claim{ @@ -53,8 +57,5 @@ func (h *handler) Login(c *gin.Context) { Version: version, } token, status := h.jwtService.SignClaim(claim) - e.Pong(c, status, gin.H{ - "token": token, - "nickname": u.NickName, - }) + e.Pong(c, status, loginResponse{Token: token, NickName: u.NickName}) } diff --git a/internal/api/user/logout.go b/internal/api/user/logout.go index f1b2a34..0132c1f 100644 --- a/internal/api/user/logout.go +++ b/internal/api/user/logout.go @@ -12,16 +12,16 @@ import ( // @Tags user // @Accept application/x-www-form-urlencoded // @Produce json -// @Response 200 {object} e.Response "nil" +// @Response 200 {object} e.Response[any] "nothing" // @Security Authentication // @Router /v1/user/logout [post] func (h *handler) Logout(c *gin.Context) { claim, exist := c.Get("claim") if !exist { - e.Pong(c, e.UserUnauthenticated, nil) + e.Pong[any](c, e.UserUnauthenticated, nil) return } _, status := h.userService.IncrVersion(claim.(*model.Claim).UID) - e.Pong(c, status, nil) + e.Pong[any](c, status, nil) } diff --git a/internal/api/user/profile.go b/internal/api/user/profile.go index 68c85fa..72e798b 100644 --- a/internal/api/user/profile.go +++ b/internal/api/user/profile.go @@ -3,6 +3,7 @@ package user import ( "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" ) @@ -17,15 +18,13 @@ type profileRequest struct { // @Accept application/x-www-form-urlencoded // @Produce json // @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 // @Router /v1/user/profile [post] func (h *handler) Profile(c *gin.Context) { - // TODO: create a new struct for profile (user info & solve info) - claim, exist := c.Get("claim") if !exist { - e.Pong(c, e.UserUnauthenticated, nil) + e.Pong[any](c, e.UserUnauthenticated, nil) return } @@ -33,22 +32,21 @@ func (h *handler) Profile(c *gin.Context) { role := claim.(*model.Claim).Role req := new(profileRequest) - if err := c.ShouldBind(req); err != nil { - e.Pong(c, e.InvalidParameter, nil) + e.Pong(c, e.InvalidParameter, err.Error()) return } - if req.UID == 0 { - req.UID = uid - } else if req.UID != uid && role < model.RoleGeneral { - e.Pong(c, e.UserUnauthorized, nil) + user, status := h.userService.Profile(utils.If(req.UID == 0, uid, req.UID)) + if status != e.Success { + e.Pong[any](c, status, nil) return } - user, status := h.userService.Profile(req.UID) - - // TODO: >= admin can see is_enable + if role < model.RoleAdmin && user.ID != uid { + e.Pong[any](c, e.UserUnauthorized, nil) + return + } e.Pong(c, status, user) } diff --git a/internal/e/resp.go b/internal/e/resp.go index bc374d4..cd07d7f 100644 --- a/internal/e/resp.go +++ b/internal/e/resp.go @@ -6,30 +6,21 @@ import ( "net/http" ) -type Response struct { - Code int `json:"code"` - Msg string `json:"msg"` - Body interface{} `json:"body"` +type Response[T any] struct { + Code int `json:"code"` + Msg string `json:"msg"` + Body T `json:"body"` } -func Wrap(status Status, body interface{}) interface{} { - return Response{ +func wrap[T any](status Status, body T) Response[interface{}] { + return Response[interface{}]{ Code: int(status), 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.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) - } + c.JSON(http.StatusOK, wrap(status, body)) } diff --git a/internal/web/jwt/middleware.go b/internal/web/jwt/middleware.go index 4ea2d0b..e4195e6 100644 --- a/internal/web/jwt/middleware.go +++ b/internal/web/jwt/middleware.go @@ -37,7 +37,7 @@ func (s *service) Handler(forced bool) gin.HandlerFunc { c.Set("claim", claim) } if forced && status != e.Success { - e.Pong(c, status, nil) + e.Pong[any](c, status, nil) c.Abort() } else { c.Next() diff --git a/internal/web/router/api.go b/internal/web/router/api.go index 222cdbd..7344798 100644 --- a/internal/web/router/api.go +++ b/internal/web/router/api.go @@ -11,8 +11,8 @@ import ( "github.com/samber/do" ) -// @title OJ Server API Documentation -// @version 1.0 +// @title WOJ Server API Documentation +// @version 1.1.0 // @BasePath /api // @securityDefinitions.apikey Authentication // @in header