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:
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 ./...

View File

@ -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)
}

View File

@ -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)

View File

@ -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
}

View File

@ -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(),
})
}

View File

@ -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

View File

@ -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)

View File

@ -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})
}

View File

@ -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)
}

View File

@ -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)
}

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
// @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)
}

View File

@ -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)
}

View File

@ -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)
}

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
// @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)
}

View File

@ -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,

View File

@ -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})
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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))
}

View File

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

View File

@ -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