├── .gitattributes ├── .github ├── FUNDING.yml ├── pr-title-check.json └── workflows │ ├── cla.yml │ ├── coverage.yml │ ├── pr-title-checker.yml │ └── test.yml ├── .gitignore ├── .golangci.yaml ├── .pre-commit-config.yaml ├── app ├── controller │ ├── admin_logs.go │ ├── admin_logs_test.go │ ├── admin_user.go │ ├── admin_user_test.go │ ├── auth.go │ ├── auth_test.go │ ├── class.go │ ├── class_test.go │ ├── controller_test.go │ ├── images.go │ ├── images_test.go │ ├── judger.go │ ├── judger_test.go │ ├── problem.go │ ├── problem_set.go │ ├── problem_set_submission.go │ ├── problem_set_submission_test.go │ ├── problem_set_test.go │ ├── problem_test.go │ ├── script.go │ ├── script_test.go │ ├── submission.go │ ├── submission_test.go │ ├── test_test.go │ ├── todo.go │ ├── user.go │ ├── user_test.go │ └── webauthn.go ├── middleware │ ├── auth.go │ ├── authentication.go │ ├── authentication_test.go │ ├── judger.go │ ├── judger_test.go │ ├── middleware_test.go │ ├── param.go │ ├── param_test.go │ ├── permission.go │ ├── permission_test.go │ ├── recover.go │ └── recover_test.go ├── request │ ├── admin_logs.go │ ├── admin_user.go │ ├── auth.go │ ├── class.go │ ├── judger.go │ ├── problem.go │ ├── problem_set.go │ ├── problem_set_submission.go │ ├── script.go │ ├── submission.go │ └── user.go ├── response │ ├── admin_logs.go │ ├── admin_user.go │ ├── auth.go │ ├── basic.go │ ├── class.go │ ├── images.go │ ├── judger.go │ ├── problem.go │ ├── problem_set.go │ ├── problem_set_submission.go │ ├── readme.md │ ├── resource │ │ ├── class.go │ │ ├── permission.go │ │ ├── permission_test.go │ │ ├── problem.go │ │ ├── problem_set.go │ │ ├── problem_test.go │ │ ├── submission.go │ │ ├── submission_test.go │ │ ├── user.go │ │ └── user_test.go │ ├── script.go │ ├── submission.go │ └── user.go └── routes.go ├── base ├── event │ ├── event.go │ └── event_test.go ├── exit │ ├── exit.go │ └── test.go ├── log │ ├── echo_logger.go │ ├── echo_logger_test.go │ ├── gorm_logger.go │ ├── log.go │ ├── log_test.go │ ├── logger.go │ ├── logger_test.go │ ├── logging.go │ ├── logging_test.go │ ├── writer.go │ └── writer_test.go ├── main.go ├── procedure │ ├── procedure.go │ └── procedure_test.go ├── utils │ ├── auth.go │ ├── class.go │ ├── class_test.go │ ├── email.go │ ├── email_test.go │ ├── helpers.go │ ├── http_error.go │ ├── origins.go │ ├── password.go │ ├── problem_set.go │ ├── problem_set_test.go │ ├── query_helper.go │ ├── query_helper_test.go │ ├── storage.go │ ├── storage_test.go │ ├── utils.go │ └── utils_test.go └── validator │ ├── translations │ └── zh │ │ └── zh.go │ ├── validator.go │ └── validator_test.go ├── clean.go ├── codecov.yml ├── config.yml.example ├── database ├── iterator.go ├── iterator_test.go ├── migrate.go ├── migrate_test.go ├── models │ ├── class.go │ ├── class_test.go │ ├── config.go │ ├── email.go │ ├── image.go │ ├── language.go │ ├── log │ │ └── log.go │ ├── models_test.go │ ├── permission.go │ ├── permission_test.go │ ├── problem.go │ ├── problem_set.go │ ├── problem_set_test.go │ ├── script.go │ ├── submission.go │ ├── token.go │ ├── user.go │ ├── user_test.go │ └── webauthn.go ├── string_array.go └── test.go ├── docs ├── docs.go ├── swagger.json └── swagger.yaml ├── event ├── log │ ├── log.go │ └── log_test.go ├── register │ └── listener.go ├── run │ ├── listener.go │ └── run.go └── submission │ ├── listener.go │ └── submission.go ├── flag.go ├── go.mod ├── go.sum ├── init.go ├── license.md ├── main.go ├── migrate.go ├── permission.go ├── readme.md ├── serve.go ├── template └── email_verification.html └── test_config.go /.gitattributes: -------------------------------------------------------------------------------- 1 | *.js linguist-language=GO 2 | *.css linguist-language=GO 3 | *.html linguist-language=GO 4 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [leoleoasd]# Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/pr-title-check.json: -------------------------------------------------------------------------------- 1 | { 2 | "LABEL": { 3 | "name": "title needs formatting", 4 | "color": "EEEEEE" 5 | }, 6 | "CHECKS": { 7 | "prefixes": ["fix: ", "feat: "] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.github/workflows/cla.yml: -------------------------------------------------------------------------------- 1 | name: "CLA Assistant" 2 | on: 3 | issue_comment: 4 | types: [created] 5 | pull_request_target: 6 | types: [opened,closed,synchronize] 7 | 8 | jobs: 9 | CLAssistant: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: "CLA Assistant" 13 | if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target' 14 | # Beta Release 15 | uses: cla-assistant/github-action@v2.3.0 16 | env: 17 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 18 | # the below token should have repo scope and must be manually added by you in the repository's secret 19 | PERSONAL_ACCESS_TOKEN : ${{ secrets.PERSONAL_ACCESS_TOKEN }} 20 | with: 21 | path-to-signatures: 'signatures/version1/cla.json' 22 | path-to-document: 'https://github.com/EduOJ/cla/blob/main/cla.txt' # e.g. a CLA or a DCO document 23 | # branch should not be protected 24 | branch: main 25 | remote-organization-name: eduoj 26 | remote-repository-name: cla 27 | signed-commit-message: '$contributorName has signed the CLA in eduoj/backend#$pullRequestNo' 28 | lock-pullrequest-aftermerge: false 29 | allowlist: bot* 30 | 31 | #below are the optional inputs - If the optional inputs are not given, then default values will be taken 32 | #remote-organization-name: enter the remote organization name where the signatures should be stored (Default is storing the signatures in the same repository) 33 | #remote-repository-name: enter the remote repository name where the signatures should be stored (Default is storing the signatures in the same repository) 34 | #create-file-commit-message: 'For example: Creating file for storing CLA Signatures' 35 | #signed-commit-message: 'For example: $contributorName has signed the CLA in #$pullRequestNo' 36 | #custom-notsigned-prcomment: 'pull request comment with Introductory message to ask new contributors to sign' 37 | #custom-pr-sign-comment: 'The signature to be committed in order to sign the CLA' 38 | #custom-allsigned-prcomment: 'pull request comment when all contributors has signed, defaults to **CLA Assistant Lite bot** All Contributors have signed the CLA.' 39 | #lock-pullrequest-aftermerge: false - if you don't want this bot to automatically lock the pull request after merging (default - true) 40 | #use-dco-flag: true - If you are using DCO instead of CLA 41 | -------------------------------------------------------------------------------- /.github/workflows/coverage.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | name: Coverage 3 | jobs: 4 | coverage: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - name: Install Go 8 | uses: actions/setup-go@v2 9 | with: 10 | go-version: 1.19.x 11 | - name: Checkout code 12 | uses: actions/checkout@v2 13 | - name: Go Test 14 | run: go test ./... -race -coverprofile=coverage.txt -covermode=atomic 15 | continue-on-error: true 16 | - name: Upload coverage to Codecov 17 | uses: codecov/codecov-action@v2 18 | with: 19 | file: ./coverage.txt 20 | fail_ci_if_error: true 21 | -------------------------------------------------------------------------------- /.github/workflows/pr-title-checker.yml: -------------------------------------------------------------------------------- 1 | name: "PR Title Checker" 2 | on: 3 | pull_request: 4 | types: 5 | - opened 6 | - edited 7 | - synchronize 8 | - labeled 9 | - unlabeled 10 | 11 | jobs: 12 | check: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: thehanimo/pr-title-checker@v1.3.1 16 | with: 17 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 18 | pass_on_octokit_error: false 19 | configuration_path: ".github/pr-title-check.json" 20 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | on: [pull_request, push] 2 | name: Test 3 | jobs: 4 | test: 5 | strategy: 6 | matrix: 7 | go-version: [1.17.x, 1.18.x, 1.19.x] 8 | platform: [ubuntu-latest, macos-latest, windows-latest] 9 | runs-on: ${{ matrix.platform }} 10 | steps: 11 | - name: Install Go 12 | uses: actions/setup-go@v2 13 | with: 14 | go-version: ${{ matrix.go-version }} 15 | - name: Checkout code 16 | uses: actions/checkout@v2 17 | - name: Install Dependences 18 | run: go mod download 19 | - name: Go Test 20 | run: go test ./... 21 | - name: golangci-lint 22 | uses: golangci/golangci-lint-action@v3 23 | test-race: 24 | strategy: 25 | matrix: 26 | go-version: [1.17.x, 1.18.x, 1.19.x] 27 | platform: [ubuntu-latest, macos-latest] 28 | runs-on: ${{ matrix.platform }} 29 | steps: 30 | - name: Install Go 31 | uses: actions/setup-go@v2 32 | with: 33 | go-version: ${{ matrix.go-version }} 34 | - name: Checkout code 35 | uses: actions/checkout@v2 36 | - name: Install Dependences 37 | run: go mod download 38 | - name: Go Test Race 39 | run: go test ./... -race 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | main 8 | 9 | # Test binary, built with `go test -c` 10 | *.test 11 | 12 | # Output of the go coverage tool, specifically when used with LiteIDE 13 | *.out 14 | 15 | # Dependency directories (remove the comment below to include it) 16 | # vendor/ 17 | 18 | /.idea 19 | 20 | /coverage* 21 | /test.* 22 | /config.yml 23 | 24 | /testing_requests 25 | /EduOJBackend 26 | /backend 27 | -------------------------------------------------------------------------------- /.golangci.yaml: -------------------------------------------------------------------------------- 1 | issues: 2 | exclude-rules: 3 | - path: base/exit/test.go 4 | text: "copylocks: assignment copies lock value to" 5 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | repos: 4 | - repo: https://github.com/pre-commit/pre-commit-hooks 5 | rev: v4.4.0 6 | hooks: 7 | - id: trailing-whitespace 8 | - id: end-of-file-fixer 9 | - id: check-yaml 10 | - id: check-added-large-files 11 | - repo: https://github.com/golangci/golangci-lint 12 | rev: v1.51.2 13 | hooks: 14 | - id: golangci-lint 15 | - repo: https://github.com/Bahjat/pre-commit-golang 16 | rev: v1.0.2 17 | hooks: 18 | - id: go-fmt-import 19 | - id: go-unit-tests 20 | ci: 21 | skip: 22 | - golangci-lint 23 | - go-unit-tests 24 | -------------------------------------------------------------------------------- /app/controller/admin_logs.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "net/http" 5 | "strconv" 6 | "strings" 7 | 8 | "github.com/EduOJ/backend/app/request" 9 | "github.com/EduOJ/backend/app/response" 10 | "github.com/EduOJ/backend/base" 11 | "github.com/EduOJ/backend/base/utils" 12 | "github.com/EduOJ/backend/database/models/log" 13 | "github.com/labstack/echo/v4" 14 | ) 15 | 16 | func AdminGetLogs(c echo.Context) error { 17 | req := request.AdminGetLogsRequest{} 18 | if err, ok := utils.BindAndValidate(&req, c); !ok { 19 | return err 20 | } 21 | 22 | query := base.DB.Model(&log.Log{}).Order("id desc") 23 | 24 | if len(req.Levels) != 0 { 25 | levelsS := strings.Split(req.Levels, ",") 26 | levels := make([]int, 0, len(levelsS)) 27 | for _, ll := range levelsS { 28 | l, err := strconv.ParseInt(ll, 10, 32) 29 | if err != nil { 30 | return c.JSON(http.StatusBadRequest, response.ErrorResp("INVALID_LEVEL", nil)) 31 | } 32 | if l < 0 || l > 4 { // TODO: replace hard-coded level 33 | return c.JSON(http.StatusBadRequest, response.ErrorResp("INVALID_LEVEL", nil)) 34 | } 35 | levels = append(levels, int(l)) 36 | } 37 | query = query.Where("level in (?)", levels) 38 | } 39 | 40 | var logs []log.Log 41 | total, prevUrl, nextUrl, err := utils.Paginator(query, req.Limit, req.Offset, c.Request().URL, &logs) 42 | if err != nil { 43 | if herr, ok := err.(utils.HttpError); ok { 44 | return herr.Response(c) 45 | } 46 | panic(err) 47 | } 48 | return c.JSON(http.StatusOK, response.AdminGetLogsResponse{ 49 | Message: "SUCCESS", 50 | Error: nil, 51 | Data: struct { 52 | Logs []log.Log `json:"logs"` 53 | Total int `json:"total"` 54 | Count int `json:"count"` 55 | Offset int `json:"offset"` 56 | Prev *string `json:"prev"` 57 | Next *string `json:"next"` 58 | }{ 59 | logs, 60 | total, 61 | len(logs), 62 | req.Offset, 63 | prevUrl, 64 | nextUrl, 65 | }, 66 | }) 67 | } 68 | -------------------------------------------------------------------------------- /app/controller/admin_logs_test.go: -------------------------------------------------------------------------------- 1 | package controller_test 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "testing" 7 | 8 | "github.com/EduOJ/backend/app/request" 9 | "github.com/EduOJ/backend/app/response" 10 | "github.com/EduOJ/backend/base" 11 | "github.com/EduOJ/backend/base/log" 12 | log2 "github.com/EduOJ/backend/database/models/log" 13 | "github.com/stretchr/testify/assert" 14 | ) 15 | 16 | func TestAdminGetLogs(t *testing.T) { 17 | 18 | failTests := []failTest{ 19 | { 20 | name: "LevelIsNotANumber", 21 | method: "GET", 22 | path: base.Echo.Reverse("admin.getLogs"), 23 | req: request.AdminGetLogsRequest{ 24 | Levels: "1,2,NotANumber", 25 | Limit: 0, 26 | Offset: 0, 27 | }, 28 | reqOptions: []reqOption{ 29 | applyAdminUser, 30 | }, 31 | statusCode: http.StatusBadRequest, 32 | resp: response.ErrorResp("INVALID_LEVEL", nil), 33 | }, 34 | { 35 | name: "LevelExceeded", 36 | method: "GET", 37 | path: base.Echo.Reverse("admin.getLogs"), 38 | req: request.AdminGetLogsRequest{ 39 | Levels: "1,2,5", 40 | Limit: 0, 41 | Offset: 0, 42 | }, 43 | reqOptions: []reqOption{ 44 | applyAdminUser, 45 | }, 46 | statusCode: http.StatusBadRequest, 47 | resp: response.ErrorResp("INVALID_LEVEL", nil), 48 | }, 49 | { 50 | name: "PermissionDenied", 51 | method: "GET", 52 | path: base.Echo.Reverse("admin.getLogs"), 53 | req: request.AdminGetLogsRequest{ 54 | Levels: "1,2,3", 55 | Limit: 0, 56 | Offset: 0, 57 | }, 58 | reqOptions: []reqOption{ 59 | applyNormalUser, 60 | }, 61 | statusCode: http.StatusForbidden, 62 | resp: response.ErrorResp("PERMISSION_DENIED", nil), 63 | }, 64 | } 65 | 66 | runFailTests(t, failTests, "AdminGetLogs") 67 | 68 | logs := make([]log2.Log, 5) 69 | var ll log.Level 70 | for ll = 0; ll < 5; ll++ { 71 | llInt := int(ll) 72 | l := log2.Log{ 73 | Level: &llInt, 74 | Message: fmt.Sprintf("test_admin_get_logs_%s_message", ll.String()), 75 | Caller: fmt.Sprintf("test_admin_get_logs_%s_caller", ll.String()), 76 | } 77 | assert.NoError(t, base.DB.Create(&l).Error) 78 | logs[ll] = l 79 | } 80 | 81 | successTests := []struct { 82 | name string 83 | req request.AdminGetLogsRequest 84 | resp response.AdminGetLogsResponse 85 | }{ 86 | { 87 | name: "SingleLevel", 88 | req: request.AdminGetLogsRequest{ 89 | Levels: "2", 90 | Limit: 0, 91 | Offset: 0, 92 | }, 93 | resp: response.AdminGetLogsResponse{ 94 | Message: "SUCCESS", 95 | Error: nil, 96 | Data: struct { 97 | Logs []log2.Log `json:"logs"` 98 | Total int `json:"total"` 99 | Count int `json:"count"` 100 | Offset int `json:"offset"` 101 | Prev *string `json:"prev"` 102 | Next *string `json:"next"` 103 | }{ 104 | Logs: []log2.Log{ 105 | logs[2], 106 | }, 107 | Total: 1, 108 | Count: 1, 109 | Offset: 0, 110 | Prev: nil, 111 | Next: nil, 112 | }, 113 | }, 114 | }, 115 | { 116 | name: "MultipleLevel", 117 | req: request.AdminGetLogsRequest{ 118 | Levels: "0,1,2,4", 119 | Limit: 0, 120 | Offset: 0, 121 | }, 122 | resp: response.AdminGetLogsResponse{ 123 | Message: "SUCCESS", 124 | Error: nil, 125 | Data: struct { 126 | Logs []log2.Log `json:"logs"` 127 | Total int `json:"total"` 128 | Count int `json:"count"` 129 | Offset int `json:"offset"` 130 | Prev *string `json:"prev"` 131 | Next *string `json:"next"` 132 | }{ 133 | Logs: []log2.Log{ 134 | logs[4], logs[2], logs[1], logs[0], 135 | }, 136 | Total: 4, 137 | Count: 4, 138 | Offset: 0, 139 | Prev: nil, 140 | Next: nil, 141 | }, 142 | }, 143 | }, 144 | { 145 | name: "LimitAndOffset", 146 | req: request.AdminGetLogsRequest{ 147 | Levels: "0,1,2,4", 148 | Limit: 2, 149 | Offset: 1, 150 | }, 151 | resp: response.AdminGetLogsResponse{ 152 | Message: "SUCCESS", 153 | Error: nil, 154 | Data: struct { 155 | Logs []log2.Log `json:"logs"` 156 | Total int `json:"total"` 157 | Count int `json:"count"` 158 | Offset int `json:"offset"` 159 | Prev *string `json:"prev"` 160 | Next *string `json:"next"` 161 | }{ 162 | Logs: []log2.Log{ 163 | logs[2], logs[1], 164 | }, 165 | Total: 4, 166 | Count: 2, 167 | Offset: 1, 168 | Prev: nil, 169 | Next: getUrlStringPointer("admin.getLogs", map[string]string{ 170 | "limit": "2", 171 | "offset": "3", 172 | }), 173 | }, 174 | }, 175 | }, 176 | } 177 | 178 | t.Run("testAdminGetLogsSuccess", func(t *testing.T) { 179 | t.Parallel() 180 | for _, test := range successTests { 181 | test := test 182 | t.Run("testAdminGetLogs"+test.name, func(t *testing.T) { 183 | t.Parallel() 184 | httpResp := makeResp(makeReq(t, "GET", base.Echo.Reverse("admin.getLogs"), test.req, applyAdminUser)) 185 | assert.Equal(t, http.StatusOK, httpResp.StatusCode) 186 | resp := response.Response{} 187 | mustJsonDecode(httpResp, &resp) 188 | jsonEQ(t, test.resp, resp) 189 | }) 190 | } 191 | }) 192 | 193 | } 194 | -------------------------------------------------------------------------------- /app/controller/images.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | path2 "path" 9 | "strings" 10 | 11 | "github.com/EduOJ/backend/app/response" 12 | "github.com/EduOJ/backend/base" 13 | "github.com/EduOJ/backend/base/log" 14 | "github.com/EduOJ/backend/base/utils" 15 | "github.com/EduOJ/backend/database/models" 16 | "github.com/gabriel-vasile/mimetype" 17 | "github.com/labstack/echo/v4" 18 | "github.com/minio/minio-go/v7" 19 | "github.com/pkg/errors" 20 | "gorm.io/gorm" 21 | ) 22 | 23 | func GetImage(c echo.Context) error { 24 | // TODO: check referrer 25 | id := c.Param("id") 26 | imageModel := models.Image{} 27 | err := base.DB.Model(&models.Image{}).Where("file_path = ?", id).First(&imageModel).Error 28 | if errors.Is(err, gorm.ErrRecordNotFound) { 29 | return c.JSON(http.StatusNotFound, response.ErrorResp("IMAGE_NOT_FOUND", nil)) 30 | } else if err != nil { 31 | panic(err) 32 | } 33 | object, err := base.Storage.GetObject(context.Background(), "images", imageModel.FilePath, minio.GetObjectOptions{}) 34 | if err != nil { 35 | panic(err) 36 | } 37 | mime, err := mimetype.DetectReader(object) 38 | if err != nil { 39 | if merr, ok := err.(minio.ErrorResponse); ok { 40 | if merr.StatusCode == 404 { 41 | return c.JSON(http.StatusNotFound, response.ErrorResp("IMAGE_NOT_FOUND", nil)) 42 | } else { 43 | panic(merr) 44 | } 45 | } 46 | log.Error("could not detect MIME of image!") 47 | log.Error(err) 48 | return c.JSON(http.StatusForbidden, response.ErrorResp("ILLEGAL_TYPE", nil)) 49 | } 50 | _, err = object.Seek(0, io.SeekStart) 51 | if err != nil { 52 | panic(err) 53 | } 54 | 55 | c.Response().Header().Set("Access-Control-Allow-Origin", strings.Join(utils.Origins, ", ")) 56 | c.Response().Header().Set("Cache-Control", "public; max-age=31536000") 57 | c.Response().Header().Set("Content-Disposition", fmt.Sprintf(`inline; filename="%s"`, imageModel.Filename)) 58 | return c.Stream(http.StatusOK, mime.String(), object) 59 | } 60 | 61 | func CreateImage(c echo.Context) error { 62 | user, ok := c.Get("user").(models.User) 63 | if !ok { 64 | panic("could not convert my user into type models.User") 65 | } 66 | file, err := c.FormFile("file") 67 | if err != nil { 68 | panic(err) 69 | } 70 | count := int64(1) 71 | fileIndex := "" 72 | for count != 0 { 73 | fileIndex = utils.RandStr(32) 74 | utils.PanicIfDBError(base.DB.Model(&models.Image{}).Where("file_path = ?", fileIndex).Count(&count), "could not save image") 75 | } 76 | fileModel := models.Image{ 77 | Filename: file.Filename, 78 | FilePath: fileIndex, 79 | User: user, 80 | } 81 | utils.PanicIfDBError(base.DB.Save(&fileModel), "could not save image") 82 | src, err := file.Open() 83 | if err != nil { 84 | panic(err) 85 | } 86 | defer src.Close() 87 | mime, err := mimetype.DetectReader(src) 88 | if err != nil { 89 | log.Error("could not detect MIME of image!") 90 | log.Error(err) 91 | return c.JSON(http.StatusForbidden, response.ErrorResp("ILLEGAL_TYPE", nil)) 92 | } 93 | if mime.String()[:5] != "image" || mime.Extension() != path2.Ext(file.Filename) { 94 | return c.JSON(http.StatusForbidden, response.ErrorResp("ILLEGAL_TYPE", nil)) 95 | } 96 | _, err = src.Seek(0, io.SeekStart) 97 | if err != nil { 98 | panic(errors.Wrap(err, "could not seek to file start")) 99 | } 100 | 101 | _, err = base.Storage.PutObject(c.Request().Context(), "images", fileIndex, src, file.Size, minio.PutObjectOptions{ 102 | ContentType: mime.String(), 103 | }) 104 | if err != nil { 105 | panic(errors.Wrap(err, "could write image to s3 storage.")) 106 | } 107 | return c.JSON(http.StatusCreated, response.CreateImageResponse{ 108 | Message: "SUCCESS", 109 | Error: nil, 110 | Data: struct { 111 | FilePath string `json:"filename"` 112 | }{ 113 | base.Echo.Reverse("image.getImage", fileIndex), 114 | }, 115 | }) 116 | } 117 | -------------------------------------------------------------------------------- /app/controller/script.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/EduOJ/backend/app/response" 7 | "github.com/EduOJ/backend/base" 8 | "github.com/EduOJ/backend/base/utils" 9 | "github.com/EduOJ/backend/database/models" 10 | "github.com/labstack/echo/v4" 11 | "github.com/pkg/errors" 12 | "gorm.io/gorm" 13 | ) 14 | 15 | func GetScript(c echo.Context) error { 16 | script := models.Script{} 17 | name := c.Param("name") 18 | err := base.DB.First(&script, "name = ?", name).Error 19 | if err != nil { 20 | if errors.Is(err, gorm.ErrRecordNotFound) { 21 | return c.JSON(http.StatusNotFound, response.ErrorResp("NOT_FOUND", nil)) 22 | } else { 23 | panic(errors.Wrap(err, "could not query script")) 24 | } 25 | } 26 | url, err := utils.GetPresignedURL("scripts", script.Name, script.Filename) 27 | if err != nil { 28 | panic(errors.Wrap(err, "could not get presigned url of script")) 29 | } 30 | return c.Redirect(http.StatusFound, url) 31 | } 32 | -------------------------------------------------------------------------------- /app/controller/script_test.go: -------------------------------------------------------------------------------- 1 | package controller_test 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/xml" 7 | "io" 8 | "net/http" 9 | "testing" 10 | 11 | "github.com/EduOJ/backend/app/response" 12 | "github.com/EduOJ/backend/base" 13 | "github.com/EduOJ/backend/database/models" 14 | "github.com/minio/minio-go/v7" 15 | "github.com/stretchr/testify/assert" 16 | ) 17 | 18 | func TestGetScript(t *testing.T) { 19 | script := models.Script{ 20 | Name: "test_get_script", 21 | Filename: "test_get_script.zip", 22 | } 23 | assert.NoError(t, base.DB.Create(&script).Error) 24 | file := newFileContent("test_get_script", "test_get_script.zip", b64Encode("test_get_script_zip_content")) 25 | _, err := base.Storage.PutObject(context.Background(), "scripts", script.Name, file.reader, file.size, minio.PutObjectOptions{}) 26 | assert.NoError(t, err) 27 | _, err = file.reader.Seek(0, io.SeekStart) 28 | assert.NoError(t, err) 29 | 30 | noFileScript := models.Script{ 31 | Name: "test_no_file_script", 32 | Filename: "test_no_file_script", 33 | } 34 | assert.NoError(t, base.DB.Create(&noFileScript).Error) 35 | 36 | t.Run("Success", func(t *testing.T) { 37 | req := makeReq(t, "GET", base.Echo.Reverse("judger.getScript", script.Name), "", judgerAuthorize) 38 | resp := makeResp(req) 39 | assert.Equal(t, http.StatusFound, resp.StatusCode) 40 | content := getPresignedURLContent(t, resp.Header.Get("Location")) 41 | assert.Equal(t, "test_get_script_zip_content", content) 42 | }) 43 | 44 | t.Run("MissingName", func(t *testing.T) { 45 | req := makeReq(t, "GET", base.Echo.Reverse("judger.getScript"), "", judgerAuthorize) 46 | resp := makeResp(req) 47 | assert.Equal(t, http.StatusNotFound, resp.StatusCode) 48 | jsonEQ(t, response.ErrorResp("NOT_FOUND", nil), resp) 49 | }) 50 | 51 | t.Run("MissingFile", func(t *testing.T) { 52 | req := makeReq(t, "GET", base.Echo.Reverse("judger.getScript", noFileScript.Name), "", judgerAuthorize) 53 | resp := makeResp(req) 54 | assert.Equal(t, http.StatusFound, resp.StatusCode) 55 | assert.NotEmpty(t, resp.Header.Get("Location")) 56 | resp, err := http.Get(resp.Header.Get("Location")) 57 | assert.NoError(t, err) 58 | assert.Equal(t, http.StatusNotFound, resp.StatusCode) 59 | bodyBuf := bytes.Buffer{} 60 | _, err = bodyBuf.ReadFrom(resp.Body) 61 | assert.NoError(t, err) 62 | var xmlresp struct { 63 | Name xml.Name `xml:"Error"` 64 | Code string `xml:"Code"` 65 | Resource string `xml:"Resource"` 66 | } 67 | assert.NoError(t, xml.Unmarshal(bodyBuf.Bytes(), &xmlresp)) 68 | assert.Equal(t, struct { 69 | Name xml.Name `xml:"Error"` 70 | Code string `xml:"Code"` 71 | Resource string `xml:"Resource"` 72 | }{ 73 | Code: "NoSuchKey", 74 | Resource: "test_no_file_script", 75 | }, xmlresp) 76 | }) 77 | } 78 | -------------------------------------------------------------------------------- /app/controller/test_test.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | func init() { 4 | inTest = true 5 | } 6 | -------------------------------------------------------------------------------- /app/controller/todo.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import "github.com/labstack/echo/v4" 4 | 5 | func Todo(c echo.Context) error { 6 | return nil 7 | } 8 | -------------------------------------------------------------------------------- /app/controller/webauthn.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "io/ioutil" 7 | "net/http" 8 | "strings" 9 | "time" 10 | 11 | "github.com/EduOJ/backend/app/response" 12 | "github.com/EduOJ/backend/app/response/resource" 13 | "github.com/EduOJ/backend/base" 14 | "github.com/EduOJ/backend/base/utils" 15 | "github.com/EduOJ/backend/database/models" 16 | "github.com/go-webauthn/webauthn/protocol" 17 | "github.com/go-webauthn/webauthn/webauthn" 18 | "github.com/labstack/echo/v4" 19 | "github.com/patrickmn/go-cache" 20 | "github.com/pkg/errors" 21 | "gorm.io/gorm" 22 | ) 23 | 24 | var cac = cache.New(5*time.Minute, 10*time.Minute) 25 | 26 | func BeginRegistration(c echo.Context) error { 27 | user := c.Get("user").(models.User) 28 | options, sessionData, err := base.WebAuthn.BeginRegistration(&user) 29 | if err != nil { 30 | panic(errors.Wrap(err, "could not begin register webauthn")) 31 | } 32 | cac.Set("register"+user.Username, sessionData, cache.DefaultExpiration) 33 | return c.JSON(http.StatusOK, response.Response{ 34 | Message: "SUCCESS", 35 | Error: nil, 36 | Data: options, 37 | }) 38 | } 39 | 40 | func FinishRegistration(c echo.Context) error { 41 | user := c.Get("user").(models.User) 42 | sessionData, found := cac.Get("register" + user.Username) 43 | if !found { 44 | panic(errors.New("not registered")) 45 | } 46 | b, err := ioutil.ReadAll(c.Request().Body) 47 | if err != nil { 48 | panic(errors.Wrap(err, "could not read body")) 49 | } 50 | parsedResponse, err := protocol.ParseCredentialCreationResponseBody(bytes.NewReader(b)) 51 | if err != nil { 52 | panic(errors.Wrap(err, "could not parse credential from body")) 53 | } 54 | credential, err := base.WebAuthn.CreateCredential(&user, *sessionData.(*webauthn.SessionData), parsedResponse) 55 | if err != nil { 56 | panic(errors.Wrap(err, "could not create credential")) 57 | } 58 | webauthnCredential := models.WebauthnCredential{} 59 | m, err := json.Marshal(credential) 60 | if err != nil { 61 | panic(errors.Wrap(err, "could not marshal credential to json")) 62 | } 63 | webauthnCredential.Content = string(m) 64 | if err := base.DB.Model(&user).Association("Credentials").Append(&webauthnCredential); err != nil { 65 | panic(errors.Wrap(err, "could not save credentials")) 66 | } 67 | return c.JSON(http.StatusOK, response.Response{ 68 | Message: "SUCCESS", 69 | Error: nil, 70 | Data: nil, 71 | }) 72 | } 73 | 74 | func BeginLogin(c echo.Context) error { 75 | var id string 76 | err := echo.QueryParamsBinder(c).String("username", &id).BindError() 77 | if err != nil { 78 | return c.JSON(http.StatusBadRequest, response.ErrorResp("WRONG_USERNAME", nil)) 79 | } 80 | user, err := utils.FindUser(id) 81 | if err != nil { 82 | if errors.Is(err, gorm.ErrRecordNotFound) { 83 | return c.JSON(http.StatusNotFound, response.ErrorResp("NOT_FOUND", nil)) 84 | } 85 | panic(errors.Wrap(err, "could not find user")) 86 | } 87 | options, sessionData, err := base.WebAuthn.BeginLogin(user) 88 | if err != nil { 89 | var perr *protocol.Error 90 | if errors.As(err, &perr) { 91 | return c.JSON(http.StatusBadRequest, response.ErrorResp(strings.ToUpper(perr.Type), perr)) 92 | } 93 | panic(errors.Wrap(err, "could not begin login webauthn")) 94 | } 95 | cac.Set("login"+user.Username, sessionData, cache.DefaultExpiration) 96 | return c.JSON(http.StatusOK, response.Response{ 97 | Message: "SUCCESS", 98 | Error: nil, 99 | Data: options, 100 | }) 101 | } 102 | 103 | func FinishLogin(c echo.Context) error { 104 | var id string 105 | err := echo.QueryParamsBinder(c).String("username", &id).BindError() 106 | if err != nil { 107 | return c.JSON(http.StatusBadRequest, response.ErrorResp("WRONG_USERNAME", nil)) 108 | } 109 | var user models.User 110 | err = base.DB.Where("email = ? or username = ?", id, id).First(&user).Error 111 | if err != nil { 112 | if errors.Is(err, gorm.ErrRecordNotFound) { 113 | return c.JSON(http.StatusNotFound, response.ErrorResp("NOT_FOUND", nil)) 114 | } 115 | panic(errors.Wrap(err, "could not find user")) 116 | } 117 | sessionData, found := cac.Get("login" + user.Username) 118 | if !found { 119 | panic(errors.New("not registered")) 120 | } 121 | b, err := ioutil.ReadAll(c.Request().Body) 122 | if err != nil { 123 | return c.JSON(http.StatusInternalServerError, response.ErrorResp("INTERNAL_ERROR", nil)) 124 | } 125 | parsedResponse, err := protocol.ParseCredentialRequestResponseBody(bytes.NewReader(b)) 126 | if err != nil { 127 | panic(errors.Wrap(err, "could not parse response")) 128 | } 129 | _, err = base.WebAuthn.ValidateLogin(&user, *sessionData.(*webauthn.SessionData), parsedResponse) 130 | if err != nil { 131 | panic(errors.Wrap(err, "could not validate login webauthn")) 132 | } 133 | token := models.Token{ 134 | Token: utils.RandStr(32), 135 | User: user, 136 | RememberMe: false, 137 | } 138 | utils.PanicIfDBError(base.DB.Create(&token), "could not create token for users") 139 | if !user.RoleLoaded { 140 | user.LoadRoles() 141 | } 142 | return c.JSON(http.StatusOK, response.LoginResponse{ 143 | Message: "SUCCESS", 144 | Error: nil, 145 | Data: struct { 146 | User resource.UserForAdmin `json:"user"` 147 | Token string `json:"token"` 148 | }{ 149 | User: *resource.GetUserForAdmin(&user), 150 | Token: token.Token, 151 | }, 152 | }) 153 | } 154 | -------------------------------------------------------------------------------- /app/middleware/auth.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import "github.com/labstack/echo/v4" 4 | 5 | func Auth(next echo.HandlerFunc) echo.HandlerFunc { 6 | return func(c echo.Context) error { 7 | // do nothing. 8 | // we allow register/login when already logged in from api side. 9 | return next(c) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /app/middleware/authentication.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | 7 | "github.com/EduOJ/backend/app/response" 8 | "github.com/EduOJ/backend/base" 9 | "github.com/EduOJ/backend/base/log" 10 | "github.com/EduOJ/backend/base/utils" 11 | "github.com/EduOJ/backend/database/models" 12 | "github.com/labstack/echo/v4" 13 | "github.com/pkg/errors" 14 | "github.com/spf13/viper" 15 | "gorm.io/gorm" 16 | "gorm.io/gorm/clause" 17 | ) 18 | 19 | func Authentication(next echo.HandlerFunc) echo.HandlerFunc { 20 | return func(c echo.Context) error { 21 | tokenString := c.Request().Header.Get("Authorization") 22 | if tokenString == "" { 23 | return next(c) 24 | } 25 | token, err := utils.GetToken(tokenString) 26 | if errors.Is(err, gorm.ErrRecordNotFound) { 27 | return next(c) 28 | } 29 | if err != nil { 30 | log.Error(errors.Wrap(err, "fail to get user from token"), c) 31 | return response.InternalErrorResp(c) 32 | } 33 | if utils.IsTokenExpired(token) { 34 | base.DB.Delete(&token) 35 | return c.JSON(http.StatusRequestTimeout, response.ErrorResp("AUTH_SESSION_EXPIRED", nil)) 36 | } 37 | token.UpdatedAt = time.Now() 38 | utils.PanicIfDBError(base.DB.Omit(clause.Associations).Save(&token), "could not update token") 39 | c.Set("user", token.User) 40 | return next(c) 41 | } 42 | } 43 | 44 | func Logged(next echo.HandlerFunc) echo.HandlerFunc { 45 | return func(c echo.Context) error { 46 | _, ok := c.Get("user").(models.User) 47 | if !ok { 48 | return c.JSON(http.StatusUnauthorized, response.ErrorResp("AUTH_NEED_TOKEN", nil)) 49 | } 50 | return next(c) 51 | } 52 | } 53 | 54 | func EmailVerified(next echo.HandlerFunc) echo.HandlerFunc { 55 | return func(c echo.Context) error { 56 | user, ok := c.Get("user").(models.User) 57 | // for some APIs, we allow guest access, but for logged user, we require email verification. 58 | if ok && viper.GetBool("email.need_verification") && !user.EmailVerified { 59 | return c.JSON(http.StatusUnauthorized, response.ErrorResp("AUTH_NEED_EMAIL_VERIFICATION", nil)) 60 | } 61 | return next(c) 62 | } 63 | } 64 | 65 | // Using this middleware means the controller could accept access from guests. 66 | // The only exception is role information, guest users don't have any permissions or roles. 67 | // Any user information other than roles SHOULD NOT be read in controllers that use this middleware. 68 | func AllowGuest(next echo.HandlerFunc) echo.HandlerFunc { 69 | return func(c echo.Context) error { 70 | guestUser := models.User{ 71 | ID: 0, 72 | Username: "guest_user", 73 | Nickname: "guest_user_nick", 74 | Email: "guest_user@email.com", 75 | Password: "guest_user_pwd", 76 | CreatedAt: time.Time{}, 77 | UpdatedAt: time.Time{}, 78 | DeletedAt: gorm.DeletedAt{}, 79 | // The above content is to filled for debug, and should not be used in formal applications. 80 | 81 | Roles: []models.UserHasRole{}, 82 | RoleLoaded: true, 83 | } 84 | if c.Get("user") == nil { 85 | c.Set("user", guestUser) 86 | } 87 | return next(c) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /app/middleware/judger.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/EduOJ/backend/app/response" 7 | "github.com/labstack/echo/v4" 8 | "github.com/spf13/viper" 9 | ) 10 | 11 | func Judger(next echo.HandlerFunc) echo.HandlerFunc { 12 | return func(c echo.Context) error { 13 | if c.Request().Header.Get("Authorization") == viper.GetString("judger.token") { 14 | if c.Request().Header.Get("Judger-Name") == "" { 15 | return c.JSON(http.StatusBadRequest, response.ErrorResp("JUDGER_NAME_EXPECTED", nil)) 16 | } else { 17 | return next(c) 18 | } 19 | } 20 | return c.JSON(http.StatusForbidden, response.ErrorResp("PERMISSION_DENIED", nil)) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/middleware/judger_test.go: -------------------------------------------------------------------------------- 1 | package middleware_test 2 | 3 | import ( 4 | "io/ioutil" 5 | "net/http" 6 | "testing" 7 | 8 | "github.com/EduOJ/backend/app/middleware" 9 | "github.com/EduOJ/backend/app/response" 10 | "github.com/labstack/echo/v4" 11 | "github.com/spf13/viper" 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | func TestJudger(t *testing.T) { 16 | e := echo.New() 17 | e.GET("/", func(c echo.Context) error { 18 | return c.String(http.StatusOK, "OK") 19 | }, middleware.Judger) 20 | viper.Set("judger.token", "token_for_test_random_str_askudhoewiudhozSDjkfhqosuidfhasloihoase") 21 | 22 | t.Run("Success", func(t *testing.T) { 23 | resp := makeResp(makeReq(t, "GET", "/", "", headerOption{ 24 | "Authorization": []string{"token_for_test_random_str_askudhoewiudhozSDjkfhqosuidfhasloihoase"}, 25 | "Judger-Name": []string{"name"}, 26 | }), e) 27 | assert.Equal(t, http.StatusOK, resp.StatusCode) 28 | body, err := ioutil.ReadAll(resp.Body) 29 | assert.Equal(t, "OK", string(body)) 30 | assert.NoError(t, err) 31 | }) 32 | 33 | t.Run("NoName", func(t *testing.T) { 34 | resp := makeResp(makeReq(t, "GET", "/", "", headerOption{ 35 | "Authorization": []string{"token_for_test_random_str_askudhoewiudhozSDjkfhqosuidfhasloihoase"}, 36 | }), e) 37 | assert.Equal(t, http.StatusBadRequest, resp.StatusCode) 38 | jsonEQ(t, response.ErrorResp("JUDGER_NAME_EXPECTED", nil), resp) 39 | }) 40 | 41 | t.Run("WrongToken", func(t *testing.T) { 42 | resp := makeResp(makeReq(t, "GET", "/", "", headerOption{ 43 | "Authorization": []string{"wrong_token"}, 44 | }), e) 45 | assert.Equal(t, http.StatusForbidden, resp.StatusCode) 46 | jsonEQ(t, response.ErrorResp("PERMISSION_DENIED", nil), resp) 47 | }) 48 | 49 | t.Run("MissionToken", func(t *testing.T) { 50 | resp := makeResp(makeReq(t, "GET", "/", ""), e) 51 | assert.Equal(t, http.StatusForbidden, resp.StatusCode) 52 | jsonEQ(t, response.ErrorResp("PERMISSION_DENIED", nil), resp) 53 | }) 54 | } 55 | -------------------------------------------------------------------------------- /app/middleware/middleware_test.go: -------------------------------------------------------------------------------- 1 | package middleware_test 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "io/ioutil" 7 | "net/http" 8 | "net/http/httptest" 9 | "os" 10 | "testing" 11 | 12 | "github.com/EduOJ/backend/app" 13 | "github.com/EduOJ/backend/base" 14 | "github.com/EduOJ/backend/base/exit" 15 | "github.com/EduOJ/backend/base/log" 16 | "github.com/EduOJ/backend/base/validator" 17 | "github.com/EduOJ/backend/database" 18 | "github.com/labstack/echo/v4" 19 | "github.com/spf13/viper" 20 | "github.com/stretchr/testify/assert" 21 | ) 22 | 23 | func jsonEQ(t *testing.T, expected, actual interface{}) { 24 | assert.JSONEq(t, mustJsonEncode(t, expected), mustJsonEncode(t, actual)) 25 | } 26 | 27 | func mustJsonEncode(t *testing.T, data interface{}) string { 28 | var err error 29 | if dataResp, ok := data.(*http.Response); ok { 30 | data, err = ioutil.ReadAll(dataResp.Body) 31 | assert.NoError(t, err) 32 | } 33 | if dataString, ok := data.(string); ok { 34 | data = []byte(dataString) 35 | } 36 | if dataBytes, ok := data.([]byte); ok { 37 | err := json.Unmarshal(dataBytes, &data) 38 | assert.NoError(t, err) 39 | } 40 | j, err := json.Marshal(data) 41 | if err != nil { 42 | t.Fatal(data, err) 43 | } 44 | return string(j) 45 | } 46 | 47 | func mustJsonDecode(data interface{}, out interface{}) { 48 | var err error 49 | if dataResp, ok := data.(*http.Response); ok { 50 | data, err = ioutil.ReadAll(dataResp.Body) 51 | if err != nil { 52 | panic(err) 53 | } 54 | } 55 | if dataString, ok := data.(string); ok { 56 | data = []byte(dataString) 57 | } 58 | if dataBytes, ok := data.([]byte); ok { 59 | err = json.Unmarshal(dataBytes, out) 60 | if err != nil { 61 | panic(err) 62 | } 63 | } 64 | } 65 | 66 | type reqOption interface { 67 | make(r *http.Request) 68 | } 69 | 70 | type headerOption map[string][]string 71 | type queryOption map[string][]string 72 | 73 | func (h headerOption) make(r *http.Request) { 74 | for k, v := range h { 75 | for _, s := range v { 76 | r.Header.Add(k, s) 77 | } 78 | } 79 | } 80 | 81 | func (q queryOption) make(r *http.Request) { 82 | for k, v := range q { 83 | for _, s := range v { 84 | q := r.URL.Query() 85 | q.Add(k, s) 86 | r.URL.RawQuery = q.Encode() 87 | } 88 | } 89 | } 90 | 91 | var _ = queryOption{} // explictly mark as used 92 | 93 | func makeReq(t *testing.T, method string, path string, data interface{}, options ...reqOption) *http.Request { 94 | j, err := json.Marshal(data) 95 | assert.NoError(t, err) 96 | req := httptest.NewRequest(method, path, bytes.NewReader(j)) 97 | req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) 98 | for _, option := range options { 99 | option.make(req) 100 | } 101 | return req 102 | } 103 | 104 | func makeResp(req *http.Request, e *echo.Echo) *http.Response { 105 | rec := httptest.NewRecorder() 106 | e.ServeHTTP(rec, req) 107 | return rec.Result() 108 | } 109 | 110 | func TestMain(m *testing.M) { 111 | defer database.SetupDatabaseForTest()() 112 | defer exit.SetupExitForTest()() 113 | viper.SetConfigType("yaml") 114 | configFile := bytes.NewBufferString(`debug: false 115 | server: 116 | port: 8080 117 | origin: 118 | - http://127.0.0.1:8000 119 | email: 120 | need_verification: true 121 | auth: 122 | session_timeout: 1200 123 | remember_me_timeout: 604800 124 | session_count: 10`) 125 | err := viper.ReadConfig(configFile) 126 | if err != nil { 127 | panic(err) 128 | } 129 | log.Disable() 130 | 131 | base.Echo = echo.New() 132 | base.Echo.Validator = validator.NewEchoValidator() 133 | app.Register(base.Echo) 134 | os.Exit(m.Run()) 135 | } 136 | -------------------------------------------------------------------------------- /app/middleware/param.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "net/http" 5 | "strconv" 6 | 7 | "github.com/EduOJ/backend/app/response" 8 | "github.com/labstack/echo/v4" 9 | ) 10 | 11 | func ValidateParams(intParams map[string]string) func(next echo.HandlerFunc) echo.HandlerFunc { 12 | return func(next echo.HandlerFunc) echo.HandlerFunc { 13 | return func(c echo.Context) error { 14 | for p, v := range intParams { 15 | param := c.Param(p) 16 | if param != "" { 17 | if _, err := strconv.Atoi(param); err != nil { 18 | return c.JSON(http.StatusNotFound, response.ErrorResp(v, nil)) 19 | } 20 | } 21 | } 22 | return next(c) 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/middleware/param_test.go: -------------------------------------------------------------------------------- 1 | package middleware_test 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | 7 | "github.com/EduOJ/backend/app/middleware" 8 | "github.com/EduOJ/backend/app/response" 9 | "github.com/EduOJ/backend/database/models" 10 | "github.com/labstack/echo/v4" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func TestValidateParams(t *testing.T) { 15 | t.Parallel() 16 | e := echo.New() 17 | group1 := e.Group("/group1", middleware.ValidateParams(map[string]string{ 18 | "test_param1": "PARAM1_NOT_FOUND", 19 | })) 20 | group2 := e.Group("/group2", middleware.ValidateParams(map[string]string{ 21 | "test_param1": "PARAM1_NOT_FOUND", 22 | "test_param2": "PARAM2_NOT_FOUND", 23 | })) 24 | group3 := e.Group("/group3", middleware.ValidateParams(map[string]string{ 25 | "test_param1": "PARAM1_NOT_FOUND", 26 | "test_param2": "PARAM2_NOT_FOUND", 27 | })) 28 | group1.GET("/:test_param1/:test_param2/test", testController).Name = "group1.test" 29 | group2.GET("/:test_param1/:test_param2/test", testController).Name = "group2.test" 30 | group3.GET("/:test_param1/test", testController).Name = "group3.test" 31 | 32 | t.Run("Pass", func(t *testing.T) { 33 | t.Run("Group1", func(t *testing.T) { 34 | httpResp := makeResp(makeReq(t, "GET", e.Reverse("group1.test", 1, "non_int_string"), nil), e) 35 | assert.Equal(t, http.StatusOK, httpResp.StatusCode) 36 | resp := response.Response{} 37 | mustJsonDecode(httpResp, &resp) 38 | jsonEQ(t, response.Response{ 39 | Message: "SUCCESS", 40 | Error: nil, 41 | Data: models.User{}, 42 | }, resp) 43 | }) 44 | t.Run("Group2", func(t *testing.T) { 45 | httpResp := makeResp(makeReq(t, "GET", e.Reverse("group2.test", "", -1), nil), e) 46 | assert.Equal(t, http.StatusOK, httpResp.StatusCode) 47 | resp := response.Response{} 48 | mustJsonDecode(httpResp, &resp) 49 | jsonEQ(t, response.Response{ 50 | Message: "SUCCESS", 51 | Error: nil, 52 | Data: models.User{}, 53 | }, resp) 54 | }) 55 | t.Run("Group3", func(t *testing.T) { 56 | httpResp := makeResp(makeReq(t, "GET", e.Reverse("group3.test", -2), nil), e) 57 | assert.Equal(t, http.StatusOK, httpResp.StatusCode) 58 | resp := response.Response{} 59 | mustJsonDecode(httpResp, &resp) 60 | jsonEQ(t, response.Response{ 61 | Message: "SUCCESS", 62 | Error: nil, 63 | Data: models.User{}, 64 | }, resp) 65 | }) 66 | }) 67 | t.Run("Fail", func(t *testing.T) { 68 | t.Run("Group1", func(t *testing.T) { 69 | httpResp := makeResp(makeReq(t, "GET", e.Reverse("group1.test", "non_int_string", 2), nil), e) 70 | assert.Equal(t, http.StatusNotFound, httpResp.StatusCode) 71 | resp := response.Response{} 72 | mustJsonDecode(httpResp, &resp) 73 | jsonEQ(t, response.ErrorResp("PARAM1_NOT_FOUND", nil), resp) 74 | }) 75 | t.Run("Group2", func(t *testing.T) { 76 | httpResp := makeResp(makeReq(t, "GET", e.Reverse("group2.test", 0, "non_int_string"), nil), e) 77 | assert.Equal(t, http.StatusNotFound, httpResp.StatusCode) 78 | resp := response.Response{} 79 | mustJsonDecode(httpResp, &resp) 80 | jsonEQ(t, response.ErrorResp("PARAM2_NOT_FOUND", nil), resp) 81 | }) 82 | t.Run("Group3", func(t *testing.T) { 83 | httpResp := makeResp(makeReq(t, "GET", e.Reverse("group3.test", "non_int_string"), nil), e) 84 | assert.Equal(t, http.StatusNotFound, httpResp.StatusCode) 85 | resp := response.Response{} 86 | mustJsonDecode(httpResp, &resp) 87 | jsonEQ(t, response.ErrorResp("PARAM1_NOT_FOUND", nil), resp) 88 | }) 89 | }) 90 | } 91 | -------------------------------------------------------------------------------- /app/middleware/permission.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "net/http" 5 | "strconv" 6 | "time" 7 | 8 | "github.com/EduOJ/backend/app/response" 9 | "github.com/EduOJ/backend/base" 10 | "github.com/EduOJ/backend/base/log" 11 | "github.com/EduOJ/backend/base/utils" 12 | "github.com/EduOJ/backend/database/models" 13 | "github.com/labstack/echo/v4" 14 | ) 15 | 16 | type hasRole struct { 17 | ID uint 18 | Name string 19 | } 20 | 21 | func (h *hasRole) GetID() uint { 22 | return h.ID 23 | } 24 | func (h *hasRole) TypeName() string { 25 | return h.Name 26 | } 27 | 28 | type PermissionOption interface { 29 | Check(ctx echo.Context) bool 30 | } 31 | 32 | type UnscopedPermission struct { 33 | P string 34 | } 35 | 36 | func (p UnscopedPermission) Check(c echo.Context) bool { 37 | u := c.Get("user").(models.User) 38 | return u.Can(p.P) 39 | } 40 | 41 | type ScopedPermission struct { 42 | P string 43 | IdFieldName string 44 | T string 45 | } 46 | 47 | func (p ScopedPermission) Check(c echo.Context) bool { 48 | idFieldName := "id" 49 | if p.IdFieldName != "" { 50 | idFieldName = p.IdFieldName 51 | } 52 | u := c.Get("user").(models.User) 53 | id, err := strconv.ParseUint(c.Param(idFieldName), 10, strconv.IntSize) 54 | if err != nil { 55 | return false 56 | } 57 | return u.Can(p.P, &hasRole{ 58 | ID: uint(id), 59 | Name: p.T, 60 | }) 61 | } 62 | 63 | type OrPermission struct { 64 | A PermissionOption 65 | B PermissionOption 66 | } 67 | 68 | func (p OrPermission) Check(c echo.Context) bool { 69 | return p.A.Check(c) || p.B.Check(c) 70 | } 71 | 72 | type AndPermission struct { 73 | A PermissionOption 74 | B PermissionOption 75 | } 76 | 77 | func (p AndPermission) Check(c echo.Context) bool { 78 | return p.A.Check(c) && p.B.Check(c) 79 | } 80 | 81 | type CustomPermission struct { 82 | F func(c echo.Context) bool 83 | } 84 | 85 | func (p CustomPermission) Check(c echo.Context) bool { 86 | return p.F(c) 87 | } 88 | 89 | func HasPermission(p PermissionOption) func(next echo.HandlerFunc) echo.HandlerFunc { 90 | return func(next echo.HandlerFunc) echo.HandlerFunc { 91 | return func(c echo.Context) error { 92 | if p.Check(c) { 93 | return next(c) 94 | } 95 | return c.JSON(http.StatusForbidden, response.ErrorResp("PERMISSION_DENIED", nil)) 96 | } 97 | } 98 | } 99 | 100 | func IsTestCaseSample(c echo.Context) (result bool) { 101 | user := c.Get("user").(models.User) 102 | testCase, problem, err := utils.FindTestCase(c.Param("id"), c.Param("test_case_id"), &user) 103 | if err == nil { 104 | result = testCase.Sample 105 | } 106 | c.Set("test_case", testCase) 107 | c.Set("problem", problem) 108 | c.Set("find_test_case_err", err) 109 | return 110 | } 111 | 112 | func IsTestCaseSampleProblemSet(c echo.Context) (result bool) { 113 | testCase, problem, err := utils.FindTestCase(c.Param("id"), c.Param("test_case_id"), nil) 114 | if err == nil { 115 | result = testCase.Sample 116 | } 117 | c.Set("test_case", testCase) 118 | c.Set("problem", problem) 119 | c.Set("find_test_case_err", err) 120 | return 121 | } 122 | 123 | func ProblemSetStarted(c echo.Context) (result bool) { 124 | problemSet := models.ProblemSet{} 125 | err := base.DB.First(&problemSet, "class_id = ? and id = ?", c.Param("class_id"), c.Param("problem_set_id")).Error 126 | if c.Param("problem_set_id") == "" { 127 | log.Infof("%+v\n%s\n", c.Request(), c.Param("problem_set_id")) 128 | } 129 | c.Set("problem_set", &problemSet) 130 | c.Set("find_problem_set_error", err) 131 | if err == nil { 132 | return time.Now().After(problemSet.StartTime) 133 | } else { 134 | log.Info(c.Request()) 135 | return true 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /app/middleware/recover.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "runtime/debug" 7 | 8 | "github.com/EduOJ/backend/app/response" 9 | "github.com/EduOJ/backend/base/log" 10 | "github.com/labstack/echo/v4" 11 | "github.com/pkg/errors" 12 | "github.com/spf13/viper" 13 | ) 14 | 15 | func Recover(next echo.HandlerFunc) echo.HandlerFunc { 16 | return func(c echo.Context) (err error) { 17 | defer func() { 18 | if xx := recover(); xx != nil { 19 | if err, ok := xx.(error); ok { 20 | log.Error(errors.Wrap(err, "controller panics")) 21 | } else { 22 | log.Error("controller panics: ", xx) 23 | } 24 | if viper.GetBool("debug") { 25 | stack := debug.Stack() 26 | err = c.JSON(http.StatusInternalServerError, response.ErrorResp("INTERNAL_ERROR", fmt.Sprintf("%+v\n%s\n", xx, stack))) 27 | } else { 28 | err = response.InternalErrorResp(c) 29 | } 30 | } 31 | }() 32 | err = next(c) 33 | return 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/middleware/recover_test.go: -------------------------------------------------------------------------------- 1 | package middleware_test 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/EduOJ/backend/app/middleware" 8 | "github.com/EduOJ/backend/app/response" 9 | "github.com/labstack/echo/v4" 10 | "github.com/pkg/errors" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func TestRecover(t *testing.T) { 15 | t.Parallel() 16 | 17 | e := echo.New() 18 | e.Use(middleware.Recover) 19 | e.POST("/panics_with_error", func(context echo.Context) error { 20 | panic(errors.New("123")) 21 | }) 22 | e.POST("/panics_with_other", func(context echo.Context) error { 23 | panic("123") 24 | }) 25 | 26 | resp := response.Response{} 27 | req := makeReq(t, "POST", "/panics_with_error", &bytes.Buffer{}) 28 | httpResp := makeResp(req, e) 29 | mustJsonDecode(httpResp, &resp) 30 | assert.Equal(t, response.MakeInternalErrorResp(), resp) 31 | req = makeReq(t, "POST", "/panics_with_other", &bytes.Buffer{}) 32 | httpResp = makeResp(req, e) 33 | mustJsonDecode(httpResp, &resp) 34 | assert.Equal(t, response.MakeInternalErrorResp(), resp) 35 | } 36 | -------------------------------------------------------------------------------- /app/request/admin_logs.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | type AdminGetLogsRequest struct { 4 | Levels string `json:"levels" form:"levels" query:"levels"` 5 | 6 | Limit int `json:"limit" form:"limit" query:"limit" validate:"max=100,min=0"` 7 | Offset int `json:"offset" form:"offset" query:"offset" validate:"min=0"` 8 | } 9 | -------------------------------------------------------------------------------- /app/request/admin_user.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | type AdminCreateUserRequest struct { 4 | Username string `json:"username" form:"username" query:"username" validate:"required,max=30,min=5,username"` 5 | Nickname string `json:"nickname" form:"nickname" query:"nickname" validate:"required,max=30,min=1"` 6 | Email string `json:"email" form:"email" query:"email" validate:"required,email,max=320,min=5"` 7 | Password string `json:"password" form:"password" query:"password" validate:"required,max=30,min=5"` 8 | // TODO: add to class 9 | } 10 | 11 | type AdminUpdateUserRequest struct { 12 | Username string `json:"username" form:"username" query:"username" validate:"required,max=30,min=5,username"` 13 | Nickname string `json:"nickname" form:"nickname" query:"nickname" validate:"required,max=30,min=1"` 14 | Email string `json:"email" form:"email" query:"email" validate:"required,email,max=320,min=5"` 15 | Password string `json:"password" form:"password" query:"password" validate:"omitempty,required,max=30,min=5"` 16 | // TODO: add to class 17 | } 18 | 19 | type AdminDeleteUserRequest struct { 20 | } 21 | 22 | type AdminGetUserRequest struct { 23 | } 24 | 25 | type AdminGetUsersRequest struct { 26 | Search string `json:"search" form:"search" query:"search"` 27 | 28 | Limit int `json:"limit" form:"limit" query:"limit" validate:"max=100,min=0"` 29 | Offset int `json:"offset" form:"offset" query:"offset" validate:"min=0"` 30 | 31 | // OrderBy example: username.DESC 32 | OrderBy string `json:"order_by" form:"order_by" query:"order_by"` 33 | } 34 | -------------------------------------------------------------------------------- /app/request/auth.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | type LoginRequest struct { 4 | // The username or email of the user logging in. 5 | UsernameOrEmail string `json:"username" form:"username" query:"username" validate:"required,min=5,max=320,username|email" example:"username"` 6 | // The password of the user logging in. 7 | Password string `json:"password" form:"password" query:"password" validate:"required,min=5,max=30" example:"password"` 8 | // If true, the created token will last longer. 9 | RememberMe bool `json:"remember_me" example:"false"` 10 | } 11 | 12 | type RegisterRequest struct { 13 | Username string `json:"username" form:"username" query:"username" validate:"required,max=30,min=5,username"` 14 | Nickname string `json:"nickname" form:"nickname" query:"nickname" validate:"required,max=30,min=1"` 15 | Email string `json:"email" form:"email" query:"email" validate:"required,email,max=320,min=5"` 16 | Password string `json:"password" form:"password" query:"password" validate:"required,max=30,min=5"` 17 | } 18 | 19 | type UpdateEmailRequest struct { 20 | Email string `json:"email" form:"email" query:"email" validate:"required,email,max=320,min=5"` 21 | } 22 | 23 | type EmailRegisteredRequest struct { 24 | Email string `json:"email" form:"email" query:"email" validate:"required,email,max=320,min=5"` 25 | } 26 | 27 | type RequestResetPasswordRequest struct { 28 | UsernameOrEmail string `json:"username" form:"username" query:"username" validate:"required,min=5,username|email"` 29 | } 30 | 31 | type DoResetPasswordRequest struct { 32 | UsernameOrEmail string `json:"username" form:"username" query:"username" validate:"required,min=5,username|email"` 33 | Token string `json:"token" form:"token" query:"token" validate:"required,max=5,min=5"` 34 | Password string `json:"password" form:"password" query:"password" validate:"required,max=30,min=5"` 35 | } 36 | -------------------------------------------------------------------------------- /app/request/class.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | type CreateClassRequest struct { 4 | Name string `json:"name" form:"name" query:"name" validate:"required,max=255"` 5 | CourseName string `json:"course_name" form:"course_name" query:"course_name" validate:"required,max=255"` 6 | Description string `json:"description" form:"description" query:"description" validate:"required"` 7 | } 8 | 9 | type GetClassRequest struct { 10 | } 11 | 12 | type UpdateClassRequest struct { 13 | Name string `json:"name" form:"name" query:"name" validate:"required,max=255"` 14 | CourseName string `json:"course_name" form:"course_name" query:"course_name" validate:"required,max=255"` 15 | Description string `json:"description" form:"description" query:"description" validate:"required"` 16 | } 17 | 18 | type AddStudentsRequest struct { 19 | UserIds []uint `json:"user_ids" form:"user_ids" query:"user_ids" validate:"required,min=1"` 20 | } 21 | 22 | type DeleteStudentsRequest struct { 23 | UserIds []uint `json:"user_ids" form:"user_ids" query:"user_ids" validate:"required,min=1"` 24 | } 25 | 26 | type RefreshInviteCodeRequest struct { 27 | } 28 | 29 | type JoinClassRequest struct { 30 | InviteCode string `json:"invite_code" form:"invite_code" query:"invite_code" validate:"required,max=255"` 31 | } 32 | 33 | type DeleteClassRequest struct { 34 | } 35 | -------------------------------------------------------------------------------- /app/request/judger.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | // QueryParam: poll 4 | // 1 for poll 5 | // 0 for immediate response 6 | type GetTaskRequest struct { 7 | } 8 | 9 | type UpdateRunRequest struct { 10 | /* 11 | PENDING / JUDGING / JUDGEMENT_FAILED / NO_COMMENT 12 | ACCEPTED / WRONG_ANSWER / RUNTIME_ERROR / TIME_LIMIT_EXCEEDED / MEMORY_LIMIT_EXCEEDED / DANGEROUS_SYSTEM_CALLS 13 | */ 14 | Status string `json:"status" form:"status" query:"status" validate:"required"` 15 | MemoryUsed *uint `json:"memory_used" form:"memory_used" query:"memory_used" validate:"required"` // Byte 16 | TimeUsed *uint `json:"time_used" form:"time_used" query:"time_used" validate:"required"` // ms 17 | // 去掉空格回车tab后的sha256 18 | OutputStrippedHash *string `json:"output_stripped_hash" form:"output_stripped_hash" query:"output_stripped_hash" validate:"required"` 19 | // OutputFile multipart:file 20 | // CompilerFile multipart:file 21 | // ComparerFile multipart:file 22 | Message string `json:"message" form:"message" query:"message"` 23 | } 24 | -------------------------------------------------------------------------------- /app/request/problem.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | type CreateProblemRequest struct { 4 | Name string `json:"name" form:"name" query:"name" validate:"required,max=255"` 5 | Description string `json:"description" form:"description" query:"description" validate:"required"` 6 | // attachment_file(optional) 7 | Public *bool `json:"public" form:"public" query:"public" validate:"required"` 8 | Privacy *bool `json:"privacy" form:"privacy" query:"privacy" validate:"required"` 9 | 10 | //Sanitize *bool `json:"sanitize" form:"sanitize" query:"sanitize" validate:"required"` 11 | 12 | MemoryLimit uint64 `json:"memory_limit" form:"memory_limit" query:"memory_limit" validate:"required"` // Byte 13 | TimeLimit uint `json:"time_limit" form:"time_limit" query:"time_limit" validate:"required"` // ms 14 | LanguageAllowed string `json:"language_allowed" form:"language_allowed" query:"language_allowed" validate:"required,max=255"` // E.g. cpp,c,java,python 15 | BuildArg string `json:"build_arg" form:"build_arg" query:"build_arg" validate:"max=255"` // E.g. O2=false 16 | CompareScriptName string `json:"compare_script_name" form:"compare_script_name" query:"compare_script_name" validate:"required"` 17 | 18 | Tags string `json:"tags" form:"tags" query:"tags"` 19 | } 20 | 21 | type UpdateProblemRequest struct { 22 | Name string `json:"name" form:"name" query:"name" validate:"required,max=255"` 23 | Description string `json:"description" form:"description" query:"description" validate:"required"` 24 | // attachment_file(optional) 25 | Public *bool `json:"public" form:"public" query:"public" validate:"required"` 26 | Privacy *bool `json:"privacy" form:"privacy" query:"privacy" validate:"required"` 27 | 28 | MemoryLimit uint64 `json:"memory_limit" form:"memory_limit" query:"memory_limit" validate:"required"` // Byte 29 | TimeLimit uint `json:"time_limit" form:"time_limit" query:"time_limit" validate:"required"` // ms 30 | LanguageAllowed string `json:"language_allowed" form:"language_allowed" query:"language_allowed" validate:"required,max=255"` // E.g. cpp,c,java,python 31 | BuildArg string `json:"build_arg" form:"build_arg" query:"build_arg" validate:"max=255"` // E.g. O2=false 32 | CompareScriptName string `json:"compare_script_name" form:"compare_script_name" query:"compare_script_name" validate:"required"` 33 | 34 | Tags string `json:"tags" form:"tags" query:"tags"` 35 | } 36 | 37 | type DeleteProblemRequest struct { 38 | } 39 | 40 | type CreateTestCaseRequest struct { 41 | Score uint `json:"score" form:"score" query:"score"` 42 | Sample *bool `json:"sample" form:"sample" query:"sample" validate:"required"` 43 | Sanitize *bool `json:"sanitize" form:"sanitize" query:"sanitize" validate:"required"` 44 | // input_file(required) 45 | // output_file(required) 46 | } 47 | 48 | type GetTestCaseInputFileRequest struct { 49 | } 50 | 51 | type GetTestCaseOutputFileRequest struct { 52 | } 53 | 54 | type UpdateTestCaseRequest struct { 55 | Score uint `json:"score" form:"score" query:"score"` 56 | Sample *bool `json:"sample" form:"sample" query:"sample" validate:"required"` 57 | Sanitize *bool `json:"sanitize" form:"sanitize" query:"sanitize" validate:"required"` 58 | // input_file(optional) 59 | // output_file(optional) 60 | } 61 | 62 | type DeleteTestCaseRequest struct { 63 | } 64 | 65 | type DeleteTestCasesRequest struct { 66 | } 67 | 68 | type GetProblemRequest struct { 69 | } 70 | 71 | type GetProblemsRequest struct { 72 | Search string `json:"search" form:"search" query:"search"` 73 | UserID uint `json:"user_id" form:"user_id" query:"user_id" validate:"min=0,required_with=Tried Passed"` 74 | 75 | Limit int `json:"limit" form:"limit" query:"limit" validate:"max=100,min=0"` 76 | Offset int `json:"offset" form:"offset" query:"offset" validate:"min=0"` 77 | 78 | Tried bool `json:"tried" form:"tried" query:"tried"` 79 | Passed bool `json:"passed" form:"passed" query:"passed"` 80 | 81 | Tags string `json:"tags" form:"tags" query:"tags"` 82 | } 83 | 84 | type GetRandomProblemRequest struct { 85 | } 86 | -------------------------------------------------------------------------------- /app/request/problem_set.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | import "time" 4 | 5 | type CreateProblemSetRequest struct { 6 | Name string `json:"name" form:"name" query:"name" validate:"required,max=255"` 7 | Description string `json:"description" form:"description" query:"description" validate:"required"` 8 | 9 | StartTime time.Time `json:"start_time" form:"start_time" query:"start_time" validate:"required"` 10 | EndTime time.Time `json:"end_time" form:"end_time" query:"end_time" validate:"required,gtefield=StartTime"` 11 | } 12 | 13 | type CloneProblemSetRequest struct { 14 | SourceClassID uint `json:"source_class_id" form:"source_class_id" query:"source_class_id" validate:"required"` 15 | SourceProblemSetID uint `json:"source_problem_set_id" form:"source_problem_set_id" query:"source_problem_set_id" validate:"required"` 16 | } 17 | 18 | type GetProblemSetRequest struct { 19 | } 20 | 21 | type UpdateProblemSetRequest struct { 22 | Name string `json:"name" form:"name" query:"name" validate:"required,max=255"` 23 | Description string `json:"description" form:"description" query:"description" validate:"required"` 24 | 25 | StartTime time.Time `json:"start_time" form:"start_time" query:"start_time" validate:"required"` 26 | EndTime time.Time `json:"end_time" form:"end_time" query:"end_time" validate:"required,gtefield=StartTime"` 27 | } 28 | 29 | type AddProblemsToSetRequest struct { 30 | ProblemIDs []uint `json:"problem_ids" form:"problem_ids" query:"problem_ids" validate:"required,min=1"` 31 | } 32 | 33 | type DeleteProblemsFromSetRequest struct { 34 | ProblemIDs []uint `json:"problem_ids" form:"problem_ids" query:"problem_ids" validate:"required,min=1"` 35 | } 36 | 37 | type DeleteProblemSetRequest struct { 38 | } 39 | 40 | type GetProblemSetProblemRequest struct { 41 | } 42 | 43 | type GetProblemSetProblemInputFileRequest struct { 44 | } 45 | 46 | type GetProblemSetProblemOutputFileRequest struct { 47 | } 48 | 49 | type GetClassGradesRequest struct { 50 | } 51 | 52 | type GetProblemSetGradesRequest struct { 53 | } 54 | 55 | type RefreshGradesRequest struct { 56 | } 57 | -------------------------------------------------------------------------------- /app/request/problem_set_submission.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | type ProblemSetCreateSubmissionRequest struct { 4 | Language string `json:"language" form:"language" query:"language" validate:"required"` 5 | // code(required) 6 | } 7 | 8 | type ProblemSetGetSubmissionRequest struct { 9 | } 10 | 11 | type ProblemSetGetSubmissionsRequest struct { 12 | ProblemId uint `json:"problem_id" form:"problem_id" query:"problem_id"` 13 | UserId uint `json:"user_id" form:"user_id" query:"user_id"` 14 | 15 | Limit int `json:"limit" form:"limit" query:"limit" validate:"max=100,min=0"` 16 | Offset int `json:"offset" form:"offset" query:"offset" validate:"min=0"` 17 | } 18 | -------------------------------------------------------------------------------- /app/request/script.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | // GetScriptRequest 4 | // No request params / bodies 5 | type GetScriptRequest struct { 6 | } 7 | -------------------------------------------------------------------------------- /app/request/submission.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | type CreateSubmissionRequest struct { 4 | Language string `json:"language" form:"language" query:"language" validate:"required"` 5 | // code(required) 6 | } 7 | 8 | type GetSubmissionRequest struct { 9 | } 10 | 11 | type GetSubmissionsRequest struct { 12 | ProblemId uint `json:"problem_id" form:"problem_id" query:"problem_id"` 13 | UserId uint `json:"user_id" form:"user_id" query:"user_id"` 14 | 15 | Limit int `json:"limit" form:"limit" query:"limit" validate:"max=100,min=0"` 16 | Offset int `json:"offset" form:"offset" query:"offset" validate:"min=0"` 17 | } 18 | -------------------------------------------------------------------------------- /app/request/user.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | type GetUserRequest struct { 4 | } 5 | 6 | type GetUsersRequest struct { 7 | Search string `json:"search" form:"search" query:"search"` 8 | 9 | Limit int `json:"limit" form:"limit" query:"limit" validate:"max=100,min=0"` 10 | Offset int `json:"offset" form:"offset" query:"offset" validate:"min=0"` 11 | 12 | // OrderBy example: username.DESC 13 | OrderBy string `json:"order_by" form:"order_by" query:"order_by"` 14 | } 15 | 16 | type GetMeRequest struct { 17 | } 18 | 19 | type UpdateMeRequest struct { 20 | Username string `json:"username" form:"username" query:"username" validate:"required,max=30,min=5,username"` 21 | Nickname string `json:"nickname" form:"nickname" query:"nickname" validate:"required,max=30,min=1"` 22 | Email string `json:"email" form:"email" query:"email" validate:"required,email,max=320,min=5"` 23 | } 24 | type ChangePasswordRequest struct { 25 | OldPassword string `json:"old_password" form:"old_password" query:"old_password" validate:"required,max=30,min=5"` 26 | NewPassword string `json:"new_password" form:"new_password" query:"new_password" validate:"required,max=30,min=5"` 27 | } 28 | 29 | type GetClassesIManageRequest struct { 30 | } 31 | 32 | type GetClassesITakeRequest struct { 33 | } 34 | 35 | type GetUserProblemInfoRequest struct { 36 | } 37 | 38 | type VerifyEmailRequest struct { 39 | Token string `json:"token" form:"token" query:"token" validate:"required,max=5,min=5"` 40 | } 41 | -------------------------------------------------------------------------------- /app/response/admin_logs.go: -------------------------------------------------------------------------------- 1 | package response 2 | 3 | import ( 4 | "github.com/EduOJ/backend/database/models/log" 5 | ) 6 | 7 | type AdminGetLogsResponse struct { 8 | Message string `json:"message"` 9 | Error interface{} `json:"error"` 10 | Data struct { 11 | Logs []log.Log `json:"logs"` 12 | Total int `json:"total"` 13 | Count int `json:"count"` 14 | Offset int `json:"offset"` 15 | Prev *string `json:"prev"` 16 | Next *string `json:"next"` 17 | } `json:"data"` 18 | } 19 | -------------------------------------------------------------------------------- /app/response/admin_user.go: -------------------------------------------------------------------------------- 1 | package response 2 | 3 | import ( 4 | "github.com/EduOJ/backend/app/response/resource" 5 | ) 6 | 7 | type AdminCreateUserResponse struct { 8 | Message string `json:"message"` 9 | Error interface{} `json:"error"` 10 | Data struct { 11 | *resource.UserForAdmin `json:"user"` 12 | } `json:"data"` 13 | } 14 | 15 | type AdminUpdateUserResponse struct { 16 | Message string `json:"message"` 17 | Error interface{} `json:"error"` 18 | Data struct { 19 | *resource.UserForAdmin `json:"user"` 20 | } `json:"data"` 21 | } 22 | 23 | type AdminGetUserResponse struct { 24 | Message string `json:"message"` 25 | Error interface{} `json:"error"` 26 | Data struct { 27 | *resource.UserForAdmin `json:"user"` 28 | } `json:"data"` 29 | } 30 | 31 | type AdminGetUsersResponse struct { 32 | Message string `json:"message"` 33 | Error interface{} `json:"error"` 34 | Data struct { 35 | Users []resource.User `json:"users"` 36 | Total int `json:"total"` 37 | Count int `json:"count"` 38 | Offset int `json:"offset"` 39 | Prev *string `json:"prev"` 40 | Next *string `json:"next"` 41 | } `json:"data"` 42 | } 43 | -------------------------------------------------------------------------------- /app/response/auth.go: -------------------------------------------------------------------------------- 1 | package response 2 | 3 | import ( 4 | "github.com/EduOJ/backend/app/response/resource" 5 | ) 6 | 7 | type RegisterResponse struct { 8 | Message string `json:"message"` 9 | Error interface{} `json:"error"` 10 | Data struct { 11 | User resource.UserForAdmin `json:"user"` 12 | Token string `json:"token"` 13 | } `json:"data"` 14 | } 15 | 16 | type LoginResponse struct { 17 | Message string `json:"message"` 18 | Error interface{} `json:"error"` 19 | Data struct { 20 | User resource.UserForAdmin `json:"user"` 21 | Token string `json:"token"` 22 | } `json:"data"` 23 | } 24 | 25 | type UpdateEmailResponse struct { 26 | Message string `json:"message"` 27 | Error interface{} `json:"error"` 28 | Data struct { 29 | *resource.UserForAdmin `json:"user"` 30 | } `json:"data"` 31 | } 32 | 33 | type RequestResetPasswordResponse struct { 34 | Message string `json:"message"` 35 | Error interface{} `json:"error"` 36 | Data interface{} `json:"data"` 37 | } 38 | 39 | type ResendEmailVerificationResponse struct { 40 | Message string `json:"message"` 41 | Error interface{} `json:"error"` 42 | Data interface{} `json:"data"` 43 | } 44 | 45 | type EmailVerificationResponse struct { 46 | Message string `json:"message"` 47 | Error interface{} `json:"error"` 48 | Data interface{} `json:"data"` 49 | } 50 | -------------------------------------------------------------------------------- /app/response/basic.go: -------------------------------------------------------------------------------- 1 | package response 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/labstack/echo/v4" 7 | ) 8 | 9 | type ValidationError struct { 10 | Field string `json:"field"` 11 | Reason string `json:"reason"` 12 | Translation string `json:"translation"` 13 | } 14 | 15 | type Response struct { 16 | Message string `json:"message"` 17 | Error interface{} `json:"error"` 18 | Data interface{} `json:"data"` 19 | } 20 | 21 | func InternalErrorResp(c echo.Context) error { 22 | return c.JSON(http.StatusInternalServerError, MakeInternalErrorResp()) 23 | } 24 | 25 | func MakeInternalErrorResp() Response { 26 | return Response{ 27 | Message: "INTERNAL_ERROR", 28 | Error: nil, 29 | Data: nil, 30 | } 31 | } 32 | 33 | func ErrorResp(message string, error interface{}) Response { 34 | return Response{ 35 | Message: message, 36 | Error: error, 37 | Data: nil, 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/response/class.go: -------------------------------------------------------------------------------- 1 | package response 2 | 3 | import "github.com/EduOJ/backend/app/response/resource" 4 | 5 | type CreateClassResponse struct { 6 | Message string `json:"message"` 7 | Error interface{} `json:"error"` 8 | Data struct { 9 | *resource.ClassDetail `json:"class"` 10 | } `json:"data"` 11 | } 12 | 13 | type GetClassResponse struct { 14 | Message string `json:"message"` 15 | Error interface{} `json:"error"` 16 | Data struct { 17 | *resource.Class `json:"class"` 18 | } `json:"data"` 19 | } 20 | 21 | type GetClassResponseForAdmin struct { 22 | Message string `json:"message"` 23 | Error interface{} `json:"error"` 24 | Data struct { 25 | *resource.ClassDetail `json:"class"` 26 | } `json:"data"` 27 | } 28 | 29 | type UpdateClassResponse struct { 30 | Message string `json:"message"` 31 | Error interface{} `json:"error"` 32 | Data struct { 33 | *resource.ClassDetail `json:"class"` 34 | } `json:"data"` 35 | } 36 | 37 | type AddStudentsResponse struct { 38 | Message string `json:"message"` 39 | Error interface{} `json:"error"` 40 | Data struct { 41 | *resource.ClassDetail `json:"class"` 42 | } `json:"data"` 43 | } 44 | 45 | type DeleteStudentsResponse struct { 46 | Message string `json:"message"` 47 | Error interface{} `json:"error"` 48 | Data struct { 49 | *resource.ClassDetail `json:"class"` 50 | } `json:"data"` 51 | } 52 | 53 | type RefreshInviteCodeResponse struct { 54 | Message string `json:"message"` 55 | Error interface{} `json:"error"` 56 | Data struct { 57 | *resource.ClassDetail `json:"class"` 58 | } `json:"data"` 59 | } 60 | 61 | type JoinClassResponse struct { 62 | Message string `json:"message"` 63 | Error interface{} `json:"error"` 64 | Data struct { 65 | *resource.Class `json:"class"` 66 | } `json:"data"` 67 | } 68 | -------------------------------------------------------------------------------- /app/response/images.go: -------------------------------------------------------------------------------- 1 | package response 2 | 3 | type CreateImageResponse struct { 4 | Message string `json:"message"` 5 | Error interface{} `json:"error"` 6 | Data struct { 7 | FilePath string `json:"filename"` 8 | } `json:"data"` 9 | } 10 | -------------------------------------------------------------------------------- /app/response/judger.go: -------------------------------------------------------------------------------- 1 | package response 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/EduOJ/backend/database/models" 7 | ) 8 | 9 | type GetTaskResponse struct { 10 | Message string `json:"message"` 11 | Error interface{} `json:"error"` 12 | Data struct { 13 | RunID uint `json:"run_id"` 14 | Language models.Language `json:"language"` 15 | TestCaseID uint `json:"test_case_id"` 16 | InputFile string `json:"input_file"` // pre-signed url 17 | OutputFile string `json:"output_file"` // same as above 18 | CodeFile string `json:"code_file"` 19 | TestCaseUpdatedAt time.Time `json:"test_case_updated_at"` 20 | MemoryLimit uint64 `json:"memory_limit"` // Byte 21 | TimeLimit uint `json:"time_limit"` // ms 22 | BuildArg string `json:"build_arg"` // E.g. O2=false 23 | CompareScript models.Script `json:"compare_script"` 24 | } `json:"data"` 25 | } 26 | -------------------------------------------------------------------------------- /app/response/problem.go: -------------------------------------------------------------------------------- 1 | package response 2 | 3 | import ( 4 | "github.com/EduOJ/backend/app/response/resource" 5 | ) 6 | 7 | type GetProblemResponse struct { 8 | Message string `json:"message"` 9 | Error interface{} `json:"error"` 10 | Data struct { 11 | *resource.Problem `json:"problem"` 12 | } `json:"data"` 13 | } 14 | 15 | type GetProblemsResponse struct { 16 | Message string `json:"message"` 17 | Error interface{} `json:"error"` 18 | Data struct { 19 | Problems []resource.ProblemSummary `json:"problems"` 20 | Total int `json:"total"` 21 | Count int `json:"count"` 22 | Offset int `json:"offset"` 23 | Prev *string `json:"prev"` 24 | Next *string `json:"next"` 25 | } `json:"data"` 26 | } 27 | 28 | type CreateProblemResponse struct { 29 | Message string `json:"message"` 30 | Error interface{} `json:"error"` 31 | Data struct { 32 | *resource.ProblemForAdmin `json:"problem"` 33 | } `json:"data"` 34 | } 35 | 36 | type GetProblemResponseForAdmin struct { 37 | Message string `json:"message"` 38 | Error interface{} `json:"error"` 39 | Data struct { 40 | *resource.ProblemForAdmin `json:"problem"` 41 | } `json:"data"` 42 | } 43 | 44 | type GetProblemsResponseForAdmin struct { 45 | Message string `json:"message"` 46 | Error interface{} `json:"error"` 47 | Data struct { 48 | Problems []resource.ProblemSummaryForAdmin `json:"problems"` 49 | Total int `json:"total"` 50 | Count int `json:"count"` 51 | Offset int `json:"offset"` 52 | Prev *string `json:"prev"` 53 | Next *string `json:"next"` 54 | } `json:"data"` 55 | } 56 | 57 | type UpdateProblemResponse struct { 58 | Message string `json:"message"` 59 | Error interface{} `json:"error"` 60 | Data struct { 61 | *resource.ProblemForAdmin `json:"problem"` 62 | } `json:"data"` 63 | } 64 | 65 | type CreateTestCaseResponse struct { 66 | Message string `json:"message"` 67 | Error interface{} `json:"error"` 68 | Data struct { 69 | *resource.TestCaseForAdmin `json:"test_case"` 70 | } `json:"data"` 71 | } 72 | 73 | type UpdateTestCaseResponse struct { 74 | Message string `json:"message"` 75 | Error interface{} `json:"error"` 76 | Data struct { 77 | *resource.TestCaseForAdmin `json:"test_case"` 78 | } `json:"data"` 79 | } 80 | 81 | type GetRandomProblemResponse struct { 82 | Message string `json:"message"` 83 | Error interface{} `json:"error"` 84 | Data struct { 85 | *resource.Problem `json:"problem"` 86 | } `json:"data"` 87 | } 88 | -------------------------------------------------------------------------------- /app/response/problem_set.go: -------------------------------------------------------------------------------- 1 | package response 2 | 3 | import "github.com/EduOJ/backend/app/response/resource" 4 | 5 | type CreateProblemSetResponse struct { 6 | Message string `json:"message"` 7 | Error interface{} `json:"error"` 8 | Data struct { 9 | *resource.ProblemSetDetail `json:"problem_set"` 10 | } `json:"data"` 11 | } 12 | 13 | type CloneProblemSetResponse struct { 14 | Message string `json:"message"` 15 | Error interface{} `json:"error"` 16 | Data struct { 17 | *resource.ProblemSetDetail `json:"problem_set"` 18 | } `json:"data"` 19 | } 20 | 21 | type GetProblemSetResponseForAdmin struct { 22 | Message string `json:"message"` 23 | Error interface{} `json:"error"` 24 | Data struct { 25 | *resource.ProblemSetDetail `json:"problem_set"` 26 | } `json:"data"` 27 | } 28 | 29 | type GetProblemSetResponse struct { 30 | Message string `json:"message"` 31 | Error interface{} `json:"error"` 32 | Data struct { 33 | *resource.ProblemSet `json:"problem_set"` 34 | } `json:"data"` 35 | } 36 | 37 | type GetProblemSetResponseSummary struct { 38 | Message string `json:"message"` 39 | Error interface{} `json:"error"` 40 | Data struct { 41 | *resource.ProblemSetSummary `json:"problem_set"` 42 | } `json:"data"` 43 | } 44 | 45 | type UpdateProblemSetResponse struct { 46 | Message string `json:"message"` 47 | Error interface{} `json:"error"` 48 | Data struct { 49 | *resource.ProblemSetDetail `json:"problem_set"` 50 | } `json:"data"` 51 | } 52 | 53 | type AddProblemsToSetResponse struct { 54 | Message string `json:"message"` 55 | Error interface{} `json:"error"` 56 | Data struct { 57 | *resource.ProblemSetDetail `json:"problem_set"` 58 | } `json:"data"` 59 | } 60 | 61 | type DeleteProblemsFromSetResponse struct { 62 | Message string `json:"message"` 63 | Error interface{} `json:"error"` 64 | Data struct { 65 | *resource.ProblemSetDetail `json:"problem_set"` 66 | } `json:"data"` 67 | } 68 | 69 | type GetProblemSetProblemResponseForAdmin struct { 70 | Message string `json:"message"` 71 | Error interface{} `json:"error"` 72 | Data struct { 73 | *resource.ProblemForAdmin `json:"problem"` 74 | } `json:"data"` 75 | } 76 | 77 | type GetProblemSetProblemResponse struct { 78 | Message string `json:"message"` 79 | Error interface{} `json:"error"` 80 | Data struct { 81 | *resource.Problem `json:"problem"` 82 | } `json:"data"` 83 | } 84 | 85 | type GetClassGradesResponse struct { 86 | Message string `json:"message"` 87 | Error interface{} `json:"error"` 88 | Data struct { 89 | ProblemSets []*resource.ProblemSetWithGrades `json:"problem_sets"` 90 | } `json:"data"` 91 | } 92 | 93 | type GetProblemSetGradesResponse struct { 94 | Message string `json:"message"` 95 | Error interface{} `json:"error"` 96 | Data struct { 97 | *resource.ProblemSetWithGrades `json:"problem_set"` 98 | } `json:"data"` 99 | } 100 | 101 | type RefreshGradesResponse struct { 102 | Message string `json:"message"` 103 | Error interface{} `json:"error"` 104 | Data struct { 105 | *resource.ProblemSetWithGrades `json:"problem_set"` 106 | } `json:"data"` 107 | } 108 | -------------------------------------------------------------------------------- /app/response/problem_set_submission.go: -------------------------------------------------------------------------------- 1 | package response 2 | 3 | import ( 4 | "github.com/EduOJ/backend/app/response/resource" 5 | ) 6 | 7 | type ProblemSetCreateSubmissionResponse struct { 8 | Message string `json:"message"` 9 | Error interface{} `json:"error"` 10 | Data struct { 11 | *resource.SubmissionDetail `json:"submission"` 12 | } `json:"data"` 13 | } 14 | 15 | type ProblemSetGetSubmissionResponse struct { 16 | Message string `json:"message"` 17 | Error interface{} `json:"error"` 18 | Data struct { 19 | *resource.SubmissionDetail `json:"submission"` 20 | } `json:"data"` 21 | } 22 | 23 | type ProblemSetGetSubmissionsResponse struct { 24 | Message string `json:"message"` 25 | Error interface{} `json:"error"` 26 | Data struct { 27 | Submissions []resource.Submission `json:"submissions"` 28 | Total int `json:"total"` 29 | Count int `json:"count"` 30 | Offset int `json:"offset"` 31 | Prev *string `json:"prev"` 32 | Next *string `json:"next"` 33 | } `json:"data"` 34 | } 35 | -------------------------------------------------------------------------------- /app/response/resource/class.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import "github.com/EduOJ/backend/database/models" 4 | 5 | type Class struct { 6 | ID uint `json:"id"` 7 | 8 | Name string `json:"name"` 9 | CourseName string `json:"course_name"` 10 | Description string `json:"description"` 11 | 12 | Managers []User `json:"managers"` 13 | Students []User `json:"students"` 14 | ProblemSets []ProblemSetSummary `json:"problem_sets"` 15 | } 16 | 17 | type ClassDetail struct { 18 | ID uint `json:"id"` 19 | 20 | Name string `json:"name"` 21 | CourseName string `json:"course_name"` 22 | Description string `json:"description"` 23 | InviteCode string `json:"invite_code"` 24 | 25 | Managers []User `json:"managers"` 26 | Students []User `json:"students"` 27 | ProblemSets []ProblemSetSummary `json:"problem_sets"` 28 | } 29 | 30 | func (c *Class) convert(class *models.Class) { 31 | c.ID = class.ID 32 | c.Name = class.Name 33 | c.CourseName = class.CourseName 34 | c.Description = class.Description 35 | c.Managers = GetUserSlice(class.Managers) 36 | c.Students = GetUserSlice(class.Students) 37 | c.ProblemSets = GetProblemSetSummarySlice(class.ProblemSets) 38 | } 39 | 40 | func (c *ClassDetail) convert(class *models.Class) { 41 | c.ID = class.ID 42 | c.Name = class.Name 43 | c.CourseName = class.CourseName 44 | c.Description = class.Description 45 | c.InviteCode = class.InviteCode 46 | c.Managers = GetUserSlice(class.Managers) 47 | c.Students = GetUserSlice(class.Students) 48 | c.ProblemSets = GetProblemSetSummarySlice(class.ProblemSets) 49 | } 50 | 51 | func GetClass(class *models.Class) *Class { 52 | c := Class{} 53 | c.convert(class) 54 | return &c 55 | } 56 | 57 | func GetClassDetail(class *models.Class) *ClassDetail { 58 | c := ClassDetail{} 59 | c.convert(class) 60 | return &c 61 | } 62 | 63 | func GetClassSlice(classes []models.Class) (c []Class) { 64 | c = make([]Class, len(classes)) 65 | for i, class := range classes { 66 | c[i].convert(&class) 67 | } 68 | return 69 | } 70 | -------------------------------------------------------------------------------- /app/response/resource/permission.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import "github.com/EduOJ/backend/database/models" 4 | 5 | type Permission struct { 6 | ID uint `gorm:"primaryKey" json:"id"` 7 | Name string `json:"name"` 8 | } 9 | 10 | type Role struct { 11 | ID uint `json:"id"` 12 | Name string `json:"name"` 13 | Target *string `json:"target"` 14 | Permissions []Permission `json:"permissions"` 15 | TargetID uint `json:"target_id"` 16 | } 17 | 18 | func (p *Permission) convert(perm *models.Permission) { 19 | p.ID = perm.ID 20 | p.Name = perm.Name 21 | } 22 | 23 | func (p *Role) convert(userHasRole *models.UserHasRole) { 24 | p.Name = userHasRole.Role.Name 25 | p.Target = userHasRole.Role.Target 26 | p.TargetID = userHasRole.TargetID 27 | p.Permissions = make([]Permission, len(userHasRole.Role.Permissions)) 28 | for i, perm := range userHasRole.Role.Permissions { 29 | p.Permissions[i].convert(&perm) 30 | } 31 | } 32 | 33 | func GetPermission(perm *models.Permission) *Permission { 34 | p := Permission{} 35 | p.convert(perm) 36 | return &p 37 | } 38 | 39 | func GetRole(role *models.UserHasRole) *Role { 40 | p := Role{} 41 | p.convert(role) 42 | return &p 43 | } 44 | 45 | func GetRoleSlice(roles []models.UserHasRole) (profiles []Role) { 46 | profiles = make([]Role, len(roles)) 47 | for i, role := range roles { 48 | profiles[i].convert(&role) 49 | } 50 | return 51 | } 52 | -------------------------------------------------------------------------------- /app/response/resource/permission_test.go: -------------------------------------------------------------------------------- 1 | package resource_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "time" 7 | 8 | "github.com/EduOJ/backend/app/response/resource" 9 | "github.com/EduOJ/backend/base/utils" 10 | "github.com/EduOJ/backend/database/models" 11 | "github.com/stretchr/testify/assert" 12 | "gorm.io/gorm" 13 | ) 14 | 15 | type roleWithTargetID struct { 16 | role models.Role 17 | id uint 18 | } 19 | 20 | func createPermissionForTest(name string, id uint, roleId uint) models.Permission { 21 | return models.Permission{ 22 | ID: id, 23 | RoleID: roleId, 24 | Name: fmt.Sprintf("test_%s_permission_%d", name, id), 25 | } 26 | } 27 | 28 | func createRoleForTest(name string, id uint, permissionCount uint) models.Role { 29 | target := fmt.Sprintf("test_%s_role_%d_target", name, id) 30 | permissions := make([]models.Permission, permissionCount) 31 | for i := range permissions { 32 | permissions[i] = createPermissionForTest(name, uint(i), id) 33 | } 34 | return models.Role{ 35 | ID: id, 36 | Name: fmt.Sprintf("test_%s_role_%d", name, id), 37 | Target: &target, 38 | Permissions: permissions, 39 | } 40 | } 41 | 42 | func createUserForTest(name string, id uint, roles ...roleWithTargetID) (user models.User) { 43 | user = models.User{ 44 | ID: id, 45 | Username: fmt.Sprintf("test_%s_user_%d", name, id), 46 | Nickname: fmt.Sprintf("test_%s_user_%d_nick", name, id), 47 | Email: fmt.Sprintf("test_%s_user_%d@e.e", name, id), 48 | Password: utils.HashPassword(fmt.Sprintf("test_%s_user_%d_pwd", name, id)), 49 | Roles: make([]models.UserHasRole, len(roles)), 50 | RoleLoaded: true, 51 | CreatedAt: time.Date(int(id), 1, 1, 1, 1, 1, 1, time.FixedZone("test_zone", 0)), 52 | UpdatedAt: time.Date(int(id), 2, 2, 2, 2, 2, 2, time.FixedZone("test_zone", 0)), 53 | DeletedAt: gorm.DeletedAt{}, 54 | } 55 | for i, role := range roles { 56 | user.Roles[i] = models.UserHasRole{ 57 | ID: uint(i), 58 | UserID: user.ID, 59 | RoleID: role.role.ID, 60 | Role: role.role, 61 | TargetID: role.id, 62 | } 63 | } 64 | return 65 | } 66 | 67 | func TestGetPermission(t *testing.T) { 68 | permission := createPermissionForTest("get_permission", 0, 0) 69 | actualPermission := resource.GetPermission(&permission) 70 | expectedPermission := resource.Permission{ 71 | ID: 0, 72 | Name: "test_get_permission_permission_0", 73 | } 74 | assert.Equal(t, expectedPermission, *actualPermission) 75 | } 76 | 77 | func TestGetRoleAndGetRoleSlice(t *testing.T) { 78 | role1 := createRoleForTest("get_role", 1, 1) 79 | role2 := createRoleForTest("get_role", 2, 2) 80 | user := createUserForTest("get_role", 0, 81 | roleWithTargetID{role: role1, id: 1}, 82 | roleWithTargetID{role: role2, id: 2}, 83 | ) 84 | t.Run("testGetRole", func(t *testing.T) { 85 | actualRole := resource.GetRole(&user.Roles[0]) 86 | target := "test_get_role_role_1_target" 87 | expectedRole := resource.Role{ 88 | ID: 0, 89 | Name: "test_get_role_role_1", 90 | Target: &target, 91 | Permissions: []resource.Permission{ 92 | {ID: 0, Name: "test_get_role_permission_0"}, 93 | }, 94 | TargetID: 1, 95 | } 96 | assert.Equal(t, expectedRole, *actualRole) 97 | }) 98 | t.Run("testGetRoleSlice", func(t *testing.T) { 99 | actualRoleSlice := resource.GetRoleSlice(user.Roles) 100 | target1 := "test_get_role_role_1_target" 101 | target2 := "test_get_role_role_2_target" 102 | expectedRoleSlice := []resource.Role{ 103 | { 104 | ID: 0, 105 | Name: "test_get_role_role_1", 106 | Target: &target1, 107 | Permissions: []resource.Permission{ 108 | {ID: 0, Name: "test_get_role_permission_0"}, 109 | }, 110 | TargetID: 1, 111 | }, 112 | { 113 | ID: 0, 114 | Name: "test_get_role_role_2", 115 | Target: &target2, 116 | Permissions: []resource.Permission{ 117 | {ID: 0, Name: "test_get_role_permission_0"}, 118 | {ID: 1, Name: "test_get_role_permission_1"}, 119 | }, 120 | TargetID: 2, 121 | }, 122 | } 123 | assert.Equal(t, expectedRoleSlice, actualRoleSlice) 124 | }) 125 | } 126 | -------------------------------------------------------------------------------- /app/response/resource/problem_set.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "database/sql" 5 | "time" 6 | 7 | "github.com/EduOJ/backend/database/models" 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | type ProblemSetWithGrades struct { 12 | ID uint `json:"id"` 13 | 14 | ClassID uint `json:"class_id"` 15 | Name string `json:"name"` 16 | Description string `json:"description"` 17 | 18 | Problems []ProblemSummary `json:"problems"` 19 | Grades []Grade `json:"grades"` 20 | 21 | StartTime time.Time `json:"start_time"` 22 | EndTime time.Time `json:"end_time"` 23 | } 24 | 25 | type ProblemSetDetail struct { 26 | ID uint `json:"id"` 27 | 28 | ClassID uint `json:"class_id"` 29 | Name string `json:"name"` 30 | Description string `json:"description"` 31 | 32 | Problems []ProblemSummary `json:"problems"` 33 | 34 | StartTime time.Time `json:"start_time"` 35 | EndTime time.Time `json:"end_time"` 36 | } 37 | 38 | type ProblemSet struct { 39 | ID uint `json:"id"` 40 | 41 | ClassID uint `json:"class_id"` 42 | Name string `json:"name"` 43 | Description string `json:"description"` 44 | 45 | Problems []ProblemSummary `json:"problems"` 46 | 47 | StartTime time.Time `json:"start_time"` 48 | EndTime time.Time `json:"end_time"` 49 | } 50 | 51 | type ProblemSetSummary struct { 52 | ID uint `json:"id"` 53 | 54 | ClassID uint `json:"class_id"` 55 | Name string `json:"name"` 56 | Description string `json:"description"` 57 | 58 | StartTime time.Time `json:"start_time"` 59 | EndTime time.Time `json:"end_time"` 60 | } 61 | 62 | type Grade struct { 63 | ID uint `json:"id"` 64 | 65 | UserID uint `json:"user_id"` 66 | User *User `json:"user"` 67 | ProblemSetID uint `json:"problem_set_id"` 68 | 69 | Detail string `json:"detail"` 70 | Total uint `json:"total"` 71 | } 72 | 73 | func (p *ProblemSetWithGrades) convert(problemSet *models.ProblemSet) { 74 | p.ID = problemSet.ID 75 | p.ClassID = problemSet.ClassID 76 | p.Name = problemSet.Name 77 | p.Description = problemSet.Description 78 | p.Problems = GetProblemSummarySlice(problemSet.Problems, make([]sql.NullBool, len(problemSet.Problems))) 79 | p.Grades = GetGradeSlice(problemSet.Grades) 80 | p.StartTime = problemSet.StartTime 81 | p.EndTime = problemSet.EndTime 82 | } 83 | 84 | func (p *ProblemSetDetail) convert(problemSet *models.ProblemSet) { 85 | p.ID = problemSet.ID 86 | p.ClassID = problemSet.ClassID 87 | p.Name = problemSet.Name 88 | p.Description = problemSet.Description 89 | p.Problems = GetProblemSummarySlice(problemSet.Problems, make([]sql.NullBool, len(problemSet.Problems))) 90 | p.StartTime = problemSet.StartTime 91 | p.EndTime = problemSet.EndTime 92 | } 93 | 94 | func (p *ProblemSet) convert(problemSet *models.ProblemSet) { 95 | p.ID = problemSet.ID 96 | p.ClassID = problemSet.ClassID 97 | p.Name = problemSet.Name 98 | p.Description = problemSet.Description 99 | p.Problems = GetProblemSummarySlice(problemSet.Problems, make([]sql.NullBool, len(problemSet.Problems))) 100 | p.StartTime = problemSet.StartTime 101 | p.EndTime = problemSet.EndTime 102 | } 103 | 104 | func (p *ProblemSetSummary) convert(problemSet *models.ProblemSet) { 105 | p.ID = problemSet.ID 106 | p.ClassID = problemSet.ClassID 107 | p.Name = problemSet.Name 108 | p.Description = problemSet.Description 109 | p.StartTime = problemSet.StartTime 110 | p.EndTime = problemSet.EndTime 111 | } 112 | 113 | func GetProblemSet(problemSet *models.ProblemSet) *ProblemSet { 114 | p := ProblemSet{} 115 | p.convert(problemSet) 116 | return &p 117 | } 118 | 119 | func GetProblemSetWithGrades(problemSet *models.ProblemSet) *ProblemSetWithGrades { 120 | p := ProblemSetWithGrades{} 121 | p.convert(problemSet) 122 | return &p 123 | } 124 | 125 | func GetProblemSetDetail(problemSet *models.ProblemSet) *ProblemSetDetail { 126 | p := ProblemSetDetail{} 127 | p.convert(problemSet) 128 | return &p 129 | } 130 | 131 | func GetProblemSetSummary(problemSet *models.ProblemSet) *ProblemSetSummary { 132 | p := ProblemSetSummary{} 133 | p.convert(problemSet) 134 | return &p 135 | } 136 | 137 | func GetProblemSetSummarySlice(problemSetSlice []*models.ProblemSet) (ps []ProblemSetSummary) { 138 | ps = make([]ProblemSetSummary, len(problemSetSlice)) 139 | for i, problemSet := range problemSetSlice { 140 | ps[i].convert(problemSet) 141 | } 142 | return 143 | } 144 | 145 | func GetProblemSetSlice(problemSetSlice []*models.ProblemSet) (ps []ProblemSet) { 146 | ps = make([]ProblemSet, len(problemSetSlice)) 147 | for i, problemSet := range problemSetSlice { 148 | ps[i].convert(problemSet) 149 | } 150 | return 151 | } 152 | 153 | func (g *Grade) convert(grade *models.Grade) { 154 | g.ID = grade.ID 155 | g.UserID = grade.UserID 156 | g.User = GetUser(grade.User) 157 | g.ProblemSetID = grade.ProblemSetID 158 | b, err := grade.Detail.MarshalJSON() 159 | if err != nil { 160 | panic(errors.Wrap(err, "could not marshal json for converting grade")) 161 | } 162 | g.Detail = string(b) 163 | g.Total = grade.Total 164 | } 165 | 166 | func GetGrade(grade *models.Grade) *Grade { 167 | g := Grade{} 168 | g.convert(grade) 169 | return &g 170 | } 171 | 172 | func GetGradeSlice(grades []*models.Grade) (g []Grade) { 173 | g = make([]Grade, len(grades)) 174 | for i, grade := range grades { 175 | g[i].convert(grade) 176 | } 177 | return 178 | } 179 | -------------------------------------------------------------------------------- /app/response/resource/submission.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/EduOJ/backend/database/models" 7 | ) 8 | 9 | type Submission struct { 10 | ID uint `json:"id"` 11 | 12 | UserID uint `json:"user_id"` 13 | User *User `json:"user"` 14 | ProblemID uint `json:"problem_id"` 15 | ProblemName string `json:"problem_name"` 16 | ProblemSetID uint `json:"problem_set_id"` // 0 means not in problem set 17 | Language string `json:"language"` 18 | 19 | Judged bool `json:"judged"` 20 | Score uint `json:"score"` 21 | Status string `json:"status"` 22 | 23 | CreatedAt time.Time `json:"created_at"` 24 | UpdatedAt time.Time `json:"updated_at"` 25 | } 26 | 27 | func (s *Submission) convert(submission *models.Submission) { 28 | s.ID = submission.ID 29 | s.UserID = submission.UserID 30 | s.User = GetUser(submission.User) 31 | s.ProblemID = submission.ProblemID 32 | s.ProblemName = submission.Problem.Name 33 | s.ProblemSetID = submission.ProblemSetID 34 | s.Language = submission.LanguageName 35 | s.Judged = submission.Judged 36 | s.Score = submission.Score 37 | s.Status = submission.Status 38 | s.CreatedAt = submission.CreatedAt 39 | s.UpdatedAt = submission.UpdatedAt 40 | } 41 | 42 | func GetSubmission(submission *models.Submission) *Submission { 43 | s := Submission{} 44 | s.convert(submission) 45 | return &s 46 | } 47 | 48 | func GetSubmissionSlice(submissions []models.Submission) []Submission { 49 | s := make([]Submission, len(submissions)) 50 | for i, submission := range submissions { 51 | s[i].convert(&submission) 52 | } 53 | return s 54 | } 55 | 56 | type SubmissionDetail struct { 57 | ID uint `json:"id"` 58 | 59 | UserID uint `json:"user_id"` 60 | User *User `json:"user"` 61 | ProblemID uint `json:"problem_id"` 62 | ProblemName string `json:"problem_name"` 63 | ProblemSetID uint `json:"problem_set_id"` 64 | Language string `json:"language"` 65 | FileName string `json:"file_name"` 66 | Priority uint8 `json:"priority"` 67 | 68 | Judged bool `json:"judged"` 69 | Score uint `json:"score"` 70 | Status string `json:"status"` 71 | 72 | Runs []Run `json:"runs"` 73 | 74 | CreatedAt time.Time `json:"created_at"` 75 | UpdatedAt time.Time `json:"updated_at"` 76 | } 77 | 78 | func (s *SubmissionDetail) convert(submission *models.Submission) { 79 | s.ID = submission.ID 80 | s.UserID = submission.UserID 81 | s.User = GetUser(submission.User) 82 | s.ProblemID = submission.ProblemID 83 | s.ProblemName = submission.Problem.Name 84 | s.ProblemSetID = submission.ProblemSetID 85 | s.Language = submission.LanguageName 86 | s.FileName = submission.FileName 87 | s.Priority = submission.Priority 88 | s.Judged = submission.Judged 89 | s.Score = submission.Score 90 | s.Status = submission.Status 91 | s.Runs = GetRunSlice(submission.Runs) 92 | s.CreatedAt = submission.CreatedAt 93 | s.UpdatedAt = submission.UpdatedAt 94 | } 95 | 96 | func GetSubmissionDetail(submission *models.Submission) *SubmissionDetail { 97 | s := SubmissionDetail{} 98 | s.convert(submission) 99 | return &s 100 | } 101 | 102 | func GetSubmissionDetailSlice(submissions []models.Submission) []SubmissionDetail { 103 | s := make([]SubmissionDetail, len(submissions)) 104 | for i, submission := range submissions { 105 | s[i].convert(&submission) 106 | } 107 | return s 108 | } 109 | 110 | type Run struct { 111 | ID uint `json:"id"` 112 | 113 | UserID uint `json:"user_id"` 114 | ProblemID uint `json:"problem_id"` 115 | ProblemSetID uint `json:"problem_set_id"` 116 | TestCaseID uint `json:"test_case_id"` 117 | Sample bool `json:"sample"` 118 | SubmissionID uint `json:"submission_id"` 119 | Priority uint8 `json:"priority"` 120 | 121 | Judged bool `json:"judged"` 122 | Status string `json:"status"` // AC WA TLE MLE OLE 123 | MemoryUsed uint `json:"memory_used"` // Byte 124 | TimeUsed uint `json:"time_used"` // ms 125 | 126 | CreatedAt time.Time `json:"created_at"` 127 | UpdatedAt time.Time `json:"updated_at"` 128 | } 129 | 130 | func (r *Run) convert(run *models.Run) { 131 | r.ID = run.ID 132 | r.UserID = run.UserID 133 | r.ProblemID = run.ProblemID 134 | r.ProblemSetID = run.ProblemSetID 135 | r.TestCaseID = run.TestCaseID 136 | r.Sample = run.Sample 137 | r.SubmissionID = run.SubmissionID 138 | r.Priority = run.Priority 139 | r.Judged = run.Judged 140 | r.Status = run.Status 141 | r.MemoryUsed = run.MemoryUsed 142 | r.TimeUsed = run.TimeUsed 143 | r.CreatedAt = run.CreatedAt 144 | r.UpdatedAt = run.UpdatedAt 145 | } 146 | 147 | func GetRun(run *models.Run) *Run { 148 | r := Run{} 149 | r.convert(run) 150 | return &r 151 | } 152 | 153 | func GetRunSlice(runs []models.Run) []Run { 154 | r := make([]Run, len(runs)) 155 | for i, run := range runs { 156 | r[i].convert(&run) 157 | } 158 | return r 159 | } 160 | -------------------------------------------------------------------------------- /app/response/resource/user.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import "github.com/EduOJ/backend/database/models" 4 | 5 | type User struct { 6 | ID uint `json:"id"` 7 | Username string `json:"username"` 8 | Nickname string `json:"nickname"` 9 | Email string `json:"email"` 10 | } 11 | 12 | // @description UserForAdmin is a user with additional, credential data, only accessible by people has permission, 13 | // @description e.g. admin can access to all user's credential data, and a user can access to his/her credential data. 14 | type UserForAdmin struct { 15 | // ID is the user's id. 16 | ID uint `json:"id"` 17 | // Username is the user's username, usually the student ID if used in schools. 18 | Username string `json:"username"` 19 | // Nickname is the user's nickname, usually the student name if used in schools. 20 | Nickname string `json:"nickname"` 21 | // Email is the user's email. 22 | Email string `json:"email"` 23 | 24 | // Role is the user's role, and is used to obtain the permissions of a user. 25 | Roles []Role `json:"roles"` 26 | } 27 | 28 | func (p *User) convert(user *models.User) { 29 | if user == nil { 30 | return 31 | } 32 | p.ID = user.ID 33 | p.Username = user.Username 34 | p.Nickname = user.Nickname 35 | p.Email = user.Email 36 | } 37 | 38 | func (p *UserForAdmin) convert(user *models.User) { 39 | if user == nil { 40 | return 41 | } 42 | p.ID = user.ID 43 | p.Username = user.Username 44 | p.Nickname = user.Nickname 45 | p.Email = user.Email 46 | p.Roles = GetRoleSlice(user.Roles) 47 | } 48 | 49 | func GetUser(user *models.User) *User { 50 | p := User{} 51 | p.convert(user) 52 | return &p 53 | } 54 | 55 | func GetUserForAdmin(user *models.User) *UserForAdmin { 56 | p := UserForAdmin{} 57 | p.convert(user) 58 | return &p 59 | } 60 | 61 | func GetUserSlice(users []*models.User) (profiles []User) { 62 | profiles = make([]User, len(users)) 63 | for i, user := range users { 64 | profiles[i] = *GetUser(user) 65 | } 66 | return 67 | } 68 | -------------------------------------------------------------------------------- /app/response/resource/user_test.go: -------------------------------------------------------------------------------- 1 | package resource_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/EduOJ/backend/app/response/resource" 7 | "github.com/EduOJ/backend/database/models" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestGetUserGetUserForAdminAndGetUserSlice(t *testing.T) { 12 | role1 := createRoleForTest("get_user", 1, 1) 13 | role2 := createRoleForTest("get_user", 2, 2) 14 | user1 := createUserForTest("get_user", 1, 15 | roleWithTargetID{role: role1, id: 1}, 16 | roleWithTargetID{role: role2, id: 2}, 17 | ) 18 | user2 := createUserForTest("get_user", 2) 19 | t.Run("testGetUser", func(t *testing.T) { 20 | actualUser := resource.GetUser(&user1) 21 | expectedUser := resource.User{ 22 | ID: 1, 23 | Username: "test_get_user_user_1", 24 | Nickname: "test_get_user_user_1_nick", 25 | Email: "test_get_user_user_1@e.e", 26 | } 27 | assert.Equal(t, expectedUser, *actualUser) 28 | }) 29 | t.Run("testGetUserNilUser", func(t *testing.T) { 30 | emptyUser := resource.User{} 31 | assert.Equal(t, emptyUser, *resource.GetUser(nil)) 32 | }) 33 | t.Run("testGetUserForAdmin", func(t *testing.T) { 34 | actualUser := resource.GetUserForAdmin(&user1) 35 | target1 := "test_get_user_role_1_target" 36 | target2 := "test_get_user_role_2_target" 37 | expectedUser := resource.UserForAdmin{ 38 | ID: 1, 39 | Username: "test_get_user_user_1", 40 | Nickname: "test_get_user_user_1_nick", 41 | Email: "test_get_user_user_1@e.e", 42 | Roles: []resource.Role{ 43 | { 44 | ID: 0, 45 | Name: "test_get_user_role_1", 46 | Target: &target1, 47 | Permissions: []resource.Permission{ 48 | {ID: 0, Name: "test_get_user_permission_0"}, 49 | }, 50 | TargetID: 1, 51 | }, 52 | { 53 | ID: 0, 54 | Name: "test_get_user_role_2", 55 | Target: &target2, 56 | Permissions: []resource.Permission{ 57 | {ID: 0, Name: "test_get_user_permission_0"}, 58 | {ID: 1, Name: "test_get_user_permission_1"}, 59 | }, 60 | TargetID: 2, 61 | }, 62 | }, 63 | //Grades: []resource.Grade{}, 64 | } 65 | assert.Equal(t, expectedUser, *actualUser) 66 | }) 67 | t.Run("testGetUserForAdminNilUser", func(t *testing.T) { 68 | emptyUser := resource.UserForAdmin{} 69 | assert.Equal(t, emptyUser, *resource.GetUserForAdmin(nil)) 70 | }) 71 | t.Run("testGetUserSlice", func(t *testing.T) { 72 | actualUserSlice := resource.GetUserSlice([]*models.User{ 73 | &user1, &user2, 74 | }) 75 | expectedUserSlice := []resource.User{ 76 | { 77 | ID: 1, 78 | Username: "test_get_user_user_1", 79 | Nickname: "test_get_user_user_1_nick", 80 | Email: "test_get_user_user_1@e.e", 81 | }, 82 | { 83 | ID: 2, 84 | Username: "test_get_user_user_2", 85 | Nickname: "test_get_user_user_2_nick", 86 | Email: "test_get_user_user_2@e.e", 87 | }, 88 | } 89 | assert.Equal(t, expectedUserSlice, actualUserSlice) 90 | }) 91 | } 92 | -------------------------------------------------------------------------------- /app/response/script.go: -------------------------------------------------------------------------------- 1 | package response 2 | 3 | // GetScriptResponse 4 | // Will redirect to download url 5 | type GetScriptResponse struct { 6 | } 7 | -------------------------------------------------------------------------------- /app/response/submission.go: -------------------------------------------------------------------------------- 1 | package response 2 | 3 | import ( 4 | "github.com/EduOJ/backend/app/response/resource" 5 | ) 6 | 7 | type CreateSubmissionResponse struct { 8 | Message string `json:"message"` 9 | Error interface{} `json:"error"` 10 | Data struct { 11 | *resource.SubmissionDetail `json:"submission"` 12 | } `json:"data"` 13 | } 14 | 15 | type GetSubmissionResponse struct { 16 | Message string `json:"message"` 17 | Error interface{} `json:"error"` 18 | Data struct { 19 | *resource.SubmissionDetail `json:"submission"` 20 | } `json:"data"` 21 | } 22 | 23 | type GetSubmissionsResponse struct { 24 | Message string `json:"message"` 25 | Error interface{} `json:"error"` 26 | Data struct { 27 | Submissions []resource.Submission `json:"submissions"` 28 | Total int `json:"total"` 29 | Count int `json:"count"` 30 | Offset int `json:"offset"` 31 | Prev *string `json:"prev"` 32 | Next *string `json:"next"` 33 | } `json:"data"` 34 | } 35 | -------------------------------------------------------------------------------- /app/response/user.go: -------------------------------------------------------------------------------- 1 | package response 2 | 3 | import ( 4 | "github.com/EduOJ/backend/app/response/resource" 5 | ) 6 | 7 | type GetUserResponse struct { 8 | Message string `json:"message"` 9 | Error interface{} `json:"error"` 10 | Data struct { 11 | *resource.User `json:"user"` 12 | } `json:"data"` 13 | } 14 | 15 | type GetUsersResponse struct { 16 | Message string `json:"message"` 17 | Error interface{} `json:"error"` 18 | Data struct { 19 | Users []resource.User `json:"users"` 20 | Total int `json:"total"` 21 | Count int `json:"count"` 22 | Offset int `json:"offset"` 23 | Prev *string `json:"prev"` 24 | Next *string `json:"next"` 25 | } `json:"data"` 26 | } 27 | 28 | type GetMeResponse struct { 29 | Message string `json:"message"` 30 | Error interface{} `json:"error"` 31 | Data struct { 32 | *resource.UserForAdmin `json:"user"` 33 | } `json:"data"` 34 | } 35 | 36 | type UpdateMeResponse struct { 37 | Message string `json:"message"` 38 | Error interface{} `json:"error"` 39 | Data struct { 40 | *resource.UserForAdmin `json:"user"` 41 | } `json:"data"` 42 | } 43 | 44 | type GetClassesIManageResponse struct { 45 | Message string `json:"message"` 46 | Error interface{} `json:"error"` 47 | Data struct { 48 | Classes []resource.Class `json:"classes"` 49 | } `json:"data"` 50 | } 51 | 52 | type GetClassesITakeResponse struct { 53 | Message string `json:"message"` 54 | Error interface{} `json:"error"` 55 | Data struct { 56 | Classes []resource.Class `json:"classes"` 57 | } `json:"data"` 58 | } 59 | 60 | type GetUserProblemInfoResponse struct { 61 | Message string `json:"message"` 62 | Error interface{} `json:"error"` 63 | Data struct { 64 | TriedCount int `json:"tried_count"` 65 | PassedCount int `json:"passed_count"` 66 | Rank int `json:"rank"` // TODO: develop this 67 | } `json:"data"` 68 | } 69 | -------------------------------------------------------------------------------- /base/event/event.go: -------------------------------------------------------------------------------- 1 | package event 2 | 3 | import ( 4 | "reflect" 5 | "sync" 6 | 7 | "github.com/pkg/errors" 8 | ) 9 | 10 | var eventLock = sync.RWMutex{} 11 | var listeners = map[string][]interface{}{} 12 | 13 | // RegisterListener registers a listener with a event name. 14 | // The listener should be a function. 15 | func RegisterListener(eventName string, listener interface{}) { 16 | eventLock.Lock() 17 | defer eventLock.Unlock() 18 | listenerValue := reflect.ValueOf(listener) 19 | if listenerValue.Kind() != reflect.Func { 20 | panic("Trying to register a non-func listener!") 21 | } 22 | listeners[eventName] = append(listeners[eventName], listener) 23 | } 24 | 25 | // FireEvent Fires a given event name with args. 26 | // Args will be passed to all registered listeners. 27 | // Returns a slice of results, each result is a slice of interface {}, 28 | // representing the return value of each call. 29 | func FireEvent(eventName string, args ...interface{}) (result [][]interface{}, err error) { 30 | eventLock.RLock() 31 | defer eventLock.RUnlock() 32 | defer func() { 33 | if p := recover(); p != nil { 34 | result = nil 35 | err = errors.New(p.(string)) 36 | return 37 | } 38 | }() 39 | argsValue := make([]reflect.Value, len(args)) 40 | for i, arg := range args { 41 | argsValue[i] = reflect.ValueOf(arg) 42 | } 43 | result = make([][]interface{}, len(listeners[eventName])) 44 | for i, listener := range listeners[eventName] { 45 | listenerFunc := reflect.ValueOf(listener) 46 | rst := listenerFunc.Call(argsValue) 47 | result[i] = make([]interface{}, len(rst)) 48 | for j, v := range rst { 49 | result[i][j] = v.Interface() 50 | } 51 | } 52 | return 53 | } 54 | -------------------------------------------------------------------------------- /base/event/event_test.go: -------------------------------------------------------------------------------- 1 | package event 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "testing" 7 | "time" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestEvent(t *testing.T) { 13 | t.Cleanup(func() { 14 | listeners = make(map[string][]interface{}) 15 | }) 16 | tests := []struct { 17 | Listener interface{} 18 | Args []interface{} 19 | Results []interface{} 20 | }{ 21 | { 22 | Listener: func(a int, b string, c time.Time) (int, string, time.Time) { 23 | return a, b, c 24 | }, 25 | Args: append(make([]interface{}, 0), 1, "123", time.Date(2001, 2, 3, 4, 5, 6, 7, time.UTC)), 26 | Results: append(make([]interface{}, 0), 1, "123", time.Date(2001, 2, 3, 4, 5, 6, 7, time.UTC)), 27 | }, 28 | } 29 | for i, test := range tests { 30 | t.Run(fmt.Sprint("event_test_", i), func(t *testing.T) { 31 | t.Cleanup(func() { 32 | listeners = make(map[string][]interface{}) 33 | }) 34 | RegisterListener("event_test", test.Listener) 35 | result, err := FireEvent("event_test", test.Args...) 36 | if err != nil { 37 | t.Error("Errors when calling hook: ", err) 38 | } 39 | assert.Equal(t, len(result), 1, "Result length should be 1.") 40 | for i, v := range result[0] { 41 | vv := reflect.ValueOf(v) 42 | assert.Equal(t, vv.Type(), reflect.TypeOf(test.Results[i]), "Type of result should be same.") 43 | assert.Equal(t, v, test.Results[i], "Value of result should be same.") 44 | } 45 | }) 46 | } 47 | } 48 | 49 | func TestFireEvent(t *testing.T) { 50 | t.Cleanup(func() { 51 | listeners = make(map[string][]interface{}) 52 | }) 53 | RegisterListener("test_fire_event", func() int { 54 | return 123 55 | }) 56 | result, err := FireEvent("test_fire_event") 57 | assert.Equal(t, err, nil, "Should not have error.") 58 | assert.Equal(t, result[0][0], 123, "Should be the same.") 59 | 60 | RegisterListener("test_fire_event_1", func(int) int { 61 | return 123 62 | }) 63 | result, err = FireEvent("test_fire_event_1") 64 | assert.NotEqual(t, err, nil, "Should have error.") 65 | assert.Equal(t, err.Error(), "reflect: Call with too few input arguments", "Error should be too few arguments.") 66 | assert.Equal(t, result, [][]interface{}(nil), "Should not have result on error.") 67 | 68 | assert.PanicsWithValue(t, "Trying to register a non-func listener!", func() { 69 | RegisterListener("test_fire_event_2", 123) 70 | }, "Should panic on non-function listeners.") 71 | } 72 | -------------------------------------------------------------------------------- /base/exit/exit.go: -------------------------------------------------------------------------------- 1 | package exit 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | ) 7 | 8 | var BaseContext, Close = context.WithCancel(context.Background()) 9 | var QuitWG = sync.WaitGroup{} 10 | 11 | var testExitLock = sync.Mutex{} 12 | -------------------------------------------------------------------------------- /base/exit/test.go: -------------------------------------------------------------------------------- 1 | package exit 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | ) 7 | 8 | // noinspection GoVetCopyLock 9 | func SetupExitForTest() func() { 10 | testExitLock.Lock() 11 | oldWG := QuitWG 12 | oldClose := Close 13 | oldContext := BaseContext 14 | QuitWG = sync.WaitGroup{} 15 | BaseContext, Close = context.WithCancel(context.Background()) 16 | return func() { 17 | QuitWG = oldWG 18 | Close = oldClose 19 | BaseContext = oldContext 20 | testExitLock.Unlock() 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /base/log/echo_logger.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | . "os" 7 | 8 | "github.com/labstack/gommon/log" 9 | ) 10 | 11 | // EchoLogger is a fake logger for echo. 12 | type EchoLogger struct { 13 | prefix string 14 | } 15 | 16 | func (e *EchoLogger) Output() io.Writer { 17 | return Stdout 18 | } 19 | 20 | func (e *EchoLogger) SetOutput(w io.Writer) { 21 | // do nothing 22 | } 23 | 24 | func (e *EchoLogger) Prefix() string { 25 | return e.prefix 26 | } 27 | 28 | func (e *EchoLogger) SetPrefix(p string) { 29 | e.prefix = p 30 | } 31 | 32 | func (e *EchoLogger) Level() log.Lvl { 33 | return 0 34 | } 35 | 36 | func (e *EchoLogger) SetLevel(v log.Lvl) { 37 | // do nothing 38 | } 39 | 40 | func (e *EchoLogger) SetHeader(h string) { 41 | // do nothing 42 | } 43 | 44 | func (e *EchoLogger) Print(i ...interface{}) { 45 | Info(i...) 46 | } 47 | 48 | func (e *EchoLogger) Printf(format string, args ...interface{}) { 49 | Infof(format, args...) 50 | } 51 | 52 | func (e *EchoLogger) Printj(j log.JSON) { 53 | b, _ := json.Marshal(j) 54 | Info(string(b)) 55 | } 56 | 57 | func (e *EchoLogger) Debug(i ...interface{}) { 58 | Debug(i...) 59 | } 60 | 61 | func (e *EchoLogger) Debugf(format string, args ...interface{}) { 62 | Debugf(format, args...) 63 | } 64 | 65 | func (e *EchoLogger) Debugj(j log.JSON) { 66 | b, _ := json.Marshal(j) 67 | Debug(string(b)) 68 | } 69 | 70 | func (e *EchoLogger) Info(i ...interface{}) { 71 | Info(i...) 72 | } 73 | 74 | func (e *EchoLogger) Infof(format string, args ...interface{}) { 75 | Infof(format, args...) 76 | } 77 | 78 | func (e *EchoLogger) Infoj(j log.JSON) { 79 | b, _ := json.Marshal(j) 80 | Info(string(b)) 81 | } 82 | 83 | func (e *EchoLogger) Warn(i ...interface{}) { 84 | Warning(i...) 85 | } 86 | 87 | func (e *EchoLogger) Warnf(format string, args ...interface{}) { 88 | Warningf(format, args...) 89 | } 90 | 91 | func (e *EchoLogger) Warnj(j log.JSON) { 92 | b, _ := json.Marshal(j) 93 | Warning(string(b)) 94 | } 95 | 96 | func (e *EchoLogger) Error(i ...interface{}) { 97 | Error(i...) 98 | } 99 | 100 | func (e *EchoLogger) Errorf(format string, args ...interface{}) { 101 | Errorf(format, args...) 102 | } 103 | 104 | func (e *EchoLogger) Errorj(j log.JSON) { 105 | b, _ := json.Marshal(j) 106 | Error(string(b)) 107 | } 108 | 109 | func (e *EchoLogger) Fatal(i ...interface{}) { 110 | Fatal(i...) 111 | } 112 | 113 | func (e *EchoLogger) Fatalf(format string, args ...interface{}) { 114 | Fatalf(format, args...) 115 | } 116 | 117 | func (e *EchoLogger) Fatalj(j log.JSON) { 118 | b, _ := json.Marshal(j) 119 | Fatal(string(b)) 120 | } 121 | 122 | func (e *EchoLogger) Panic(i ...interface{}) { 123 | Fatal(i...) 124 | } 125 | 126 | func (e *EchoLogger) Panicf(format string, args ...interface{}) { 127 | Fatalf(format, args...) 128 | } 129 | 130 | func (e *EchoLogger) Panicj(j log.JSON) { 131 | b, _ := json.Marshal(j) 132 | Fatal(string(b)) 133 | } 134 | -------------------------------------------------------------------------------- /base/log/echo_logger_test.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "os" 5 | "reflect" 6 | "testing" 7 | 8 | "github.com/labstack/gommon/log" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestEchoLogger(t *testing.T) { 13 | oldLogger := logger0 14 | t.Cleanup(func() { 15 | logger0 = oldLogger 16 | }) 17 | fl := &fakeLogger{} 18 | logger0 = fl 19 | el := &EchoLogger{} 20 | assert.Equal(t, os.Stdout, el.Output()) 21 | el.SetOutput(os.Stdout) 22 | assert.Equal(t, "", el.Prefix()) 23 | el.SetPrefix("test_echo_logger") 24 | assert.Equal(t, "test_echo_logger", el.Prefix()) 25 | assert.Equal(t, log.Lvl(0), el.Level()) 26 | el.SetLevel(0) 27 | el.SetHeader("") 28 | 29 | tests := []struct { 30 | f interface{} 31 | string 32 | }{ 33 | {el.Print, "Info"}, 34 | {el.Printf, "Infof"}, 35 | {el.Printj, "Info"}, 36 | {el.Debug, "Debug"}, 37 | {el.Debugf, "Debugf"}, 38 | {el.Debugj, "Debug"}, 39 | {el.Info, "Info"}, 40 | {el.Infof, "Infof"}, 41 | {el.Infoj, "Info"}, 42 | {el.Warn, "Warning"}, 43 | {el.Warnf, "Warningf"}, 44 | {el.Warnj, "Warning"}, 45 | {el.Error, "Error"}, 46 | {el.Errorf, "Errorf"}, 47 | {el.Errorj, "Error"}, 48 | {el.Fatal, "Fatal"}, 49 | {el.Fatalf, "Fatalf"}, 50 | {el.Fatalj, "Fatal"}, 51 | {el.Panic, "Fatal"}, 52 | {el.Panicf, "Fatalf"}, 53 | {el.Panicj, "Fatal"}, 54 | } 55 | for _, test := range tests { 56 | t.Run("testEchoLogger"+test.string, func(t *testing.T) { 57 | ff := reflect.ValueOf(test.f) 58 | ty := reflect.TypeOf(test.f) 59 | ff.Call([]reflect.Value{ 60 | reflect.New(ty.In(0)).Elem(), 61 | }) 62 | assert.Equal(t, test.string, fl.lastFunctionCalled) 63 | }) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /base/log/gorm_logger.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "strings" 7 | "time" 8 | 9 | "github.com/spf13/viper" 10 | "gorm.io/gorm" 11 | logger2 "gorm.io/gorm/logger" 12 | ) 13 | 14 | // // Interface logger interface 15 | //type Interface interface { 16 | // LogMode(LogLevel) Interface 17 | // Info(context.Context, string, ...interface{}) 18 | // Warn(context.Context, string, ...interface{}) 19 | // Error(context.Context, string, ...interface{}) 20 | // Trace(ctx context.Context, begin time.Time, fc func() (string, int64), err error) 21 | //} 22 | 23 | type GormLogger struct{} 24 | 25 | func (GormLogger) LogMode(_ logger2.LogLevel) logger2.Interface { 26 | // do nothing 27 | // we use our own log level system. 28 | return GormLogger{} 29 | } 30 | 31 | func (GormLogger) Info(_ context.Context, msg string, param ...interface{}) { 32 | Infof(msg, param...) 33 | } 34 | 35 | func (GormLogger) Warn(_ context.Context, msg string, param ...interface{}) { 36 | Warningf(msg, param...) 37 | } 38 | 39 | func (GormLogger) Error(_ context.Context, msg string, param ...interface{}) { 40 | Errorf(msg, param...) 41 | } 42 | 43 | func (GormLogger) Trace(_ context.Context, begin time.Time, fc func() (string, int64), err error) { 44 | sql, rows := fc() 45 | if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { 46 | Errorf("%v, SQL: %s, rows: %v", err, sql, rows) 47 | } else if viper.GetBool("debug") && !strings.Contains(sql, "INSERT INTO \"logs\"") { 48 | Debugf("SQL: %s, rows: %v", sql, rows) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /base/log/log.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // Level for logs. 8 | type Level int 9 | 10 | /* 11 | Debug: debug information. 12 | Info: Running information. 13 | Warning: notable, but the process wont fail because of this. 14 | Error: a process (request) fails because of this. 15 | Fatal: multiple process(request) fails because of this. 16 | */ 17 | const ( 18 | DEBUG Level = iota 19 | INFO 20 | WARNING 21 | ERROR 22 | FATAL 23 | ) 24 | 25 | func (l Level) String() string { 26 | switch l { 27 | case DEBUG: 28 | return "DEBUG" 29 | case INFO: 30 | return "INFO" 31 | case WARNING: 32 | return "WARNING" 33 | case ERROR: 34 | return "ERROR" 35 | case FATAL: 36 | return "FATAL" 37 | default: 38 | // Shouldn't reach here. 39 | return "" 40 | } 41 | } 42 | 43 | var StringToLevel = map[string]Level{ 44 | "DEBUG": DEBUG, 45 | "INFO": INFO, 46 | "WARNING": WARNING, 47 | "ERROR": ERROR, 48 | "FATAL": FATAL, 49 | } 50 | 51 | // Log struct contains essential information of a log. 52 | type Log struct { 53 | Level Level `json:"level"` // The level of this log. 54 | Time time.Time `json:"time"` // The time of this log. 55 | Message string `json:"message"` // The message of this log. 56 | Caller string `json:"caller"` // The function produces this log. 57 | } 58 | -------------------------------------------------------------------------------- /base/log/log_test.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestLevel_String(t *testing.T) { 10 | levels := []struct { 11 | Level 12 | string 13 | }{ 14 | { 15 | DEBUG, 16 | "DEBUG", 17 | }, 18 | { 19 | INFO, 20 | "INFO", 21 | }, 22 | { 23 | WARNING, 24 | "WARNING", 25 | }, 26 | { 27 | ERROR, 28 | "ERROR", 29 | }, 30 | { 31 | FATAL, 32 | "FATAL", 33 | }, 34 | { 35 | 100, 36 | "", 37 | }, 38 | } 39 | for _, level := range levels { 40 | assert.Equal(t, level.Level.String(), level.string) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /base/log/logger.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "runtime" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | type _logger interface { 12 | addWriter(writer _writer) 13 | removeWriter(t reflect.Type) 14 | isReady() bool 15 | setReady() 16 | Disabled() bool 17 | Disable() 18 | Debug(items ...interface{}) 19 | Info(items ...interface{}) 20 | Warning(items ...interface{}) 21 | Error(items ...interface{}) 22 | Fatal(items ...interface{}) 23 | Debugf(format string, items ...interface{}) 24 | Infof(format string, items ...interface{}) 25 | Warningf(format string, items ...interface{}) 26 | Errorf(format string, items ...interface{}) 27 | Fatalf(format string, items ...interface{}) 28 | } 29 | 30 | type logger struct { 31 | writers []_writer //Writers. 32 | ready bool 33 | disabled bool 34 | } 35 | 36 | // Add a writer to the logger. 37 | func (l *logger) addWriter(writer _writer) { 38 | l.writers = append(l.writers, writer) 39 | } 40 | 41 | // Remove all writers of specific type. 42 | // Should not be used. All calls of this function 43 | // must provide a reason in it's comment. 44 | func (l *logger) removeWriter(t reflect.Type) { 45 | oldWriters := l.writers 46 | l.writers = make([]_writer, 0) 47 | for _, w := range oldWriters { 48 | if reflect.TypeOf(w) != t { 49 | l.writers = append(l.writers, w) 50 | } 51 | } 52 | } 53 | 54 | func (l *logger) isReady() bool { 55 | return l.ready 56 | } 57 | 58 | func (l *logger) setReady() { 59 | l.ready = true 60 | } 61 | 62 | func (l *logger) Disabled() bool { 63 | return l.disabled 64 | } 65 | 66 | func (l *logger) Disable() { 67 | l.disabled = true 68 | } 69 | 70 | func (l *logger) logWithLevelString(level Level, message string) { 71 | if l.disabled { 72 | return 73 | } 74 | caller := "unknown" 75 | { 76 | // Find caller out of the log package. 77 | pc := make([]uintptr, 20) 78 | runtime.Callers(1, pc) 79 | frames := runtime.CallersFrames(pc) 80 | more := true 81 | for more { 82 | var frame runtime.Frame 83 | frame, more = frames.Next() 84 | if !strings.HasPrefix(frame.Function, "github.com/EduOJ/backend/base/log") && 85 | !strings.HasPrefix(frame.Function, "gorm.io/gorm") { 86 | caller = fmt.Sprint(frame.Func.Name(), ":", frame.Line) 87 | break 88 | } 89 | } 90 | } 91 | log := Log{ 92 | Level: level, 93 | Time: time.Now(), 94 | Message: message, 95 | Caller: caller, 96 | } 97 | if !l.ready { 98 | // Logger don't been initialized yet. 99 | // So we should just output to stdout. 100 | (&consoleWriter{}).log(log) 101 | return 102 | } 103 | for _, w := range l.writers { 104 | w.log(log) 105 | } 106 | } 107 | 108 | func (l *logger) logWithLevel(level Level, items ...interface{}) { 109 | l.logWithLevelString(level, fmt.Sprint(items...)) 110 | } 111 | 112 | func (l *logger) logWithLevelF(level Level, format string, items ...interface{}) { 113 | l.logWithLevelString(level, fmt.Sprintf(format, items...)) 114 | } 115 | 116 | func (l *logger) Debug(items ...interface{}) { 117 | l.logWithLevel(DEBUG, items...) 118 | } 119 | 120 | func (l *logger) Info(items ...interface{}) { 121 | l.logWithLevel(INFO, items...) 122 | } 123 | 124 | func (l *logger) Warning(items ...interface{}) { 125 | l.logWithLevel(WARNING, items...) 126 | } 127 | 128 | func (l *logger) Error(items ...interface{}) { 129 | l.logWithLevel(ERROR, items...) 130 | } 131 | 132 | func (l *logger) Fatal(items ...interface{}) { 133 | l.logWithLevel(FATAL, items...) 134 | } 135 | 136 | func (l *logger) Debugf(format string, items ...interface{}) { 137 | l.logWithLevelF(DEBUG, format, items...) 138 | } 139 | 140 | func (l *logger) Infof(format string, items ...interface{}) { 141 | l.logWithLevelF(INFO, format, items...) 142 | } 143 | 144 | func (l *logger) Warningf(format string, items ...interface{}) { 145 | l.logWithLevelF(WARNING, format, items...) 146 | } 147 | 148 | func (l *logger) Errorf(format string, items ...interface{}) { 149 | l.logWithLevelF(ERROR, format, items...) 150 | } 151 | 152 | func (l *logger) Fatalf(format string, items ...interface{}) { 153 | l.logWithLevelF(FATAL, format, items...) 154 | } 155 | -------------------------------------------------------------------------------- /base/log/logger_test.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "testing" 7 | 8 | "github.com/kami-zh/go-capturer" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | type fakeWriter struct { 13 | lastLog Log 14 | } 15 | 16 | func (f *fakeWriter) init() error { 17 | return nil 18 | } 19 | func (f *fakeWriter) log(l Log) { 20 | f.lastLog = l 21 | } 22 | 23 | func TestLogger(t *testing.T) { 24 | var l _logger = &logger{} 25 | l.setReady() 26 | w := &fakeWriter{} 27 | l.addWriter(w) 28 | assert.Equal(t, w, l.(*logger).writers[0], "Writer should be fake writer.") 29 | 30 | levels := []struct { 31 | l Level 32 | logFunction func(items ...interface{}) 33 | logFFunction func(format string, items ...interface{}) 34 | }{ 35 | { 36 | DEBUG, 37 | l.Debug, 38 | l.Debugf, 39 | }, { 40 | INFO, 41 | l.Info, 42 | l.Infof, 43 | }, { 44 | WARNING, 45 | l.Warning, 46 | l.Warningf, 47 | }, { 48 | ERROR, 49 | l.Error, 50 | l.Errorf, 51 | }, { 52 | FATAL, 53 | l.Fatal, 54 | l.Fatalf, 55 | }, 56 | } 57 | for _, level := range levels { 58 | level.logFunction(123, "321") 59 | assert.Equal(t, w.lastLog.Level, level.l, "Level should be same.") 60 | assert.Equal(t, w.lastLog.Message, fmt.Sprint(123, "321")) 61 | 62 | level.logFFunction("%d 123 %s", 123, "321") 63 | assert.Equal(t, w.lastLog.Level, level.l, "Level should be same.") 64 | assert.Equal(t, w.lastLog.Message, fmt.Sprintf("%d 123 %s", 123, "321")) 65 | } 66 | } 67 | 68 | func TestRemoveLogger(t *testing.T) { 69 | var l _logger = &logger{} 70 | l.setReady() 71 | w1 := &fakeWriter{} 72 | w2 := &fakeWriter{} 73 | w3 := &consoleWriter{} 74 | l.addWriter(w1) 75 | l.addWriter(w2) 76 | l.addWriter(w3) 77 | assert.Equal(t, w1, l.(*logger).writers[0], "Writer should be fake writer.") 78 | assert.Equal(t, w2, l.(*logger).writers[1], "Writer should be fake writer.") 79 | assert.Equal(t, w3, l.(*logger).writers[2], "Writer should be fake writer.") 80 | l.removeWriter(reflect.TypeOf((*fakeWriter)(nil))) 81 | assert.Equal(t, l.(*logger).writers, []_writer{w3}, "Should not have any writers here.") 82 | } 83 | 84 | func TestLogWithLevelString(t *testing.T) { 85 | var l logger 86 | w := &fakeWriter{} 87 | l.addWriter(w) 88 | l.setReady() 89 | levels := []Level{ 90 | DEBUG, 91 | INFO, 92 | WARNING, 93 | ERROR, 94 | FATAL, 95 | } 96 | for _, level := range levels { 97 | l.logWithLevelString(level, "test") 98 | assert.Equal(t, w.lastLog.Level, level) 99 | assert.Equal(t, w.lastLog.Message, "test") 100 | assert.Regexp(t, "^testing\\.tRunner:[0-9]+$", w.lastLog.Caller) 101 | } 102 | } 103 | 104 | func TestLogNotReady(t *testing.T) { 105 | var l _logger = &logger{} 106 | 107 | levels := []struct { 108 | Level 109 | logFunction func(items ...interface{}) 110 | logFFunction func(format string, items ...interface{}) 111 | }{ 112 | { 113 | DEBUG, 114 | l.Debug, 115 | l.Debugf, 116 | }, { 117 | INFO, 118 | l.Info, 119 | l.Infof, 120 | }, { 121 | WARNING, 122 | l.Warning, 123 | l.Warningf, 124 | }, { 125 | ERROR, 126 | l.Error, 127 | l.Errorf, 128 | }, { 129 | FATAL, 130 | l.Fatal, 131 | l.Fatalf, 132 | }, 133 | } 134 | for _, level := range levels { 135 | output := capturer.CaptureOutput(func() { 136 | level.logFunction("sample log output") 137 | }) 138 | txt := fmt.Sprintf("[%s] ▶ %s %s\n", 139 | "github.com/kami-zh/go-capturer.(*Capturer).capture:55", 140 | level.Level.String(), 141 | "sample log output") 142 | assert.Equal(t, txt, output[10:]) 143 | output = capturer.CaptureOutput(func() { 144 | level.logFFunction("sample log output") 145 | }) 146 | txt = fmt.Sprintf("[%s] ▶ %s %s\n", 147 | "github.com/kami-zh/go-capturer.(*Capturer).capture:55", 148 | level.Level.String(), 149 | "sample log output") 150 | assert.Equal(t, txt, output[10:]) 151 | } 152 | } 153 | 154 | func TestIsReady(t *testing.T) { 155 | var l _logger = &logger{} 156 | assert.Equal(t, false, l.isReady()) 157 | l.setReady() 158 | assert.Equal(t, true, l.isReady()) 159 | } 160 | 161 | func TestLogger_Disable(t *testing.T) { 162 | var l = &logger{} 163 | assert.Equal(t, false, l.disabled) 164 | 165 | output := capturer.CaptureOutput(func() { 166 | l.Debug("test") 167 | }) 168 | assert.NotEqual(t, "", output) 169 | 170 | l.Disable() 171 | assert.Equal(t, true, l.Disabled()) 172 | output = capturer.CaptureOutput(func() { 173 | l.Debug("test") 174 | }) 175 | assert.Equal(t, "", output) 176 | } 177 | -------------------------------------------------------------------------------- /base/log/logging.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/pkg/errors" 7 | "github.com/spf13/viper" 8 | ) 9 | 10 | var logger0 _logger = &logger{} 11 | 12 | type logConf []struct { 13 | Name string 14 | Level string 15 | } 16 | 17 | func InitFromConfig() (err error) { 18 | if logger0.isReady() { 19 | return errors.New("already initialized") 20 | } 21 | var confSlice logConf 22 | err = viper.UnmarshalKey("log", &confSlice) 23 | if err != nil { 24 | return errors.Wrap(err, "Wrong log conf") 25 | } 26 | for _, c := range confSlice { 27 | switch c.Name { 28 | case "console": 29 | logger0.addWriter(&consoleWriter{ 30 | Level: StringToLevel[strings.ToUpper(c.Level)], 31 | }) 32 | case "database": 33 | w := &databaseWriter{ 34 | Level: StringToLevel[strings.ToUpper(c.Level)], 35 | } 36 | w.init() 37 | logger0.addWriter(w) 38 | case "event": 39 | // nothing to do. 40 | default: 41 | return errors.New("invalid writer name") 42 | } 43 | } 44 | logger0.addWriter(&eventWriter{}) 45 | logger0.setReady() 46 | return nil 47 | } 48 | 49 | func Debug(items ...interface{}) { 50 | logger0.Debug(items...) 51 | } 52 | 53 | func Info(items ...interface{}) { 54 | logger0.Info(items...) 55 | } 56 | 57 | func Warning(items ...interface{}) { 58 | logger0.Warning(items...) 59 | } 60 | 61 | func Error(items ...interface{}) { 62 | logger0.Error(items...) 63 | } 64 | 65 | func Fatal(items ...interface{}) { 66 | logger0.Fatal(items...) 67 | } 68 | 69 | func Debugf(format string, items ...interface{}) { 70 | logger0.Debugf(format, items...) 71 | } 72 | 73 | func Infof(format string, items ...interface{}) { 74 | logger0.Infof(format, items...) 75 | } 76 | 77 | func Warningf(format string, items ...interface{}) { 78 | logger0.Warningf(format, items...) 79 | } 80 | 81 | func Errorf(format string, items ...interface{}) { 82 | logger0.Errorf(format, items...) 83 | } 84 | 85 | func Fatalf(format string, items ...interface{}) { 86 | logger0.Fatalf(format, items...) 87 | } 88 | 89 | func Disable() { 90 | logger0.Disable() 91 | } 92 | 93 | func Disabled() bool { 94 | return logger0.Disabled() 95 | } 96 | -------------------------------------------------------------------------------- /base/log/logging_test.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "reflect" 8 | "testing" 9 | 10 | "github.com/EduOJ/backend/base/exit" 11 | "github.com/EduOJ/backend/database" 12 | "github.com/kami-zh/go-capturer" 13 | "github.com/spf13/viper" 14 | "github.com/stretchr/testify/assert" 15 | ) 16 | 17 | type fakeLogger struct { 18 | ready bool 19 | disabled bool 20 | lastFunctionCalled string 21 | } 22 | 23 | func (f *fakeLogger) addWriter(writer _writer) {} 24 | 25 | func (f *fakeLogger) removeWriter(t reflect.Type) {} 26 | 27 | func (f *fakeLogger) isReady() bool { 28 | return f.ready 29 | } 30 | 31 | func (f *fakeLogger) setReady() { 32 | f.ready = true 33 | } 34 | func (f *fakeLogger) Disabled() bool { 35 | return f.disabled 36 | } 37 | 38 | func (f *fakeLogger) Disable() { 39 | f.disabled = true 40 | } 41 | 42 | func (f *fakeLogger) Debug(items ...interface{}) { 43 | f.lastFunctionCalled = "Debug" 44 | } 45 | 46 | func (f *fakeLogger) Info(items ...interface{}) { 47 | f.lastFunctionCalled = "Info" 48 | } 49 | 50 | func (f *fakeLogger) Warning(items ...interface{}) { 51 | f.lastFunctionCalled = "Warning" 52 | } 53 | 54 | func (f *fakeLogger) Error(items ...interface{}) { 55 | f.lastFunctionCalled = "Error" 56 | } 57 | 58 | func (f *fakeLogger) Fatal(items ...interface{}) { 59 | f.lastFunctionCalled = "Fatal" 60 | } 61 | 62 | func (f *fakeLogger) Debugf(format string, items ...interface{}) { 63 | f.lastFunctionCalled = "Debugf" 64 | } 65 | 66 | func (f *fakeLogger) Infof(format string, items ...interface{}) { 67 | f.lastFunctionCalled = "Infof" 68 | } 69 | 70 | func (f *fakeLogger) Warningf(format string, items ...interface{}) { 71 | f.lastFunctionCalled = "Warningf" 72 | } 73 | 74 | func (f *fakeLogger) Errorf(format string, items ...interface{}) { 75 | f.lastFunctionCalled = "Errorf" 76 | } 77 | 78 | func (f *fakeLogger) Fatalf(format string, items ...interface{}) { 79 | f.lastFunctionCalled = "Fatalf" 80 | } 81 | 82 | func TestLogFunctions(t *testing.T) { 83 | oldLogger := logger0 84 | t.Cleanup(func() { 85 | logger0 = oldLogger 86 | }) 87 | f := &fakeLogger{} 88 | logger0 = f 89 | tests := []struct { 90 | function interface{} 91 | name string 92 | }{ 93 | { 94 | Debug, 95 | "Debug", 96 | }, 97 | { 98 | Info, 99 | "Info", 100 | }, 101 | { 102 | Warning, 103 | "Warning", 104 | }, 105 | { 106 | Error, 107 | "Error", 108 | }, 109 | { 110 | Fatal, 111 | "Fatal", 112 | }, 113 | { 114 | Debugf, 115 | "Debugf", 116 | }, 117 | { 118 | Infof, 119 | "Infof", 120 | }, 121 | { 122 | Warningf, 123 | "Warningf", 124 | }, 125 | { 126 | Errorf, 127 | "Errorf", 128 | }, 129 | { 130 | Fatalf, 131 | "Fatalf", 132 | }, 133 | } 134 | for _, test := range tests { 135 | t.Run("testLogFunction"+test.name, func(t *testing.T) { 136 | if _, ok := test.function.(func(...interface{})); ok { 137 | test.function.(func(...interface{}))() 138 | } else { 139 | test.function.(func(string, ...interface{}))("") 140 | } 141 | assert.Equal(t, test.name, f.lastFunctionCalled) 142 | }) 143 | } 144 | } 145 | 146 | func TestInitFromConfigFail(t *testing.T) { 147 | oldLogger := logger0 148 | t.Cleanup(func() { 149 | logger0 = oldLogger 150 | }) 151 | tests := []struct { 152 | s string 153 | error 154 | }{ 155 | { 156 | `{ 157 | "log": [{ 158 | "name": "blah", 159 | "level": "blah" 160 | }] 161 | }`, 162 | errors.New("invalid writer name"), 163 | }, 164 | } 165 | for i, test := range tests { 166 | t.Run(fmt.Sprint("testInit_", i), func(t *testing.T) { 167 | l := &logger{} 168 | logger0 = l 169 | viper.SetConfigType("json") 170 | err := viper.ReadConfig(bytes.NewBufferString(test.s)) 171 | assert.NoError(t, err) 172 | err = InitFromConfig() 173 | if test.error != nil && err != nil { 174 | assert.Equal(t, test.error.Error(), err.Error()) 175 | } else { 176 | assert.Equal(t, test.error, err) 177 | } 178 | }) 179 | } 180 | } 181 | 182 | func TestInitFromConfigSuccess(t *testing.T) { 183 | t.Cleanup(database.SetupDatabaseForTest()) 184 | t.Cleanup(exit.SetupExitForTest()) 185 | oldLogger := logger0 186 | t.Cleanup(func() { 187 | logger0 = oldLogger 188 | }) 189 | l := &logger{} 190 | logger0 = l 191 | assert.Equal(t, false, l.ready) 192 | viper.SetConfigType("yaml") 193 | assert.NoError(t, viper.ReadConfig(bytes.NewBufferString(` 194 | log: 195 | - name: console 196 | level: debug 197 | - name: database 198 | level: debug 199 | `))) 200 | err := InitFromConfig() 201 | assert.NoError(t, err) 202 | assert.Equal(t, true, l.ready) 203 | err = InitFromConfig() 204 | assert.EqualError(t, err, "already initialized") 205 | exit.Close() 206 | exit.QuitWG.Wait() 207 | } 208 | 209 | func TestLogging_Disable(t *testing.T) { 210 | oldLogger := logger0 211 | t.Cleanup(func() { 212 | logger0 = oldLogger 213 | }) 214 | l := &logger{} 215 | logger0 = l 216 | assert.Equal(t, false, l.disabled) 217 | 218 | output := capturer.CaptureOutput(func() { 219 | l.Debug("test") 220 | }) 221 | assert.NotEqual(t, "", output) 222 | 223 | Disable() 224 | assert.Equal(t, true, Disabled()) 225 | output = capturer.CaptureOutput(func() { 226 | l.Debug("test") 227 | }) 228 | assert.Equal(t, "", output) 229 | } 230 | -------------------------------------------------------------------------------- /base/log/writer.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/EduOJ/backend/database/models/log" 8 | "github.com/fatih/color" 9 | 10 | "github.com/EduOJ/backend/base" 11 | "github.com/EduOJ/backend/base/event" 12 | "github.com/EduOJ/backend/base/exit" 13 | ) 14 | 15 | // Writers should only be used in this package. 16 | // Other packages should use listeners to receive logs. 17 | type _writer interface { 18 | log(log Log) 19 | } 20 | 21 | // Writes to the console. 22 | type consoleWriter struct { 23 | Level 24 | } 25 | 26 | // Writes to the database for reading from web. 27 | type databaseWriter struct { 28 | Level 29 | queue chan Log 30 | } 31 | 32 | // Calling log listeners. 33 | type eventWriter struct{} 34 | 35 | var ( 36 | colors = []func(format string, a ...interface{}) string{ 37 | FATAL: color.MagentaString, 38 | ERROR: color.RedString, 39 | WARNING: color.YellowString, 40 | INFO: color.GreenString, 41 | DEBUG: color.CyanString, 42 | } 43 | ) 44 | 45 | func (w *consoleWriter) log(l Log) { 46 | if l.Level >= w.Level { 47 | fmt.Print(colors[l.Level]("[%s][%s] ▶ %s ", 48 | l.Time.Format("15:04:05"), 49 | strings.Replace(l.Caller, "github.com/EduOJ/backend/", "", -1), 50 | l.Level.String()), 51 | l.Message, 52 | "\n") 53 | } 54 | } 55 | 56 | func (w *databaseWriter) log(l Log) { 57 | // avoid blocking the main thread. 58 | if l.Level >= w.Level && !strings.HasPrefix(l.Caller, "gorm.io/gorm") { 59 | select { 60 | case w.queue <- l: 61 | default: 62 | } 63 | } 64 | } 65 | 66 | func (w *databaseWriter) init() { 67 | w.queue = make(chan Log, 100) 68 | exit.QuitWG.Add(1) 69 | go func() { 70 | for { 71 | select { 72 | case l := <-w.queue: 73 | ll := int(l.Level) 74 | lm := log.Log{ 75 | Level: &ll, 76 | Message: l.Message, 77 | Caller: l.Caller, 78 | CreatedAt: l.Time, 79 | } 80 | base.DB.Create(&lm) 81 | case <-exit.BaseContext.Done(): 82 | select { 83 | case l := <-w.queue: 84 | ll := int(l.Level) 85 | lm := log.Log{ 86 | Level: &ll, 87 | Message: l.Message, 88 | Caller: l.Caller, 89 | CreatedAt: l.Time, 90 | } 91 | base.DB.Create(&lm) 92 | default: 93 | exit.QuitWG.Done() 94 | return 95 | } 96 | } 97 | } 98 | }() 99 | } 100 | 101 | func (w *eventWriter) log(l Log) { 102 | go (func() { 103 | if _, err := event.FireEvent("log", l); err != nil { 104 | panic(err) 105 | } 106 | })() 107 | } 108 | -------------------------------------------------------------------------------- /base/log/writer_test.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/EduOJ/backend/base" 8 | "github.com/EduOJ/backend/base/event" 9 | "github.com/EduOJ/backend/base/exit" 10 | "github.com/EduOJ/backend/database" 11 | log2 "github.com/EduOJ/backend/database/models/log" 12 | "github.com/kami-zh/go-capturer" 13 | "github.com/stretchr/testify/assert" 14 | ) 15 | 16 | func TestConsoleWriter(t *testing.T) { 17 | levels := []struct { 18 | Level 19 | }{ 20 | { 21 | DEBUG, 22 | }, 23 | { 24 | INFO, 25 | }, 26 | { 27 | WARNING, 28 | }, 29 | { 30 | ERROR, 31 | }, 32 | { 33 | FATAL, 34 | }, 35 | } 36 | w := &consoleWriter{} 37 | ti := time.Now() 38 | for _, level := range levels { 39 | out := capturer.CaptureOutput(func() { 40 | w.log(Log{ 41 | Level: level.Level, 42 | Time: ti, 43 | Message: "sample log output", 44 | Caller: "main.main.func", 45 | }) 46 | }) 47 | txt := colors[level.Level]("[%s][%s] ▶ %s ", ti.Format("15:04:05"), "main.main.func", level.Level.String()) + "sample log output\n" 48 | assert.Equal(t, txt, out) 49 | } 50 | } 51 | 52 | func TestEventWriter(t *testing.T) { 53 | lastLog := Log{} 54 | done := make(chan struct{}) 55 | event.RegisterListener("log", func(arg Log) { 56 | lastLog = arg 57 | done <- struct{}{} 58 | }) 59 | w := &eventWriter{} 60 | log := Log{ 61 | Level: DEBUG, 62 | Time: time.Now(), 63 | Message: "123", 64 | Caller: "233", 65 | } 66 | w.log(log) 67 | <-done 68 | assert.Equal(t, log, lastLog) 69 | } 70 | 71 | // This test contains a data race on exit's base context. 72 | // So this file isn't included in the race test. 73 | // This race won't happen in real situation. 74 | // Cause the exit lock shouldn't be replaced out of test. 75 | func TestDatabaseWriter(t *testing.T) { 76 | t.Cleanup(database.SetupDatabaseForTest()) 77 | t.Cleanup(exit.SetupExitForTest()) 78 | log := Log{ 79 | Level: DEBUG, 80 | Time: time.Now(), 81 | Message: "123", 82 | Caller: "233", 83 | } 84 | w := databaseWriter{} 85 | w.queue = make(chan Log, 100) 86 | for i := 0; i < 1000; i += 1 { 87 | w.log(log) 88 | } 89 | assert.Equal(t, 100, len(w.queue)) 90 | w.init() 91 | for i := 0; i < 1000; i += 1 { 92 | w.log(log) 93 | } 94 | exit.Close() 95 | exit.QuitWG.Wait() 96 | lm := log2.Log{} 97 | count := int64(-1) 98 | assert.NoError(t, base.DB.Table("logs").Count(&count).Error) 99 | assert.LessOrEqual(t, int64(100), count) 100 | 101 | assert.NoError(t, base.DB.First(&lm).Error) 102 | ll := int(DEBUG) 103 | assert.Equal(t, log2.Log{ 104 | ID: lm.ID, 105 | Level: &ll, 106 | Message: "123", 107 | Caller: "233", 108 | CreatedAt: lm.CreatedAt, 109 | }, lm) 110 | } 111 | -------------------------------------------------------------------------------- /base/main.go: -------------------------------------------------------------------------------- 1 | package base 2 | 3 | import ( 4 | "html/template" 5 | 6 | "github.com/go-mail/mail" 7 | "github.com/go-redis/redis/v8" 8 | "github.com/go-webauthn/webauthn/webauthn" 9 | "github.com/labstack/echo/v4" 10 | "github.com/minio/minio-go/v7" 11 | "gorm.io/gorm" 12 | ) 13 | 14 | var Echo *echo.Echo 15 | var Redis *redis.Client 16 | var DB *gorm.DB 17 | var Storage *minio.Client 18 | var WebAuthn *webauthn.WebAuthn 19 | var Mail mail.Dialer 20 | var Template *template.Template 21 | -------------------------------------------------------------------------------- /base/procedure/procedure.go: -------------------------------------------------------------------------------- 1 | package procedure 2 | 3 | import ( 4 | "reflect" 5 | 6 | "github.com/pkg/errors" 7 | ) 8 | 9 | var handlers = map[string]interface{}{} 10 | 11 | // RegisterProcedure registers a procedure handlers with given name. 12 | // The handler should be a function. 13 | func RegisterProcedure(procedureName string, handler interface{}) { 14 | listenerValue := reflect.ValueOf(handler) 15 | if listenerValue.Kind() != reflect.Func { 16 | panic("Trying to register a non-func handler!") 17 | } 18 | if _, ok := handlers[procedureName]; ok { 19 | panic("Trying to re-register a handler!") 20 | } 21 | handlers[procedureName] = handler 22 | } 23 | 24 | // CallProcedure calls the procedure with given name, providing the procedure with given args and returns what the procedure returns as return value. 25 | func CallProcedure(procedureName string, args ...interface{}) (result []interface{}, err error) { 26 | defer func() { 27 | if p := recover(); p != nil { 28 | result = nil 29 | err = errors.New(p.(string)) 30 | return 31 | } 32 | }() 33 | argsValue := make([]reflect.Value, len(args)) 34 | for i, arg := range args { 35 | argsValue[i] = reflect.ValueOf(arg) 36 | } 37 | handlerFunc := reflect.ValueOf(handlers[procedureName]) 38 | rst := handlerFunc.Call(argsValue) 39 | result = make([]interface{}, len(rst)) 40 | for i, v := range rst { 41 | result[i] = v.Interface() 42 | } 43 | return 44 | } 45 | -------------------------------------------------------------------------------- /base/procedure/procedure_test.go: -------------------------------------------------------------------------------- 1 | package procedure 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "testing" 7 | "time" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestRegisterProcedure(t *testing.T) { 13 | t.Cleanup(func() { 14 | handlers = make(map[string]interface{}) 15 | }) 16 | tests := []struct { 17 | handler interface{} 18 | Args []interface{} 19 | Results []interface{} 20 | }{ 21 | { 22 | handler: func(a int, b string, c time.Time) (int, string, time.Time) { 23 | return a, b, c 24 | }, 25 | Args: append(make([]interface{}, 0), 1, "123", time.Date(2001, 2, 3, 4, 5, 6, 7, time.UTC)), 26 | Results: append(make([]interface{}, 0), 1, "123", time.Date(2001, 2, 3, 4, 5, 6, 7, time.UTC)), 27 | }, 28 | } 29 | for i, test := range tests { 30 | t.Run(fmt.Sprint("procedure_test_", i), func(t *testing.T) { 31 | t.Cleanup(func() { 32 | handlers = make(map[string]interface{}) 33 | }) 34 | RegisterProcedure("procedure_test", test.handler) 35 | result, err := CallProcedure("procedure_test", test.Args...) 36 | if err != nil { 37 | t.Error("Errors when calling procedure: ", err) 38 | } 39 | for i, v := range result { 40 | vv := reflect.ValueOf(v) 41 | assert.Equal(t, vv.Type(), reflect.TypeOf(test.Results[i]), "Type of result should be same.") 42 | assert.Equal(t, v, test.Results[i], "Value of result should be same.") 43 | } 44 | }) 45 | } 46 | } 47 | 48 | func TestCallProcedure(t *testing.T) { 49 | t.Cleanup(func() { 50 | handlers = make(map[string]interface{}) 51 | }) 52 | RegisterProcedure("test_call_procedure", func() int { 53 | return 123 54 | }) 55 | result, err := CallProcedure("test_call_procedure") 56 | assert.Equal(t, err, nil, "Should not have error.") 57 | assert.Equal(t, result[0], 123, "Should be the same.") 58 | 59 | RegisterProcedure("test_call_procedure_1", func(int) int { 60 | return 123 61 | }) 62 | result, err = CallProcedure("test_call_procedure_1") 63 | assert.NotEqual(t, err, nil, "Should have error.") 64 | assert.Equal(t, err.Error(), "reflect: Call with too few input arguments", "Error should be too few arguments.") 65 | assert.Equal(t, result, []interface{}(nil), "Should not have result on error.") 66 | 67 | assert.PanicsWithValue(t, "Trying to register a non-func handler!", func() { 68 | RegisterProcedure("test_call_procedure_2", 123) 69 | }, "Should panic on non-function handlers.") 70 | assert.Panics(t, func() { 71 | RegisterProcedure("test_call_procedure_1", func(int) int { 72 | return 123 73 | }) 74 | }) 75 | } 76 | -------------------------------------------------------------------------------- /base/utils/auth.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | 7 | "github.com/EduOJ/backend/base" 8 | "github.com/EduOJ/backend/database/models" 9 | "github.com/pkg/errors" 10 | "github.com/spf13/viper" 11 | ) 12 | 13 | var initAuth sync.Once 14 | var SessionTimeout time.Duration 15 | var RememberMeTimeout time.Duration 16 | var SessionCount int 17 | 18 | func init() { 19 | viper.SetDefault("auth.session_timeout", 1200) 20 | viper.SetDefault("auth.remember_me_timeout", 604800) 21 | viper.SetDefault("auth.session_count", 10) 22 | } 23 | 24 | func initAuthConfig() { 25 | SessionTimeout = time.Second * viper.GetDuration("auth.session_timeout") 26 | RememberMeTimeout = time.Second * viper.GetDuration("auth.remember_me_timeout") 27 | SessionCount = viper.GetInt("auth.session_timeout") 28 | } 29 | 30 | func IsTokenExpired(token models.Token) bool { 31 | initAuth.Do(initAuthConfig) 32 | if token.RememberMe { 33 | return token.UpdatedAt.Add(RememberMeTimeout).Before(time.Now()) 34 | } else { 35 | return token.UpdatedAt.Add(SessionTimeout).Before(time.Now()) 36 | } 37 | } 38 | 39 | // TODO: Use this function in timed tasks 40 | func CleanUpExpiredTokens() error { 41 | initAuth.Do(initAuthConfig) 42 | var users []models.User 43 | err := base.DB.Model(models.User{}).Find(&users).Error 44 | if err != nil { 45 | return errors.Wrap(err, "could not find users") 46 | } 47 | for _, user := range users { 48 | var tokens []models.Token 49 | var tokenIds []uint 50 | storedTokenCount := 0 51 | err = base.DB.Where("user_id = ?", user.ID).Order("updated_at desc").Find(&tokens).Error 52 | // TODO: select updated_at > xxx limit 5; delete not in 53 | if err != nil { 54 | return errors.Wrap(err, "could not find tokens") 55 | } 56 | for _, token := range tokens { 57 | if storedTokenCount < SessionCount && !IsTokenExpired(token) { 58 | storedTokenCount++ 59 | continue 60 | } 61 | tokenIds = append(tokenIds, token.ID) 62 | } 63 | err = base.DB.Delete(&models.Token{}, "id in (?)", tokenIds).Error 64 | if err != nil { 65 | return errors.Wrap(err, "could not delete tokens") 66 | } 67 | } 68 | return nil 69 | } 70 | -------------------------------------------------------------------------------- /base/utils/class.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/EduOJ/backend/base" 7 | "github.com/EduOJ/backend/database/models" 8 | ) 9 | 10 | var inviteCodeLock sync.Mutex 11 | 12 | func GenerateInviteCode() (code string) { 13 | inviteCodeLock.Lock() 14 | defer inviteCodeLock.Unlock() 15 | var count int64 = 1 16 | for count > 0 { 17 | // 5: Fixed invite code length 18 | code = RandStr(5) 19 | PanicIfDBError(base.DB.Model(models.Class{}).Where("invite_code = ?", code).Count(&count), 20 | "could not check if invite code crashed for generating invite code") 21 | } 22 | return 23 | } 24 | -------------------------------------------------------------------------------- /base/utils/class_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "testing" 7 | 8 | "github.com/EduOJ/backend/base" 9 | "github.com/EduOJ/backend/database/models" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func checkInviteCode(t *testing.T, code string) { 14 | assert.Regexp(t, regexp.MustCompile("^[a-zA-Z2-9]{5}$"), code) 15 | var count int64 16 | assert.NoError(t, base.DB.Model(models.Class{}).Where("invite_code = ?", code).Count(&count).Error) 17 | assert.Equal(t, int64(1), count) 18 | } 19 | 20 | func createClassForTest(t *testing.T, name string, id int, managers, students []*models.User) models.Class { 21 | inviteCode := GenerateInviteCode() 22 | class := models.Class{ 23 | Name: fmt.Sprintf("test_%s_%d_name", name, id), 24 | CourseName: fmt.Sprintf("test_%s_%d_course_name", name, id), 25 | Description: fmt.Sprintf("test_%s_%d_description", name, id), 26 | InviteCode: inviteCode, 27 | Managers: managers, 28 | Students: students, 29 | } 30 | assert.NoError(t, base.DB.Create(&class).Error) 31 | return class 32 | } 33 | 34 | func TestGenerateInviteCode(t *testing.T) { 35 | t.Parallel() 36 | class := createClassForTest(t, "test_generate_invite_code_success", 0, nil, nil) 37 | checkInviteCode(t, class.InviteCode) 38 | } 39 | -------------------------------------------------------------------------------- /base/utils/email.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "github.com/EduOJ/backend/base" 5 | "github.com/go-mail/mail" 6 | "github.com/spf13/viper" 7 | ) 8 | 9 | type DialSender interface { 10 | DialAndSend(m ...*mail.Message) error 11 | } 12 | 13 | var sender = (DialSender)(&base.Mail) 14 | 15 | type fakeSender struct { 16 | messages []*mail.Message 17 | } 18 | 19 | func (f *fakeSender) DialAndSend(m ...*mail.Message) error { 20 | f.messages = append(f.messages, m...) 21 | return nil 22 | } 23 | 24 | func SetTest() { 25 | sender = &fakeSender{} 26 | } 27 | 28 | func GetTestMessages() []*mail.Message { 29 | return sender.(*fakeSender).messages 30 | } 31 | 32 | func SendMail(address string, subject string, message string) error { 33 | 34 | m := mail.NewMessage() 35 | m.SetHeader("From", viper.GetString("email.from")) 36 | m.SetHeader("To", address) 37 | m.SetHeader("Subject", subject) 38 | m.SetBody("text/html", message) 39 | 40 | return sender.DialAndSend(m) 41 | } 42 | -------------------------------------------------------------------------------- /base/utils/email_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "bytes" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestSendMail(t *testing.T) { 12 | t.Parallel() 13 | SetTest() 14 | err := SendMail("a.com", "123", "123") 15 | assert.NoError(t, err) 16 | message := GetTestMessages()[0] 17 | assert.Equal(t, []string{""}, message.GetHeader("From")) 18 | assert.Equal(t, []string{"a.com"}, message.GetHeader("To")) 19 | x := bytes.Buffer{} 20 | _, err = message.WriteTo(&x) 21 | assert.NoError(t, err) 22 | messageRead := strings.SplitN(x.String(), "\r\n\r\n", 2)[1] 23 | assert.Equal(t, "123", messageRead) 24 | } 25 | -------------------------------------------------------------------------------- /base/utils/helpers.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | "io" 7 | "mime/multipart" 8 | "net/http" 9 | "os" 10 | "strings" 11 | 12 | "github.com/EduOJ/backend/app/response" 13 | "github.com/EduOJ/backend/base" 14 | "github.com/EduOJ/backend/base/log" 15 | validator2 "github.com/EduOJ/backend/base/validator" 16 | "github.com/go-playground/validator/v10" 17 | "github.com/labstack/echo/v4" 18 | "github.com/minio/minio-go/v7" 19 | "github.com/pkg/errors" 20 | "gorm.io/gorm" 21 | ) 22 | 23 | func PanicIfDBError(db *gorm.DB, message string) { 24 | if db.Error != nil { 25 | panic(errors.Wrap(db.Error, message)) 26 | } 27 | } 28 | 29 | func BindAndValidate(req interface{}, c echo.Context) (err error, ok bool) { 30 | // TODO: return a HttpError instead of writing to response. 31 | if err := c.Bind(req); err != nil { 32 | return c.JSON(http.StatusBadRequest, response.ErrorResp("BAD_REQUEST_PARAMETER", nil)), false 33 | } 34 | if err := c.Validate(req); err != nil { 35 | if e, ok := err.(validator.ValidationErrors); ok { 36 | validationErrors := make([]response.ValidationError, len(e)) 37 | for i, v := range e { 38 | validationErrors[i] = response.ValidationError{ 39 | Field: v.Field(), 40 | Reason: v.Tag(), 41 | Translation: v.Translate(validator2.Trans), 42 | } 43 | } 44 | return c.JSON(http.StatusBadRequest, response.ErrorResp("VALIDATION_ERROR", validationErrors)), false 45 | } 46 | log.Error(errors.Wrap(err, "validate failed"), c) 47 | return response.InternalErrorResp(c), false 48 | } 49 | return nil, true 50 | } 51 | 52 | func MustPutObject(object *multipart.FileHeader, ctx context.Context, bucket string, path string) { 53 | // TODO: use reader 54 | src, err := object.Open() 55 | if err != nil { 56 | panic(err) 57 | } 58 | defer src.Close() 59 | _, err = base.Storage.PutObject(ctx, bucket, path, src, object.Size, minio.PutObjectOptions{}) 60 | if err != nil { 61 | panic(errors.Wrap(err, "could write file to s3 storage.")) 62 | } 63 | } 64 | 65 | func MustPutInputFile(sanitize bool, object *multipart.FileHeader, ctx context.Context, bucket string, path string) { 66 | originalSrc, err := object.Open() 67 | if err != nil { 68 | panic(err) 69 | } 70 | defer originalSrc.Close() 71 | var ( 72 | fileSize int64 73 | src io.Reader = originalSrc 74 | ) 75 | 76 | if sanitize { 77 | reader := bufio.NewReader(originalSrc) 78 | tempFile, err := os.CreateTemp("", "tempFile*.txt") 79 | if err != nil { 80 | panic(err) 81 | } 82 | tempFileName := tempFile.Name() 83 | defer os.Remove(tempFileName) 84 | writer := bufio.NewWriter(tempFile) 85 | 86 | for { 87 | line, err := reader.ReadString('\n') 88 | if err != nil && err != io.EOF { 89 | panic(err) 90 | } 91 | 92 | if len(line) > 0 { 93 | line = strings.ReplaceAll(line, "\r\n", "\n") 94 | if !strings.HasSuffix(line, "\n") { 95 | line += "\n" 96 | } 97 | _, writeErr := writer.WriteString(line) 98 | if writeErr != nil { 99 | panic(writeErr) 100 | } 101 | } 102 | if err == io.EOF { 103 | break 104 | } 105 | } 106 | if err := writer.Flush(); err != nil { 107 | panic(err) 108 | } 109 | if err := tempFile.Close(); err != nil { 110 | panic(err) 111 | } 112 | tempSrc, err := os.Open(tempFileName) 113 | if err != nil { 114 | panic(err) 115 | } 116 | defer tempSrc.Close() 117 | fileInfo, err := os.Stat(tempFileName) 118 | if err != nil { 119 | panic(err) 120 | } 121 | fileSize = fileInfo.Size() 122 | src = tempSrc 123 | } else { 124 | fileSize = object.Size 125 | } 126 | 127 | _, err = base.Storage.PutObject(ctx, bucket, path, src, fileSize, minio.PutObjectOptions{}) 128 | if err != nil { 129 | panic(errors.Wrap(err, "couldn't write file to s3 storage.")) 130 | } 131 | } 132 | 133 | func MustGetObject(c echo.Context, bucket string, path string) *minio.Object { 134 | object, err := base.Storage.GetObject(c.Request().Context(), bucket, path, minio.GetObjectOptions{}) 135 | if err != nil { 136 | panic(err) 137 | } 138 | return object 139 | } 140 | -------------------------------------------------------------------------------- /base/utils/http_error.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/EduOJ/backend/app/response" 7 | "github.com/labstack/echo/v4" 8 | ) 9 | 10 | type HttpError struct { 11 | Code int 12 | Message string 13 | Err error 14 | } 15 | 16 | func (e HttpError) Error() string { 17 | return fmt.Sprintf("[%d]%s", e.Code, e.Message) 18 | } 19 | 20 | func (e HttpError) Response(c echo.Context) error { 21 | return c.JSON(e.Code, response.ErrorResp(e.Message, e.Err)) 22 | } 23 | -------------------------------------------------------------------------------- /base/utils/origins.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "github.com/pkg/errors" 5 | "github.com/spf13/viper" 6 | ) 7 | 8 | var Origins []string 9 | 10 | func InitOrigin() { 11 | err := viper.UnmarshalKey("server.origin", &Origins) 12 | if err != nil { 13 | panic(errors.Wrap(err, "could not read server.origin from config")) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /base/utils/password.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "github.com/EduOJ/backend/base" 5 | "github.com/EduOJ/backend/database/models" 6 | "golang.org/x/crypto/bcrypt" 7 | ) 8 | 9 | func HashPassword(password string) (hashed string) { 10 | hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) 11 | if err != nil { 12 | panic(err) 13 | } 14 | hashed = string(hash) 15 | return 16 | } 17 | 18 | func VerifyPassword(password, hashed string) bool { 19 | return bcrypt.CompareHashAndPassword([]byte(hashed), []byte(password)) == nil 20 | } 21 | 22 | func GetToken(tokenString string) (token models.Token, err error) { 23 | t := models.Token{} 24 | err = base.DB.Preload("User").Where("token = ?", tokenString).First(&t).Error 25 | if err != nil { 26 | return 27 | } 28 | return t, nil 29 | } 30 | -------------------------------------------------------------------------------- /base/utils/problem_set.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "encoding/json" 5 | "sync" 6 | "time" 7 | 8 | "github.com/EduOJ/backend/base" 9 | "github.com/EduOJ/backend/base/log" 10 | "github.com/EduOJ/backend/database/models" 11 | "github.com/pkg/errors" 12 | "gorm.io/datatypes" 13 | "gorm.io/gorm" 14 | ) 15 | 16 | var gradeLock sync.Mutex 17 | 18 | func UpdateGrade(submission *models.Submission) error { 19 | gradeLock.Lock() 20 | defer gradeLock.Unlock() 21 | if submission.ProblemSetID == 0 { 22 | return nil 23 | } 24 | if submission.ProblemSet == nil { 25 | problemSet := models.ProblemSet{} 26 | if err := base.DB.First(&problemSet, submission.ProblemSetID).Error; err != nil { 27 | return errors.Wrap(err, "could not get problem set for updating grade") 28 | } 29 | submission.ProblemSet = &problemSet 30 | } 31 | if time.Now().After(submission.ProblemSet.EndTime) { 32 | return nil 33 | } 34 | grade := models.Grade{} 35 | detail := make(map[uint]uint) 36 | var err error 37 | err = base.DB.First(&grade, "problem_set_id = ? and user_id = ?", submission.ProblemSetID, submission.UserID).Error 38 | if err != nil { 39 | if errors.Is(err, gorm.ErrRecordNotFound) { 40 | problemSet := models.ProblemSet{} 41 | if err := base.DB.First(&problemSet, submission.ProblemSetID).Error; err != nil { 42 | return err 43 | } 44 | grade = models.Grade{ 45 | UserID: submission.UserID, 46 | ProblemSetID: submission.ProblemSetID, 47 | ClassID: problemSet.ClassID, 48 | Detail: datatypes.JSON("{}"), 49 | Total: 0, 50 | } 51 | } else { 52 | return err 53 | } 54 | } 55 | err = json.Unmarshal(grade.Detail, &detail) 56 | if err != nil { 57 | return err 58 | } 59 | if detail[submission.ProblemID] < submission.Score { 60 | detail[submission.ProblemID] = submission.Score 61 | } 62 | grade.Detail, err = json.Marshal(detail) 63 | if err != nil { 64 | return err 65 | } 66 | return base.DB.Save(&grade).Error 67 | } 68 | 69 | func RefreshGrades(problemSet *models.ProblemSet) error { 70 | gradeLock.Lock() 71 | defer gradeLock.Unlock() 72 | if err := base.DB.Delete(&models.Grade{}, "problem_set_id = ?", problemSet.ID).Error; err != nil { 73 | return err 74 | } 75 | var grades []*models.Grade 76 | for _, u := range problemSet.Class.Students { 77 | grade := models.Grade{ 78 | UserID: u.ID, 79 | ProblemSetID: problemSet.ID, 80 | ClassID: problemSet.ClassID, 81 | Detail: nil, 82 | Total: 0, 83 | } 84 | detail := make(map[uint]uint) 85 | for _, p := range problemSet.Problems { 86 | var score uint = 0 87 | submission := models.Submission{} 88 | err := base.DB. 89 | Where("user_id = ?", u.ID). 90 | Where("problem_id = ?", p.ID). 91 | Where("problem_set_id = ?", problemSet.ID). 92 | Where("created_at < ?", problemSet.EndTime). 93 | Order("score desc"). 94 | Order("created_at desc"). 95 | First(&submission).Error 96 | if err != nil { 97 | if !errors.Is(err, gorm.ErrRecordNotFound) { 98 | return errors.Wrap(err, "could not get submission when refreshing grades") 99 | } 100 | } else { 101 | score = submission.Score 102 | } 103 | detail[p.ID] = score 104 | grade.Total += score 105 | } 106 | var err error 107 | grade.Detail, err = json.Marshal(detail) 108 | if err != nil { 109 | return errors.Wrap(err, "could not marshal grade detail when refreshing grades") 110 | } 111 | grades = append(grades, &grade) 112 | } 113 | if err := base.DB.Create(&grades).Error; err != nil { 114 | return errors.Wrap(err, "could not create grades when refreshing grades") 115 | } 116 | problemSet.Grades = grades 117 | return nil 118 | } 119 | 120 | // CreateEmptyGrades Creates empty grades(score 0 for all the problems) 121 | // 122 | // for users who don't have a grade for this problem set. 123 | func CreateEmptyGrades(problemSet *models.ProblemSet) error { 124 | gradeLock.Lock() 125 | defer gradeLock.Unlock() 126 | 127 | // Create empty grade JSON object 128 | detail := make(map[uint]uint) 129 | for _, p := range problemSet.Problems { 130 | detail[p.ID] = 0 131 | } 132 | emptyDetail, err := json.Marshal(detail) // turn map to json 133 | if err != nil { 134 | log.Errorf("Error marshalling grade detail for empty grade: %v", err) 135 | return errors.Wrap(err, "could not marshal grade detail for empty grade") 136 | } 137 | 138 | // Record students who have a grade 139 | gradeSet := make(map[uint]bool) 140 | for _, g := range problemSet.Grades { 141 | gradeSet[g.UserID] = true 142 | } 143 | 144 | // Generate empty grade slice 145 | grades := make([]*models.Grade, 0, len(problemSet.Class.Students)-len(problemSet.Grades)) 146 | for _, u := range problemSet.Class.Students { 147 | if gradeSet[u.ID] { 148 | continue 149 | } 150 | newGrade := models.Grade{ 151 | UserID: u.ID, 152 | ProblemSetID: problemSet.ID, 153 | ClassID: problemSet.ClassID, 154 | Detail: emptyDetail, 155 | Total: 0, 156 | } 157 | grades = append(grades, &newGrade) 158 | } 159 | 160 | // Store empty grades into DB 161 | if len(grades) > 0 { 162 | if err = base.DB.Create(&grades).Error; err != nil { 163 | return errors.Wrap(err, "could not create empty grades") 164 | } 165 | } 166 | 167 | // Update problem set 168 | problemSet.Grades = append(problemSet.Grades, grades...) 169 | return nil 170 | } 171 | -------------------------------------------------------------------------------- /base/utils/storage.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/url" 7 | "sync" 8 | "time" 9 | 10 | "github.com/EduOJ/backend/base" 11 | "github.com/minio/minio-go/v7" 12 | "github.com/pkg/errors" 13 | "github.com/spf13/viper" 14 | ) 15 | 16 | var bucketsCreated sync.Map 17 | 18 | func CreateBucket(name string) error { 19 | _, ok := bucketsCreated.Load(name) 20 | if ok { 21 | return nil 22 | } 23 | found, err := base.Storage.BucketExists(context.Background(), name) 24 | if err != nil { 25 | return errors.Wrap(err, "could not query if bucket exists") 26 | } 27 | if !found { 28 | err = base.Storage.MakeBucket(context.Background(), name, minio.MakeBucketOptions{ 29 | Region: viper.GetString("storage.region"), 30 | }) 31 | if err != nil { 32 | return errors.Wrap(err, "could not create bucket") 33 | } 34 | } 35 | bucketsCreated.Store(name, true) 36 | return nil 37 | } 38 | 39 | func GetPresignedURL(bucket string, path string, fileName string) (string, error) { 40 | reqParams := make(url.Values) 41 | reqParams.Set("response-content-disposition", fmt.Sprintf(`attachment; filename="%s"; filename*=utf-8''%s`, fileName, url.QueryEscape(fileName))) 42 | presignedURL, err := base.Storage.PresignedGetObject(context.Background(), bucket, path, time.Second*60, reqParams) 43 | if err != nil { 44 | return "", err 45 | } 46 | return presignedURL.String(), err 47 | } 48 | -------------------------------------------------------------------------------- /base/utils/storage_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "io/ioutil" 7 | "net/http" 8 | "testing" 9 | 10 | "github.com/EduOJ/backend/base" 11 | "github.com/minio/minio-go/v7" 12 | "github.com/spf13/viper" 13 | "github.com/stretchr/testify/assert" 14 | ) 15 | 16 | func getPresignedURLContent(t *testing.T, presignedUrl string) (content string) { 17 | resp, err := http.Get(presignedUrl) 18 | assert.NoError(t, err) 19 | assert.Equal(t, http.StatusOK, resp.StatusCode) 20 | body, err := ioutil.ReadAll(resp.Body) 21 | assert.NoError(t, err) 22 | return string(body) 23 | } 24 | 25 | func TestMustCreateBucket(t *testing.T) { 26 | t.Run("testMustCreateBucketExistingBucket", func(t *testing.T) { 27 | assert.NoError(t, base.Storage.MakeBucket(context.Background(), "existing-bucket", minio.MakeBucketOptions{ 28 | Region: viper.GetString("storage.region"), 29 | })) 30 | found, err := base.Storage.BucketExists(context.Background(), "existing-bucket") 31 | assert.True(t, found) 32 | assert.NoError(t, err) 33 | assert.NoError(t, CreateBucket("existing-bucket")) 34 | found, err = base.Storage.BucketExists(context.Background(), "existing-bucket") 35 | assert.True(t, found) 36 | assert.NoError(t, err) 37 | }) 38 | t.Run("testMustCreateBucketNonExistingBucket", func(t *testing.T) { 39 | found, err := base.Storage.BucketExists(context.Background(), "non-existing-bucket") 40 | assert.False(t, found) 41 | assert.NoError(t, err) 42 | assert.NoError(t, CreateBucket("non-existing-bucket")) 43 | found, err = base.Storage.BucketExists(context.Background(), "non-existing-bucket") 44 | assert.True(t, found) 45 | assert.NoError(t, err) 46 | }) 47 | } 48 | 49 | func TestGetPresignedURL(t *testing.T) { 50 | b := []byte("test_get_presigned_url") 51 | reader := bytes.NewReader(b) 52 | info, err := base.Storage.PutObject(context.Background(), "test-bucket", "test_get_presigned_url_object", reader, int64(len(b)), minio.PutObjectOptions{}) 53 | assert.NoError(t, err) 54 | assert.Equal(t, int64(len(b)), info.Size) 55 | presignedUrl, err := GetPresignedURL("test-bucket", "test_get_presigned_url_object", "test_get_presigned_url_file_name") 56 | assert.NoError(t, err) 57 | resp, err := http.Get(presignedUrl) 58 | assert.NoError(t, err) 59 | assert.Equal(t, http.StatusOK, resp.StatusCode) 60 | assert.Equal(t, string(b), getPresignedURLContent(t, presignedUrl)) 61 | } 62 | -------------------------------------------------------------------------------- /base/utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "math/rand" 5 | "reflect" 6 | "unsafe" 7 | ) 8 | 9 | // Random string generator by https://stackoverflow.com/a/22892986/8031146 10 | 11 | const letterBytes = "abcdefghjkmnpqrstuvwxyzABCDEFGHJKMNPQRSTUVWXYZ23456789" 12 | const ( 13 | letterIdxBits = 6 // 6 bits to represent a letter index 14 | letterIdxMask = 1<= 0; { 24 | if remain == 0 { 25 | cache, remain = rand.Int63(), letterIdxMax 26 | } 27 | if idx := int(cache & letterIdxMask); idx < len(letterBytes) { 28 | b[i] = letterBytes[idx] 29 | i-- 30 | } 31 | cache >>= letterIdxBits 32 | remain-- 33 | } 34 | 35 | return *(*string)(unsafe.Pointer(&b)) 36 | } 37 | 38 | func Contain(obj interface{}, target interface{}) bool { 39 | targetValue := reflect.ValueOf(target) 40 | switch reflect.TypeOf(target).Kind() { 41 | case reflect.Slice, reflect.Array: 42 | for i := 0; i < targetValue.Len(); i++ { 43 | if targetValue.Index(i).Interface() == obj { 44 | return true 45 | } 46 | } 47 | case reflect.Map: 48 | if targetValue.MapIndex(reflect.ValueOf(obj)).IsValid() { 49 | return true 50 | } 51 | } 52 | 53 | return false 54 | } 55 | -------------------------------------------------------------------------------- /base/utils/utils_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "net/http/httptest" 7 | "os" 8 | "testing" 9 | 10 | "github.com/EduOJ/backend/base" 11 | "github.com/EduOJ/backend/base/log" 12 | "github.com/EduOJ/backend/database" 13 | "github.com/johannesboyne/gofakes3" 14 | "github.com/johannesboyne/gofakes3/backend/s3mem" 15 | "github.com/minio/minio-go/v7" 16 | "github.com/minio/minio-go/v7/pkg/credentials" 17 | "github.com/pkg/errors" 18 | "github.com/spf13/viper" 19 | ) 20 | 21 | func TestMain(m *testing.M) { 22 | defer database.SetupDatabaseForTest()() 23 | if err := base.DB.Migrator().AutoMigrate(&TestObject{}); err != nil { 24 | panic(errors.Wrap(err, "could not create table for test object")) 25 | } 26 | 27 | configFile := bytes.NewBufferString(`debug: true 28 | server: 29 | port: 8080 30 | origin: 31 | - http://127.0.0.1:8000 32 | `) 33 | viper.SetConfigType("yaml") 34 | if err := viper.ReadConfig(configFile); err != nil { 35 | panic(err) 36 | } 37 | 38 | // fake s3 39 | faker := gofakes3.New(s3mem.New()) // in-memory s3 server. 40 | ts := httptest.NewServer(faker.Server()) 41 | defer ts.Close() 42 | var err error 43 | base.Storage, err = minio.New(ts.URL[7:], &minio.Options{ 44 | Creds: credentials.NewStaticV4("accessKey", "secretAccessKey", ""), 45 | Secure: false, 46 | }) 47 | if err != nil { 48 | panic(err) 49 | } 50 | err = base.Storage.MakeBucket(context.Background(), "test-bucket", minio.MakeBucketOptions{ 51 | Region: viper.GetString("storage.region"), 52 | }) 53 | if err != nil { 54 | panic(err) 55 | } 56 | log.Disable() 57 | 58 | os.Exit(m.Run()) 59 | } 60 | -------------------------------------------------------------------------------- /base/validator/validator.go: -------------------------------------------------------------------------------- 1 | package validator 2 | 3 | import ( 4 | "regexp" 5 | 6 | "github.com/EduOJ/backend/base/log" 7 | zhTranslations "github.com/EduOJ/backend/base/validator/translations/zh" 8 | zhLocal "github.com/go-playground/locales/zh" 9 | ut "github.com/go-playground/universal-translator" 10 | "github.com/go-playground/validator/v10" 11 | "github.com/pkg/errors" 12 | ) 13 | 14 | type echoValidator struct { 15 | V *validator.Validate 16 | } 17 | 18 | func (cv *echoValidator) Validate(i interface{}) error { 19 | return cv.V.Struct(i) 20 | } 21 | 22 | var Trans ut.Translator 23 | 24 | func init() { 25 | zh := zhLocal.New() 26 | uni := ut.New(zh, zh) 27 | var found bool 28 | Trans, found = uni.GetTranslator("zh") 29 | if !found { 30 | log.Fatal("could not found zh translator") 31 | panic("could not found zh translator") 32 | } 33 | } 34 | 35 | func New() *validator.Validate { 36 | v := validator.New() 37 | // add custom translation here 38 | if err := zhTranslations.RegisterDefaultTranslations(v, Trans); err != nil { 39 | log.Fatal(errors.Wrap(err, "could not register default translations")) 40 | panic(errors.Wrap(err, "could not register default translations")) 41 | } 42 | err := v.RegisterValidation("username", validateUsername) 43 | if err != nil { 44 | log.Fatal(errors.Wrap(err, "could not register validation")) 45 | panic(errors.Wrap(err, "could not register validation")) 46 | } 47 | return v 48 | } 49 | 50 | func NewEchoValidator() *echoValidator { 51 | return &echoValidator{ 52 | V: New(), 53 | } 54 | } 55 | 56 | var usernameRegex = regexp.MustCompile("^[a-zA-Z0-9_]+$") 57 | 58 | func validateUsername(fl validator.FieldLevel) bool { 59 | return usernameRegex.MatchString(fl.Field().String()) 60 | } 61 | -------------------------------------------------------------------------------- /base/validator/validator_test.go: -------------------------------------------------------------------------------- 1 | package validator 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/go-playground/validator/v10" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestUsernameValidator(t *testing.T) { 11 | v := New() 12 | t.Run("testUsernameValidatorSuccess", func(t *testing.T) { 13 | assert.NoError(t, v.Var("abcdefghijklmnopqrstuvwxyz", "username")) 14 | assert.NoError(t, v.Var("ABCDEFGHIJKLMNOPQRSTUVWXYZ", "username")) 15 | assert.NoError(t, v.Var("1234567890", "username")) 16 | assert.NoError(t, v.Var("_____", "username")) 17 | assert.NoError(t, v.Var("abcABC123_", "username")) 18 | }) 19 | tests := []struct { 20 | field string 21 | err string 22 | }{ 23 | { 24 | field: "test_username_with_@", 25 | err: "Key: '' Error:Field validation for '' failed on the 'username' tag", 26 | }, 27 | { 28 | field: "test_username_with_non_ascii_char_中文", 29 | err: "Key: '' Error:Field validation for '' failed on the 'username' tag", 30 | }, 31 | } 32 | t.Run("testUsernameValidatorFail", func(t *testing.T) { 33 | for _, test := range tests { 34 | err := v.Var(test.field, "username") 35 | e, ok := err.(validator.ValidationErrors) 36 | assert.True(t, ok) 37 | assert.NotNil(t, e) 38 | assert.Equal(t, test.err, e.Error()) 39 | } 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /clean.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/EduOJ/backend/base/exit" 7 | "github.com/EduOJ/backend/base/log" 8 | "github.com/EduOJ/backend/base/utils" 9 | ) 10 | 11 | func clean() { 12 | readConfig() 13 | initGorm() 14 | initLog() 15 | err := utils.CleanUpExpiredTokens() 16 | if err != nil { 17 | log.Error(err) 18 | os.Exit(-1) 19 | } 20 | exit.Close() 21 | exit.QuitWG.Wait() 22 | log.Fatal("Clean succeed!") 23 | } 24 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | target: auto 6 | threshold: 0.5% 7 | patch: 8 | default: 9 | target: auto 10 | threshold: 0.5% 11 | -------------------------------------------------------------------------------- /config.yml.example: -------------------------------------------------------------------------------- 1 | database: 2 | dialect: postgres 3 | uri: host=myhost port=myport user=gorm dbname=gorm password=mypassword 4 | storage: 5 | endpoint: 127.0.0.1:9000 6 | access_key_id: minioadmin 7 | access_key_secret: minioadmin 8 | ssl: false 9 | region: us-east-1 10 | redis: 11 | host: 127.0.0.1 12 | port: 6379 13 | log: 14 | - name: console 15 | level: debug 16 | - name: database 17 | level: error 18 | debug: false 19 | server: 20 | port: 8080 21 | origin: 22 | - http://127.0.0.1:8000 23 | auth: 24 | session_timeout: 1200 # The valid duration of token without choosing "remember me" 25 | remember_me_timeout: 604800 # The valid duration of token with choosing "remember me" 26 | session_count: 10 # The count of maximum active sessions for a user 27 | judger: 28 | token: REPLACE_THIS_WITH_RANDOM_STRING 29 | polling_timeout: 60s 30 | webauthn: 31 | display_name: EduOJ 32 | domain: localhost 33 | origin: http://localhost 34 | icon: http://localhost/favicon.ico 35 | email: 36 | from: example@example.com 37 | host: smtp.example.com 38 | port: 587 39 | username: user 40 | password: pass 41 | tls: true 42 | need_verification: true 43 | -------------------------------------------------------------------------------- /database/iterator.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "context" 5 | 6 | "gorm.io/gorm" 7 | ) 8 | 9 | const IteratorLimit = 1000 10 | 11 | type Iterator struct { 12 | query *gorm.DB 13 | bunch interface{} 14 | count int 15 | offset int 16 | limit int 17 | index int 18 | } 19 | 20 | func NewIterator(query *gorm.DB, bunch interface{}, limit ...int) (it *Iterator, err error) { 21 | it = &Iterator{ 22 | bunch: bunch, 23 | query: query, 24 | limit: IteratorLimit, 25 | offset: 0, 26 | index: -1, 27 | } 28 | if len(limit) > 0 { 29 | it.limit = limit[0] 30 | } 31 | err = it.nextBunch() 32 | return 33 | } 34 | 35 | func (it *Iterator) nextBunch() error { 36 | 37 | if err := it.query.WithContext(context.Background()).Limit(it.limit).Offset(it.offset).Find(it.bunch).Error; err != nil { 38 | return err 39 | } 40 | var count int64 41 | if err := it.query.WithContext(context.Background()).Model(it.bunch).Count(&count).Error; err != nil { 42 | return err 43 | } 44 | if c := int(count) - it.offset; c < it.limit { 45 | it.count = c 46 | } else { 47 | it.count = it.limit 48 | } 49 | it.offset += it.limit 50 | it.index = -1 51 | return nil 52 | } 53 | 54 | func (it *Iterator) Next() (ok bool, err error) { 55 | if it.index+1 == it.limit { 56 | if err = it.saveBranch(); err != nil { 57 | return 58 | } 59 | if err = it.nextBunch(); err != nil { 60 | return 61 | } 62 | } 63 | if it.index+1 == it.count { 64 | if it.count > 0 { 65 | if err = it.saveBranch(); err != nil { 66 | return 67 | } 68 | } 69 | return false, nil 70 | } 71 | it.index++ 72 | return true, nil 73 | } 74 | 75 | func (it *Iterator) Index() int { 76 | return it.index 77 | } 78 | 79 | func (it *Iterator) saveBranch() error { 80 | return it.query.Save(it.bunch).Error 81 | } 82 | -------------------------------------------------------------------------------- /database/iterator_test.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "gorm.io/driver/sqlite" 9 | "gorm.io/gorm" 10 | "gorm.io/gorm/logger" 11 | ) 12 | 13 | type TestingObject struct { 14 | ID uint `gorm:"primaryKey" json:"id"` 15 | Name string `json:"name" gorm:"size:255;default:'';not null"` 16 | 17 | CreatedAt time.Time `json:"created_at"` 18 | UpdatedAt time.Time `json:"-"` 19 | DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at"` 20 | } 21 | 22 | func TestIterator(t *testing.T) { 23 | tx, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{ 24 | Logger: logger.Default.LogMode(logger.Silent), 25 | DisableForeignKeyConstraintWhenMigrating: true, 26 | }) 27 | assert.NoError(t, err) 28 | err = tx.AutoMigrate(TestingObject{}) 29 | assert.NoError(t, err) 30 | 31 | t.Run("Global", func(t *testing.T) { 32 | // Not Parallel 33 | err = tx.Delete(&TestingObject{}, "id > 0").Error 34 | assert.NoError(t, err) 35 | objects := []TestingObject{ 36 | { 37 | Name: "test_iterator_1", 38 | }, 39 | { 40 | Name: "test_iterator_2", 41 | }, 42 | { 43 | Name: "test_iterator_3", 44 | }, 45 | } 46 | err = tx.Create(&objects).Error 47 | assert.NoError(t, err) 48 | 49 | ts := make([]TestingObject, 2) 50 | it, err := NewIterator(tx, &ts, 2) 51 | assert.NoError(t, err) 52 | for i := 0; true; i++ { 53 | ok, err := it.Next() 54 | assert.NoError(t, err) 55 | if !ok { 56 | break 57 | } 58 | assert.Equal(t, objects[i].ID, ts[it.index].ID) 59 | assert.Equal(t, objects[i].Name, ts[it.index].Name) 60 | } 61 | }) 62 | t.Run("Selected", func(t *testing.T) { 63 | // Not Parallel 64 | objects := []TestingObject{ 65 | { 66 | Name: "test_iterator_search_1", 67 | }, 68 | { 69 | Name: "test_iterator_search_2", 70 | }, 71 | { 72 | Name: "test_iterator_search_3", 73 | }, 74 | } 75 | err = tx.Create(&objects).Error 76 | assert.NoError(t, err) 77 | 78 | ts := make([]TestingObject, 2) 79 | it, err := NewIterator(tx.Where("name like ?", "%search%"), &ts, 2) 80 | assert.NoError(t, err) 81 | for i := 0; true; i++ { 82 | ok, err := it.Next() 83 | assert.NoError(t, err) 84 | if !ok { 85 | break 86 | } 87 | assert.Equal(t, objects[i].ID, ts[it.index].ID) 88 | assert.Equal(t, objects[i].Name, ts[it.index].Name) 89 | } 90 | }) 91 | t.Run("Empty", func(t *testing.T) { 92 | // Not Parallel 93 | err = tx.Delete(&TestingObject{}, "id > 0").Error 94 | assert.NoError(t, err) 95 | 96 | ts := make([]TestingObject, 2) 97 | it, err := NewIterator(tx.Where("name like ?", "%search%"), &ts, 2) 98 | assert.NoError(t, err) 99 | ok, err := it.Next() 100 | assert.NoError(t, err) 101 | assert.False(t, ok) 102 | }) 103 | 104 | } 105 | -------------------------------------------------------------------------------- /database/migrate_test.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestMigrate(t *testing.T) { 10 | defer SetupDatabaseForTest()() 11 | m := GetMigration() 12 | assert.NoError(t, m.RollbackTo("start")) 13 | } 14 | -------------------------------------------------------------------------------- /database/models/class.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/EduOJ/backend/base" 7 | "gorm.io/gorm" 8 | ) 9 | 10 | type Class struct { 11 | ID uint `gorm:"primaryKey" json:"id"` 12 | Name string `json:"name" gorm:"size:255;default:'';not null"` 13 | CourseName string `json:"course_name" gorm:"size:255;default:'';not null"` 14 | Description string `json:"description"` 15 | InviteCode string `json:"invite_code" gorm:"size:255;default:'';not null"` 16 | Managers []*User `json:"managers" gorm:"many2many:user_manage_classes"` 17 | Students []*User `json:"students" gorm:"many2many:user_in_classes"` 18 | 19 | ProblemSets []*ProblemSet `json:"problem_sets"` 20 | 21 | CreatedAt time.Time `json:"created_at"` 22 | UpdatedAt time.Time `json:"-"` 23 | DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at"` 24 | } 25 | 26 | func (c Class) GetID() uint { 27 | return c.ID 28 | } 29 | 30 | func (c Class) TypeName() string { 31 | return "class" 32 | } 33 | 34 | func (c *Class) AddStudents(ids []uint) error { 35 | if len(ids) == 0 { 36 | return nil 37 | } 38 | existingIds := make([]uint, len(c.Students)) 39 | for i, s := range c.Students { 40 | existingIds[i] = s.ID 41 | } 42 | var users []User 43 | query := base.DB 44 | if len(existingIds) != 0 { 45 | query = base.DB.Where("id not in (?)", existingIds) 46 | } 47 | if err := query.Find(&users, ids).Error; err != nil { 48 | return err 49 | } 50 | return base.DB.Model(c).Association("Students").Append(&users) 51 | } 52 | 53 | func (c *Class) DeleteStudents(ids []uint) error { 54 | if len(ids) == 0 { 55 | return nil 56 | } 57 | var users []User 58 | if err := base.DB.Find(&users, ids).Error; err != nil { 59 | return err 60 | } 61 | return base.DB.Model(c).Association("Students").Delete(&users) 62 | } 63 | 64 | func (c *Class) AfterDelete(tx *gorm.DB) error { 65 | var problemSets []ProblemSet 66 | err := tx.Find(&problemSets, "class_id = ?", c.ID).Error 67 | if err != nil { 68 | return err 69 | } 70 | if len(problemSets) != 0 { 71 | return tx.Delete(&problemSets).Error 72 | } 73 | return nil 74 | } 75 | -------------------------------------------------------------------------------- /database/models/config.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "time" 4 | 5 | type Config struct { 6 | ID uint `gorm:"primaryKey"` 7 | Key string 8 | Value *string `gorm:"default:''"` // 可能是空字符串, 因此得是指针 9 | CreatedAt time.Time 10 | UpdatedAt time.Time 11 | } 12 | -------------------------------------------------------------------------------- /database/models/email.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "time" 5 | 6 | "gorm.io/gorm" 7 | ) 8 | 9 | type EmailVerificationToken struct { 10 | ID uint `gorm:"primaryKey" json:"id"` 11 | UserID uint 12 | User *User 13 | Email string 14 | Token string 15 | 16 | Used bool 17 | 18 | CreatedAt time.Time `json:"created_at"` 19 | UpdatedAt time.Time `json:"-"` 20 | DeletedAt gorm.DeletedAt `json:"deleted_at"` 21 | } 22 | -------------------------------------------------------------------------------- /database/models/image.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "time" 4 | 5 | type Image struct { 6 | ID uint `gorm:"primaryKey" json:"id"` 7 | Filename string `gorm:"filename"` 8 | FilePath string `gorm:"filepath"` 9 | UserID uint 10 | User User 11 | CreatedAt time.Time `json:"created_at"` 12 | UpdatedAt time.Time `json:"updated_at"` 13 | } 14 | -------------------------------------------------------------------------------- /database/models/language.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/EduOJ/backend/database" 7 | ) 8 | 9 | type Language struct { 10 | Name string `gorm:"primaryKey" json:"name"` 11 | ExtensionAllowed database.StringArray `gorm:"type:string" json:"extension_allowed"` 12 | BuildScriptName string `json:"-"` 13 | BuildScript *Script `gorm:"foreignKey:BuildScriptName" json:"build_script"` 14 | RunScriptName string `json:"-"` 15 | RunScript *Script `gorm:"foreignKey:RunScriptName" json:"run_script"` 16 | CreatedAt time.Time `json:"created_at"` 17 | UpdatedAt time.Time `json:"updated_at"` 18 | } 19 | -------------------------------------------------------------------------------- /database/models/log/log.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type Log struct { 8 | ID uint `gorm:"primaryKey" json:"id"` 9 | Level *int `json:"level"` 10 | Message string `json:"message"` 11 | Caller string `json:"caller"` 12 | CreatedAt time.Time `json:"created_at"` 13 | } 14 | -------------------------------------------------------------------------------- /database/models/models_test.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/EduOJ/backend/base/exit" 8 | "github.com/EduOJ/backend/database" 9 | ) 10 | 11 | func TestMain(m *testing.M) { 12 | defer database.SetupDatabaseForTest()() 13 | defer exit.SetupExitForTest()() 14 | os.Exit(m.Run()) 15 | } 16 | -------------------------------------------------------------------------------- /database/models/permission.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/EduOJ/backend/base" 7 | ) 8 | 9 | type HasRole interface { 10 | GetID() uint 11 | TypeName() string 12 | } 13 | 14 | type UserHasRole struct { 15 | ID uint `gorm:"primaryKey" json:"id"` 16 | UserID uint `json:"user_id"` 17 | RoleID uint `json:"role_id"` 18 | Role Role `json:"role"` 19 | TargetID uint `json:"target_id"` 20 | } 21 | 22 | type Role struct { 23 | ID uint `gorm:"primaryKey" json:"id"` 24 | Name string `json:"name"` 25 | Target *string `json:"target"` 26 | Permissions []Permission 27 | } 28 | 29 | type Permission struct { 30 | ID uint `gorm:"primaryKey" json:"id"` 31 | RoleID uint `json:"role_id"` 32 | Name string `json:"name"` 33 | } 34 | 35 | func (r *Role) AddPermission(name string) (err error) { 36 | p := Permission{ 37 | RoleID: r.ID, 38 | Name: name, 39 | } 40 | return base.DB.Model(r).Association("Permissions").Append(&p) 41 | } 42 | 43 | func (u UserHasRole) MarshalJSON() ([]byte, error) { 44 | ret, err := json.Marshal(struct { 45 | Role 46 | TargetID uint `json:"target_id"` 47 | }{ 48 | Role: u.Role, 49 | TargetID: u.TargetID, 50 | }) 51 | return ret, err 52 | } 53 | -------------------------------------------------------------------------------- /database/models/permission_test.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "gorm.io/gorm" 10 | ) 11 | 12 | func TestUserHasRoleMarshalJSON(t *testing.T) { 13 | dummy := "test_class" 14 | perm1ForRole1 := Permission{ 15 | ID: 1001, 16 | RoleID: 2001, 17 | Name: "perm1", 18 | } 19 | perm1ForRole2 := Permission{ 20 | ID: 1002, 21 | RoleID: 2002, 22 | Name: "perm1", 23 | } 24 | perm2ForRole1 := Permission{ 25 | ID: 1003, 26 | RoleID: 2001, 27 | Name: "perm2", 28 | } 29 | role1 := Role{ 30 | ID: 2001, 31 | Name: "role1", 32 | Target: &dummy, 33 | Permissions: []Permission{ 34 | perm1ForRole1, 35 | perm2ForRole1, 36 | }, 37 | } 38 | role2 := Role{ 39 | ID: 2002, 40 | Name: "role2", 41 | Target: &dummy, 42 | Permissions: []Permission{ 43 | perm1ForRole2, 44 | }, 45 | } 46 | userHasRole1ForClassA := UserHasRole{ 47 | ID: 3001, 48 | UserID: 4001, 49 | RoleID: 2001, 50 | Role: role1, 51 | TargetID: 5001, // classA 52 | } 53 | userHasRole2ForClassB := UserHasRole{ 54 | ID: 3002, 55 | UserID: 4001, 56 | RoleID: 2002, 57 | Role: role2, 58 | TargetID: 5002, // classB 59 | } 60 | userHasRole1Global := UserHasRole{ 61 | ID: 3003, 62 | UserID: 4001, 63 | RoleID: 2001, 64 | Role: role1, 65 | } 66 | location, _ := time.LoadLocation("Asia/Shanghai") 67 | baseTime := time.Date(2020, 8, 18, 13, 24, 24, 31972138, location) 68 | deleteTime := baseTime.Add(time.Minute * 5) 69 | user := User{ 70 | ID: 4001, 71 | Username: "test_marshal_json_username", 72 | Nickname: "test_marshal_json_nickname", 73 | Email: "test_marshal_json@mail.com", 74 | Password: "test_marshal_json_password", 75 | 76 | Roles: []UserHasRole{ 77 | userHasRole1ForClassA, 78 | userHasRole2ForClassB, 79 | userHasRole1Global, 80 | }, 81 | RoleLoaded: true, 82 | 83 | CreatedAt: baseTime.Add(time.Second), 84 | UpdatedAt: baseTime.Add(time.Minute), 85 | DeletedAt: gorm.DeletedAt{ 86 | Valid: true, 87 | Time: deleteTime, 88 | }, 89 | } 90 | j, err := json.Marshal(user) 91 | assert.NoError(t, err) 92 | expected := `{"id":4001,"username":"test_marshal_json_username","nickname":"test_marshal_json_nickname","email":"test_marshal_json@mail.com","email_verified":false,"roles":[{"id":2001,"name":"role1","target":"test_class","Permissions":[{"id":1001,"role_id":2001,"name":"perm1"},{"id":1003,"role_id":2001,"name":"perm2"}],"target_id":5001},{"id":2002,"name":"role2","target":"test_class","Permissions":[{"id":1002,"role_id":2002,"name":"perm1"}],"target_id":5002},{"id":2001,"name":"role1","target":"test_class","Permissions":[{"id":1001,"role_id":2001,"name":"perm1"},{"id":1003,"role_id":2001,"name":"perm2"}],"target_id":0}],"class_managing":null,"class_taking":null,"grades":null,"created_at":"2020-08-18T13:24:25.031972138+08:00","deleted_at":"2020-08-18T13:29:24.031972138+08:00","Credentials":null}` 93 | assert.Equal(t, expected, string(j)) 94 | } 95 | -------------------------------------------------------------------------------- /database/models/problem.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/EduOJ/backend/base" 9 | "github.com/EduOJ/backend/base/log" 10 | "github.com/EduOJ/backend/database" 11 | "github.com/minio/minio-go/v7" 12 | "gorm.io/gorm" 13 | ) 14 | 15 | type TestCase struct { 16 | ID uint `gorm:"primaryKey" json:"id"` 17 | 18 | ProblemID uint `sql:"index" json:"problem_id" gorm:"not null"` 19 | Score uint `json:"score" gorm:"default:0;not null"` // 0 for 平均分配 20 | Sample bool `json:"sample" gorm:"default:false;not null"` 21 | 22 | InputFileName string `json:"input_file_name" gorm:"size:255;default:'';not null"` 23 | OutputFileName string `json:"output_file_name" gorm:"size:255;default:'';not null"` 24 | 25 | CreatedAt time.Time `json:"created_at"` 26 | UpdatedAt time.Time `json:"updated_at"` 27 | DeletedAt gorm.DeletedAt `json:"deleted_at"` 28 | } 29 | 30 | type ProblemTag struct { 31 | ID uint `gorm:"primaryKey" json:"id"` 32 | ProblemID uint `gorm:"index"` 33 | Name string 34 | CreatedAt time.Time `json:"created_at"` 35 | } 36 | 37 | type Problem struct { 38 | ID uint `gorm:"primaryKey" json:"id"` 39 | Name string `sql:"index" json:"name" gorm:"size:255;default:'';not null"` 40 | Description string `json:"description"` 41 | AttachmentFileName string `json:"attachment_file_name" gorm:"size:255;default:'';not null"` 42 | Public bool `json:"public" gorm:"default:false;not null"` 43 | Privacy bool `json:"privacy" gorm:"default:false;not null"` 44 | 45 | MemoryLimit uint64 `json:"memory_limit" gorm:"default:0;not null;type:bigint"` // Byte 46 | TimeLimit uint `json:"time_limit" gorm:"default:0;not null"` // ms 47 | LanguageAllowed database.StringArray `json:"language_allowed" gorm:"size:255;default:'';not null;type:string"` // E.g. cpp,c,java,python 48 | BuildArg string `json:"build_arg" gorm:"size:2047;default:'';not null"` // E.g. O2=false 49 | CompareScriptName string `json:"compare_script_name" gorm:"default:0;not null"` 50 | CompareScript Script `json:"compare_script"` 51 | 52 | TestCases []TestCase `json:"test_cases"` 53 | Tags []Tag `json:"tags" gorm:"OnDelete:CASCADE"` 54 | 55 | CreatedAt time.Time `json:"created_at"` 56 | UpdatedAt time.Time `json:"-"` 57 | DeletedAt gorm.DeletedAt `json:"deleted_at"` 58 | } 59 | 60 | type Tag struct { 61 | ID uint `gorm:"primaryKey" json:"id"` 62 | ProblemID uint 63 | Name string 64 | CreatedAt time.Time `json:"created_at"` 65 | } 66 | 67 | func (p Problem) GetID() uint { 68 | return p.ID 69 | } 70 | 71 | func (p Problem) TypeName() string { 72 | return "problem" 73 | } 74 | 75 | func (p *Problem) LoadTestCases() { 76 | err := base.DB.Model(p).Association("TestCases").Find(&p.TestCases) 77 | if err != nil { 78 | panic(err) 79 | } 80 | } 81 | 82 | func (p *Problem) LoadTags() { 83 | err := base.DB.Model(p).Association("Tags").Find(&p.Tags) 84 | if err != nil { 85 | panic(err) 86 | } 87 | } 88 | 89 | func (p *Problem) AfterDelete(tx *gorm.DB) (err error) { 90 | if err := tx.Where("problem_id = ?", p.ID).Delete(&Submission{}).Error; err != nil { 91 | return err 92 | } 93 | return tx.Where("problem_id = ?", p.ID).Delete(&TestCase{}).Error 94 | } 95 | 96 | func (t *TestCase) AfterDelete(tx *gorm.DB) (err error) { 97 | err = base.Storage.RemoveObject(context.Background(), "problems", fmt.Sprintf("%d/input/%d.in", t.ProblemID, t.ID), minio.RemoveObjectOptions{}) 98 | if err != nil { 99 | log.Errorf("Error occurred in TestCase afterDelete, %+v\n", err) 100 | } 101 | err = base.Storage.RemoveObject(context.Background(), "problems", fmt.Sprintf("%d/output/%d.out", t.ProblemID, t.ID), minio.RemoveObjectOptions{}) 102 | if err != nil { 103 | log.Errorf("Error occurred in TestCase afterDelete, %+v\n", err) 104 | } 105 | return tx.Where("test_case_id = ?", t.ID).Delete(&Run{}).Error 106 | } 107 | -------------------------------------------------------------------------------- /database/models/problem_set.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "encoding/json" 5 | "time" 6 | 7 | "github.com/EduOJ/backend/base" 8 | "gorm.io/datatypes" 9 | "gorm.io/gorm" 10 | ) 11 | 12 | type ProblemSet struct { 13 | ID uint `gorm:"primaryKey" json:"id"` 14 | 15 | ClassID uint `sql:"index" json:"class_id" gorm:"not null"` 16 | Class *Class `json:"class"` 17 | Name string `json:"name" gorm:"not null;size:255"` 18 | Description string `json:"description"` 19 | 20 | Problems []*Problem `json:"problems" gorm:"many2many:problems_in_problem_sets"` 21 | Grades []*Grade `json:"grades"` 22 | 23 | StartTime time.Time `json:"start_time"` 24 | EndTime time.Time `json:"end_time"` 25 | 26 | CreatedAt time.Time `json:"created_at"` 27 | UpdatedAt time.Time `json:"-"` 28 | DeletedAt gorm.DeletedAt `json:"deleted_at"` 29 | } 30 | 31 | type Grade struct { 32 | ID uint `gorm:"primaryKey" json:"id"` 33 | 34 | UserID uint `json:"user_id"` 35 | User *User `json:"user"` 36 | ProblemSetID uint `json:"problem_set_id"` 37 | ProblemSet *ProblemSet `json:"problem_set"` 38 | ClassID uint `json:"class_id"` 39 | Class *Class `json:"class"` 40 | 41 | Detail datatypes.JSON `json:"detail"` 42 | Total uint `json:"total"` 43 | 44 | CreatedAt time.Time `json:"created_at"` 45 | UpdatedAt time.Time `json:"-"` 46 | } 47 | 48 | func (p *ProblemSet) AddProblems(ids []uint) error { 49 | if len(ids) == 0 { 50 | return nil 51 | } 52 | existingIds := make([]uint, len(p.Problems)) 53 | for i, problem := range p.Problems { 54 | existingIds[i] = problem.ID 55 | } 56 | var problems []Problem 57 | query := base.DB.Preload("TestCases") 58 | if len(existingIds) != 0 { 59 | query = query.Where("id not in (?)", existingIds) 60 | } 61 | if err := query.Find(&problems, ids).Error; err != nil { 62 | return err 63 | } 64 | return base.DB.Model(p).Association("Problems").Append(&problems) 65 | } 66 | 67 | func (p *ProblemSet) DeleteProblems(ids []uint) error { 68 | if len(ids) == 0 { 69 | return nil 70 | } 71 | var problems []Problem 72 | if err := base.DB.Find(&problems, ids).Error; err != nil { 73 | return err 74 | } 75 | return base.DB.Model(p).Association("Problems").Delete(&problems) 76 | } 77 | 78 | func (g *Grade) BeforeSave(tx *gorm.DB) (err error) { 79 | detail := make(map[uint]uint) 80 | err = json.Unmarshal(g.Detail, &detail) 81 | if err != nil { 82 | return 83 | } 84 | g.Total = 0 85 | for _, score := range detail { 86 | g.Total += score 87 | } 88 | return nil 89 | } 90 | 91 | func (p *ProblemSet) AfterDelete(tx *gorm.DB) error { 92 | err := tx.Model(p).Association("Grades").Clear() 93 | if err != nil { 94 | return err 95 | } 96 | var submissions []Submission 97 | err = tx.Where("problem_set_id = ?", p.ID).Find(&submissions).Error 98 | if err != nil { 99 | return err 100 | } 101 | if len(submissions) == 0 { 102 | return nil 103 | } 104 | return tx.Delete(&submissions).Error 105 | } 106 | -------------------------------------------------------------------------------- /database/models/script.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "time" 4 | 5 | type Script struct { 6 | Name string `gorm:"primaryKey" json:"name"` 7 | Filename string `json:"file_name"` 8 | CreatedAt time.Time `json:"created_at"` 9 | UpdatedAt time.Time `json:"updated_at"` 10 | } 11 | -------------------------------------------------------------------------------- /database/models/submission.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/EduOJ/backend/base" 7 | "gorm.io/gorm" 8 | ) 9 | 10 | const PriorityDefault = uint8(127) 11 | 12 | type Submission struct { 13 | ID uint `gorm:"primaryKey" json:"id"` 14 | 15 | UserID uint `sql:"index" json:"user_id"` 16 | User *User `json:"user"` 17 | ProblemID uint `sql:"index" json:"problem_id"` 18 | Problem *Problem `json:"problem"` 19 | ProblemSetID uint `sql:"index" json:"problem_set_id"` 20 | ProblemSet *ProblemSet `json:"problem_set"` 21 | LanguageName string `json:"language_name"` 22 | Language *Language `json:"language"` 23 | FileName string `json:"file_name"` 24 | Priority uint8 `json:"priority"` 25 | 26 | Judged bool `json:"judged"` 27 | Score uint `json:"score"` 28 | 29 | /* 30 | PENDING / JUDGEMENT_FAILED / NO_COMMENT 31 | ACCEPTED / WRONG_ANSWER / RUNTIME_ERROR / TIME_LIMIT_EXCEEDED / MEMORY_LIMIT_EXCEEDED / DANGEROUS_SYSTEM_CALLS 32 | */ 33 | Status string `json:"status"` 34 | 35 | Runs []Run `json:"runs"` 36 | 37 | CreatedAt time.Time `sql:"index" json:"created_at"` 38 | UpdatedAt time.Time `json:"-"` 39 | DeletedAt gorm.DeletedAt `json:"deleted_at"` 40 | } 41 | 42 | type Run struct { 43 | ID uint `gorm:"primaryKey" json:"id"` 44 | 45 | UserID uint `sql:"index" json:"user_id"` 46 | User *User `json:"user"` 47 | ProblemID uint `sql:"index" json:"problem_id"` 48 | Problem *Problem `json:"problem"` 49 | ProblemSetID uint `sql:"index" json:"problem_set_id"` 50 | TestCaseID uint `json:"test_case_id"` 51 | TestCase *TestCase `json:"test_case"` 52 | Sample bool `json:"sample" gorm:"not null"` 53 | SubmissionID uint `json:"submission_id"` 54 | Submission *Submission `json:"submission"` 55 | Priority uint8 `json:"priority"` 56 | 57 | Judged bool `json:"judged"` 58 | JudgerName string 59 | JudgerMessage string 60 | 61 | /* 62 | PENDING / JUDGING / JUDGEMENT_FAILED / NO_COMMENT 63 | ACCEPTED / WRONG_ANSWER / RUNTIME_ERROR / TIME_LIMIT_EXCEEDED / MEMORY_LIMIT_EXCEEDED / DANGEROUS_SYSTEM_CALLS 64 | */ 65 | Status string `json:"status"` 66 | MemoryUsed uint `json:"memory_used"` // Byte 67 | TimeUsed uint `json:"time_used"` // ms 68 | OutputStrippedHash string `json:"output_stripped_hash"` 69 | 70 | CreatedAt time.Time `sql:"index" json:"created_at"` 71 | UpdatedAt time.Time `json:"-"` 72 | DeletedAt gorm.DeletedAt `json:"deleted_at"` 73 | } 74 | 75 | func (s *Submission) LoadRuns() { 76 | err := base.DB.Model(s).Association("Runs").Find(&s.Runs) 77 | if err != nil { 78 | panic(err) 79 | } 80 | } 81 | 82 | func (s *Submission) AfterDelete(tx *gorm.DB) (err error) { 83 | var runs []Run 84 | err = tx.Where("submission_id = ?", s.ID).Find(&runs).Error 85 | if err != nil { 86 | return err 87 | } 88 | if len(runs) == 0 { 89 | return nil 90 | } 91 | return tx.Delete(&runs).Error 92 | } 93 | -------------------------------------------------------------------------------- /database/models/token.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "time" 4 | 5 | type Token struct { 6 | ID uint `gorm:"primaryKey" json:"id"` 7 | Token string `gorm:"unique_index" json:"token"` 8 | UserID uint 9 | User User 10 | RememberMe bool `json:"remember_me"` 11 | CreatedAt time.Time `json:"created_at"` 12 | UpdatedAt time.Time `json:"updated_at"` 13 | } 14 | -------------------------------------------------------------------------------- /database/models/user_test.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/EduOJ/backend/base" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | type TestClass struct { 11 | ID uint 12 | } 13 | 14 | func (c TestClass) TypeName() string { 15 | return "test_class" 16 | } 17 | 18 | func (c TestClass) GetID() uint { 19 | return c.ID 20 | } 21 | 22 | func TestUser_GrantRole(t *testing.T) { 23 | t.Parallel() 24 | 25 | u := User{ 26 | Username: "test_user_grant_role", 27 | Nickname: "test_user_grant_role", 28 | Email: "test_user_grant_role", 29 | Password: "test_user_grant_role", 30 | } 31 | base.DB.Create(&u) 32 | r := Role{ 33 | Name: "ttt", 34 | } 35 | base.DB.Create(&r) 36 | u.GrantRole(r.Name) 37 | assert.Equal(t, r, u.Roles[0].Role) 38 | { 39 | dummy := "test_class" 40 | r = Role{ 41 | Name: "ttt123", 42 | Target: &dummy, 43 | } 44 | } 45 | base.DB.Create(&r) 46 | u.GrantRole(r.Name, TestClass{ID: 2}) 47 | assert.Equal(t, r, u.Roles[1].Role) 48 | assert.Equal(t, uint(2), u.Roles[1].TargetID) 49 | } 50 | 51 | func TestCan(t *testing.T) { 52 | t.Parallel() 53 | 54 | assert.NoError(t, base.DB.AutoMigrate(&TestClass{})) 55 | classA := TestClass{} 56 | classB := TestClass{} 57 | base.DB.Create(&classA) 58 | base.DB.Create(&classB) 59 | dummy := "test_class" 60 | teacher := Role{ 61 | Name: "testCanTeacher", 62 | Target: &dummy, 63 | } 64 | assistant := Role{ 65 | Name: "testCanAssistant", 66 | Target: &dummy, 67 | } 68 | admin := Role{ 69 | Name: "testCanAdmin", 70 | Target: &dummy, 71 | } 72 | globalRole := Role{ 73 | Name: "testCanGlobalRole", 74 | } 75 | globalAdmin := Role{ 76 | Name: "testCanGlobalAdmin", 77 | } 78 | base.DB.Create(&teacher) 79 | base.DB.Create(&assistant) 80 | base.DB.Create(&admin) 81 | base.DB.Create(&globalRole) 82 | base.DB.Create(&globalAdmin) 83 | if err := teacher.AddPermission("permission_teacher"); err != nil { 84 | panic(err) 85 | } 86 | if err := teacher.AddPermission("permission_both"); err != nil { 87 | panic(err) 88 | } 89 | if err := assistant.AddPermission("permission_both"); err != nil { 90 | panic(err) 91 | } 92 | if err := admin.AddPermission("all"); err != nil { 93 | panic(err) 94 | } 95 | if err := globalRole.AddPermission("global_permission"); err != nil { 96 | panic(err) 97 | } 98 | if err := globalAdmin.AddPermission("all"); err != nil { 99 | panic(err) 100 | } 101 | 102 | testUser0 := User{ 103 | Username: "test_user_0", 104 | Nickname: "tu0", 105 | Email: "tu0@e.com", 106 | Password: "", 107 | } 108 | testUser1 := User{ 109 | Username: "test_user_1", 110 | Nickname: "tu1", 111 | Email: "tu1@e.com", 112 | Password: "", 113 | } 114 | base.DB.Create(&testUser0) 115 | base.DB.Create(&testUser1) 116 | testUser0.GrantRole(teacher.Name, classA) 117 | testUser0.GrantRole(assistant.Name, classB) 118 | testUser1.GrantRole(admin.Name, classB) 119 | testUser0.GrantRole(globalRole.Name) 120 | testUser1.GrantRole(globalAdmin.Name) 121 | t.Run("scoped", func(t *testing.T) { 122 | t.Run("normal", func(t *testing.T) { 123 | thisAssert := assert.New(t) 124 | thisAssert.True(testUser0.Can("permission_teacher", classA)) 125 | thisAssert.False(testUser0.Can("permission_teacher", classB)) 126 | thisAssert.True(testUser0.Can("permission_both", classA)) 127 | thisAssert.True(testUser0.Can("permission_both", classB)) 128 | thisAssert.False(testUser0.Can("permission_both")) 129 | thisAssert.True(testUser1.Can("permission_teacher", classB)) 130 | thisAssert.True(testUser1.Can("permission_both", classB)) 131 | }) 132 | t.Run("admin", func(t *testing.T) { 133 | thisAssert := assert.New(t) 134 | thisAssert.False(testUser1.Can("all", classA)) 135 | thisAssert.True(testUser1.Can("all", classB)) 136 | thisAssert.True(testUser1.Can("permission_teacher", classB)) 137 | thisAssert.True(testUser1.Can("permission_both", classB)) 138 | thisAssert.True(testUser1.Can("permission_non_existing", classB)) 139 | }) 140 | }) 141 | t.Run("global", func(t *testing.T) { 142 | thisAssert := assert.New(t) 143 | thisAssert.True(testUser0.Can("global_permission")) 144 | thisAssert.False(testUser0.Can("non_existing_permission")) 145 | thisAssert.True(testUser1.Can("global_permission")) 146 | thisAssert.True(testUser1.Can("non_existing_permission")) 147 | }) 148 | assert.Panics(t, func() { 149 | testUser0.Can("xxx", classA, classB) 150 | }) 151 | } 152 | -------------------------------------------------------------------------------- /database/models/webauthn.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type WebauthnCredential struct { 8 | ID uint `gorm:"primaryKey" json:"id"` 9 | UserID uint 10 | Content string 11 | CreatedAt time.Time `json:"created_at"` 12 | } 13 | -------------------------------------------------------------------------------- /database/test.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/EduOJ/backend/base" 7 | "github.com/spf13/viper" 8 | "gorm.io/driver/sqlite" 9 | "gorm.io/gorm" 10 | "gorm.io/gorm/logger" 11 | ) 12 | 13 | // SetupDatabaseForTest setups a in-memory db for testing purpose. 14 | // Shouldn't be called out of test. 15 | func SetupDatabaseForTest() func() { 16 | oldDB := base.DB 17 | x, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{ 18 | Logger: logger.Default.LogMode(logger.Silent), 19 | DisableForeignKeyConstraintWhenMigrating: true, 20 | }) 21 | viper.Set("database.dialect", "sqlite") 22 | if err != nil { 23 | fmt.Print(err) 24 | panic(err) 25 | } 26 | base.DB = x 27 | sqlDB, err := base.DB.DB() 28 | if err != nil { 29 | fmt.Print(err) 30 | panic(err) 31 | } 32 | sqlDB.SetMaxOpenConns(1) 33 | Migrate() 34 | return func() { 35 | base.DB = oldDB 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /event/log/log.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import "github.com/EduOJ/backend/base/log" 4 | 5 | // EventArgs is the arguments of "log" event. 6 | // Only contains the log itself. 7 | type EventArgs = log.Log 8 | 9 | // EventRst is the result of "log" event. 10 | // Cause there is no need for result, this 11 | // is a empty struct. 12 | type EventRst struct{} 13 | -------------------------------------------------------------------------------- /event/log/log_test.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/EduOJ/backend/base/event" 8 | "github.com/EduOJ/backend/base/log" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestLogEvent(t *testing.T) { 13 | lastLog := log.Log{} 14 | event.RegisterListener("test_log_event", func(arg EventArgs) { 15 | lastLog = arg 16 | }) 17 | log := log.Log{ 18 | Level: log.DEBUG, 19 | Time: time.Now(), 20 | Message: "123", 21 | Caller: "233", 22 | } 23 | if _, err := event.FireEvent("test_log_event", log); err != nil { 24 | panic(err) 25 | } 26 | assert.Equal(t, log, lastLog) 27 | } 28 | -------------------------------------------------------------------------------- /event/register/listener.go: -------------------------------------------------------------------------------- 1 | package register 2 | 3 | import ( 4 | "bytes" 5 | 6 | "github.com/EduOJ/backend/base" 7 | "github.com/EduOJ/backend/base/log" 8 | "github.com/EduOJ/backend/base/utils" 9 | "github.com/EduOJ/backend/database/models" 10 | "github.com/spf13/viper" 11 | ) 12 | 13 | func SendVerificationEmail(user *models.User) { 14 | action := func() { 15 | verification := models.EmailVerificationToken{ 16 | User: user, 17 | Email: user.Email, 18 | Token: utils.RandStr(5), 19 | Used: false, 20 | } 21 | if err := base.DB.Save(&verification).Error; err != nil { 22 | log.Error("Error saving email verification code:", err) 23 | return 24 | } 25 | if viper.GetBool("email.inTest") { 26 | return 27 | } 28 | buf := new(bytes.Buffer) 29 | if err := base.Template.Execute(buf, map[string]string{ 30 | "Code": verification.Token, 31 | "Nickname": user.Nickname, 32 | }); err != nil { 33 | log.Errorf("%+v\n", err) 34 | return 35 | } 36 | if err := utils.SendMail(user.Email, "Your email verification code", buf.String()); err != nil { 37 | log.Errorf("%+v\n", err) 38 | return 39 | } 40 | } 41 | if viper.GetBool("email.inTest") { 42 | action() 43 | } else { 44 | go action() 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /event/run/listener.go: -------------------------------------------------------------------------------- 1 | package submission 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/EduOJ/backend/base" 8 | ) 9 | 10 | func NotifyGetSubmissionPoll(r EventArgs) EventRst { 11 | base.Redis.Publish(context.Background(), fmt.Sprintf("submission_update:%d", r.Submission.ID), nil) 12 | return EventRst{} 13 | } 14 | -------------------------------------------------------------------------------- /event/run/run.go: -------------------------------------------------------------------------------- 1 | package submission 2 | 3 | import "github.com/EduOJ/backend/database/models" 4 | 5 | // EventArgs is the arguments of "run" event. 6 | type EventArgs = *models.Run 7 | 8 | // EventRst is the result of "run" event. 9 | type EventRst struct{} 10 | -------------------------------------------------------------------------------- /event/submission/listener.go: -------------------------------------------------------------------------------- 1 | package submission 2 | 3 | import ( 4 | "github.com/EduOJ/backend/base/utils" 5 | "github.com/pkg/errors" 6 | ) 7 | 8 | func UpdateGrade(r EventArgs) EventRst { 9 | err := utils.UpdateGrade(r) 10 | return errors.Wrap(err, "could not update grade") 11 | } 12 | -------------------------------------------------------------------------------- /event/submission/submission.go: -------------------------------------------------------------------------------- 1 | package submission 2 | 3 | import "github.com/EduOJ/backend/database/models" 4 | 5 | // EventArgs is the arguments of "submission" event. 6 | type EventArgs = *models.Submission 7 | 8 | // EventRst is the result of "submission" event. 9 | type EventRst error 10 | -------------------------------------------------------------------------------- /flag.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/EduOJ/backend/base/log" 7 | "github.com/jessevdk/go-flags" 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | type _opt struct { 12 | Config string `short:"c" long:"verbose" description:"Config file name" default:"config.yml"` 13 | } 14 | 15 | var parser *flags.Parser 16 | var opt _opt 17 | var args []string 18 | 19 | func init() { 20 | parser = flags.NewNamedParser("eduOJ server", flags.HelpFlag|flags.PassDoubleDash) 21 | _, _ = parser.AddGroup("Application", "Application Options", &opt) 22 | } 23 | 24 | func parse() { 25 | var err error 26 | // TODO: remove useless logs for parser debugging. 27 | log.Debug("Parsing command-line arguments.") 28 | args, err = parser.Parse() 29 | if err != nil { 30 | log.Fatal(errors.Wrap(err, "could not parse argument ")) 31 | os.Exit(-1) 32 | } 33 | log.Debug(args, err) 34 | log.Debug(opt) 35 | } 36 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/EduOJ/backend 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/fatih/color v1.10.0 7 | github.com/gabriel-vasile/mimetype v1.1.2 8 | github.com/go-gormigrate/gormigrate/v2 v2.0.0 9 | github.com/go-mail/mail v2.3.1+incompatible 10 | github.com/go-playground/locales v0.13.0 11 | github.com/go-playground/universal-translator v0.17.0 12 | github.com/go-playground/validator/v10 v10.4.1 13 | github.com/go-redis/redis/v8 v8.4.11 14 | github.com/go-webauthn/webauthn v0.8.2 15 | github.com/jackc/pgproto3/v2 v2.0.7 // indirect 16 | github.com/jessevdk/go-flags v1.4.0 17 | github.com/johannesboyne/gofakes3 v0.0.0-20210124080349-901cf567bf01 18 | github.com/kami-zh/go-capturer v0.0.0-20171211120116-e492ea43421d 19 | github.com/labstack/echo/v4 v4.9.0 20 | github.com/labstack/gommon v0.3.1 21 | github.com/leodido/go-urn v1.2.1 // indirect 22 | github.com/lib/pq v1.7.1 // indirect 23 | github.com/mattn/go-sqlite3 v1.14.6 // indirect 24 | github.com/minio/md5-simd v1.1.1 // indirect 25 | github.com/minio/minio-go/v7 v7.0.8 26 | github.com/patrickmn/go-cache v2.1.0+incompatible 27 | github.com/pkg/errors v0.9.1 28 | github.com/spf13/viper v1.7.1 29 | github.com/stretchr/testify v1.8.1 30 | github.com/swaggo/echo-swagger v1.3.0 31 | github.com/swaggo/swag v1.8.0 32 | github.com/xlab/treeprint v1.0.0 33 | golang.org/x/crypto v0.6.0 34 | golang.org/x/tools v0.7.0 // indirect 35 | gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect 36 | gopkg.in/ini.v1 v1.62.0 // indirect 37 | gopkg.in/mail.v2 v2.3.1 // indirect 38 | gorm.io/datatypes v1.0.0 39 | gorm.io/driver/mysql v1.0.4 40 | gorm.io/driver/postgres v1.0.8 41 | gorm.io/driver/sqlite v1.1.4 42 | gorm.io/gorm v1.20.12 43 | ) 44 | 45 | replace github.com/stretchr/testify v1.8.1 => github.com/leoleoasd/testify v1.6.2-0.20220217095700-4ed8551c7e3c 46 | 47 | replace github.com/johannesboyne/gofakes3 => github.com/leoleoasd/gofakes3 v0.0.0-20210203155129-abef9ae90e02 48 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // @title EduOJ Backend 2 | // @version 0.1.0 3 | // @description The backend module for the EduOJ project. 4 | // @BasePath /api 5 | // @securityDefinitions.apikey ApiKeyAuth 6 | // @in header 7 | // @name Authorization 8 | package main 9 | 10 | import ( 11 | "os" 12 | 13 | "github.com/EduOJ/backend/base/log" 14 | _ "github.com/EduOJ/backend/docs" 15 | ) 16 | 17 | func main() { 18 | parse() 19 | if len(args) < 1 { 20 | log.Fatal("Please specific a command to run.") 21 | // TODO: output usage 22 | os.Exit(-1) 23 | } 24 | switch args[0] { 25 | case "test-config": 26 | testConfig() 27 | case "serve", "server", "http", "run": 28 | serve() 29 | case "migrate", "migration": 30 | doMigrate() 31 | case "clean", "clean-up", "clean-db": 32 | clean() 33 | case "permission", "perm": 34 | permission() 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /migrate.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/EduOJ/backend/base/log" 7 | "github.com/EduOJ/backend/database" 8 | ) 9 | 10 | func doMigrate() { 11 | 12 | if len(args) == 1 { 13 | readConfig() 14 | initGorm() 15 | log.Fatal("Migrate succeed!") 16 | } else { 17 | switch args[1] { 18 | case "help": 19 | fmt.Println(`Usage: 20 | ./backend migrate 21 | command: The command to run. default: migrate 22 | Commands: 23 | migrate: run migrations. 24 | rollback: rollback all migrations. 25 | rollback_last: rollback last migration.`) 26 | case "migrate": 27 | readConfig() 28 | initGorm() 29 | log.Fatal("Migrate succeed!") 30 | case "rollback": 31 | readConfig() 32 | initGorm(false) 33 | m := database.GetMigration() 34 | err := m.RollbackTo("start") 35 | if err != nil { 36 | log.Error(err) 37 | } else { 38 | log.Fatal("Migrate succeed!") 39 | } 40 | case "rollback_last": 41 | readConfig() 42 | initGorm(false) 43 | m := database.GetMigration() 44 | err := m.RollbackLast() 45 | if err != nil { 46 | log.Error(err) 47 | } else { 48 | log.Fatal("Migrate succeed!") 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | # EduOJ 5 | 6 | [![codecov](https://codecov.io/gh/EduOJ/backend/branch/master/graph/badge.svg?token=5A6UTFJL0J)](https://codecov.io/gh/EduOJ/backend) 7 | [![Go Report Card](https://goreportcard.com/badge/github.com/eduoj/backend)](https://goreportcard.com/report/github.com/eduoj/backend) 8 | [![Test](https://github.com/EduOJ/backend/actions/workflows/%20test.yml/badge.svg)](https://github.com/EduOJ/backend/actions/workflows/%20test.yml) 9 |
10 | 11 | # License. 12 | 13 | This project is licensed under 14 | [GNU AFFERO GENERAL PUBLIC LICENSE Version 3](./license.md). 15 | 16 | # Contribution 17 | 18 | Our document is still under construction. 19 | 20 | # Code style. 21 | 22 | All code must be formatted by go fmt. 23 | 24 | All tests of app/controllers is running with the same in-memory 25 | database, so they shouldn't rely on a clean database and shouldn't 26 | cleanup after runs. Also, they should be running under parallel mode. 27 | 28 | All other tests should make it own database and clean it up after 29 | running. 30 | 31 | # Roles 32 | 33 | | Name | Target | Permission | 34 | |:---------------:|:-------:|:----------:| 35 | | admin | N/A | all | 36 | | problem_creator | problem | all | 37 | | class_creator | class | all | 38 | 39 | # Permissions 40 | 41 | Here are the permissions and their descriptions. 42 | 43 | | Name | Description | 44 | |:--------------------:|:-----------------------------------------------------------------------------------------------------------:| 45 | | read_user | the permission to read users | 46 | | manage_user | the permission to manage users | 47 | | manage_problem | 无实际意义,但是有可能有update_problem / delete_problem权限的人必须拥有此全局权限。 | 48 | | create_problem | create problem | 49 | | read_submission | read submission of a certain problem. unscoped can read all problems. | 50 | | update_problem | update problem. A scoped update_problem can only update selected problem. unscoped can update all problems. | 51 | | delete_problem | delete a problem. same as above. | 52 | | read_problem_secrets | read sensitive information such as test case. | 53 | | read_logs | read logs. | 54 | | read_class_secrets | read sensitive information such as invite code | 55 | | manage_class | the permission to manage a class or all classes | 56 | | manage_students | the permission to manage students of a class or all classes | 57 | | manage_problem_sets | the permission to manage problem sets of a class or all classes | 58 | | clone_problem_sets | the permission to clone problem sets of a class or all classes | 59 | | read_answers | read submissions in a class | 60 | # Buckets: 61 | ## images: 62 | images with their "path" as filename. 63 | ## problems 64 | ``` 65 | problems 66 | └── problemID 67 | ├── attachment 68 | ├── input 69 | │ └── testcase_id.in 70 | └── output 71 | └── testcase_id.out 72 | ``` 73 | ## scripts 74 | ``` 75 | scripts 76 | └── script_name 77 | ``` 78 | ## submissions 79 | ``` 80 | submissions 81 | └── submissionID 82 | ├── run 83 | | └── runID 84 | | ├── output 85 | | ├── compiler_output 86 | | └── comparer_output 87 | └── code 88 | ``` 89 | -------------------------------------------------------------------------------- /serve.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "os/signal" 6 | "syscall" 7 | 8 | "github.com/EduOJ/backend/base/exit" 9 | "github.com/EduOJ/backend/base/log" 10 | ) 11 | 12 | func serve() { 13 | readConfig() 14 | initGorm() 15 | initLog() 16 | initRedis() 17 | initStorage() 18 | initWebAuthn() 19 | initMail() 20 | initEvent() 21 | startEcho() 22 | s := make(chan os.Signal, 1) 23 | signal.Notify(s, syscall.SIGHUP, 24 | syscall.SIGINT, 25 | syscall.SIGTERM, 26 | syscall.SIGQUIT) 27 | 28 | <-s 29 | 30 | go func() { 31 | <-s 32 | log.Fatal("Force quitting") 33 | os.Exit(-1) 34 | }() 35 | 36 | log.Fatal("Server closing.") 37 | log.Fatal("Hit ctrl+C again to force quit.") 38 | exit.Close() 39 | exit.QuitWG.Wait() 40 | } 41 | -------------------------------------------------------------------------------- /test_config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/EduOJ/backend/base/log" 5 | ) 6 | 7 | func testConfig() { 8 | // TODO: test config using config.Get 9 | readConfig() 10 | initGorm(false) 11 | initLog() 12 | initRedis() 13 | initStorage() 14 | initWebAuthn() 15 | log.Fatalf("should success.") 16 | } 17 | --------------------------------------------------------------------------------