From 714f50c003fb59c51561b15c48750973cf98a735 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=9F=83=E6=8B=89?= Date: Sat, 27 Jul 2024 13:22:17 +0800 Subject: [PATCH] remake: rust --- .gitattributes | 3 - .github/workflows/docker.yml | 108 +- .gitignore | 36 +- CHANGELOG.md | 1 + Cargo.toml | 65 + Dockerfile | 37 +- Makefile | 37 - README.md | 14 +- SECURITY.md | 1 - api/docs.go | 3038 ----------------- api/swagger.json | 3011 ---------------- api/swagger.yaml | 1948 ----------- assets/favicon.webp | Bin 0 -> 13476 bytes cmd/cloudsdale/main.go | 12 - deploy/docker-compose.yml | 27 - deploys/docker-compose.yml | 27 + docs/.gitattributes | 2 - docs/.gitignore | 6 + docs/.vitepress/config/en.mts | 17 + docs/.vitepress/config/index.mts | 32 + docs/.vitepress/config/zh.mts | 28 + docs/.vitepress/theme/index.ts | 6 + docs/.vitepress/theme/style.css | 151 + docs/en/guide/index.md | 32 + docs/en/index.md | 34 + docs/mkdocs.yml | 50 - docs/package.json | 17 +- docs/pages/assets/favicon.ico | Bin 18390 -> 0 bytes docs/pages/assets/img/GPLv3_Logo.svg | 22 - docs/pages/challenge/index.md | 3 - docs/pages/config/cache.md | 3 - docs/pages/config/database.md | 3 - docs/pages/config/index.md | 3 - docs/pages/config/proxy/index.md | 13 - docs/pages/deploy/docker-k8s.md | 1 - docs/pages/deploy/docker.md | 75 - docs/pages/deploy/img/1.png | Bin 52750 -> 0 bytes docs/pages/deploy/k8s.md | 1 - docs/pages/faq/index.md | 1 - docs/pages/game/index.md | 9 - docs/pages/index.md | 37 - docs/pages/stylesheets/extra.css | 5 - docs/public/favicon.webp | Bin 0 -> 13476 bytes docs/requirements.txt | 3 - docs/zh/guide/index.md | 32 + docs/zh/index.md | 34 + example/application.yml | 90 + go.mod | 154 - go.sum | 525 --- internal/app/app.go | 123 - internal/app/config/application.go | 179 - internal/app/config/config.go | 24 - internal/app/config/platform.go | 98 - internal/app/db/db.go | 178 - internal/app/logger/adapter/gin.go | 96 - internal/app/logger/adapter/gorm.go | 139 - internal/app/logger/encoder.go | 35 - internal/app/logger/logger.go | 89 - internal/controller/category.go | 165 - internal/controller/challenge.go | 319 -- internal/controller/config.go | 95 - internal/controller/controller.go | 52 - internal/controller/game.go | 638 ---- internal/controller/media.go | 35 - internal/controller/pod.go | 157 - internal/controller/proxy.go | 48 - internal/controller/submission.go | 139 - internal/controller/team.go | 395 --- internal/controller/user.go | 325 -- internal/extension/broadcast/broadcast.go | 12 - internal/extension/broadcast/game.go | 82 - internal/extension/cache/cache.go | 34 - internal/extension/cache/memory.go | 44 - internal/extension/cache/redis.go | 97 - internal/extension/captcha/captcha.go | 19 - internal/extension/captcha/recaptcha.go | 53 - internal/extension/captcha/turnstile.go | 50 - internal/extension/casbin/casbin.go | 32 - internal/extension/casbin/policy.go | 68 - .../extension/container/manager/docker.go | 232 -- internal/extension/container/manager/k8s.go | 233 -- .../extension/container/manager/manager.go | 27 - .../extension/container/provider/docker.go | 48 - internal/extension/container/provider/k8s.go | 44 - .../extension/container/provider/provider.go | 17 - internal/extension/extensions.go | 1 - internal/extension/proxy/proxy.go | 11 - internal/extension/proxy/ws.go | 196 -- internal/extension/webhook/payload.go | 8 - internal/extension/webhook/webhook.go | 17 - internal/files/configs/application.json | 84 - internal/files/configs/casbin.conf | 14 - internal/files/configs/platform.json | 23 - internal/files/files.go | 35 - internal/files/i18n/en.yaml | 16 - internal/files/i18n/zh-Hans.yaml | 16 - internal/files/templates/email/captcha.html | 3 - internal/middleware/casbin.go | 62 - internal/middleware/frontend.go | 58 - internal/model/article.go | 11 - internal/model/category.go | 25 - internal/model/challenge.go | 88 - internal/model/env.go | 9 - internal/model/file.go | 8 - internal/model/flag.go | 13 - internal/model/game.go | 60 - internal/model/game_challenge.go | 21 - internal/model/game_team.go | 11 - internal/model/nat.go | 11 - internal/model/notice.go | 38 - internal/model/pod.go | 31 - internal/model/port.go | 11 - internal/model/request/category.go | 25 - internal/model/request/challenge.go | 63 - internal/model/request/config.go | 7 - internal/model/request/flag.go | 22 - internal/model/request/game.go | 52 - internal/model/request/game_challenge.go | 31 - internal/model/request/game_team.go | 24 - internal/model/request/notice.go | 29 - internal/model/request/pod.go | 33 - internal/model/request/port.go | 6 - internal/model/request/submission.go | 26 - internal/model/request/team.go | 43 - internal/model/request/team_user.go | 18 - internal/model/request/user.go | 49 - internal/model/request/webhook.go | 22 - internal/model/submission.go | 25 - internal/model/team.go | 78 - internal/model/user.go | 54 - internal/model/user_team.go | 7 - internal/model/webhook.go | 11 - internal/repository/category.go | 54 - internal/repository/challenge.go | 101 - internal/repository/env.go | 24 - internal/repository/flag.go | 35 - internal/repository/game.go | 68 - internal/repository/game_challenge.go | 91 - internal/repository/game_team.go | 65 - internal/repository/nat.go | 23 - internal/repository/notice.go | 69 - internal/repository/pod.go | 79 - internal/repository/port.go | 35 - internal/repository/repository.go | 64 - internal/repository/submission.go | 87 - internal/repository/team.go | 92 - internal/repository/user.go | 97 - internal/repository/user_team.go | 32 - internal/repository/webhook.go | 35 - internal/router/category.go | 29 - internal/router/challenge.go | 54 - internal/router/config.go | 28 - internal/router/game.go | 55 - internal/router/media.go | 26 - internal/router/pod.go | 29 - internal/router/proxy.go | 26 - internal/router/router.go | 44 - internal/router/submission.go | 44 - internal/router/team.go | 56 - internal/router/user.go | 51 - internal/service/auth.go | 42 - internal/service/category.go | 47 - internal/service/challenge.go | 83 - internal/service/config.go | 30 - internal/service/flag.go | 50 - internal/service/game.go | 76 - internal/service/game_challenge.go | 95 - internal/service/game_team.go | 128 - internal/service/media.go | 134 - internal/service/notice.go | 58 - internal/service/pod.go | 231 -- internal/service/service.go | 62 - internal/service/submission.go | 243 -- internal/service/team.go | 117 - internal/service/user.go | 152 - internal/service/user_team.go | 79 - internal/service/webhook.go | 36 - internal/utils/calculate/calculate.go | 29 - internal/utils/const.go | 21 - internal/utils/convertor/convertor.go | 96 - internal/utils/utils.go | 25 - internal/utils/validator/validator.go | 27 - main.go | 1 - src/captcha/mod.rs | 22 + src/captcha/recaptcha.rs | 79 + src/captcha/traits.rs | 8 + src/captcha/turnstile.rs | 59 + src/config/auth/jwt.rs | 7 + src/config/auth/mod.rs | 10 + src/config/auth/registration/email.rs | 7 + src/config/auth/registration/mod.rs | 10 + src/config/axum/cors.rs | 7 + src/config/axum/mod.rs | 10 + src/config/cache/mod.rs | 9 + src/config/cache/redis.rs | 9 + src/config/captcha/mod.rs | 11 + src/config/captcha/recaptcha.rs | 9 + src/config/captcha/turnstile.rs | 8 + src/config/consts.rs | 6 + src/config/container/docker.rs | 6 + src/config/container/k8s.rs | 7 + src/config/container/mod.rs | 16 + src/config/container/proxy.rs | 7 + src/config/container/strategy.rs | 7 + src/config/db/mod.rs | 13 + src/config/db/mysql.rs | 10 + src/config/db/postgres.rs | 11 + src/config/db/sqlite.rs | 6 + src/config/mod.rs | 42 + src/config/site/mod.rs | 9 + src/container/docker.rs | 162 + src/container/k8s.rs | 159 + src/container/mod.rs | 37 + src/container/traits.rs | 14 + src/database/migration.rs | 29 + src/database/mod.rs | 136 + src/email/mod.rs | 0 src/logger/mod.rs | 27 + .../files/statics/banner.txt => src/main.rs | 25 + src/media/mod.rs | 65 + src/model/category/mod.rs | 39 + src/model/category/request.rs | 49 + src/model/challenge/mod.rs | 124 + src/model/challenge/request.rs | 106 + src/model/challenge/response.rs | 10 + src/model/game/mod.rs | 91 + src/model/game/request.rs | 114 + src/model/game_challenge/mod.rs | 65 + src/model/game_challenge/request.rs | 69 + src/model/game_team/mod.rs | 49 + src/model/game_team/request.rs | 53 + src/model/mod.rs | 10 + src/model/pod/mod.rs | 105 + src/model/pod/request.rs | 49 + src/model/submission/mod.rs | 125 + src/model/submission/request.rs | 70 + src/model/team/mod.rs | 92 + src/model/team/request.rs | 68 + src/model/user/mod.rs | 79 + src/model/user/request.rs | 109 + src/model/user_team/mod.rs | 42 + src/model/user_team/request.rs | 37 + src/proxy/mod.rs | 1 + src/repository/category.rs | 45 + src/repository/challenge.rs | 84 + src/repository/game.rs | 65 + src/repository/game_challenge.rs | 71 + src/repository/game_team.rs | 70 + src/repository/mod.rs | 10 + src/repository/pod.rs | 112 + src/repository/submission.rs | 120 + src/repository/team.rs | 106 + src/repository/user.rs | 101 + src/repository/user_team.rs | 48 + src/server/controller/category.rs | 119 + src/server/controller/challenge.rs | 243 ++ src/server/controller/config.rs | 54 + src/server/controller/game.rs | 411 +++ src/server/controller/media.rs | 22 + src/server/controller/mod.rs | 9 + src/server/controller/pod.rs | 163 + src/server/controller/submission.rs | 113 + src/server/controller/team.rs | 400 +++ src/server/controller/user.rs | 383 +++ src/server/middleware/auth.rs | 95 + src/server/middleware/mod.rs | 1 + src/server/mod.rs | 59 + src/server/router/category.rs | 25 + src/server/router/challenge.rs | 52 + src/server/router/config.rs | 9 + src/server/router/game.rs | 88 + src/server/router/media.rs | 7 + src/server/router/mod.rs | 36 + src/server/router/pod.rs | 28 + src/server/router/proxy.rs | 0 src/server/router/submission.rs | 24 + src/server/router/team.rs | 68 + src/server/router/user.rs | 46 + src/server/service/category.rs | 37 + src/server/service/challenge.rs | 119 + src/server/service/game.rs | 37 + src/server/service/game_challenge.rs | 49 + src/server/service/game_team.rs | 54 + src/server/service/mod.rs | 10 + src/server/service/pod.rs | 134 + src/server/service/submission.rs | 131 + src/server/service/team.rs | 79 + src/server/service/user.rs | 113 + src/server/service/user_team.rs | 54 + src/traits.rs | 9 + src/util/jwt.rs | 64 + src/util/math.rs | 14 + src/util/mod.rs | 3 + src/util/validate.rs | 62 + web/src/App.tsx | 33 +- web/src/api/category.ts | 4 +- web/src/api/challenge.ts | 32 +- web/src/api/config.ts | 13 +- web/src/api/game.ts | 11 +- web/src/api/pod.ts | 4 +- web/src/api/submission.ts | 4 +- web/src/api/team.ts | 9 +- web/src/api/user.ts | 14 +- web/src/components/modals/ChallengeModal.tsx | 57 +- .../components/modals/GameTeamApplyModal.tsx | 2 +- web/src/components/modals/TeamEditModal.tsx | 41 +- web/src/components/modals/TeamJoinModal.tsx | 2 +- .../modals/admin/ChallengeFlagCreateModal.tsx | 37 +- .../modals/admin/GameTeamCreateModal.tsx | 2 +- .../components/modals/admin/TeamEditModal.tsx | 8 +- .../modals/admin/TeamSelectModal.tsx | 2 +- .../components/modals/admin/UserEditModal.tsx | 2 +- .../modals/admin/UserSelectModal.tsx | 2 +- web/src/components/navigations/Navbar.tsx | 14 +- web/src/components/widgets/ChallengeCard.tsx | 79 +- web/src/components/widgets/GameCard.tsx | 2 +- web/src/components/widgets/TeamCard.tsx | 4 +- .../widgets/admin/ChallengeFlagAccordion.tsx | 45 +- web/src/pages/admin/challenges/[id]/flags.tsx | 106 +- .../pages/admin/challenges/[id]/images.tsx | 26 +- web/src/pages/admin/challenges/[id]/index.tsx | 30 +- .../admin/challenges/[id]/submissions.tsx | 4 +- web/src/pages/admin/challenges/index.tsx | 50 +- web/src/pages/admin/games/[id]/index.tsx | 15 +- .../pages/admin/games/[id]/submissions.tsx | 4 +- web/src/pages/admin/games/[id]/teams.tsx | 4 +- web/src/pages/admin/games/index.tsx | 15 +- web/src/pages/admin/global/index.tsx | 156 - web/src/pages/admin/teams/index.tsx | 4 +- web/src/pages/admin/users/index.tsx | 2 +- web/src/pages/challenges/index.tsx | 32 +- web/src/pages/games/[id]/challenges.tsx | 10 +- web/src/pages/games/[id]/index.tsx | 2 +- web/src/pages/games/[id]/scoreboard.tsx | 9 +- web/src/pages/profile.tsx | 13 +- web/src/stores/category.ts | 5 +- web/src/types/category.ts | 3 - web/src/types/challenge.ts | 28 +- web/src/types/config.ts | 36 +- web/src/types/env.ts | 2 - web/src/types/file.ts | 4 - web/src/types/flag.ts | 24 - web/src/types/game.ts | 4 - web/src/types/game_challenge.ts | 1 + web/src/types/media.ts | 4 + web/src/types/nat.ts | 6 +- web/src/types/port.ts | 6 +- web/src/types/submission.ts | 7 +- web/src/types/team.ts | 9 +- web/src/types/user.ts | 2 - 350 files changed, 7999 insertions(+), 18954 deletions(-) delete mode 100644 .gitattributes create mode 100644 CHANGELOG.md create mode 100644 Cargo.toml delete mode 100644 Makefile delete mode 100644 SECURITY.md delete mode 100644 api/docs.go delete mode 100644 api/swagger.json delete mode 100644 api/swagger.yaml create mode 100644 assets/favicon.webp delete mode 100644 cmd/cloudsdale/main.go delete mode 100644 deploy/docker-compose.yml create mode 100644 deploys/docker-compose.yml delete mode 100644 docs/.gitattributes create mode 100644 docs/.gitignore create mode 100644 docs/.vitepress/config/en.mts create mode 100644 docs/.vitepress/config/index.mts create mode 100644 docs/.vitepress/config/zh.mts create mode 100644 docs/.vitepress/theme/index.ts create mode 100644 docs/.vitepress/theme/style.css create mode 100644 docs/en/guide/index.md create mode 100644 docs/en/index.md delete mode 100644 docs/mkdocs.yml delete mode 100644 docs/pages/assets/favicon.ico delete mode 100644 docs/pages/assets/img/GPLv3_Logo.svg delete mode 100644 docs/pages/challenge/index.md delete mode 100644 docs/pages/config/cache.md delete mode 100644 docs/pages/config/database.md delete mode 100644 docs/pages/config/index.md delete mode 100644 docs/pages/config/proxy/index.md delete mode 100644 docs/pages/deploy/docker-k8s.md delete mode 100644 docs/pages/deploy/docker.md delete mode 100644 docs/pages/deploy/img/1.png delete mode 100644 docs/pages/deploy/k8s.md delete mode 100644 docs/pages/faq/index.md delete mode 100644 docs/pages/game/index.md delete mode 100644 docs/pages/index.md delete mode 100644 docs/pages/stylesheets/extra.css create mode 100644 docs/public/favicon.webp delete mode 100644 docs/requirements.txt create mode 100644 docs/zh/guide/index.md create mode 100644 docs/zh/index.md create mode 100644 example/application.yml delete mode 100644 go.mod delete mode 100644 go.sum delete mode 100644 internal/app/app.go delete mode 100644 internal/app/config/application.go delete mode 100644 internal/app/config/config.go delete mode 100644 internal/app/config/platform.go delete mode 100644 internal/app/db/db.go delete mode 100644 internal/app/logger/adapter/gin.go delete mode 100644 internal/app/logger/adapter/gorm.go delete mode 100644 internal/app/logger/encoder.go delete mode 100644 internal/app/logger/logger.go delete mode 100644 internal/controller/category.go delete mode 100644 internal/controller/challenge.go delete mode 100644 internal/controller/config.go delete mode 100644 internal/controller/controller.go delete mode 100644 internal/controller/game.go delete mode 100644 internal/controller/media.go delete mode 100644 internal/controller/pod.go delete mode 100644 internal/controller/proxy.go delete mode 100644 internal/controller/submission.go delete mode 100644 internal/controller/team.go delete mode 100644 internal/controller/user.go delete mode 100644 internal/extension/broadcast/broadcast.go delete mode 100644 internal/extension/broadcast/game.go delete mode 100644 internal/extension/cache/cache.go delete mode 100644 internal/extension/cache/memory.go delete mode 100644 internal/extension/cache/redis.go delete mode 100644 internal/extension/captcha/captcha.go delete mode 100644 internal/extension/captcha/recaptcha.go delete mode 100644 internal/extension/captcha/turnstile.go delete mode 100644 internal/extension/casbin/casbin.go delete mode 100644 internal/extension/casbin/policy.go delete mode 100644 internal/extension/container/manager/docker.go delete mode 100644 internal/extension/container/manager/k8s.go delete mode 100644 internal/extension/container/manager/manager.go delete mode 100644 internal/extension/container/provider/docker.go delete mode 100644 internal/extension/container/provider/k8s.go delete mode 100644 internal/extension/container/provider/provider.go delete mode 100644 internal/extension/extensions.go delete mode 100644 internal/extension/proxy/proxy.go delete mode 100644 internal/extension/proxy/ws.go delete mode 100644 internal/extension/webhook/payload.go delete mode 100644 internal/extension/webhook/webhook.go delete mode 100644 internal/files/configs/application.json delete mode 100644 internal/files/configs/casbin.conf delete mode 100644 internal/files/configs/platform.json delete mode 100644 internal/files/files.go delete mode 100644 internal/files/i18n/en.yaml delete mode 100644 internal/files/i18n/zh-Hans.yaml delete mode 100644 internal/files/templates/email/captcha.html delete mode 100644 internal/middleware/casbin.go delete mode 100644 internal/middleware/frontend.go delete mode 100644 internal/model/article.go delete mode 100644 internal/model/category.go delete mode 100644 internal/model/challenge.go delete mode 100644 internal/model/env.go delete mode 100644 internal/model/file.go delete mode 100644 internal/model/flag.go delete mode 100644 internal/model/game.go delete mode 100644 internal/model/game_challenge.go delete mode 100644 internal/model/game_team.go delete mode 100644 internal/model/nat.go delete mode 100644 internal/model/notice.go delete mode 100644 internal/model/pod.go delete mode 100644 internal/model/port.go delete mode 100644 internal/model/request/category.go delete mode 100644 internal/model/request/challenge.go delete mode 100644 internal/model/request/config.go delete mode 100644 internal/model/request/flag.go delete mode 100644 internal/model/request/game.go delete mode 100644 internal/model/request/game_challenge.go delete mode 100644 internal/model/request/game_team.go delete mode 100644 internal/model/request/notice.go delete mode 100644 internal/model/request/pod.go delete mode 100644 internal/model/request/port.go delete mode 100644 internal/model/request/submission.go delete mode 100644 internal/model/request/team.go delete mode 100644 internal/model/request/team_user.go delete mode 100644 internal/model/request/user.go delete mode 100644 internal/model/request/webhook.go delete mode 100644 internal/model/submission.go delete mode 100644 internal/model/team.go delete mode 100644 internal/model/user.go delete mode 100644 internal/model/user_team.go delete mode 100644 internal/model/webhook.go delete mode 100644 internal/repository/category.go delete mode 100644 internal/repository/challenge.go delete mode 100644 internal/repository/env.go delete mode 100644 internal/repository/flag.go delete mode 100644 internal/repository/game.go delete mode 100644 internal/repository/game_challenge.go delete mode 100644 internal/repository/game_team.go delete mode 100644 internal/repository/nat.go delete mode 100644 internal/repository/notice.go delete mode 100644 internal/repository/pod.go delete mode 100644 internal/repository/port.go delete mode 100644 internal/repository/repository.go delete mode 100644 internal/repository/submission.go delete mode 100644 internal/repository/team.go delete mode 100644 internal/repository/user.go delete mode 100644 internal/repository/user_team.go delete mode 100644 internal/repository/webhook.go delete mode 100644 internal/router/category.go delete mode 100644 internal/router/challenge.go delete mode 100644 internal/router/config.go delete mode 100644 internal/router/game.go delete mode 100644 internal/router/media.go delete mode 100644 internal/router/pod.go delete mode 100644 internal/router/proxy.go delete mode 100644 internal/router/router.go delete mode 100644 internal/router/submission.go delete mode 100644 internal/router/team.go delete mode 100644 internal/router/user.go delete mode 100644 internal/service/auth.go delete mode 100644 internal/service/category.go delete mode 100644 internal/service/challenge.go delete mode 100644 internal/service/config.go delete mode 100644 internal/service/flag.go delete mode 100644 internal/service/game.go delete mode 100644 internal/service/game_challenge.go delete mode 100644 internal/service/game_team.go delete mode 100644 internal/service/media.go delete mode 100644 internal/service/notice.go delete mode 100644 internal/service/pod.go delete mode 100644 internal/service/service.go delete mode 100644 internal/service/submission.go delete mode 100644 internal/service/team.go delete mode 100644 internal/service/user.go delete mode 100644 internal/service/user_team.go delete mode 100644 internal/service/webhook.go delete mode 100644 internal/utils/calculate/calculate.go delete mode 100644 internal/utils/const.go delete mode 100644 internal/utils/convertor/convertor.go delete mode 100644 internal/utils/utils.go delete mode 100644 internal/utils/validator/validator.go delete mode 100644 main.go create mode 100644 src/captcha/mod.rs create mode 100644 src/captcha/recaptcha.rs create mode 100644 src/captcha/traits.rs create mode 100644 src/captcha/turnstile.rs create mode 100644 src/config/auth/jwt.rs create mode 100644 src/config/auth/mod.rs create mode 100644 src/config/auth/registration/email.rs create mode 100644 src/config/auth/registration/mod.rs create mode 100644 src/config/axum/cors.rs create mode 100644 src/config/axum/mod.rs create mode 100644 src/config/cache/mod.rs create mode 100644 src/config/cache/redis.rs create mode 100644 src/config/captcha/mod.rs create mode 100644 src/config/captcha/recaptcha.rs create mode 100644 src/config/captcha/turnstile.rs create mode 100644 src/config/consts.rs create mode 100644 src/config/container/docker.rs create mode 100644 src/config/container/k8s.rs create mode 100644 src/config/container/mod.rs create mode 100644 src/config/container/proxy.rs create mode 100644 src/config/container/strategy.rs create mode 100644 src/config/db/mod.rs create mode 100644 src/config/db/mysql.rs create mode 100644 src/config/db/postgres.rs create mode 100644 src/config/db/sqlite.rs create mode 100644 src/config/mod.rs create mode 100644 src/config/site/mod.rs create mode 100644 src/container/docker.rs create mode 100644 src/container/k8s.rs create mode 100644 src/container/mod.rs create mode 100644 src/container/traits.rs create mode 100644 src/database/migration.rs create mode 100644 src/database/mod.rs create mode 100644 src/email/mod.rs create mode 100644 src/logger/mod.rs rename internal/files/statics/banner.txt => src/main.rs (50%) create mode 100644 src/media/mod.rs create mode 100644 src/model/category/mod.rs create mode 100644 src/model/category/request.rs create mode 100644 src/model/challenge/mod.rs create mode 100644 src/model/challenge/request.rs create mode 100644 src/model/challenge/response.rs create mode 100644 src/model/game/mod.rs create mode 100644 src/model/game/request.rs create mode 100644 src/model/game_challenge/mod.rs create mode 100644 src/model/game_challenge/request.rs create mode 100644 src/model/game_team/mod.rs create mode 100644 src/model/game_team/request.rs create mode 100644 src/model/mod.rs create mode 100644 src/model/pod/mod.rs create mode 100644 src/model/pod/request.rs create mode 100644 src/model/submission/mod.rs create mode 100644 src/model/submission/request.rs create mode 100644 src/model/team/mod.rs create mode 100644 src/model/team/request.rs create mode 100644 src/model/user/mod.rs create mode 100644 src/model/user/request.rs create mode 100644 src/model/user_team/mod.rs create mode 100644 src/model/user_team/request.rs create mode 100644 src/proxy/mod.rs create mode 100644 src/repository/category.rs create mode 100644 src/repository/challenge.rs create mode 100644 src/repository/game.rs create mode 100644 src/repository/game_challenge.rs create mode 100644 src/repository/game_team.rs create mode 100644 src/repository/mod.rs create mode 100644 src/repository/pod.rs create mode 100644 src/repository/submission.rs create mode 100644 src/repository/team.rs create mode 100644 src/repository/user.rs create mode 100644 src/repository/user_team.rs create mode 100644 src/server/controller/category.rs create mode 100644 src/server/controller/challenge.rs create mode 100644 src/server/controller/config.rs create mode 100644 src/server/controller/game.rs create mode 100644 src/server/controller/media.rs create mode 100644 src/server/controller/mod.rs create mode 100644 src/server/controller/pod.rs create mode 100644 src/server/controller/submission.rs create mode 100644 src/server/controller/team.rs create mode 100644 src/server/controller/user.rs create mode 100644 src/server/middleware/auth.rs create mode 100644 src/server/middleware/mod.rs create mode 100644 src/server/mod.rs create mode 100644 src/server/router/category.rs create mode 100644 src/server/router/challenge.rs create mode 100644 src/server/router/config.rs create mode 100644 src/server/router/game.rs create mode 100644 src/server/router/media.rs create mode 100644 src/server/router/mod.rs create mode 100644 src/server/router/pod.rs create mode 100644 src/server/router/proxy.rs create mode 100644 src/server/router/submission.rs create mode 100644 src/server/router/team.rs create mode 100644 src/server/router/user.rs create mode 100644 src/server/service/category.rs create mode 100644 src/server/service/challenge.rs create mode 100644 src/server/service/game.rs create mode 100644 src/server/service/game_challenge.rs create mode 100644 src/server/service/game_team.rs create mode 100644 src/server/service/mod.rs create mode 100644 src/server/service/pod.rs create mode 100644 src/server/service/submission.rs create mode 100644 src/server/service/team.rs create mode 100644 src/server/service/user.rs create mode 100644 src/server/service/user_team.rs create mode 100644 src/traits.rs create mode 100644 src/util/jwt.rs create mode 100644 src/util/math.rs create mode 100644 src/util/mod.rs create mode 100644 src/util/validate.rs delete mode 100644 web/src/pages/admin/global/index.tsx delete mode 100644 web/src/types/file.ts create mode 100644 web/src/types/media.ts diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index 68725dc6..00000000 --- a/.gitattributes +++ /dev/null @@ -1,3 +0,0 @@ -# Auto detect text files and perform LF normalization -* text=auto -*.py linguist-vendored \ No newline at end of file diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index b854ebfa..44bdb85f 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -1,61 +1,61 @@ name: Docker Build & Push on: - push: - workflow_dispatch: + push: + workflow_dispatch: permissions: - contents: read - packages: write + contents: read + packages: write jobs: - build: - runs-on: ubuntu-latest - steps: - - name: Check out repository - uses: actions/checkout@v4 - with: - repository: elabosak233/cloudsdale - ref: ${{ github.ref }} - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Login to Ghcr Registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: elabosak233 - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Login to Docker Hub Registry - uses: docker/login-action@v3 - with: - registry: docker.io - username: elabosak233 - password: ${{ secrets.DOCKER_TOKEN }} - - - name: Docker metadata action - uses: docker/metadata-action@v5 - id: meta - with: - images: | - ghcr.io/${{ github.repository_owner }}/cloudsdale - docker.io/${{ github.repository_owner }}/cloudsdale - flavor: | - latest=${{ startsWith(github.ref, 'refs/tags/v') && !contains(github.ref, '-') }} - tags: | - type=ref,event=branch - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=sha,enable=true,priority=100,prefix=,suffix=,format=short - - - name: Build and Push - uses: docker/build-push-action@v3 - with: - context: ./ - file: ./Dockerfile - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - platforms: linux/amd64 - push: true + build: + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v4 + with: + repository: elabosak233/cloudsdale + ref: ${{ github.ref }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Ghcr Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: elabosak233 + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Login to Docker Hub Registry + uses: docker/login-action@v3 + with: + registry: docker.io + username: elabosak233 + password: ${{ secrets.DOCKER_TOKEN }} + + - name: Docker metadata action + uses: docker/metadata-action@v5 + id: meta + with: + images: | + ghcr.io/${{ github.repository_owner }}/cloudsdale + docker.io/${{ github.repository_owner }}/cloudsdale + flavor: | + latest=${{ startsWith(github.ref, 'refs/tags/v') && !contains(github.ref, '-') }} + tags: | + type=ref,event=branch + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=sha,enable=true,priority=100,prefix=,suffix=,format=short + + - name: Build and Push + uses: docker/build-push-action@v3 + with: + context: ./ + file: ./Dockerfile + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + platforms: linux/amd64 + push: true diff --git a/.gitignore b/.gitignore index 93753753..10fe1e10 100644 --- a/.gitignore +++ b/.gitignore @@ -1,21 +1,17 @@ -*.exe -*.exe~ -*.dll -*.so -*.dylib -*.test -*.out +# Project +/target +/.idea +/.vscode -go.work -.idea -config.yml -logs -plugins -uploads -/media/ -/configs/ -/db/ -/build/ -/captures/ -test.go -/dist/ +*.lock + +# Cloudsdale +/application.yml +/favicon.webp +/k8s-config.yml +/configs +/db +/media +/caputres +/logs +/dist \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..505d21df --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1 @@ +# CHANGELOG \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 00000000..7011097d --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,65 @@ +[package] +name = "cloudsdale" +version = "0.0.1" +edition = "2021" +description = "The Cloudsdale project is an open-source, light weight, Jeopardy-style's CTF platform." + +[dependencies] +axum = { version = "0.7", features = [ + "ws", + "http2", + "multipart", + "macros", + "tower-log", + "tracing", + "json", +] } +axum-extra = { version = "0.9", features = [ + "typed-header", + "query", + "multipart", + "typed-routing", + "async-read-body", +] } +futures = { version = "^0.3" } +futures-util = { version = "^0.3" } +tokio = { version = "1.38", features = ["full"] } +tower = { version = "0.4" } +tower-http = { version = "0.5", features = ["cors", "fs", "trace"] } +serde = { version = "1.0", features = ["derive"] } +tracing = { version = "0.1" } +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +uuid = { version = "1.8", features = ["v4", "fast-rng", "macro-diagnostics"] } +sea-orm = { version = "0.12", features = [ + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "debug-print", + "with-uuid", + "macros", + "with-json", + "runtime-tokio-native-tls", +] } +sea-orm-migration = { version = "0.12" } +chrono = "0.4.38" +serde_json = "1.0.117" +bollard = "*" +bcrypt = "0.15.1" +once_cell = "1.19.0" +jsonwebtoken = "9.3.0" +prometheus = "0.13.4" +regex = "1.10.5" +thiserror = "1.0.61" +mime = "0.3.17" +validator = { version = "0.18", features = ["derive"] } +openssl = { version = "0.10", features = ["vendored"] } +wsrx = { version = "0.2", features = ["server"] } +serde_yaml = "0.9.34" +kube = { version = "0.92.1", features = ["runtime", "derive"] } +k8s-openapi = { version = "0.22.0", features = ["latest"] } +reqwest = { version = "0.12", features = ["json"] } +async-trait = "0.1.81" + +[[bin]] +name = "cloudsdale" +path = "src/main.rs" diff --git a/Dockerfile b/Dockerfile index 8121a460..a95d2c76 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,34 +1,43 @@ -FROM golang:1.22-alpine AS backend +FROM rust:latest AS backend -RUN apk add --no-cache git gcc make musl-dev +WORKDIR /app -COPY ./ /app +COPY Cargo.toml Cargo.lock ./ -WORKDIR /app +RUN cargo fetch + +COPY . . + +RUN rustup target add x86_64-unknown-linux-musl -RUN go install github.com/swaggo/swag/cmd/swag@latest -RUN go mod download +RUN apt update && apt install -y musl-tools musl-dev pkg-config libssl-dev ca-certificates -RUN make build +ENV OPENSSL_DIR=/usr +ENV OPENSSL_INCLUDE_DIR=/usr/include +ENV OPENSSL_LIB_DIR=/usr/lib/x86_64-linux-gnu +ENV PKG_CONFIG_ALLOW_CROSS=1 +ENV PKG_CONFIG_PATH=/usr/lib/x86_64-linux-gnu/pkgconfig + +RUN cargo build --release --target x86_64-unknown-linux-musl FROM node:20 AS frontend COPY ./web /app - + WORKDIR /app - + RUN npm install RUN npm run build -FROM alpine:3.14 +FROM alpine:latest -COPY --from=backend /app/build/cloudsdale /app/cloudsdale -COPY --from=frontend /app/dist /app/dist +RUN apk --no-cache add ca-certificates WORKDIR /app -VOLUME /var/run/docker.sock +COPY --from=backend /app/target/x86_64-unknown-linux-musl/release/cloudsdale . +COPY --from=frontend /app/dist ./dist EXPOSE 8888 -CMD ["./cloudsdale"] \ No newline at end of file +CMD ["./cloudsdale"] diff --git a/Makefile b/Makefile deleted file mode 100644 index 5010694c..00000000 --- a/Makefile +++ /dev/null @@ -1,37 +0,0 @@ -BINARY := cloudsdale -PACKAGE := github.com/elabosak233/cloudsdale - -GOOS := $(shell go env GOOS) -GOARCH := $(shell go env GOARCH) - -export TERM := xterm-256color -export CGO_ENABLED := 1 - -GIT_TAG := $(shell git describe --tags --always) -GIT_COMMIT := $(shell git rev-parse HEAD) -GIT_BRANCH := $(shell git rev-parse --abbrev-ref HEAD) - -LDFLAGS := -X $(PACKAGE)/internal/utils.GitTag=$(GIT_TAG) -X $(PACKAGE)/internal/utils.GitCommitID=$(GIT_COMMIT) -X $(PACKAGE)/internal/utils.GitBranch=$(GIT_BRANCH) - -.PHONY: all build run clean swag - -all: build - -clean: - @rm -rf ./build - -swag: - @echo Generating swagger docs... - swag init -g ./cmd/cloudsdale/main.go -o ./api - @echo Swagger docs generated. - -build: swag - @echo Building $(PACKAGE)... - @go build -ldflags "-linkmode external -w -s $(LDFLAGS)" -o ./build/$(BINARY) $(PACKAGE)/cmd/cloudsdale - @echo Build finished. - -run: export DEBUG = true -run: swag - @echo Running $(PACKAGE)... - go run -ldflags "$(LDFLAGS)" $(PACKAGE)/cmd/cloudsdale - @echo Run finished. \ No newline at end of file diff --git a/README.md b/README.md index 2b257d4a..bc717993 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,19 @@ # Cloudsdale -[![Go Report Card](https://goreportcard.com/badge/github.com/elabosak233/cloudsdale)](https://goreportcard.com/report/github.com/elabosak233/cloudsdale) - -The **Cloudsdale** project is an _open-source, light-weight, Jeopardy-style's_ CTF platform. +The **Cloudsdale** project is an _open-source, high-performance, Jeopardy-style's_ CTF platform. You can read more in the [Documentation](https://docs.ctf.e23.dev). -## Special Thanks +## Contributors Thanks to everyone who has contributed to the project! Without you, Cloudsdale would not be what it is today. ![](https://contrib.rocks/image?repo=ElaBosak233/Cloudsdale) -The current version of Cloudsdale, while ensuring the originality of the code, largely draws inspiration from [GZ::CTF](https://github.com/GZTimeWalker/GZCTF) in its frontend design. Therefore, I express my highest respect to [GZTimeWalker](https://github.com/GZTimeWalker), the author of GZ::CTF. At the same time, I also thank GZTime for his suggestions on Cloudsdale. \ No newline at end of file +## Stars + +![](https://starchart.cc/ElaBosak233/Cloudsdale.svg) + +## License + +This project is licensed under the [GNU General Public License v3.0](https://github.com/ElaBosak233/Cloudsdale/blob/main/LICENSE). \ No newline at end of file diff --git a/SECURITY.md b/SECURITY.md deleted file mode 100644 index 04e907af..00000000 --- a/SECURITY.md +++ /dev/null @@ -1 +0,0 @@ -# Security Policy \ No newline at end of file diff --git a/api/docs.go b/api/docs.go deleted file mode 100644 index 4716d1b5..00000000 --- a/api/docs.go +++ /dev/null @@ -1,3038 +0,0 @@ -// Package api Code generated by swaggo/swag. DO NOT EDIT -package api - -import "github.com/swaggo/swag" - -const docTemplate = `{ - "schemes": {{ marshal .Schemes }}, - "swagger": "2.0", - "info": { - "description": "{{escape .Description}}", - "title": "{{.Title}}", - "contact": {}, - "version": "{{.Version}}" - }, - "host": "{{.Host}}", - "basePath": "{{.BasePath}}", - "paths": { - "/categories/": { - "get": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Category" - ], - "summary": "get category", - "parameters": [ - { - "type": "integer", - "name": "id", - "in": "query" - }, - { - "type": "string", - "name": "name", - "in": "query" - } - ], - "responses": {} - }, - "post": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Category" - ], - "summary": "create new category", - "parameters": [ - { - "description": "CategoryCreateRequest", - "name": "req", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/request.CategoryCreateRequest" - } - } - ], - "responses": {} - } - }, - "/categories/{id}": { - "put": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Category" - ], - "summary": "update category", - "parameters": [ - { - "description": "CategoryUpdateRequest", - "name": "req", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/request.CategoryUpdateRequest" - } - } - ], - "responses": {} - }, - "delete": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Category" - ], - "summary": "delete category", - "parameters": [ - { - "description": "CategoryDeleteRequest", - "name": "req", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/request.CategoryDeleteRequest" - } - } - ], - "responses": {} - } - }, - "/challenges/": { - "get": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Challenge" - ], - "summary": "题目查询", - "parameters": [ - { - "type": "integer", - "name": "category_id", - "in": "query" - }, - { - "type": "integer", - "name": "difficulty", - "in": "query" - }, - { - "type": "integer", - "name": "game_id", - "in": "query" - }, - { - "type": "integer", - "name": "id", - "in": "query" - }, - { - "type": "boolean", - "name": "is_detailed", - "in": "query" - }, - { - "type": "boolean", - "name": "is_dynamic", - "in": "query" - }, - { - "type": "boolean", - "name": "is_practicable", - "in": "query" - }, - { - "type": "integer", - "name": "page", - "in": "query" - }, - { - "type": "integer", - "name": "size", - "in": "query" - }, - { - "type": "string", - "name": "sort_key", - "in": "query" - }, - { - "type": "string", - "name": "sort_order", - "in": "query" - }, - { - "type": "integer", - "name": "team_id", - "in": "query" - }, - { - "type": "string", - "name": "title", - "in": "query" - }, - { - "type": "integer", - "name": "user_id", - "in": "query" - } - ], - "responses": {} - }, - "post": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Challenge" - ], - "summary": "创建题目", - "parameters": [ - { - "description": "ChallengeCreateRequest", - "name": "创建请求", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/request.ChallengeCreateRequest" - } - } - ], - "responses": {} - } - }, - "/challenges/{id}": { - "put": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Challenge" - ], - "summary": "更新题目", - "parameters": [ - { - "description": "ChallengeUpdateRequest", - "name": "request", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/request.ChallengeUpdateRequest" - } - } - ], - "responses": {} - }, - "delete": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Challenge" - ], - "summary": "删除题目", - "parameters": [ - { - "description": "ChallengeDeleteRequest", - "name": "request", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/request.ChallengeDeleteRequest" - } - } - ], - "responses": {} - } - }, - "/challenges/{id}/attachment": { - "post": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Challenge" - ], - "summary": "保存附件", - "parameters": [ - { - "type": "file", - "description": "attachment", - "name": "file", - "in": "formData", - "required": true - } - ], - "responses": {} - }, - "delete": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Challenge" - ], - "summary": "删除附件", - "responses": {} - } - }, - "/challenges/{id}/flags": { - "post": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Challenge" - ], - "summary": "创建 flag", - "responses": {} - } - }, - "/challenges/{id}/flags/{flag_id}": { - "put": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Challenge" - ], - "summary": "更新 flag", - "responses": {} - }, - "delete": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Challenge" - ], - "summary": "删除 flag", - "responses": {} - } - }, - "/configs/": { - "get": { - "description": "配置全部查询", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Config" - ], - "summary": "配置全部查询", - "responses": {} - }, - "put": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "更新配置", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Config" - ], - "summary": "更新配置", - "parameters": [ - { - "description": "body", - "name": "input", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/request.ConfigUpdateRequest" - } - } - ], - "responses": {} - } - }, - "/configs/captcha": { - "get": { - "description": "Captcha 配置查询", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Config" - ], - "summary": "Captcha 配置查询", - "responses": {} - } - }, - "/games/": { - "get": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Game" - ], - "summary": "比赛查询", - "parameters": [ - { - "type": "integer", - "name": "id", - "in": "query" - }, - { - "type": "boolean", - "name": "is_enabled", - "in": "query" - }, - { - "type": "integer", - "name": "page", - "in": "query" - }, - { - "type": "integer", - "name": "size", - "in": "query" - }, - { - "type": "string", - "name": "sort_key", - "in": "query" - }, - { - "type": "string", - "name": "sort_order", - "in": "query" - }, - { - "type": "string", - "name": "title", - "in": "query" - } - ], - "responses": {} - }, - "post": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Game" - ], - "summary": "创建比赛", - "parameters": [ - { - "description": "GameCreateRequest", - "name": "创建请求", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/request.GameCreateRequest" - } - } - ], - "responses": {} - } - }, - "/games/{id}": { - "put": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Game" - ], - "summary": "更新比赛", - "parameters": [ - { - "description": "GameUpdateRequest", - "name": "更新请求", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/request.GameUpdateRequest" - } - } - ], - "responses": {} - }, - "delete": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Game" - ], - "summary": "删除比赛", - "parameters": [ - { - "description": "GameDeleteRequest", - "name": "删除请求", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/request.GameDeleteRequest" - } - } - ], - "responses": {} - } - }, - "/games/{id}/broadcast": { - "get": { - "description": "广播消息", - "tags": [ - "Game" - ], - "summary": "广播消息", - "responses": {} - } - }, - "/games/{id}/challenges": { - "get": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Game" - ], - "summary": "查询比赛的挑战", - "responses": {} - }, - "post": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Game" - ], - "summary": "添加比赛的挑战", - "responses": {} - } - }, - "/games/{id}/challenges/{challenge_id}": { - "put": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Game" - ], - "summary": "更新比赛的挑战", - "responses": {} - }, - "delete": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Game" - ], - "summary": "删除比赛的挑战", - "responses": {} - } - }, - "/games/{id}/notices": { - "get": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Game" - ], - "summary": "查询比赛的通知", - "responses": {} - }, - "post": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Game" - ], - "summary": "添加比赛的通知", - "responses": {} - } - }, - "/games/{id}/notices/{notice_id}": { - "put": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Game" - ], - "summary": "更新比赛的通知", - "responses": {} - }, - "delete": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Game" - ], - "summary": "删除比赛的通知", - "responses": {} - } - }, - "/games/{id}/poster": { - "post": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Game" - ], - "summary": "保存头图", - "parameters": [ - { - "type": "file", - "description": "poster", - "name": "file", - "in": "formData", - "required": true - } - ], - "responses": {} - }, - "delete": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Game" - ], - "summary": "删除海报", - "responses": {} - } - }, - "/games/{id}/teams": { - "get": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Game" - ], - "summary": "查询比赛的团队", - "responses": {} - }, - "post": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Game" - ], - "summary": "加入比赛", - "parameters": [ - { - "description": "GameTeamCreateRequest", - "name": "加入请求", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/request.GameTeamCreateRequest" - } - } - ], - "responses": {} - } - }, - "/games/{id}/teams/{team_id}": { - "put": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Game" - ], - "summary": "允许加入比赛", - "parameters": [ - { - "description": "GameTeamUpdateRequest", - "name": "允许加入请求", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/request.GameTeamUpdateRequest" - } - } - ], - "responses": {} - }, - "delete": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Game" - ], - "summary": "删除比赛的团队", - "responses": {} - } - }, - "/pods/": { - "get": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "实例查询", - "produces": [ - "application/json" - ], - "tags": [ - "Pod" - ], - "summary": "实例查询", - "parameters": [ - { - "type": "integer", - "name": "challenge_id", - "in": "query" - }, - { - "type": "integer", - "name": "game_id", - "in": "query" - }, - { - "type": "integer", - "name": "id", - "in": "query" - }, - { - "type": "boolean", - "name": "is_available", - "in": "query" - }, - { - "type": "integer", - "name": "page", - "in": "query" - }, - { - "type": "integer", - "name": "size", - "in": "query" - }, - { - "type": "integer", - "name": "team_id", - "in": "query" - }, - { - "type": "integer", - "name": "user_id", - "in": "query" - } - ], - "responses": {} - }, - "post": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "创建实例", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Pod" - ], - "summary": "创建实例", - "parameters": [ - { - "description": "PodCreateRequest", - "name": "input", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/request.PodCreateRequest" - } - } - ], - "responses": {} - } - }, - "/pods/{id}": { - "put": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "容器续期", - "produces": [ - "application/json" - ], - "tags": [ - "Pod" - ], - "summary": "容器续期", - "parameters": [ - { - "description": "PodRenewRequest", - "name": "input", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/request.PodRenewRequest" - } - } - ], - "responses": {} - }, - "delete": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "停止并删除容器", - "produces": [ - "application/json" - ], - "tags": [ - "Pod" - ], - "summary": "停止并删除容器", - "parameters": [ - { - "description": "PodRemoveRequest", - "name": "input", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/request.PodRemoveRequest" - } - } - ], - "responses": {} - } - }, - "/submissions/": { - "get": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Submission" - ], - "summary": "提交记录查询", - "parameters": [ - { - "type": "integer", - "description": "题目 Id", - "name": "challenge_id", - "in": "query" - }, - { - "type": "integer", - "description": "比赛 Id", - "name": "game_id", - "in": "query" - }, - { - "type": "boolean", - "description": "是否详细", - "name": "is_detailed", - "in": "query" - }, - { - "type": "integer", - "description": "页码", - "name": "page", - "in": "query" - }, - { - "type": "integer", - "description": "每页大小", - "name": "size", - "in": "query" - }, - { - "type": "string", - "description": "排序参数", - "name": "sort_key", - "in": "query" - }, - { - "type": "string", - "description": "排序方式", - "name": "sort_order", - "in": "query" - }, - { - "type": "integer", - "description": "评判结果", - "name": "status", - "in": "query" - }, - { - "type": "integer", - "description": "团队 Id", - "name": "team_id", - "in": "query" - }, - { - "type": "integer", - "description": "用户 Id", - "name": "user_id", - "in": "query" - } - ], - "responses": {} - }, - "post": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Submission" - ], - "summary": "提交", - "parameters": [ - { - "description": "SubmissionCreateRequest", - "name": "创建请求", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/request.SubmissionCreateRequest" - } - } - ], - "responses": {} - } - }, - "/submissions/{id}": { - "delete": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Submission" - ], - "summary": "delete submission", - "parameters": [ - { - "description": "SubmissionDeleteRequest", - "name": "删除请求", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/request.SubmissionDeleteRequest" - } - } - ], - "responses": {} - } - }, - "/teams/": { - "get": { - "description": "查找团队", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Team" - ], - "summary": "查找团队", - "parameters": [ - { - "type": "integer", - "name": "captain_id", - "in": "query" - }, - { - "type": "integer", - "name": "game_id", - "in": "query" - }, - { - "type": "integer", - "name": "id", - "in": "query" - }, - { - "type": "string", - "name": "name", - "in": "query" - }, - { - "type": "integer", - "name": "page", - "in": "query" - }, - { - "type": "integer", - "name": "size", - "in": "query" - }, - { - "type": "string", - "name": "sort_key", - "in": "query" - }, - { - "type": "string", - "name": "sort_order", - "in": "query" - }, - { - "type": "integer", - "name": "user_id", - "in": "query" - } - ], - "responses": {} - }, - "post": { - "description": "创建团队", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Team" - ], - "summary": "创建团队", - "parameters": [ - { - "description": "TeamCreateRequest", - "name": "input", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/request.TeamCreateRequest" - } - } - ], - "responses": {} - } - }, - "/teams/{id}": { - "put": { - "description": "更新团队", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Team" - ], - "summary": "更新团队", - "parameters": [ - { - "description": "TeamUpdateRequest", - "name": "input", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/request.TeamUpdateRequest" - } - } - ], - "responses": {} - }, - "delete": { - "description": "删除团队", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Team" - ], - "summary": "删除团队", - "parameters": [ - { - "description": "TeamDeleteRequest", - "name": "input", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/request.TeamDeleteRequest" - } - } - ], - "responses": {} - } - }, - "/teams/{id}/avatar": { - "post": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Challenge" - ], - "summary": "保存头像", - "parameters": [ - { - "type": "file", - "description": "avatar", - "name": "file", - "in": "formData", - "required": true - } - ], - "responses": {} - }, - "delete": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Challenge" - ], - "summary": "删除头像", - "responses": {} - } - }, - "/teams/{id}/invite": { - "get": { - "description": "获取邀请码", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Team" - ], - "summary": "获取邀请码", - "parameters": [ - { - "type": "string", - "description": "id", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": {} - }, - "put": { - "description": "更新邀请码", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Team" - ], - "summary": "更新邀请码", - "parameters": [ - { - "type": "string", - "description": "id", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": {} - } - }, - "/teams/{id}/join": { - "post": { - "description": "加入团队", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Team" - ], - "summary": "加入团队", - "parameters": [ - { - "type": "string", - "description": "id", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": {} - } - }, - "/teams/{id}/leave": { - "delete": { - "description": "离开团队", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Team" - ], - "summary": "离开团队", - "parameters": [ - { - "type": "string", - "description": "id", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": {} - } - }, - "/teams/{id}/users/": { - "post": { - "description": "加入团队", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Team" - ], - "summary": "加入团队", - "parameters": [ - { - "description": "TeamUserCreateRequest", - "name": "input", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/request.TeamUserCreateRequest" - } - } - ], - "responses": {} - } - }, - "/teams/{id}/users/{user_id}": { - "delete": { - "description": "踢出团队", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Team" - ], - "summary": "踢出团队", - "parameters": [ - { - "description": "TeamUserDeleteRequest", - "name": "input", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/request.TeamUserDeleteRequest" - } - } - ], - "responses": {} - } - }, - "/users/": { - "get": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "User" - ], - "summary": "用户查询", - "parameters": [ - { - "type": "string", - "name": "email", - "in": "query" - }, - { - "type": "string", - "name": "group", - "in": "query" - }, - { - "type": "integer", - "name": "id", - "in": "query" - }, - { - "type": "string", - "name": "name", - "in": "query" - }, - { - "type": "integer", - "name": "page", - "in": "query" - }, - { - "type": "integer", - "name": "size", - "in": "query" - }, - { - "type": "string", - "name": "sort_key", - "in": "query" - }, - { - "type": "string", - "name": "sort_order", - "in": "query" - }, - { - "type": "string", - "name": "username", - "in": "query" - } - ], - "responses": {} - }, - "post": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "User" - ], - "summary": "用户创建", - "parameters": [ - { - "description": "UserCreateRequest", - "name": "创建请求", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/request.UserCreateRequest" - } - } - ], - "responses": {} - } - }, - "/users/login": { - "post": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "User" - ], - "summary": "用户登录", - "parameters": [ - { - "description": "UserLoginRequest", - "name": "登录请求", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/request.UserLoginRequest" - } - } - ], - "responses": {} - } - }, - "/users/logout": { - "post": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "User" - ], - "summary": "用户登出", - "responses": {} - } - }, - "/users/register": { - "post": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "User" - ], - "summary": "用户注册", - "parameters": [ - { - "description": "UserRegisterRequest", - "name": "input", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/request.UserRegisterRequest" - } - } - ], - "responses": {} - } - }, - "/users/{id}": { - "put": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "User" - ], - "summary": "用户更新", - "parameters": [ - { - "description": "UserUpdateRequest", - "name": "更新请求", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/request.UserUpdateRequest" - } - } - ], - "responses": {} - }, - "delete": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "User" - ], - "summary": "用户删除", - "parameters": [ - { - "description": "UserDeleteRequest", - "name": "input", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/request.UserDeleteRequest" - } - } - ], - "responses": {} - } - }, - "/users/{id}/avatar": { - "post": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Challenge" - ], - "summary": "保存头像", - "parameters": [ - { - "type": "file", - "description": "avatar", - "name": "file", - "in": "formData", - "required": true - } - ], - "responses": {} - }, - "delete": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Challenge" - ], - "summary": "删除头像", - "responses": {} - } - } - }, - "definitions": { - "model.Category": { - "type": "object", - "properties": { - "color": { - "description": "The category's theme color. (Such as Rainbow Dash's color is \"#60AEE4\")", - "type": "string" - }, - "created_at": { - "description": "The category's creation time.", - "type": "integer" - }, - "description": { - "description": "The category's description.", - "type": "string" - }, - "icon": { - "description": "The category's icon. (Based on Material Design Icons, Reference site: https://pictogrammers.com/library/mdi/) (Such as \"fingerprint\": https://pictogrammers.com/library/mdi/icon/fingerprint/)", - "type": "string" - }, - "id": { - "description": "The category's id. As primary key.", - "type": "integer" - }, - "name": { - "description": "The category's name.", - "type": "string" - }, - "updated_at": { - "description": "The category's last update time.", - "type": "integer" - } - } - }, - "model.Challenge": { - "type": "object", - "properties": { - "attachment": { - "description": "The challenge's attachment.", - "allOf": [ - { - "$ref": "#/definitions/model.File" - } - ] - }, - "bloods": { - "type": "array", - "items": { - "$ref": "#/definitions/model.Submission" - } - }, - "category": { - "description": "The challenge's category.", - "allOf": [ - { - "$ref": "#/definitions/model.Category" - } - ] - }, - "category_id": { - "description": "The challenge's category.", - "type": "integer" - }, - "cpu_limit": { - "description": "The challenge's CPU limit. (0 means no limit)", - "type": "integer" - }, - "created_at": { - "description": "The challenge's creation time.", - "type": "integer" - }, - "description": { - "description": "The challenge's description.", - "type": "string" - }, - "difficulty": { - "description": "The degree of difficulty. (From 1 to 5)", - "type": "integer" - }, - "duration": { - "description": "The duration of container maintenance in the initial state. (Seconds)", - "type": "integer" - }, - "envs": { - "type": "array", - "items": { - "$ref": "#/definitions/model.Env" - } - }, - "flags": { - "type": "array", - "items": { - "$ref": "#/definitions/model.Flag" - } - }, - "id": { - "description": "The challenge's id. As primary key.", - "type": "integer" - }, - "image_name": { - "description": "The challenge's image name.", - "type": "string" - }, - "is_dynamic": { - "description": "Whether the challenge is based on dynamic container.", - "type": "boolean" - }, - "is_practicable": { - "description": "Whether the challenge is practicable. (Is the practice field visible.)", - "type": "boolean" - }, - "memory_limit": { - "description": "The challenge's memory limit. (0 means no limit)", - "type": "integer" - }, - "ports": { - "type": "array", - "items": { - "$ref": "#/definitions/model.Port" - } - }, - "practice_pts": { - "description": "The points will be given when the challenge is solved in practice field.", - "type": "integer" - }, - "solved": { - "$ref": "#/definitions/model.Submission" - }, - "solved_times": { - "type": "integer" - }, - "title": { - "description": "The challenge's title.", - "type": "string" - }, - "updated_at": { - "description": "The challenge's last update time.", - "type": "integer" - } - } - }, - "model.Env": { - "type": "object", - "properties": { - "challenge": { - "$ref": "#/definitions/model.Challenge" - }, - "challenge_id": { - "type": "integer" - }, - "id": { - "type": "integer" - }, - "key": { - "type": "string" - }, - "value": { - "type": "string" - } - } - }, - "model.File": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "size": { - "type": "integer" - } - } - }, - "model.Flag": { - "type": "object", - "properties": { - "banned": { - "description": "Whether the flag is banned. If banned, the user who submitted the flag will be judged as cheating.", - "type": "boolean" - }, - "challenge": { - "description": "The challenge which the flag belongs to.", - "allOf": [ - { - "$ref": "#/definitions/model.Challenge" - } - ] - }, - "challenge_id": { - "description": "The challenge id. The flag belongs to.", - "type": "integer" - }, - "env": { - "description": "The environment variable which is used to be injected with the flag.", - "type": "string" - }, - "id": { - "description": "The flag id.", - "type": "integer" - }, - "type": { - "description": "The flag type. (\"static\"/\"dynamic\"/\"pattern\")", - "type": "string" - }, - "value": { - "description": "The flag content. Maybe a string or a regex, or the placeholder for dynamic challenges. (Such as \"flag{friendsh1p_1s_magic}\" or \"flag{[a-zA-Z]{5}}\" or \"flag{[UUID]}\")", - "type": "string" - } - } - }, - "model.Game": { - "type": "object", - "properties": { - "bio": { - "description": "The game's short description.", - "type": "string" - }, - "created_at": { - "description": "The game's creation time.", - "type": "integer" - }, - "description": { - "description": "The game's description. (Markdown supported.)", - "type": "string" - }, - "ended_at": { - "description": "The game's end time. (Unix)", - "type": "integer" - }, - "first_blood_reward_ratio": { - "description": "The prize ratio of first blood.", - "type": "number" - }, - "id": { - "description": "The game's id. As primary key.", - "type": "integer" - }, - "is_enabled": { - "description": "Whether the game is enabled.", - "type": "boolean" - }, - "is_need_write_up": { - "description": "Whether the game need write up.", - "type": "boolean" - }, - "is_public": { - "description": "Whether the game is public.", - "type": "boolean" - }, - "member_limit_max": { - "description": "The maximum team member limit.", - "type": "integer" - }, - "member_limit_min": { - "description": "The minimum team member limit.", - "type": "integer" - }, - "parallel_container_limit": { - "description": "The maximum parallel container limit.", - "type": "integer" - }, - "poster": { - "description": "The game's poster image.", - "allOf": [ - { - "$ref": "#/definitions/model.File" - } - ] - }, - "public_key": { - "description": "The game's public key.", - "type": "string" - }, - "second_blood_reward_ratio": { - "description": "The prize ratio of second blood.", - "type": "number" - }, - "started_at": { - "description": "The game's start time. (Unix)", - "type": "integer" - }, - "third_blood_reward_ratio": { - "description": "The prize ratio of third blood.", - "type": "number" - }, - "title": { - "description": "The game's title.", - "type": "string" - }, - "updated_at": { - "description": "The game's last update time.", - "type": "integer" - } - } - }, - "model.GameChallenge": { - "type": "object", - "properties": { - "challenge": { - "$ref": "#/definitions/model.Challenge" - }, - "challenge_id": { - "type": "integer" - }, - "game": { - "$ref": "#/definitions/model.Game" - }, - "game_id": { - "type": "integer" - }, - "id": { - "type": "integer" - }, - "is_enabled": { - "type": "boolean" - }, - "max_pts": { - "type": "integer" - }, - "min_pts": { - "type": "integer" - }, - "pts": { - "type": "integer" - } - } - }, - "model.Port": { - "type": "object", - "properties": { - "challenge": { - "description": "The JeopardyImage which the port belongs to.", - "allOf": [ - { - "$ref": "#/definitions/model.Challenge" - } - ] - }, - "challenge_id": { - "description": "The JeopardyImage which the port belongs to.", - "type": "integer" - }, - "description": { - "description": "The port's description.", - "type": "string" - }, - "id": { - "description": "The port's id. As primary key.", - "type": "integer" - }, - "value": { - "description": "The port number.", - "type": "integer" - } - } - }, - "model.Submission": { - "type": "object", - "properties": { - "challenge": { - "description": "The challenge which is related to this submission.", - "allOf": [ - { - "$ref": "#/definitions/model.Challenge" - } - ] - }, - "challenge_id": { - "description": "The challenge which is related to this submission.", - "type": "integer" - }, - "created_at": { - "description": "The submission's creation time.", - "type": "integer" - }, - "flag": { - "description": "The flag which was submitted for judgement.", - "type": "string" - }, - "game": { - "description": "The game which is related to this submission.", - "allOf": [ - { - "$ref": "#/definitions/model.Game" - } - ] - }, - "game_challenge": { - "description": "The game_challenge which is related to this submission.", - "allOf": [ - { - "$ref": "#/definitions/model.GameChallenge" - } - ] - }, - "game_challenge_id": { - "description": "The game_challenge which is related to this submission.", - "type": "integer" - }, - "game_id": { - "description": "The game which is related to this submission. (Must be set when TeamID is set)", - "type": "integer" - }, - "id": { - "description": "The submission's id. As primary key.", - "type": "integer" - }, - "pts": { - "description": "The points of the submission.", - "type": "integer" - }, - "rank": { - "description": "The rank of the submission.", - "type": "integer" - }, - "status": { - "description": "The status of the submission. (0-meaningless, 1-accepted, 2-incorrect, 3-cheat, 4-invalid(duplicate, etc.))", - "type": "integer" - }, - "team": { - "description": "The team which submitted the flag.", - "allOf": [ - { - "$ref": "#/definitions/model.Team" - } - ] - }, - "team_id": { - "description": "The team which submitted the flag. (Must be set when GameID is set)", - "type": "integer" - }, - "updated_at": { - "description": "The submission's last update time.", - "type": "integer" - }, - "user": { - "description": "The user who submitted the flag.", - "allOf": [ - { - "$ref": "#/definitions/model.User" - } - ] - }, - "user_id": { - "description": "The user who submitted the flag.", - "type": "integer" - } - } - }, - "model.Team": { - "type": "object", - "properties": { - "avatar": { - "description": "The team's avatar.", - "allOf": [ - { - "$ref": "#/definitions/model.File" - } - ] - }, - "captain": { - "description": "The captain's user.", - "allOf": [ - { - "$ref": "#/definitions/model.User" - } - ] - }, - "captain_id": { - "description": "The captain's id.", - "type": "integer" - }, - "created_at": { - "description": "The team's creation time.", - "type": "integer" - }, - "description": { - "description": "The team's description.", - "type": "string" - }, - "email": { - "description": "The team's email.", - "type": "string" - }, - "id": { - "description": "The team's id. As primary key.", - "type": "integer" - }, - "invite_token": { - "description": "The team's invite token.", - "type": "string" - }, - "is_locked": { - "description": "Whether the team is locked. (true/false)", - "type": "boolean" - }, - "name": { - "description": "The team's name.", - "type": "string" - }, - "updated_at": { - "description": "The team's last update time.", - "type": "integer" - }, - "users": { - "description": "The team's users.", - "type": "array", - "items": { - "$ref": "#/definitions/model.User" - } - } - } - }, - "model.User": { - "type": "object", - "properties": { - "avatar": { - "description": "The user's avatar.", - "allOf": [ - { - "$ref": "#/definitions/model.File" - } - ] - }, - "created_at": { - "description": "The user's creation time.", - "type": "integer" - }, - "description": { - "description": "The user's description.", - "type": "string" - }, - "email": { - "description": "The user's email.", - "type": "string" - }, - "group": { - "description": "The user's group.", - "type": "string" - }, - "id": { - "description": "The user's id. As primary key.", - "type": "integer" - }, - "nickname": { - "description": "The user's nickname. Not unique.", - "type": "string" - }, - "password": { - "description": "The user's password. Crypt.", - "type": "string" - }, - "remote_ip": { - "description": "The user's remote ip.", - "type": "string" - }, - "teams": { - "description": "The user's teams.", - "type": "array", - "items": { - "$ref": "#/definitions/model.Team" - } - }, - "updated_at": { - "description": "The user's last update time.", - "type": "integer" - }, - "username": { - "description": "The user's username. As a unique identifier.", - "type": "string" - } - } - }, - "request.CategoryCreateRequest": { - "type": "object", - "required": [ - "color", - "description", - "icon", - "name" - ], - "properties": { - "color": { - "type": "string" - }, - "description": { - "type": "string" - }, - "icon": { - "type": "string" - }, - "name": { - "type": "string" - } - } - }, - "request.CategoryDeleteRequest": { - "type": "object", - "properties": { - "id": { - "type": "integer" - } - } - }, - "request.CategoryUpdateRequest": { - "type": "object", - "required": [ - "color", - "description", - "icon", - "id", - "name" - ], - "properties": { - "color": { - "type": "string" - }, - "description": { - "type": "string" - }, - "icon": { - "type": "string" - }, - "id": { - "type": "integer" - }, - "name": { - "type": "string" - } - } - }, - "request.ChallengeCreateRequest": { - "type": "object", - "properties": { - "attachment_url": { - "type": "string" - }, - "category_id": { - "type": "integer" - }, - "cpu_limit": { - "type": "integer" - }, - "description": { - "type": "string" - }, - "difficulty": { - "type": "integer" - }, - "duration": { - "type": "integer" - }, - "envs": { - "type": "array", - "items": { - "$ref": "#/definitions/model.Env" - } - }, - "has_attachment": { - "type": "boolean" - }, - "image_name": { - "type": "string" - }, - "is_dynamic": { - "type": "boolean" - }, - "is_practicable": { - "type": "boolean" - }, - "memory_limit": { - "type": "integer" - }, - "ports": { - "type": "array", - "items": { - "$ref": "#/definitions/model.Port" - } - }, - "practice_pts": { - "type": "integer" - }, - "title": { - "type": "string" - } - } - }, - "request.ChallengeDeleteRequest": { - "type": "object" - }, - "request.ChallengeUpdateRequest": { - "type": "object", - "properties": { - "attachment_url": { - "type": "string" - }, - "category_id": { - "type": "integer" - }, - "cpu_limit": { - "type": "integer" - }, - "description": { - "type": "string" - }, - "difficulty": { - "type": "integer" - }, - "duration": { - "type": "integer" - }, - "envs": { - "type": "array", - "items": { - "$ref": "#/definitions/model.Env" - } - }, - "has_attachment": { - "type": "boolean" - }, - "image_name": { - "type": "string" - }, - "is_dynamic": { - "type": "boolean" - }, - "is_practicable": { - "type": "boolean" - }, - "memory_limit": { - "type": "integer" - }, - "ports": { - "type": "array", - "items": { - "$ref": "#/definitions/model.Port" - } - }, - "practice_pts": { - "type": "integer" - }, - "title": { - "type": "string" - } - } - }, - "request.ConfigUpdateRequest": { - "type": "object", - "properties": { - "container": { - "type": "object", - "properties": { - "parallel_limit": { - "type": "integer" - }, - "request_limit": { - "type": "integer" - } - } - }, - "site": { - "type": "object", - "properties": { - "color": { - "type": "string" - }, - "description": { - "type": "string" - }, - "title": { - "type": "string" - } - } - }, - "user": { - "type": "object", - "properties": { - "register": { - "type": "object", - "properties": { - "captcha": { - "type": "object", - "properties": { - "enabled": { - "type": "boolean" - } - } - }, - "email": { - "type": "object", - "properties": { - "domains": { - "type": "array", - "items": { - "type": "string" - } - }, - "enabled": { - "type": "boolean" - } - } - }, - "enabled": { - "type": "boolean" - } - } - } - } - } - } - }, - "request.GameCreateRequest": { - "type": "object", - "required": [ - "title" - ], - "properties": { - "bio": { - "type": "string" - }, - "cover_url": { - "type": "string" - }, - "description": { - "type": "string" - }, - "ended_at": { - "type": "integer" - }, - "first_blood_reward_ratio": { - "type": "number" - }, - "is_enabled": { - "type": "boolean" - }, - "is_need_write_up": { - "type": "boolean" - }, - "is_public": { - "type": "boolean" - }, - "member_limit_max": { - "type": "integer" - }, - "member_limit_min": { - "type": "integer" - }, - "parallel_container_limit": { - "type": "integer" - }, - "second_blood_reward_ratio": { - "type": "number" - }, - "started_at": { - "type": "integer" - }, - "third_blood_reward_ratio": { - "type": "number" - }, - "title": { - "type": "string" - } - } - }, - "request.GameDeleteRequest": { - "type": "object" - }, - "request.GameTeamCreateRequest": { - "type": "object", - "properties": { - "password": { - "type": "string" - }, - "team_id": { - "type": "integer" - }, - "user_id": { - "type": "integer" - } - } - }, - "request.GameTeamUpdateRequest": { - "type": "object", - "properties": { - "is_allowed": { - "type": "boolean" - } - } - }, - "request.GameUpdateRequest": { - "type": "object", - "properties": { - "bio": { - "type": "string" - }, - "cover_url": { - "type": "string" - }, - "description": { - "type": "string" - }, - "ended_at": { - "type": "integer" - }, - "first_blood_reward_ratio": { - "type": "number" - }, - "is_enabled": { - "type": "boolean" - }, - "is_need_write_up": { - "type": "boolean" - }, - "is_public": { - "type": "boolean" - }, - "member_limit_max": { - "type": "integer" - }, - "member_limit_min": { - "type": "integer" - }, - "parallel_container_limit": { - "type": "integer" - }, - "second_blood_reward_ratio": { - "type": "number" - }, - "started_at": { - "type": "integer" - }, - "third_blood_reward_ratio": { - "type": "number" - }, - "title": { - "type": "string" - } - } - }, - "request.PodCreateRequest": { - "type": "object", - "required": [ - "challenge_id" - ], - "properties": { - "challenge_id": { - "type": "integer" - }, - "game_id": { - "type": "integer" - }, - "team_id": { - "type": "integer" - } - } - }, - "request.PodRemoveRequest": { - "type": "object", - "properties": { - "game_id": { - "type": "integer" - }, - "team_id": { - "type": "integer" - } - } - }, - "request.PodRenewRequest": { - "type": "object", - "properties": { - "game_id": { - "type": "integer" - }, - "team_id": { - "type": "integer" - } - } - }, - "request.SubmissionCreateRequest": { - "type": "object", - "required": [ - "challenge_id", - "flag" - ], - "properties": { - "challenge_id": { - "description": "题目 Id", - "type": "integer" - }, - "flag": { - "description": "提交内容", - "type": "string" - }, - "game_id": { - "description": "比赛 Id", - "type": "integer" - }, - "team_id": { - "description": "团队 Id", - "type": "integer" - } - } - }, - "request.SubmissionDeleteRequest": { - "type": "object", - "required": [ - "id" - ], - "properties": { - "id": { - "type": "integer" - } - } - }, - "request.TeamCreateRequest": { - "type": "object", - "required": [ - "captain_id", - "name" - ], - "properties": { - "captain_id": { - "type": "integer" - }, - "description": { - "type": "string" - }, - "email": { - "type": "string" - }, - "name": { - "type": "string" - } - } - }, - "request.TeamDeleteRequest": { - "type": "object" - }, - "request.TeamUpdateRequest": { - "type": "object", - "properties": { - "captain_id": { - "type": "integer" - }, - "description": { - "type": "string" - }, - "email": { - "type": "string" - }, - "is_locked": { - "type": "boolean" - }, - "name": { - "type": "string" - } - } - }, - "request.TeamUserCreateRequest": { - "type": "object", - "properties": { - "invite_token": { - "type": "string" - }, - "user_id": { - "type": "integer" - } - } - }, - "request.TeamUserDeleteRequest": { - "type": "object", - "required": [ - "team_id", - "user_id" - ], - "properties": { - "team_id": { - "type": "integer" - }, - "user_id": { - "type": "integer" - } - } - }, - "request.UserCreateRequest": { - "type": "object", - "required": [ - "email", - "nickname", - "password", - "username" - ], - "properties": { - "email": { - "type": "string" - }, - "group": { - "type": "string" - }, - "nickname": { - "type": "string", - "minLength": 2 - }, - "password": { - "type": "string", - "minLength": 6 - }, - "username": { - "type": "string", - "maxLength": 20, - "minLength": 3 - } - } - }, - "request.UserDeleteRequest": { - "type": "object" - }, - "request.UserLoginRequest": { - "type": "object", - "required": [ - "password", - "username" - ], - "properties": { - "password": { - "type": "string" - }, - "username": { - "type": "string" - } - } - }, - "request.UserRegisterRequest": { - "type": "object", - "required": [ - "email", - "nickname", - "password", - "username" - ], - "properties": { - "email": { - "type": "string" - }, - "nickname": { - "type": "string" - }, - "password": { - "type": "string" - }, - "token": { - "type": "string" - }, - "username": { - "type": "string" - } - } - }, - "request.UserUpdateRequest": { - "type": "object", - "properties": { - "email": { - "type": "string" - }, - "group": { - "type": "string" - }, - "nickname": { - "type": "string", - "minLength": 2 - }, - "password": { - "type": "string", - "minLength": 6 - }, - "username": { - "type": "string", - "maxLength": 20, - "minLength": 3 - } - } - } - } -}` - -// SwaggerInfo holds exported Swagger Info so clients can modify it -var SwaggerInfo = &swag.Spec{ - Version: "", - Host: "", - BasePath: "/api", - Schemes: []string{}, - Title: "Cloudsdale", - Description: "", - InfoInstanceName: "swagger", - SwaggerTemplate: docTemplate, - LeftDelim: "{{", - RightDelim: "}}", -} - -func init() { - swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) -} diff --git a/api/swagger.json b/api/swagger.json deleted file mode 100644 index e4203532..00000000 --- a/api/swagger.json +++ /dev/null @@ -1,3011 +0,0 @@ -{ - "swagger": "2.0", - "info": { - "title": "Cloudsdale", - "contact": {} - }, - "basePath": "/api", - "paths": { - "/categories/": { - "get": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Category" - ], - "summary": "get category", - "parameters": [ - { - "type": "integer", - "name": "id", - "in": "query" - }, - { - "type": "string", - "name": "name", - "in": "query" - } - ], - "responses": {} - }, - "post": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Category" - ], - "summary": "create new category", - "parameters": [ - { - "description": "CategoryCreateRequest", - "name": "req", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/request.CategoryCreateRequest" - } - } - ], - "responses": {} - } - }, - "/categories/{id}": { - "put": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Category" - ], - "summary": "update category", - "parameters": [ - { - "description": "CategoryUpdateRequest", - "name": "req", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/request.CategoryUpdateRequest" - } - } - ], - "responses": {} - }, - "delete": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Category" - ], - "summary": "delete category", - "parameters": [ - { - "description": "CategoryDeleteRequest", - "name": "req", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/request.CategoryDeleteRequest" - } - } - ], - "responses": {} - } - }, - "/challenges/": { - "get": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Challenge" - ], - "summary": "题目查询", - "parameters": [ - { - "type": "integer", - "name": "category_id", - "in": "query" - }, - { - "type": "integer", - "name": "difficulty", - "in": "query" - }, - { - "type": "integer", - "name": "game_id", - "in": "query" - }, - { - "type": "integer", - "name": "id", - "in": "query" - }, - { - "type": "boolean", - "name": "is_detailed", - "in": "query" - }, - { - "type": "boolean", - "name": "is_dynamic", - "in": "query" - }, - { - "type": "boolean", - "name": "is_practicable", - "in": "query" - }, - { - "type": "integer", - "name": "page", - "in": "query" - }, - { - "type": "integer", - "name": "size", - "in": "query" - }, - { - "type": "string", - "name": "sort_key", - "in": "query" - }, - { - "type": "string", - "name": "sort_order", - "in": "query" - }, - { - "type": "integer", - "name": "team_id", - "in": "query" - }, - { - "type": "string", - "name": "title", - "in": "query" - }, - { - "type": "integer", - "name": "user_id", - "in": "query" - } - ], - "responses": {} - }, - "post": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Challenge" - ], - "summary": "创建题目", - "parameters": [ - { - "description": "ChallengeCreateRequest", - "name": "创建请求", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/request.ChallengeCreateRequest" - } - } - ], - "responses": {} - } - }, - "/challenges/{id}": { - "put": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Challenge" - ], - "summary": "更新题目", - "parameters": [ - { - "description": "ChallengeUpdateRequest", - "name": "request", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/request.ChallengeUpdateRequest" - } - } - ], - "responses": {} - }, - "delete": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Challenge" - ], - "summary": "删除题目", - "parameters": [ - { - "description": "ChallengeDeleteRequest", - "name": "request", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/request.ChallengeDeleteRequest" - } - } - ], - "responses": {} - } - }, - "/challenges/{id}/attachment": { - "post": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Challenge" - ], - "summary": "保存附件", - "parameters": [ - { - "type": "file", - "description": "attachment", - "name": "file", - "in": "formData", - "required": true - } - ], - "responses": {} - }, - "delete": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Challenge" - ], - "summary": "删除附件", - "responses": {} - } - }, - "/challenges/{id}/flags": { - "post": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Challenge" - ], - "summary": "创建 flag", - "responses": {} - } - }, - "/challenges/{id}/flags/{flag_id}": { - "put": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Challenge" - ], - "summary": "更新 flag", - "responses": {} - }, - "delete": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Challenge" - ], - "summary": "删除 flag", - "responses": {} - } - }, - "/configs/": { - "get": { - "description": "配置全部查询", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Config" - ], - "summary": "配置全部查询", - "responses": {} - }, - "put": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "更新配置", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Config" - ], - "summary": "更新配置", - "parameters": [ - { - "description": "body", - "name": "input", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/request.ConfigUpdateRequest" - } - } - ], - "responses": {} - } - }, - "/configs/captcha": { - "get": { - "description": "Captcha 配置查询", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Config" - ], - "summary": "Captcha 配置查询", - "responses": {} - } - }, - "/games/": { - "get": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Game" - ], - "summary": "比赛查询", - "parameters": [ - { - "type": "integer", - "name": "id", - "in": "query" - }, - { - "type": "boolean", - "name": "is_enabled", - "in": "query" - }, - { - "type": "integer", - "name": "page", - "in": "query" - }, - { - "type": "integer", - "name": "size", - "in": "query" - }, - { - "type": "string", - "name": "sort_key", - "in": "query" - }, - { - "type": "string", - "name": "sort_order", - "in": "query" - }, - { - "type": "string", - "name": "title", - "in": "query" - } - ], - "responses": {} - }, - "post": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Game" - ], - "summary": "创建比赛", - "parameters": [ - { - "description": "GameCreateRequest", - "name": "创建请求", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/request.GameCreateRequest" - } - } - ], - "responses": {} - } - }, - "/games/{id}": { - "put": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Game" - ], - "summary": "更新比赛", - "parameters": [ - { - "description": "GameUpdateRequest", - "name": "更新请求", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/request.GameUpdateRequest" - } - } - ], - "responses": {} - }, - "delete": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Game" - ], - "summary": "删除比赛", - "parameters": [ - { - "description": "GameDeleteRequest", - "name": "删除请求", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/request.GameDeleteRequest" - } - } - ], - "responses": {} - } - }, - "/games/{id}/broadcast": { - "get": { - "description": "广播消息", - "tags": [ - "Game" - ], - "summary": "广播消息", - "responses": {} - } - }, - "/games/{id}/challenges": { - "get": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Game" - ], - "summary": "查询比赛的挑战", - "responses": {} - }, - "post": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Game" - ], - "summary": "添加比赛的挑战", - "responses": {} - } - }, - "/games/{id}/challenges/{challenge_id}": { - "put": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Game" - ], - "summary": "更新比赛的挑战", - "responses": {} - }, - "delete": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Game" - ], - "summary": "删除比赛的挑战", - "responses": {} - } - }, - "/games/{id}/notices": { - "get": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Game" - ], - "summary": "查询比赛的通知", - "responses": {} - }, - "post": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Game" - ], - "summary": "添加比赛的通知", - "responses": {} - } - }, - "/games/{id}/notices/{notice_id}": { - "put": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Game" - ], - "summary": "更新比赛的通知", - "responses": {} - }, - "delete": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Game" - ], - "summary": "删除比赛的通知", - "responses": {} - } - }, - "/games/{id}/poster": { - "post": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Game" - ], - "summary": "保存头图", - "parameters": [ - { - "type": "file", - "description": "poster", - "name": "file", - "in": "formData", - "required": true - } - ], - "responses": {} - }, - "delete": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Game" - ], - "summary": "删除海报", - "responses": {} - } - }, - "/games/{id}/teams": { - "get": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Game" - ], - "summary": "查询比赛的团队", - "responses": {} - }, - "post": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Game" - ], - "summary": "加入比赛", - "parameters": [ - { - "description": "GameTeamCreateRequest", - "name": "加入请求", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/request.GameTeamCreateRequest" - } - } - ], - "responses": {} - } - }, - "/games/{id}/teams/{team_id}": { - "put": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Game" - ], - "summary": "允许加入比赛", - "parameters": [ - { - "description": "GameTeamUpdateRequest", - "name": "允许加入请求", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/request.GameTeamUpdateRequest" - } - } - ], - "responses": {} - }, - "delete": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Game" - ], - "summary": "删除比赛的团队", - "responses": {} - } - }, - "/pods/": { - "get": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "实例查询", - "produces": [ - "application/json" - ], - "tags": [ - "Pod" - ], - "summary": "实例查询", - "parameters": [ - { - "type": "integer", - "name": "challenge_id", - "in": "query" - }, - { - "type": "integer", - "name": "game_id", - "in": "query" - }, - { - "type": "integer", - "name": "id", - "in": "query" - }, - { - "type": "boolean", - "name": "is_available", - "in": "query" - }, - { - "type": "integer", - "name": "page", - "in": "query" - }, - { - "type": "integer", - "name": "size", - "in": "query" - }, - { - "type": "integer", - "name": "team_id", - "in": "query" - }, - { - "type": "integer", - "name": "user_id", - "in": "query" - } - ], - "responses": {} - }, - "post": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "创建实例", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Pod" - ], - "summary": "创建实例", - "parameters": [ - { - "description": "PodCreateRequest", - "name": "input", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/request.PodCreateRequest" - } - } - ], - "responses": {} - } - }, - "/pods/{id}": { - "put": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "容器续期", - "produces": [ - "application/json" - ], - "tags": [ - "Pod" - ], - "summary": "容器续期", - "parameters": [ - { - "description": "PodRenewRequest", - "name": "input", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/request.PodRenewRequest" - } - } - ], - "responses": {} - }, - "delete": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "停止并删除容器", - "produces": [ - "application/json" - ], - "tags": [ - "Pod" - ], - "summary": "停止并删除容器", - "parameters": [ - { - "description": "PodRemoveRequest", - "name": "input", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/request.PodRemoveRequest" - } - } - ], - "responses": {} - } - }, - "/submissions/": { - "get": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Submission" - ], - "summary": "提交记录查询", - "parameters": [ - { - "type": "integer", - "description": "题目 Id", - "name": "challenge_id", - "in": "query" - }, - { - "type": "integer", - "description": "比赛 Id", - "name": "game_id", - "in": "query" - }, - { - "type": "boolean", - "description": "是否详细", - "name": "is_detailed", - "in": "query" - }, - { - "type": "integer", - "description": "页码", - "name": "page", - "in": "query" - }, - { - "type": "integer", - "description": "每页大小", - "name": "size", - "in": "query" - }, - { - "type": "string", - "description": "排序参数", - "name": "sort_key", - "in": "query" - }, - { - "type": "string", - "description": "排序方式", - "name": "sort_order", - "in": "query" - }, - { - "type": "integer", - "description": "评判结果", - "name": "status", - "in": "query" - }, - { - "type": "integer", - "description": "团队 Id", - "name": "team_id", - "in": "query" - }, - { - "type": "integer", - "description": "用户 Id", - "name": "user_id", - "in": "query" - } - ], - "responses": {} - }, - "post": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Submission" - ], - "summary": "提交", - "parameters": [ - { - "description": "SubmissionCreateRequest", - "name": "创建请求", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/request.SubmissionCreateRequest" - } - } - ], - "responses": {} - } - }, - "/submissions/{id}": { - "delete": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Submission" - ], - "summary": "delete submission", - "parameters": [ - { - "description": "SubmissionDeleteRequest", - "name": "删除请求", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/request.SubmissionDeleteRequest" - } - } - ], - "responses": {} - } - }, - "/teams/": { - "get": { - "description": "查找团队", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Team" - ], - "summary": "查找团队", - "parameters": [ - { - "type": "integer", - "name": "captain_id", - "in": "query" - }, - { - "type": "integer", - "name": "game_id", - "in": "query" - }, - { - "type": "integer", - "name": "id", - "in": "query" - }, - { - "type": "string", - "name": "name", - "in": "query" - }, - { - "type": "integer", - "name": "page", - "in": "query" - }, - { - "type": "integer", - "name": "size", - "in": "query" - }, - { - "type": "string", - "name": "sort_key", - "in": "query" - }, - { - "type": "string", - "name": "sort_order", - "in": "query" - }, - { - "type": "integer", - "name": "user_id", - "in": "query" - } - ], - "responses": {} - }, - "post": { - "description": "创建团队", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Team" - ], - "summary": "创建团队", - "parameters": [ - { - "description": "TeamCreateRequest", - "name": "input", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/request.TeamCreateRequest" - } - } - ], - "responses": {} - } - }, - "/teams/{id}": { - "put": { - "description": "更新团队", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Team" - ], - "summary": "更新团队", - "parameters": [ - { - "description": "TeamUpdateRequest", - "name": "input", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/request.TeamUpdateRequest" - } - } - ], - "responses": {} - }, - "delete": { - "description": "删除团队", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Team" - ], - "summary": "删除团队", - "parameters": [ - { - "description": "TeamDeleteRequest", - "name": "input", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/request.TeamDeleteRequest" - } - } - ], - "responses": {} - } - }, - "/teams/{id}/avatar": { - "post": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Challenge" - ], - "summary": "保存头像", - "parameters": [ - { - "type": "file", - "description": "avatar", - "name": "file", - "in": "formData", - "required": true - } - ], - "responses": {} - }, - "delete": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Challenge" - ], - "summary": "删除头像", - "responses": {} - } - }, - "/teams/{id}/invite": { - "get": { - "description": "获取邀请码", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Team" - ], - "summary": "获取邀请码", - "parameters": [ - { - "type": "string", - "description": "id", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": {} - }, - "put": { - "description": "更新邀请码", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Team" - ], - "summary": "更新邀请码", - "parameters": [ - { - "type": "string", - "description": "id", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": {} - } - }, - "/teams/{id}/join": { - "post": { - "description": "加入团队", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Team" - ], - "summary": "加入团队", - "parameters": [ - { - "type": "string", - "description": "id", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": {} - } - }, - "/teams/{id}/leave": { - "delete": { - "description": "离开团队", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Team" - ], - "summary": "离开团队", - "parameters": [ - { - "type": "string", - "description": "id", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": {} - } - }, - "/teams/{id}/users/": { - "post": { - "description": "加入团队", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Team" - ], - "summary": "加入团队", - "parameters": [ - { - "description": "TeamUserCreateRequest", - "name": "input", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/request.TeamUserCreateRequest" - } - } - ], - "responses": {} - } - }, - "/teams/{id}/users/{user_id}": { - "delete": { - "description": "踢出团队", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Team" - ], - "summary": "踢出团队", - "parameters": [ - { - "description": "TeamUserDeleteRequest", - "name": "input", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/request.TeamUserDeleteRequest" - } - } - ], - "responses": {} - } - }, - "/users/": { - "get": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "User" - ], - "summary": "用户查询", - "parameters": [ - { - "type": "string", - "name": "email", - "in": "query" - }, - { - "type": "string", - "name": "group", - "in": "query" - }, - { - "type": "integer", - "name": "id", - "in": "query" - }, - { - "type": "string", - "name": "name", - "in": "query" - }, - { - "type": "integer", - "name": "page", - "in": "query" - }, - { - "type": "integer", - "name": "size", - "in": "query" - }, - { - "type": "string", - "name": "sort_key", - "in": "query" - }, - { - "type": "string", - "name": "sort_order", - "in": "query" - }, - { - "type": "string", - "name": "username", - "in": "query" - } - ], - "responses": {} - }, - "post": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "User" - ], - "summary": "用户创建", - "parameters": [ - { - "description": "UserCreateRequest", - "name": "创建请求", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/request.UserCreateRequest" - } - } - ], - "responses": {} - } - }, - "/users/login": { - "post": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "User" - ], - "summary": "用户登录", - "parameters": [ - { - "description": "UserLoginRequest", - "name": "登录请求", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/request.UserLoginRequest" - } - } - ], - "responses": {} - } - }, - "/users/logout": { - "post": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "User" - ], - "summary": "用户登出", - "responses": {} - } - }, - "/users/register": { - "post": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "User" - ], - "summary": "用户注册", - "parameters": [ - { - "description": "UserRegisterRequest", - "name": "input", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/request.UserRegisterRequest" - } - } - ], - "responses": {} - } - }, - "/users/{id}": { - "put": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "User" - ], - "summary": "用户更新", - "parameters": [ - { - "description": "UserUpdateRequest", - "name": "更新请求", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/request.UserUpdateRequest" - } - } - ], - "responses": {} - }, - "delete": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "User" - ], - "summary": "用户删除", - "parameters": [ - { - "description": "UserDeleteRequest", - "name": "input", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/request.UserDeleteRequest" - } - } - ], - "responses": {} - } - }, - "/users/{id}/avatar": { - "post": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Challenge" - ], - "summary": "保存头像", - "parameters": [ - { - "type": "file", - "description": "avatar", - "name": "file", - "in": "formData", - "required": true - } - ], - "responses": {} - }, - "delete": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Challenge" - ], - "summary": "删除头像", - "responses": {} - } - } - }, - "definitions": { - "model.Category": { - "type": "object", - "properties": { - "color": { - "description": "The category's theme color. (Such as Rainbow Dash's color is \"#60AEE4\")", - "type": "string" - }, - "created_at": { - "description": "The category's creation time.", - "type": "integer" - }, - "description": { - "description": "The category's description.", - "type": "string" - }, - "icon": { - "description": "The category's icon. (Based on Material Design Icons, Reference site: https://pictogrammers.com/library/mdi/) (Such as \"fingerprint\": https://pictogrammers.com/library/mdi/icon/fingerprint/)", - "type": "string" - }, - "id": { - "description": "The category's id. As primary key.", - "type": "integer" - }, - "name": { - "description": "The category's name.", - "type": "string" - }, - "updated_at": { - "description": "The category's last update time.", - "type": "integer" - } - } - }, - "model.Challenge": { - "type": "object", - "properties": { - "attachment": { - "description": "The challenge's attachment.", - "allOf": [ - { - "$ref": "#/definitions/model.File" - } - ] - }, - "bloods": { - "type": "array", - "items": { - "$ref": "#/definitions/model.Submission" - } - }, - "category": { - "description": "The challenge's category.", - "allOf": [ - { - "$ref": "#/definitions/model.Category" - } - ] - }, - "category_id": { - "description": "The challenge's category.", - "type": "integer" - }, - "cpu_limit": { - "description": "The challenge's CPU limit. (0 means no limit)", - "type": "integer" - }, - "created_at": { - "description": "The challenge's creation time.", - "type": "integer" - }, - "description": { - "description": "The challenge's description.", - "type": "string" - }, - "difficulty": { - "description": "The degree of difficulty. (From 1 to 5)", - "type": "integer" - }, - "duration": { - "description": "The duration of container maintenance in the initial state. (Seconds)", - "type": "integer" - }, - "envs": { - "type": "array", - "items": { - "$ref": "#/definitions/model.Env" - } - }, - "flags": { - "type": "array", - "items": { - "$ref": "#/definitions/model.Flag" - } - }, - "id": { - "description": "The challenge's id. As primary key.", - "type": "integer" - }, - "image_name": { - "description": "The challenge's image name.", - "type": "string" - }, - "is_dynamic": { - "description": "Whether the challenge is based on dynamic container.", - "type": "boolean" - }, - "is_practicable": { - "description": "Whether the challenge is practicable. (Is the practice field visible.)", - "type": "boolean" - }, - "memory_limit": { - "description": "The challenge's memory limit. (0 means no limit)", - "type": "integer" - }, - "ports": { - "type": "array", - "items": { - "$ref": "#/definitions/model.Port" - } - }, - "practice_pts": { - "description": "The points will be given when the challenge is solved in practice field.", - "type": "integer" - }, - "solved": { - "$ref": "#/definitions/model.Submission" - }, - "solved_times": { - "type": "integer" - }, - "title": { - "description": "The challenge's title.", - "type": "string" - }, - "updated_at": { - "description": "The challenge's last update time.", - "type": "integer" - } - } - }, - "model.Env": { - "type": "object", - "properties": { - "challenge": { - "$ref": "#/definitions/model.Challenge" - }, - "challenge_id": { - "type": "integer" - }, - "id": { - "type": "integer" - }, - "key": { - "type": "string" - }, - "value": { - "type": "string" - } - } - }, - "model.File": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "size": { - "type": "integer" - } - } - }, - "model.Flag": { - "type": "object", - "properties": { - "banned": { - "description": "Whether the flag is banned. If banned, the user who submitted the flag will be judged as cheating.", - "type": "boolean" - }, - "challenge": { - "description": "The challenge which the flag belongs to.", - "allOf": [ - { - "$ref": "#/definitions/model.Challenge" - } - ] - }, - "challenge_id": { - "description": "The challenge id. The flag belongs to.", - "type": "integer" - }, - "env": { - "description": "The environment variable which is used to be injected with the flag.", - "type": "string" - }, - "id": { - "description": "The flag id.", - "type": "integer" - }, - "type": { - "description": "The flag type. (\"static\"/\"dynamic\"/\"pattern\")", - "type": "string" - }, - "value": { - "description": "The flag content. Maybe a string or a regex, or the placeholder for dynamic challenges. (Such as \"flag{friendsh1p_1s_magic}\" or \"flag{[a-zA-Z]{5}}\" or \"flag{[UUID]}\")", - "type": "string" - } - } - }, - "model.Game": { - "type": "object", - "properties": { - "bio": { - "description": "The game's short description.", - "type": "string" - }, - "created_at": { - "description": "The game's creation time.", - "type": "integer" - }, - "description": { - "description": "The game's description. (Markdown supported.)", - "type": "string" - }, - "ended_at": { - "description": "The game's end time. (Unix)", - "type": "integer" - }, - "first_blood_reward_ratio": { - "description": "The prize ratio of first blood.", - "type": "number" - }, - "id": { - "description": "The game's id. As primary key.", - "type": "integer" - }, - "is_enabled": { - "description": "Whether the game is enabled.", - "type": "boolean" - }, - "is_need_write_up": { - "description": "Whether the game need write up.", - "type": "boolean" - }, - "is_public": { - "description": "Whether the game is public.", - "type": "boolean" - }, - "member_limit_max": { - "description": "The maximum team member limit.", - "type": "integer" - }, - "member_limit_min": { - "description": "The minimum team member limit.", - "type": "integer" - }, - "parallel_container_limit": { - "description": "The maximum parallel container limit.", - "type": "integer" - }, - "poster": { - "description": "The game's poster image.", - "allOf": [ - { - "$ref": "#/definitions/model.File" - } - ] - }, - "public_key": { - "description": "The game's public key.", - "type": "string" - }, - "second_blood_reward_ratio": { - "description": "The prize ratio of second blood.", - "type": "number" - }, - "started_at": { - "description": "The game's start time. (Unix)", - "type": "integer" - }, - "third_blood_reward_ratio": { - "description": "The prize ratio of third blood.", - "type": "number" - }, - "title": { - "description": "The game's title.", - "type": "string" - }, - "updated_at": { - "description": "The game's last update time.", - "type": "integer" - } - } - }, - "model.GameChallenge": { - "type": "object", - "properties": { - "challenge": { - "$ref": "#/definitions/model.Challenge" - }, - "challenge_id": { - "type": "integer" - }, - "game": { - "$ref": "#/definitions/model.Game" - }, - "game_id": { - "type": "integer" - }, - "id": { - "type": "integer" - }, - "is_enabled": { - "type": "boolean" - }, - "max_pts": { - "type": "integer" - }, - "min_pts": { - "type": "integer" - }, - "pts": { - "type": "integer" - } - } - }, - "model.Port": { - "type": "object", - "properties": { - "challenge": { - "description": "The JeopardyImage which the port belongs to.", - "allOf": [ - { - "$ref": "#/definitions/model.Challenge" - } - ] - }, - "challenge_id": { - "description": "The JeopardyImage which the port belongs to.", - "type": "integer" - }, - "description": { - "description": "The port's description.", - "type": "string" - }, - "id": { - "description": "The port's id. As primary key.", - "type": "integer" - }, - "value": { - "description": "The port number.", - "type": "integer" - } - } - }, - "model.Submission": { - "type": "object", - "properties": { - "challenge": { - "description": "The challenge which is related to this submission.", - "allOf": [ - { - "$ref": "#/definitions/model.Challenge" - } - ] - }, - "challenge_id": { - "description": "The challenge which is related to this submission.", - "type": "integer" - }, - "created_at": { - "description": "The submission's creation time.", - "type": "integer" - }, - "flag": { - "description": "The flag which was submitted for judgement.", - "type": "string" - }, - "game": { - "description": "The game which is related to this submission.", - "allOf": [ - { - "$ref": "#/definitions/model.Game" - } - ] - }, - "game_challenge": { - "description": "The game_challenge which is related to this submission.", - "allOf": [ - { - "$ref": "#/definitions/model.GameChallenge" - } - ] - }, - "game_challenge_id": { - "description": "The game_challenge which is related to this submission.", - "type": "integer" - }, - "game_id": { - "description": "The game which is related to this submission. (Must be set when TeamID is set)", - "type": "integer" - }, - "id": { - "description": "The submission's id. As primary key.", - "type": "integer" - }, - "pts": { - "description": "The points of the submission.", - "type": "integer" - }, - "rank": { - "description": "The rank of the submission.", - "type": "integer" - }, - "status": { - "description": "The status of the submission. (0-meaningless, 1-accepted, 2-incorrect, 3-cheat, 4-invalid(duplicate, etc.))", - "type": "integer" - }, - "team": { - "description": "The team which submitted the flag.", - "allOf": [ - { - "$ref": "#/definitions/model.Team" - } - ] - }, - "team_id": { - "description": "The team which submitted the flag. (Must be set when GameID is set)", - "type": "integer" - }, - "updated_at": { - "description": "The submission's last update time.", - "type": "integer" - }, - "user": { - "description": "The user who submitted the flag.", - "allOf": [ - { - "$ref": "#/definitions/model.User" - } - ] - }, - "user_id": { - "description": "The user who submitted the flag.", - "type": "integer" - } - } - }, - "model.Team": { - "type": "object", - "properties": { - "avatar": { - "description": "The team's avatar.", - "allOf": [ - { - "$ref": "#/definitions/model.File" - } - ] - }, - "captain": { - "description": "The captain's user.", - "allOf": [ - { - "$ref": "#/definitions/model.User" - } - ] - }, - "captain_id": { - "description": "The captain's id.", - "type": "integer" - }, - "created_at": { - "description": "The team's creation time.", - "type": "integer" - }, - "description": { - "description": "The team's description.", - "type": "string" - }, - "email": { - "description": "The team's email.", - "type": "string" - }, - "id": { - "description": "The team's id. As primary key.", - "type": "integer" - }, - "invite_token": { - "description": "The team's invite token.", - "type": "string" - }, - "is_locked": { - "description": "Whether the team is locked. (true/false)", - "type": "boolean" - }, - "name": { - "description": "The team's name.", - "type": "string" - }, - "updated_at": { - "description": "The team's last update time.", - "type": "integer" - }, - "users": { - "description": "The team's users.", - "type": "array", - "items": { - "$ref": "#/definitions/model.User" - } - } - } - }, - "model.User": { - "type": "object", - "properties": { - "avatar": { - "description": "The user's avatar.", - "allOf": [ - { - "$ref": "#/definitions/model.File" - } - ] - }, - "created_at": { - "description": "The user's creation time.", - "type": "integer" - }, - "description": { - "description": "The user's description.", - "type": "string" - }, - "email": { - "description": "The user's email.", - "type": "string" - }, - "group": { - "description": "The user's group.", - "type": "string" - }, - "id": { - "description": "The user's id. As primary key.", - "type": "integer" - }, - "nickname": { - "description": "The user's nickname. Not unique.", - "type": "string" - }, - "password": { - "description": "The user's password. Crypt.", - "type": "string" - }, - "remote_ip": { - "description": "The user's remote ip.", - "type": "string" - }, - "teams": { - "description": "The user's teams.", - "type": "array", - "items": { - "$ref": "#/definitions/model.Team" - } - }, - "updated_at": { - "description": "The user's last update time.", - "type": "integer" - }, - "username": { - "description": "The user's username. As a unique identifier.", - "type": "string" - } - } - }, - "request.CategoryCreateRequest": { - "type": "object", - "required": [ - "color", - "description", - "icon", - "name" - ], - "properties": { - "color": { - "type": "string" - }, - "description": { - "type": "string" - }, - "icon": { - "type": "string" - }, - "name": { - "type": "string" - } - } - }, - "request.CategoryDeleteRequest": { - "type": "object", - "properties": { - "id": { - "type": "integer" - } - } - }, - "request.CategoryUpdateRequest": { - "type": "object", - "required": [ - "color", - "description", - "icon", - "id", - "name" - ], - "properties": { - "color": { - "type": "string" - }, - "description": { - "type": "string" - }, - "icon": { - "type": "string" - }, - "id": { - "type": "integer" - }, - "name": { - "type": "string" - } - } - }, - "request.ChallengeCreateRequest": { - "type": "object", - "properties": { - "attachment_url": { - "type": "string" - }, - "category_id": { - "type": "integer" - }, - "cpu_limit": { - "type": "integer" - }, - "description": { - "type": "string" - }, - "difficulty": { - "type": "integer" - }, - "duration": { - "type": "integer" - }, - "envs": { - "type": "array", - "items": { - "$ref": "#/definitions/model.Env" - } - }, - "has_attachment": { - "type": "boolean" - }, - "image_name": { - "type": "string" - }, - "is_dynamic": { - "type": "boolean" - }, - "is_practicable": { - "type": "boolean" - }, - "memory_limit": { - "type": "integer" - }, - "ports": { - "type": "array", - "items": { - "$ref": "#/definitions/model.Port" - } - }, - "practice_pts": { - "type": "integer" - }, - "title": { - "type": "string" - } - } - }, - "request.ChallengeDeleteRequest": { - "type": "object" - }, - "request.ChallengeUpdateRequest": { - "type": "object", - "properties": { - "attachment_url": { - "type": "string" - }, - "category_id": { - "type": "integer" - }, - "cpu_limit": { - "type": "integer" - }, - "description": { - "type": "string" - }, - "difficulty": { - "type": "integer" - }, - "duration": { - "type": "integer" - }, - "envs": { - "type": "array", - "items": { - "$ref": "#/definitions/model.Env" - } - }, - "has_attachment": { - "type": "boolean" - }, - "image_name": { - "type": "string" - }, - "is_dynamic": { - "type": "boolean" - }, - "is_practicable": { - "type": "boolean" - }, - "memory_limit": { - "type": "integer" - }, - "ports": { - "type": "array", - "items": { - "$ref": "#/definitions/model.Port" - } - }, - "practice_pts": { - "type": "integer" - }, - "title": { - "type": "string" - } - } - }, - "request.ConfigUpdateRequest": { - "type": "object", - "properties": { - "container": { - "type": "object", - "properties": { - "parallel_limit": { - "type": "integer" - }, - "request_limit": { - "type": "integer" - } - } - }, - "site": { - "type": "object", - "properties": { - "color": { - "type": "string" - }, - "description": { - "type": "string" - }, - "title": { - "type": "string" - } - } - }, - "user": { - "type": "object", - "properties": { - "register": { - "type": "object", - "properties": { - "captcha": { - "type": "object", - "properties": { - "enabled": { - "type": "boolean" - } - } - }, - "email": { - "type": "object", - "properties": { - "domains": { - "type": "array", - "items": { - "type": "string" - } - }, - "enabled": { - "type": "boolean" - } - } - }, - "enabled": { - "type": "boolean" - } - } - } - } - } - } - }, - "request.GameCreateRequest": { - "type": "object", - "required": [ - "title" - ], - "properties": { - "bio": { - "type": "string" - }, - "cover_url": { - "type": "string" - }, - "description": { - "type": "string" - }, - "ended_at": { - "type": "integer" - }, - "first_blood_reward_ratio": { - "type": "number" - }, - "is_enabled": { - "type": "boolean" - }, - "is_need_write_up": { - "type": "boolean" - }, - "is_public": { - "type": "boolean" - }, - "member_limit_max": { - "type": "integer" - }, - "member_limit_min": { - "type": "integer" - }, - "parallel_container_limit": { - "type": "integer" - }, - "second_blood_reward_ratio": { - "type": "number" - }, - "started_at": { - "type": "integer" - }, - "third_blood_reward_ratio": { - "type": "number" - }, - "title": { - "type": "string" - } - } - }, - "request.GameDeleteRequest": { - "type": "object" - }, - "request.GameTeamCreateRequest": { - "type": "object", - "properties": { - "password": { - "type": "string" - }, - "team_id": { - "type": "integer" - }, - "user_id": { - "type": "integer" - } - } - }, - "request.GameTeamUpdateRequest": { - "type": "object", - "properties": { - "is_allowed": { - "type": "boolean" - } - } - }, - "request.GameUpdateRequest": { - "type": "object", - "properties": { - "bio": { - "type": "string" - }, - "cover_url": { - "type": "string" - }, - "description": { - "type": "string" - }, - "ended_at": { - "type": "integer" - }, - "first_blood_reward_ratio": { - "type": "number" - }, - "is_enabled": { - "type": "boolean" - }, - "is_need_write_up": { - "type": "boolean" - }, - "is_public": { - "type": "boolean" - }, - "member_limit_max": { - "type": "integer" - }, - "member_limit_min": { - "type": "integer" - }, - "parallel_container_limit": { - "type": "integer" - }, - "second_blood_reward_ratio": { - "type": "number" - }, - "started_at": { - "type": "integer" - }, - "third_blood_reward_ratio": { - "type": "number" - }, - "title": { - "type": "string" - } - } - }, - "request.PodCreateRequest": { - "type": "object", - "required": [ - "challenge_id" - ], - "properties": { - "challenge_id": { - "type": "integer" - }, - "game_id": { - "type": "integer" - }, - "team_id": { - "type": "integer" - } - } - }, - "request.PodRemoveRequest": { - "type": "object", - "properties": { - "game_id": { - "type": "integer" - }, - "team_id": { - "type": "integer" - } - } - }, - "request.PodRenewRequest": { - "type": "object", - "properties": { - "game_id": { - "type": "integer" - }, - "team_id": { - "type": "integer" - } - } - }, - "request.SubmissionCreateRequest": { - "type": "object", - "required": [ - "challenge_id", - "flag" - ], - "properties": { - "challenge_id": { - "description": "题目 Id", - "type": "integer" - }, - "flag": { - "description": "提交内容", - "type": "string" - }, - "game_id": { - "description": "比赛 Id", - "type": "integer" - }, - "team_id": { - "description": "团队 Id", - "type": "integer" - } - } - }, - "request.SubmissionDeleteRequest": { - "type": "object", - "required": [ - "id" - ], - "properties": { - "id": { - "type": "integer" - } - } - }, - "request.TeamCreateRequest": { - "type": "object", - "required": [ - "captain_id", - "name" - ], - "properties": { - "captain_id": { - "type": "integer" - }, - "description": { - "type": "string" - }, - "email": { - "type": "string" - }, - "name": { - "type": "string" - } - } - }, - "request.TeamDeleteRequest": { - "type": "object" - }, - "request.TeamUpdateRequest": { - "type": "object", - "properties": { - "captain_id": { - "type": "integer" - }, - "description": { - "type": "string" - }, - "email": { - "type": "string" - }, - "is_locked": { - "type": "boolean" - }, - "name": { - "type": "string" - } - } - }, - "request.TeamUserCreateRequest": { - "type": "object", - "properties": { - "invite_token": { - "type": "string" - }, - "user_id": { - "type": "integer" - } - } - }, - "request.TeamUserDeleteRequest": { - "type": "object", - "required": [ - "team_id", - "user_id" - ], - "properties": { - "team_id": { - "type": "integer" - }, - "user_id": { - "type": "integer" - } - } - }, - "request.UserCreateRequest": { - "type": "object", - "required": [ - "email", - "nickname", - "password", - "username" - ], - "properties": { - "email": { - "type": "string" - }, - "group": { - "type": "string" - }, - "nickname": { - "type": "string", - "minLength": 2 - }, - "password": { - "type": "string", - "minLength": 6 - }, - "username": { - "type": "string", - "maxLength": 20, - "minLength": 3 - } - } - }, - "request.UserDeleteRequest": { - "type": "object" - }, - "request.UserLoginRequest": { - "type": "object", - "required": [ - "password", - "username" - ], - "properties": { - "password": { - "type": "string" - }, - "username": { - "type": "string" - } - } - }, - "request.UserRegisterRequest": { - "type": "object", - "required": [ - "email", - "nickname", - "password", - "username" - ], - "properties": { - "email": { - "type": "string" - }, - "nickname": { - "type": "string" - }, - "password": { - "type": "string" - }, - "token": { - "type": "string" - }, - "username": { - "type": "string" - } - } - }, - "request.UserUpdateRequest": { - "type": "object", - "properties": { - "email": { - "type": "string" - }, - "group": { - "type": "string" - }, - "nickname": { - "type": "string", - "minLength": 2 - }, - "password": { - "type": "string", - "minLength": 6 - }, - "username": { - "type": "string", - "maxLength": 20, - "minLength": 3 - } - } - } - } -} \ No newline at end of file diff --git a/api/swagger.yaml b/api/swagger.yaml deleted file mode 100644 index f6a97526..00000000 --- a/api/swagger.yaml +++ /dev/null @@ -1,1948 +0,0 @@ -basePath: /api -definitions: - model.Category: - properties: - color: - description: The category's theme color. (Such as Rainbow Dash's color is - "#60AEE4") - type: string - created_at: - description: The category's creation time. - type: integer - description: - description: The category's description. - type: string - icon: - description: 'The category''s icon. (Based on Material Design Icons, Reference - site: https://pictogrammers.com/library/mdi/) (Such as "fingerprint": https://pictogrammers.com/library/mdi/icon/fingerprint/)' - type: string - id: - description: The category's id. As primary key. - type: integer - name: - description: The category's name. - type: string - updated_at: - description: The category's last update time. - type: integer - type: object - model.Challenge: - properties: - attachment: - allOf: - - $ref: '#/definitions/model.File' - description: The challenge's attachment. - bloods: - items: - $ref: '#/definitions/model.Submission' - type: array - category: - allOf: - - $ref: '#/definitions/model.Category' - description: The challenge's category. - category_id: - description: The challenge's category. - type: integer - cpu_limit: - description: The challenge's CPU limit. (0 means no limit) - type: integer - created_at: - description: The challenge's creation time. - type: integer - description: - description: The challenge's description. - type: string - difficulty: - description: The degree of difficulty. (From 1 to 5) - type: integer - duration: - description: The duration of container maintenance in the initial state. (Seconds) - type: integer - envs: - items: - $ref: '#/definitions/model.Env' - type: array - flags: - items: - $ref: '#/definitions/model.Flag' - type: array - id: - description: The challenge's id. As primary key. - type: integer - image_name: - description: The challenge's image name. - type: string - is_dynamic: - description: Whether the challenge is based on dynamic container. - type: boolean - is_practicable: - description: Whether the challenge is practicable. (Is the practice field - visible.) - type: boolean - memory_limit: - description: The challenge's memory limit. (0 means no limit) - type: integer - ports: - items: - $ref: '#/definitions/model.Port' - type: array - practice_pts: - description: The points will be given when the challenge is solved in practice - field. - type: integer - solved: - $ref: '#/definitions/model.Submission' - solved_times: - type: integer - title: - description: The challenge's title. - type: string - updated_at: - description: The challenge's last update time. - type: integer - type: object - model.Env: - properties: - challenge: - $ref: '#/definitions/model.Challenge' - challenge_id: - type: integer - id: - type: integer - key: - type: string - value: - type: string - type: object - model.File: - properties: - name: - type: string - size: - type: integer - type: object - model.Flag: - properties: - banned: - description: Whether the flag is banned. If banned, the user who submitted - the flag will be judged as cheating. - type: boolean - challenge: - allOf: - - $ref: '#/definitions/model.Challenge' - description: The challenge which the flag belongs to. - challenge_id: - description: The challenge id. The flag belongs to. - type: integer - env: - description: The environment variable which is used to be injected with the - flag. - type: string - id: - description: The flag id. - type: integer - type: - description: The flag type. ("static"/"dynamic"/"pattern") - type: string - value: - description: The flag content. Maybe a string or a regex, or the placeholder - for dynamic challenges. (Such as "flag{friendsh1p_1s_magic}" or "flag{[a-zA-Z]{5}}" - or "flag{[UUID]}") - type: string - type: object - model.Game: - properties: - bio: - description: The game's short description. - type: string - created_at: - description: The game's creation time. - type: integer - description: - description: The game's description. (Markdown supported.) - type: string - ended_at: - description: The game's end time. (Unix) - type: integer - first_blood_reward_ratio: - description: The prize ratio of first blood. - type: number - id: - description: The game's id. As primary key. - type: integer - is_enabled: - description: Whether the game is enabled. - type: boolean - is_need_write_up: - description: Whether the game need write up. - type: boolean - is_public: - description: Whether the game is public. - type: boolean - member_limit_max: - description: The maximum team member limit. - type: integer - member_limit_min: - description: The minimum team member limit. - type: integer - parallel_container_limit: - description: The maximum parallel container limit. - type: integer - poster: - allOf: - - $ref: '#/definitions/model.File' - description: The game's poster image. - public_key: - description: The game's public key. - type: string - second_blood_reward_ratio: - description: The prize ratio of second blood. - type: number - started_at: - description: The game's start time. (Unix) - type: integer - third_blood_reward_ratio: - description: The prize ratio of third blood. - type: number - title: - description: The game's title. - type: string - updated_at: - description: The game's last update time. - type: integer - type: object - model.GameChallenge: - properties: - challenge: - $ref: '#/definitions/model.Challenge' - challenge_id: - type: integer - game: - $ref: '#/definitions/model.Game' - game_id: - type: integer - id: - type: integer - is_enabled: - type: boolean - max_pts: - type: integer - min_pts: - type: integer - pts: - type: integer - type: object - model.Port: - properties: - challenge: - allOf: - - $ref: '#/definitions/model.Challenge' - description: The JeopardyImage which the port belongs to. - challenge_id: - description: The JeopardyImage which the port belongs to. - type: integer - description: - description: The port's description. - type: string - id: - description: The port's id. As primary key. - type: integer - value: - description: The port number. - type: integer - type: object - model.Submission: - properties: - challenge: - allOf: - - $ref: '#/definitions/model.Challenge' - description: The challenge which is related to this submission. - challenge_id: - description: The challenge which is related to this submission. - type: integer - created_at: - description: The submission's creation time. - type: integer - flag: - description: The flag which was submitted for judgement. - type: string - game: - allOf: - - $ref: '#/definitions/model.Game' - description: The game which is related to this submission. - game_challenge: - allOf: - - $ref: '#/definitions/model.GameChallenge' - description: The game_challenge which is related to this submission. - game_challenge_id: - description: The game_challenge which is related to this submission. - type: integer - game_id: - description: The game which is related to this submission. (Must be set when - TeamID is set) - type: integer - id: - description: The submission's id. As primary key. - type: integer - pts: - description: The points of the submission. - type: integer - rank: - description: The rank of the submission. - type: integer - status: - description: The status of the submission. (0-meaningless, 1-accepted, 2-incorrect, - 3-cheat, 4-invalid(duplicate, etc.)) - type: integer - team: - allOf: - - $ref: '#/definitions/model.Team' - description: The team which submitted the flag. - team_id: - description: The team which submitted the flag. (Must be set when GameID is - set) - type: integer - updated_at: - description: The submission's last update time. - type: integer - user: - allOf: - - $ref: '#/definitions/model.User' - description: The user who submitted the flag. - user_id: - description: The user who submitted the flag. - type: integer - type: object - model.Team: - properties: - avatar: - allOf: - - $ref: '#/definitions/model.File' - description: The team's avatar. - captain: - allOf: - - $ref: '#/definitions/model.User' - description: The captain's user. - captain_id: - description: The captain's id. - type: integer - created_at: - description: The team's creation time. - type: integer - description: - description: The team's description. - type: string - email: - description: The team's email. - type: string - id: - description: The team's id. As primary key. - type: integer - invite_token: - description: The team's invite token. - type: string - is_locked: - description: Whether the team is locked. (true/false) - type: boolean - name: - description: The team's name. - type: string - updated_at: - description: The team's last update time. - type: integer - users: - description: The team's users. - items: - $ref: '#/definitions/model.User' - type: array - type: object - model.User: - properties: - avatar: - allOf: - - $ref: '#/definitions/model.File' - description: The user's avatar. - created_at: - description: The user's creation time. - type: integer - description: - description: The user's description. - type: string - email: - description: The user's email. - type: string - group: - description: The user's group. - type: string - id: - description: The user's id. As primary key. - type: integer - nickname: - description: The user's nickname. Not unique. - type: string - password: - description: The user's password. Crypt. - type: string - remote_ip: - description: The user's remote ip. - type: string - teams: - description: The user's teams. - items: - $ref: '#/definitions/model.Team' - type: array - updated_at: - description: The user's last update time. - type: integer - username: - description: The user's username. As a unique identifier. - type: string - type: object - request.CategoryCreateRequest: - properties: - color: - type: string - description: - type: string - icon: - type: string - name: - type: string - required: - - color - - description - - icon - - name - type: object - request.CategoryDeleteRequest: - properties: - id: - type: integer - type: object - request.CategoryUpdateRequest: - properties: - color: - type: string - description: - type: string - icon: - type: string - id: - type: integer - name: - type: string - required: - - color - - description - - icon - - id - - name - type: object - request.ChallengeCreateRequest: - properties: - attachment_url: - type: string - category_id: - type: integer - cpu_limit: - type: integer - description: - type: string - difficulty: - type: integer - duration: - type: integer - envs: - items: - $ref: '#/definitions/model.Env' - type: array - has_attachment: - type: boolean - image_name: - type: string - is_dynamic: - type: boolean - is_practicable: - type: boolean - memory_limit: - type: integer - ports: - items: - $ref: '#/definitions/model.Port' - type: array - practice_pts: - type: integer - title: - type: string - type: object - request.ChallengeDeleteRequest: - type: object - request.ChallengeUpdateRequest: - properties: - attachment_url: - type: string - category_id: - type: integer - cpu_limit: - type: integer - description: - type: string - difficulty: - type: integer - duration: - type: integer - envs: - items: - $ref: '#/definitions/model.Env' - type: array - has_attachment: - type: boolean - image_name: - type: string - is_dynamic: - type: boolean - is_practicable: - type: boolean - memory_limit: - type: integer - ports: - items: - $ref: '#/definitions/model.Port' - type: array - practice_pts: - type: integer - title: - type: string - type: object - request.ConfigUpdateRequest: - properties: - container: - properties: - parallel_limit: - type: integer - request_limit: - type: integer - type: object - site: - properties: - color: - type: string - description: - type: string - title: - type: string - type: object - user: - properties: - register: - properties: - captcha: - properties: - enabled: - type: boolean - type: object - email: - properties: - domains: - items: - type: string - type: array - enabled: - type: boolean - type: object - enabled: - type: boolean - type: object - type: object - type: object - request.GameCreateRequest: - properties: - bio: - type: string - cover_url: - type: string - description: - type: string - ended_at: - type: integer - first_blood_reward_ratio: - type: number - is_enabled: - type: boolean - is_need_write_up: - type: boolean - is_public: - type: boolean - member_limit_max: - type: integer - member_limit_min: - type: integer - parallel_container_limit: - type: integer - second_blood_reward_ratio: - type: number - started_at: - type: integer - third_blood_reward_ratio: - type: number - title: - type: string - required: - - title - type: object - request.GameDeleteRequest: - type: object - request.GameTeamCreateRequest: - properties: - password: - type: string - team_id: - type: integer - user_id: - type: integer - type: object - request.GameTeamUpdateRequest: - properties: - is_allowed: - type: boolean - type: object - request.GameUpdateRequest: - properties: - bio: - type: string - cover_url: - type: string - description: - type: string - ended_at: - type: integer - first_blood_reward_ratio: - type: number - is_enabled: - type: boolean - is_need_write_up: - type: boolean - is_public: - type: boolean - member_limit_max: - type: integer - member_limit_min: - type: integer - parallel_container_limit: - type: integer - second_blood_reward_ratio: - type: number - started_at: - type: integer - third_blood_reward_ratio: - type: number - title: - type: string - type: object - request.PodCreateRequest: - properties: - challenge_id: - type: integer - game_id: - type: integer - team_id: - type: integer - required: - - challenge_id - type: object - request.PodRemoveRequest: - properties: - game_id: - type: integer - team_id: - type: integer - type: object - request.PodRenewRequest: - properties: - game_id: - type: integer - team_id: - type: integer - type: object - request.SubmissionCreateRequest: - properties: - challenge_id: - description: 题目 Id - type: integer - flag: - description: 提交内容 - type: string - game_id: - description: 比赛 Id - type: integer - team_id: - description: 团队 Id - type: integer - required: - - challenge_id - - flag - type: object - request.SubmissionDeleteRequest: - properties: - id: - type: integer - required: - - id - type: object - request.TeamCreateRequest: - properties: - captain_id: - type: integer - description: - type: string - email: - type: string - name: - type: string - required: - - captain_id - - name - type: object - request.TeamDeleteRequest: - type: object - request.TeamUpdateRequest: - properties: - captain_id: - type: integer - description: - type: string - email: - type: string - is_locked: - type: boolean - name: - type: string - type: object - request.TeamUserCreateRequest: - properties: - invite_token: - type: string - user_id: - type: integer - type: object - request.TeamUserDeleteRequest: - properties: - team_id: - type: integer - user_id: - type: integer - required: - - team_id - - user_id - type: object - request.UserCreateRequest: - properties: - email: - type: string - group: - type: string - nickname: - minLength: 2 - type: string - password: - minLength: 6 - type: string - username: - maxLength: 20 - minLength: 3 - type: string - required: - - email - - nickname - - password - - username - type: object - request.UserDeleteRequest: - type: object - request.UserLoginRequest: - properties: - password: - type: string - username: - type: string - required: - - password - - username - type: object - request.UserRegisterRequest: - properties: - email: - type: string - nickname: - type: string - password: - type: string - token: - type: string - username: - type: string - required: - - email - - nickname - - password - - username - type: object - request.UserUpdateRequest: - properties: - email: - type: string - group: - type: string - nickname: - minLength: 2 - type: string - password: - minLength: 6 - type: string - username: - maxLength: 20 - minLength: 3 - type: string - type: object -info: - contact: {} - title: Cloudsdale -paths: - /categories/: - get: - consumes: - - application/json - parameters: - - in: query - name: id - type: integer - - in: query - name: name - type: string - produces: - - application/json - responses: {} - security: - - ApiKeyAuth: [] - summary: get category - tags: - - Category - post: - consumes: - - application/json - parameters: - - description: CategoryCreateRequest - in: body - name: req - required: true - schema: - $ref: '#/definitions/request.CategoryCreateRequest' - produces: - - application/json - responses: {} - security: - - ApiKeyAuth: [] - summary: create new category - tags: - - Category - /categories/{id}: - delete: - consumes: - - application/json - parameters: - - description: CategoryDeleteRequest - in: body - name: req - required: true - schema: - $ref: '#/definitions/request.CategoryDeleteRequest' - produces: - - application/json - responses: {} - security: - - ApiKeyAuth: [] - summary: delete category - tags: - - Category - put: - consumes: - - application/json - parameters: - - description: CategoryUpdateRequest - in: body - name: req - required: true - schema: - $ref: '#/definitions/request.CategoryUpdateRequest' - produces: - - application/json - responses: {} - security: - - ApiKeyAuth: [] - summary: update category - tags: - - Category - /challenges/: - get: - consumes: - - application/json - parameters: - - in: query - name: category_id - type: integer - - in: query - name: difficulty - type: integer - - in: query - name: game_id - type: integer - - in: query - name: id - type: integer - - in: query - name: is_detailed - type: boolean - - in: query - name: is_dynamic - type: boolean - - in: query - name: is_practicable - type: boolean - - in: query - name: page - type: integer - - in: query - name: size - type: integer - - in: query - name: sort_key - type: string - - in: query - name: sort_order - type: string - - in: query - name: team_id - type: integer - - in: query - name: title - type: string - - in: query - name: user_id - type: integer - produces: - - application/json - responses: {} - security: - - ApiKeyAuth: [] - summary: 题目查询 - tags: - - Challenge - post: - consumes: - - application/json - parameters: - - description: ChallengeCreateRequest - in: body - name: 创建请求 - required: true - schema: - $ref: '#/definitions/request.ChallengeCreateRequest' - produces: - - application/json - responses: {} - security: - - ApiKeyAuth: [] - summary: 创建题目 - tags: - - Challenge - /challenges/{id}: - delete: - consumes: - - application/json - parameters: - - description: ChallengeDeleteRequest - in: body - name: request - required: true - schema: - $ref: '#/definitions/request.ChallengeDeleteRequest' - produces: - - application/json - responses: {} - security: - - ApiKeyAuth: [] - summary: 删除题目 - tags: - - Challenge - put: - consumes: - - application/json - parameters: - - description: ChallengeUpdateRequest - in: body - name: request - required: true - schema: - $ref: '#/definitions/request.ChallengeUpdateRequest' - produces: - - application/json - responses: {} - security: - - ApiKeyAuth: [] - summary: 更新题目 - tags: - - Challenge - /challenges/{id}/attachment: - delete: - consumes: - - application/json - produces: - - application/json - responses: {} - security: - - ApiKeyAuth: [] - summary: 删除附件 - tags: - - Challenge - post: - consumes: - - application/json - parameters: - - description: attachment - in: formData - name: file - required: true - type: file - produces: - - application/json - responses: {} - security: - - ApiKeyAuth: [] - summary: 保存附件 - tags: - - Challenge - /challenges/{id}/flags: - post: - consumes: - - application/json - produces: - - application/json - responses: {} - security: - - ApiKeyAuth: [] - summary: 创建 flag - tags: - - Challenge - /challenges/{id}/flags/{flag_id}: - delete: - consumes: - - application/json - produces: - - application/json - responses: {} - security: - - ApiKeyAuth: [] - summary: 删除 flag - tags: - - Challenge - put: - consumes: - - application/json - produces: - - application/json - responses: {} - security: - - ApiKeyAuth: [] - summary: 更新 flag - tags: - - Challenge - /configs/: - get: - consumes: - - application/json - description: 配置全部查询 - produces: - - application/json - responses: {} - summary: 配置全部查询 - tags: - - Config - put: - consumes: - - application/json - description: 更新配置 - parameters: - - description: body - in: body - name: input - required: true - schema: - $ref: '#/definitions/request.ConfigUpdateRequest' - produces: - - application/json - responses: {} - security: - - ApiKeyAuth: [] - summary: 更新配置 - tags: - - Config - /configs/captcha: - get: - consumes: - - application/json - description: Captcha 配置查询 - produces: - - application/json - responses: {} - summary: Captcha 配置查询 - tags: - - Config - /games/: - get: - consumes: - - application/json - parameters: - - in: query - name: id - type: integer - - in: query - name: is_enabled - type: boolean - - in: query - name: page - type: integer - - in: query - name: size - type: integer - - in: query - name: sort_key - type: string - - in: query - name: sort_order - type: string - - in: query - name: title - type: string - produces: - - application/json - responses: {} - security: - - ApiKeyAuth: [] - summary: 比赛查询 - tags: - - Game - post: - consumes: - - application/json - parameters: - - description: GameCreateRequest - in: body - name: 创建请求 - required: true - schema: - $ref: '#/definitions/request.GameCreateRequest' - produces: - - application/json - responses: {} - security: - - ApiKeyAuth: [] - summary: 创建比赛 - tags: - - Game - /games/{id}: - delete: - consumes: - - application/json - parameters: - - description: GameDeleteRequest - in: body - name: 删除请求 - required: true - schema: - $ref: '#/definitions/request.GameDeleteRequest' - produces: - - application/json - responses: {} - security: - - ApiKeyAuth: [] - summary: 删除比赛 - tags: - - Game - put: - consumes: - - application/json - parameters: - - description: GameUpdateRequest - in: body - name: 更新请求 - required: true - schema: - $ref: '#/definitions/request.GameUpdateRequest' - produces: - - application/json - responses: {} - security: - - ApiKeyAuth: [] - summary: 更新比赛 - tags: - - Game - /games/{id}/broadcast: - get: - description: 广播消息 - responses: {} - summary: 广播消息 - tags: - - Game - /games/{id}/challenges: - get: - consumes: - - application/json - produces: - - application/json - responses: {} - security: - - ApiKeyAuth: [] - summary: 查询比赛的挑战 - tags: - - Game - post: - consumes: - - application/json - produces: - - application/json - responses: {} - security: - - ApiKeyAuth: [] - summary: 添加比赛的挑战 - tags: - - Game - /games/{id}/challenges/{challenge_id}: - delete: - consumes: - - application/json - produces: - - application/json - responses: {} - security: - - ApiKeyAuth: [] - summary: 删除比赛的挑战 - tags: - - Game - put: - consumes: - - application/json - produces: - - application/json - responses: {} - security: - - ApiKeyAuth: [] - summary: 更新比赛的挑战 - tags: - - Game - /games/{id}/notices: - get: - consumes: - - application/json - produces: - - application/json - responses: {} - security: - - ApiKeyAuth: [] - summary: 查询比赛的通知 - tags: - - Game - post: - consumes: - - application/json - produces: - - application/json - responses: {} - security: - - ApiKeyAuth: [] - summary: 添加比赛的通知 - tags: - - Game - /games/{id}/notices/{notice_id}: - delete: - consumes: - - application/json - produces: - - application/json - responses: {} - security: - - ApiKeyAuth: [] - summary: 删除比赛的通知 - tags: - - Game - put: - consumes: - - application/json - produces: - - application/json - responses: {} - security: - - ApiKeyAuth: [] - summary: 更新比赛的通知 - tags: - - Game - /games/{id}/poster: - delete: - consumes: - - application/json - produces: - - application/json - responses: {} - security: - - ApiKeyAuth: [] - summary: 删除海报 - tags: - - Game - post: - consumes: - - application/json - parameters: - - description: poster - in: formData - name: file - required: true - type: file - produces: - - application/json - responses: {} - security: - - ApiKeyAuth: [] - summary: 保存头图 - tags: - - Game - /games/{id}/teams: - get: - consumes: - - application/json - produces: - - application/json - responses: {} - security: - - ApiKeyAuth: [] - summary: 查询比赛的团队 - tags: - - Game - post: - consumes: - - application/json - parameters: - - description: GameTeamCreateRequest - in: body - name: 加入请求 - required: true - schema: - $ref: '#/definitions/request.GameTeamCreateRequest' - produces: - - application/json - responses: {} - security: - - ApiKeyAuth: [] - summary: 加入比赛 - tags: - - Game - /games/{id}/teams/{team_id}: - delete: - consumes: - - application/json - produces: - - application/json - responses: {} - security: - - ApiKeyAuth: [] - summary: 删除比赛的团队 - tags: - - Game - put: - consumes: - - application/json - parameters: - - description: GameTeamUpdateRequest - in: body - name: 允许加入请求 - required: true - schema: - $ref: '#/definitions/request.GameTeamUpdateRequest' - produces: - - application/json - responses: {} - security: - - ApiKeyAuth: [] - summary: 允许加入比赛 - tags: - - Game - /pods/: - get: - description: 实例查询 - parameters: - - in: query - name: challenge_id - type: integer - - in: query - name: game_id - type: integer - - in: query - name: id - type: integer - - in: query - name: is_available - type: boolean - - in: query - name: page - type: integer - - in: query - name: size - type: integer - - in: query - name: team_id - type: integer - - in: query - name: user_id - type: integer - produces: - - application/json - responses: {} - security: - - ApiKeyAuth: [] - summary: 实例查询 - tags: - - Pod - post: - consumes: - - application/json - description: 创建实例 - parameters: - - description: PodCreateRequest - in: body - name: input - required: true - schema: - $ref: '#/definitions/request.PodCreateRequest' - produces: - - application/json - responses: {} - security: - - ApiKeyAuth: [] - summary: 创建实例 - tags: - - Pod - /pods/{id}: - delete: - description: 停止并删除容器 - parameters: - - description: PodRemoveRequest - in: body - name: input - required: true - schema: - $ref: '#/definitions/request.PodRemoveRequest' - produces: - - application/json - responses: {} - security: - - ApiKeyAuth: [] - summary: 停止并删除容器 - tags: - - Pod - put: - description: 容器续期 - parameters: - - description: PodRenewRequest - in: body - name: input - required: true - schema: - $ref: '#/definitions/request.PodRenewRequest' - produces: - - application/json - responses: {} - security: - - ApiKeyAuth: [] - summary: 容器续期 - tags: - - Pod - /submissions/: - get: - consumes: - - application/json - parameters: - - description: 题目 Id - in: query - name: challenge_id - type: integer - - description: 比赛 Id - in: query - name: game_id - type: integer - - description: 是否详细 - in: query - name: is_detailed - type: boolean - - description: 页码 - in: query - name: page - type: integer - - description: 每页大小 - in: query - name: size - type: integer - - description: 排序参数 - in: query - name: sort_key - type: string - - description: 排序方式 - in: query - name: sort_order - type: string - - description: 评判结果 - in: query - name: status - type: integer - - description: 团队 Id - in: query - name: team_id - type: integer - - description: 用户 Id - in: query - name: user_id - type: integer - produces: - - application/json - responses: {} - security: - - ApiKeyAuth: [] - summary: 提交记录查询 - tags: - - Submission - post: - consumes: - - application/json - parameters: - - description: SubmissionCreateRequest - in: body - name: 创建请求 - required: true - schema: - $ref: '#/definitions/request.SubmissionCreateRequest' - produces: - - application/json - responses: {} - security: - - ApiKeyAuth: [] - summary: 提交 - tags: - - Submission - /submissions/{id}: - delete: - consumes: - - application/json - parameters: - - description: SubmissionDeleteRequest - in: body - name: 删除请求 - required: true - schema: - $ref: '#/definitions/request.SubmissionDeleteRequest' - produces: - - application/json - responses: {} - security: - - ApiKeyAuth: [] - summary: delete submission - tags: - - Submission - /teams/: - get: - consumes: - - application/json - description: 查找团队 - parameters: - - in: query - name: captain_id - type: integer - - in: query - name: game_id - type: integer - - in: query - name: id - type: integer - - in: query - name: name - type: string - - in: query - name: page - type: integer - - in: query - name: size - type: integer - - in: query - name: sort_key - type: string - - in: query - name: sort_order - type: string - - in: query - name: user_id - type: integer - produces: - - application/json - responses: {} - summary: 查找团队 - tags: - - Team - post: - consumes: - - application/json - description: 创建团队 - parameters: - - description: TeamCreateRequest - in: body - name: input - required: true - schema: - $ref: '#/definitions/request.TeamCreateRequest' - produces: - - application/json - responses: {} - summary: 创建团队 - tags: - - Team - /teams/{id}: - delete: - consumes: - - application/json - description: 删除团队 - parameters: - - description: TeamDeleteRequest - in: body - name: input - required: true - schema: - $ref: '#/definitions/request.TeamDeleteRequest' - produces: - - application/json - responses: {} - summary: 删除团队 - tags: - - Team - put: - consumes: - - application/json - description: 更新团队 - parameters: - - description: TeamUpdateRequest - in: body - name: input - required: true - schema: - $ref: '#/definitions/request.TeamUpdateRequest' - produces: - - application/json - responses: {} - summary: 更新团队 - tags: - - Team - /teams/{id}/avatar: - delete: - consumes: - - application/json - produces: - - application/json - responses: {} - security: - - ApiKeyAuth: [] - summary: 删除头像 - tags: - - Challenge - post: - consumes: - - application/json - parameters: - - description: avatar - in: formData - name: file - required: true - type: file - produces: - - application/json - responses: {} - security: - - ApiKeyAuth: [] - summary: 保存头像 - tags: - - Challenge - /teams/{id}/invite: - get: - consumes: - - application/json - description: 获取邀请码 - parameters: - - description: id - in: path - name: id - required: true - type: string - produces: - - application/json - responses: {} - summary: 获取邀请码 - tags: - - Team - put: - consumes: - - application/json - description: 更新邀请码 - parameters: - - description: id - in: path - name: id - required: true - type: string - produces: - - application/json - responses: {} - summary: 更新邀请码 - tags: - - Team - /teams/{id}/join: - post: - consumes: - - application/json - description: 加入团队 - parameters: - - description: id - in: path - name: id - required: true - type: string - produces: - - application/json - responses: {} - summary: 加入团队 - tags: - - Team - /teams/{id}/leave: - delete: - consumes: - - application/json - description: 离开团队 - parameters: - - description: id - in: path - name: id - required: true - type: string - produces: - - application/json - responses: {} - summary: 离开团队 - tags: - - Team - /teams/{id}/users/: - post: - consumes: - - application/json - description: 加入团队 - parameters: - - description: TeamUserCreateRequest - in: body - name: input - required: true - schema: - $ref: '#/definitions/request.TeamUserCreateRequest' - produces: - - application/json - responses: {} - summary: 加入团队 - tags: - - Team - /teams/{id}/users/{user_id}: - delete: - consumes: - - application/json - description: 踢出团队 - parameters: - - description: TeamUserDeleteRequest - in: body - name: input - required: true - schema: - $ref: '#/definitions/request.TeamUserDeleteRequest' - produces: - - application/json - responses: {} - summary: 踢出团队 - tags: - - Team - /users/: - get: - consumes: - - application/json - parameters: - - in: query - name: email - type: string - - in: query - name: group - type: string - - in: query - name: id - type: integer - - in: query - name: name - type: string - - in: query - name: page - type: integer - - in: query - name: size - type: integer - - in: query - name: sort_key - type: string - - in: query - name: sort_order - type: string - - in: query - name: username - type: string - produces: - - application/json - responses: {} - summary: 用户查询 - tags: - - User - post: - consumes: - - application/json - parameters: - - description: UserCreateRequest - in: body - name: 创建请求 - required: true - schema: - $ref: '#/definitions/request.UserCreateRequest' - produces: - - application/json - responses: {} - security: - - ApiKeyAuth: [] - summary: 用户创建 - tags: - - User - /users/{id}: - delete: - consumes: - - application/json - parameters: - - description: UserDeleteRequest - in: body - name: input - required: true - schema: - $ref: '#/definitions/request.UserDeleteRequest' - produces: - - application/json - responses: {} - security: - - ApiKeyAuth: [] - summary: 用户删除 - tags: - - User - put: - consumes: - - application/json - parameters: - - description: UserUpdateRequest - in: body - name: 更新请求 - required: true - schema: - $ref: '#/definitions/request.UserUpdateRequest' - produces: - - application/json - responses: {} - security: - - ApiKeyAuth: [] - summary: 用户更新 - tags: - - User - /users/{id}/avatar: - delete: - consumes: - - application/json - produces: - - application/json - responses: {} - security: - - ApiKeyAuth: [] - summary: 删除头像 - tags: - - Challenge - post: - consumes: - - application/json - parameters: - - description: avatar - in: formData - name: file - required: true - type: file - produces: - - application/json - responses: {} - security: - - ApiKeyAuth: [] - summary: 保存头像 - tags: - - Challenge - /users/login: - post: - consumes: - - application/json - parameters: - - description: UserLoginRequest - in: body - name: 登录请求 - required: true - schema: - $ref: '#/definitions/request.UserLoginRequest' - produces: - - application/json - responses: {} - summary: 用户登录 - tags: - - User - /users/logout: - post: - consumes: - - application/json - produces: - - application/json - responses: {} - security: - - ApiKeyAuth: [] - summary: 用户登出 - tags: - - User - /users/register: - post: - consumes: - - application/json - parameters: - - description: UserRegisterRequest - in: body - name: input - required: true - schema: - $ref: '#/definitions/request.UserRegisterRequest' - produces: - - application/json - responses: {} - summary: 用户注册 - tags: - - User -swagger: "2.0" diff --git a/assets/favicon.webp b/assets/favicon.webp new file mode 100644 index 0000000000000000000000000000000000000000..cdbaf63232ad07ce38e9f2cd3bc70efdeb9a68a8 GIT binary patch literal 13476 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 Spyo>852hgV-2FH4Zp8yT3e$xE literal 0 HcmV?d00001 diff --git a/cmd/cloudsdale/main.go b/cmd/cloudsdale/main.go deleted file mode 100644 index 71379052..00000000 --- a/cmd/cloudsdale/main.go +++ /dev/null @@ -1,12 +0,0 @@ -package main - -import "github.com/elabosak233/cloudsdale/internal/app" - -// @title Cloudsdale -// @securityDefinitions.api_key ApiKeyAuth -// @in header -// @name Authorization -// @BasePath /api -func main() { - app.Run() -} diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml deleted file mode 100644 index a7ca28a9..00000000 --- a/deploy/docker-compose.yml +++ /dev/null @@ -1,27 +0,0 @@ -version: "3.0" -services: - core: - image: elabosak233/cloudsdale:main - restart: always - ports: - - "8888:8888" - volumes: - - "/var/run/docker.sock:/var/run/docker.sock" - - "./configs:/app/configs" - - "./captures:/app/captures" - - "./media:/app/media" - - "./logs:/app/logs" - depends_on: - - db - - db: - image: postgres:alpine - restart: always - ports: - - "5432:5432" - environment: - POSTGRES_USER: cloudsdale - POSTGRES_PASSWORD: cloudsdale - POSTGRES_DB: cloudsdale - volumes: - - "./db:/var/lib/postgresql/data" \ No newline at end of file diff --git a/deploys/docker-compose.yml b/deploys/docker-compose.yml new file mode 100644 index 00000000..3f3558e2 --- /dev/null +++ b/deploys/docker-compose.yml @@ -0,0 +1,27 @@ +version: "3.0" +services: + core: + image: elabosak233/cloudsdale:main + restart: always + ports: + - "8888:8888" + volumes: + - "/var/run/docker.sock:/var/run/docker.sock" + - "./application.yml:/app/application.yml" + - "./captures:/app/captures" + - "./media:/app/media" + - "./logs:/app/logs" + depends_on: + - db + + db: + image: postgres:alpine + restart: always + ports: + - "5432:5432" + environment: + POSTGRES_USER: cloudsdale + POSTGRES_PASSWORD: cloudsdale + POSTGRES_DB: cloudsdale + volumes: + - "./db:/var/lib/postgresql/data" diff --git a/docs/.gitattributes b/docs/.gitattributes deleted file mode 100644 index dfe07704..00000000 --- a/docs/.gitattributes +++ /dev/null @@ -1,2 +0,0 @@ -# Auto detect text files and perform LF normalization -* text=auto diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 00000000..d126fdf4 --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,6 @@ +yarn.lock +node_modules +package-lock.json + +/.vitepress/cache +/.vitepress/dist \ No newline at end of file diff --git a/docs/.vitepress/config/en.mts b/docs/.vitepress/config/en.mts new file mode 100644 index 00000000..956c0a3e --- /dev/null +++ b/docs/.vitepress/config/en.mts @@ -0,0 +1,17 @@ +import { defineConfig } from "vitepress"; + +export default defineConfig({ + lang: "en-US", + description: + "The Cloudsdale project is an open-source, high-performance, Jeopardy-style's CTF platform. ", + themeConfig: { + nav: [{ text: "Guide", link: "/guide/" }], + + sidebar: [ + { + text: "Introduction", + items: [{ text: "What is Cloudsdale?", link: "/guide/" }], + }, + ], + }, +}); diff --git a/docs/.vitepress/config/index.mts b/docs/.vitepress/config/index.mts new file mode 100644 index 00000000..6f3db408 --- /dev/null +++ b/docs/.vitepress/config/index.mts @@ -0,0 +1,32 @@ +import { defineConfig } from "vitepress"; +import en from "./en.mts"; +import zh from "./zh.mts"; + +// https://vitepress.dev/reference/site-config +export default defineConfig({ + title: "Cloudsdale", + description: + "The Cloudsdale project is an open-source, high-performance, Jeopardy-style's CTF platform. ", + head: [ + ["link", { rel: "icon", href: "/favicon.webp", type: "image/webp" }], + ], + rewrites: { + "en/:rest*": ":rest*", + }, + themeConfig: { + logo: { + light: "/favicon.webp", + dark: "/favicon.webp", + }, + socialLinks: [ + { + icon: "github", + link: "https://github.com/elabosak233/cloudsdale", + }, + ], + }, + locales: { + root: { label: "English", ...en }, + zh: { label: "简体中文", ...zh }, + }, +}); diff --git a/docs/.vitepress/config/zh.mts b/docs/.vitepress/config/zh.mts new file mode 100644 index 00000000..f471ee86 --- /dev/null +++ b/docs/.vitepress/config/zh.mts @@ -0,0 +1,28 @@ +import { defineConfig } from "vitepress"; + +export default defineConfig({ + lang: "zh-Hans", + description: "Cloudsdale 是一个开源、高性能的解题模式 CTF 平台", + themeConfig: { + nav: [{ text: "指南", link: "/zh/guide" }], + + sidebar: [ + { + text: "简介", + items: [{ text: "什么是 Cloudsdale?", link: "/zh/guide/" }], + }, + ], + + docFooter: { + prev: "上一页", + next: "下一页", + }, + + outline: { + label: "页面导航", + }, + + lightModeSwitchTitle: "切换到浅色模式", + darkModeSwitchTitle: "切换到深色模式", + }, +}); diff --git a/docs/.vitepress/theme/index.ts b/docs/.vitepress/theme/index.ts new file mode 100644 index 00000000..1d8700fb --- /dev/null +++ b/docs/.vitepress/theme/index.ts @@ -0,0 +1,6 @@ +import Theme from "vitepress/theme"; +import "./style.css"; + +export default { + extends: Theme, +}; diff --git a/docs/.vitepress/theme/style.css b/docs/.vitepress/theme/style.css new file mode 100644 index 00000000..3df145c1 --- /dev/null +++ b/docs/.vitepress/theme/style.css @@ -0,0 +1,151 @@ +/** + * Customize default theme styling by overriding CSS variables: + * https://github.com/vuejs/vitepress/blob/main/src/client/theme-default/styles/vars.css + */ + +/** + * Colors + * + * Each colors have exact same color scale system with 3 levels of solid + * colors with different brightness, and 1 soft color. + * + * - `XXX-1`: The most solid color used mainly for colored text. It must + * satisfy the contrast ratio against when used on top of `XXX-soft`. + * + * - `XXX-2`: The color used mainly for hover state of the button. + * + * - `XXX-3`: The color for solid background, such as bg color of the button. + * It must satisfy the contrast ratio with pure white (#ffffff) text on + * top of it. + * + * - `XXX-soft`: The color used for subtle background such as custom container + * or badges. It must satisfy the contrast ratio when putting `XXX-1` colors + * on top of it. + * + * The soft color must be semi transparent alpha channel. This is crucial + * because it allows adding multiple "soft" colors on top of each other + * to create a accent, such as when having inline code block inside + * custom containers. + * + * - `default`: The color used purely for subtle indication without any + * special meanings attched to it such as bg color for menu hover state. + * + * - `brand`: Used for primary brand colors, such as link text, button with + * brand theme, etc. + * + * - `tip`: Used to indicate useful information. The default theme uses the + * brand color for this by default. + * + * - `warning`: Used to indicate warning to the users. Used in custom + * container, badges, etc. + * + * - `danger`: Used to show error, or dangerous message to the users. Used + * in custom container, badges, etc. + * -------------------------------------------------------------------------- */ + +:root { + --vp-c-default-1: var(--vp-c-gray-1); + --vp-c-default-2: var(--vp-c-gray-2); + --vp-c-default-3: var(--vp-c-gray-3); + --vp-c-default-soft: var(--vp-c-gray-soft); + + --vp-c-brand-1: #3582f5; + --vp-c-brand-2: #4a94f6; + --vp-c-brand-3: #5ea6f7; + --vp-c-brand-soft: rgba(53, 130, 245, 0.2); + + --vp-c-tip-1: var(--vp-c-brand-1); + --vp-c-tip-2: var(--vp-c-brand-2); + --vp-c-tip-3: var(--vp-c-brand-3); + --vp-c-tip-soft: var(--vp-c-brand-soft); + + --vp-c-warning-1: var(--vp-c-yellow-1); + --vp-c-warning-2: var(--vp-c-yellow-2); + --vp-c-warning-3: var(--vp-c-yellow-3); + --vp-c-warning-soft: var(--vp-c-yellow-soft); + + --vp-c-danger-1: var(--vp-c-red-1); + --vp-c-danger-2: var(--vp-c-red-2); + --vp-c-danger-3: var(--vp-c-red-3); + --vp-c-danger-soft: var(--vp-c-red-soft); +} + +/** + * Component: Button + * -------------------------------------------------------------------------- */ + +:root { + --vp-button-brand-border: transparent; + --vp-button-brand-text: var(--vp-c-white); + --vp-button-brand-bg: var(--vp-c-brand-1); + --vp-button-brand-hover-border: transparent; + --vp-button-brand-hover-text: var(--vp-c-white); + --vp-button-brand-hover-bg: var(--vp-c-brand-2); + --vp-button-brand-active-border: transparent; + --vp-button-brand-active-text: var(--vp-c-white); + --vp-button-brand-active-bg: var(--vp-c-brand-3); +} + +/** + * Component: Home + * -------------------------------------------------------------------------- */ + +:root { + --vp-home-hero-name-color: transparent; + --vp-home-hero-name-background: -webkit-linear-gradient( + 200deg, + #509bfd 30%, + #236bd6 + ); + + --vp-home-hero-image-background-image: linear-gradient( + 45deg, + var(--vp-c-brand-3) 50%, + var(--vp-c-brand-1) 50% + ); + --vp-home-hero-image-filter: blur(44px); +} + +@media (min-width: 640px) { + :root { + --vp-home-hero-image-filter: blur(56px); + } +} + +@media (min-width: 960px) { + :root { + --vp-home-hero-image-filter: blur(68px); + } +} + +/** + * Component: Custom Block + * -------------------------------------------------------------------------- */ + +:root { + --vp-custom-block-tip-border: transparent; + --vp-custom-block-tip-text: var(--vp-c-text-1); + --vp-custom-block-tip-bg: var(--vp-c-brand-soft); + --vp-custom-block-tip-code-bg: var(--vp-c-brand-soft); +} + +/** + * Component: Algolia + * -------------------------------------------------------------------------- */ + +.DocSearch { + --docsearch-primary-color: var(--vp-c-brand-1) !important; +} + +.VPNavBarTitle { + transition-property: opacity; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 350ms; +} + +.VPNavBarTitle:hover { + opacity: 0.6; + transition-property: opacity; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 350ms; +} diff --git a/docs/en/guide/index.md b/docs/en/guide/index.md new file mode 100644 index 00000000..fa6bc7cb --- /dev/null +++ b/docs/en/guide/index.md @@ -0,0 +1,32 @@ +# What is Cloudsdale? + +Cloudsdale is a CTF (Capture The Flag) platform built with Rust and uses a Jeopardy-style format. It is extremely lightweight and can be quickly deployed using a _simple_ configuration file. + +The project draws inspiration from CTFd, Cardinal, GZ::CTF, and Ret2Shell, combining the best of each to create this project. Due to the author's unique and exaggerated understanding of software, as well as the limited resources of the author's school, the aim is to create a lightweight and user-friendly CTF platform to provide a great experience for the school's CTF team. + +## Use Cases + +Just like ACM, CTF should have its own customized platform. Cloudsdale is very suitable for organizing small-scale CTF games or for CTF team training. The flexible challenge management function can save and use challenges more efficiently. + +## Features + +- Challenges + - Static Challenges: No target machine, the grading relies on one or more known flag strings, usually dependent on the attachment system. + - Dynamic Challenges: Dynamic target machines, the grading can rely on static flag strings or dynamically generated flags (usually a `UUID`). +- Target Machines + - Multi-port support + - Customizable basic environment variables for custom images + - Customizable container resource requests (memory and CPU) + - Customizable flag injection variable names + - Optional port mapping mode + - Traffic capture implemented through platform proxy +- Competitions + - Customizable challenge scores + - Customizable first, second, and third blood reward ratios + - Ability to disable/enable challenges at any time during the competition, allowing for multiple releases of challenges + - Competition message broadcasting based on Websocket +- Database + - Support for multiple relational databases via SeaORM (PostgreSQL, SQLite3, MySQL) +- Container Support + - Docker + - Kubernetes diff --git a/docs/en/index.md b/docs/en/index.md new file mode 100644 index 00000000..5e86a306 --- /dev/null +++ b/docs/en/index.md @@ -0,0 +1,34 @@ +--- +layout: home + +hero: + name: "Cloudsdale" + text: "An Open Source CTF Platform" + tagline: High-performance, lightweight, and easy to use + image: + src: /favicon.webp + actions: + - theme: brand + text: What is Cloudsdale? + link: /en/guide/ + - theme: alt + text: Quick Start + link: /en/quick-start/ + - theme: alt + text: GitHub + link: https://github.com/elabosak233/cloudsdale + +features: + - title: High Performance + icon: ⚡ + details: Empowered by Rust, minimal performance overhead, and the fastest response speeds. + - title: Customizable + icon: 🎨 + details: Challenges, games, users, teams, and even the platform itself, all offer high levels of customization. + - title: Easy Deployment + icon: 🐋 + details: Binary or Docker image, easily deployable on local machines or cloud servers. + - title: Open Source + icon: 🌟 + details: As an open source project, you are free to view, modify, distribute, and improve it. +--- diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml deleted file mode 100644 index f4258f8b..00000000 --- a/docs/mkdocs.yml +++ /dev/null @@ -1,50 +0,0 @@ -site_name: Cloudsdale - -repo_url: https://github.com/elabosak233/cloudsdale -repo_name: elabosak233/cloudsdale - -docs_dir: pages - -theme: - name: material - language: zh - logo: assets/favicon.ico - favicon: assets/favicon.ico - icon: - repo: fontawesome/brands/github - palette: - primary: custom - font: - code: JetBrains Mono - features: - - navigation.instant - - navigation.expand - - navigation.path - - navigation.top - - navigation.indexes - -extra_css: - - stylesheets/extra.css - -plugins: - - glightbox - -markdown_extensions: - - admonition - - pymdownx.details - - pymdownx.superfences - -nav: - - 简介: index.md - - 部署: - - Docker: deploy/docker.md - - Docker + K8s: deploy/docker-k8s.md - - K8s: deploy/k8s.md - - 配置: - - config/index.md - - 数据库: config/database.md - - 缓存: config/cache.md - - 代理与流量捕获: config/proxy/index.md - - 题目: challenge/index.md - - 比赛: game/index.md - - FAQ: faq/index.md \ No newline at end of file diff --git a/docs/package.json b/docs/package.json index 418578bc..2e2d9a61 100644 --- a/docs/package.json +++ b/docs/package.json @@ -1,9 +1,10 @@ { - "name": "cloudsdale.docs", - "version": "1.0.0", - "private": true, - "scripts": { - "dev": "mkdocs serve", - "build": "mkdocs build -d public" - } -} \ No newline at end of file + "scripts": { + "dev": "vitepress dev", + "build": "vitepress build", + "preview": "vitepress preview" + }, + "devDependencies": { + "vitepress": "^1.3.1" + } +} diff --git a/docs/pages/assets/favicon.ico b/docs/pages/assets/favicon.ico deleted file mode 100644 index e63fc3d0a35641af107f9e467da7d84f234d957f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18390 zcmWh!1ymH@8{J)&&Xo@75|EITTv{asNfA&$5F|vpcIlN4NreRw_#p^L_tGgLARSAC z)Y7%={`t?GIWu$Syl>9D_vV}X-FpWB0N$i|(j33J`>F>QVNE zBH(hXlMctUgHrtleu#cYWFT-coWN(5kXHMvyed{bUMN)s^r1KfKvT{&mk!IxsU+%# z^vSyi@*bZ26r#m*Ai&+;57*6Bc!#LiDsMg(qJ+ypAG&{$9dqz?$~=eE13MX?N65up z{vtXq#qg~@=r+`V^RmfsLj@D$rZ@rGll~#FZXJZX7iX~dCBrZB6>cz;?L%27*6ho^ z4kc%Z14Xb`-mmy@v)o~omrZkW0rsC5E!jSZe6S4IoNe%1pOfQeE>#~$@?*UV$4)i7 z2t8JYclosE39sz9P|tRR-|C^4PV~z0zbc-N#gZ$!U}c%<{AH+=HD&ZKT|V4BOnU*Z zX}mq2wUcTk45Qx7MZ}}^*l%+iE3U z$sBBdc6cQ&<%ux~9K|YKDoyij{<+Zl&b11`A*K*o2$Xz)Gae@Y;4?=LMEfhtKyIJg zs54@TA|HCH76&cUU{kY#2E|=c9Vx6rKqODBoK})Yl>@*I?VlG!F@Hq^_qpM)^TYU` z`L?#88tVSAboN2iE*xfdb}NNpI*LAZUsAJ5!HJd9@IXF|<7=5|iGHdWm(vh__;>i> zSSacDCYG$*?8+4U?14uv|JVO$_c0Ysa|CUZ4vx`*Bn?AWfb`${$K zzTB^NTFaaD%rDqEk%&=<7-ughh{oW>??+=U*%h$>Ea5Lw;JpNH7Qt6xM5{phzRSK@ z9-Eh#1LSzPBJk+Z>Pzxe-S|c0TVTS>BVeWpjjQvC4b^t7JYTC1ZrsI!3vT}SEkJhf z1qdD0@RJMR3t8!;r0D~9v?zXISfck_p+h?hUo~j+p)!O$A+yFQ0oCoxJtnSPiq1)V zzhS|c_*2Vqm${Jv;fv89)AQxtp>vv?o;o0Lnj@$>fP{8-OWBp1-SR^mWUSP4exAFD zN9JkCuRb=~7(xPd9^!C!Cl)ymEPgU2q^dI7l;X9w$a((+%o|`JwiAfy!zkx6LQwsKZgI-DoiWRV+YJi!R-f zRO1y~u9huuR4C2=MKWXPOx$G!C%X!#PEkDU951T4doHoy1h53dF$~`#Ey3mFI#9NU zTc3JqYtmMIv<&sI7hxV>8y~S>p;vRrPXx5vIWkyGv_t5pcvwsYX`22#Q9NBW)KEcg zWC>?oKfiYbqp@FGugiF{+^NLzmA;W?)|>PFLvr}C8243$Y?&T_ojX?@{PYZD`GW}0 z*Kdd{xYlRykuk!BR8L~K5q+Ps3?BTh!_CsrB0&Ywy&Wem^q8xX^k=Kn`XPEjk$z;! zkr-8}@h3bn(AT$(XQklFInF^E8?x~u%@AazSEdrzy^+0l#5SlUHg=n8!iG;c+!@g| z4eAj)SDUJp#_zOeh)BTX*~Ir(mJX}N#7pAcflb-@VVRri!brB6)&)j$_+eDX_X^S* zh>sfJUri%2Q5N>A2+1Lq_D^{-^74hogoJo1VQ!gRqMJ!Q%JB(N%SBlAajz`pk;x_1 z%0qA-6+3aF&?|p8#9su5cQDQTwL{nHYlug1j_~_0bRNvB;9K|MJST%3m`{@XF4sU((AweDsq}DU*q`OPD-na=Vk$L_?1q5%$_NhC4Rw5Unij&wh{#0?io#{yE2Wl_=})iUA#wMC~lMoZaA7%YbA zek4(hl(rqNZ;O@sdu|p?uD_6kb#dlGc~()yw6hTkhD~VNj_sv~R+YTlt2CF|VKsY6 z4K?jdVa}Oxtq3=q7@H6%tns*t_*O}$Dw>;E-J7IGyn!Az@AhvDKAF7@?Buw~p3)7W zgk&1i5k8Ehw}o85jUq?O&!^aF+f@{9yk2@xkCeHMfADUI+TPhqtzfrzK77TC=-v2P zA&3+gjz_b^UOqce*QE8-iR$iJBWMzvG?#d!+W=%>^BS**xnJ&Br3#q>7I6hJYvxy# zaff=h9L5kz>Ee5zy6Oi|_mA4q*pG%p+2>2OF9A<*I#RC$0+MI|-{AnP`j^a$xMb!DjJ(ET-)q9aF8X)@sN0i@CW(Pkq$xAK{7hKB?UoRe3$Z7sg za{O_Oxgo&ki6*dN7Lf4%*uD(~!BJ+vVO#qSv1RB}>w9YFnjX%L-0Y@QU69f2&f>;e zJ_PdO>3LxkDafvl>@zC+RY!+Y_)W&XkOtl{LGmAZLIJ^667!-Jt;r_I9v!1_f}@ro zJW72qp|1;mtHbTlub+gRIJI^9ZF{-r5Ln`v$At|lC*|c6V7*bgCX{&j(W^0)1}Ld{ z-AXpoNx2j0&i^po&o&`CR>Ss?!sgP)K*!JS9kFT%m8vAjG(D;_T)6HxXa=R_r)~fFq05Ji2M>So3JP z!La*u%3x5Z(QG*&F;90YXyTbAa}hUfzQdVI*RF!w6Lq^tlFTIRhjGD!>qWXX3w_UH ze+95TcUCm?%|%;M#yCOIW^eo8^Bi=^JDkV+T*j8hG9{~5i$qKFGc3wWs}xALk71+r z3Y|IksXQEu34(qNL6qnBN$St#I-(68cg456*yP7&gQA% zZ(E0aFpF7lF)JUm{p<`BW*R4{i{d{;sfGur7H4)0+I}e>H>$kouc!!y)E7FMAm9>i zAp!>J$2uu>eN$h&Ovwi#ik&r@W{c53aeKNp4Q(ToZGk@=uB2TECHq-Y*ZArz8x)Ev{?uag)tS^?d67Xzj3Oc>%x$dj24I?Hw&ZPAp@Ejv_(lU< z%e;MRbfnqJsdTlMzn#iR^$dPLm0;!CQnukn_oMepa>|}`8Xu2!cZL7o@~@a_q4TaU zV3zc*g3>z7FzfINc1fl(#9I72)zgRs)vVt_|7mkW8BGniKvBsmzeJa@LMOSpfn?qB zPj-ojpS5Ns;-Du{6}61{$>U|P`=qcxb=atSUMUg6DjI9;jC?cX`ZWU-OZVp|)}O%} zB+ZU&cr4?p-a8}bUmfIZwnJF;~ zWv@(SzHPbS#E3SEZQA()=hqaqgze@ z(~J7x7h9qKhk}HMGcCYZPYvY4{FqVa5B1#wl%NC(*Qfi&9Hj!wnqVQFs7M9F4X@$3 z*AvnoiDTM*uD2P#HeBAn=pR5c$br7LlEWj~JD&Blt8$09AlbGTd9tp~Z1y}v5+^w( zKP&+VPx>oRPA(?jr@cs>-kVLc-|o6xof2vg*D9csxe%jnI9;s(`UM(y?mU-X9|Ut1 zbLVwA7+GF8CSxy^=VB|ZmdC9ePtUqg%;>^`2Enr5bq$VT z6`KH3!S(U){vH1$6riE{2eB&;j|4a+p_o@`cu+)54*T7S3irrVQ_kNva^B2C+R z+N%Sh-M&4nNQ6P89BR+C=%u&;Z6DvN%`d4xtWF~%(c07REesRgPVl*Iz_(h_8 zSF^SiI;Tq%BN|q85OVsBCn#i)y{)_A3Y|&2XK_HD(?r2=OFQUL?ZYi;n2g0o&lqN5 z=ehxlU<6wwL(7|@aB?!B0RvEJ;YF5qkuv=@wu~F6s)T?|c6LwqA5JV_4GiY0vC2O; zztJzKpEP&CKl8pFjP9d)L+eAIGu|O774C}3Pn0Uj2n&{q6et_wJa=(ijQTJ|tf(cT zify%yh001_)#FK3^tBZK(*p=u63%nAEIV&ANkG2Ro!V|#O66!%%X*W#ePIGGtNc~N zv*Ho!Yc_SZO$SCirW?La;f%joIC>E5Y)TO(m+#KO|7m%ez2p1=k3lkaS3L6e^E@o&#v;;6#T-lLAeN3Vi!gMKV1xN3d0 z;l_>n%_Haj0JZ463GoOiZ@zEhe;(#CJ6Vfa0V$91w7+J}*Qaa1Zzk$OzYS455VZ}m zSteB}y3mVoaalN(y)dcAy{vo7hf8$)(s_qP-y_)zSGx5^@2BhR?*1z$=}t>Uic@?T zy#(cfdkCL^<6Mt@DSk)vbaf4#ToIq0o*85vO=Y+RWq-<8sGw#&=s<||_wt0O+duH7 zTA!8k-y2d)sc#ucCRGIC>G$N6fMd+eN|U?D%bLj*o2S z|K<{&S>9|SItr&k4QPNu!tF!Ch5FgJhmeXd*;pZ8PJVB4FA`R>?FgGk7b?mtB&pgxmFF*2gG#$TXspR^kp1^8xY@xCPab0B%zm6fOjKYr z75Wm5VbN??EtrPCAuhv1pOfr^JZ-H%&|x|I)>Q-xFGRm=P2ty!cnM|o{BhR%xjU@XMn9Of>ABf1LDLkeFtqE` zJ-)%&?YI=W(G`=){O`+BPd&*355KBMMGuXV6Hf^4>2{1-2U+YRU-jOx4a`)O;B9cQ ziRLo1WES@F$mH~FjrQi#OO)G+o`y~-1MswiZvxh5bF{{ylWapz&G~IY&tMiP#KJvZ z$`U^r+7Fx(>T zDAze;edL3FHMc4rbI4A7X6MBV2(Zmi;F`R4Uk<|=>EP0_MS!3tHgxrdhy9j_(}vyg z#R}w&NFyL2#)_5L#e-1F4R6?u_Y=UIw_F|Wy%WH1%F7;O0Z5T>gJnw+u2(Tx4z{Z>JVWP@kX2~;##*aWd!O>sfQqNr zgP^wNaNT3sM_*R)IpYIo>;<0fq{;R)Huuba}+qiMyRO4!H`oUxI?;ek_xV~V10RHwLG2+?RC>2Ml zj1HAi{Ya0U8O6e@ihBUMaIujLRo%pUbdJ^LKrycOjb4OCM!6Goa0x_TPzp?dLeQLG zOID-*Zh{=4-sN&5k)zVDgKxYghP4^mH(v>;db6t#Why$#)@)}EXYC-#%R{azJv-?9 zPV;;BE4V<>b))w0R6PlGzos=N_P?EoP&#g)#Qrfqyzt|NS#9cXr&Tj2R6Kz`8v1Xy z_(%Kn@I;@%Vi0+8R5c&B_fJIBlcnHxwoB|teV%{$(j^%5rjIAtQ-o!YXFPSlp!+ac zk?q_q8+fr~WBCU=-M*HX-tnneCx5veGNxP!FC==ItxqBP!HkPZ);=}`JJ^YwZOO(h zR@HT#UzK3Cp|rOH27W!dwYN^!6PScLoW7^)kf~~Mv10WfYQglE<_)1BQEPoUZ`MnJDKYZ^nc9`z7Y6b`yHjvF|Z{+#-+IyDn z$tW9PTe4;m4bYi^<>!LZPGfFWf*gjMxxbBs?%jAnGznM~+zkmTcY{t~{fvS2E9_i z@)Y0KdgF1jjO?6JD&};Vadu*rn-o!^K6N*ASk8RDW_!5Fj6qH_RKDSOWe51o*AVQ( z#kQbKTcpc0Iv&nXwVF`t^a|d)zszt1Ni4#*loX&Zf*-Em+?oy3o;C-Lu=nccBoyI9 zXV=E!rSW5Q+)!CL6Wo~>}c=brfw2RsoRszK(y5Ca2Np<`s#YHU| zAI?h@Q>o_1@G$J38&39o^!2N-xYN!3k?&ghpnHIfgtwy92ib+?SM}OfYr@MYf&hE> z$PCk-As#%Jq~2+=E`GkECc0*2DAS4A?p~Glr2T;;!N}4T=69$hJs5rsvTQmrBYhGK z2+fIIdDXHZ+(6)W0`vBVl+ueEtH5PiLY?f-&7srB+4%R}>Px!#$cd<~)~zo?CpQLj zS*|9GKj*Bg6*F#;@y90x3S<6bT28czF}oB%3*hYb@^UjCnst41Ab{9a7%KCJ1q*{1 zhjU*PGNP|sC7=HmX|tD_Edz~7pFNJ7-_U>uhB%8<04K>rEgKj^ljNj!k8z2Gh_^{?1H^uH;s zG^llMDSZ3}$kGNAS#`_>b1J(i?Agp|Teso!uAT*_)uZlv$hnR#ytU_H(H zwQF$sfxx{8fB9hoUA)D%p?PZ~Wn7E&xt=v+i%x$WzD*=n>1} z-yE2w|G^&)m`r@`QA14UA}L+5jTW`0o7?Y*fQX?VN!K^uOG68q>?G@0fBq48;7j{; znMPCv<2w~pukw07hHdQ%X9+D)t&P1pa!^>cn4=;rk~;?pD22Op5;=_cPUV44>!hT|_P6944awui-ROW1r6*#+uEm z&0#k98*imoX8p5G&M3_Z1bLwER2xsaK6w7R(yOpUmYeX@Jnw+Nj4DBm^Hh{! zb$fHVJH$kMOrr!v)}@Tf(7}zH6t6UM3@LOTEW$`$=C)ESR&Q8<=&o#36o}b5-isWb zt2p$|p=6Kb(9R%$Eo*@qNo&}LEe2UgVmq9V zR3Z`w2E(Pnw&}n6Z8X82r?kMP+i1F5c- z;^JLc+2P^Oz9*$_Fi9d@KWL^wXkxt{_0?$A?Z+ZQkS0GkT|LmU^nkC9%7R&+hlOd8 za;*PWd$)NrwqGT`RmOA^_qFEOnf9x&$Xbf$ zy#Z)N5Vb_Ebn(2>FVKocl;LzglnGxs#v zeNEOQ=R)?xABioyHc`% zLL%Q-wP+?!*AsVAKp7_M6*Q=-aGsOfeB)Jn%)ehWN$ZFnbZU+HRq}1fpNA2wwD55w zb$1v)U;uH``_9Cq7c405ccNm`5l3IK{q;iN*m(A^PeJh~&snj=k#9%Xis=sC4AJ`C zZ6jpn$t%p&C#;!ieaW*BN;YdY%H&(eaPH@N0eTDCr+h%nQ!D$=r_0=S#A)|VE}%v$ z#`4S9AL!+>0lN)iytNbzlNbXFX`Nxy(iN&5q=0T*4BsEtd(P)1f2{4p4$g4lLXiDE105JqT~l!J!^O5A;) zJw503-~?iwJ(16Hhz5 z_1IXeOTBSO$sn`w*gzdHvKJe1Fl%6WyFV%d&)hk)27HdykX-5cdGH>|vk(-va>qf@ zEI!n%qbM{$+Q zM%-0Fp-aIxl@)*d)v64*8v2IIpw{<7w}FKoUQfSQq4z6)YT$C4Qji#6=Tmp!m^l93 z^=P7^E_P8yMGx0-XtpP!9`Xa|H_yP3mS)}xKv2Lp-=qd*J{FDn6ll`nPEy^gy(0>U z33B3)wh#_|3jTJT(V6ggai#aIfw_DkM+&U1d|dz=LE`8J#ok*a%zNn%G%VukQi+d@ z*mL!8Q;!J3Bl&UG^>Ki)b7bR*mPIXKY@Z{mipugT4RMO(1*B^CI>CzEWZ;Z4{!n3# z#fWifd%Al8uWNbvt8Vzxeg&!Aw7PYhtw?acw4kmu=KvbM1dumA!P8g3#V%^nkho@h z7YTAW1N`#{v)x}1O$LtFPeTtv4`%_17xC6*=eZMHu164nwJ1xxEd;Z0LMW8X_EW47 zp{nwy*fSp>E=I0B9Ix^LOLx_bv#!a*8-P6iQoS8!cKHiJF9)+v>u8B^0eE7YI@g@X z2Vqvu;CJs@JFK6V9W|NIkJhcMZKPOouxVlSD&*k206iKre;RB0@)y530q#86 zgAjNELhRuo%q=>@%vZYX0^h_(*v_zNZb1PL3VC+*{HCHh6Z`_`RnQDh2z_2){DTqB zYS13)`g|iNfaI|I(Nww4e)Fl)b^&w0!KGN|D@UNyN>b!Z0Qu&nA5jXYC|(ky%8EE`qqGpeMlm z+f9?>#pe`;Bnx(q+j}>xKor-i`BM*$SN!0{=6NsnmI3~Nv8eahOD15t+GO5v(sMbE zw!oq^MMg9sIR$>~4OO{SO1#RDOkG7H2iirKsRqJLToyjIG}KP)9^`yc$olO2jnFsm zF%nFQu8(t@MN1y?Tq^H07Z%b)-(9R%TK;PKtDz%E{><810sl&2eXfBpq4M3n&7*KF zsKvdJkS;DeDD2V>3EW3bi--VZS=xUzJ|7!|@uGK~y&8z9ra6E@6m~B|@4oPZPsI>6 z(k6_G_vlDZ{(k2M&eVJpXx=FeT!twxQ$*1v{wOydUC4Y28smQ(vJXO|)7;n4$(k>< zWCeLL(g_G(@;mq@Yln~l+TPCs@`S`%fubw2FhS;iP2JgLcf3tEEY{QVYl_YinWgNDP!0XmB^{q|C6Cu_$1SmE0pUE*rp7 z&3hu~2FS{9igJ$lZD=@Wno0hvb&a2dk5>pb+ZFoc@6>E5Vozhn;6Wd~Pvj&ogTZ;s zt3{fD^Surw}jG+3Pjd5>@6_xy1-PX4GimHB!-7zq$ur27AS3(V6f_ zcRb>g<$wl%@Y?~8%drv_NanqT#fcs|Npo;sq!XZEE# znc;n!s>raf2O|GH=yxFOQC=eaySeieP&E9+?(BYTAE}TZq5O#p8osFyWMECVU&3*-k%%iTg>;2QjJ-*wZqfK9b8`gWy!#)ij1ejkxY zD$p2Bi2O1I5o7!&7cB-_D*!bEZpSyczXLMK^RlzwcM_=}LW7Alp)3sTu!P5EYtmcWRfy{o8RBUO+a46vY2#KOS1Z1Qu#sj zdrTV;#mR1A3E;Qy859G_zw0BuDx62(sSEk;_@z!tae<^$4JWeg=VrNP7yfzMzu;LH z(xLy(N>A`Qy$tQGL-F1L%N$m<;m>K0{YnT0r?df{B1@j1G_s88_)wIW4hdlBG3l0W z939A3OC{IM^DuXOW{EKF)Om%lQuhST|310j%Di6t&enDm=KguG;Giv*n7;M~&yx9AGK~?K`6Ce! zx2g&-P58%a1F&8h1zjGNKICkghgLG0!Uy^dHrsrq`CC-$VWU0b|9MbgF{b1>SQ!20 zFmrz_x#9m?Z{<_l?q`lQ@c`b-K-DwFTVnqs7Kdza$`W3g;PB&-!Drkm!LGAdU<*lm zP%zjw#Y7hLK?ty}kUO&}x&WZP!p5+l=&7-%UBN<7iPoDcr0w4bZ~Yt4L7mdJMg zJEgxS2h2mDnO}8ZM*hHtF9JFi=t}9}M~Yye8Ph$mVowQR4A;OT`c1GOu}!1YV_&aQ zh-)e~hL0lc>9kBz^6zDN@LI(5(5W(DloErXxiTBI0p~(C|Y`HPbhS@ ze?#r?H<(}I_MPfhyM@Qvf6XBsD)GN;?f5q_+(VTU_4@>jt821`e^XuDkS4GQ;A<#1ZC{c> zLsGcP0F9W>AUHn_;P1YHy3+arF2Y7sD!gVjxrGE8+O`O%7P(Krw4m@5_XJab5N0EX z(~&c&w8U0|DCz(V*}Rw95bT`{>EIaH7C6D4@=*XIPK}1VK`4W09C!Qy8Zqogx9B_b zVA{#8y({77OF%!_futt|RQlp@E!C}x8=pO-+El2&_L_SvSR>^w z!ke8>Pz?m;zYffXKJk?#P;BP-A;9Rv*uTeIVZZsAmiZXlo;nP+psbE{B!SCHQ$TK_ z-&(-RhAX`Aw`ye&I`nd|N?r8?3i(QP==%Pl0ifU6FE+fSU4DUXIrkm^TS6xIxmFng zCoCt*5Bs0vM4}8(?&K>6_}tgo{f0ZYr%v$zgv@Nw z?be<2N%NGM;>(;9$ll;ZCC9v-3JFT5V=1>EfMpG-yKI9Cn;BA*-P48U0I8nReUsXn7bY}*?vndWyGy3=_q%&qWvMA~KqcBEtH0NrCJSjY!+kg)w z>{kg0@Hs5iCA@<*jAfkk|MlGS^Qo1sq3mZk0x&zVDqC0h0j_Rqb`RHcX$lswKQ5B^ ziH&KK!x9(atSz)d@5eNMyaHGTloHy0EnL?9mwqB^uaxPmf}Wq^1HIs^@pvY8T^Bay ziDryHTrHw;N<_@V@PF&08ZSKLhWcn~Ez`Pv5 zJ6?ctu$KpLu9h|b9^9jQ)KrCO{d3M=3qZEIMl^*!ovJE9BzysMAu8D}98w-22LI;B ztectgt|reW?J+>|I#l~2a&Ngb=4!LMA|{vo4Q9EPbkFi_!9+4Rk8)L#9TYlAcj^IC zA_be0k%a_xLQ+yvBT1N=h~i`juIQ@9UpwMg49#a4+aBAw2OU?tj_Gm!GJtuUK6s7K zjXZKFg}=TJk&)+k3fQAzyz6{<->0D!zVkzeqzz6kIUvqY?SW!)>WSam)WC5^ivH`W zb4j_F4dI7z3`Z5%kVVCHkahk7o%gE`nBT5(W5xhF5&Fxr9)i%C(nS^Wk)KWiDpZ&v z`d(m79Ax)_MmzL)OWo3$J1FEw;k$mZhw$0v!RTVZ!3hF&uh^Rrpn1o8XLj|lnlTE1 zhb+VcCP`Cn&LGmDN1~9X_>Yfh(IdkAyr>s{9=n-~7&LQi-pj~oJ-%y4%ux&YZW$ns z5TZ(2Lg*?Xzh-reWCx*V@(uX_stAs|jtJ|FZvW{z;@vE5t_{f}&LaBM;I|bs&{?Y}>x!*@ z-SL^^^2&-fRpUk&CF_WMN4^iL@`G$0LPDQqDEDbq@Q^}vx|^-Of5g02G5q)Nt``FC zLjVsnFXwx-D&Ojh`+*%TRjoM9!m2(*Ypw;yTEvv6!9KBws3pSmQ$u!kU)Z76O_Q&+ zzjqvLv|-ROIXeSlq=5OW_R*Uos&A3jI2a%Bl64h8twPZVERbyKv}+6oex)W&vvW&9 zJuU5h(^OnZoGY9Grt&abkFZN290dO2DnVS{1B=h%$MZn~C-fuyZ|JtF$TMzI(`mmln-gf$t3d!!HC+{fD(OkT=ZFh*ZWn~Laovb{N885!^25J&q z-cevkLY7HW2^X}cIr?;q-hNAcl7&rM3QsG{S4?xmhi;dw{wm|S-KbN10sgT7e_e8P zTngIFW;GlD7e?c53r55PoY3RgG<{67$Yx{Ox3hDEtq4ZH3na&7B?Djtd_Pv4lP zmasp2)H83mb^rP_{hgNug|*TkXQ>>ATaGiO#|dd{>!!Q%Uxqt8eq@hBAufwwgAA2f zZAA^9Z#<+8TW8u*j`<0~6zA!3t#=5N0xt+1dB}m9M+1Gbe@)qgBjKBesD|cL z&)8A#lb67WKCP4V_$=3?E1Rc)dwj^wRTZYz{$Sdhc7s5N19G`ud?!so6QLmZ#n&pw zMzK>Z+S*!osv0j@5iRKRU}Mqs&k92-HOq?TzXX{Na$Ve%A$eE$;yo^IHyQ(+%Kqv0 z>q>xA15gw}?f+o+{cZ+jF=ZA6Q{;L}1AnFlW#}OPqR1(x5Gq}2*b7}g<)WN7yqIBi zAi}@F9JbPdQZv*&P$o2UFljHAN}^7qsnxz(niv#ShYIz-W)Sv9#KFd9!i(ct&c}!< z=qkH<))P+`wDI0=h?vANe4v600Ncoycrro8l(l36CU79@_`k32JanqkK7h$Z+*z2A z!exlPhLqLu^U=J8!-Vd6$0U9ltl`YS91;YFWiU7hk&}$GjREmrQc$J!a&! zBbjeDzp)+@*O2|+I89y@p?>A+ZhNi!G?WX8nkDtD0=E|mY>>B6E3%q0$+kvJ5BDAQY z66<4#^0PrNET4a2T6o_+7zb0HX3Di>r3!7`8nkRul~vBVEaGmJNs5IsgbJBL4*r~A9(P4| zoHD%rtSKs`r#0vhEnzdRl3Mt@x|MhF`E+)-;=JIeIOsNdn6oDMUecU(8C8= zv7&(gK9BFLb`!DgoZZLY=guUBo0Z2uPmE&4+;cZ=Pgz+xb_SJ6Gqm^50Ydyr30rCe z!2Ei{5nx`~;RXSe8DUc+>C(LysNzxoXUO6O|7RWM=B87O<@b*w0?jbejaOoRS0%RF zlkVl#&HE4HwIRXm{S{PnZxsiOVq-KM43k7S)nA3)nC;E<`%t;#&1ejm{$kB?Ob;Fl zL6~~x$D)%ET$qj`(_=f})MrRLYa9ZBoEj*k8R|1@keNal zU9SyAwR{K&HsH2kONkdac4-2?Foq7P)R#RI_t^I(@C)`;*k<}Xu(nxrq$P{5^&$@7 zKEUvUB##w_ko1@odUIfkJN}0&JVuxGv_dwL zlE_@T4o*%%N!(l1y71*(7a^F(HodYt?u)iU)k~X=p%XP6e0^i#3R|16bM3R8`jV`^ zB+!sU7&Rjjwb#r z-`tsUENT1B_jdG%RZIcHt61GA+tzmN0A?_tUh}?p68K{dCZO3C|1?qSnFp1-n1;@1 z5F(ZpfOMC&eLv&>(ybqVT@4(Q_Le6fXPy()Y4msK8h+;8MQJkfr($Px`(sF}i(B0v zjmvg#)2AU0Z*3XlvK|rqUmCF4C6dp=W#oP&Kw_kgxud6>5QjNf@|!1Gk& z)yy6qp4dEI?=cGW95B$}r_K4c{9Ik;*Y!;{$ous1qpgA?mlE1UaK5}m-bjm_@X<{z zt}II--vFoOx79%94IHmQ#?|Du=-U+iS$@q=i8C>0F=~@WgoPR$HIRI>gYtj1+J2qG z(|xpw-}4e?dLgHkK5Ov3Fi0_ZDGAL+3d*|7zZIUECO)<3)yq$OZQ8EyMR5K1fG7{} z-jb%D=!b{A&n@RF$8YRKf33vOpGuI^Ho9V=5%k|A>0LdSz(pJJ@CDc9O(B1o&#RH) ziXW|sLkn)FG4ewDh3BuXYim<&m?;(rCv^I{ZiVAr>bi4%BjNooto87LM$je2dQ`d` zRK5Fwb$zGqz&4 zin-2*KZU6(2~v(_I)?2+EY|>u<+GigB`b>VRsnHL`fTrao;%r{7pxFTsavi8CCf`+ z*>zc5r>1)BQp&iq(~&RQer9RclBHKR`FaiEmf$Fz7_ee50TDl6 z$8_`loieQLGAqb?HUsmm<7;V9pexchaHMrEDbLOR81&b!a?qUKX7u+1ac4Erz4`IR zAh-LwN?WGQT6k*mBh$QH!$tJm@v{L7PnY@%)`n6+a!xC7Cz% zb=+?-DE~BlbuLhCvw-a-1(#dozI6Mv(pO>QP8Cjcwi>)5M0xw(?$5l9j-`iGg(-j( zdgrg6kyAFuk^C|flBRgd*QDM@H|X~J`Qo{HjMlGt8%AIr9ErJ#=L0n7mz8Ne7F-Wd zzr20JlPUEm%CXueGRrCnWh4SGgS#wP0&}m>p~{QoAL);+Le=Sa39RR62g$tSo_{MH zE9gajyA5mUtp>auy-#^Jp1&YYHEuZ!W%3w39Ny#OO{pD_-C5X{X%$i-#!g5W&J)$% zNWZ^MOt@U-5Ze0RXQNrA%PNJ#>vFmhWgc?q&Vqz4;QZ%N6qTyQ-ZwP%9J$vog+S`5gy!+bQ_sNj-Pa_2MJO1(7&O5k6{v7bH%)8QKSETG$e-j zdF4rnWXc&%4Ock-&L2@YUU`?LIf>Sz`NYv>JoQUrs^(#F5z77N&w8tAp#A=xW4-#w z0L1VLyo)Dl6S=;%j?Iqx#Jk1o#ak^Uigb|y6WT1O&j;g7u3rlCHv zZqqQvV1Dxc%FUOzYDt0Xyr(xG{RQx12xY5Olu;tUo6PMO$iyTDYC{)lDH5ugO0?~8 zO`E*2Fvm+b>81Y4SIR5A&EVC$_7zd0q$c_~a3WY6M`&CBV=d7I_@6zYAD5d&ED6Oy zD!9Jx-6vA?KK{&jBUEeYOBl(4q>c7*1!S`+?Gm5wJ0J`W!%ofEVJMY{qMq~MDm*SwozRMS?!W1VNo-cOHx z7~^{Rn>#GHAI>;54GejeWE;>Fx$*G6E8zw4!`!yk3a_^0Kirr}D(^Xb%)0eD z`BI>d)bb!tnG6P>ix3F7r#2CY>9Cb3tQV_8bSWVuszGm^ft<)?gPW^}`1b5#(nlxo@IQ&@rZcAPX#~lR-FjYp zS%E>^<_{z$;fuo_E>!x*-zD$C(+W-So}LjOq74{|^-# zb_5K#{UiFX(PxtI7n;5Og8cet25h8ai7)jP0ax_V*vKy}Wxt~GPe9hF_Oxx#F=C>w zzWMglORv(4*>)$nO_FPuF1}w8*2kAirtcfgXrY^c8`&;p0Kk zW4<~jz$yXLMJ1p>?LI$E3Z|kT9S4JcB0Zym{3aBE7{q(}x&yQWfX0Cbr_fc!@$Wh}1w7!}^*r&=72ZXhSeaE;iPR&V<3{z$Pxher|kC>3t8w=OH+8F?h$$ zWbM9eZJ2HWKYA8}t!{;vwH*hROI3?NN5o0l+kx=Sb@nM@lV0VZRc-LA2}3Q0)+m7d zV3_Ga+n=g|@@q=i>q1LFRK-0Gs&#R?Jx?d4XHh)@UmO!#0uJT7258~PE^AppDqyKq zaMi(}@BK(x+wG`)CUZre->emb<(5E0#6rYx;$ass6MmDPIa{^a?zN{;S?}2{)Z@%o;j(pN08s*!9l0;{(-l}Ln>e*Tt{@%a6ttDIm{4rK=L*r3RG_n`DQbXHf&U&KI2D3DZ-APEqjkF5$xkv1j=Lv??35<4iicW2*Qvd`q-RI{EO~jQ2!2^l zTp-`3rB}A*)Zs#X`v?T;C2mR2%Ky=`)3o(?HlsouD?A;1^EI*rRH%{XBLxWWiw9kx z67EYAdwn(QOzgeI;LbV`Ak>HtJR#+AKNJtWfP@dFTmK2w@D4G+qmCUX&)mwf0T0AB z#4myX^%g%PwjkU!pA9F4i1OE6A<)>xCaq+q$t`b5xn4nv`jgi`BMt8On;6(x-vWd_ zIhCz}7Klkw07wv|ZiBW_k3}@AaO;c0o_Z(*8e_<%-U3g=I>dK^D0L2BA(of7?h@(q zSr?x;^*&`-?iS!f@fZ9UiP$Uoh=M%UU&%*xAff2n|4EecilgLZtqP>Hn zqTA*mYWKx4nGjT*N2o$`!~n@X5M#+q2U-vJlvKRcwDSAl{^WN%QT)@=D7Pp?BTehd>mNZjj-JA| z7spCYi}ppV)Y!qJN*Ch4BsU=sv0WNF^;110Jk?aZD=pV3WPSLmc3c%avl;2_JEn>U zN*wFA44f4es|_8zStg-{WF3r<$Z%EaMW{Fd*!`tS z^j?w2vMb__ - - - - - diff --git a/docs/pages/challenge/index.md b/docs/pages/challenge/index.md deleted file mode 100644 index 96967688..00000000 --- a/docs/pages/challenge/index.md +++ /dev/null @@ -1,3 +0,0 @@ -# 题目 - -## 创建第一道题目 \ No newline at end of file diff --git a/docs/pages/config/cache.md b/docs/pages/config/cache.md deleted file mode 100644 index 0fbf8f2a..00000000 --- a/docs/pages/config/cache.md +++ /dev/null @@ -1,3 +0,0 @@ -# 缓存 - -Cloudsdale 有两种缓存实现方式,一种是基于 `go-cache`,另一种是基于 `go-redis`,如果你有分布式需求,请使用 `go-redis` 保证缓存的正确性。 \ No newline at end of file diff --git a/docs/pages/config/database.md b/docs/pages/config/database.md deleted file mode 100644 index 875d5c34..00000000 --- a/docs/pages/config/database.md +++ /dev/null @@ -1,3 +0,0 @@ -# 数据库 - -Cloudsdale 目前支持三种数据库,PostgreSQL,MySQL 和 SQLite \ No newline at end of file diff --git a/docs/pages/config/index.md b/docs/pages/config/index.md deleted file mode 100644 index e8e40962..00000000 --- a/docs/pages/config/index.md +++ /dev/null @@ -1,3 +0,0 @@ -# 配置 - -对于 Cloudsdale 而言,配置通常指的是 `application.json` \ No newline at end of file diff --git a/docs/pages/config/proxy/index.md b/docs/pages/config/proxy/index.md deleted file mode 100644 index 7cc965ef..00000000 --- a/docs/pages/config/proxy/index.md +++ /dev/null @@ -1,13 +0,0 @@ -# 代理与流量捕获 - -Cloudsdale 的代理功能主要是为了流量捕获功能服务的。 - -当然,如果你不希望选手直接通过 IP 地址进入容器,你可以使用平台代理。你只需要保证 Cloudsdale 能够访问容器即可。 - -Clousdale 通过 **TCP over Websocket** 进行代理,简单来说,TCP over Websocket 就是通过一个 Websocket 链接(在 Cloudsdale 中是 `/api/proxies/[UUID]`)作为桥梁,连接靶机。 - -那又产生了一个问题,我是不是需要以 `ws://` 这样的形式来访问题目呢?这时候你就需要连接器。 - -所谓无感交互,就是让连接器在本地开启一个 TCP 端口,做题的时候你只需要向连接器提供 `ws://` 链接,然后访问被分配到的 TCP 端口即可,做题的体验没什么太大的区别。 - -Cloudsdale 推荐使用 [WebsocketReflectorX(简称 WSRX)](https://github.com/XDSEC/WebsocketReflectorX) 作为你的连接器。 diff --git a/docs/pages/deploy/docker-k8s.md b/docs/pages/deploy/docker-k8s.md deleted file mode 100644 index 3eda2f5f..00000000 --- a/docs/pages/deploy/docker-k8s.md +++ /dev/null @@ -1 +0,0 @@ -# Docker + K8s 部署 \ No newline at end of file diff --git a/docs/pages/deploy/docker.md b/docs/pages/deploy/docker.md deleted file mode 100644 index 301dcf64..00000000 --- a/docs/pages/deploy/docker.md +++ /dev/null @@ -1,75 +0,0 @@ -# Docker 部署 - -本篇将教会你如何快速地部署一个 Cloudsdale 实例。这个实例具有以下特点: - -- 使用 PostgreSQL 作为数据库 -- 使用 Docker 单机实例,承担平台和动态容器后端 - -部署她的方式非常简单,你只需要准备一个 `docker-compose.yml` 即可,当然,你可以直接从仓库中的 [deploys](https://github.com/ElaBosak233/Cloudsdale/blob/main/deploy) 目录中找到你所需要的,一个简单的 `docker-compose.yml` 应该长这样: - -```yaml -version: "3.0" -services: - core: - image: elabosak233/cloudsdale:main - restart: always - ports: - - "8888:8888" - volumes: - - "/var/run/docker.sock:/var/run/docker.sock" # 映射 Docker 守护进程,使得 Cloudsdale 可以控制宿主机 Docker - - "./configs:/app/configs" # 映射配置文件夹,里面存放 application.json - - "./media:/app/media" # 映射媒体资源文件夹,里面存放题目附件、用户头像等 - - "./logs:/app/logs" # 映射日志文件夹 - depends_on: - - db # 依赖于 db - - db: - image: postgres:alpine - restart: always - ports: - - "5432:5432" - environment: - POSTGRES_USER: cloudsdale - POSTGRES_PASSWORD: cloudsdale - POSTGRES_DB: cloudsdale - volumes: - - "./db:/var/lib/postgresql/data" -``` - -你需要在你的服务器上建一个新的空文件夹,然后将 `docker-compose.yml` 放入其中,然后运行: - -``` -docker compose up -``` - -第一次运行大概率是失败的,因为 Cloudsdale 默认生成的配置中,依赖的数据库不是 PostgreSQL,我们需要在初始化后进入 `/configs/application.json` 进行些许修改 - -```json -"db": { - "provider": "postgres", // 修改这里 - "postgres": { - "dbname": "cloudsdale", - "host": "db", - "username": "cloudsdale", - "password": "cloudsdale", - "port": 5432, - "sslmode": "disable" - }, - "mysql": { - "dbname": "cloudsdale", - "host": "db", - "username": "cloudsdale", - "password": "cloudsdale", - "port": 3306 - }, - "sqlite": { - "path": "./db/db.sqlite" - } - }, -``` - -修改完成后,再运行一遍 `docker compose up`,如果你看到下面这样的输出,那么恭喜你,Cloudsdale 启动的很成功,此后,你可以使用 `docker compose up -d` 将 Cloudsdale 以后台的形式启动 - -![](./img/1.png) - -然后,你就可以通过 `http://localhost:8888` 进入 Cloudsdale,端口你可以通过编辑 `docker-compose.yml` 中的 `ports` 进行更改 \ No newline at end of file diff --git a/docs/pages/deploy/img/1.png b/docs/pages/deploy/img/1.png deleted file mode 100644 index 01eafd184d7f4e29d7ab5e9b19d0116b98a80e1a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 52750 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 diff --git a/docs/pages/deploy/k8s.md b/docs/pages/deploy/k8s.md deleted file mode 100644 index 9a10ce1a..00000000 --- a/docs/pages/deploy/k8s.md +++ /dev/null @@ -1 +0,0 @@ -# K8s 部署 \ No newline at end of file diff --git a/docs/pages/faq/index.md b/docs/pages/faq/index.md deleted file mode 100644 index 32cce907..00000000 --- a/docs/pages/faq/index.md +++ /dev/null @@ -1 +0,0 @@ -# FAQ \ No newline at end of file diff --git a/docs/pages/game/index.md b/docs/pages/game/index.md deleted file mode 100644 index 1281d323..00000000 --- a/docs/pages/game/index.md +++ /dev/null @@ -1,9 +0,0 @@ -# 比赛 - -在介绍比赛功能前,需要先捋清一个关系,即用户、团队和比赛之间的关系。 - -用户是比赛的间接参与者,用户可以创建团队,并邀请其他用户加入团队; - -团队是比赛的直接参与者,团队可以参与比赛,且参与比赛必须以团队为单位。 - -也就是说,不论是团队赛还是个人赛,用户都需要创建或者加入一个团队来参赛。 \ No newline at end of file diff --git a/docs/pages/index.md b/docs/pages/index.md deleted file mode 100644 index 6e034f93..00000000 --- a/docs/pages/index.md +++ /dev/null @@ -1,37 +0,0 @@ -# 简介 - -!!! warning "警告" - Cloudsdale 仍然处于未发布阶段,快照构建成果可能十分不稳定,功能也可能未完善,快照的品质不能代表正式版本的品质。 - -**Cloudsdale** 是一个基于 GO 构建、使用解题模式(Jeopardy)的 CTF 平台。她非常地 _轻量_,并且可以使用 _非常简单(可能)_ 的配置文件快速部署。 - -本项目灵感来源于 [CTFd](https://github.com/CTFd/CTFd)、[Cardinal](https://github.com/05sec/Cardinal) 和 [GZ::CTF](https://github.com/GZTimeWalker/GZCTF),博采众长之下诞生了本项目。但最初的想法仅仅以尽可能处处简单的方式,给学校的 CTF 战队提供训练平台。 - -## 功能 - -- 题目 - - 静态题目:无靶机,判题依赖于一个/多个已知的 flag 字符串,通常依赖于附件系统 - - 动态题目:动态靶机,判题可依赖于静态 flag 字符串,也可以使用动态生成的 flag(通常为 `UUID`) -- 靶机 - - 多端口支持 - - 可自定义镜像的基本环境变量 - - 可自定义的容器计算资源索取量(内存与 CPU) - - 可自定义的 flag 注入变量名 - - 可选的端口映射模式 - - 通过平台代理实现的流量捕获 -- 比赛 - - 可自定义的比赛题目分值 - - 可自定义的一二三血奖励比率 - - 过程中可随时禁用/启用题目,实现多次放题 - - 基于 Websocket 实现的比赛内消息广播 -- 数据库 - - 基于 GORM 的多种关系型数据库支持(PostgreSQL, SQLite3, MySQL) -- 容器支持 - - Docker - - Kubernetes(以 k3s 为例) - -## 开源协议 - -Cloudsdale 基于 [GPLv3](https://github.com/ElaBosak233/Cloudsdale/blob/main/LICENSE) 协议开源,使用和二次开发需严格遵守此协议。 - -
\ No newline at end of file diff --git a/docs/pages/stylesheets/extra.css b/docs/pages/stylesheets/extra.css deleted file mode 100644 index d2ff92b1..00000000 --- a/docs/pages/stylesheets/extra.css +++ /dev/null @@ -1,5 +0,0 @@ -:root { - --md-primary-fg-color: #0D47A1; - --md-primary-fg-color--light: #0D47A1; - --md-primary-fg-color--dark: #0D47A1; -} \ No newline at end of file diff --git a/docs/public/favicon.webp b/docs/public/favicon.webp new file mode 100644 index 0000000000000000000000000000000000000000..cdbaf63232ad07ce38e9f2cd3bc70efdeb9a68a8 GIT binary patch literal 13476 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 Spyo>852hgV-2FH4Zp8yT3e$xE literal 0 HcmV?d00001 diff --git a/docs/requirements.txt b/docs/requirements.txt deleted file mode 100644 index 4b632cb1..00000000 --- a/docs/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -mkdocs -mkdocs-material -mkdocs-glightbox \ No newline at end of file diff --git a/docs/zh/guide/index.md b/docs/zh/guide/index.md new file mode 100644 index 00000000..ac91c569 --- /dev/null +++ b/docs/zh/guide/index.md @@ -0,0 +1,32 @@ +# 什么是 Cloudsdale? + +Cloudsdale 是一个基于 Rust 构建、使用解题模式(Jeopardy)的 CTF 平台。她非常地 轻量,并且可以使用 _简单_ 的配置文件快速部署。 + +本项目灵感来源于 CTFd、Cardinal、GZ::CTF 和 Ret2Shell,博采众长之下诞生了本项目。因为作者对软件的独特乃至夸张的理解,以及作者所在的学校资源非常有限,所以希望打造一个轻量又好用的 CTF 平台,给予学校 CTF 团队一个良好的体验。 + +## 使用场景 + +就像 ACM 一样,CTF 也应当有属于自己的定制化平台。Cloudsdale 非常适合组织小型的 CTF 比赛,或者进行 CTF 团队训练,灵活的题目管理功能可以更加高效地保存与使用题目。 + +## 功能 + +- 题目 + - 静态题目:无靶机,判题依赖于一个/多个已知的 flag 字符串,通常依赖于附件系统 + - 动态题目:动态靶机,判题可依赖于静态 flag 字符串,也可以使用动态生成的 flag(通常为 `UUID`) +- 靶机 + - 多端口支持 + - 可自定义镜像的基本环境变量 + - 可自定义的容器计算资源索取量(内存与 CPU) + - 可自定义的 flag 注入变量名 + - 可选的端口映射模式 + - 通过平台代理实现的流量捕获 +- 比赛 + - 可自定义的比赛题目分值 + - 可自定义的一二三血奖励比率 + - 过程中可随时禁用/启用题目,实现多次放题 + - 基于 Websocket 实现的比赛内消息广播 +- 数据库 + - 基于 SeaORM 的多种关系型数据库支持(PostgreSQL, SQLite3, MySQL) +- 容器支持 + - Docker + - Kubernetes \ No newline at end of file diff --git a/docs/zh/index.md b/docs/zh/index.md new file mode 100644 index 00000000..ee0835a1 --- /dev/null +++ b/docs/zh/index.md @@ -0,0 +1,34 @@ +--- +layout: home + +hero: + name: "Cloudsdale" + text: "一个开源的 CTF 平台" + tagline: 高性能、轻量、易于使用 + image: + src: /favicon.webp + actions: + - theme: brand + text: 什么是 Clousdale? + link: /zh/guide/ + - theme: alt + text: 快速开始 + link: /zh/quick-start/ + - theme: alt + text: GitHub + link: https://github.com/elabosak233/cloudsdale + +features: + - title: 高性能 + icon: ⚡ + details: 由 Rust 赋能,最小的性能开销,最快的响应速度。 + - title: 自定义 + icon: 🎨 + details: 题目、比赛、用户、团队,乃至平台本身,你都拥有高度自定义的权限。 + - title: 易于部署 + icon: 🐋 + details: 二进制或者 Docker 镜像,无论是本地还是云服务器,都能轻松部署。 + - title: 开源 + icon: 🌟 + details: 作为一个开源项目,你可以自由地查看、修改、分发和改进她。 +--- diff --git a/example/application.yml b/example/application.yml new file mode 100644 index 00000000..06f5a081 --- /dev/null +++ b/example/application.yml @@ -0,0 +1,90 @@ +site: + title: "Cloudsdale" # The title. + description: "Hack for fun not for profit." # The description/slogan. + color: "#0C4497" # The theme color's hex code. + favicon: "" # The favicon's path, such as "./arts/favicon.webp". + +axum: + cors: + allow_methods: + - "GET" + - "POST" + - "PUT" + - "DELETE" + allow_origins: + - "*" + host: "0.0.0.0" # DO NOT EDIT if you are using docker-compose. + port: 8888 # DO NOT EDIT if you are using docker-compose. + +auth: + jwt: + secret_key: "[UUID]" # The secret key for JWT. ("[UUID]" will be replaced by an actual UUID when starts.) + expiration: 180 # The expiration time of JWT in minutes. + registration: + enabled: true # Enable or disable registration. + captcha: true # Enable or disable captcha in registration. + email: + enabled: false # Enable or disable email verification in registration. + template: "" # The email verification template's path, such as "./templates/email-verification.html". + domains: # The email domains that are allowed to register. + - "example.com" + +captcha: + provider: "turnstile" # The captcha provider, can be "turnstile" or "recaptcha". + turnstile: + url: "https://challenges.cloudflare.com/turnstile/v0/siteverify" + site_key: "" + secret_key: "" + recaptcha: + url: "https://www.google.com/recaptcha/api/siteverify" + site_key: "" + secret_key: "" + threshold: 0.5 + +cache: + provider: "memory" # The cache provider, can be "memory" or "redis". + redis: + host: "cache" + port: 6379 + password: "" + db: 0 + +container: + provider: "docker" # The container provider, can be "docker" or "k8s". + entry: "127.0.0.1" # The public entry of containers. + docker: + uri: "unix://var/run/docker.sock" # DO NOT EDIT if you are using docker-compose. + k8s: + namespace: "cloudsdale" # The namespace of k8s cluster. + path: "./k8s-config.yml" # The k8s config file's path, such as "./k8s-config.yml". + proxy: + enabled: false # Enable or disable TCP over WebSocket proxy. + traffic_capture: false # Enable or disable traffic capture. + strategy: + parallel_limit: 1 # The maximum number of parallel containers. (Does not affect the settings in game) + request_limit: 0 + +db: + provider: "sqlite" # The database provider, can be "postgres", "mysql" or "sqlite". + postgres: + dbname: "cloudsdale" + host: "db" + username: "cloudsdale" + password: "cloudsdale" + port: 5432 + sslmode: "disable" + mysql: + dbname: "cloudsdale" + host: "db" + username: "cloudsdale" + password: "cloudsdale" + port: 3306 + sqlite: + path: "./db/cds.sqlite" + +email: + address: "" # The email address that will be used to send emails. + password: "" # The password of the email address. + smtp: + host: "" # The SMTP server's address. + port: 0 # The SMTP server's port. diff --git a/go.mod b/go.mod deleted file mode 100644 index 0a6e785e..00000000 --- a/go.mod +++ /dev/null @@ -1,154 +0,0 @@ -module github.com/elabosak233/cloudsdale - -go 1.22.0 - -toolchain go1.22.2 - -require ( - github.com/TwiN/go-color v1.4.1 - github.com/casbin/casbin/v2 v2.92.0 - github.com/casbin/gorm-adapter/v3 v3.25.0 - github.com/docker/docker v26.1.4+incompatible - github.com/docker/go-connections v0.5.0 - github.com/duke-git/lancet/v2 v2.3.1 - github.com/gin-contrib/cors v1.7.2 - github.com/gin-contrib/i18n v1.1.3 - github.com/gin-gonic/gin v1.10.0 - github.com/go-playground/validator/v10 v10.21.0 - github.com/golang-jwt/jwt/v5 v5.2.1 - github.com/google/gopacket v1.1.19 - github.com/google/uuid v1.6.0 - github.com/gorilla/websocket v1.5.1 - github.com/mitchellh/mapstructure v1.5.0 - github.com/patrickmn/go-cache v2.1.0+incompatible - github.com/redis/go-redis/v9 v9.5.2 - github.com/spf13/viper v1.19.0 - github.com/swaggo/files v1.0.1 - github.com/swaggo/gin-swagger v1.6.0 - github.com/swaggo/swag v1.16.3 - go.uber.org/zap v1.27.0 - golang.org/x/crypto v0.24.0 - golang.org/x/text v0.16.0 - gopkg.in/natefinch/lumberjack.v2 v2.2.1 - gorm.io/driver/mysql v1.5.6 - gorm.io/driver/postgres v1.5.7 - gorm.io/driver/sqlite v1.5.5 - gorm.io/gorm v1.25.10 - k8s.io/api v0.30.1 - k8s.io/apimachinery v0.30.1 - k8s.io/client-go v0.30.1 -) - -require ( - filippo.io/edwards25519 v1.1.0 // indirect - github.com/KyleBanks/depth v1.2.1 // indirect - github.com/Microsoft/go-winio v0.6.2 // indirect - github.com/bytedance/sonic v1.11.8 // indirect - github.com/bytedance/sonic/loader v0.1.1 // indirect - github.com/casbin/govaluate v1.1.1 // indirect - github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/cloudwego/base64x v0.1.4 // indirect - github.com/cloudwego/iasm v0.2.0 // indirect - github.com/containerd/log v0.1.0 // indirect - github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect - github.com/distribution/reference v0.6.0 // indirect - github.com/docker/go-units v0.5.0 // indirect - github.com/dustin/go-humanize v1.0.1 // indirect - github.com/emicklei/go-restful/v3 v3.12.1 // indirect - github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/fsnotify/fsnotify v1.7.0 // indirect - github.com/gabriel-vasile/mimetype v1.4.4 // indirect - github.com/gin-contrib/sse v0.1.0 // indirect - github.com/glebarez/go-sqlite v1.22.0 // indirect - github.com/glebarez/sqlite v1.11.0 // indirect - github.com/go-logr/logr v1.4.2 // indirect - github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-openapi/jsonpointer v0.21.0 // indirect - github.com/go-openapi/jsonreference v0.21.0 // indirect - github.com/go-openapi/spec v0.21.0 // indirect - github.com/go-openapi/swag v0.23.0 // indirect - github.com/go-playground/locales v0.14.1 // indirect - github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-sql-driver/mysql v1.8.1 // indirect - github.com/goccy/go-json v0.10.3 // indirect - github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect - github.com/golang-sql/sqlexp v0.1.0 // indirect - github.com/golang/protobuf v1.5.4 // indirect - github.com/google/gnostic-models v0.6.8 // indirect - github.com/google/gofuzz v1.2.0 // indirect - github.com/hashicorp/hcl v1.0.0 // indirect - github.com/imdario/mergo v0.3.16 // indirect - github.com/jackc/pgpassfile v1.0.0 // indirect - github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect - github.com/jackc/pgx/v5 v5.6.0 // indirect - github.com/jackc/puddle/v2 v2.2.1 // indirect - github.com/jinzhu/inflection v1.0.0 // indirect - github.com/jinzhu/now v1.1.5 // indirect - github.com/josharian/intern v1.0.0 // indirect - github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/cpuid/v2 v2.2.7 // indirect - github.com/leodido/go-urn v1.4.0 // indirect - github.com/magiconair/properties v1.8.7 // indirect - github.com/mailru/easyjson v0.7.7 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-sqlite3 v1.14.22 // indirect - github.com/microsoft/go-mssqldb v1.7.2 // indirect - github.com/moby/docker-image-spec v1.3.1 // indirect - github.com/moby/term v0.5.0 // indirect - github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/morikuni/aec v1.0.0 // indirect - github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/ncruces/go-strftime v0.1.9 // indirect - github.com/nicksnyder/go-i18n/v2 v2.4.0 // indirect - github.com/opencontainers/go-digest v1.0.0 // indirect - github.com/opencontainers/image-spec v1.1.0 // indirect - github.com/pelletier/go-toml/v2 v2.2.2 // indirect - github.com/pkg/errors v0.9.1 // indirect - github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect - github.com/sagikazarmark/locafero v0.6.0 // indirect - github.com/sagikazarmark/slog-shim v0.1.0 // indirect - github.com/sourcegraph/conc v0.3.0 // indirect - github.com/spf13/afero v1.11.0 // indirect - github.com/spf13/cast v1.6.0 // indirect - github.com/spf13/pflag v1.0.5 // indirect - github.com/subosito/gotenv v1.6.0 // indirect - github.com/twitchyliquid64/golang-asm v0.15.1 // indirect - github.com/ugorji/go/codec v1.2.12 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0 // indirect - go.opentelemetry.io/otel v1.27.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.23.1 // indirect - go.opentelemetry.io/otel/metric v1.27.0 // indirect - go.opentelemetry.io/otel/sdk v1.23.1 // indirect - go.opentelemetry.io/otel/trace v1.27.0 // indirect - go.uber.org/multierr v1.11.0 // indirect - golang.org/x/arch v0.8.0 // indirect - golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8 // indirect - golang.org/x/net v0.26.0 // indirect - golang.org/x/oauth2 v0.21.0 // indirect - golang.org/x/sync v0.7.0 // indirect - golang.org/x/sys v0.21.0 // indirect - golang.org/x/term v0.21.0 // indirect - golang.org/x/time v0.5.0 // indirect - golang.org/x/tools v0.22.0 // indirect - google.golang.org/protobuf v1.34.1 // indirect - gopkg.in/inf.v0 v0.9.1 // indirect - gopkg.in/ini.v1 v1.67.0 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect - gorm.io/driver/sqlserver v1.5.3 // indirect - gorm.io/plugin/dbresolver v1.5.1 // indirect - gotest.tools/v3 v3.5.1 // indirect - k8s.io/klog/v2 v2.120.1 // indirect - k8s.io/kube-openapi v0.0.0-20240521193020-835d969ad83a // indirect - k8s.io/utils v0.0.0-20240502163921-fe8a2dddb1d0 // indirect - modernc.org/libc v1.52.1 // indirect - modernc.org/mathutil v1.6.0 // indirect - modernc.org/memory v1.8.0 // indirect - modernc.org/sqlite v1.30.0 // indirect - sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect - sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect - sigs.k8s.io/yaml v1.4.0 // indirect -) diff --git a/go.sum b/go.sum deleted file mode 100644 index 989d834f..00000000 --- a/go.sum +++ /dev/null @@ -1,525 +0,0 @@ -filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= -filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.4.0/go.mod h1:ON4tFdPTwRcgWEaVDrN3584Ef+b7GgSJaXxe5fW9t4M= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.6.0/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.6.1/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.1/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.1 h1:lGlwhPtrX6EVml1hO0ivjkUxsSyl4dsiw9qcA1k/3IQ= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.1/go.mod h1:RKUqNu35KJYcVG/fqTRqmuXJZYNhYkBrnC/hX7yGbTA= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0/go.mod h1:OQeznEEkTZ9OrhHJoDD8ZDq51FHgXjqtP9z6bEwBq9U= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1 h1:sO0/P7g68FrryJzljemN+6GTssUXdANk6aJ7T1ZxnsQ= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1/go.mod h1:h8hyGFDsU5HMivxiS2iYFZsgDbU9OnnJ163x5UGVKYo= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.1.2/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.2.0/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0/go.mod h1:okt5dMMTOFjX/aovMlrjvvXoPMBVSPzk9185BT0+eZM= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.1 h1:6oNBlSdi1QqM1PNW7FPA6xOGA5UNsXnkaYZz9vdPGhA= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.1/go.mod h1:s4kgfzA0covAXNicZHDMN58jExvcng2mC/DepXiF1EI= -github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.0/go.mod h1:Q28U+75mpCaSCDowNEmhIo/rmgdkqmkmzI7N6TGR4UY= -github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1 h1:MyVTgWR8qd/Jw1Le0NZebGBUCLbtak3bJ3z1OlqZBpw= -github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1/go.mod h1:GpPjLhVR9dnUoJMyHWSPy71xY9/lcmpzIPZXmF0FCVY= -github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v0.8.0/go.mod h1:cw4zVQgBby0Z5f2v0itn6se2dDP17nTjbZFXW5uPyHA= -github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0 h1:D3occbWoio4EBLkbkevetNMAVX197GkzbUMtqjGWn80= -github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0/go.mod h1:bTSOgj05NGRuHHhQwAdPnYr9TOdNmKlZTgGLL6nyAdI= -github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= -github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= -github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0/go.mod h1:kgDmCTgBzIEPFElEF+FK0SdjAor06dRq2Go927dnQ6o= -github.com/AzureAD/microsoft-authentication-library-for-go v1.1.0/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= -github.com/AzureAD/microsoft-authentication-library-for-go v1.2.1 h1:DzHpqpoJVaCgOUdVHxE8QB52S6NiVdDQvGlny1qvPqA= -github.com/AzureAD/microsoft-authentication-library-for-go v1.2.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= -github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= -github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= -github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= -github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= -github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= -github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= -github.com/TwiN/go-color v1.4.1 h1:mqG0P/KBgHKVqmtL5ye7K0/Gr4l6hTksPgTgMk3mUzc= -github.com/TwiN/go-color v1.4.1/go.mod h1:WcPf/jtiW95WBIsEeY1Lc/b8aaWoiqQpu5cf8WFxu+s= -github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= -github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= -github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= -github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= -github.com/bytedance/sonic v1.11.8 h1:Zw/j1KfiS+OYTi9lyB3bb0CFxPJVkM17k1wyDG32LRA= -github.com/bytedance/sonic v1.11.8/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= -github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= -github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= -github.com/casbin/casbin/v2 v2.92.0 h1:gooKzb/V9lXsQ8utj0zKIAM44oJRIDW7/v+eLJwfAus= -github.com/casbin/casbin/v2 v2.92.0/go.mod h1:jX8uoN4veP85O/n2674r2qtfSXI6myvxW85f6TH50fw= -github.com/casbin/gorm-adapter/v3 v3.25.0 h1:XQkat6m7RvAiEkX4VEh+nUOq1559zbrfVwd8ip8wlCI= -github.com/casbin/gorm-adapter/v3 v3.25.0/go.mod h1:aftWi0cla0CC1bHQVrSFzBcX/98IFK28AvuPppCQgTs= -github.com/casbin/govaluate v1.1.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A= -github.com/casbin/govaluate v1.1.1 h1:J1rFKIBhiC5xr0APd5HP6rDL+xt+BRoyq1pa4o2i/5c= -github.com/casbin/govaluate v1.1.1/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A= -github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= -github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= -github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= -github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= -github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= -github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= -github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= -github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= -github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= -github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= -github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= -github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko= -github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= -github.com/docker/docker v26.1.4+incompatible h1:vuTpXDuoga+Z38m1OZHzl7NKisKWaWlhjQk7IDPSLsU= -github.com/docker/docker v26.1.4+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= -github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= -github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= -github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/duke-git/lancet/v2 v2.3.1 h1:cYZHQp57CZKP41EFkV/7TGbUrmhjaPMI5vi3Q+9KJNo= -github.com/duke-git/lancet/v2 v2.3.1/go.mod h1:zGa2R4xswg6EG9I6WnyubDbFO/+A/RROxIbXcwryTsc= -github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= -github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/emicklei/go-restful/v3 v3.12.1 h1:PJMDIM/ak7btuL8Ex0iYET9hxM3CI2sjZtzpL63nKAU= -github.com/emicklei/go-restful/v3 v3.12.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= -github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= -github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= -github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= -github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= -github.com/gabriel-vasile/mimetype v1.4.4 h1:QjV6pZ7/XZ7ryI2KuyeEDE8wnh7fHP9YnQy+R0LnH8I= -github.com/gabriel-vasile/mimetype v1.4.4/go.mod h1:JwLei5XPtWdGiMFB5Pjle1oEeoSeEuJfJE+TtfvdB/s= -github.com/gin-contrib/cors v1.7.2 h1:oLDHxdg8W/XDoN/8zamqk/Drgt4oVZDvaV0YmvVICQw= -github.com/gin-contrib/cors v1.7.2/go.mod h1:SUJVARKgQ40dmrzgXEVxj2m7Ig1v1qIboQkPDTQ9t2E= -github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4= -github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk= -github.com/gin-contrib/i18n v1.1.3 h1:tDhLDqU58Qx0K6BcBr0JugzQtWD/uiy21B9vEb8UJGo= -github.com/gin-contrib/i18n v1.1.3/go.mod h1:NKPag9OwNdiIeiBaaRUY9ILHbq9T19NPydIAwg6xyb8= -github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= -github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= -github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= -github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= -github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ= -github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc= -github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw= -github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ= -github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= -github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= -github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= -github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= -github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= -github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= -github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY= -github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk= -github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= -github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= -github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= -github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= -github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= -github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= -github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= -github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.21.0 h1:4fZA11ovvtkdgaeev9RGWPgc1uj3H8W+rNYyH/ySBb0= -github.com/go-playground/validator/v10 v10.21.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= -github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= -github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= -github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= -github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= -github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= -github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= -github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= -github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= -github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= -github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang-jwt/jwt/v4 v4.4.3/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= -github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= -github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= -github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= -github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= -github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= -github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= -github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= -github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= -github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc= -github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= -github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= -github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= -github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= -github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= -github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= -github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6 h1:k7nVchz72niMH6YLQNvHSdIE7iqsQxK1P41mySCvssg= -github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= -github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= -github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= -github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 h1:Wqo399gCIufwto+VfwCSvsnfGpF/w5E9CNxSwbpD6No= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0/go.mod h1:qmOFXW2epJhM0qSnUUYpldc7gVz2KMQwJ/QYCDIa7XU= -github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= -github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= -github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= -github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= -github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= -github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= -github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY= -github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw= -github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= -github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= -github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= -github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= -github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= -github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= -github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= -github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= -github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= -github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= -github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= -github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= -github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= -github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= -github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= -github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= -github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= -github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= -github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= -github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= -github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8= -github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= -github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= -github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= -github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= -github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= -github.com/microsoft/go-mssqldb v1.6.0/go.mod h1:00mDtPbeQCRGC1HwOOR5K/gr30P1NcEG0vx6Kbv2aJU= -github.com/microsoft/go-mssqldb v1.7.2 h1:CHkFJiObW7ItKTJfHo1QX7QBBD1iV+mn1eOyRP3b/PA= -github.com/microsoft/go-mssqldb v1.7.2/go.mod h1:kOvZKUdrhhFQmxLZqbwUV0rHkNkZpthMITIb2Ko1IoA= -github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= -github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= -github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= -github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= -github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8= -github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= -github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= -github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= -github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= -github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= -github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= -github.com/nicksnyder/go-i18n/v2 v2.4.0 h1:3IcvPOAvnCKwNm0TB0dLDTuawWEj+ax/RERNC+diLMM= -github.com/nicksnyder/go-i18n/v2 v2.4.0/go.mod h1:nxYSZE9M0bf3Y70gPQjN9ha7XNHX7gMc814+6wVyEI4= -github.com/onsi/ginkgo/v2 v2.17.2 h1:7eMhcy3GimbsA3hEnVKdw/PQM9XN9krpKVXsZdph0/g= -github.com/onsi/ginkgo/v2 v2.17.2/go.mod h1:nP2DPOQoNsQmsVyv5rDA8JkXQoCs6goXIvr/PRJ1eCc= -github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk= -github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0= -github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= -github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= -github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= -github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= -github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= -github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= -github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= -github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= -github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= -github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/redis/go-redis/v9 v9.5.2 h1:L0L3fcSNReTRGyZ6AqAEN0K56wYeYAwapBIhkvh0f3E= -github.com/redis/go-redis/v9 v9.5.2/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= -github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= -github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= -github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= -github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= -github.com/sagikazarmark/locafero v0.6.0 h1:ON7AQg37yzcRPU69mt7gwhFEBwxI6P9T4Qu3N51bwOk= -github.com/sagikazarmark/locafero v0.6.0/go.mod h1:77OmuIc6VTraTXKXIs/uvUxKGUXjE1GbemJYHqdNjX0= -github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= -github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= -github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= -github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= -github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= -github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= -github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= -github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= -github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= -github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE= -github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg= -github.com/swaggo/gin-swagger v1.6.0 h1:y8sxvQ3E20/RCyrXeFfg60r6H0Z+SwpTjMYsMm+zy8M= -github.com/swaggo/gin-swagger v1.6.0/go.mod h1:BG00cCEy294xtVpyIAHG6+e2Qzj/xKlRdOqDkvq0uzo= -github.com/swaggo/swag v1.16.3 h1:PnCYjPCah8FK4I26l2F/KQ4yz3sILcVUN3cTlBFA9Pg= -github.com/swaggo/swag v1.16.3/go.mod h1:DImHIuOFXKpMFAQjcC7FG4m3Dg4+QuUgUzJmKjI/gRk= -github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= -github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= -github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= -github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0 h1:9l89oX4ba9kHbBol3Xin3leYJ+252h0zszDtBwyKe2A= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0/go.mod h1:XLZfZboOJWHNKUv7eH0inh0E9VV6eWDFB/9yJyTLPp0= -go.opentelemetry.io/otel v1.27.0 h1:9BZoF3yMK/O1AafMiQTVu0YDj5Ea4hPhxCs7sGva+cg= -go.opentelemetry.io/otel v1.27.0/go.mod h1:DMpAK8fzYRzs+bi3rS5REupisuqTheUlSZJ1WnZaPAQ= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.23.1 h1:o8iWeVFa1BcLtVEV0LzrCxV2/55tB3xLxADr6Kyoey4= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.23.1/go.mod h1:SEVfdK4IoBnbT2FXNM/k8yC08MrfbhWk3U4ljM8B3HE= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.23.1 h1:cfuy3bXmLJS7M1RZmAL6SuhGtKUp2KEsrm00OlAXkq4= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.23.1/go.mod h1:22jr92C6KwlwItJmQzfixzQM3oyyuYLCfHiMY+rpsPU= -go.opentelemetry.io/otel/metric v1.27.0 h1:hvj3vdEKyeCi4YaYfNjv2NUje8FqKqUY8IlF0FxV/ik= -go.opentelemetry.io/otel/metric v1.27.0/go.mod h1:mVFgmRlhljgBiuk/MP/oKylr4hs85GZAylncepAX/ak= -go.opentelemetry.io/otel/sdk v1.23.1 h1:O7JmZw0h76if63LQdsBMKQDWNb5oEcOThG9IrxscV+E= -go.opentelemetry.io/otel/sdk v1.23.1/go.mod h1:LzdEVR5am1uKOOwfBWFef2DCi1nu3SA8XQxx2IerWFk= -go.opentelemetry.io/otel/trace v1.27.0 h1:IqYb813p7cmbHk0a5y6pD5JPakbVfftRXABGt5/Rscw= -go.opentelemetry.io/otel/trace v1.27.0/go.mod h1:6RiD1hkAprV4/q+yd2ln1HG9GoPx39SuvvstaLBl+l4= -go.opentelemetry.io/proto/otlp v1.1.0 h1:2Di21piLrCqJ3U3eXGCTPHE9R8Nh+0uglSnOyxikMeI= -go.opentelemetry.io/proto/otlp v1.1.0/go.mod h1:GpBHCBWiqvVLDqmHZsoMM3C5ySeKTC7ej/RNTae6MdY= -go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= -go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= -go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= -go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= -golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= -golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= -golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= -golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= -golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= -golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= -golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= -golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8 h1:LoYXNGAShUG3m/ehNk4iFctuhGX/+R1ZpfJ4/ia80JM= -golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= -golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= -golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= -golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= -golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= -golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= -golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= -golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= -golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= -golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= -golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 h1:9+tzLLstTlPTRyJTh+ah5wIMsBW5c4tQwGTN3thOW9Y= -google.golang.org/genproto/googleapis/api v0.0.0-20240311132316-a219d84964c2 h1:rIo7ocm2roD9DcFIX67Ym8icoGCKSARAiPljFhh5suQ= -google.golang.org/genproto/googleapis/api v0.0.0-20240311132316-a219d84964c2/go.mod h1:O1cOfN1Cy6QEYr7VxtjOyP5AdAuR0aJ/MYZaaof623Y= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240314234333-6e1732d8331c h1:lfpJ/2rWPa/kJgxyyXM8PrNnfCzcmxJ265mADgwmvLI= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240314234333-6e1732d8331c/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= -google.golang.org/grpc v1.62.1 h1:B4n+nfKzOICUXMgyrNd19h/I9oH0L1pizfk1d4zSgTk= -google.golang.org/grpc v1.62.1/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE= -google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= -google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= -gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= -gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= -gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gorm.io/driver/mysql v1.4.3/go.mod h1:sSIebwZAVPiT+27jK9HIwvsqOGKx3YMPmrA3mBJR10c= -gorm.io/driver/mysql v1.5.6 h1:Ld4mkIickM+EliaQZQx3uOJDJHtrd70MxAUqWqlx3Y8= -gorm.io/driver/mysql v1.5.6/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM= -gorm.io/driver/postgres v1.5.7 h1:8ptbNJTDbEmhdr62uReG5BGkdQyeasu/FZHxI0IMGnM= -gorm.io/driver/postgres v1.5.7/go.mod h1:3e019WlBaYI5o5LIdNV+LyxCMNtLOQETBXL2h4chKpA= -gorm.io/driver/sqlite v1.5.5 h1:7MDMtUZhV065SilG62E0MquljeArQZNfJnjd9i9gx3E= -gorm.io/driver/sqlite v1.5.5/go.mod h1:6NgQ7sQWAIFsPrJJl1lSNSu2TABh0ZZ/zm5fosATavE= -gorm.io/driver/sqlserver v1.5.3 h1:rjupPS4PVw+rjJkfvr8jn2lJ8BMhT4UW5FwuJY0P3Z0= -gorm.io/driver/sqlserver v1.5.3/go.mod h1:B+CZ0/7oFJ6tAlefsKoyxdgDCXJKSgwS2bMOQZT0I00= -gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= -gorm.io/gorm v1.25.2/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= -gorm.io/gorm v1.25.7-0.20240204074919-46816ad31dde/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= -gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= -gorm.io/gorm v1.25.10 h1:dQpO+33KalOA+aFYGlK+EfxcI5MbO7EP2yYygwh9h+s= -gorm.io/gorm v1.25.10/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= -gorm.io/plugin/dbresolver v1.5.1 h1:s9Dj9f7r+1rE3nx/Ywzc85nXptUEaeOO0pt27xdopM8= -gorm.io/plugin/dbresolver v1.5.1/go.mod h1:l4Cn87EHLEYuqUncpEeTC2tTJQkjngPSD+lo8hIvcT0= -gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= -gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= -k8s.io/api v0.30.1 h1:kCm/6mADMdbAxmIh0LBjS54nQBE+U4KmbCfIkF5CpJY= -k8s.io/api v0.30.1/go.mod h1:ddbN2C0+0DIiPntan/bye3SW3PdwLa11/0yqwvuRrJM= -k8s.io/apimachinery v0.30.1 h1:ZQStsEfo4n65yAdlGTfP/uSHMQSoYzU/oeEbkmF7P2U= -k8s.io/apimachinery v0.30.1/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc= -k8s.io/client-go v0.30.1 h1:uC/Ir6A3R46wdkgCV3vbLyNOYyCJ8oZnjtJGKfytl/Q= -k8s.io/client-go v0.30.1/go.mod h1:wrAqLNs2trwiCH/wxxmT/x3hKVH9PuV0GGW0oDoHVqc= -k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw= -k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/kube-openapi v0.0.0-20240521193020-835d969ad83a h1:zD1uj3Jf+mD4zmA7W+goE5TxDkI7OGJjBNBzq5fJtLA= -k8s.io/kube-openapi v0.0.0-20240521193020-835d969ad83a/go.mod h1:UxDHUPsUwTOOxSU+oXURfFBcAS6JwiRXTYqYwfuGowc= -k8s.io/utils v0.0.0-20240502163921-fe8a2dddb1d0 h1:jgGTlFYnhF1PM1Ax/lAlxUPE+KfCIXHaathvJg1C3ak= -k8s.io/utils v0.0.0-20240502163921-fe8a2dddb1d0/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -modernc.org/cc/v4 v4.21.2 h1:dycHFB/jDc3IyacKipCNSDrjIC0Lm1hyoWOZTRR20Lk= -modernc.org/cc/v4 v4.21.2/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ= -modernc.org/ccgo/v4 v4.17.10 h1:6wrtRozgrhCxieCeJh85QsxkX/2FFrT9hdaWPlbn4Zo= -modernc.org/ccgo/v4 v4.17.10/go.mod h1:0NBHgsqTTpm9cA5z2ccErvGZmtntSM9qD2kFAs6pjXM= -modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= -modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= -modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw= -modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU= -modernc.org/libc v1.52.1 h1:uau0VoiT5hnR+SpoWekCKbLqm7v6dhRL3hI+NQhgN3M= -modernc.org/libc v1.52.1/go.mod h1:HR4nVzFDSDizP620zcMCgjb1/8xk2lg5p/8yjfGv1IQ= -modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= -modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= -modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E= -modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU= -modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= -modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= -modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc= -modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss= -modernc.org/sqlite v1.30.0 h1:8YhPUs/HTnlEgErn/jSYQTwHN/ex8CjHHjg+K9iG7LM= -modernc.org/sqlite v1.30.0/go.mod h1:cgkTARJ9ugeXSNaLBPK3CqbOe7Ec7ZhWPoMFGldEYEw= -modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= -modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= -modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= -modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= -nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= -rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= -sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= -sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= -sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= -sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= -sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= -sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/internal/app/app.go b/internal/app/app.go deleted file mode 100644 index a7a94723..00000000 --- a/internal/app/app.go +++ /dev/null @@ -1,123 +0,0 @@ -package app - -import ( - "fmt" - _ "github.com/elabosak233/cloudsdale/api" - "github.com/elabosak233/cloudsdale/internal/app/config" - "github.com/elabosak233/cloudsdale/internal/app/db" - "github.com/elabosak233/cloudsdale/internal/app/logger" - "github.com/elabosak233/cloudsdale/internal/app/logger/adapter" - "github.com/elabosak233/cloudsdale/internal/extension/cache" - "github.com/elabosak233/cloudsdale/internal/extension/casbin" - "github.com/elabosak233/cloudsdale/internal/extension/container/provider" - "github.com/elabosak233/cloudsdale/internal/files" - "github.com/elabosak233/cloudsdale/internal/middleware" - "github.com/elabosak233/cloudsdale/internal/router" - "github.com/elabosak233/cloudsdale/internal/utils" - "github.com/elabosak233/cloudsdale/internal/utils/convertor" - "github.com/elabosak233/cloudsdale/internal/utils/validator" - "github.com/gin-contrib/cors" - ginI18n "github.com/gin-contrib/i18n" - "github.com/gin-gonic/gin" - "github.com/gin-gonic/gin/binding" - v10 "github.com/go-playground/validator/v10" - swaggerFiles "github.com/swaggo/files" - ginSwagger "github.com/swaggo/gin-swagger" - "go.uber.org/zap" - "golang.org/x/text/language" - "html/template" - "k8s.io/apimachinery/pkg/util/yaml" - "net/http" - "os" -) - -func init() { - data, _ := files.F().ReadFile("statics/banner.txt") - banner := string(data) - t, _ := template.New("cloudsdale").Parse(banner) - _ = t.Execute(os.Stdout, struct { - Version string - Commit string - }{ - Version: utils.GitTag, - Commit: utils.GitCommitID, - }) -} - -func Run() { - // Initialize the application - logger.InitLogger() - config.InitConfig() - db.InitDatabase() - casbin.InitCasbin() - provider.InitContainerProvider() - cache.InitCache() - - // Debug mode - isDebug := convertor.ToBoolD(os.Getenv("DEBUG"), false) - if isDebug { - db.Debug() - gin.SetMode(gin.DebugMode) - } else { - gin.SetMode(gin.ReleaseMode) - } - r := gin.New() - - if v, ok := binding.Validator.Engine().(*v10.Validate); ok { - _ = v.RegisterValidation("ascii", validator.IsASCII) - } - - r.Use(adapter.GinLogger(), adapter.GinRecovery(true)) - - // I18n configurations - r.Use(ginI18n.Localize(ginI18n.WithBundle(&ginI18n.BundleCfg{ - RootPath: "./i18n", - AcceptLanguage: []language.Tag{language.English, language.SimplifiedChinese}, - DefaultLanguage: language.English, - UnmarshalFunc: yaml.Unmarshal, - FormatBundleFile: "yaml", - Loader: &ginI18n.EmbedLoader{FS: files.F()}, - }))) - - // Cors configurations - cor := cors.DefaultConfig() - cor.AllowOrigins = config.AppCfg().Gin.CORS.AllowOrigins - cor.AllowMethods = config.AppCfg().Gin.CORS.AllowMethods - cor.AllowHeaders = []string{"Origin", "Content-Length", "Content-Type", "Authorization"} - cor.AllowCredentials = true - r.Use(cors.New(cor)) - - r.OPTIONS("/*path", func(c *gin.Context) { - c.Status(http.StatusOK) - }) - - router.InitRouter(r.Group("/api", middleware.Casbin())) - - if isDebug { - // Swagger docs - r.GET("/docs/*any", - ginSwagger.WrapHandler( - swaggerFiles.NewHandler(), - ginSwagger.PersistAuthorization(true), - ), - ) - } - - // Frontend resources - r.Use(middleware.Frontend("/")) - - srv := &http.Server{ - Addr: fmt.Sprintf( - "%s:%d", - config.AppCfg().Gin.Host, - config.AppCfg().Gin.Port, - ), - Handler: r, - } - zap.L().Info(fmt.Sprintf("Here's the address! %s:%d", config.AppCfg().Gin.Host, config.AppCfg().Gin.Port)) - zap.L().Info("The Cloudsdale service is running! Enjoy your hacking challenges!") - err := srv.ListenAndServe() - if err != nil { - zap.L().Fatal("Err... It seems that the port for Cloudsdale is not available. Plz try again.") - } -} diff --git a/internal/app/config/application.go b/internal/app/config/application.go deleted file mode 100644 index f095ae6b..00000000 --- a/internal/app/config/application.go +++ /dev/null @@ -1,179 +0,0 @@ -package config - -import ( - "github.com/elabosak233/cloudsdale/internal/files" - "github.com/elabosak233/cloudsdale/internal/utils" - "github.com/spf13/viper" - "go.uber.org/zap" - "io" - "io/fs" - "os" - "path" - "reflect" -) - -var ( - v1 *viper.Viper - appCfg ApplicationCfg -) - -type ApplicationCfg struct { - Gin struct { - Host string `yaml:"host" json:"host" mapstructure:"host"` - Port int `yaml:"port" json:"port" mapstructure:"port"` - CORS struct { - AllowOrigins []string `yaml:"allow_origins" json:"allow_origins" mapstructure:"allow_origins"` - AllowMethods []string `yaml:"allow_methods" json:"allow_methods" mapstructure:"allow_methods"` - } `yaml:"cors" json:"cors" mapstructure:"cors"` - Jwt struct { - Expiration int `yaml:"expiration" json:"expiration" mapstructure:"expiration"` - } `yaml:"jwt" json:"jwt" mapstructure:"jwt"` - Cache struct { - Provider string `yaml:"provider" json:"provider" mapstructure:"provider"` - Redis struct { - Host string `yaml:"host" json:"host" mapstructure:"host"` - Port int `yaml:"port" json:"port" mapstructure:"port"` - Password string `yaml:"password" json:"password" mapstructure:"password"` - DB int `yaml:"db" json:"db" mapstructure:"db"` - } `yaml:"redis" json:"redis" mapstructure:"redis"` - } `yaml:"cache" json:"cache" mapstructure:"cache"` - } `yaml:"gin" json:"gin" mapstructure:"gin"` - Email struct { - Address string `yaml:"address" json:"address" mapstructure:"address"` - Password string `yaml:"password" json:"password" mapstructure:"password"` - SMTP struct { - Host string `yaml:"host" json:"host" mapstructure:"host"` - Port int `yaml:"port" json:"port" mapstructure:"port"` - } `yaml:"smtp" json:"smtp" mapstructure:"smtp"` - } `yaml:"email" json:"email" mapstructure:"email"` - Captcha struct { - Enabled bool `yaml:"enabled" json:"enabled" mapstructure:"enabled"` - Provider string `yaml:"provider" json:"provider" mapstructure:"provider"` - ReCaptcha struct { - URL string `yaml:"url" json:"url" mapstructure:"url"` - SiteKey string `yaml:"site_key" json:"site_key" mapstructure:"site_key"` - SecretKey string `yaml:"secret_key" json:"secret_key" mapstructure:"secret_key"` - Threshold float64 `yaml:"threshold" json:"threshold" mapstructure:"threshold"` - } `yaml:"recaptcha" json:"recaptcha" mapstructure:"recaptcha"` - Turnstile struct { - URL string `yaml:"url" json:"url" mapstructure:"url"` - SiteKey string `yaml:"site_key" json:"site_key" mapstructure:"site_key"` - SecretKey string `yaml:"secret_key" json:"secret_key" mapstructure:"secret_key"` - } `yaml:"turnstile" json:"turnstile" mapstructure:"turnstile"` - } `yaml:"captcha" json:"captcha" mapstructure:"captcha"` - DB struct { - Provider string `yaml:"provider" json:"provider" mapstructure:"provider"` - Postgres struct { - Host string `yaml:"host" json:"host" mapstructure:"host"` - Port int `yaml:"port" json:"port" mapstructure:"port"` - Username string `yaml:"username" json:"username" mapstructure:"username"` - Password string `yaml:"password" json:"password" mapstructure:"password"` - Dbname string `yaml:"dbname" json:"dbname" mapstructure:"dbname"` - Sslmode string `yaml:"sslmode" json:"sslmode" mapstructure:"sslmode"` - } `yaml:"postgres" json:"postgres" mapstructure:"postgres"` - SQLite struct { - Path string `yaml:"path" json:"path" mapstructure:"path"` - } `yaml:"sqlite" json:"sqlite" mapstructure:"sqlite"` - MySQL struct { - Host string `yaml:"host" json:"host" mapstructure:"host"` - Port int `yaml:"port" json:"port" mapstructure:"port"` - Username string `yaml:"username" json:"username" mapstructure:"username"` - Password string `yaml:"password" json:"password" mapstructure:"password"` - Dbname string `yaml:"dbname" json:"dbname" mapstructure:"dbname"` - } `yaml:"mysql" json:"mysql" mapstructure:"mysql"` - } `yaml:"db" json:"db" mapstructure:"db"` - Container struct { - Provider string `yaml:"provider" json:"provider" mapstructure:"provider"` - Entry string `yaml:"entry" json:"entry" mapstructure:"entry"` - Docker struct { - URI string `yaml:"uri" json:"uri" mapstructure:"uri"` - } `yaml:"docker" json:"docker" mapstructure:"docker"` - K8s struct { - NameSpace string `yaml:"namespace" json:"namespace" mapstructure:"namespace"` - Config struct { - Path string `yaml:"path" json:"path" mapstructure:"path"` - } `yaml:"config" json:"config" mapstructure:"config"` - } `yaml:"k8s" json:"k8s" mapstructure:"k8s"` - Proxy struct { - Enabled bool `yaml:"enabled" json:"enabled" mapstructure:"enabled"` - TrafficCapture struct { - Enabled bool `yaml:"enabled" json:"enabled" mapstructure:"enabled"` - Path string `yaml:"path" json:"path" mapstructure:"path"` - } `yaml:"traffic_capture" json:"traffic_capture" mapstructure:"traffic_capture"` - } `yaml:"proxy" json:"proxy" mapstructure:"proxy"` - } `yaml:"container" json:"container" mapstructure:"container"` -} - -func AppCfg() *ApplicationCfg { - return &appCfg -} - -func InitApplicationCfg() { - v1 = viper.New() - configFile := path.Join(utils.ConfigsPath, "application.json") - v1.SetConfigType("json") - v1.SetConfigFile(configFile) - if _, err := os.Stat(configFile); err != nil { - zap.L().Warn("No configuration file found, default configuration file will be created.") - - // Read default configuration from files - defaultConfig, _err := files.F().Open("configs/application.json") - if _err != nil { - zap.L().Error("Unable to read default configuration file.") - return - } - defer func(defaultConfig fs.File) { - _ = defaultConfig.Close() - }(defaultConfig) - - // Create config file in current directory - dstConfig, _err := os.Create(configFile) - defer func(dstConfig *os.File) { - _ = dstConfig.Close() - }(dstConfig) - - if _, _err = io.Copy(dstConfig, defaultConfig); _err != nil { - zap.L().Fatal("Unable to create default configuration file.") - } - zap.L().Info("The default configuration file has been generated.") - } - - if err := v1.ReadInConfig(); err != nil { - zap.L().Fatal("Unable to read configuration file.", zap.Error(err)) - return - } - - if err := v1.Unmarshal(&appCfg); err != nil { - zap.L().Fatal("Unable to parse configuration file to structure.") - } - - Mkdirs() -} - -func Mkdirs() { - if AppCfg().Container.Proxy.TrafficCapture.Enabled { - if _, err := os.Stat(utils.CapturesPath); err != nil { - if _err := os.MkdirAll(utils.CapturesPath, os.ModePerm); _err != nil { - zap.L().Fatal("Unable to create directory for traffic capture.") - } - } - } - - if _, err := os.Stat(utils.MediaPath); err != nil { - if _err := os.MkdirAll(utils.MediaPath, os.ModePerm); _err != nil { - zap.L().Fatal("Unable to create directory for media.") - } - } -} - -func (a *ApplicationCfg) Save() (err error) { - val := reflect.ValueOf(appCfg) - typeOfCfg := val.Type() - - for i := 0; i < val.NumField(); i++ { - field := val.Field(i) - v1.Set(typeOfCfg.Field(i).Tag.Get("mapstructure"), field.Interface()) - } - err = v1.WriteConfig() - return err -} diff --git a/internal/app/config/config.go b/internal/app/config/config.go deleted file mode 100644 index 122b3c1a..00000000 --- a/internal/app/config/config.go +++ /dev/null @@ -1,24 +0,0 @@ -package config - -import ( - "github.com/elabosak233/cloudsdale/internal/utils" - "github.com/google/uuid" - "os" -) - -var ( - jwtSecretKey string -) - -func JwtSecretKey() string { - return jwtSecretKey -} - -func InitConfig() { - if _, err := os.Stat(utils.ConfigsPath); os.IsNotExist(err) { - _ = os.Mkdir(utils.ConfigsPath, os.ModePerm) - } - InitApplicationCfg() - InitPlatformCfg() - jwtSecretKey = uuid.NewString() -} diff --git a/internal/app/config/platform.go b/internal/app/config/platform.go deleted file mode 100644 index 0b5e66cc..00000000 --- a/internal/app/config/platform.go +++ /dev/null @@ -1,98 +0,0 @@ -package config - -import ( - "github.com/elabosak233/cloudsdale/internal/files" - "github.com/elabosak233/cloudsdale/internal/utils" - "github.com/spf13/viper" - "go.uber.org/zap" - "io" - "io/fs" - "os" - "path" - "reflect" -) - -var ( - v2 *viper.Viper - pltCfg PlatformCfg -) - -type PlatformCfg struct { - Site struct { - Title string `yaml:"title" json:"title" mapstructure:"title"` - Description string `yaml:"description" json:"description" mapstructure:"description"` - Color string `yaml:"color" json:"color" mapstructure:"color"` - } `yaml:"site" json:"site" mapstructure:"site"` - Container struct { - ParallelLimit int `yaml:"parallel_limit" json:"parallel_limit" mapstructure:"parallel_limit"` - RequestLimit int `yaml:"request_limit" json:"request_limit" mapstructure:"request_limit"` - } `yaml:"container" json:"container" mapstructure:"container"` - User struct { - Register struct { - Enabled bool `yaml:"enabled" json:"enabled" mapstructure:"enabled"` - Captcha struct { - Enabled bool `yaml:"enabled" json:"enabled" mapstructure:"enabled"` - } `yaml:"captcha" json:"captcha" mapstructure:"captcha"` - Email struct { - Domains []string `yaml:"domains" json:"domains" mapstructure:"domains"` - Enabled bool `yaml:"enabled" json:"enabled" mapstructure:"enabled"` - } - } `yaml:"register" json:"register" mapstructure:"register"` - } `yaml:"user" json:"user" mapstructure:"user"` -} - -func PltCfg() *PlatformCfg { - return &pltCfg -} - -func InitPlatformCfg() { - v2 = viper.New() - configFile := path.Join(utils.ConfigsPath, "platform.json") - v2.SetConfigType("json") - v2.SetConfigFile(configFile) - if _, err := os.Stat(configFile); err != nil { - zap.L().Warn("No configuration file found, default configuration file will be created.") - - // Read default configuration from files - defaultConfig, _err := files.F().Open("configs/platform.json") - if _err != nil { - zap.L().Error("Unable to read default configuration file.") - return - } - defer func(defaultConfig fs.File) { - _ = defaultConfig.Close() - }(defaultConfig) - - // Create config file in current directory - dstConfig, _err := os.Create(configFile) - defer func(dstConfig *os.File) { - _ = dstConfig.Close() - }(dstConfig) - - if _, _err = io.Copy(dstConfig, defaultConfig); _err != nil { - zap.L().Error("Unable to create default configuration file.") - } - zap.L().Info("The default configuration file has been generated.") - } - - if err := v2.ReadInConfig(); err != nil { - zap.L().Fatal("Unable to read configuration file.", zap.Error(err)) - return - } - - if err := v2.Unmarshal(&pltCfg); err != nil { - zap.L().Error("Unable to parse configuration file to structure.") - } -} - -func (p *PlatformCfg) Save() (err error) { - val := reflect.ValueOf(pltCfg) - typeOfCfg := val.Type() - - for i := 0; i < val.NumField(); i++ { - field := val.Field(i) - v2.Set(typeOfCfg.Field(i).Tag.Get("mapstructure"), field.Interface()) - } - err = v2.WriteConfig() - return err -} diff --git a/internal/app/db/db.go b/internal/app/db/db.go deleted file mode 100644 index 783ec5b5..00000000 --- a/internal/app/db/db.go +++ /dev/null @@ -1,178 +0,0 @@ -package db - -import ( - "fmt" - "github.com/elabosak233/cloudsdale/internal/app/config" - "github.com/elabosak233/cloudsdale/internal/app/logger/adapter" - "github.com/elabosak233/cloudsdale/internal/model" - "go.uber.org/zap" - "golang.org/x/crypto/bcrypt" - "gorm.io/driver/mysql" - "gorm.io/driver/postgres" - "gorm.io/driver/sqlite" - "gorm.io/gorm" -) - -var db *gorm.DB -var dbInfo string - -func Db() *gorm.DB { - return db -} - -// InitDatabase initializes the database connection and performs the necessary migrations. -func InitDatabase() { - initDatabaseEngine() - zap.L().Info(fmt.Sprintf("Database Connect Information: %s", dbInfo)) - db.Logger = adapter.NewGORMAdapter(zap.L()) - migrate() - initAdmin() - initDefaultCategories() - selfCheck() -} - -// Debug enables the debug mode of the database connection. -func Debug() { - db = db.Debug() -} - -// initDatabaseEngine initializes the database connection engine. -// It supports PostgreSQL, MySQL, and SQLite. -// The connection information is read from the configuration file. -// The connection information is formatted according to the database type. -// The connection is established using the GORM library. -// The database connection is stored in the global variable db. -// If an error occurs during the connection, the program will exit. -func initDatabaseEngine() { - var err error - switch config.AppCfg().DB.Provider { - case "postgres": - dbInfo = fmt.Sprintf( - "host=%s port=%d user=%s password=%s dbname=%s sslmode=%s", - config.AppCfg().DB.Postgres.Host, - config.AppCfg().DB.Postgres.Port, - config.AppCfg().DB.Postgres.Username, - config.AppCfg().DB.Postgres.Password, - config.AppCfg().DB.Postgres.Dbname, - config.AppCfg().DB.Postgres.Sslmode, - ) - db, err = gorm.Open(postgres.Open(dbInfo), &gorm.Config{}) - case "mysql": - dbInfo = fmt.Sprintf( - "%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local", - config.AppCfg().DB.MySQL.Username, - config.AppCfg().DB.MySQL.Password, - config.AppCfg().DB.MySQL.Host, - config.AppCfg().DB.MySQL.Port, - config.AppCfg().DB.MySQL.Dbname, - ) - db, err = gorm.Open(mysql.Open(dbInfo), &gorm.Config{}) - case "sqlite": - dbInfo = config.AppCfg().DB.SQLite.Path - db, err = gorm.Open(sqlite.Open(dbInfo), &gorm.Config{}) - } - if err != nil { - zap.L().Fatal("Database connection failed.", zap.Error(err)) - } -} - -// migrate performs the necessary migrations. -// It creates the tables if they do not exist. -func migrate() { - err := db.AutoMigrate( - &model.User{}, - &model.Category{}, - &model.Challenge{}, - &model.Team{}, - &model.UserTeam{}, - &model.Submission{}, - &model.Nat{}, - &model.Pod{}, - &model.Game{}, - &model.GameChallenge{}, - &model.GameTeam{}, - &model.Flag{}, - &model.Port{}, - &model.Nat{}, - &model.Env{}, - &model.Notice{}, - &model.Webhook{}, - ) - if err != nil { - zap.L().Fatal("Database sync failed.", zap.Error(err)) - } -} - -// selfCheck performs a self-check. -func selfCheck() { - db.Exec("DELETE FROM pods") -} - -func initAdmin() { - var count int64 - db.Model(&model.User{}).Where("username = ?", "admin").Count(&count) - if count == 0 { - zap.L().Warn("Administrator account does not exist, will be created soon.") - hashedPassword, _ := bcrypt.GenerateFromPassword([]byte("123456"), bcrypt.DefaultCost) - admin := model.User{ - Username: "admin", - Nickname: "Administrator", - Group: "admin", - Password: string(hashedPassword), - Email: "admin@admin.com", - } - err := db.Create(&admin).Error - if err != nil { - zap.L().Fatal("Super administrator account creation failed.", zap.Error(err)) - return - } - zap.L().Info("Super administrator account created successfully.") - } -} - -// initDefaultCategories initializes the default categories. -// If the categories do not exist, they will be created. -func initDefaultCategories() { - var count int64 - db.Model(&model.Category{}).Count(&count) - if count == 0 { - zap.L().Warn("Categories do not exist, will be created soon.") - defaultCategories := []model.Category{ - { - Name: "misc", - Description: "misc", - Color: "#3F51B5", - Icon: "fingerprint", - }, - { - Name: "web", - Description: "web", - Color: "#009688", - Icon: "language", - }, - { - Name: "pwn", - Description: "pwn", - Color: "#673AB7", - Icon: "function", - }, - { - Name: "crypto", - Description: "crypto", - Color: "#607D8B", - Icon: "tag", - }, - { - Name: "reverse", - Description: "reverse", - Color: "#6D4C41", - Icon: "keyboard_double_arrow_left", - }, - } - err := db.Create(&defaultCategories).Error - if err != nil { - zap.L().Fatal("Category initialization failed.", zap.Error(err)) - return - } - } -} diff --git a/internal/app/logger/adapter/gin.go b/internal/app/logger/adapter/gin.go deleted file mode 100644 index f0f2825a..00000000 --- a/internal/app/logger/adapter/gin.go +++ /dev/null @@ -1,96 +0,0 @@ -package adapter - -import ( - "errors" - "fmt" - "github.com/TwiN/go-color" - "github.com/gin-gonic/gin" - "go.uber.org/zap" - "net" - "net/http/httputil" - "os" - "runtime/debug" - "strings" - "time" -) - -func GinLogger() gin.HandlerFunc { - renderStatus := func(status int) string { - s := fmt.Sprintf(" %d ", status) - switch { - case status == 200: - return color.InWhiteOverCyan(s) - default: - return color.InWhiteOverYellow(s) - } - } - return func(c *gin.Context) { - start := time.Now() - path := c.Request.URL.Path - query := c.Request.URL.RawQuery - c.Next() - duration := time.Since(start) - if skip, exists := c.Get("skip_logging"); exists && skip.(bool) { - c.Next() - return - } - zap.L().Info(fmt.Sprintf( - "[%s] %s %s", - color.InCyan("GIN"), - renderStatus(c.Writer.Status()), - color.InBold(path)), - zap.Int("status", c.Writer.Status()), - zap.String("method", c.Request.Method), - zap.String("path", path), - zap.String("query", query), - zap.String("ip", c.ClientIP()), - zap.String("user-agent", c.Request.UserAgent()), - zap.String("errors", c.Errors.ByType(gin.ErrorTypePrivate).String()), - zap.Duration("duration", duration), - ) - } -} - -func GinRecovery(stack bool) gin.HandlerFunc { - return func(c *gin.Context) { - defer func() { - if err := recover(); err != nil { - var brokenPipe bool - var ne *net.OpError - if errors.As(err.(error), &ne) { - var se *os.SyscallError - if errors.As(ne.Err, &se) { - if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") { - brokenPipe = true - } - } - } - httpRequest, _ := httputil.DumpRequest(c.Request, false) - if brokenPipe { - zap.L().Error(c.Request.URL.Path, - zap.Any("error", err), - zap.String("request", string(httpRequest)), - ) - _ = c.Error(err.(error)) - c.Abort() - return - } - - if stack { - zap.L().Error("Recovery from panic.", - zap.Any("error", err), - zap.String("request", string(httpRequest)), - zap.String("stack", string(debug.Stack())), - ) - } else { - zap.L().Error("Recovery from panic.", - zap.Any("error", err), - zap.String("request", string(httpRequest)), - ) - } - c.Abort() - } - }() - c.Next() - } -} diff --git a/internal/app/logger/adapter/gorm.go b/internal/app/logger/adapter/gorm.go deleted file mode 100644 index 8a4c809e..00000000 --- a/internal/app/logger/adapter/gorm.go +++ /dev/null @@ -1,139 +0,0 @@ -package adapter - -import ( - "context" - "errors" - "fmt" - "github.com/TwiN/go-color" - "go.uber.org/zap" - "go.uber.org/zap/zapcore" - "gorm.io/gorm" - gormLogger "gorm.io/gorm/logger" - "path/filepath" - "runtime" - "strings" - "time" -) - -var ( - gormPackage = filepath.Join("gorm.io", "gorm") -) - -type ContextFn func(ctx context.Context) []zapcore.Field - -// GORMAdapter -// Modified from "moul.io/zapgorm2" -type GORMAdapter struct { - ZapLogger *zap.Logger - LogLevel gormLogger.LogLevel - SlowThreshold time.Duration - SkipCallerLookup bool - IgnoreRecordNotFoundError bool - Context ContextFn -} - -func NewGORMAdapter(zapLogger *zap.Logger) GORMAdapter { - return GORMAdapter{ - ZapLogger: zapLogger, - LogLevel: gormLogger.Warn, - SlowThreshold: 100 * time.Millisecond, - SkipCallerLookup: false, - IgnoreRecordNotFoundError: false, - Context: nil, - } -} - -func (a GORMAdapter) SetAsDefault() { - gormLogger.Default = a -} - -func (a GORMAdapter) LogMode(level gormLogger.LogLevel) gormLogger.Interface { - return GORMAdapter{ - ZapLogger: a.ZapLogger, - SlowThreshold: a.SlowThreshold, - LogLevel: level, - SkipCallerLookup: a.SkipCallerLookup, - IgnoreRecordNotFoundError: a.IgnoreRecordNotFoundError, - Context: a.Context, - } -} - -func (a GORMAdapter) Info(ctx context.Context, str string, args ...interface{}) { - if a.LogLevel < gormLogger.Info { - return - } - a.logger(ctx).Sugar().Debugf(str, args...) -} - -func (a GORMAdapter) Warn(ctx context.Context, str string, args ...interface{}) { - if a.LogLevel < gormLogger.Warn { - return - } - a.logger(ctx).Sugar().Warnf(str, args...) -} - -func (a GORMAdapter) Error(ctx context.Context, str string, args ...interface{}) { - if a.LogLevel < gormLogger.Error { - return - } - a.logger(ctx).Sugar().Errorf(str, args...) -} - -func (a GORMAdapter) Trace(ctx context.Context, begin time.Time, fc func() (string, int64), err error) { - if a.LogLevel <= 0 { - return - } - sql, rows := fc() - trace := fmt.Sprintf( - "[%s] %s", - color.InCyan("GORM"), - color.InBold(sql), - ) - elapsed := time.Since(begin) - logger := a.logger(ctx) - switch { - case err != nil && a.LogLevel >= gormLogger.Error && (!a.IgnoreRecordNotFoundError || !errors.Is(err, gorm.ErrRecordNotFound)): - logger.Error(trace, - zap.Error(err), - zap.Duration("elapsed", elapsed), - zap.Int64("rows", rows), - zap.String("sql", sql), - ) - case a.SlowThreshold != 0 && elapsed > a.SlowThreshold && a.LogLevel >= gormLogger.Warn: - logger.Warn(trace, - zap.Duration("elapsed", elapsed), - zap.Int64("rows", rows), - zap.String("sql", sql), - ) - case a.LogLevel >= gormLogger.Info: - logger.Debug(trace, - zap.Duration("elapsed", elapsed), - zap.Int64("rows", rows), - zap.String("sql", sql), - ) - } -} - -func (a GORMAdapter) logger(ctx context.Context) *zap.Logger { - logger := a.ZapLogger - if a.Context != nil { - fields := a.Context(ctx) - logger = logger.With(fields...) - } - - if a.SkipCallerLookup { - return logger - } - - for i := 2; i < 15; i++ { - _, file, _, ok := runtime.Caller(i) - switch { - case !ok: - case strings.HasSuffix(file, "_test.go"): - case strings.Contains(file, gormPackage): - default: - return logger.WithOptions(zap.AddCallerSkip(i)) - } - } - return logger -} diff --git a/internal/app/logger/encoder.go b/internal/app/logger/encoder.go deleted file mode 100644 index 689e07f0..00000000 --- a/internal/app/logger/encoder.go +++ /dev/null @@ -1,35 +0,0 @@ -package logger - -import ( - "github.com/TwiN/go-color" - "go.uber.org/zap/zapcore" - "time" -) - -var ( - _levelToColor = map[zapcore.Level]string{ - zapcore.DebugLevel: color.Cyan, - zapcore.InfoLevel: color.Cyan, - zapcore.WarnLevel: color.Yellow, - zapcore.ErrorLevel: color.Red, - zapcore.DPanicLevel: color.Red, - zapcore.PanicLevel: color.Red, - zapcore.FatalLevel: color.Red, - } -) - -func iLevelEncoder(level zapcore.Level, enc zapcore.PrimitiveArrayEncoder) { - c, ok := _levelToColor[level] - if !ok { - c = color.Cyan - } - enc.AppendString(color.Ize(c, " "+level.CapitalString()+" ")) -} - -func iTimeEncoder(t time.Time, enc zapcore.PrimitiveArrayEncoder) { - enc.AppendString("[" + t.Format("2006-01-02 15:04:05") + "]") -} - -func iCallerEncoder(caller zapcore.EntryCaller, enc zapcore.PrimitiveArrayEncoder) { - enc.AppendString("[" + caller.TrimmedPath() + "]") -} diff --git a/internal/app/logger/logger.go b/internal/app/logger/logger.go deleted file mode 100644 index e14c377c..00000000 --- a/internal/app/logger/logger.go +++ /dev/null @@ -1,89 +0,0 @@ -package logger - -import ( - "github.com/gin-gonic/gin" - "go.uber.org/zap" - "go.uber.org/zap/zapcore" - "gopkg.in/natefinch/lumberjack.v2" - "os" -) - -var ( - lg *zap.Logger -) - -type Cfg struct { - Level string `json:"level"` - Filename string `json:"filename"` - MaxSize int `json:"max_size"` - MaxAge int `json:"max_age"` - MaxBackups int `json:"max_backups"` -} - -var ( - cfg = Cfg{ - Level: "debug", - Filename: "logs/log.log", - MaxSize: 100, - MaxAge: 30, - MaxBackups: 30, - } -) - -func InitLogger() { - writeSyncer := getLogWriter(cfg.Filename, cfg.MaxSize, cfg.MaxBackups, cfg.MaxAge) - var l = new(zapcore.Level) - err := l.UnmarshalText([]byte(cfg.Level)) - // Create a console encoder config with color - consoleEncoderConfig := zapcore.EncoderConfig{ - LevelKey: "level", - TimeKey: "ts", - CallerKey: "", - MessageKey: "msg", - NameKey: "logger", - StacktraceKey: "stacktrace", - EncodeLevel: iLevelEncoder, - EncodeTime: iTimeEncoder, - EncodeCaller: iCallerEncoder, - EncodeDuration: zapcore.StringDurationEncoder, - ConsoleSeparator: " ", - } - consoleEncoder := zapcore.NewConsoleEncoder(consoleEncoderConfig) - consoleCore := zapcore.NewCore(consoleEncoder, zapcore.AddSync(os.Stdout), l) - gin.DefaultWriter = zapcore.AddSync(os.Stdout) - encoder := getEncoder() - core := zapcore.NewCore(encoder, writeSyncer, l) - multiCore := zapcore.NewTee(core, consoleCore) - lg = zap.New(multiCore, zap.AddCaller()) - zap.ReplaceGlobals(lg) - if err != nil { - zap.L().Fatal("Failed to load logging system.", - zap.Error(err), - ) - } - zap.L().Info("Logging module inits successfully.") -} - -func L() *zap.Logger { - return lg -} - -func getEncoder() zapcore.Encoder { - encoderConfig := zap.NewProductionEncoderConfig() - encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder - encoderConfig.TimeKey = "time" - encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder - encoderConfig.EncodeDuration = zapcore.SecondsDurationEncoder - encoderConfig.EncodeCaller = zapcore.ShortCallerEncoder - return zapcore.NewJSONEncoder(encoderConfig) -} - -func getLogWriter(filename string, maxSize, maxBackup, maxAge int) zapcore.WriteSyncer { - lumberJackLogger := &lumberjack.Logger{ - Filename: filename, - MaxSize: maxSize, - MaxBackups: maxBackup, - MaxAge: maxAge, - } - return zapcore.AddSync(lumberJackLogger) -} diff --git a/internal/controller/category.go b/internal/controller/category.go deleted file mode 100644 index c92e238d..00000000 --- a/internal/controller/category.go +++ /dev/null @@ -1,165 +0,0 @@ -package controller - -import ( - "fmt" - "github.com/elabosak233/cloudsdale/internal/extension/cache" - "github.com/elabosak233/cloudsdale/internal/model" - "github.com/elabosak233/cloudsdale/internal/model/request" - "github.com/elabosak233/cloudsdale/internal/service" - "github.com/elabosak233/cloudsdale/internal/utils" - "github.com/elabosak233/cloudsdale/internal/utils/convertor" - "github.com/elabosak233/cloudsdale/internal/utils/validator" - "github.com/gin-gonic/gin" - "net/http" - "time" -) - -type ICategoryController interface { - Create(ctx *gin.Context) - Update(ctx *gin.Context) - Find(ctx *gin.Context) - Delete(ctx *gin.Context) -} - -type CategoryController struct { - categoryService service.ICategoryService -} - -func NewCategoryController(s *service.Service) ICategoryController { - return &CategoryController{ - categoryService: s.CategoryService, - } -} - -// Find -// @Summary get category -// @Description -// @Tags Category -// @Accept json -// @Produce json -// @Security ApiKeyAuth -// @Param req query request.CategoryFindRequest true "CategoryFindRequest" -// @Router /categories/ [get] -func (c *CategoryController) Find(ctx *gin.Context) { - categoryFindRequest := request.CategoryFindRequest{} - err := ctx.ShouldBindQuery(&categoryFindRequest) - if err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{ - "code": http.StatusBadRequest, - "msg": validator.GetValidMsg(err, &categoryFindRequest), - }) - return - } - value, exist := cache.C().Get(fmt.Sprintf("categories:%s", utils.HashStruct(categoryFindRequest))) - if !exist { - categories, err := c.categoryService.Find(categoryFindRequest) - if err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{ - "code": http.StatusBadRequest, - "msg": err.Error(), - }) - return - } - value = gin.H{ - "code": http.StatusOK, - "data": categories, - } - cache.C().Set( - fmt.Sprintf("categories:%s", utils.HashStruct(categoryFindRequest)), - value, - 5*time.Minute, - ) - } - ctx.JSON(http.StatusOK, value) -} - -// Create -// @Summary create new category -// @Description -// @Tags Category -// @Accept json -// @Produce json -// @Security ApiKeyAuth -// @Param req body request.CategoryCreateRequest true "CategoryCreateRequest" -// @Router /categories/ [post] -func (c *CategoryController) Create(ctx *gin.Context) { - req := model.Category{} - if err := ctx.ShouldBindJSON(&req); err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{ - "code": http.StatusBadRequest, - "msg": validator.GetValidMsg(err, &req), - }) - return - } - err := c.categoryService.Create(req) - if err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{ - "code": http.StatusBadRequest, - "msg": err.Error(), - }) - return - } - cache.C().DeleteByPrefix("categories") - ctx.JSON(http.StatusOK, gin.H{ - "code": http.StatusOK, - }) -} - -// Update -// @Summary update category -// @Description -// @Tags Category -// @Accept json -// @Produce json -// @Security ApiKeyAuth -// @Param req body request.CategoryUpdateRequest true "CategoryUpdateRequest" -// @Router /categories/{id} [put] -func (c *CategoryController) Update(ctx *gin.Context) { - req := model.Category{} - if err := ctx.ShouldBindJSON(&req); err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{ - "code": http.StatusBadRequest, - "msg": validator.GetValidMsg(err, &req), - }) - return - } - req.ID = convertor.ToUintD(ctx.Param("id"), 0) - err := c.categoryService.Update(req) - if err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{ - "code": http.StatusBadRequest, - "msg": err.Error(), - }) - return - } - cache.C().DeleteByPrefix("categories") - ctx.JSON(http.StatusOK, gin.H{ - "code": http.StatusOK, - }) -} - -// Delete -// @Summary delete category -// @Description -// @Tags Category -// @Accept json -// @Produce json -// @Security ApiKeyAuth -// @Param req body request.CategoryDeleteRequest true "CategoryDeleteRequest" -// @Router /categories/{id} [delete] -func (c *CategoryController) Delete(ctx *gin.Context) { - req := request.CategoryDeleteRequest{} - req.ID = convertor.ToUintD(ctx.Param("id"), 0) - err := c.categoryService.Delete(req) - if err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{ - "code": http.StatusBadRequest, - "msg": err.Error(), - }) - return - } - cache.C().DeleteByPrefix("categories") - ctx.JSON(http.StatusOK, gin.H{ - "code": http.StatusOK, - }) -} diff --git a/internal/controller/challenge.go b/internal/controller/challenge.go deleted file mode 100644 index 6a1f7be5..00000000 --- a/internal/controller/challenge.go +++ /dev/null @@ -1,319 +0,0 @@ -package controller - -import ( - "fmt" - "github.com/elabosak233/cloudsdale/internal/extension/cache" - "github.com/elabosak233/cloudsdale/internal/model" - "github.com/elabosak233/cloudsdale/internal/model/request" - "github.com/elabosak233/cloudsdale/internal/service" - "github.com/elabosak233/cloudsdale/internal/utils" - "github.com/elabosak233/cloudsdale/internal/utils/convertor" - "github.com/elabosak233/cloudsdale/internal/utils/validator" - "github.com/gin-gonic/gin" - "net/http" - "time" -) - -type IChallengeController interface { - Find(ctx *gin.Context) - Create(ctx *gin.Context) - Update(ctx *gin.Context) - Delete(ctx *gin.Context) - CreateFlag(ctx *gin.Context) - UpdateFlag(ctx *gin.Context) - DeleteFlag(ctx *gin.Context) - SaveAttachment(ctx *gin.Context) - DeleteAttachment(ctx *gin.Context) -} - -type ChallengeController struct { - challengeService service.IChallengeService - flagService service.IFlagService - mediaService service.IMediaService -} - -func NewChallengeController(s *service.Service) IChallengeController { - return &ChallengeController{ - challengeService: s.ChallengeService, - flagService: s.FlagService, - mediaService: s.MediaService, - } -} - -// Find -// @Summary 题目查询 -// @Description -// @Tags Challenge -// @Accept json -// @Produce json -// @Security ApiKeyAuth -// @Param input query request.ChallengeFindRequest false "ChallengeFindRequest" -// @Router /challenges/ [get] -func (c *ChallengeController) Find(ctx *gin.Context) { - isDetailed := ctx.GetBool("is_detailed") - isPracticable := func() *bool { - if p, ok := ctx.Get("is_practicable"); ok { - return p.(*bool) - } - return nil - } - challengeFindRequest := request.ChallengeFindRequest{} - err := ctx.ShouldBindQuery(&challengeFindRequest) - if err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{ - "code": http.StatusBadRequest, - "msg": validator.GetValidMsg(err, &challengeFindRequest), - }) - return - } - user := ctx.MustGet("user").(*model.User) - challengeFindRequest.UserID = user.ID - challengeFindRequest.IsDetailed = &isDetailed - challengeFindRequest.IsPracticable = isPracticable() - value, exist := cache.C().Get(fmt.Sprintf("challenges:%s", utils.HashStruct(challengeFindRequest))) - if !exist { - challenges, total, _ := c.challengeService.Find(challengeFindRequest) - value = gin.H{ - "code": http.StatusOK, - "total": total, - "data": challenges, - } - cache.C().Set( - fmt.Sprintf("challenges:%s", utils.HashStruct(challengeFindRequest)), - value, - 5*time.Minute, - ) - } - ctx.JSON(http.StatusOK, value) -} - -// Create -// @Summary 创建题目 -// @Description -// @Tags Challenge -// @Accept json -// @Produce json -// @Security ApiKeyAuth -// @Param 创建请求 body request.ChallengeCreateRequest true "ChallengeCreateRequest" -// @Router /challenges/ [post] -func (c *ChallengeController) Create(ctx *gin.Context) { - challengeCreateRequest := request.ChallengeCreateRequest{} - err := ctx.ShouldBindJSON(&challengeCreateRequest) - if err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{ - "code": http.StatusBadRequest, - "msg": validator.GetValidMsg(err, &challengeCreateRequest), - }) - return - } - _ = c.challengeService.Create(challengeCreateRequest) - cache.C().DeleteByPrefix("challenges") - ctx.JSON(http.StatusOK, gin.H{ - "code": http.StatusOK, - }) -} - -// Update -// @Summary 更新题目 -// @Description -// @Tags Challenge -// @Accept json -// @Produce json -// @Security ApiKeyAuth -// @Param request body request.ChallengeUpdateRequest true "ChallengeUpdateRequest" -// @Router /challenges/{id} [put] -func (c *ChallengeController) Update(ctx *gin.Context) { - challengeUpdateRequest := request.ChallengeUpdateRequest{} - err := ctx.ShouldBindJSON(&challengeUpdateRequest) - if err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{ - "code": http.StatusBadRequest, - "msg": validator.GetValidMsg(err, &challengeUpdateRequest), - }) - return - } - challengeUpdateRequest.ID = convertor.ToUintD(ctx.Param("id"), 0) - err = c.challengeService.Update(challengeUpdateRequest) - cache.C().DeleteByPrefix("challenges") - if err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{ - "code": http.StatusBadRequest, - "msg": err.Error(), - }) - } - ctx.JSON(http.StatusOK, gin.H{ - "code": http.StatusOK, - }) -} - -// Delete -// @Summary 删除题目 -// @Description -// @Tags Challenge -// @Accept json -// @Produce json -// @Security ApiKeyAuth -// @Param request body request.ChallengeDeleteRequest true "ChallengeDeleteRequest" -// @Router /challenges/{id} [delete] -func (c *ChallengeController) Delete(ctx *gin.Context) { - challengeDeleteRequest := request.ChallengeDeleteRequest{} - challengeDeleteRequest.ID = convertor.ToUintD(ctx.Param("id"), 0) - err := c.challengeService.Delete(challengeDeleteRequest.ID) - cache.C().DeleteByPrefix("challenges") - if err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{ - "code": http.StatusBadRequest, - }) - } - ctx.JSON(http.StatusOK, gin.H{ - "code": http.StatusOK, - }) -} - -// CreateFlag -// @Summary 创建 flag -// @Description -// @Tags Challenge -// @Accept json -// @Produce json -// @Security ApiKeyAuth -// @Router /challenges/{id}/flags [post] -func (c *ChallengeController) CreateFlag(ctx *gin.Context) { - flagCreateRequest := request.FlagCreateRequest{} - if err := ctx.ShouldBindJSON(&flagCreateRequest); err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{ - "code": http.StatusBadRequest, - "msg": validator.GetValidMsg(err, &flagCreateRequest), - }) - return - } - flagCreateRequest.ChallengeID = convertor.ToUintD(ctx.Param("id"), 0) - err := c.flagService.Create(flagCreateRequest) - if err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{ - "code": http.StatusBadRequest, - "msg": err.Error(), - }) - return - } - cache.C().DeleteByPrefix("challenges") - ctx.JSON(http.StatusOK, gin.H{ - "code": http.StatusOK, - }) -} - -// UpdateFlag -// @Summary 更新 flag -// @Description -// @Tags Challenge -// @Accept json -// @Produce json -// @Security ApiKeyAuth -// @Router /challenges/{id}/flags/{flag_id} [put] -func (c *ChallengeController) UpdateFlag(ctx *gin.Context) { - flagUpdateRequest := request.FlagUpdateRequest{} - err := ctx.ShouldBindJSON(&flagUpdateRequest) - if err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{ - "code": http.StatusBadRequest, - "msg": validator.GetValidMsg(err, &flagUpdateRequest), - }) - return - } - flagUpdateRequest.ID = convertor.ToUintD(ctx.Param("flag_id"), 0) - flagUpdateRequest.ChallengeID = convertor.ToUintD(ctx.Param("id"), 0) - err = c.flagService.Update(flagUpdateRequest) - if err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{ - "code": http.StatusBadRequest, - "msg": err.Error(), - }) - return - } - cache.C().DeleteByPrefix("challenges") - ctx.JSON(http.StatusOK, gin.H{ - "code": http.StatusOK, - }) -} - -// DeleteFlag -// @Summary 删除 flag -// @Description -// @Tags Challenge -// @Accept json -// @Produce json -// @Security ApiKeyAuth -// @Router /challenges/{id}/flags/{flag_id} [delete] -func (c *ChallengeController) DeleteFlag(ctx *gin.Context) { - flagDeleteRequest := request.FlagDeleteRequest{} - flagDeleteRequest.ID = convertor.ToUintD(ctx.Param("flag_id"), 0) - err := c.flagService.Delete(flagDeleteRequest) - if err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{ - "code": http.StatusBadRequest, - "msg": err.Error(), - }) - return - } - cache.C().DeleteByPrefix("challenges") - ctx.JSON(http.StatusOK, gin.H{ - "code": http.StatusOK, - }) -} - -// SaveAttachment -// @Summary 保存附件 -// @Description -// @Tags Challenge -// @Accept json -// @Produce json -// @Security ApiKeyAuth -// @Param file formData file true "attachment" -// @Router /challenges/{id}/attachment [post] -func (c *ChallengeController) SaveAttachment(ctx *gin.Context) { - id := convertor.ToUintD(ctx.Param("id"), 0) - fileHeader, err := ctx.FormFile("file") - if err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{ - "code": http.StatusBadRequest, - "msg": err.Error(), - }) - return - } - err = c.mediaService.SaveChallengeAttachment(id, fileHeader) - if err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{ - "code": http.StatusBadRequest, - "msg": err.Error(), - }) - return - } - cache.C().DeleteByPrefix("challenges") - ctx.JSON(http.StatusOK, gin.H{ - "code": http.StatusOK, - }) -} - -// DeleteAttachment -// @Summary 删除附件 -// @Description -// @Tags Challenge -// @Accept json -// @Produce json -// @Security ApiKeyAuth -// @Router /challenges/{id}/attachment [delete] -func (c *ChallengeController) DeleteAttachment(ctx *gin.Context) { - id := convertor.ToUintD(ctx.Param("id"), 0) - err := c.mediaService.DeleteChallengeAttachment(id) - if err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{ - "code": http.StatusBadRequest, - "msg": err.Error(), - }) - return - } - cache.C().DeleteByPrefix("challenges") - ctx.JSON(http.StatusOK, gin.H{ - "code": http.StatusOK, - }) -} diff --git a/internal/controller/config.go b/internal/controller/config.go deleted file mode 100644 index 557c2a6c..00000000 --- a/internal/controller/config.go +++ /dev/null @@ -1,95 +0,0 @@ -package controller - -import ( - "github.com/elabosak233/cloudsdale/internal/app/config" - "github.com/elabosak233/cloudsdale/internal/model/request" - "github.com/elabosak233/cloudsdale/internal/service" - "github.com/elabosak233/cloudsdale/internal/utils/validator" - "github.com/gin-gonic/gin" - "net/http" -) - -type IConfigController interface { - Find(ctx *gin.Context) - Update(ctx *gin.Context) - FindCaptcha(ctx *gin.Context) -} - -type ConfigController struct { - configService service.IConfigService -} - -func NewConfigController(s *service.Service) IConfigController { - return &ConfigController{ - configService: s.ConfigService, - } -} - -// Find -// @Summary 配置全部查询 -// @Description 配置全部查询 -// @Tags Config -// @Accept json -// @Produce json -// @Router /configs/ [get] -func (c *ConfigController) Find(ctx *gin.Context) { - ctx.JSON(http.StatusOK, gin.H{ - "code": http.StatusOK, - "data": *(config.PltCfg()), - }) -} - -// Update -// @Summary 更新配置 -// @Description 更新配置 -// @Tags Config -// @Accept json -// @Produce json -// @Security ApiKeyAuth -// @Param input body request.ConfigUpdateRequest true "body" -// @Router /configs/ [put] -func (c *ConfigController) Update(ctx *gin.Context) { - configUpdateRequest := request.ConfigUpdateRequest{} - err := ctx.ShouldBindJSON(&configUpdateRequest) - if err != nil { - ctx.JSON(http.StatusOK, gin.H{ - "code": http.StatusBadRequest, - "msg": validator.GetValidMsg(err, &configUpdateRequest), - }) - return - } - if err := c.configService.Update(configUpdateRequest); err != nil { - ctx.JSON(http.StatusOK, gin.H{ - "code": http.StatusInternalServerError, - "msg": err.Error(), - }) - } else { - ctx.JSON(http.StatusOK, gin.H{ - "code": http.StatusOK, - "msg": "更新成功", - }) - } -} - -// FindCaptcha -// @Summary Captcha 配置查询 -// @Description Captcha 配置查询 -// @Tags Config -// @Accept json -// @Produce json -// @Router /configs/captcha [get] -func (c *ConfigController) FindCaptcha(ctx *gin.Context) { - ctx.JSON(http.StatusOK, gin.H{ - "code": http.StatusOK, - "data": map[string]any{ - "enabled": config.AppCfg().Captcha.Enabled, - "provider": config.AppCfg().Captcha.Provider, - "turnstile": map[string]any{ - "site_key": config.AppCfg().Captcha.Turnstile.SiteKey, - }, - "recaptcha": map[string]any{ - "site_key": config.AppCfg().Captcha.ReCaptcha.SiteKey, - }, - }, - }) -} diff --git a/internal/controller/controller.go b/internal/controller/controller.go deleted file mode 100644 index 10f0219b..00000000 --- a/internal/controller/controller.go +++ /dev/null @@ -1,52 +0,0 @@ -package controller - -import ( - "github.com/elabosak233/cloudsdale/internal/service" - "go.uber.org/zap" - "sync" -) - -var ( - c *Controller = nil - onceController sync.Once -) - -type Controller struct { - UserController IUserController - ChallengeController IChallengeController - PodController IPodController - ConfigController IConfigController - MediaController IMediaController - TeamController ITeamController - SubmissionController ISubmissionController - GameController IGameController - CategoryController ICategoryController - ProxyController IProxyController -} - -func C() *Controller { - if c == nil { - InitController() - } - return c -} - -func InitController() { - onceController.Do(func() { - s := service.S() - - c = &Controller{ - UserController: NewUserController(s), - ChallengeController: NewChallengeController(s), - PodController: NewPodController(s), - ConfigController: NewConfigController(s), - MediaController: NewMediaController(s), - TeamController: NewTeamController(s), - SubmissionController: NewSubmissionController(s), - GameController: NewGameController(s), - CategoryController: NewCategoryController(s), - ProxyController: NewProxyController(), - } - }) - zap.L().Info("Controller layer inits successfully.") -} diff --git a/internal/controller/game.go b/internal/controller/game.go deleted file mode 100644 index d58aa75e..00000000 --- a/internal/controller/game.go +++ /dev/null @@ -1,638 +0,0 @@ -package controller - -import ( - "fmt" - "github.com/elabosak233/cloudsdale/internal/extension/broadcast" - "github.com/elabosak233/cloudsdale/internal/extension/cache" - "github.com/elabosak233/cloudsdale/internal/model" - "github.com/elabosak233/cloudsdale/internal/model/request" - "github.com/elabosak233/cloudsdale/internal/service" - "github.com/elabosak233/cloudsdale/internal/utils" - "github.com/elabosak233/cloudsdale/internal/utils/convertor" - "github.com/elabosak233/cloudsdale/internal/utils/validator" - "github.com/gin-gonic/gin" - "net/http" - "time" -) - -type IGameController interface { - Create(ctx *gin.Context) - Find(ctx *gin.Context) - Delete(ctx *gin.Context) - Update(ctx *gin.Context) - BroadCast(ctx *gin.Context) - FindTeam(ctx *gin.Context) - CreateTeam(ctx *gin.Context) - UpdateTeam(ctx *gin.Context) - DeleteTeam(ctx *gin.Context) - FindChallenge(ctx *gin.Context) - CreateChallenge(ctx *gin.Context) - UpdateChallenge(ctx *gin.Context) - DeleteChallenge(ctx *gin.Context) - FindNotice(ctx *gin.Context) - CreateNotice(ctx *gin.Context) - UpdateNotice(ctx *gin.Context) - DeleteNotice(ctx *gin.Context) - SavePoster(ctx *gin.Context) - DeletePoster(ctx *gin.Context) -} - -type GameController struct { - gameService service.IGameService - gameChallengeService service.IGameChallengeService - gameTeamService service.IGameTeamService - challengeService service.IChallengeService - teamService service.ITeamService - noticeService service.INoticeService - mediaService service.IMediaService -} - -func NewGameController(s *service.Service) IGameController { - return &GameController{ - gameService: s.GameService, - gameChallengeService: s.GameChallengeService, - gameTeamService: s.GameTeamService, - challengeService: s.ChallengeService, - teamService: s.TeamService, - noticeService: s.NoticeService, - mediaService: s.MediaService, - } -} - -// BroadCast -// @Summary 广播消息 -// @Description 广播消息 -// @Tags Game -// @Router /games/{id}/broadcast [get] -func (g *GameController) BroadCast(ctx *gin.Context) { - id := convertor.ToUintD(ctx.Param("id"), 0) - if id != 0 { - broadcast.ServeGameHub(ctx.Writer, ctx.Request, id) - } -} - -// FindChallenge -// @Summary 查询比赛的挑战 -// @Description -// @Tags Game -// @Accept json -// @Produce json -// @Security ApiKeyAuth -// @Router /games/{id}/challenges [get] -func (g *GameController) FindChallenge(ctx *gin.Context) { - gameChallengeFindRequest := request.GameChallengeFindRequest{} - _ = ctx.ShouldBindQuery(&gameChallengeFindRequest) - gameChallengeFindRequest.GameID = convertor.ToUintD(ctx.Param("id"), 0) - value, exist := cache.C().Get(fmt.Sprintf("game_challenges:%s", utils.HashStruct(gameChallengeFindRequest))) - if !exist { - challenges, err := g.gameChallengeService.Find(gameChallengeFindRequest) - if err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{ - "code": http.StatusBadRequest, - }) - return - } - value = gin.H{ - "code": http.StatusOK, - "data": challenges, - } - cache.C().Set( - fmt.Sprintf("game_challenges:%s", utils.HashStruct(gameChallengeFindRequest)), - value, - 5*time.Minute, - ) - } - ctx.JSON(http.StatusOK, value) -} - -// CreateChallenge -// @Summary 添加比赛的挑战 -// @Description -// @Tags Game -// @Accept json -// @Produce json -// @Security ApiKeyAuth -// @Router /games/{id}/challenges [post] -func (g *GameController) CreateChallenge(ctx *gin.Context) { - gameChallengeCreateRequest := request.GameChallengeCreateRequest{} - err := ctx.ShouldBindJSON(&gameChallengeCreateRequest) - if err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{ - "code": http.StatusBadRequest, - "msg": validator.GetValidMsg(err, &gameChallengeCreateRequest), - }) - return - } - gameChallengeCreateRequest.GameID = convertor.ToUintD(ctx.Param("id"), 0) - err = g.gameChallengeService.Create(gameChallengeCreateRequest) - if err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{ - "code": http.StatusBadRequest, - "msg": err.Error(), - }) - return - } - cache.C().DeleteByPrefix("game_challenges") - ctx.JSON(http.StatusOK, gin.H{ - "code": http.StatusOK, - }) -} - -// UpdateChallenge -// @Summary 更新比赛的挑战 -// @Description -// @Tags Game -// @Accept json -// @Produce json -// @Security ApiKeyAuth -// @Router /games/{id}/challenges/{challenge_id} [put] -func (g *GameController) UpdateChallenge(ctx *gin.Context) { - gameChallengeUpdateRequest := request.GameChallengeUpdateRequest{} - if err := ctx.ShouldBindJSON(&gameChallengeUpdateRequest); err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{ - "code": http.StatusBadRequest, - "msg": validator.GetValidMsg(err, &gameChallengeUpdateRequest), - }) - return - } - gameChallengeUpdateRequest.GameID = convertor.ToUintD(ctx.Param("id"), 0) - gameChallengeUpdateRequest.ChallengeID = convertor.ToUintD(ctx.Param("challenge_id"), 0) - err := g.gameChallengeService.Update(gameChallengeUpdateRequest) - if err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{ - "code": http.StatusBadRequest, - "msg": err.Error(), - }) - return - } - cache.C().DeleteByPrefix("game_challenges") - ctx.JSON(http.StatusOK, gin.H{ - "code": http.StatusOK, - }) -} - -// DeleteChallenge -// @Summary 删除比赛的挑战 -// @Description -// @Tags Game -// @Accept json -// @Produce json -// @Security ApiKeyAuth -// @Router /games/{id}/challenges/{challenge_id} [delete] -func (g *GameController) DeleteChallenge(ctx *gin.Context) { - gameChallengeDeleteRequest := request.GameChallengeDeleteRequest{} - gameChallengeDeleteRequest.GameID = convertor.ToUintD(ctx.Param("id"), 0) - gameChallengeDeleteRequest.ChallengeID = convertor.ToUintD(ctx.Param("challenge_id"), 0) - err := g.gameChallengeService.Delete(gameChallengeDeleteRequest) - if err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{ - "code": http.StatusBadRequest, - "msg": err.Error(), - }) - return - } - cache.C().DeleteByPrefix("game_challenges") - ctx.JSON(http.StatusOK, gin.H{ - "code": http.StatusOK, - }) -} - -// FindTeam -// @Summary 查询比赛的团队 -// @Description -// @Tags Game -// @Accept json -// @Produce json -// @Security ApiKeyAuth -// @Router /games/{id}/teams [get] -func (g *GameController) FindTeam(ctx *gin.Context) { - gameTeamFindRequest := request.GameTeamFindRequest{} - _ = ctx.ShouldBindQuery(&gameTeamFindRequest) - gameTeamFindRequest.GameID = convertor.ToUintD(ctx.Param("id"), 0) - value, exist := cache.C().Get(fmt.Sprintf("game_teams:%s", utils.HashStruct(gameTeamFindRequest))) - if !exist { - teams, total, err := g.gameTeamService.Find(gameTeamFindRequest) - if err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{ - "code": http.StatusBadRequest, - }) - return - } - value = gin.H{ - "code": http.StatusOK, - "data": teams, - "total": total, - } - cache.C().Set( - fmt.Sprintf("game_teams:%s", utils.HashStruct(gameTeamFindRequest)), - value, - 5*time.Minute, - ) - } - ctx.JSON(http.StatusOK, value) -} - -// CreateTeam -// @Summary 加入比赛 -// @Description -// @Tags Game -// @Accept json -// @Produce json -// @Security ApiKeyAuth -// @Param 加入请求 body request.GameTeamCreateRequest true "GameTeamCreateRequest" -// @Router /games/{id}/teams [post] -func (g *GameController) CreateTeam(ctx *gin.Context) { - gameTeamCreateRequest := request.GameTeamCreateRequest{} - if err := ctx.ShouldBindJSON(&gameTeamCreateRequest); err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{ - "code": http.StatusBadRequest, - "msg": validator.GetValidMsg(err, &gameTeamCreateRequest), - }) - return - } - user := ctx.MustGet("user").(*model.User) - gameTeamCreateRequest.UserID = user.ID - gameTeamCreateRequest.ID = convertor.ToUintD(ctx.Param("id"), 0) - err := g.gameTeamService.Create(gameTeamCreateRequest) - if err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{ - "code": http.StatusBadRequest, - "msg": err.Error(), - }) - return - } - cache.C().DeleteByPrefix("game_teams") - ctx.JSON(http.StatusOK, gin.H{ - "code": http.StatusOK, - }) -} - -// UpdateTeam -// @Summary 允许加入比赛 -// @Description -// @Tags Game -// @Accept json -// @Produce json -// @Security ApiKeyAuth -// @Param 允许加入请求 body request.GameTeamUpdateRequest true "GameTeamUpdateRequest" -// @Router /games/{id}/teams/{team_id} [put] -func (g *GameController) UpdateTeam(ctx *gin.Context) { - gameTeamUpdateRequest := request.GameTeamUpdateRequest{} - gameTeamUpdateRequest.GameID = convertor.ToUintD(ctx.Param("id"), 0) - gameTeamUpdateRequest.TeamID = convertor.ToUintD(ctx.Param("team_id"), 0) - err := ctx.ShouldBindJSON(&gameTeamUpdateRequest) - err = g.gameTeamService.Update(gameTeamUpdateRequest) - if err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{ - "code": http.StatusBadRequest, - "msg": err.Error(), - }) - return - } - cache.C().DeleteByPrefix("game_teams") - ctx.JSON(http.StatusOK, gin.H{ - "code": http.StatusOK, - }) -} - -// DeleteTeam -// @Summary 删除比赛的团队 -// @Description -// @Tags Game -// @Accept json -// @Produce json -// @Security ApiKeyAuth -// @Router /games/{id}/teams/{team_id} [delete] -func (g *GameController) DeleteTeam(ctx *gin.Context) { - gameTeamDeleteRequest := request.GameTeamDeleteRequest{} - gameTeamDeleteRequest.GameID = convertor.ToUintD(ctx.Param("id"), 0) - gameTeamDeleteRequest.TeamID = convertor.ToUintD(ctx.Param("team_id"), 0) - err := g.gameTeamService.Delete(gameTeamDeleteRequest) - if err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{ - "code": http.StatusBadRequest, - "msg": err.Error(), - }) - return - } - cache.C().DeleteByPrefix("game_teams") - ctx.JSON(http.StatusOK, gin.H{ - "code": http.StatusOK, - }) -} - -// FindNotice -// @Summary 查询比赛的通知 -// @Description -// @Tags Game -// @Accept json -// @Produce json -// @Security ApiKeyAuth -// @Router /games/{id}/notices [get] -func (g *GameController) FindNotice(ctx *gin.Context) { - noticeFindRequest := request.NoticeFindRequest{} - noticeFindRequest.GameID = convertor.ToUintD(ctx.Param("id"), 0) - _ = ctx.ShouldBindQuery(¬iceFindRequest) - value, exist := cache.C().Get(fmt.Sprintf("game_notices:%s", utils.HashStruct(noticeFindRequest))) - if !exist { - notices, total, err := g.noticeService.Find(noticeFindRequest) - if err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{ - "code": http.StatusBadRequest, - }) - return - } - value = gin.H{ - "code": http.StatusOK, - "data": notices, - "total": total, - } - cache.C().Set( - fmt.Sprintf("game_notices:%s", utils.HashStruct(noticeFindRequest)), - value, - 5*time.Minute, - ) - } - ctx.JSON(http.StatusOK, value) -} - -// CreateNotice -// @Summary 添加比赛的通知 -// @Description -// @Tags Game -// @Accept json -// @Produce json -// @Security ApiKeyAuth -// @Router /games/{id}/notices [post] -func (g *GameController) CreateNotice(ctx *gin.Context) { - noticeCreateRequest := request.NoticeCreateRequest{} - err := ctx.ShouldBindJSON(¬iceCreateRequest) - if err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{ - "code": http.StatusBadRequest, - "msg": validator.GetValidMsg(err, ¬iceCreateRequest), - }) - return - } - noticeCreateRequest.GameID = convertor.ToUintD(ctx.Param("id"), 0) - err = g.noticeService.Create(noticeCreateRequest) - if err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{ - "code": http.StatusBadRequest, - "msg": err.Error(), - }) - return - } - cache.C().DeleteByPrefix("game_notices") - ctx.JSON(http.StatusOK, gin.H{ - "code": http.StatusOK, - }) -} - -// UpdateNotice -// @Summary 更新比赛的通知 -// @Description -// @Tags Game -// @Accept json -// @Produce json -// @Security ApiKeyAuth -// @Router /games/{id}/notices/{notice_id} [put] -func (g *GameController) UpdateNotice(ctx *gin.Context) { - noticeUpdateRequest := request.NoticeUpdateRequest{} - noticeUpdateRequest.GameID = convertor.ToUintD(ctx.Param("id"), 0) - noticeUpdateRequest.ID = convertor.ToUintD(ctx.Param("notice_id"), 0) - err := ctx.ShouldBindJSON(¬iceUpdateRequest) - err = g.noticeService.Update(noticeUpdateRequest) - if err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{ - "code": http.StatusBadRequest, - "msg": err.Error(), - }) - return - } - cache.C().DeleteByPrefix("game_notices") - ctx.JSON(http.StatusOK, gin.H{ - "code": http.StatusOK, - }) -} - -// DeleteNotice -// @Summary 删除比赛的通知 -// @Description -// @Tags Game -// @Accept json -// @Produce json -// @Security ApiKeyAuth -// @Router /games/{id}/notices/{notice_id} [delete] -func (g *GameController) DeleteNotice(ctx *gin.Context) { - noticeDeleteRequest := request.NoticeDeleteRequest{} - noticeDeleteRequest.ID = convertor.ToUintD(ctx.Param("notice_id"), 0) - err := g.noticeService.Delete(noticeDeleteRequest) - if err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{ - "code": http.StatusBadRequest, - "msg": err.Error(), - }) - return - } - cache.C().DeleteByPrefix("game_notices") - ctx.JSON(http.StatusOK, gin.H{ - "code": http.StatusOK, - }) -} - -// Create -// @Summary 创建比赛 -// @Description -// @Tags Game -// @Accept json -// @Produce json -// @Security ApiKeyAuth -// @Param 创建请求 body request.GameCreateRequest true "GameCreateRequest" -// @Router /games/ [post] -func (g *GameController) Create(ctx *gin.Context) { - gameCreateRequest := request.GameCreateRequest{} - err := ctx.ShouldBindJSON(&gameCreateRequest) - if err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{ - "code": http.StatusBadRequest, - "msg": validator.GetValidMsg(err, &gameCreateRequest), - }) - return - } - err = g.gameService.Create(gameCreateRequest) - if err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{ - "code": http.StatusBadRequest, - "msg": err.Error(), - }) - return - } - cache.C().DeleteByPrefix("games") - ctx.JSON(http.StatusOK, gin.H{ - "code": http.StatusOK, - }) -} - -// Delete -// @Summary 删除比赛 -// @Description -// @Tags Game -// @Accept json -// @Produce json -// @Security ApiKeyAuth -// @Param 删除请求 body request.GameDeleteRequest true "GameDeleteRequest" -// @Router /games/{id} [delete] -func (g *GameController) Delete(ctx *gin.Context) { - gameDeleteRequest := request.GameDeleteRequest{} - gameDeleteRequest.ID = convertor.ToUintD(ctx.Param("id"), 0) - err := g.gameService.Delete(gameDeleteRequest) - if err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{ - "code": http.StatusBadRequest, - "msg": err.Error(), - }) - return - } - cache.C().DeleteByPrefix("games") - ctx.JSON(http.StatusOK, gin.H{ - "code": http.StatusOK, - }) -} - -// Update -// @Summary 更新比赛 -// @Description -// @Tags Game -// @Accept json -// @Produce json -// @Security ApiKeyAuth -// @Param 更新请求 body request.GameUpdateRequest true "GameUpdateRequest" -// @Router /games/{id} [put] -func (g *GameController) Update(ctx *gin.Context) { - gameUpdateRequest := request.GameUpdateRequest{} - err := ctx.ShouldBindJSON(&gameUpdateRequest) - if err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{ - "code": http.StatusBadRequest, - "msg": validator.GetValidMsg(err, &gameUpdateRequest), - }) - return - } - gameUpdateRequest.ID = convertor.ToUintD(ctx.Param("id"), 0) - err = g.gameService.Update(gameUpdateRequest) - if err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{ - "code": http.StatusBadRequest, - "msg": err.Error(), - }) - return - } - cache.C().DeleteByPrefix("games") - ctx.JSON(http.StatusOK, gin.H{ - "code": http.StatusOK, - }) -} - -// Find -// @Summary 比赛查询 -// @Description -// @Tags Game -// @Accept json -// @Produce json -// @Security ApiKeyAuth -// @Param 查找请求 query request.GameFindRequest false "GameFindRequest" -// @Router /games/ [get] -func (g *GameController) Find(ctx *gin.Context) { - isEnabled, ok := ctx.Get("is_enabled") - gameFindRequest := request.GameFindRequest{} - err := ctx.ShouldBindQuery(&gameFindRequest) - if err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{ - "code": http.StatusBadRequest, - "msg": validator.GetValidMsg(err, &gameFindRequest), - }) - return - } - if ok && isEnabled.(bool) { - gameFindRequest.IsEnabled = &utils.True - } - value, exist := cache.C().Get(fmt.Sprintf("games:%s", utils.HashStruct(gameFindRequest))) - if !exist { - games, total, err := g.gameService.Find(gameFindRequest) - if err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{ - "code": http.StatusBadRequest, - }) - return - } - value = gin.H{ - "code": http.StatusOK, - "data": games, - "total": total, - } - cache.C().Set( - fmt.Sprintf("games:%s", utils.HashStruct(gameFindRequest)), - value, - 5*time.Minute, - ) - } - ctx.JSON(http.StatusOK, value) -} - -// SavePoster -// @Summary 保存头图 -// @Description -// @Tags Game -// @Accept json -// @Produce json -// @Security ApiKeyAuth -// @Param file formData file true "poster" -// @Router /games/{id}/poster [post] -func (g *GameController) SavePoster(ctx *gin.Context) { - id := convertor.ToUintD(ctx.Param("id"), 0) - fileHeader, err := ctx.FormFile("file") - if err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{ - "code": http.StatusBadRequest, - "msg": err.Error(), - }) - return - } - err = g.mediaService.SaveGamePoster(id, fileHeader) - if err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{ - "code": http.StatusBadRequest, - "msg": err.Error(), - }) - return - } - cache.C().DeleteByPrefix("games") - ctx.JSON(http.StatusOK, gin.H{ - "code": http.StatusOK, - }) -} - -// DeletePoster -// @Summary 删除海报 -// @Description -// @Tags Game -// @Accept json -// @Produce json -// @Security ApiKeyAuth -// @Router /games/{id}/poster [delete] -func (g *GameController) DeletePoster(ctx *gin.Context) { - id := convertor.ToUintD(ctx.Param("id"), 0) - err := g.mediaService.DeleteGamePoster(id) - if err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{ - "code": http.StatusBadRequest, - "msg": err.Error(), - }) - return - } - cache.C().DeleteByPrefix("games") - ctx.JSON(http.StatusOK, gin.H{ - "code": http.StatusOK, - }) -} diff --git a/internal/controller/media.go b/internal/controller/media.go deleted file mode 100644 index 79af7088..00000000 --- a/internal/controller/media.go +++ /dev/null @@ -1,35 +0,0 @@ -package controller - -import ( - "github.com/elabosak233/cloudsdale/internal/service" - "github.com/elabosak233/cloudsdale/internal/utils" - "github.com/gin-gonic/gin" - "net/http" - "os" - "path" -) - -type IMediaController interface { - GetFile(ctx *gin.Context) -} - -type MediaController struct { - mediaService service.IMediaService -} - -func NewMediaController(s *service.Service) IMediaController { - return &MediaController{ - mediaService: s.MediaService, - } -} - -func (m *MediaController) GetFile(ctx *gin.Context) { - a := ctx.Param("path") - p := path.Join(utils.MediaPath, a) - _, err := os.Stat(p) - if os.IsNotExist(err) { - ctx.Status(http.StatusNotFound) - return - } - ctx.File(p) -} diff --git a/internal/controller/pod.go b/internal/controller/pod.go deleted file mode 100644 index 9b4af160..00000000 --- a/internal/controller/pod.go +++ /dev/null @@ -1,157 +0,0 @@ -package controller - -import ( - "fmt" - "github.com/elabosak233/cloudsdale/internal/extension/cache" - "github.com/elabosak233/cloudsdale/internal/model" - "github.com/elabosak233/cloudsdale/internal/model/request" - "github.com/elabosak233/cloudsdale/internal/service" - "github.com/elabosak233/cloudsdale/internal/utils" - "github.com/elabosak233/cloudsdale/internal/utils/convertor" - "github.com/elabosak233/cloudsdale/internal/utils/validator" - "github.com/gin-gonic/gin" - "net/http" - "time" -) - -type IPodController interface { - Create(ctx *gin.Context) - Remove(ctx *gin.Context) - Renew(ctx *gin.Context) - Find(ctx *gin.Context) -} - -type PodController struct { - podService service.IPodService -} - -func NewPodController(s *service.Service) IPodController { - return &PodController{ - podService: s.PodService, - } -} - -// Create -// @Summary 创建实例 -// @Description 创建实例 -// @Tags Pod -// @Accept json -// @Produce json -// @Security ApiKeyAuth -// @Param input body request.PodCreateRequest true "PodCreateRequest" -// @Router /pods/ [post] -func (c *PodController) Create(ctx *gin.Context) { - podCreateRequest := request.PodCreateRequest{} - if err := ctx.ShouldBindJSON(&podCreateRequest); err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{ - "code": http.StatusBadRequest, - "msg": validator.GetValidMsg(err, &podCreateRequest), - }) - return - } - user := ctx.MustGet("user").(*model.User) - podCreateRequest.UserID = user.ID - pod, err := c.podService.Create(podCreateRequest) - if err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{ - "code": http.StatusBadRequest, - "msg": err.Error(), - }) - return - } - cache.C().DeleteByPrefix("pods") - ctx.JSON(http.StatusOK, gin.H{ - "code": http.StatusOK, - "data": pod, - }) -} - -// Remove -// @Summary 停止并删除容器 -// @Description 停止并删除容器 -// @Tags Pod -// @Produce json -// @Security ApiKeyAuth -// @Param input body request.PodRemoveRequest true "PodRemoveRequest" -// @Router /pods/{id} [delete] -func (c *PodController) Remove(ctx *gin.Context) { - instanceRemoveRequest := request.PodRemoveRequest{} - err := ctx.ShouldBindJSON(&instanceRemoveRequest) - instanceRemoveRequest.ID = convertor.ToUintD(ctx.Param("id"), 0) - user := ctx.MustGet("user").(*model.User) - instanceRemoveRequest.UserID = user.ID - err = c.podService.Remove(instanceRemoveRequest) - if err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{ - "code": http.StatusBadRequest, - "msg": err.Error(), - }) - return - } - cache.C().DeleteByPrefix("pods") - ctx.JSON(http.StatusOK, gin.H{ - "code": http.StatusOK, - }) -} - -// Renew -// @Summary 容器续期 -// @Description 容器续期 -// @Tags Pod -// @Produce json -// @Security ApiKeyAuth -// @Param input body request.PodRenewRequest true "PodRenewRequest" -// @Router /pods/{id} [put] -func (c *PodController) Renew(ctx *gin.Context) { - instanceRenewRequest := request.PodRenewRequest{} - err := ctx.ShouldBindJSON(&instanceRenewRequest) - instanceRenewRequest.ID = convertor.ToUintD(ctx.Param("id"), 0) - user := ctx.MustGet("user").(*model.User) - instanceRenewRequest.UserID = user.ID - err = c.podService.Renew(instanceRenewRequest) - if err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{ - "code": http.StatusBadRequest, - "msg": err.Error(), - }) - return - } - cache.C().DeleteByPrefix("pods") - ctx.JSON(http.StatusOK, gin.H{ - "code": http.StatusOK, - }) -} - -// Find -// @Summary 实例查询 -// @Description 实例查询 -// @Tags Pod -// @Produce json -// @Security ApiKeyAuth -// @Param input query request.PodFindRequest false "PodFindRequest" -// @Router /pods/ [get] -func (c *PodController) Find(ctx *gin.Context) { - podFindRequest := request.PodFindRequest{} - if err := ctx.ShouldBindQuery(&podFindRequest); err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{ - "code": http.StatusBadRequest, - "msg": validator.GetValidMsg(err, &podFindRequest), - }) - return - } - value, exist := cache.C().Get(fmt.Sprintf("pods:%s", utils.HashStruct(podFindRequest))) - if !exist { - pods, total, _ := c.podService.Find(podFindRequest) - value = gin.H{ - "code": http.StatusOK, - "data": pods, - "total": total, - } - cache.C().Set( - fmt.Sprintf("pods:%s", utils.HashStruct(podFindRequest)), - value, - 2*time.Minute, - ) - } - ctx.JSON(http.StatusOK, value) -} diff --git a/internal/controller/proxy.go b/internal/controller/proxy.go deleted file mode 100644 index 5005abaa..00000000 --- a/internal/controller/proxy.go +++ /dev/null @@ -1,48 +0,0 @@ -package controller - -import ( - "fmt" - "github.com/elabosak233/cloudsdale/internal/extension/proxy" - "github.com/gin-gonic/gin" - "github.com/gorilla/websocket" - "go.uber.org/zap" - "net/http" - "time" -) - -type IProxyController interface { - Connect(ctx *gin.Context) -} - -type ProxyController struct { -} - -func NewProxyController() IProxyController { - return &ProxyController{} -} - -// Connect -// TCP over WebSocket in Cloudsdale requires a complete Websocket header to establish a connection. -func (p *ProxyController) Connect(ctx *gin.Context) { - id := ctx.Param("id") - upgrade := websocket.Upgrader{ - CheckOrigin: func(r *http.Request) bool { - return true - }, - } - - conn, err := upgrade.Upgrade(ctx.Writer, ctx.Request, nil) - if err != nil { - zap.L().Error("Failed to upgrade to WebSocket", zap.Error(err)) - return - } - defer func(conn *websocket.Conn) { - _ = conn.Close() - }(conn) - _ = conn.SetReadDeadline(time.Time{}) - _ = conn.SetWriteDeadline(time.Time{}) - if ws, ok := proxy.WSProxyMap[id]; ok { - zap.L().Info(fmt.Sprintf("Websocket proxy found %s -> %s", id, ws.Target)) - ws.Handle(conn) - } -} diff --git a/internal/controller/submission.go b/internal/controller/submission.go deleted file mode 100644 index d27a6127..00000000 --- a/internal/controller/submission.go +++ /dev/null @@ -1,139 +0,0 @@ -package controller - -import ( - "fmt" - "github.com/elabosak233/cloudsdale/internal/extension/cache" - "github.com/elabosak233/cloudsdale/internal/model" - "github.com/elabosak233/cloudsdale/internal/model/request" - "github.com/elabosak233/cloudsdale/internal/service" - "github.com/elabosak233/cloudsdale/internal/utils" - "github.com/elabosak233/cloudsdale/internal/utils/convertor" - "github.com/elabosak233/cloudsdale/internal/utils/validator" - "github.com/gin-gonic/gin" - "net/http" - "time" -) - -type ISubmissionController interface { - Find(ctx *gin.Context) - Create(ctx *gin.Context) - Delete(ctx *gin.Context) -} - -type SubmissionController struct { - submissionService service.ISubmissionService -} - -func NewSubmissionController(s *service.Service) ISubmissionController { - return &SubmissionController{ - submissionService: s.SubmissionService, - } -} - -// Find -// @Summary 提交记录查询 -// @Description -// @Tags Submission -// @Accept json -// @Produce json -// @Security ApiKeyAuth -// @Param 查找请求 query request.SubmissionFindRequest false "SubmissionFindRequest" -// @Router /submissions/ [get] -func (c *SubmissionController) Find(ctx *gin.Context) { - submissionFindRequest := request.SubmissionFindRequest{} - if err := ctx.ShouldBind(&submissionFindRequest); err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{ - "code": http.StatusBadRequest, - "msg": validator.GetValidMsg(err, &submissionFindRequest), - }) - return - } - submissionFindRequest.IsDetailed = ctx.GetBool("is_detailed") - value, exist := cache.C().Get(fmt.Sprintf("submissions:%s", utils.HashStruct(submissionFindRequest))) - if !exist { - submissions, total, err := c.submissionService.Find(submissionFindRequest) - if err != nil { - ctx.JSON(http.StatusInternalServerError, gin.H{ - "code": http.StatusInternalServerError, - "msg": err.Error(), - }) - return - } - value = gin.H{ - "code": http.StatusOK, - "total": total, - "data": submissions, - } - cache.C().Set( - fmt.Sprintf("submissions:%s", utils.HashStruct(submissionFindRequest)), - value, - 5*time.Minute, - ) - } - ctx.JSON(http.StatusOK, value) -} - -// Create -// @Summary 提交 -// @Description -// @Tags Submission -// @Accept json -// @Produce json -// @Security ApiKeyAuth -// @Param 创建请求 body request.SubmissionCreateRequest true "SubmissionCreateRequest" -// @Router /submissions/ [post] -func (c *SubmissionController) Create(ctx *gin.Context) { - submissionCreateRequest := request.SubmissionCreateRequest{} - if err := ctx.ShouldBindJSON(&submissionCreateRequest); err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{ - "code": http.StatusBadRequest, - "msg": validator.GetValidMsg(err, &submissionCreateRequest), - }) - return - } - user := ctx.MustGet("user").(*model.User) - submissionCreateRequest.UserID = user.ID - status, rank, err := c.submissionService.Create(submissionCreateRequest) - if err != nil { - ctx.JSON(http.StatusInternalServerError, gin.H{ - "code": http.StatusInternalServerError, - "msg": err.Error(), - }) - return - } - cache.C().DeleteByPrefix("submissions") - if status == 2 { - cache.C().DeleteByPrefix("challenges") - cache.C().DeleteByPrefix("game_challenges") - } - ctx.JSON(http.StatusOK, gin.H{ - "code": http.StatusOK, - "rank": rank, - "status": status, - }) -} - -// Delete -// @Summary delete submission -// @Description -// @Tags Submission -// @Accept json -// @Produce json -// @Security ApiKeyAuth -// @Param 删除请求 body request.SubmissionDeleteRequest true "SubmissionDeleteRequest" -// @Router /submissions/{id} [delete] -func (c *SubmissionController) Delete(ctx *gin.Context) { - deleteSubmissionRequest := request.SubmissionDeleteRequest{} - deleteSubmissionRequest.SubmissionID = convertor.ToUintD(ctx.Param("id"), 0) - if err := c.submissionService.Delete(deleteSubmissionRequest.SubmissionID); err != nil { - ctx.JSON(http.StatusInternalServerError, gin.H{ - "code": http.StatusInternalServerError, - "msg": err.Error(), - }) - return - } - cache.C().DeleteByPrefix("submissions") - ctx.JSON(http.StatusOK, gin.H{ - "code": http.StatusOK, - }) -} diff --git a/internal/controller/team.go b/internal/controller/team.go deleted file mode 100644 index 146e3fa6..00000000 --- a/internal/controller/team.go +++ /dev/null @@ -1,395 +0,0 @@ -package controller - -import ( - "fmt" - "github.com/elabosak233/cloudsdale/internal/extension/cache" - "github.com/elabosak233/cloudsdale/internal/model" - "github.com/elabosak233/cloudsdale/internal/model/request" - "github.com/elabosak233/cloudsdale/internal/service" - "github.com/elabosak233/cloudsdale/internal/utils" - "github.com/elabosak233/cloudsdale/internal/utils/convertor" - "github.com/elabosak233/cloudsdale/internal/utils/validator" - "github.com/gin-gonic/gin" - "net/http" - "time" -) - -type ITeamController interface { - Create(ctx *gin.Context) - Update(ctx *gin.Context) - Delete(ctx *gin.Context) - Find(ctx *gin.Context) - CreateUser(ctx *gin.Context) - DeleteUser(ctx *gin.Context) - GetInviteToken(ctx *gin.Context) - UpdateInviteToken(ctx *gin.Context) - Join(ctx *gin.Context) - Leave(ctx *gin.Context) - SaveAvatar(ctx *gin.Context) - DeleteAvatar(ctx *gin.Context) -} - -type TeamController struct { - teamService service.ITeamService - userTeamService service.IUserTeamService - mediaService service.IMediaService -} - -func NewTeamController(s *service.Service) ITeamController { - return &TeamController{ - teamService: s.TeamService, - userTeamService: s.UserTeamService, - mediaService: s.MediaService, - } -} - -// Create -// @Summary 创建团队 -// @Description 创建团队 -// @Tags Team -// @Accept json -// @Produce json -// @Param input body request.TeamCreateRequest true "TeamCreateRequest" -// @Router /teams/ [post] -func (c *TeamController) Create(ctx *gin.Context) { - createTeamRequest := request.TeamCreateRequest{} - err := ctx.ShouldBindJSON(&createTeamRequest) - if err != nil { - ctx.JSON(http.StatusOK, gin.H{ - "code": http.StatusBadRequest, - "msg": validator.GetValidMsg(err, &createTeamRequest), - }) - return - } - err = c.teamService.Create(createTeamRequest) - if err != nil { - ctx.JSON(http.StatusOK, gin.H{ - "code": http.StatusBadRequest, - "msg": err.Error(), - }) - return - } - cache.C().DeleteByPrefix("teams") - ctx.JSON(http.StatusOK, gin.H{ - "code": http.StatusOK, - }) -} - -// Update -// @Summary 更新团队 -// @Description 更新团队 -// @Tags Team -// @Accept json -// @Produce json -// @Param input body request.TeamUpdateRequest true "TeamUpdateRequest" -// @Router /teams/{id} [put] -func (c *TeamController) Update(ctx *gin.Context) { - updateTeamRequest := request.TeamUpdateRequest{} - if err := ctx.ShouldBindJSON(&updateTeamRequest); err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{ - "code": http.StatusBadRequest, - "msg": validator.GetValidMsg(err, &updateTeamRequest), - }) - return - } - updateTeamRequest.ID = convertor.ToUintD(ctx.Param("id"), 0) - err := c.teamService.Update(updateTeamRequest) - if err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{ - "code": http.StatusBadRequest, - "msg": err.Error(), - }) - return - } - cache.C().DeleteByPrefix("teams") - ctx.JSON(http.StatusOK, gin.H{ - "code": http.StatusOK, - }) -} - -// Delete -// @Summary 删除团队 -// @Description 删除团队 -// @Tags Team -// @Accept json -// @Produce json -// @Param input body request.TeamDeleteRequest true "TeamDeleteRequest" -// @Router /teams/{id} [delete] -func (c *TeamController) Delete(ctx *gin.Context) { - deleteTeamRequest := request.TeamDeleteRequest{} - deleteTeamRequest.ID = convertor.ToUintD(ctx.Param("id"), 0) - err := c.teamService.Delete(deleteTeamRequest.ID) - if err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{ - "code": http.StatusBadRequest, - "msg": err.Error(), - }) - return - } - cache.C().DeleteByPrefix("teams") - ctx.JSON(http.StatusOK, gin.H{ - "code": http.StatusOK, - }) -} - -// Find -// @Summary 查找团队 -// @Description 查找团队 -// @Tags Team -// @Accept json -// @Produce json -// @Param input query request.TeamFindRequest false "TeamFindRequest" -// @Router /teams/ [get] -func (c *TeamController) Find(ctx *gin.Context) { - teamFindRequest := request.TeamFindRequest{} - err := ctx.ShouldBindQuery(&teamFindRequest) - if err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{ - "code": http.StatusBadRequest, - "msg": validator.GetValidMsg(err, &teamFindRequest), - }) - return - } - value, exist := cache.C().Get(fmt.Sprintf("teams:%s", utils.HashStruct(teamFindRequest))) - if !exist { - teams, total, _ := c.teamService.Find(teamFindRequest) - value = gin.H{ - "code": http.StatusOK, - "total": total, - "data": teams, - } - cache.C().Set( - fmt.Sprintf("teams:%s", utils.HashStruct(teamFindRequest)), - value, - 5*time.Minute, - ) - } - ctx.JSON(http.StatusOK, value) -} - -// CreateUser -// @Summary 加入团队 -// @Description 加入团队 -// @Tags Team -// @Accept json -// @Produce json -// @Param input body request.TeamUserCreateRequest true "TeamUserCreateRequest" -// @Router /teams/{id}/users/ [post] -func (c *TeamController) CreateUser(ctx *gin.Context) { - teamUserCreateRequest := request.TeamUserCreateRequest{ - TeamID: convertor.ToUintD(ctx.Param("id"), 0), - } - err := ctx.ShouldBindJSON(&teamUserCreateRequest) - if err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{ - "code": http.StatusBadRequest, - "msg": validator.GetValidMsg(err, &teamUserCreateRequest), - }) - return - } - err = c.userTeamService.Create(teamUserCreateRequest) - if err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{ - "code": http.StatusBadRequest, - "msg": err.Error(), - }) - return - } - cache.C().DeleteByPrefix("teams") - ctx.JSON(http.StatusOK, gin.H{ - "code": http.StatusOK, - }) -} - -// DeleteUser -// @Summary 踢出团队 -// @Description 踢出团队 -// @Tags Team -// @Accept json -// @Produce json -// @Param input body request.TeamUserDeleteRequest true "TeamUserDeleteRequest" -// @Router /teams/{id}/users/{user_id} [delete] -func (c *TeamController) DeleteUser(ctx *gin.Context) { - teamUserDeleteRequest := request.TeamUserDeleteRequest{ - TeamID: convertor.ToUintD(ctx.Param("id"), 0), - UserID: convertor.ToUintD(ctx.Param("user_id"), 0), - } - err := c.userTeamService.Delete(teamUserDeleteRequest) - if err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{ - "code": http.StatusBadRequest, - "msg": err.Error(), - }) - return - } - cache.C().DeleteByPrefix("teams") - ctx.JSON(http.StatusOK, gin.H{ - "code": http.StatusOK, - }) -} - -// GetInviteToken -// @Summary 获取邀请码 -// @Description 获取邀请码 -// @Tags Team -// @Accept json -// @Produce json -// @Param id path string true "id" -// @Router /teams/{id}/invite [get] -func (c *TeamController) GetInviteToken(ctx *gin.Context) { - id := ctx.Param("id") - token, err := c.teamService.GetInviteToken(request.TeamGetInviteTokenRequest{ - ID: convertor.ToUintD(id, 0), - }) - if err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{ - "code": http.StatusBadRequest, - "msg": err.Error(), - }) - return - } - ctx.JSON(http.StatusOK, gin.H{ - "code": http.StatusOK, - "invite_token": token, - }) -} - -// UpdateInviteToken -// @Summary 更新邀请码 -// @Description 更新邀请码 -// @Tags Team -// @Accept json -// @Produce json -// @Param id path string true "id" -// @Router /teams/{id}/invite [put] -func (c *TeamController) UpdateInviteToken(ctx *gin.Context) { - id := ctx.Param("id") - teamUpdateInviteTokenRequest := request.TeamUpdateInviteTokenRequest{} - teamUpdateInviteTokenRequest.ID = convertor.ToUintD(id, 0) - token, err := c.teamService.UpdateInviteToken(teamUpdateInviteTokenRequest) - if err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{ - "code": http.StatusBadRequest, - "msg": err.Error(), - }) - return - } - cache.C().DeleteByPrefix("teams") - ctx.JSON(http.StatusOK, gin.H{ - "code": http.StatusOK, - "invite_token": token, - }) -} - -// Join -// @Summary 加入团队 -// @Description 加入团队 -// @Tags Team -// @Accept json -// @Produce json -// @Param id path string true "id" -// @Router /teams/{id}/join [post] -func (c *TeamController) Join(ctx *gin.Context) { - id := ctx.Param("id") - user := ctx.MustGet("user").(*model.User) - err := c.userTeamService.Create(request.TeamUserCreateRequest{ - TeamID: convertor.ToUintD(id, 0), - UserID: user.ID, - }) - if err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{ - "code": http.StatusBadRequest, - "msg": err.Error(), - }) - return - } - cache.C().DeleteByPrefix("teams") - ctx.JSON(http.StatusOK, gin.H{ - "code": http.StatusOK, - }) -} - -// Leave -// @Summary 离开团队 -// @Description 离开团队 -// @Tags Team -// @Accept json -// @Produce json -// @Param id path string true "id" -// @Router /teams/{id}/leave [delete] -func (c *TeamController) Leave(ctx *gin.Context) { - id := ctx.Param("id") - user := ctx.MustGet("user").(*model.User) - err := c.userTeamService.Delete(request.TeamUserDeleteRequest{ - TeamID: convertor.ToUintD(id, 0), - UserID: user.ID, - }) - if err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{ - "code": http.StatusBadRequest, - "msg": err.Error(), - }) - return - } - ctx.JSON(http.StatusOK, gin.H{ - "code": http.StatusOK, - }) -} - -// SaveAvatar -// @Summary 保存头像 -// @Description -// @Tags Challenge -// @Accept json -// @Produce json -// @Security ApiKeyAuth -// @Param file formData file true "avatar" -// @Router /teams/{id}/avatar [post] -func (c *TeamController) SaveAvatar(ctx *gin.Context) { - id := convertor.ToUintD(ctx.Param("id"), 0) - fileHeader, err := ctx.FormFile("file") - fmt.Println("coming!!!!!!!!!!!!!!!!!!!!!") - if err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{ - "code": http.StatusBadRequest, - "msg": err.Error(), - }) - return - } - err = c.mediaService.SaveTeamAvatar(id, fileHeader) - if err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{ - "code": http.StatusBadRequest, - "msg": err.Error(), - }) - return - } - cache.C().DeleteByPrefix("teams") - ctx.JSON(http.StatusOK, gin.H{ - "code": http.StatusOK, - }) -} - -// DeleteAvatar -// @Summary 删除头像 -// @Description -// @Tags Challenge -// @Accept json -// @Produce json -// @Security ApiKeyAuth -// @Router /teams/{id}/avatar [delete] -func (c *TeamController) DeleteAvatar(ctx *gin.Context) { - id := convertor.ToUintD(ctx.Param("id"), 0) - err := c.mediaService.DeleteTeamAvatar(id) - if err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{ - "code": http.StatusBadRequest, - "msg": err.Error(), - }) - return - } - cache.C().DeleteByPrefix("teams") - ctx.JSON(http.StatusOK, gin.H{ - "code": http.StatusOK, - }) -} diff --git a/internal/controller/user.go b/internal/controller/user.go deleted file mode 100644 index 24c5a25a..00000000 --- a/internal/controller/user.go +++ /dev/null @@ -1,325 +0,0 @@ -package controller - -import ( - "fmt" - "github.com/elabosak233/cloudsdale/internal/extension/cache" - "github.com/elabosak233/cloudsdale/internal/model/request" - "github.com/elabosak233/cloudsdale/internal/service" - "github.com/elabosak233/cloudsdale/internal/utils" - "github.com/elabosak233/cloudsdale/internal/utils/convertor" - "github.com/elabosak233/cloudsdale/internal/utils/validator" - ginI18n "github.com/gin-contrib/i18n" - "github.com/gin-gonic/gin" - "go.uber.org/zap" - "net/http" - "time" -) - -type IUserController interface { - Login(ctx *gin.Context) - Logout(ctx *gin.Context) - Register(ctx *gin.Context) - Create(ctx *gin.Context) - Update(ctx *gin.Context) - Delete(ctx *gin.Context) - Find(ctx *gin.Context) - SaveAvatar(ctx *gin.Context) - DeleteAvatar(ctx *gin.Context) -} - -type UserController struct { - userService service.IUserService - mediaService service.IMediaService -} - -func NewUserController(s *service.Service) IUserController { - return &UserController{ - userService: s.UserService, - mediaService: s.MediaService, - } -} - -// Login -// @Summary 用户登录 -// @Description -// @Tags User -// @Accept json -// @Produce json -// @Param 登录请求 body request.UserLoginRequest true "UserLoginRequest" -// @Router /users/login [post] -func (c *UserController) Login(ctx *gin.Context) { - userLoginRequest := request.UserLoginRequest{} - if err := ctx.ShouldBindJSON(&userLoginRequest); err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{ - "code": http.StatusBadRequest, - "msg": validator.GetValidMsg(err, &userLoginRequest), - }) - return - } - user, token, err := c.userService.Login(userLoginRequest) - if err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{ - "code": http.StatusBadRequest, - "msg": ginI18n.MustGetMessage(ctx, err.Error()), - }) - return - } - err = c.userService.Update(request.UserUpdateRequest{ - ID: user.ID, - RemoteIP: ctx.RemoteIP(), - }) - if err != nil { - ctx.JSON(http.StatusInternalServerError, gin.H{ - "code": http.StatusInternalServerError, - "msg": err.Error(), - }) - return - } - ctx.JSON(http.StatusOK, gin.H{ - "code": http.StatusOK, - "data": user, - "token": token, - }) - zap.L().Info(fmt.Sprintf("User %s login successful", user.Username), zap.Uint("user_id", user.ID)) -} - -// Logout -// @Summary 用户登出 -// @Description -// @Tags User -// @Accept json -// @Produce json -// @Security ApiKeyAuth -// @Router /users/logout [post] -func (c *UserController) Logout(ctx *gin.Context) { - id, err := c.userService.Logout(ctx.GetHeader("Authorization")) - if err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{ - "code": http.StatusBadRequest, - "msg": err.Error(), - }) - return - } - ctx.JSON(http.StatusOK, gin.H{ - "code": http.StatusOK, - "id": id, - }) -} - -// Register -// @Summary 用户注册 -// @Description -// @Tags User -// @Accept json -// @Produce json -// @Param input body request.UserRegisterRequest true "UserRegisterRequest" -// @Router /users/register [post] -func (c *UserController) Register(ctx *gin.Context) { - registerUserRequest := request.UserRegisterRequest{} - if err := ctx.ShouldBindJSON(®isterUserRequest); err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{ - "code": http.StatusBadRequest, - "msg": validator.GetValidMsg(err, ®isterUserRequest), - }) - return - } - registerUserRequest.RemoteIP = ctx.RemoteIP() - if err := c.userService.Register(registerUserRequest); err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{ - "code": http.StatusBadRequest, - "msg": "用户名或邮箱重复", - }) - return - } - cache.C().DeleteByPrefix("users") - ctx.JSON(http.StatusOK, gin.H{ - "code": http.StatusOK, - }) -} - -// Create -// @Summary 用户创建 -// @Description -// @Tags User -// @Accept json -// @Produce json -// @Security ApiKeyAuth -// @Param 创建请求 body request.UserCreateRequest true "UserCreateRequest" -// @Router /users/ [post] -func (c *UserController) Create(ctx *gin.Context) { - createUserRequest := request.UserCreateRequest{} - if err := ctx.ShouldBindJSON(&createUserRequest); err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{ - "code": http.StatusBadRequest, - "msg": validator.GetValidMsg(err, &createUserRequest), - }) - return - } - if err := c.userService.Create(createUserRequest); err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{ - "code": http.StatusBadRequest, - "msg": "用户名或邮箱重复", - }) - return - } - cache.C().DeleteByPrefix("users") - ctx.JSON(http.StatusOK, gin.H{ - "code": http.StatusOK, - }) -} - -// Update -// @Summary 用户更新 -// @Description -// @Tags User -// @Accept json -// @Produce json -// @Security ApiKeyAuth -// @Param 更新请求 body request.UserUpdateRequest true "UserUpdateRequest" -// @Router /users/{id} [put] -func (c *UserController) Update(ctx *gin.Context) { - updateUserRequest := request.UserUpdateRequest{} - if err := ctx.ShouldBindJSON(&updateUserRequest); err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{ - "code": http.StatusBadRequest, - "msg": validator.GetValidMsg(err, &updateUserRequest), - }) - return - } - updateUserRequest.ID = convertor.ToUintD(ctx.Param("id"), 0) - if err := c.userService.Update(updateUserRequest); err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{ - "code": http.StatusBadRequest, - "msg": err.Error(), - }) - return - } - cache.C().DeleteByPrefix("users") - ctx.JSON(http.StatusOK, gin.H{ - "code": http.StatusOK, - }) -} - -// Delete -// @Summary 用户删除 -// @Description -// @Tags User -// @Accept json -// @Produce json -// @Security ApiKeyAuth -// @Param input body request.UserDeleteRequest true "UserDeleteRequest" -// @Router /users/{id} [delete] -func (c *UserController) Delete(ctx *gin.Context) { - deleteUserRequest := request.UserDeleteRequest{} - deleteUserRequest.ID = convertor.ToUintD(ctx.Param("id"), 0) - err := c.userService.Delete(deleteUserRequest.ID) - if err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{ - "code": http.StatusBadRequest, - "msg": err.Error(), - }) - return - } - cache.C().DeleteByPrefix("users") - ctx.JSON(http.StatusOK, gin.H{ - "code": http.StatusOK, - }) -} - -// Find -// @Summary 用户查询 -// @Description -// @Tags User -// @Accept json -// @Produce json -// @Param input query request.UserFindRequest false "UserFindRequest" -// @Router /users/ [get] -func (c *UserController) Find(ctx *gin.Context) { - userFindRequest := request.UserFindRequest{} - if err := ctx.ShouldBindQuery(&userFindRequest); err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{ - "code": http.StatusBadRequest, - "msg": validator.GetValidMsg(err, &userFindRequest), - }) - return - } - value, exist := cache.C().Get(fmt.Sprintf("users:%s", utils.HashStruct(userFindRequest))) - if !exist { - users, total, err := c.userService.Find(userFindRequest) - if err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{ - "code": http.StatusBadRequest, - "msg": err.Error(), - }) - return - } - value = gin.H{ - "code": http.StatusOK, - "data": users, - "total": total, - } - cache.C().Set( - fmt.Sprintf("users:%s", utils.HashStruct(userFindRequest)), - value, - 5*time.Minute, - ) - } - ctx.JSON(http.StatusOK, value) -} - -// SaveAvatar -// @Summary 保存头像 -// @Description -// @Tags Challenge -// @Accept json -// @Produce json -// @Security ApiKeyAuth -// @Param file formData file true "avatar" -// @Router /users/{id}/avatar [post] -func (c *UserController) SaveAvatar(ctx *gin.Context) { - id := convertor.ToUintD(ctx.Param("id"), 0) - fileHeader, err := ctx.FormFile("file") - if err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{ - "code": http.StatusBadRequest, - "msg": err.Error(), - }) - return - } - err = c.mediaService.SaveUserAvatar(id, fileHeader) - if err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{ - "code": http.StatusBadRequest, - "msg": err.Error(), - }) - return - } - cache.C().DeleteByPrefix("users") - ctx.JSON(http.StatusOK, gin.H{ - "code": http.StatusOK, - }) -} - -// DeleteAvatar -// @Summary 删除头像 -// @Description -// @Tags Challenge -// @Accept json -// @Produce json -// @Security ApiKeyAuth -// @Router /users/{id}/avatar [delete] -func (c *UserController) DeleteAvatar(ctx *gin.Context) { - id := convertor.ToUintD(ctx.Param("id"), 0) - err := c.mediaService.DeleteUserAvatar(id) - if err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{ - "code": http.StatusBadRequest, - "msg": err.Error(), - }) - return - } - cache.C().DeleteByPrefix("users") - ctx.JSON(http.StatusOK, gin.H{ - "code": http.StatusOK, - }) -} diff --git a/internal/extension/broadcast/broadcast.go b/internal/extension/broadcast/broadcast.go deleted file mode 100644 index 9dd7e1f2..00000000 --- a/internal/extension/broadcast/broadcast.go +++ /dev/null @@ -1,12 +0,0 @@ -package broadcast - -import ( - "github.com/gorilla/websocket" - "net/http" -) - -var Upgrader = websocket.Upgrader{ - CheckOrigin: func(r *http.Request) bool { - return true - }, -} diff --git a/internal/extension/broadcast/game.go b/internal/extension/broadcast/game.go deleted file mode 100644 index 20dfeb13..00000000 --- a/internal/extension/broadcast/game.go +++ /dev/null @@ -1,82 +0,0 @@ -package broadcast - -import ( - "github.com/gorilla/websocket" - "go.uber.org/zap" - "net/http" - "sync" -) - -type GameBroadcast struct { - Upgrader websocket.Upgrader - Clients map[*websocket.Conn]bool - BroadCast chan interface{} - M sync.RWMutex -} - -var gameBroadcasts = make(map[uint]*GameBroadcast) -var gameBroadcastLock sync.Mutex - -func ServeGameHub(w http.ResponseWriter, r *http.Request, gameID uint) { - gameBroadcastLock.Lock() - gameBroadcast, ok := gameBroadcasts[gameID] - if !ok { - g := &GameBroadcast{ - Clients: make(map[*websocket.Conn]bool), - BroadCast: make(chan interface{}), - M: sync.RWMutex{}, - } - gameBroadcasts[gameID] = g - gameBroadcast = g - go handleGameBroadcast(g) - } - gameBroadcastLock.Unlock() - conn, _ := Upgrader.Upgrade(w, r, nil) - - defer func() { - gameBroadcast.M.Lock() - if conn != nil { - if _, exists := gameBroadcast.Clients[conn]; exists { - delete(gameBroadcast.Clients, conn) - } - } - gameBroadcast.M.Unlock() - _ = conn.Close() - }() - gameBroadcast.M.Lock() - gameBroadcast.Clients[conn] = true - gameBroadcast.M.Unlock() - for { - _, _, err := conn.ReadMessage() - if err != nil { - break - } - } -} - -func handleGameBroadcast(gameBroadcast *GameBroadcast) { - for { - msg := <-gameBroadcast.BroadCast - gameBroadcast.M.RLock() - for client := range gameBroadcast.Clients { - err := client.WriteJSON(msg) - if err != nil { - zap.L().Error("", zap.Error(err)) - _ = client.Close() - delete(gameBroadcast.Clients, client) - gameBroadcast.M.RUnlock() - break - } - } - gameBroadcast.M.RUnlock() - } -} - -func SendGameMsg(gameID uint, msg interface{}) { - gameBroadcastLock.Lock() - gameHub, ok := gameBroadcasts[gameID] - gameBroadcastLock.Unlock() - if ok { - gameHub.BroadCast <- msg - } -} diff --git a/internal/extension/cache/cache.go b/internal/extension/cache/cache.go deleted file mode 100644 index 2cb08355..00000000 --- a/internal/extension/cache/cache.go +++ /dev/null @@ -1,34 +0,0 @@ -package cache - -import ( - "github.com/elabosak233/cloudsdale/internal/app/config" - "sync" - "time" -) - -var ( - cache ICache = nil - onceCache sync.Once -) - -type ICache interface { - Get(key string) (interface{}, bool) - Set(key string, value interface{}, expiration time.Duration) - Delete(key string) - DeleteByPrefix(prefix string) -} - -func C() ICache { - return cache -} - -func InitCache() { - onceCache.Do(func() { - switch config.AppCfg().Gin.Cache.Provider { - case "memory": - cache = NewMemoryCache() - case "redis": - cache = NewRedisCache() - } - }) -} diff --git a/internal/extension/cache/memory.go b/internal/extension/cache/memory.go deleted file mode 100644 index ea5ad2fc..00000000 --- a/internal/extension/cache/memory.go +++ /dev/null @@ -1,44 +0,0 @@ -package cache - -import ( - goCache "github.com/patrickmn/go-cache" - "go.uber.org/zap" - "strings" - "time" -) - -type MemoryCache struct { - gc *goCache.Cache -} - -func NewMemoryCache() ICache { - gc := goCache.New(5*time.Minute, 10*time.Minute) - zap.L().Info("Cache module inits successfully. Using memory as cache provider.") - return &MemoryCache{ - gc: gc, - } -} -func (c *MemoryCache) Get(key string) (interface{}, bool) { - value, exist := c.gc.Get(key) - if exist { - zap.L().Info("Cache hit", zap.String("key", key)) - } - return value, exist -} - -func (c *MemoryCache) Set(key string, value interface{}, expiration time.Duration) { - zap.L().Info("Cache set", zap.String("key", key)) - c.gc.Set(key, value, expiration) -} - -func (c *MemoryCache) Delete(key string) { - c.gc.Delete(key) -} - -func (c *MemoryCache) DeleteByPrefix(prefix string) { - for k := range c.gc.Items() { - if strings.HasPrefix(k, prefix) { - c.gc.Delete(k) - } - } -} diff --git a/internal/extension/cache/redis.go b/internal/extension/cache/redis.go deleted file mode 100644 index c9595967..00000000 --- a/internal/extension/cache/redis.go +++ /dev/null @@ -1,97 +0,0 @@ -package cache - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "github.com/elabosak233/cloudsdale/internal/app/config" - "github.com/gin-gonic/gin" - "github.com/redis/go-redis/v9" - "go.uber.org/zap" - "time" -) - -type RedisCache struct { - ctx context.Context - rdb *redis.Client -} - -func NewRedisCache() ICache { - rdb := redis.NewClient(&redis.Options{ - Addr: fmt.Sprintf("%s:%d", config.AppCfg().Gin.Cache.Redis.Host, config.AppCfg().Gin.Cache.Redis.Port), - Password: config.AppCfg().Gin.Cache.Redis.Password, - DB: config.AppCfg().Gin.Cache.Redis.DB, - }) - zap.L().Info("Cache module inits successfully. Using Redis as cache provider.") - return &RedisCache{ - ctx: context.Background(), - rdb: rdb, - } -} - -func (r *RedisCache) Set(key string, value interface{}, expiration time.Duration) { - h, ok := value.(gin.H) - if !ok { - zap.L().Error("Value is not of type gin.H", zap.Any("value", value)) - return - } - - jsonData, err := json.Marshal(h) - if err != nil { - zap.L().Error("Error marshalling gin.H to JSON", zap.Error(err)) - return - } - - if err := r.rdb.Set(r.ctx, key, jsonData, expiration).Err(); err != nil { - zap.L().Error("Error setting cache", zap.String("key", key), zap.Error(err)) - } - zap.L().Info("Cache set", zap.String("key", key)) -} - -func (r *RedisCache) Get(key string) (interface{}, bool) { - jsonData, err := r.rdb.Get(r.ctx, key).Result() - if errors.Is(err, redis.Nil) { - zap.L().Info("Cache miss", zap.String("key", key)) - return nil, false - } else if err != nil { - zap.L().Error("Error retrieving from cache", zap.String("key", key), zap.Error(err)) - return nil, false - } - - var h gin.H - if err := json.Unmarshal([]byte(jsonData), &h); err != nil { - zap.L().Error("Error unmarshalling JSON to gin.H", zap.Error(err)) - return nil, false - } - - zap.L().Info("Cache hit", zap.String("key", key)) - return h, true -} - -func (r *RedisCache) Delete(key string) { - if err := r.rdb.Del(r.ctx, key).Err(); err != nil { - zap.L().Error("Error deleting from cache", zap.String("key", key), zap.Error(err)) - } -} - -func (r *RedisCache) DeleteByPrefix(prefix string) { - var cursor uint64 - var keys []string - var err error - for { - keys, cursor, err = r.rdb.Scan(r.ctx, cursor, fmt.Sprintf("%s*", prefix), 10).Result() - if err != nil { - zap.L().Error("Error scanning cache for prefix", zap.String("prefix", prefix), zap.Error(err)) - break - } - if len(keys) > 0 { - if err := r.rdb.Del(r.ctx, keys...).Err(); err != nil { - zap.L().Error("Error deleting keys by prefix", zap.String("prefix", prefix), zap.Error(err)) - } - } - if cursor == 0 { - break - } - } -} diff --git a/internal/extension/captcha/captcha.go b/internal/extension/captcha/captcha.go deleted file mode 100644 index ffc1ca7d..00000000 --- a/internal/extension/captcha/captcha.go +++ /dev/null @@ -1,19 +0,0 @@ -package captcha - -import ( - "github.com/elabosak233/cloudsdale/internal/app/config" -) - -type ICaptcha interface { - Verify(token string, clientIP string) (success bool, err error) -} - -func NewCaptcha() ICaptcha { - switch config.AppCfg().Captcha.Provider { - case "recaptcha": - return NewGoogleRecaptcha() - case "turnstile": - return NewCloudflareTurnstile() - } - return nil -} diff --git a/internal/extension/captcha/recaptcha.go b/internal/extension/captcha/recaptcha.go deleted file mode 100644 index 2e683796..00000000 --- a/internal/extension/captcha/recaptcha.go +++ /dev/null @@ -1,53 +0,0 @@ -package captcha - -import ( - "bytes" - "encoding/json" - "github.com/elabosak233/cloudsdale/internal/app/config" - "io" - "net/http" -) - -type GoogleRecaptcha struct { - URL string - SiteKey string - SecretKey string - Threshold float64 -} - -func NewGoogleRecaptcha() ICaptcha { - return &GoogleRecaptcha{ - URL: config.AppCfg().Captcha.ReCaptcha.URL, - SiteKey: config.AppCfg().Captcha.ReCaptcha.SiteKey, - SecretKey: config.AppCfg().Captcha.ReCaptcha.SecretKey, - Threshold: config.AppCfg().Captcha.ReCaptcha.Threshold, - } -} - -func (g *GoogleRecaptcha) Verify(token string, clientIP string) (success bool, err error) { - type RecaptchaRequest struct { - Secret string `json:"secret"` - Response string `json:"response"` - RemoteIP string `json:"remoteip"` - } - requestBody, err := json.Marshal( - RecaptchaRequest{ - Secret: g.SecretKey, - Response: token, - RemoteIP: clientIP, - }, - ) - result, err := http.Post(g.URL, "application/json", bytes.NewBuffer(requestBody)) - defer func() { - _ = result.Body.Close() - }() - body, err := io.ReadAll(result.Body) - var response map[string]interface{} - err = json.Unmarshal(body, &response) - success, ok := response["success"].(bool) - score, ok := response["score"].(float64) - if ok && success && score >= g.Threshold { - return true, err - } - return false, err -} diff --git a/internal/extension/captcha/turnstile.go b/internal/extension/captcha/turnstile.go deleted file mode 100644 index bac4370c..00000000 --- a/internal/extension/captcha/turnstile.go +++ /dev/null @@ -1,50 +0,0 @@ -package captcha - -import ( - "bytes" - "github.com/elabosak233/cloudsdale/internal/app/config" - "io" - "k8s.io/apimachinery/pkg/util/json" - "net/http" -) - -type CloudflareTurnstile struct { - URL string - SiteKey string - SecretKey string -} - -func NewCloudflareTurnstile() ICaptcha { - return &CloudflareTurnstile{ - URL: config.AppCfg().Captcha.Turnstile.URL, - SiteKey: config.AppCfg().Captcha.Turnstile.SiteKey, - SecretKey: config.AppCfg().Captcha.Turnstile.SecretKey, - } -} - -func (c *CloudflareTurnstile) Verify(token string, clientIP string) (success bool, err error) { - type TurnstileRequest struct { - SecretKey string `json:"secret"` - Response string `json:"response"` - RemoteIP string `json:"remoteip"` - } - requestBody, err := json.Marshal( - TurnstileRequest{ - SecretKey: c.SecretKey, - Response: token, - RemoteIP: clientIP, - }, - ) - result, err := http.Post(c.URL, "application/json", bytes.NewBuffer(requestBody)) - defer func() { - _ = result.Body.Close() - }() - body, err := io.ReadAll(result.Body) - var response map[string]interface{} - err = json.Unmarshal(body, &response) - success, ok := response["success"].(bool) - if ok && success { - return true, err - } - return false, err -} diff --git a/internal/extension/casbin/casbin.go b/internal/extension/casbin/casbin.go deleted file mode 100644 index 1b4bbba4..00000000 --- a/internal/extension/casbin/casbin.go +++ /dev/null @@ -1,32 +0,0 @@ -package casbin - -import ( - "github.com/casbin/casbin/v2" - "github.com/casbin/casbin/v2/model" - gormadapter "github.com/casbin/gorm-adapter/v3" - "github.com/elabosak233/cloudsdale/internal/app/db" - "github.com/elabosak233/cloudsdale/internal/files" - "go.uber.org/zap" -) - -var ( - Enforcer *casbin.Enforcer -) - -func InitCasbin() { - adapter, err := gormadapter.NewAdapterByDBWithCustomTable( - db.Db(), - &gormadapter.CasbinRule{}, - "casbins", - ) - cfg, err := files.F().ReadFile("configs/casbin.conf") - md, _ := model.NewModelFromString(string(cfg)) - Enforcer, err = casbin.NewEnforcer(md, adapter) - if err != nil { - zap.L().Fatal("Casbin module inits failed.", zap.Error(err)) - } - Enforcer.ClearPolicy() - _ = Enforcer.SavePolicy() - initDefaultPolicy() - zap.L().Info("Casbin module inits successfully.") -} diff --git a/internal/extension/casbin/policy.go b/internal/extension/casbin/policy.go deleted file mode 100644 index c44bdcba..00000000 --- a/internal/extension/casbin/policy.go +++ /dev/null @@ -1,68 +0,0 @@ -package casbin - -import "go.uber.org/zap" - -func initDefaultPolicy() { - _, err := Enforcer.AddPolicies([][]string{ - {"admin", "/api/*", "GET"}, - {"admin", "/api/*", "POST"}, - {"admin", "/api/*", "PUT"}, - {"admin", "/api/*", "DELETE"}, - - {"user", "/api/", "GET"}, - {"user", "/api/users/logout", "POST"}, - {"user", "/api/users/{id}", "PUT"}, - {"user", "/api/users/{id}", "DELETE"}, - {"user", "/api/users/{id}/avatar", "POST"}, - {"user", "/api/users/{id}/avatar", "DELETE"}, - {"user", "/api/teams/", "GET"}, - {"user", "/api/teams/", "POST"}, - {"user", "/api/teams/{id}", "PUT"}, - {"user", "/api/teams/{id}", "GET"}, - {"user", "/api/teams/{id}", "DELETE"}, - {"user", "/api/teams/{id}/avatar", "POST"}, - {"user", "/api/teams/{id}/avatar", "DELETE"}, - {"user", "/api/teams/{id}/invite", "PUT"}, - {"user", "/api/teams/{id}/invite", "GET"}, - {"user", "/api/teams/{id}/users/{user_id}", "DELETE"}, - {"user", "/api/teams/{id}/join", "POST"}, - {"user", "/api/teams/{id}/leave", "DELETE"}, - {"user", "/api/challenges/*", "GET"}, - {"user", "/api/games/", "GET"}, - {"user", "/api/games/{id}", "GET"}, - {"user", "/api/games/{id}/scoreboard", "GET"}, - {"user", "/api/games/{id}/challenges", "GET"}, - {"user", "/api/games/{id}/teams", "GET"}, - {"user", "/api/games/{id}/teams", "POST"}, - {"user", "/api/games/{id}/teams/{team_id}", "GET"}, - {"user", "/api/games/{id}/notices", "GET"}, - {"user", "/api/submissions/", "GET"}, - {"user", "/api/submissions/", "POST"}, - {"user", "/api/pods/", "GET"}, - {"user", "/api/pods/", "POST"}, - {"user", "/api/pods/{id}", "GET"}, - {"user", "/api/pods/{id}", "PUT"}, - {"user", "/api/pods/{id}", "DELETE"}, - - {"guest", "/api/", "GET"}, - {"guest", "/api/configs/*", "GET"}, - {"guest", "/api/categories/", "GET"}, - {"guest", "/api/users/", "GET"}, - {"guest", "/api/users/register", "POST"}, - {"guest", "/api/users/login", "POST"}, - {"guest", "/api/games/{id}/broadcast", "GET"}, - {"guest", "/api/proxies/{id}", "GET"}, - {"guest", "/api/media/*", "GET"}, - {"guest", "/api/groups/", "GET"}, - {"guest", "/api/*", "OPTIONS"}, - }) - - _, err = Enforcer.AddGroupingPolicies([][]string{ - {"user", "guest"}, - {"admin", "user"}, - }) - - if err != nil { - zap.L().Fatal("Casbin init default policy error.", zap.Error(err)) - } -} diff --git a/internal/extension/container/manager/docker.go b/internal/extension/container/manager/docker.go deleted file mode 100644 index 752c9a56..00000000 --- a/internal/extension/container/manager/docker.go +++ /dev/null @@ -1,232 +0,0 @@ -package manager - -import ( - "context" - "fmt" - "github.com/TwiN/go-color" - ctn "github.com/docker/docker/api/types/container" - "github.com/docker/docker/client" - "github.com/docker/go-connections/nat" - "github.com/elabosak233/cloudsdale/internal/app/config" - "github.com/elabosak233/cloudsdale/internal/extension/container/provider" - "github.com/elabosak233/cloudsdale/internal/extension/proxy" - "github.com/elabosak233/cloudsdale/internal/model" - "github.com/elabosak233/cloudsdale/internal/utils/convertor" - "go.uber.org/zap" - "strconv" - "strings" - "time" -) - -type DockerManager struct { - challenge model.Challenge - flag model.Flag - duration time.Duration - - PodID uint - RespID string - Proxies []proxy.IProxy - Nats []*model.Nat - CancelCtx context.Context - CancelFunc context.CancelFunc -} - -func NewDockerManager(challenge model.Challenge, flag model.Flag, duration time.Duration) IContainerManager { - return &DockerManager{ - challenge: challenge, - duration: duration, - flag: flag, - Proxies: make([]proxy.IProxy, 0), - } -} - -func (c *DockerManager) SetPodID(podID uint) { - c.PodID = podID -} - -func (c *DockerManager) Duration() (duration time.Duration) { - return c.duration -} - -func (c *DockerManager) Setup() (nats []*model.Nat, err error) { - - c.CancelCtx, c.CancelFunc = context.WithCancel(context.Background()) - - envs := make([]string, 0) - for _, env := range c.challenge.Envs { - envs = append(envs, fmt.Sprintf("%s=%s", env.Key, env.Value)) - } - envs = append(envs, fmt.Sprintf("%s=%s", c.flag.Env, c.flag.Value)) - - containerConfig := &ctn.Config{ - Image: c.challenge.ImageName, - Env: envs, - } - - portBindings := make(nat.PortMap) - for _, port := range c.challenge.Ports { - portStr := strconv.Itoa(port.Value) + "/tcp" - portBindings[nat.Port(portStr)] = []nat.PortBinding{ - { - HostIP: "0.0.0.0", - HostPort: "", // Let docker decide the port. - }, - } - } - - hostConfig := &ctn.HostConfig{ - PortBindings: portBindings, - Resources: ctn.Resources{ - Memory: c.challenge.MemoryLimit * 1024 * 1024, - NanoCPUs: c.challenge.CPULimit * 1e9, - }, - } - - resp, _err := provider.DockerCli().ContainerCreate( - context.Background(), - containerConfig, - hostConfig, - nil, - nil, - "", - ) - - if _err != nil { - zap.L().Error(fmt.Sprintf("[%s] Failed to create: %s", color.InCyan("DOCKER"), _err.Error())) - return nil, _err - } - - c.RespID = resp.ID - - // Handle the container - _err = provider.DockerCli().ContainerStart( - context.Background(), - c.RespID, - ctn.StartOptions{}, - ) - - if _err != nil { - zap.L().Error(fmt.Sprintf("[%s] Failed to start: %s", color.InCyan("DOCKER"), _err.Error())) - return nil, _err - } - - // Get the container's inspect information - inspect, _ := provider.DockerCli().ContainerInspect( - context.Background(), - c.RespID, - ) - - nats = make([]*model.Nat, 0) - - switch config.AppCfg().Container.Proxy.Enabled { - case true: - for port, bindings := range inspect.NetworkSettings.Ports { - entries := make([]string, 0) - for _, binding := range bindings { - entry := fmt.Sprintf( - "%s:%d", - config.AppCfg().Container.Entry, - convertor.ToIntD(binding.HostPort, 0), - ) - entries = append(entries, entry) - c.Proxies = append(c.Proxies, proxy.NewProxy(entry)) - } - for index, p := range c.Proxies { - p.Setup() - nats = append(nats, &model.Nat{ - SrcPort: port.Int(), - DstPort: convertor.ToIntD(strings.Split(entries[index], ":")[1], 0), - Proxy: entries[index], - Entry: p.Entry(), - }) - } - } - case false: - for port, bindings := range inspect.NetworkSettings.Ports { - for _, binding := range bindings { - nats = append(nats, &model.Nat{ - SrcPort: port.Int(), - DstPort: convertor.ToIntD(binding.HostPort, 0), - Entry: fmt.Sprintf( - "%s:%d", - config.AppCfg().Container.Entry, - convertor.ToIntD(binding.HostPort, 0), - ), - }) - } - } - } - - c.Nats = nats - - return nats, err -} - -func (c *DockerManager) Status() (status string, err error) { - status = "removed" - resp, err := provider.DockerCli().ContainerInspect(context.Background(), c.RespID) - if err == nil { - status = resp.State.Status - } - return status, err -} - -func (c *DockerManager) RemoveAfterDuration() (success bool) { - select { - case <-time.After(c.duration): - c.Remove() - return true - case <-c.CancelCtx.Done(): - zap.L().Warn(fmt.Sprintf("[%s] Pod %d (RespID %s)'s removal plan has been cancelled.", color.InCyan("DOCKER"), c.PodID, c.RespID)) - return false - } -} - -func (c *DockerManager) Remove() { - go func(respID string) { - // Check if the container is running before stopping it - info, err := provider.DockerCli().ContainerInspect(context.Background(), respID) - if err != nil { - return - } - - if info.State.Running { - _ = provider.DockerCli().ContainerStop(context.Background(), respID, ctn.StopOptions{}) // Stop the container - _, _ = provider.DockerCli().ContainerWait(context.Background(), respID, ctn.WaitConditionNotRunning) // Wait for the container to stop - } - - // Check if the container still exists before removing it - _, err = provider.DockerCli().ContainerInspect(context.Background(), respID) - if err != nil && client.IsErrNotFound(err) { - return // Container not found, it has been removed - } - _ = provider.DockerCli().ContainerRemove( - context.Background(), - respID, - ctn.RemoveOptions{}, - ) // Remove the container - }(c.RespID) - - // Close the proxies if they exist - if len(c.Proxies) > 0 { - for _, p := range c.Proxies { - p.Close() - } - } -} - -func (c *DockerManager) Renew(duration time.Duration) { - if c.CancelFunc != nil { - c.CancelFunc() // Calling the cancel function - } - c.duration = duration - c.CancelCtx, c.CancelFunc = context.WithCancel(context.Background()) - go c.RemoveAfterDuration() - zap.L().Warn( - fmt.Sprintf("[%s] Pod %d (RespID %s) successfully renewed.", - color.InCyan("DOCKER"), - c.PodID, - c.RespID, - ), - ) -} diff --git a/internal/extension/container/manager/k8s.go b/internal/extension/container/manager/k8s.go deleted file mode 100644 index 514878a8..00000000 --- a/internal/extension/container/manager/k8s.go +++ /dev/null @@ -1,233 +0,0 @@ -package manager - -import ( - "context" - "errors" - "fmt" - "github.com/TwiN/go-color" - "github.com/elabosak233/cloudsdale/internal/app/config" - "github.com/elabosak233/cloudsdale/internal/extension/container/provider" - "github.com/elabosak233/cloudsdale/internal/extension/proxy" - "github.com/elabosak233/cloudsdale/internal/model" - "github.com/elabosak233/cloudsdale/internal/utils" - "go.uber.org/zap" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/resource" - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/util/intstr" - "time" -) - -var ( - namespace string -) - -type K8sManager struct { - challenge model.Challenge - flag model.Flag - duration time.Duration - - PodID uint - RespID string - Nats []*model.Nat - Proxies []proxy.IProxy - Inspect corev1.Pod - CancelCtx context.Context - CancelFunc context.CancelFunc -} - -func NewK8sManager(challenge model.Challenge, flag model.Flag, duration time.Duration) IContainerManager { - namespace = config.AppCfg().Container.K8s.NameSpace - return &K8sManager{ - challenge: challenge, - duration: duration, - flag: flag, - Proxies: make([]proxy.IProxy, 0), - } -} - -func (c *K8sManager) Setup() (nats []*model.Nat, err error) { - var containers []corev1.Container - var ports []corev1.ContainerPort - for _, port := range c.challenge.Ports { - // Don't set HostPort because it should be decided by Kubernetes - ports = append(ports, corev1.ContainerPort{ - ContainerPort: int32(port.Value), - }) - } - - var envs []corev1.EnvVar - for _, env := range c.challenge.Envs { - envs = append(envs, corev1.EnvVar{Name: env.Key, Value: env.Value}) - } - // Add the flag information to the environment variables - envs = append(envs, corev1.EnvVar{Name: c.flag.Env, Value: c.flag.Value}) - uid := utils.HyphenlessUUID() - containers = append(containers, corev1.Container{ - Name: uid, - Image: c.challenge.ImageName, - Env: envs, - Ports: ports, - Resources: corev1.ResourceRequirements{ - Limits: corev1.ResourceList{ - corev1.ResourceCPU: resource.MustParse(fmt.Sprintf("%d", c.challenge.CPULimit)), - corev1.ResourceMemory: resource.MustParse(fmt.Sprintf("%dMi", c.challenge.MemoryLimit)), - }, - }, - }) - - pod := &corev1.Pod{ - ObjectMeta: v1.ObjectMeta{ - GenerateName: fmt.Sprintf("cloudsdale-%s", uid), - Labels: map[string]string{ - "app": "cloudsdale", - }, - }, - Spec: corev1.PodSpec{ - Containers: containers, - }, - } - - pod, err = provider.K8sCli().CoreV1().Pods(namespace).Create(context.Background(), pod, v1.CreateOptions{}) - if err != nil { - zap.L().Error(fmt.Sprintf("[%s] Unable to create pod.", color.InCyan("K8S")), zap.Error(err)) - return nil, err - } - c.RespID = pod.Name - c.Inspect = *pod - c.CancelCtx, c.CancelFunc = context.WithCancel(context.Background()) - - // Create a NodePort service to expose the pod - service := &corev1.Service{ - ObjectMeta: v1.ObjectMeta{ - Name: fmt.Sprintf("cloudsdale-%s", uid), - Namespace: namespace, - Labels: map[string]string{ - "app": "cloudsdale", - }, - }, - Spec: corev1.ServiceSpec{ - Type: corev1.ServiceTypeNodePort, - Selector: map[string]string{ - "app": "cloudsdale", - }, - Ports: []corev1.ServicePort{}, - }, - } - - for _, port := range c.challenge.Ports { - servicePort := corev1.ServicePort{ - Port: int32(port.Value), - TargetPort: intstr.FromInt32(int32(port.Value)), - } - service.Spec.Ports = append(service.Spec.Ports, servicePort) - } - - service, err = provider.K8sCli().CoreV1().Services(namespace).Create(context.Background(), service, v1.CreateOptions{}) - if err != nil { - zap.L().Error(fmt.Sprintf("[%s] Unable to create service.", color.InCyan("K8S")), zap.Error(err)) - return nil, err - } - - // Get the created service's information - createdService, err := provider.K8sCli().CoreV1().Services(namespace).Get(context.Background(), service.Name, v1.GetOptions{}) - if err != nil { - return nil, err - } - - switch config.AppCfg().Container.Proxy.Enabled { - case true: - srcPorts := make([]int, 0) - dstPorts := make([]int, 0) - entries := make([]string, 0) - for _, servicePort := range createdService.Spec.Ports { - entry := fmt.Sprintf( - "%s:%d", - config.AppCfg().Container.Entry, - int(servicePort.NodePort), - ) - srcPorts = append(srcPorts, int(servicePort.Port)) - dstPorts = append(dstPorts, int(servicePort.NodePort)) - entries = append(entries, entry) - c.Proxies = append(c.Proxies, proxy.NewProxy(entry)) - } - for index, p := range c.Proxies { - p.Setup() - nats = append(nats, &model.Nat{ - SrcPort: srcPorts[index], - DstPort: dstPorts[index], - Proxy: entries[index], - Entry: p.Entry(), - }) - } - case false: - for _, servicePort := range createdService.Spec.Ports { - nat := &model.Nat{ - SrcPort: int(servicePort.Port), - DstPort: int(servicePort.NodePort), - Entry: fmt.Sprintf( - "%s:%d", - config.AppCfg().Container.Entry, - servicePort.NodePort, - ), - } - nats = append(nats, nat) - } - } - - c.Nats = nats - - return nats, nil -} - -func (c *K8sManager) SetPodID(podID uint) { - c.PodID = podID -} - -func (c *K8sManager) Duration() (duration time.Duration) { - return c.duration -} - -func (c *K8sManager) Status() (status string, err error) { - if c.RespID == "" { - return "", errors.New("pod not created or initialization failed") - } - pod, err := provider.K8sCli().CoreV1().Pods(namespace).Get(context.Background(), c.RespID, v1.GetOptions{}) - if err != nil { - return "removed", err - } - return string(pod.Status.Phase), err -} - -func (c *K8sManager) RemoveAfterDuration() (success bool) { - select { - case <-time.After(c.duration): - c.Remove() - return true - case <-c.CancelCtx.Done(): - zap.L().Warn(fmt.Sprintf("[%s] Pod %d (RespID %s)'s removal plan has been cancelled.", color.InCyan("K8S"), c.PodID, c.RespID)) - return false - } -} - -func (c *K8sManager) Remove() { - go func() { - _ = provider.K8sCli().CoreV1().Pods(namespace).Delete(context.Background(), c.RespID, v1.DeleteOptions{}) - }() -} - -func (c *K8sManager) Renew(duration time.Duration) { - if c.CancelFunc != nil { - c.CancelFunc() // Calling the cancel function - } - c.duration = duration - c.CancelCtx, c.CancelFunc = context.WithCancel(context.Background()) - go c.RemoveAfterDuration() - zap.L().Warn( - fmt.Sprintf("[%s] Pod %d (RespID %s) successfully renewed.", - color.InCyan("K8S"), - c.PodID, - c.RespID, - ), - ) -} diff --git a/internal/extension/container/manager/manager.go b/internal/extension/container/manager/manager.go deleted file mode 100644 index 54e15385..00000000 --- a/internal/extension/container/manager/manager.go +++ /dev/null @@ -1,27 +0,0 @@ -package manager - -import ( - "github.com/elabosak233/cloudsdale/internal/app/config" - "github.com/elabosak233/cloudsdale/internal/model" - "time" -) - -type IContainerManager interface { - Setup() (nats []*model.Nat, err error) - Status() (status string, err error) - Duration() (duration time.Duration) - Remove() - RemoveAfterDuration() (success bool) - Renew(duration time.Duration) - SetPodID(podID uint) -} - -func NewContainerManager(challenge model.Challenge, flag model.Flag, duration time.Duration) IContainerManager { - switch config.AppCfg().Container.Provider { - case "docker": - return NewDockerManager(challenge, flag, duration) - case "k8s": - return NewK8sManager(challenge, flag, duration) - } - return nil -} diff --git a/internal/extension/container/provider/docker.go b/internal/extension/container/provider/docker.go deleted file mode 100644 index 965a362c..00000000 --- a/internal/extension/container/provider/docker.go +++ /dev/null @@ -1,48 +0,0 @@ -package provider - -import ( - "context" - "fmt" - "github.com/docker/docker/client" - "github.com/elabosak233/cloudsdale/internal/app/config" - "go.uber.org/zap" -) - -var ( - dockerCli *client.Client -) - -func DockerCli() *client.Client { - return dockerCli -} - -func InitDockerProvider() { - dockerUri := config.AppCfg().Container.Docker.URI - dockerClient, err := client.NewClientWithOpts( - client.FromEnv, - client.WithAPIVersionNegotiation(), - client.WithHost(dockerUri), - ) - if err != nil { - zap.L().Fatal("Docker client initialization failed.") - } - zap.L().Info( - fmt.Sprintf( - "Docker client inits successfully, client version %s.", - dockerClient.ClientVersion(), - ), - ) - dockerCli = dockerClient - version, err := dockerClient.ServerVersion(context.Background()) - if err != nil { - zap.L().Fatal("Docker server connects failure.", - zap.Error(err), - ) - } - zap.L().Info( - fmt.Sprintf( - "Docker remote server connects successfully, server version %s.", - version.Version, - ), - ) -} diff --git a/internal/extension/container/provider/k8s.go b/internal/extension/container/provider/k8s.go deleted file mode 100644 index a8010201..00000000 --- a/internal/extension/container/provider/k8s.go +++ /dev/null @@ -1,44 +0,0 @@ -package provider - -import ( - "fmt" - "github.com/elabosak233/cloudsdale/internal/app/config" - "go.uber.org/zap" - "k8s.io/client-go/kubernetes" - "k8s.io/client-go/tools/clientcmd" - "os" -) - -var ( - // k8sCli Store Kubernetes client pointers - k8sCli *kubernetes.Clientset -) - -func K8sCli() *kubernetes.Clientset { - return k8sCli -} - -func InitK8sProvider() { - k8sConfig := config.AppCfg().Container.K8s.Config.Path - checkK8sConfig(k8sConfig) - cfg, err := clientcmd.BuildConfigFromFlags("", k8sConfig) - if err != nil { - zap.L().Fatal("Kubernetes config initialization failed.", zap.Error(err)) - } - k8sClient, err := kubernetes.NewForConfig(cfg) - if err != nil { - zap.L().Fatal("Kubernetes client initialization failed.", zap.Error(err)) - } - k8sCli = k8sClient - serverVersion, err := k8sClient.Discovery().ServerVersion() - if err != nil { - zap.L().Fatal("Kubernetes server connection failure.", zap.Error(err)) - } - zap.L().Info(fmt.Sprintf("Kubernetes remote server connection successful, server version %s", serverVersion)) -} - -func checkK8sConfig(k8sConfig string) { - if _, err := os.Stat(k8sConfig); err != nil { - zap.L().Fatal("Kubernetes configuration file not found.") - } -} diff --git a/internal/extension/container/provider/provider.go b/internal/extension/container/provider/provider.go deleted file mode 100644 index 496fa798..00000000 --- a/internal/extension/container/provider/provider.go +++ /dev/null @@ -1,17 +0,0 @@ -package provider - -import ( - "github.com/elabosak233/cloudsdale/internal/app/config" - "go.uber.org/zap" -) - -func InitContainerProvider() { - switch config.AppCfg().Container.Provider { - case "docker": - InitDockerProvider() - case "k8s": - InitK8sProvider() - default: - zap.L().Fatal("Invalid container provider!") - } -} diff --git a/internal/extension/extensions.go b/internal/extension/extensions.go deleted file mode 100644 index 48cb91f5..00000000 --- a/internal/extension/extensions.go +++ /dev/null @@ -1 +0,0 @@ -package extension diff --git a/internal/extension/proxy/proxy.go b/internal/extension/proxy/proxy.go deleted file mode 100644 index bbf83213..00000000 --- a/internal/extension/proxy/proxy.go +++ /dev/null @@ -1,11 +0,0 @@ -package proxy - -type IProxy interface { - Setup() - Close() - Entry() (entry string) -} - -func NewProxy(target string) IProxy { - return NewWSProxy(target) -} diff --git a/internal/extension/proxy/ws.go b/internal/extension/proxy/ws.go deleted file mode 100644 index 9688a9d5..00000000 --- a/internal/extension/proxy/ws.go +++ /dev/null @@ -1,196 +0,0 @@ -package proxy - -import ( - "fmt" - "github.com/elabosak233/cloudsdale/internal/app/config" - "github.com/elabosak233/cloudsdale/internal/utils" - "github.com/google/gopacket" - "github.com/google/gopacket/layers" - "github.com/google/gopacket/pcapgo" - "github.com/google/uuid" - "github.com/gorilla/websocket" - "go.uber.org/zap" - "net" - "net/http" - "os" - "path" - "strings" - "time" -) - -var ( - WSProxyMap = make(map[string]*WSProxy) -) - -type WSProxy struct { - Listen string - Target string - Upgrader websocket.Upgrader -} - -func NewWSProxy(target string) IProxy { - return &WSProxy{ - Target: target, - Upgrader: websocket.Upgrader{ - CheckOrigin: func(r *http.Request) bool { - return true - }, - }, - } -} - -func (w *WSProxy) Setup() { - w.Listen = uuid.NewString() - WSProxyMap[w.Listen] = w -} - -func (w *WSProxy) Handle(conn *websocket.Conn) { - switch config.AppCfg().Container.Proxy.TrafficCapture.Enabled { - case true: - w.handleInTrafficCapture(conn) - case false: - w.handle(conn) - } -} - -func (w *WSProxy) Close() { - delete(WSProxyMap, w.Listen) -} - -func (w *WSProxy) handle(conn *websocket.Conn) { - // 创建一个TCP连接到目标地址 - tcpConn, err := net.Dial("tcp", w.Target) - if err != nil { - zap.L().Error("Failed to connect to target.", zap.Error(err)) - return - } - defer func(tcpConn net.Conn) { - _ = tcpConn.Close() - }(tcpConn) - - // websocket -> tcp - go func() { - for { - messageType, message, err := conn.ReadMessage() - if err != nil { - zap.L().Debug("WebSocket read error.", zap.Error(err)) - return - } - if _, err := tcpConn.Write(message); err != nil { - zap.L().Debug("TCP connection write error.", zap.Error(err)) - return - } - if messageType == websocket.CloseMessage { - zap.L().Debug("WebSocket closed by client.") - return - } - } - }() - - // tcp -> websocket - for { - buf := make([]byte, 1024) - n, err := tcpConn.Read(buf) - if err != nil { - zap.L().Debug("TCP connection read error.", zap.Error(err)) - return - } - if err := conn.WriteMessage(websocket.BinaryMessage, buf[:n]); err != nil { - zap.L().Debug("WebSocket write error.", zap.Error(err)) - return - } - } -} - -func (w *WSProxy) handleInTrafficCapture(conn *websocket.Conn) { - tcpConn, err := net.Dial("tcp", w.Target) - if err != nil { - zap.L().Error("Failed to connect to target.", zap.Error(err)) - return - } - defer func(tcpConn net.Conn) { - _ = tcpConn.Close() - }(tcpConn) - - clientIP := strings.Split(conn.RemoteAddr().String(), ":")[0] - targetIP := strings.Split(w.Target, ":")[0] - targetPort := strings.Split(w.Target, ":")[1] - f, err := os.Create( - path.Join( - utils.CapturesPath, - fmt.Sprintf( - "%s-%s-%s-%s.pcap", - clientIP, - targetIP, - targetPort, - time.Now().Format("2006-01-02-15-04-05"), - ), - ), - ) - if err != nil { - zap.L().Error("Failed to create pcap file.", zap.Error(err)) - return - } - defer func(f *os.File) { - _ = f.Close() - }(f) - - writer := pcapgo.NewWriter(f) - if _err := writer.WriteFileHeader(1024, layers.LinkTypeEthernet); _err != nil { - zap.L().Error("Failed to write PCAP file header.", zap.Error(_err)) - return - } - - go func() { - for { - messageType, message, _err := conn.ReadMessage() - if _err != nil { - zap.L().Debug("WebSocket read error.", zap.Error(_err)) - return - } - _, _err = tcpConn.Write(message) - if _err != nil { - zap.L().Debug("TCP connection write error.", zap.Error(_err)) - return - } - if messageType == websocket.CloseMessage { - zap.L().Debug("WebSocket closed by client.") - return - } - if __err := writer.WritePacket(gopacket.CaptureInfo{ - CaptureLength: len(message), - Length: len(message), - Timestamp: time.Now(), - }, message); __err != nil { - zap.L().Debug("Failed to write packet to PCAP file.", zap.Error(__err)) - return - } - } - }() - - buf := make([]byte, 1024) - for { - n, _err := tcpConn.Read(buf) - if _err != nil { - zap.L().Debug("TCP connection read error.", zap.Error(_err)) - return - } - _err = conn.WriteMessage(websocket.BinaryMessage, buf[:n]) - if _err != nil { - zap.L().Debug("WebSocket write error.", zap.Error(_err)) - return - } - if __err := writer.WritePacket(gopacket.CaptureInfo{ - CaptureLength: n, - Length: n, - Timestamp: time.Now(), - }, buf[:n]); __err != nil { - zap.L().Debug("Failed to write packet to PCAP file.", zap.Error(__err)) - return - } - } -} - -func (w *WSProxy) Entry() string { - return w.Listen -} diff --git a/internal/extension/webhook/payload.go b/internal/extension/webhook/payload.go deleted file mode 100644 index 3d916c89..00000000 --- a/internal/extension/webhook/payload.go +++ /dev/null @@ -1,8 +0,0 @@ -package webhook - -import "github.com/elabosak233/cloudsdale/internal/model" - -type Payload struct { - GameID uint `json:"game_id,omitempty"` - Game *model.Game `json:"game,omitempty"` -} diff --git a/internal/extension/webhook/webhook.go b/internal/extension/webhook/webhook.go deleted file mode 100644 index cf004f80..00000000 --- a/internal/extension/webhook/webhook.go +++ /dev/null @@ -1,17 +0,0 @@ -package webhook - -import ( - "github.com/elabosak233/cloudsdale/internal/model" -) - -func POST(webhooks []*model.Webhook, object interface{}) { - for _, webhook := range webhooks { - switch webhook.Type { - case "application/x-www-form-urlencoded": - break - default: - case "application/json": - break - } - } -} diff --git a/internal/files/configs/application.json b/internal/files/configs/application.json deleted file mode 100644 index ee214f36..00000000 --- a/internal/files/configs/application.json +++ /dev/null @@ -1,84 +0,0 @@ -{ - "gin": { - "cors": { - "allow_methods": ["GET", "POST", "PUT", "DELETE"], - "allow_origins": ["*"] - }, - "jwt": { - "expiration": 180 - }, - "cache": { - "provider": "memory", - "redis": { - "host": "cache", - "port": 6379, - "password": "", - "db": 0 - } - }, - "host": "0.0.0.0", - "port": 8888 - }, - "container": { - "provider": "docker", - "entry": "127.0.0.1", - "docker": { - "uri": "unix:///var/run/docker.sock" - }, - "k8s": { - "namespace": "default", - "config": { - "path": "./configs/k8s.yml" - } - }, - "proxy": { - "enabled": false, - "traffic_capture": { - "enabled": false - } - } - }, - "db": { - "provider": "postgres", - "postgres": { - "dbname": "cloudsdale", - "host": "db", - "username": "cloudsdale", - "password": "cloudsdale", - "port": 5432, - "sslmode": "disable" - }, - "mysql": { - "dbname": "cloudsdale", - "host": "db", - "username": "cloudsdale", - "password": "cloudsdale", - "port": 3306 - }, - "sqlite": { - "path": "./db/db.sqlite" - } - }, - "email": { - "address": "", - "password": "", - "smtp": { - "host": "", - "port": 0 - } - }, - "captcha": { - "provider": "turnstile", - "turnstile": { - "url": "https://challenges.cloudflare.com/turnstile/v0/siteverify", - "site_key": "", - "secret_key": "" - }, - "recaptcha": { - "url:": "https://www.google.com/recaptcha/api/siteverify", - "site_key": "", - "secret_key": "", - "threshold": 0.5 - } - } -} diff --git a/internal/files/configs/casbin.conf b/internal/files/configs/casbin.conf deleted file mode 100644 index 93dafcfe..00000000 --- a/internal/files/configs/casbin.conf +++ /dev/null @@ -1,14 +0,0 @@ -[request_definition] -r = sub, obj, act - -[policy_definition] -p = sub, obj, act - -[role_definition] -g = _, _ - -[policy_effect] -e = some(where (p.eft == allow)) - -[matchers] -m = g(r.sub, p.sub) && keyMatch5(r.obj, p.obj) && regexMatch(r.act, p.act) \ No newline at end of file diff --git a/internal/files/configs/platform.json b/internal/files/configs/platform.json deleted file mode 100644 index 57a3010f..00000000 --- a/internal/files/configs/platform.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "site": { - "title": "Cloudsdale", - "description": "Hack for fun not for profit.", - "color": "#0C4497" - }, - "container": { - "parallel_limit": 1, - "request_limit": 0 - }, - "user": { - "register": { - "enabled": true, - "captcha": { - "enabled": false - }, - "email": { - "domains": [], - "enabled": false - } - } - } -} \ No newline at end of file diff --git a/internal/files/files.go b/internal/files/files.go deleted file mode 100644 index 1fe1debe..00000000 --- a/internal/files/files.go +++ /dev/null @@ -1,35 +0,0 @@ -package files - -import ( - "embed" - "github.com/elabosak233/cloudsdale/internal/utils" - "os" - "path" -) - -var ( - //go:embed * statics/* templates/* i18n/* - fs embed.FS -) - -func F() embed.FS { - return fs -} - -func ReadStaticFile(filename string) (data []byte, err error) { - if _, err = os.Stat(path.Join(utils.FilesPath, "statics", filename)); err == nil { - data, err = os.ReadFile(path.Join(utils.FilesPath, "statics", filename)) - } else { - data, err = F().ReadFile("statics/" + filename) - } - return data, err -} - -func ReadTemplateFile(filename string) (data []byte, err error) { - if _, err = os.Stat(path.Join(utils.FilesPath, "templates", filename)); err == nil { - data, err = os.ReadFile(path.Join(utils.FilesPath, "templates", filename)) - } else { - data, err = F().ReadFile("templates/" + filename) - } - return data, err -} diff --git a/internal/files/i18n/en.yaml b/internal/files/i18n/en.yaml deleted file mode 100644 index 1252d413..00000000 --- a/internal/files/i18n/en.yaml +++ /dev/null @@ -1,16 +0,0 @@ -welcome: "welcome" - -user: - not_found: "user not found" - login: - invalid_password: "password incorrect" - -team: - not_found: "team not found" - join: - invalid_token: "invalid token" - -pod: - not_found: "pod not found" - create: - invalid_image: "invalid image" \ No newline at end of file diff --git a/internal/files/i18n/zh-Hans.yaml b/internal/files/i18n/zh-Hans.yaml deleted file mode 100644 index 84dbe47e..00000000 --- a/internal/files/i18n/zh-Hans.yaml +++ /dev/null @@ -1,16 +0,0 @@ -welcome: "欢迎" - -user: - not_found: "用户不存在" - login: - invalid_password: "密码错误" - -team: - not_found: "团队不存在" - join: - invalid_token: "邀请码无效" - -pod: - not_found: "实例不存在" - create: - invalid_image: "无效镜像" \ No newline at end of file diff --git a/internal/files/templates/email/captcha.html b/internal/files/templates/email/captcha.html deleted file mode 100644 index d4c6f823..00000000 --- a/internal/files/templates/email/captcha.html +++ /dev/null @@ -1,3 +0,0 @@ - -
CAPTCHA
- \ No newline at end of file diff --git a/internal/middleware/casbin.go b/internal/middleware/casbin.go deleted file mode 100644 index 9b2e1ad3..00000000 --- a/internal/middleware/casbin.go +++ /dev/null @@ -1,62 +0,0 @@ -package middleware - -import ( - "github.com/elabosak233/cloudsdale/internal/app/config" - "github.com/elabosak233/cloudsdale/internal/extension/casbin" - "github.com/elabosak233/cloudsdale/internal/model" - "github.com/elabosak233/cloudsdale/internal/model/request" - "github.com/elabosak233/cloudsdale/internal/service" - "github.com/gin-gonic/gin" - "github.com/golang-jwt/jwt/v5" - "net/http" -) - -// Casbin -// The first layer of access control -// Default role is guest -// If the user is logged in, the role will be the user's group -// If the user's role has permission to access the resource, the request will be passed -// By the way, the user's information will be set to the context -func Casbin() gin.HandlerFunc { - - s := service.S() - - return func(ctx *gin.Context) { - var sub string - var user model.User - sub = "guest" - - userToken := ctx.GetHeader("Authorization") - if userToken != "" { - pgsToken, _ := jwt.Parse(userToken, func(token *jwt.Token) (interface{}, error) { - return []byte(config.JwtSecretKey()), nil - }) - if claims, ok := pgsToken.Claims.(jwt.MapClaims); ok && pgsToken.Valid { - if users, _, err := s.UserService.Find(request.UserFindRequest{ - ID: uint(claims["user_id"].(float64)), - }); err == nil && len(users) > 0 { - user = users[0] - } - sub = user.Group - } - } - - ok, err := casbin.Enforcer.Enforce(sub, ctx.Request.URL.Path, ctx.Request.Method) - if !ok || err != nil { - switch sub { - case "guest": - ctx.JSON(http.StatusUnauthorized, gin.H{ - "code": http.StatusUnauthorized, - }) - default: - ctx.JSON(http.StatusForbidden, gin.H{ - "code": http.StatusForbidden, - }) - } - ctx.Abort() - } - ctx.Set("user", &user) - ctx.Next() - return - } -} diff --git a/internal/middleware/frontend.go b/internal/middleware/frontend.go deleted file mode 100644 index 314372b0..00000000 --- a/internal/middleware/frontend.go +++ /dev/null @@ -1,58 +0,0 @@ -package middleware - -import ( - "github.com/elabosak233/cloudsdale/internal/app/config" - "github.com/elabosak233/cloudsdale/internal/utils" - "github.com/gin-gonic/gin" - "net/http" - "os" - "path/filepath" - "strings" -) - -func index(ctx *gin.Context) { - filePath := filepath.Join(utils.FrontendPath, "index.html") - indexContent, err := os.ReadFile(filePath) - if err != nil { - ctx.JSON(http.StatusInternalServerError, gin.H{ - "code": http.StatusInternalServerError, - "msg": "Error reading index.html", - }) - ctx.Abort() - return - } - indexContentStr := string(indexContent) - indexContentStr = strings.ReplaceAll(indexContentStr, "{{ Cloudsdale.Title }}", config.PltCfg().Site.Title) - ctx.Header("Content-Type", "text/html; charset=utf-8") - ctx.String(http.StatusOK, indexContentStr) - ctx.Abort() -} - -func Frontend(urlPrefix string) gin.HandlerFunc { - fileServer := http.FileServer(http.Dir(utils.FrontendPath)) - if !strings.HasSuffix(urlPrefix, "/") { - urlPrefix = urlPrefix + "/" - } - staticServerPrefix := strings.TrimRight(urlPrefix, "/") - return func(ctx *gin.Context) { - if strings.HasPrefix(ctx.Request.URL.Path, "/api") || strings.HasPrefix(ctx.Request.URL.Path, "/docs") { - ctx.Next() - } else { - ctx.Set("skip_logging", true) - filePath := filepath.Join(utils.FrontendPath, ctx.Request.URL.Path) - _, err := os.Stat(filePath) - if err == nil { - if ctx.Request.URL.Path == "/" || ctx.Request.URL.Path == "/index.html" { - index(ctx) - } else { - http.StripPrefix(staticServerPrefix, fileServer).ServeHTTP(ctx.Writer, ctx.Request) - ctx.Abort() - } - } else if os.IsNotExist(err) { - index(ctx) - } else { - ctx.Next() - } - } - } -} diff --git a/internal/model/article.go b/internal/model/article.go deleted file mode 100644 index 157b7188..00000000 --- a/internal/model/article.go +++ /dev/null @@ -1,11 +0,0 @@ -package model - -type Article struct { - ID uint `json:"id"` // The article's id. As primary key. - Title string `gorm:"type:varchar(50);not null;" json:"title"` // The article's title. - Summary string `gorm:"type:text;not null;" json:"summary"` // The article's summary. - Content string `gorm:"type:text;not null;" json:"content"` // The article's content. - AuthorID uint `gorm:"not null;" json:"author_id"` // The article's author's id. - CreatedAt int64 `gorm:"autoUpdateTime:milli" json:"created_at"` // The article's creation time. - UpdatedAt int64 `gorm:"autoUpdateTime:milli" json:"updated_at"` // The article's last update time. -} diff --git a/internal/model/category.go b/internal/model/category.go deleted file mode 100644 index 6fefc41e..00000000 --- a/internal/model/category.go +++ /dev/null @@ -1,25 +0,0 @@ -package model - -import ( - "gorm.io/gorm" -) - -// Category is the category of the challenge. -type Category struct { - ID uint `json:"id"` // The category's id. As primary key. - Name string `gorm:"type:varchar(32);not null;unique" json:"name"` // The category's name. - Description string `gorm:"type:text" json:"description"` // The category's description. - Color string `gorm:"type:varchar(7)" json:"color"` // The category's theme color. (Such as Rainbow Dash's color is "#60AEE4") - Icon string `gorm:"type:varchar(32);default:'fingerprint';" json:"icon"` // The category's icon. (Based on Material Design Icons, Reference site: https://pictogrammers.com/library/mdi/) (Such as "fingerprint": https://pictogrammers.com/library/mdi/icon/fingerprint/) - CreatedAt int64 `gorm:"autoUpdateTime:milli" json:"created_at,omitempty"` // The category's creation time. - UpdatedAt int64 `gorm:"autoUpdateTime:milli" json:"updated_at,omitempty"` // The category's last update time. -} - -func (c *Category) BeforeDelete(db *gorm.DB) (err error) { - var challenges []Challenge - db.Table("challenges").Where("category_id = ?", c.ID).Find(&challenges) - for _, challenge := range challenges { - db.Table("challenges").Delete(&challenge) - } - return nil -} diff --git a/internal/model/challenge.go b/internal/model/challenge.go deleted file mode 100644 index 49e1268c..00000000 --- a/internal/model/challenge.go +++ /dev/null @@ -1,88 +0,0 @@ -package model - -import ( - "fmt" - "github.com/elabosak233/cloudsdale/internal/utils" - "gorm.io/gorm" - "os" - "path" -) - -// Challenge is the challenge for Jeopardy-style CTF game. -type Challenge struct { - ID uint `json:"id"` // The challenge's id. As primary key. - Title string `gorm:"type:varchar(32);not null;" json:"title"` // The challenge's title. - Description string `gorm:"type:text;not null;" json:"description"` // The challenge's description. - CategoryID uint `gorm:"not null;" json:"category_id"` // The challenge's category. - Category *Category `json:"category,omitempty"` // The challenge's category. - Attachment *File `gorm:"-" json:"attachment"` // The challenge's attachment. - IsPracticable *bool `gorm:"not null;default:false" json:"is_practicable,omitempty"` // Whether the challenge is practicable. (Is the practice field visible.) - IsDynamic *bool `gorm:"default:false" json:"is_dynamic"` // Whether the challenge is based on dynamic container. - Difficulty int64 `gorm:"default:1" json:"difficulty"` // The degree of difficulty. (From 1 to 5) - PracticePts int64 `gorm:"default:200" json:"practice_pts,omitempty"` // The points will be given when the challenge is solved in practice field. - Duration int64 `gorm:"default:1800" json:"duration,omitempty"` // The duration of container maintenance in the initial state. (Seconds) - ImageName string `gorm:"type:varchar(255);" json:"image_name,omitempty"` // The challenge's image name. - CPULimit int64 `gorm:"default:1" json:"cpu_limit,omitempty"` // The challenge's CPU limit. (0 means no limit) - MemoryLimit int64 `gorm:"default:64" json:"memory_limit,omitempty"` // The challenge's memory limit. (0 means no limit) - Flags []*Flag `json:"flags,omitempty"` - Ports []*Port `json:"ports,omitempty"` - Envs []*Env `json:"envs,omitempty"` - Solved *Submission `json:"solved,omitempty"` - SolvedTimes int `gorm:"-" json:"solved_times"` - Submissions []*Submission `json:"-"` - Bloods []*Submission `gorm:"-" json:"bloods,omitempty"` - CreatedAt int64 `gorm:"autoUpdateTime:milli" json:"created_at,omitempty"` // The challenge's creation time. - UpdatedAt int64 `gorm:"autoUpdateTime:milli" json:"updated_at,omitempty"` // The challenge's last update time. -} - -func (c *Challenge) Simplify() { - c.ImageName = "" - c.CPULimit = 0 - c.MemoryLimit = 0 - c.Flags = nil - c.Ports = nil - c.Envs = nil -} - -func (c *Challenge) AfterFind(db *gorm.DB) (err error) { - p := path.Join(utils.MediaPath, "challenges", fmt.Sprintf("%d", c.ID)) - var name string - var size int64 - if files, _err := os.ReadDir(p); _err == nil { - for _, file := range files { - name = file.Name() - info, _ := file.Info() - size = info.Size() - break - } - } - attachment := File{ - Name: name, - Size: size, - } - c.Attachment = &attachment - return nil -} - -func (c *Challenge) BeforeUpdate(db *gorm.DB) (err error) { - if c.Ports != nil { - db.Table("ports").Where("challenge_id = ?", c.ID).Delete(&Port{}) - db.Table("envs").Where("challenge_id = ?", c.ID).Delete(&Env{}) - } - return nil -} - -func (c *Challenge) BeforeDelete(db *gorm.DB) (err error) { - var pods []Pod - db.Table("pods").Where("challenge_id = ?", c.ID).Find(&pods) - for _, pod := range pods { - db.Table("pods").Delete(&pod) - } - - db.Table("flags").Where("challenge_id = ?", c.ID).Delete(&Flag{}) - db.Table("ports").Where("challenge_id = ?", c.ID).Delete(&Port{}) - db.Table("envs").Where("challenge_id = ?", c.ID).Delete(&Env{}) - db.Table("submissions").Where("challenge_id = ?", c.ID).Delete(&Submission{}) - db.Table("game_challenges").Where("challenge_id = ?", c.ID).Delete(&GameChallenge{}) - return nil -} diff --git a/internal/model/env.go b/internal/model/env.go deleted file mode 100644 index 4d327255..00000000 --- a/internal/model/env.go +++ /dev/null @@ -1,9 +0,0 @@ -package model - -type Env struct { - ID uint `json:"id"` - Key string `gorm:"type:varchar(128);not null;" json:"key"` - Value string `gorm:"type:varchar(128);not null;" json:"value"` - ChallengeID uint `gorm:"not null;" json:"challenge_id"` - Challenge *Challenge `json:"challenge,omitempty"` -} diff --git a/internal/model/file.go b/internal/model/file.go deleted file mode 100644 index 832bd15f..00000000 --- a/internal/model/file.go +++ /dev/null @@ -1,8 +0,0 @@ -package model - -// File is only a struct for the file information. -// Not a real model for GORM. -type File struct { - Name string `json:"name"` - Size int64 `json:"size"` -} diff --git a/internal/model/flag.go b/internal/model/flag.go deleted file mode 100644 index ba7cb4e4..00000000 --- a/internal/model/flag.go +++ /dev/null @@ -1,13 +0,0 @@ -package model - -// Flag is the answer of a Challenge. -// Because of the Flag is only a subsidiary table, it doesn't need the creation time or updated time. -type Flag struct { - ID uint `json:"id"` // The flag id. - Type string `gorm:"type:varchar(16);not null;default:'static';" json:"type"` // The flag type. ("static"/"dynamic"/"pattern") - Banned *bool `gorm:"not null;default:false;" json:"banned"` // Whether the flag is banned. If banned, the user who submitted the flag will be judged as cheating. - Value string `gorm:"type:varchar(255);" json:"value"` // The flag content. Maybe a string or a regex, or the placeholder for dynamic challenges. (Such as "flag{friendsh1p_1s_magic}" or "flag{[a-zA-Z]{5}}" or "flag{[UUID]}") - Env string `gorm:"type:varchar(16);" json:"env"` // The environment variable which is used to be injected with the flag. - ChallengeID uint `json:"challenge_id"` // The challenge id. The flag belongs to. - Challenge *Challenge `json:"challenge"` // The challenge which the flag belongs to. -} diff --git a/internal/model/game.go b/internal/model/game.go deleted file mode 100644 index 385f05ce..00000000 --- a/internal/model/game.go +++ /dev/null @@ -1,60 +0,0 @@ -package model - -import ( - "fmt" - "github.com/elabosak233/cloudsdale/internal/utils" - "gorm.io/gorm" - "os" - "path" -) - -type Game struct { - ID uint `json:"id"` // The game's id. As primary key. - Title string `gorm:"type:varchar(64);not null" json:"title,omitempty"` // The game's title. - Bio string `gorm:"type:text" json:"bio,omitempty"` // The game's short description. - Description string `gorm:"type:text" json:"description,omitempty"` // The game's description. (Markdown supported.) - Poster *File `gorm:"-" json:"poster"` // The game's poster image. - PublicKey string `gorm:"type:varchar(255)" json:"public_key,omitempty"` // The game's public key. - PrivateKey string `gorm:"type:varchar(255)" json:"-"` // The game's private key. - IsEnabled *bool `gorm:"not null;default:false" json:"is_enabled,omitempty"` // Whether the game is enabled. - IsPublic *bool `gorm:"not null;default:true" json:"is_public,omitempty"` // Whether the game is public. - MemberLimitMin int64 `gorm:"not null;default:1" json:"member_limit_min,omitempty"` // The minimum team member limit. - MemberLimitMax int64 `gorm:"default:10" json:"member_limit_max,omitempty"` // The maximum team member limit. - ParallelContainerLimit int64 `gorm:"not null;default:2" json:"parallel_container_limit,omitempty"` // The maximum parallel container limit. - FirstBloodRewardRatio float64 `gorm:"default:5" json:"first_blood_reward_ratio,omitempty"` // The prize ratio of first blood. - SecondBloodRewardRatio float64 `gorm:"default:3" json:"second_blood_reward_ratio,omitempty"` // The prize ratio of second blood. - ThirdBloodRewardRatio float64 `gorm:"default:1" json:"third_blood_reward_ratio,omitempty"` // The prize ratio of third blood. - IsNeedWriteUp *bool `gorm:"not null;default:true" json:"is_need_write_up,omitempty"` // Whether the game need write up. - StartedAt int64 `gorm:"not null" json:"started_at,omitempty"` // The game's start time. (Unix) - EndedAt int64 `gorm:"not null" json:"ended_at,omitempty"` // The game's end time. (Unix) - CreatedAt int64 `gorm:"autoUpdateTime:milli" json:"created_at,omitempty"` // The game's creation time. - UpdatedAt int64 `gorm:"autoUpdateTime:milli" json:"updated_at,omitempty"` // The game's last update time. -} - -func (g *Game) AfterFind(db *gorm.DB) (err error) { - p := path.Join(utils.MediaPath, "games", fmt.Sprintf("%d", g.ID), "poster") - var name string - var size int64 - if files, _err := os.ReadDir(p); _err == nil { - for _, file := range files { - name = file.Name() - info, _ := file.Info() - size = info.Size() - break - } - } - poster := File{ - Name: name, - Size: size, - } - g.Poster = &poster - return nil -} - -func (g *Game) BeforeDelete(db *gorm.DB) (err error) { - db.Table("game_teams").Where("game_id = ?", g.ID).Delete(&GameTeam{}) - db.Table("game_challenges").Where("game_id = ?", g.ID).Delete(&GameChallenge{}) - db.Table("submissions").Where("game_id = ?", g.ID).Delete(&Submission{}) - db.Table("notices").Where("game_id = ?", g.ID).Delete(&Notice{}) - return nil -} diff --git a/internal/model/game_challenge.go b/internal/model/game_challenge.go deleted file mode 100644 index 41a10577..00000000 --- a/internal/model/game_challenge.go +++ /dev/null @@ -1,21 +0,0 @@ -package model - -import "gorm.io/gorm" - -type GameChallenge struct { - ID uint `json:"id,omitempty"` - GameID uint `gorm:"uniqueIndex:game_challenge_idx" json:"game_id,omitempty"` - Game *Game `json:"game,omitempty"` - ChallengeID uint `gorm:"uniqueIndex:game_challenge_idx" json:"challenge_id,omitempty"` - Challenge *Challenge `json:"challenge,omitempty"` - IsEnabled *bool `gorm:"default:false;not null;" json:"is_enabled,omitempty"` - Pts int64 `gorm:"-" json:"pts,omitempty"` - MaxPts int64 `gorm:"default:1000;not null;" json:"max_pts,omitempty"` - MinPts int64 `gorm:"default:200;not null;" json:"min_pts,omitempty"` -} - -func (g *GameChallenge) BeforeDelete(db *gorm.DB) (err error) { - db.Table("submissions").Where("game_id = ?", g.GameID).Where("challenge_id = ?", g.ChallengeID).Delete(&Submission{}) - db.Table("notices").Where("game_id = ?", g.GameID).Where("challenge_id = ?", g.ChallengeID).Delete(&Notice{}) - return nil -} diff --git a/internal/model/game_team.go b/internal/model/game_team.go deleted file mode 100644 index 2214a076..00000000 --- a/internal/model/game_team.go +++ /dev/null @@ -1,11 +0,0 @@ -package model - -type GameTeam struct { - ID uint `json:"id,omitempty"` - TeamID uint `gorm:"uniqueIndex:game_team_idx" json:"team_id,omitempty"` - Team *Team `json:"team,omitempty"` - GameID uint `gorm:"uniqueIndex:game_team_idx" json:"game_id,omitempty"` - Game *Game `json:"game,omitempty"` - IsAllowed *bool `gorm:"default:false;not null;" json:"is_allowed,omitempty"` - Signature string `gorm:"unique" json:"signature,omitempty"` -} diff --git a/internal/model/nat.go b/internal/model/nat.go deleted file mode 100644 index a5d9c212..00000000 --- a/internal/model/nat.go +++ /dev/null @@ -1,11 +0,0 @@ -package model - -type Nat struct { - ID uint `json:"id"` - PodID uint `gorm:"not null" json:"pod_id"` - Pod *Pod `json:"pod,omitempty"` - SrcPort int `gorm:"not null" json:"src_port"` // Of image - DstPort int `gorm:"not null" json:"dst_port"` // Of pod - Proxy string `json:"proxy"` // Of platform - Entry string `gorm:"type:varchar(128)" json:"entry"` -} diff --git a/internal/model/notice.go b/internal/model/notice.go deleted file mode 100644 index 1ee36281..00000000 --- a/internal/model/notice.go +++ /dev/null @@ -1,38 +0,0 @@ -package model - -import ( - "github.com/elabosak233/cloudsdale/internal/extension/broadcast" - "gorm.io/gorm" -) - -type Notice struct { - ID uint `json:"id"` // The game event's id. - Type string `gorm:"type:varchar(16);not null;default:'notice'" json:"type,omitempty"` // The game event's type. (Such as "first_blood", "second_blood", "third_blood", "new_challenge", "new_hint", "normal") - GameID *uint `gorm:"index;not null;" json:"game_id,omitempty"` // The game which this event belongs to. - Game *Game `json:"game,omitempty"` // The game which this event belongs to. - UserID *uint `gorm:"index" json:"user_id,omitempty"` // The user who is related to this event. - User *User `json:"user,omitempty"` // The user who is related to this event. - TeamID *uint `gorm:"index" json:"team_id,omitempty"` // The team which is related to this event. - Team *Team `json:"team,omitempty"` // The team which is related to this event. - ChallengeID *uint `json:"challenge_id,omitempty"` // The challenge which is related to this event. - Challenge *Challenge `json:"challenge,omitempty"` // The challenge which is related to this event. - Content string `gorm:"type:text" json:"content,omitempty"` // The content of this event. (Only for "notice" type) - CreatedAt int64 `gorm:"autoUpdateTime:milli" json:"created_at,omitempty"` // The game event's creation time. - UpdatedAt int64 `gorm:"autoUpdateTime:milli" json:"updated_at,omitempty"` // The game event's last update time. -} - -func (n *Notice) AfterCreate(db *gorm.DB) (err error) { - result := db.Table("notices"). - Preload("User", func(db *gorm.DB) *gorm.DB { - return db.Select([]string{"id", "username", "nickname", "email"}) - }). - Preload("Team", func(db *gorm.DB) *gorm.DB { - return db.Select([]string{"id", "name", "email"}) - }). - Preload("Challenge", func(db *gorm.DB) *gorm.DB { - return db.Select([]string{"id", "title"}) - }). - First(n, n.ID) - broadcast.SendGameMsg(*(n.GameID), n) - return result.Error -} diff --git a/internal/model/pod.go b/internal/model/pod.go deleted file mode 100644 index 2a053f7c..00000000 --- a/internal/model/pod.go +++ /dev/null @@ -1,31 +0,0 @@ -package model - -import "gorm.io/gorm" - -type Pod struct { - ID uint `json:"id"` - GameID *uint `gorm:"index;null;default:null" json:"game_id"` - Game *Game `gorm:"foreignkey:GameID;association_foreignkey:ID" json:"game,omitempty"` - UserID *uint `gorm:"index;null;default:null" json:"user_id"` - User *User `gorm:"foreignkey:UserID;association_foreignkey:ID" json:"user,omitempty"` - TeamID *uint `gorm:"index;null;default:null" json:"team_id"` - Team *Team `gorm:"foreignkey:TeamID;association_foreignkey:ID" json:"team,omitempty"` - ChallengeID *uint `gorm:"index;null;default:null" json:"challenge_id"` - Challenge *Challenge `gorm:"foreignkey:ChallengeID;association_foreignkey:ID" json:"challenge,omitempty"` - Flag string `json:"flag,omitempty"` // The generated flag, which will be injected into the container. - RemovedAt int64 `json:"removed_at"` - CreatedAt int64 `gorm:"autoUpdateTime:milli" json:"created_at,omitempty"` - Nats []*Nat `json:"nats,omitempty"` -} - -func (p *Pod) Simplify() { - p.Flag = "" - if p.Challenge != nil { - p.Challenge.Simplify() - } -} - -func (p *Pod) BeforeDelete(db *gorm.DB) (err error) { - db.Table("nats").Where("pod_id = ?", p.ID).Delete(&Nat{}) - return nil -} diff --git a/internal/model/port.go b/internal/model/port.go deleted file mode 100644 index dfb57d79..00000000 --- a/internal/model/port.go +++ /dev/null @@ -1,11 +0,0 @@ -package model - -// Port is the mapping between the JeopardyImage and the exposed port of the image. -// Because of the port is only a subsidiary table, it doesn't need the creation time or updated time. -type Port struct { - ID uint `json:"id"` // The port's id. As primary key. - ChallengeID uint `gorm:"not null;" json:"challenge_id"` // The JeopardyImage which the port belongs to. - Challenge *Challenge `json:"challenge,omitempty"` // The JeopardyImage which the port belongs to. - Value int `gorm:"not null;" json:"value"` // The port number. - Description string `gorm:"type:varchar(32)" json:"description"` // The port's description. -} diff --git a/internal/model/request/category.go b/internal/model/request/category.go deleted file mode 100644 index d5f75e0f..00000000 --- a/internal/model/request/category.go +++ /dev/null @@ -1,25 +0,0 @@ -package request - -type CategoryCreateRequest struct { - Name string `json:"name" binding:"required"` - Description string `json:"description" binding:"required"` - Color string `json:"color" binding:"required"` - Icon string `json:"icon" binding:"required"` -} - -type CategoryUpdateRequest struct { - ID uint `json:"id" binding:"required"` - Name string `json:"name" binding:"required"` - Description string `json:"description" binding:"required"` - Color string `json:"color" binding:"required"` - Icon string `json:"icon" binding:"required"` -} - -type CategoryFindRequest struct { - ID uint `json:"id" form:"id"` - Name string `json:"name" form:"name"` -} - -type CategoryDeleteRequest struct { - ID uint `json:"id"` -} diff --git a/internal/model/request/challenge.go b/internal/model/request/challenge.go deleted file mode 100644 index d980c3e1..00000000 --- a/internal/model/request/challenge.go +++ /dev/null @@ -1,63 +0,0 @@ -package request - -import ( - "github.com/elabosak233/cloudsdale/internal/model" -) - -type ChallengeCreateRequest struct { - Title string `json:"title"` - Description string `json:"description"` - HasAttachment *bool `json:"has_attachment"` - AttachmentURL string `json:"attachment_url"` - IsPracticable *bool `json:"is_practicable"` - IsDynamic *bool `json:"is_dynamic"` - CategoryID uint `json:"category_id"` - Duration int64 `json:"duration"` - Difficulty int64 `json:"difficulty"` - PracticePts int64 `json:"practice_pts"` - ImageName string `json:"image_name"` - CPULimit *int64 `json:"cpu_limit"` - MemoryLimit *int64 `json:"memory_limit"` - Ports []*model.Port `json:"ports"` - Envs []*model.Env `json:"envs"` -} - -type ChallengeUpdateRequest struct { - ID uint `json:"-"` - Title string `json:"title"` - Description string `json:"description"` - HasAttachment *bool `json:"has_attachment"` - AttachmentURL string `json:"attachment_url"` - IsPracticable *bool `json:"is_practicable"` - IsDynamic *bool `json:"is_dynamic"` - CategoryID int64 `json:"category_id"` - Duration int64 `json:"duration"` - Difficulty int64 `json:"difficulty"` - PracticePts int64 `json:"practice_pts"` - ImageName string `json:"image_name"` - CPULimit *int64 `json:"cpu_limit"` - MemoryLimit *int64 `json:"memory_limit"` - Ports []*model.Port `json:"ports"` - Envs []*model.Env `json:"envs"` -} - -type ChallengeDeleteRequest struct { - ID uint `json:"-"` -} - -type ChallengeFindRequest struct { - ID uint `json:"id" form:"id"` - CategoryID *uint `json:"category_id" form:"category_id"` - Title string `json:"title" form:"title"` - IsPracticable *bool `json:"is_practicable" form:"is_practicable"` - IsDynamic *bool `json:"is_dynamic" form:"is_dynamic"` - Difficulty int64 `json:"difficulty" form:"difficulty"` - UserID uint `json:"user_id" form:"user_id"` - GameID *uint `json:"game_id" form:"game_id"` - TeamID *uint `json:"team_id" form:"team_id"` - IsDetailed *bool `json:"is_detailed" form:"is_detailed"` - Page int `json:"page" form:"page"` - Size int `json:"size" form:"size"` - SortKey string `json:"sort_key" form:"sort_key"` - SortOrder string `json:"sort_order" form:"sort_order"` -} diff --git a/internal/model/request/config.go b/internal/model/request/config.go deleted file mode 100644 index 69a6a057..00000000 --- a/internal/model/request/config.go +++ /dev/null @@ -1,7 +0,0 @@ -package request - -import "github.com/elabosak233/cloudsdale/internal/app/config" - -type ConfigUpdateRequest struct { - config.PlatformCfg -} diff --git a/internal/model/request/flag.go b/internal/model/request/flag.go deleted file mode 100644 index 6cb8ea6e..00000000 --- a/internal/model/request/flag.go +++ /dev/null @@ -1,22 +0,0 @@ -package request - -type FlagCreateRequest struct { - ChallengeID uint `json:"-"` - Type string `json:"type"` - Banned *bool `json:"banned"` - Value string `json:"value"` - Env string `json:"env"` -} - -type FlagUpdateRequest struct { - ID uint `json:"-"` - ChallengeID uint `json:"-"` - Type string `json:"type"` - Banned *bool `json:"banned"` - Value string `json:"value"` - Env string `json:"env"` -} - -type FlagDeleteRequest struct { - ID uint `json:"-"` -} diff --git a/internal/model/request/game.go b/internal/model/request/game.go deleted file mode 100644 index ba5fc987..00000000 --- a/internal/model/request/game.go +++ /dev/null @@ -1,52 +0,0 @@ -package request - -type GameFindRequest struct { - ID uint `json:"id" form:"id"` - Title string `json:"title" form:"title"` - IsEnabled *bool `json:"is_enabled" form:"is_enabled"` - Page int `json:"page" form:"page"` - Size int `json:"size" form:"size"` - SortKey string `json:"sort_key" form:"sort_key"` - SortOrder string `json:"sort_order" form:"sort_order"` -} - -type GameCreateRequest struct { - Title string `json:"title" binding:"required" msg:"标题不能为空"` - Bio string `json:"bio"` - Description string `json:"description"` - IsEnabled *bool `json:"is_enabled"` - IsPublic *bool `json:"is_public"` - CoverURL string `json:"cover_url"` - MemberLimitMin int64 `json:"member_limit_min"` - MemberLimitMax int64 `json:"member_limit_max"` - ParallelContainerLimit int64 `json:"parallel_container_limit"` - FirstBloodRewardRatio float64 `json:"first_blood_reward_ratio"` - SecondBloodRewardRatio float64 `json:"second_blood_reward_ratio"` - ThirdBloodRewardRatio float64 `json:"third_blood_reward_ratio"` - IsNeedWriteUp *bool `json:"is_need_write_up"` - StartedAt int64 `json:"started_at"` - EndedAt int64 `json:"ended_at"` -} - -type GameUpdateRequest struct { - ID uint `json:"-"` - Title string `json:"title"` - Bio string `json:"bio"` - Description string `json:"description"` - CoverURL string `json:"cover_url"` - IsEnabled *bool `json:"is_enabled"` - IsPublic *bool `json:"is_public"` - MemberLimitMin int64 `json:"member_limit_min"` - MemberLimitMax int64 `json:"member_limit_max"` - ParallelContainerLimit int64 `json:"parallel_container_limit"` - FirstBloodRewardRatio float64 `json:"first_blood_reward_ratio"` - SecondBloodRewardRatio float64 `json:"second_blood_reward_ratio"` - ThirdBloodRewardRatio float64 `json:"third_blood_reward_ratio"` - IsNeedWriteUp *bool `json:"is_need_write_up"` - StartedAt int64 `json:"started_at"` - EndedAt int64 `json:"ended_at"` -} - -type GameDeleteRequest struct { - ID uint `json:"-"` -} diff --git a/internal/model/request/game_challenge.go b/internal/model/request/game_challenge.go deleted file mode 100644 index 48b8f2bd..00000000 --- a/internal/model/request/game_challenge.go +++ /dev/null @@ -1,31 +0,0 @@ -package request - -type GameChallengeFindRequest struct { - GameID uint `json:"game_id" form:"game_id"` - ChallengeID uint `json:"challenge_id" form:"challenge_id"` - TeamID uint `json:"team_id" form:"team_id"` - IsEnabled *bool `json:"is_enabled" form:"is_enabled"` -} - -type GameChallengeCreateRequest struct { - GameID uint `json:"-"` - ChallengeID uint `json:"challenge_id"` - IsEnabled *bool `json:"is_enabled"` - MaxPts int64 `json:"max_pts"` - MinPts int64 `json:"min_pts"` -} - -type GameChallengeUpdateRequest struct { - ID uint `json:"id"` - GameID uint `json:"-"` - ChallengeID uint `json:"challenge_id"` - IsEnabled *bool `json:"is_enabled"` - MaxPts int64 `json:"max_pts"` - MinPts int64 `json:"min_pts"` -} - -type GameChallengeDeleteRequest struct { - ID uint `json:"-"` - GameID uint `json:"-"` - ChallengeID uint `json:"-"` -} diff --git a/internal/model/request/game_team.go b/internal/model/request/game_team.go deleted file mode 100644 index 8b59f52a..00000000 --- a/internal/model/request/game_team.go +++ /dev/null @@ -1,24 +0,0 @@ -package request - -type GameTeamCreateRequest struct { - ID uint `json:"-"` - TeamID uint `json:"team_id"` - UserID uint `json:"user_id"` - Password string `json:"password"` -} - -type GameTeamUpdateRequest struct { - GameID uint `json:"-"` - TeamID uint `json:"-"` - IsAllowed *bool `json:"is_allowed"` -} - -type GameTeamFindRequest struct { - GameID uint `json:"game_id" form:"game_id"` - TeamID uint `json:"team_id" form:"team_id"` -} - -type GameTeamDeleteRequest struct { - GameID uint `json:"game_id"` - TeamID uint `json:"team_id"` -} diff --git a/internal/model/request/notice.go b/internal/model/request/notice.go deleted file mode 100644 index 1f937557..00000000 --- a/internal/model/request/notice.go +++ /dev/null @@ -1,29 +0,0 @@ -package request - -type NoticeFindRequest struct { - ID uint `json:"id" form:"id"` - GameID uint `json:"game_id" form:"id"` - Type string `json:"type" form:"id"` -} - -type NoticeCreateRequest struct { - GameID uint `json:"game_id"` - ChallengeID *uint `json:"challenge_id"` - UserID *uint `json:"user_id"` - TeamID *uint `json:"team_id"` - Type string `json:"type"` - Content string `json:"content"` -} - -type NoticeUpdateRequest struct { - ID uint `json:"id"` - GameID uint `json:"game_id"` - ChallengeID *uint `json:"challenge_id"` - UserID *uint `json:"user_id"` - TeamID *uint `json:"team_id"` - Content string `json:"content"` -} - -type NoticeDeleteRequest struct { - ID uint `json:"id"` -} diff --git a/internal/model/request/pod.go b/internal/model/request/pod.go deleted file mode 100644 index 438d44b1..00000000 --- a/internal/model/request/pod.go +++ /dev/null @@ -1,33 +0,0 @@ -package request - -type PodCreateRequest struct { - ChallengeID uint `binding:"required" json:"challenge_id"` - TeamID *uint `json:"team_id"` - GameID *uint `json:"game_id"` - UserID uint `json:"-"` -} - -type PodFindRequest struct { - ID uint `json:"id" form:"id"` - ChallengeID uint `json:"challenge_id" form:"challenge_id"` - UserID *uint `json:"user_id" form:"user_id"` - TeamID *uint `json:"team_id" form:"team_id"` - GameID *uint `json:"game_id" form:"game_id"` - IsAvailable *bool `json:"is_available" form:"is_available"` - Page int `json:"page" form:"page"` - Size int `json:"size" form:"size"` -} - -type PodRemoveRequest struct { - ID uint `json:"-"` - TeamID *uint `json:"team_id"` - GameID *uint `json:"game_id"` - UserID uint `json:"-"` -} - -type PodRenewRequest struct { - ID uint `json:"-"` - TeamID *uint `json:"team_id"` - GameID *uint `json:"game_id"` - UserID uint `json:"-"` -} diff --git a/internal/model/request/port.go b/internal/model/request/port.go deleted file mode 100644 index acda867f..00000000 --- a/internal/model/request/port.go +++ /dev/null @@ -1,6 +0,0 @@ -package request - -type PortCreateRequest struct { - Value int `xorm:"'value' notnull" json:"value"` - Description string `xorm:"'description' varchar(32)" json:"description"` -} diff --git a/internal/model/request/submission.go b/internal/model/request/submission.go deleted file mode 100644 index bfa5feb0..00000000 --- a/internal/model/request/submission.go +++ /dev/null @@ -1,26 +0,0 @@ -package request - -type SubmissionCreateRequest struct { - Flag string `json:"flag" binding:"required"` // 提交内容 - UserID uint `json:"-"` // 用户 Id - ChallengeID uint `json:"challenge_id" binding:"required"` // 题目 Id - TeamID *uint `json:"team_id"` // 团队 Id - GameID *uint `json:"game_id"` // 比赛 Id -} - -type SubmissionDeleteRequest struct { - SubmissionID uint `json:"id" binding:"required"` -} - -type SubmissionFindRequest struct { - UserID uint `json:"user_id" form:"user_id"` // 用户 Id - Status int `json:"status" form:"status"` // 评判结果 - ChallengeID uint `json:"challenge_id" form:"challenge_id"` // 题目 Id - TeamID *uint `json:"team_id" form:"team_id"` // 团队 Id - GameID *uint `json:"game_id" form:"game_id"` // 比赛 Id - IsDetailed bool `json:"is_detailed" form:"is_detailed"` // 是否详细 - Page int `json:"page" form:"page"` // 页码 - Size int `json:"size" form:"size"` // 每页大小 - SortKey string `json:"sort_key" form:"sort_key"` // 排序参数 - SortOrder string `json:"sort_order" form:"sort_order"` // 排序方式 -} diff --git a/internal/model/request/team.go b/internal/model/request/team.go deleted file mode 100644 index 16cfa7a5..00000000 --- a/internal/model/request/team.go +++ /dev/null @@ -1,43 +0,0 @@ -package request - -type TeamCreateRequest struct { - Name string `binding:"required" json:"name"` - Description string `json:"description"` - Email string `json:"email"` - CaptainId uint `binding:"required" json:"captain_id"` -} - -type TeamUpdateRequest struct { - ID uint `json:"-"` - Name string `json:"name"` - Description string `json:"description"` - Email string `json:"email"` - CaptainId uint `json:"captain_id"` - IsLocked *bool `json:"is_locked"` -} - -type TeamFindRequest struct { - ID uint `json:"id" form:"id"` - Name string `json:"name" form:"name"` - UserID *uint `json:"user_id" form:"user_id"` - CaptainID uint `json:"captain_id" form:"captain_id"` - GameID *uint `json:"game_id" form:"game_id"` - Page int `json:"page" form:"page"` - Size int `json:"size" form:"size"` - SortKey string `json:"sort_key" form:"sort_key"` - SortOrder string `json:"sort_order" form:"sort_order"` -} - -type TeamDeleteRequest struct { - ID uint `json:"-"` -} - -type TeamGetInviteTokenRequest struct { - ID uint `json:"-"` - UserID uint `json:"-"` -} - -type TeamUpdateInviteTokenRequest struct { - ID uint `json:"-"` - UserID uint `json:"-"` -} diff --git a/internal/model/request/team_user.go b/internal/model/request/team_user.go deleted file mode 100644 index 297ad54c..00000000 --- a/internal/model/request/team_user.go +++ /dev/null @@ -1,18 +0,0 @@ -package request - -type TeamUserCreateRequest struct { - TeamID uint `json:"-"` - UserID uint `json:"user_id"` - InviteToken string `json:"invite_token"` -} - -type TeamUserDeleteRequest struct { - TeamID uint `binding:"required" json:"team_id"` - UserID uint `binding:"required" json:"user_id"` -} - -type TeamUserJoinRequest struct { - TeamID uint `json:"-"` - UserID uint `json:"-"` - InviteToken string `json:"invite_token"` -} diff --git a/internal/model/request/user.go b/internal/model/request/user.go deleted file mode 100644 index 2475210b..00000000 --- a/internal/model/request/user.go +++ /dev/null @@ -1,49 +0,0 @@ -package request - -type UserFindRequest struct { - ID uint `json:"id" form:"id"` - Username string `json:"username" form:"username"` - Name string `json:"name" form:"name"` - Group string `json:"group" from:"group"` - Email string `json:"email" form:"email"` - Page int `json:"page" form:"page"` - Size int `json:"size" form:"size"` - SortKey string `json:"sort_key" form:"sort_key"` - SortOrder string `json:"sort_order" form:"sort_order"` -} - -type UserRegisterRequest struct { - Username string `binding:"required" json:"username"` - Nickname string `binding:"required" json:"nickname"` - Email string `binding:"required" json:"email"` - Password string `binding:"required" json:"password"` - CaptchaToken string `json:"token"` - RemoteIP string `json:"-"` -} - -type UserCreateRequest struct { - Username string `binding:"required,max=20,min=3,ascii" json:"username"` - Nickname string `binding:"required,min=2" json:"nickname"` - Email string `binding:"required,email" json:"email"` - Password string `binding:"required,min=6" json:"password"` - Group string `json:"group"` -} - -type UserLoginRequest struct { - Username string `binding:"required,ascii" json:"username"` - Password string `binding:"required" json:"password"` -} - -type UserUpdateRequest struct { - ID uint `json:"-"` - Nickname string `binding:"omitempty,min=2" json:"nickname"` - Username string `binding:"omitempty,max=20,min=3" json:"username,omitempty"` - Password string `binding:"omitempty,min=6" json:"password,omitempty"` - Email string `binding:"omitempty,email" json:"email,omitempty"` - Group string `json:"group"` - RemoteIP string `json:"-"` -} - -type UserDeleteRequest struct { - ID uint `json:"-"` -} diff --git a/internal/model/request/webhook.go b/internal/model/request/webhook.go deleted file mode 100644 index a9af4c1e..00000000 --- a/internal/model/request/webhook.go +++ /dev/null @@ -1,22 +0,0 @@ -package request - -type WebhookCreateRequest struct { - URL string `json:"url"` - Type string `json:"type"` - Secret string `json:"secret"` - SSL *bool `json:"ssl"` - GameID *uint `json:"game_id,omitempty"` -} - -type WebhookUpdateRequest struct { - ID uint `json:"id"` - URL string `json:"url"` - Type string `json:"type"` - Secret string `json:"secret"` - SSL *bool `json:"ssl"` - GameID *uint `json:"game_id,omitempty"` -} - -type WebhookDeleteRequest struct { - ID uint `json:"id"` -} diff --git a/internal/model/submission.go b/internal/model/submission.go deleted file mode 100644 index fc356666..00000000 --- a/internal/model/submission.go +++ /dev/null @@ -1,25 +0,0 @@ -package model - -type Submission struct { - ID uint `json:"id"` // The submission's id. As primary key. - Flag string `gorm:"type:varchar(128);not null" json:"flag,omitempty"` // The flag which was submitted for judgement. - Status int `gorm:"not null;default:0" json:"status"` // The status of the submission. (0-meaningless, 1-accepted, 2-incorrect, 3-cheat, 4-invalid(duplicate, etc.)) - UserID uint `gorm:"not null" json:"user_id"` // The user who submitted the flag. - User *User `json:"user"` // The user who submitted the flag. - ChallengeID uint `gorm:"not null;" json:"challenge_id"` // The challenge which is related to this submission. - Challenge *Challenge `json:"challenge"` // The challenge which is related to this submission. - GameChallengeID *uint `gorm:"index;null;default:null" json:"game_challenge_id,omitempty"` // The game_challenge which is related to this submission. - GameChallenge *GameChallenge `gorm:"foreignkey:GameChallengeID;association_foreignkey:ID" json:"game_challenge,omitempty"` // The game_challenge which is related to this submission. - TeamID *uint `gorm:"index;null;default:null" json:"team_id,omitempty"` // The team which submitted the flag. (Must be set when GameID is set) - Team *Team `gorm:"foreignkey:TeamID;association_foreignkey:ID" json:"team,omitempty"` // The team which submitted the flag. - GameID *uint `gorm:"index;null;default:null" json:"game_id,omitempty"` // The game which is related to this submission. (Must be set when TeamID is set) - Game *Game `gorm:"foreignkey:GameID;association_foreignkey:ID" json:"game,omitempty"` // The game which is related to this submission. - Rank int64 `json:"rank"` // The rank of the submission. - Pts int64 `gorm:"-" json:"pts"` // The points of the submission. - CreatedAt int64 `gorm:"autoUpdateTime:milli" json:"created_at,omitempty"` // The submission's creation time. - UpdatedAt int64 `gorm:"autoUpdateTime:milli" json:"updated_at,omitempty"` // The submission's last update time. -} - -func (s *Submission) Simplify() { - s.Flag = "" -} diff --git a/internal/model/team.go b/internal/model/team.go deleted file mode 100644 index 8f394512..00000000 --- a/internal/model/team.go +++ /dev/null @@ -1,78 +0,0 @@ -package model - -import ( - "fmt" - "github.com/elabosak233/cloudsdale/internal/utils" - "gorm.io/gorm" - "os" - "path" -) - -type Team struct { - ID uint `json:"id"` // The team's id. As primary key. - Name string `gorm:"type:varchar(36);not null" json:"name"` // The team's name. - Description string `gorm:"type:text" json:"description"` // The team's description. - Email string `gorm:"type:varchar(64);" json:"email,omitempty"` // The team's email. - Avatar *File `gorm:"-" json:"avatar"` // The team's avatar. - CaptainID uint `gorm:"not null" json:"captain_id,omitempty"` // The captain's id. - Captain *User `json:"captain,omitempty"` // The captain's user. - IsLocked *bool `gorm:"not null;default:false" json:"is_locked,omitempty"` // Whether the team is locked. (true/false) - InviteToken string `gorm:"type:varchar(32);" json:"invite_token,omitempty"` // The team's invite token. - CreatedAt int64 `gorm:"autoUpdateTime:milli" json:"created_at,omitempty"` // The team's creation time. - UpdatedAt int64 `gorm:"autoUpdateTime:milli" json:"updated_at,omitempty"` // The team's last update time. - Users []*User `gorm:"many2many:user_teams;" json:"users,omitempty"` // The team's users. -} - -func (t *Team) AfterFind(db *gorm.DB) (err error) { - p := path.Join(utils.MediaPath, "teams", fmt.Sprintf("%d", t.ID)) - var name string - var size int64 - if files, _err := os.ReadDir(p); _err == nil { - for _, file := range files { - name = file.Name() - info, _ := file.Info() - size = info.Size() - break - } - } - avatar := File{ - Name: name, - Size: size, - } - t.Avatar = &avatar - return nil -} - -func (t *Team) BeforeDelete(db *gorm.DB) (err error) { - db.Table("user_teams").Where("team_id = ?", t.ID).Delete(&UserTeam{}) - db.Table("game_teams").Where("team_id = ?", t.ID).Delete(&GameTeam{}) - return nil -} - -func (t *Team) AfterCreate(db *gorm.DB) (err error) { - db.Table("user_teams").Create(&UserTeam{ - TeamID: t.ID, - UserID: t.CaptainID, - }) - return nil -} - -func (t *Team) AfterUpdate(db *gorm.DB) (err error) { - var userTeams []UserTeam - db.Table("user_teams").Where("team_id = ?", t.ID).Find(&userTeams) - - flag := true - for _, userTeam := range userTeams { - if userTeam.UserID == t.CaptainID { - flag = false - } - } - - if flag { - db.Table("user_teams").Create(&UserTeam{ - TeamID: t.ID, - UserID: t.CaptainID, - }) - } - return nil -} diff --git a/internal/model/user.go b/internal/model/user.go deleted file mode 100644 index 5bd18303..00000000 --- a/internal/model/user.go +++ /dev/null @@ -1,54 +0,0 @@ -package model - -import ( - "fmt" - "github.com/elabosak233/cloudsdale/internal/utils" - "gorm.io/gorm" - "os" - "path" -) - -type User struct { - ID uint `json:"id"` // The user's id. As primary key. - Username string `gorm:"column:username;type:varchar(16);unique;not null;index;" json:"username"` // The user's username. As a unique identifier. - Nickname string `gorm:"column:nickname;type:varchar(36);not null" json:"nickname"` // The user's nickname. Not unique. - Description string `gorm:"column:description;type:text" json:"description"` // The user's description. - Email string `gorm:"column:email;varchar(64);unique;not null" json:"email,omitempty"` // The user's email. - Avatar *File `gorm:"-" json:"avatar"` // The user's avatar. - Group string `gorm:"column:group;varchar(16);not null;" json:"group,omitempty"` // The user's group. - Password string `gorm:"column:password;type:varchar(255);not null" json:"password,omitempty"` // The user's password. Crypt. - RemoteIP string `gorm:"column:remote_ip;type:varchar(32)" json:"remote_ip,omitempty"` // The user's remote ip. - CreatedAt int64 `gorm:"autoUpdateTime:milli" json:"created_at,omitempty"` // The user's creation time. - UpdatedAt int64 `gorm:"autoUpdateTime:milli" json:"updated_at,omitempty"` // The user's last update time. - Teams []*Team `gorm:"many2many:user_teams;" json:"teams,omitempty"` // The user's teams. -} - -func (u *User) Simplify() { - u.Password = "" - u.Description = "" -} - -func (u *User) AfterFind(db *gorm.DB) (err error) { - p := path.Join(utils.MediaPath, "users", fmt.Sprintf("%d", u.ID)) - var name string - var size int64 - if files, _err := os.ReadDir(p); _err == nil { - for _, file := range files { - name = file.Name() - info, _ := file.Info() - size = info.Size() - break - } - } - avatar := File{ - Name: name, - Size: size, - } - u.Avatar = &avatar - return nil -} - -func (u *User) BeforeDelete(db *gorm.DB) (err error) { - db.Table("user_teams").Where("user_id = ?", u.ID).Delete(&UserTeam{}) - return nil -} diff --git a/internal/model/user_team.go b/internal/model/user_team.go deleted file mode 100644 index bee15199..00000000 --- a/internal/model/user_team.go +++ /dev/null @@ -1,7 +0,0 @@ -package model - -type UserTeam struct { - ID uint `json:"id"` - UserID uint `gorm:"uniqueIndex:user_team_idx;" json:"user_id"` - TeamID uint `gorm:"uniqueIndex:user_team_idx;" json:"team_id"` -} diff --git a/internal/model/webhook.go b/internal/model/webhook.go deleted file mode 100644 index e690c163..00000000 --- a/internal/model/webhook.go +++ /dev/null @@ -1,11 +0,0 @@ -package model - -type Webhook struct { - ID uint `json:"id"` - URL string `gorm:"type:varchar(255);" json:"url"` // The payload URL of the webhook. - Type string `gorm:"type:varchar(64);" json:"type"` // The type of the webhook. Such as "application/json" or "application/x-www-form-urlencoded". - Secret string `gorm:"type:varchar(255);" json:"secret"` // The secret of the webhook. - SSL *bool `gorm:"default:false;" json:"ssl"` // The SSL verification of the webhook. - GameID *uint `gorm:"index;not null;" json:"game_id,omitempty"` // The game which this webhook belongs to. - Game *Game `json:"game,omitempty"` // The game which this webhook belongs to. -} diff --git a/internal/repository/category.go b/internal/repository/category.go deleted file mode 100644 index 3cfa5a78..00000000 --- a/internal/repository/category.go +++ /dev/null @@ -1,54 +0,0 @@ -package repository - -import ( - "github.com/elabosak233/cloudsdale/internal/model" - "github.com/elabosak233/cloudsdale/internal/model/request" - "gorm.io/gorm" -) - -type ICategoryRepository interface { - Create(category model.Category) error - Update(category model.Category) error - Find(req request.CategoryFindRequest) ([]model.Category, error) - Delete(id uint) error -} - -type CategoryRepository struct { - db *gorm.DB -} - -func NewCategoryRepository(db *gorm.DB) ICategoryRepository { - return &CategoryRepository{db: db} -} - -func (t *CategoryRepository) Create(category model.Category) (err error) { - result := t.db.Table("categories").Create(&category) - return result.Error -} - -func (t *CategoryRepository) Update(category model.Category) (err error) { - result := t.db.Table("categories").Updates(&category) - return result.Error -} - -func (t *CategoryRepository) Find(req request.CategoryFindRequest) ([]model.Category, error) { - var categories []model.Category - applyFilters := func(db *gorm.DB) *gorm.DB { - if req.ID != 0 { - db = db.Where("id = ?", req.ID) - } - if req.Name != "" { - db = db.Where("name = ?", req.Name) - } - return db - } - result := applyFilters(t.db.Table("categories")).Find(&categories) - return categories, result.Error -} - -func (t *CategoryRepository) Delete(id uint) (err error) { - result := t.db.Table("categories").Delete(&model.Category{ - ID: id, - }) - return result.Error -} diff --git a/internal/repository/challenge.go b/internal/repository/challenge.go deleted file mode 100644 index 8f40fc46..00000000 --- a/internal/repository/challenge.go +++ /dev/null @@ -1,101 +0,0 @@ -package repository - -import ( - "github.com/elabosak233/cloudsdale/internal/model" - "github.com/elabosak233/cloudsdale/internal/model/request" - "gorm.io/gorm" -) - -type IChallengeRepository interface { - Create(challenge model.Challenge) (model.Challenge, error) - Update(challenge model.Challenge) (model.Challenge, error) - Delete(id uint) error - Find(req request.ChallengeFindRequest) ([]model.Challenge, int64, error) -} - -type ChallengeRepository struct { - db *gorm.DB -} - -func NewChallengeRepository(db *gorm.DB) IChallengeRepository { - return &ChallengeRepository{db: db} -} - -func (t *ChallengeRepository) Create(challenge model.Challenge) (model.Challenge, error) { - result := t.db.Table("challenges").Create(&challenge) - return challenge, result.Error -} - -func (t *ChallengeRepository) Delete(id uint) (err error) { - result := t.db.Table("challenges").Delete(&model.Challenge{ID: id}) - return result.Error -} - -func (t *ChallengeRepository) Update(challenge model.Challenge) (model.Challenge, error) { - result := t.db.Table("challenges").Model(&challenge).Updates(&challenge) - return challenge, result.Error -} - -func (t *ChallengeRepository) Find(req request.ChallengeFindRequest) ([]model.Challenge, int64, error) { - var challenges []model.Challenge - applyFilter := func(q *gorm.DB) *gorm.DB { - if req.CategoryID != nil { - q = q.Where("category_id = ?", *(req.CategoryID)) - } - if req.Title != "" { - q = q.Where("title LIKE ?", "%"+req.Title+"%") - } - if req.IsPracticable != nil { - q = q.Where("is_practicable = ?", *(req.IsPracticable)) - } - if req.IsDynamic != nil { - q = q.Where("is_dynamic = ?", *(req.IsDynamic)) - } - if req.Difficulty > 0 { - q = q.Where("difficulty = ?", req.Difficulty) - } - if req.ID != 0 { - q = q.Where("id = ?", req.ID) - } - return q - } - db := applyFilter(t.db.Table("challenges")) - var total int64 = 0 - result := db.Model(&model.Challenge{}).Count(&total) - if req.SortOrder != "" && req.SortKey != "" { - db = db.Order(req.SortKey + " " + req.SortOrder) - } else { - db = db.Order("challenges.id DESC") - } - if req.Page != 0 && req.Size > 0 { - offset := (req.Page - 1) * req.Size - db = db.Offset(offset).Limit(req.Size) - } - - result = db. - Preload("Category", func(db *gorm.DB) *gorm.DB { - return db.Omit("created_at", "updated_at") - }). - Preload("Flags"). - Preload("Ports"). - Preload("Envs"). - Preload("Submissions", func(db *gorm.DB) *gorm.DB { - return db. - Preload("User", func(db *gorm.DB) *gorm.DB { - return db.Select([]string{"id", "username", "nickname", "email"}) - }). - Preload("Team"). - Preload("Game"). - Order("submissions.created_at ASC"). - Where("submissions.status = ?", 2). - Omit("flag") - }). - Preload("Solved", func(db *gorm.DB) *gorm.DB { - return db. - Where("status = ?", 2). - Where("user_id = ?", req.UserID). - Omit("flag") - }). - Find(&challenges) - return challenges, total, result.Error -} diff --git a/internal/repository/env.go b/internal/repository/env.go deleted file mode 100644 index db6ae70a..00000000 --- a/internal/repository/env.go +++ /dev/null @@ -1,24 +0,0 @@ -package repository - -import ( - "github.com/elabosak233/cloudsdale/internal/model" - "gorm.io/gorm" -) - -type IEnvRepository interface { - Create(env model.Env) (model.Env, error) -} - -type EnvRepository struct { - db *gorm.DB -} - -func NewEnvRepository(db *gorm.DB) IEnvRepository { - return &EnvRepository{db: db} -} - -func (t *EnvRepository) Create(env model.Env) (model.Env, error) { - result := t.db.Table("envs"). - Create(&env) - return env, result.Error -} diff --git a/internal/repository/flag.go b/internal/repository/flag.go deleted file mode 100644 index 0228baa8..00000000 --- a/internal/repository/flag.go +++ /dev/null @@ -1,35 +0,0 @@ -package repository - -import ( - "github.com/elabosak233/cloudsdale/internal/model" - "gorm.io/gorm" -) - -type IFlagRepository interface { - Create(flag model.Flag) (model.Flag, error) - Update(flag model.Flag) (model.Flag, error) - Delete(flag model.Flag) error -} - -type FlagRepository struct { - db *gorm.DB -} - -func NewFlagRepository(db *gorm.DB) IFlagRepository { - return &FlagRepository{db: db} -} - -func (t *FlagRepository) Create(flag model.Flag) (model.Flag, error) { - result := t.db.Table("flags").Create(&flag) - return flag, result.Error -} - -func (t *FlagRepository) Update(flag model.Flag) (model.Flag, error) { - result := t.db.Table("flags").Model(&flag).Updates(&flag) - return flag, result.Error -} - -func (t *FlagRepository) Delete(flag model.Flag) error { - result := t.db.Table("flags").Delete(&flag) - return result.Error -} diff --git a/internal/repository/game.go b/internal/repository/game.go deleted file mode 100644 index 93c18f16..00000000 --- a/internal/repository/game.go +++ /dev/null @@ -1,68 +0,0 @@ -package repository - -import ( - "github.com/elabosak233/cloudsdale/internal/model" - "github.com/elabosak233/cloudsdale/internal/model/request" - "gorm.io/gorm" -) - -type IGameRepository interface { - Create(game model.Game) (model.Game, error) - Update(game model.Game) error - Delete(game model.Game) error - Find(req request.GameFindRequest) ([]model.Game, int64, error) -} - -type GameRepository struct { - Db *gorm.DB -} - -func NewGameRepository(Db *gorm.DB) IGameRepository { - return &GameRepository{Db: Db} -} - -func (t *GameRepository) Create(game model.Game) (model.Game, error) { - result := t.Db.Table("games").Create(&game) - return game, result.Error -} - -func (t *GameRepository) Update(game model.Game) error { - result := t.Db.Table("games").Model(&game).Updates(&game) - return result.Error -} - -func (t *GameRepository) Delete(game model.Game) error { - result := t.Db.Table("games").Delete(&game) - return result.Error -} - -func (t *GameRepository) Find(req request.GameFindRequest) ([]model.Game, int64, error) { - var games []model.Game - applyFilters := func(q *gorm.DB) *gorm.DB { - if req.ID != 0 { - q = q.Where("id = ?", req.ID) - } - if req.Title != "" { - q = q.Where("title LIKE ?", "%"+req.Title+"%") - } - if req.IsEnabled != nil { - q = q.Where("is_enabled = ?", *(req.IsEnabled)) - } - return q - } - db := applyFilters(t.Db.Table("games")) - var total int64 = 0 - result := db.Model(&model.Game{}).Count(&total) - if req.SortKey != "" && req.SortOrder != "" { - db = db.Order(req.SortKey + " " + req.SortOrder) - } else { - db = db.Order("games.id DESC") - } - if req.Page != 0 && req.Size > 0 { - offset := (req.Page - 1) * req.Size - db = db.Offset(offset).Limit(req.Size) - } - - result = db.Find(&games) - return games, total, result.Error -} diff --git a/internal/repository/game_challenge.go b/internal/repository/game_challenge.go deleted file mode 100644 index ee5bdb01..00000000 --- a/internal/repository/game_challenge.go +++ /dev/null @@ -1,91 +0,0 @@ -package repository - -import ( - "github.com/elabosak233/cloudsdale/internal/model" - "github.com/elabosak233/cloudsdale/internal/model/request" - "gorm.io/gorm" -) - -type IGameChallengeRepository interface { - Find(req request.GameChallengeFindRequest) ([]model.GameChallenge, error) - Create(gameChallenge model.GameChallenge) error - Update(gameChallenge model.GameChallenge) error - Delete(gameChallenge model.GameChallenge) error -} - -type GameChallengeRepository struct { - db *gorm.DB -} - -func NewGameChallengeRepository(db *gorm.DB) IGameChallengeRepository { - return &GameChallengeRepository{db: db} -} - -func (t *GameChallengeRepository) Find(req request.GameChallengeFindRequest) ([]model.GameChallenge, error) { - var gameChallenges []model.GameChallenge - applyFilters := func(q *gorm.DB) *gorm.DB { - if req.GameID != 0 { - q = q.Where("game_id = ?", req.GameID) - } - if req.ChallengeID != 0 { - q = q.Where("challenge_id = ?", req.ChallengeID) - } - if req.IsEnabled != nil { - q = q.Where("is_enabled = ?", *(req.IsEnabled)) - } - return q - } - db := applyFilters(t.db.Table("game_challenges")) - result := db. - Preload("Challenge", func(db *gorm.DB) *gorm.DB { - return db. - Preload("Category", func(dv *gorm.DB) *gorm.DB { - return dv.Omit("created_at", "updated_at") - }). - Preload("Submissions", func(db *gorm.DB) *gorm.DB { - return db. - Preload("User", func(db *gorm.DB) *gorm.DB { - return db.Select([]string{"id", "username", "nickname", "email"}) - }). - Preload("Team", func(db *gorm.DB) *gorm.DB { - return db.Select([]string{"id", "name", "email"}) - }). - Order("submissions.created_at ASC"). - Where("status = ?", 2). - Where("game_id = ?", req.GameID). - Omit("flag") - }). - Preload("Solved", func(db *gorm.DB) *gorm.DB { - return db. - Where("status = ?", 2). - Where("team_id = ?", req.TeamID). - Omit("flag") - }). - Omit("flags", "images", "is_practicable", "practice_pts", "created_at", "updated_at") - }). - Preload("Game"). - Find(&gameChallenges) - return gameChallenges, result.Error -} - -func (t *GameChallengeRepository) Create(gameChallenge model.GameChallenge) error { - result := t.db.Table("game_challenges").Create(&gameChallenge) - return result.Error -} - -func (t *GameChallengeRepository) Update(gameChallenge model.GameChallenge) error { - result := t.db.Table("game_challenges"). - Where("challenge_id = ?", gameChallenge.ChallengeID). - Where("game_id = ?", gameChallenge.GameID). - Model(&gameChallenge). - Updates(&gameChallenge) - return result.Error -} - -func (t *GameChallengeRepository) Delete(gameChallenge model.GameChallenge) error { - result := t.db.Table("game_challenges"). - Where("game_id = ?", gameChallenge.GameID). - Where("challenge_id = ?", gameChallenge.ChallengeID). - Delete(&gameChallenge) - return result.Error -} diff --git a/internal/repository/game_team.go b/internal/repository/game_team.go deleted file mode 100644 index cd8cda14..00000000 --- a/internal/repository/game_team.go +++ /dev/null @@ -1,65 +0,0 @@ -package repository - -import ( - "github.com/elabosak233/cloudsdale/internal/model" - "gorm.io/gorm" -) - -type IGameTeamRepository interface { - Create(gameTeam model.GameTeam) error - Update(gameTeam model.GameTeam) error - Delete(gameTeam model.GameTeam) error - Find(gameTeam model.GameTeam) ([]model.GameTeam, int64, error) -} - -type GameTeamRepository struct { - db *gorm.DB -} - -func NewGameTeamRepository(db *gorm.DB) IGameTeamRepository { - return &GameTeamRepository{db: db} -} - -func (g *GameTeamRepository) Create(gameTeam model.GameTeam) error { - result := g.db.Table("game_teams").Create(&gameTeam) - return result.Error -} - -func (g *GameTeamRepository) Delete(gameTeam model.GameTeam) error { - result := g.db.Table("game_teams"). - Where("game_id = ?", gameTeam.GameID). - Where("team_id = ?", gameTeam.TeamID). - Delete(&gameTeam) - return result.Error -} - -func (g *GameTeamRepository) Update(gameTeam model.GameTeam) error { - result := g.db.Table("game_teams"). - Where("game_id = ?", gameTeam.GameID). - Where("team_id = ?", gameTeam.TeamID). - Model(&gameTeam). - Updates(&gameTeam) - return result.Error -} - -func (g *GameTeamRepository) Find(gameTeam model.GameTeam) ([]model.GameTeam, int64, error) { - var gameTeams []model.GameTeam - db := g.db.Table("game_teams"). - Where(&gameTeam) - var total int64 = 0 - result := db.Model(&model.GameTeam{}).Count(&total) - - result = db.Preload("Team", func(db *gorm.DB) *gorm.DB { - return db.Preload("Captain", func(db *gorm.DB) *gorm.DB { - return db.Select([]string{"id", "nickname", "username", "email"}) - }).Preload("Users", func(db *gorm.DB) *gorm.DB { - return db.Select([]string{"id", "nickname", "username", "email"}) - }) - }). - Preload("Game", func(db *gorm.DB) *gorm.DB { - return db.Select([]string{"id", "title", "started_at", "ended_at"}) - }). - Find(&gameTeams) - - return gameTeams, total, result.Error -} diff --git a/internal/repository/nat.go b/internal/repository/nat.go deleted file mode 100644 index 9f83f097..00000000 --- a/internal/repository/nat.go +++ /dev/null @@ -1,23 +0,0 @@ -package repository - -import ( - "github.com/elabosak233/cloudsdale/internal/model" - "gorm.io/gorm" -) - -type INatRepository interface { - Create(nat model.Nat) (model.Nat, error) -} - -type NatRepository struct { - db *gorm.DB -} - -func NewNatRepository(db *gorm.DB) INatRepository { - return &NatRepository{db: db} -} - -func (t *NatRepository) Create(nat model.Nat) (model.Nat, error) { - result := t.db.Table("nats").Create(&nat) - return nat, result.Error -} diff --git a/internal/repository/notice.go b/internal/repository/notice.go deleted file mode 100644 index 7ac1b527..00000000 --- a/internal/repository/notice.go +++ /dev/null @@ -1,69 +0,0 @@ -package repository - -import ( - "github.com/elabosak233/cloudsdale/internal/model" - "github.com/elabosak233/cloudsdale/internal/model/request" - "gorm.io/gorm" -) - -type INoticeRepository interface { - Find(req request.NoticeFindRequest) ([]model.Notice, int64, error) - Create(notice model.Notice) (model.Notice, error) - Update(notice model.Notice) (model.Notice, error) - Delete(notice model.Notice) error -} - -type NoticeRepository struct { - db *gorm.DB -} - -func NewNoticeRepository(db *gorm.DB) INoticeRepository { - return &NoticeRepository{db: db} -} - -func (t *NoticeRepository) Find(req request.NoticeFindRequest) ([]model.Notice, int64, error) { - var notices []model.Notice - applyFilters := func(q *gorm.DB) *gorm.DB { - if req.ID != 0 { - q = q.Where("id = ?", req.ID) - } - if req.GameID != 0 { - q = q.Where("game_id = ?", req.GameID) - } - if req.Type != "" { - q = q.Where("type = ?", req.Type) - } - return q - } - db := applyFilters(t.db.Table("notices")) - var total int64 = 0 - result := db.Model(&model.Notice{}).Count(&total) - db = db.Order("notices.id DESC") - result = db. - Preload("User", func(db *gorm.DB) *gorm.DB { - return db.Select([]string{"id", "username", "nickname", "email"}) - }). - Preload("Team", func(db *gorm.DB) *gorm.DB { - return db.Select([]string{"id", "name", "email"}) - }). - Preload("Challenge", func(db *gorm.DB) *gorm.DB { - return db.Select([]string{"id", "title"}) - }). - Find(¬ices) - return notices, total, result.Error -} - -func (t *NoticeRepository) Create(notice model.Notice) (model.Notice, error) { - result := t.db.Table("notices").Create(¬ice) - return notice, result.Error -} - -func (t *NoticeRepository) Update(notice model.Notice) (model.Notice, error) { - result := t.db.Table("notices").Model(¬ice).Updates(¬ice) - return notice, result.Error -} - -func (t *NoticeRepository) Delete(notice model.Notice) error { - result := t.db.Table("notices").Delete(¬ice) - return result.Error -} diff --git a/internal/repository/pod.go b/internal/repository/pod.go deleted file mode 100644 index d118ea2d..00000000 --- a/internal/repository/pod.go +++ /dev/null @@ -1,79 +0,0 @@ -package repository - -import ( - "github.com/elabosak233/cloudsdale/internal/model" - "github.com/elabosak233/cloudsdale/internal/model/request" - "gorm.io/gorm" - "time" -) - -type IPodRepository interface { - Create(pod model.Pod) (model.Pod, error) - Update(pod model.Pod) error - Find(req request.PodFindRequest) ([]model.Pod, int64, error) -} - -type PodRepository struct { - db *gorm.DB -} - -func NewPodRepository(db *gorm.DB) IPodRepository { - return &PodRepository{db: db} -} - -func (t *PodRepository) Create(pod model.Pod) (model.Pod, error) { - result := t.db.Table("pods").Create(&pod) - return pod, result.Error -} - -func (t *PodRepository) Update(pod model.Pod) error { - result := t.db.Table("pods").Model(&pod).Updates(&pod) - return result.Error -} - -func (t *PodRepository) Find(req request.PodFindRequest) ([]model.Pod, int64, error) { - var pods []model.Pod - applyFilter := func(q *gorm.DB) *gorm.DB { - if req.ID != 0 { - q = q.Where("id = ?", req.ID) - } - if req.ChallengeID != 0 { - q = q.Where("challenge_id = ?", req.ChallengeID) - } - if req.UserID != nil { - q = q.Where("user_id = ?", req.UserID) - } - if req.TeamID != nil { - q = q.Where("team_id = ?", *(req.TeamID)) - } - if req.GameID != nil { - q = q.Where("game_id = ?", *(req.GameID)) - } - if req.IsAvailable != nil { - if *(req.IsAvailable) == false { - q = q.Where("removed_at < ?", time.Now().Unix()) - } else if *(req.IsAvailable) == true { - q = q.Where("removed_at > ?", time.Now().Unix()) - } - } - return q - } - db := applyFilter(t.db.Table("pods")) - var total int64 = 0 - result := db.Model(&model.Pod{}).Count(&total) - if req.Page != 0 && req.Size != 0 { - offset := (req.Page - 1) * req.Size - db = db.Offset(offset).Limit(req.Size) - } - - result = db. - Preload("Challenge", func(db *gorm.DB) *gorm.DB { - return db. - Preload("Ports"). - Preload("Envs"). - Select([]string{"id", "title"}) - }). - Preload("Nats"). - Find(&pods) - return pods, total, result.Error -} diff --git a/internal/repository/port.go b/internal/repository/port.go deleted file mode 100644 index bbc89e0f..00000000 --- a/internal/repository/port.go +++ /dev/null @@ -1,35 +0,0 @@ -package repository - -import ( - "github.com/elabosak233/cloudsdale/internal/model" - "gorm.io/gorm" -) - -type IPortRepository interface { - Create(port model.Port) (model.Port, error) - Update(port model.Port) (model.Port, error) - Delete(port model.Port) error -} - -type PortRepository struct { - db *gorm.DB -} - -func NewPortRepository(db *gorm.DB) IPortRepository { - return &PortRepository{db: db} -} - -func (t *PortRepository) Create(port model.Port) (model.Port, error) { - result := t.db.Table("ports").Create(&port) - return port, result.Error -} - -func (t *PortRepository) Update(port model.Port) (model.Port, error) { - result := t.db.Table("ports").Model(&port).Updates(&port) - return port, result.Error -} - -func (t *PortRepository) Delete(port model.Port) error { - result := t.db.Table("ports").Delete(&port) - return result.Error -} diff --git a/internal/repository/repository.go b/internal/repository/repository.go deleted file mode 100644 index c5ff2002..00000000 --- a/internal/repository/repository.go +++ /dev/null @@ -1,64 +0,0 @@ -package repository - -import ( - "github.com/elabosak233/cloudsdale/internal/app/db" - "go.uber.org/zap" - "sync" -) - -var ( - r *Repository = nil - onceRepository sync.Once -) - -type Repository struct { - UserRepository IUserRepository - ChallengeRepository IChallengeRepository - TeamRepository ITeamRepository - SubmissionRepository ISubmissionRepository - PodRepository IPodRepository - GameRepository IGameRepository - UserTeamRepository IUserTeamRepository - GameChallengeRepository IGameChallengeRepository - CategoryRepository ICategoryRepository - FlagRepository IFlagRepository - PortRepository IPortRepository - NatRepository INatRepository - EnvRepository IEnvRepository - GameTeamRepository IGameTeamRepository - NoticeRepository INoticeRepository - WebhookRepository IWebhookRepository -} - -func R() *Repository { - if r == nil { - InitRepository() - } - return r -} - -func InitRepository() { - onceRepository.Do(func() { - d := db.Db() - - r = &Repository{ - UserRepository: NewUserRepository(d), - ChallengeRepository: NewChallengeRepository(d), - TeamRepository: NewTeamRepository(d), - SubmissionRepository: NewSubmissionRepository(d), - PodRepository: NewPodRepository(d), - GameRepository: NewGameRepository(d), - UserTeamRepository: NewUserTeamRepository(d), - GameChallengeRepository: NewGameChallengeRepository(d), - CategoryRepository: NewCategoryRepository(d), - FlagRepository: NewFlagRepository(d), - PortRepository: NewPortRepository(d), - NatRepository: NewNatRepository(d), - EnvRepository: NewEnvRepository(d), - GameTeamRepository: NewGameTeamRepository(d), - NoticeRepository: NewNoticeRepository(d), - WebhookRepository: NewWebhookRepository(d), - } - }) - zap.L().Info("Repository layer inits successfully.") -} diff --git a/internal/repository/submission.go b/internal/repository/submission.go deleted file mode 100644 index c46638bd..00000000 --- a/internal/repository/submission.go +++ /dev/null @@ -1,87 +0,0 @@ -package repository - -import ( - "github.com/elabosak233/cloudsdale/internal/model" - "github.com/elabosak233/cloudsdale/internal/model/request" - "gorm.io/gorm" -) - -type ISubmissionRepository interface { - Create(submission model.Submission) error - Delete(id uint) error - Find(req request.SubmissionFindRequest) ([]model.Submission, int64, error) -} - -type SubmissionRepository struct { - db *gorm.DB -} - -func NewSubmissionRepository(db *gorm.DB) ISubmissionRepository { - return &SubmissionRepository{db: db} -} - -func (t *SubmissionRepository) Create(submission model.Submission) error { - result := t.db.Table("submissions").Create(&submission) - return result.Error -} - -func (t *SubmissionRepository) Delete(id uint) error { - result := t.db.Table("submissions").Delete(&model.Submission{ID: id}) - return result.Error -} - -func (t *SubmissionRepository) Find(req request.SubmissionFindRequest) ([]model.Submission, int64, error) { - var submissions []model.Submission - applyFilters := func(q *gorm.DB) *gorm.DB { - if req.UserID != 0 && req.TeamID == nil && req.GameID == nil { - q = q.Where("user_id = ?", req.UserID) - } - if req.ChallengeID != 0 { - q = q.Where("challenge_id = ?", req.ChallengeID) - } - if req.TeamID != nil { - q = q.Where("team_id = ?", *(req.TeamID)) - } - if req.GameID != nil { - q = q.Where("game_id = ?", *(req.GameID)) - } - if req.Status != 0 { - q = q.Where("status = ?", req.Status) - } - return q - } - db := applyFilters(t.db.Table("submissions")) - var total int64 = 0 - result := db.Model(&model.Submission{}).Count(&total) - if req.SortKey != "" && req.SortOrder != "" { - db = db.Order(req.SortKey + " " + req.SortOrder) - } else { - db = db.Order("submissions.id DESC") - } - if req.Page != 0 && req.Size > 0 { - offset := (req.Page - 1) * req.Size - db = db.Offset(offset).Limit(req.Size) - } - - db = db.Joins("INNER JOIN users ON submissions.user_id = users.id"). - Joins("INNER JOIN challenges ON submissions.challenge_id = challenges.id") - - result = db. - Preload("User", func(db *gorm.DB) *gorm.DB { - return db.Select([]string{"id", "username", "nickname", "email"}) - }). - Preload("Challenge", func(db *gorm.DB) *gorm.DB { - return db. - Preload("Category"). - Select([]string{"id", "title", "category_id", "difficulty", "practice_pts"}) - }). - Preload("GameChallenge"). - Preload("Team", func(db *gorm.DB) *gorm.DB { - return db.Select([]string{"id", "name", "email"}) - }). - Preload("Game", func(db *gorm.DB) *gorm.DB { - return db.Select([]string{"id", "title", "bio", "first_blood_reward_ratio", "second_blood_reward_ratio", "third_blood_reward_ratio"}) - }). - Find(&submissions) - return submissions, total, result.Error -} diff --git a/internal/repository/team.go b/internal/repository/team.go deleted file mode 100644 index 1dd484e1..00000000 --- a/internal/repository/team.go +++ /dev/null @@ -1,92 +0,0 @@ -package repository - -import ( - "github.com/elabosak233/cloudsdale/internal/model" - "github.com/elabosak233/cloudsdale/internal/model/request" - "gorm.io/gorm" -) - -type ITeamRepository interface { - Create(team model.Team) (model.Team, error) - Update(team model.Team) error - Delete(id uint) error - Find(req request.TeamFindRequest) ([]model.Team, int64, error) - FindByID(id uint) (model.Team, error) -} - -type TeamRepository struct { - db *gorm.DB -} - -func NewTeamRepository(db *gorm.DB) ITeamRepository { - return &TeamRepository{db: db} -} - -func (t *TeamRepository) Create(team model.Team) (model.Team, error) { - result := t.db.Table("teams").Create(&team) - return team, result.Error -} - -func (t *TeamRepository) Update(team model.Team) error { - result := t.db.Table("teams").Model(&team).Updates(&team) - return result.Error -} - -func (t *TeamRepository) Delete(id uint) error { - result := t.db.Table("teams").Where("id = ?", id).Delete(&model.Team{ - ID: id, - }) - return result.Error -} - -func (t *TeamRepository) Find(req request.TeamFindRequest) ([]model.Team, int64, error) { - var teams []model.Team - applyFilters := func(q *gorm.DB) *gorm.DB { - if req.ID != 0 { - q = q.Where("id = ?", req.ID) - } - if req.Name != "" { - q = q.Where("name LIKE ?", "%"+req.Name+"%") - } - if req.CaptainID != 0 { - q = q.Where("captain_id = ?", req.CaptainID) - } - if req.GameID != nil { - q = q.Joins("INNER JOIN game_teams ON game_teams.team_id = teams.id"). - Where("game_teams.game_id = ?", *(req.GameID)) - } - if req.UserID != nil { - q = q.Joins("INNER JOIN user_teams ON user_teams.team_id = teams.id"). - Where("user_teams.user_id = ?", *(req.UserID)) - } - return q - } - db := applyFilters(t.db.Table("teams")) - var total int64 = 0 - result := db.Model(&model.Team{}).Count(&total) - if req.SortKey != "" && req.SortOrder != "" { - db = db.Order(req.SortKey + " " + req.SortOrder) - } else { - db = db.Order("teams.id ASC") - } - if req.Page != 0 && req.Size > 0 { - offset := (req.Page - 1) * req.Size - db = db.Offset(offset).Limit(req.Size) - } - - result = db. - Preload("Captain", func(db *gorm.DB) *gorm.DB { - return db.Select([]string{"id", "nickname", "username", "email"}) - }). - Preload("Users", func(db *gorm.DB) *gorm.DB { - return db.Select([]string{"id", "nickname", "username", "email"}) - }). - Find(&teams) - return teams, total, result.Error -} - -func (t *TeamRepository) FindByID(id uint) (model.Team, error) { - var team model.Team - result := t.db.Table("teams").Where("id = ?", id).First(&team) - return team, result.Error -} diff --git a/internal/repository/user.go b/internal/repository/user.go deleted file mode 100644 index ed2c0945..00000000 --- a/internal/repository/user.go +++ /dev/null @@ -1,97 +0,0 @@ -package repository - -import ( - "github.com/elabosak233/cloudsdale/internal/model" - "github.com/elabosak233/cloudsdale/internal/model/request" - "gorm.io/gorm" -) - -type IUserRepository interface { - Create(user model.User) error - Update(user model.User) error - Delete(id uint) error - FindByID(id uint) (model.User, error) - FindByUsername(username string) (model.User, error) - Find(req request.UserFindRequest) ([]model.User, int64, error) -} - -type UserRepository struct { - db *gorm.DB -} - -func NewUserRepository(db *gorm.DB) IUserRepository { - return &UserRepository{db: db} -} - -func (t *UserRepository) Create(user model.User) error { - result := t.db.Table("users").Create(&user) - return result.Error -} - -func (t *UserRepository) Delete(id uint) error { - result := t.db.Table("users").Where("id = ?", id).Delete(&model.User{ - ID: id, - }) - return result.Error -} - -func (t *UserRepository) Update(user model.User) error { - result := t.db.Table("users").Model(&user).Updates(&user) - return result.Error -} - -func (t *UserRepository) Find(req request.UserFindRequest) ([]model.User, int64, error) { - var users []model.User - applyFilter := func(q *gorm.DB) *gorm.DB { - if req.ID != 0 { - q = q.Where("id = ?", req.ID) - } - if req.Username != "" { - q = q.Where("username = ?", req.Username) - } - if req.Group != "" { - q = q.Where("group = ?", req.Group) - } - if req.Email != "" { - q = q.Where("email LIKE ?", "%"+req.Email+"%") - } - if req.Name != "" { - q = q.Where("nickname LIKE ? OR username LIKE ?", "%"+req.Name+"%", "%"+req.Name+"%") - } - return q - } - db := applyFilter(t.db.Table("users")) - var total int64 = 0 - result := db.Model(&model.User{}).Count(&total) - if req.SortKey != "" && req.SortOrder != "" { - db = db.Order(req.SortKey + " " + req.SortOrder) - } else { - db = db.Order("users.id ASC") - } - if req.Page != 0 && req.Size > 0 { - offset := (req.Page - 1) * req.Size - db = db.Offset(offset).Limit(req.Size) - } - result = db. - Preload("Teams"). - Find(&users) - return users, total, result.Error -} - -func (t *UserRepository) FindByID(id uint) (model.User, error) { - var user model.User - result := t.db.Table("users"). - Where("id = ?", id). - Preload("Teams"). - First(&user) - return user, result.Error -} - -func (t *UserRepository) FindByUsername(username string) (model.User, error) { - var user model.User - result := t.db.Table("users"). - Where("username = ?", username). - Preload("Teams"). - First(&user) - return user, result.Error -} diff --git a/internal/repository/user_team.go b/internal/repository/user_team.go deleted file mode 100644 index 32b6b2e2..00000000 --- a/internal/repository/user_team.go +++ /dev/null @@ -1,32 +0,0 @@ -package repository - -import ( - "github.com/elabosak233/cloudsdale/internal/model" - "gorm.io/gorm" -) - -type IUserTeamRepository interface { - Create(userTeam model.UserTeam) error - Delete(userTeam model.UserTeam) error -} - -type UserTeamRepository struct { - db *gorm.DB -} - -func NewUserTeamRepository(db *gorm.DB) IUserTeamRepository { - return &UserTeamRepository{db: db} -} - -func (t *UserTeamRepository) Create(userTeam model.UserTeam) error { - result := t.db.Table("user_teams").Create(&userTeam) - return result.Error -} - -func (t *UserTeamRepository) Delete(userTeam model.UserTeam) error { - result := t.db.Table("user_teams"). - Where("user_id = ?", userTeam.UserID). - Where("team_id = ?", userTeam.TeamID). - Delete(&userTeam) - return result.Error -} diff --git a/internal/repository/webhook.go b/internal/repository/webhook.go deleted file mode 100644 index 933198b5..00000000 --- a/internal/repository/webhook.go +++ /dev/null @@ -1,35 +0,0 @@ -package repository - -import ( - "github.com/elabosak233/cloudsdale/internal/model" - "gorm.io/gorm" -) - -type IWebhookRepository interface { - Create(webhook model.Webhook) (model.Webhook, error) - Update(webhook model.Webhook) error - Delete(webhook model.Webhook) error -} - -type WebhookRepository struct { - db *gorm.DB -} - -func NewWebhookRepository(db *gorm.DB) IWebhookRepository { - return &WebhookRepository{db: db} -} - -func (t *WebhookRepository) Create(webhook model.Webhook) (model.Webhook, error) { - result := t.db.Table("webhooks").Create(&webhook) - return webhook, result.Error -} - -func (t *WebhookRepository) Update(webhook model.Webhook) error { - result := t.db.Table("webhooks").Model(&webhook).Updates(&webhook) - return result.Error -} - -func (t *WebhookRepository) Delete(webhook model.Webhook) error { - result := t.db.Table("webhooks").Where("id = ?", webhook.ID).Delete(&webhook) - return result.Error -} diff --git a/internal/router/category.go b/internal/router/category.go deleted file mode 100644 index 5efa0605..00000000 --- a/internal/router/category.go +++ /dev/null @@ -1,29 +0,0 @@ -package router - -import ( - "github.com/elabosak233/cloudsdale/internal/controller" - "github.com/gin-gonic/gin" -) - -type ICategoryRouter interface { - Register() -} - -type CategoryRouter struct { - router *gin.RouterGroup - controller controller.ICategoryController -} - -func NewCategoryRouter(categoryRouter *gin.RouterGroup, categoryController controller.ICategoryController) ICategoryRouter { - return &CategoryRouter{ - router: categoryRouter, - controller: categoryController, - } -} - -func (c *CategoryRouter) Register() { - c.router.POST("/", c.controller.Create) - c.router.PUT("/:id", c.controller.Update) - c.router.GET("/", c.controller.Find) - c.router.DELETE("/:id", c.controller.Delete) -} diff --git a/internal/router/challenge.go b/internal/router/challenge.go deleted file mode 100644 index 03506b32..00000000 --- a/internal/router/challenge.go +++ /dev/null @@ -1,54 +0,0 @@ -package router - -import ( - "github.com/elabosak233/cloudsdale/internal/controller" - "github.com/elabosak233/cloudsdale/internal/model" - "github.com/elabosak233/cloudsdale/internal/utils" - "github.com/elabosak233/cloudsdale/internal/utils/convertor" - "github.com/gin-gonic/gin" -) - -type IChallengeRouter interface { - Register() -} - -type ChallengeRouter struct { - router *gin.RouterGroup - controller controller.IChallengeController -} - -func NewChallengeRouter(challengeRouter *gin.RouterGroup, challengeController controller.IChallengeController) IChallengeRouter { - return &ChallengeRouter{ - router: challengeRouter, - controller: challengeController, - } -} - -func (c *ChallengeRouter) Register() { - c.router.GET("/", c.PreProcess(), c.controller.Find) - c.router.POST("/", c.controller.Create) - c.router.PUT("/:id", c.controller.Update) - c.router.DELETE("/:id", c.controller.Delete) - c.router.POST("/:id/flags", c.controller.CreateFlag) - c.router.PUT("/:id/flags/:flag_id", c.controller.UpdateFlag) - c.router.DELETE("/:id/flags/:flag_id", c.controller.DeleteFlag) - c.router.POST("/:id/attachment", c.controller.SaveAttachment) - c.router.DELETE("/:id/attachment", c.controller.DeleteAttachment) -} - -func (c *ChallengeRouter) PreProcess() gin.HandlerFunc { - return func(ctx *gin.Context) { - user := ctx.MustGet("user").(*model.User) - if user.Group == "admin" { - ctx.Set("is_detailed", convertor.ToBoolD(ctx.Query("is_detailed"), false)) - } else { - ctx.Set("is_detailed", false) - } - if user.Group == "admin" { - ctx.Set("is_practicable", convertor.ToBoolP(ctx.Query("is_practicable"))) - } else { - ctx.Set("is_practicable", &utils.True) - } - ctx.Next() - } -} diff --git a/internal/router/config.go b/internal/router/config.go deleted file mode 100644 index d5451814..00000000 --- a/internal/router/config.go +++ /dev/null @@ -1,28 +0,0 @@ -package router - -import ( - "github.com/elabosak233/cloudsdale/internal/controller" - "github.com/gin-gonic/gin" -) - -type IConfigRouter interface { - Register() -} - -type ConfigRouter struct { - router *gin.RouterGroup - controller controller.IConfigController -} - -func NewConfigRouter(configRouter *gin.RouterGroup, configController controller.IConfigController) IConfigRouter { - return &ConfigRouter{ - router: configRouter, - controller: configController, - } -} - -func (c *ConfigRouter) Register() { - c.router.GET("/", c.controller.Find) - c.router.PUT("/", c.controller.Update) - c.router.GET("/captcha", c.controller.FindCaptcha) -} diff --git a/internal/router/game.go b/internal/router/game.go deleted file mode 100644 index 336a1094..00000000 --- a/internal/router/game.go +++ /dev/null @@ -1,55 +0,0 @@ -package router - -import ( - "github.com/elabosak233/cloudsdale/internal/controller" - "github.com/elabosak233/cloudsdale/internal/model" - "github.com/gin-gonic/gin" -) - -type IGameRouter interface { - Register() -} - -type GameRouter struct { - router *gin.RouterGroup - controller controller.IGameController -} - -func NewGameRouter(gameRouter *gin.RouterGroup, gameController controller.IGameController) IGameRouter { - return &GameRouter{ - router: gameRouter, - controller: gameController, - } -} - -func (g *GameRouter) Register() { - g.router.GET("/", g.PreProcess(), g.controller.Find) - g.router.POST("/", g.controller.Create) - g.router.PUT("/:id", g.controller.Update) - g.router.DELETE("/:id", g.controller.Delete) - g.router.GET("/:id/challenges", g.controller.FindChallenge) - g.router.POST("/:id/challenges", g.controller.CreateChallenge) - g.router.PUT("/:id/challenges/:challenge_id", g.controller.UpdateChallenge) - g.router.DELETE("/:id/challenges/:challenge_id", g.controller.DeleteChallenge) - g.router.GET("/:id/teams", g.controller.FindTeam) - g.router.POST("/:id/teams", g.controller.CreateTeam) - g.router.PUT("/:id/teams/:team_id", g.controller.UpdateTeam) - g.router.DELETE("/:id/teams/:team_id", g.controller.DeleteTeam) - g.router.GET("/:id/notices", g.controller.FindNotice) - g.router.POST("/:id/notices", g.controller.CreateNotice) - g.router.PUT("/:id/notices/:notice_id", g.controller.UpdateNotice) - g.router.DELETE("/:id/notices/:notice_id", g.controller.DeleteNotice) - g.router.GET("/:id/broadcast", g.controller.BroadCast) - g.router.POST("/:id/poster", g.controller.SavePoster) - g.router.DELETE("/:id/poster", g.controller.DeletePoster) -} - -func (g *GameRouter) PreProcess() gin.HandlerFunc { - return func(ctx *gin.Context) { - user := ctx.MustGet("user").(*model.User) - if !(user.Group == "admin") { - ctx.Set("is_enabled", true) - } - ctx.Next() - } -} diff --git a/internal/router/media.go b/internal/router/media.go deleted file mode 100644 index e295b98c..00000000 --- a/internal/router/media.go +++ /dev/null @@ -1,26 +0,0 @@ -package router - -import ( - "github.com/elabosak233/cloudsdale/internal/controller" - "github.com/gin-gonic/gin" -) - -type IMediaRouter interface { - Register() -} - -type MediaRouter struct { - router *gin.RouterGroup - controller controller.IMediaController -} - -func NewMediaRouter(mediaRouter *gin.RouterGroup, mediaController controller.IMediaController) IMediaRouter { - return &MediaRouter{ - router: mediaRouter, - controller: mediaController, - } -} - -func (m *MediaRouter) Register() { - m.router.GET("/*path", m.controller.GetFile) -} diff --git a/internal/router/pod.go b/internal/router/pod.go deleted file mode 100644 index c0624f63..00000000 --- a/internal/router/pod.go +++ /dev/null @@ -1,29 +0,0 @@ -package router - -import ( - "github.com/elabosak233/cloudsdale/internal/controller" - "github.com/gin-gonic/gin" -) - -type IPodRouter interface { - Register() -} - -type PodRouter struct { - router *gin.RouterGroup - controller controller.IPodController -} - -func NewPodRouter(podRouter *gin.RouterGroup, podController controller.IPodController) IPodRouter { - return &PodRouter{ - router: podRouter, - controller: podController, - } -} - -func (p *PodRouter) Register() { - p.router.GET("/", p.controller.Find) - p.router.POST("/", p.controller.Create) - p.router.DELETE("/:id", p.controller.Remove) - p.router.PUT("/:id", p.controller.Renew) -} diff --git a/internal/router/proxy.go b/internal/router/proxy.go deleted file mode 100644 index fe5282ce..00000000 --- a/internal/router/proxy.go +++ /dev/null @@ -1,26 +0,0 @@ -package router - -import ( - "github.com/elabosak233/cloudsdale/internal/controller" - "github.com/gin-gonic/gin" -) - -type IProxyRouter interface { - Register() -} - -type ProxyRouter struct { - router *gin.RouterGroup - controller controller.IProxyController -} - -func NewProxyRouter(proxyRouter *gin.RouterGroup, proxyController controller.IProxyController) IProxyRouter { - return &ProxyRouter{ - router: proxyRouter, - controller: proxyController, - } -} - -func (p *ProxyRouter) Register() { - p.router.GET("/:id", p.controller.Connect) -} diff --git a/internal/router/router.go b/internal/router/router.go deleted file mode 100644 index 4304e9da..00000000 --- a/internal/router/router.go +++ /dev/null @@ -1,44 +0,0 @@ -package router - -import ( - "github.com/elabosak233/cloudsdale/internal/controller" - ginI18n "github.com/gin-contrib/i18n" - "github.com/gin-gonic/gin" - "net/http" -) - -var ( - r *Router = nil -) - -type Router struct { - router *gin.RouterGroup - controller *controller.Controller -} - -func InitRouter( - router *gin.RouterGroup, -) { - r = &Router{ - router: router, - controller: controller.C(), - } - - r.router.GET("/", func(ctx *gin.Context) { - ctx.JSON(http.StatusOK, gin.H{ - "code": http.StatusOK, - "msg": ginI18n.MustGetMessage(ctx, "welcome"), - }) - }) - - NewUserRouter(r.router.Group("/users"), r.controller.UserController).Register() - NewChallengeRouter(r.router.Group("/challenges"), r.controller.ChallengeController).Register() - NewPodRouter(r.router.Group("/pods"), r.controller.PodController).Register() - NewConfigRouter(r.router.Group("/configs"), r.controller.ConfigController).Register() - NewMediaRouter(r.router.Group("/media"), r.controller.MediaController).Register() - NewTeamRouter(r.router.Group("/teams"), r.controller.TeamController).Register() - NewSubmissionRouter(r.router.Group("/submissions"), r.controller.SubmissionController).Register() - NewGameRouter(r.router.Group("/games"), r.controller.GameController).Register() - NewCategoryRouter(r.router.Group("/categories"), r.controller.CategoryController).Register() - NewProxyRouter(r.router.Group("/proxies"), r.controller.ProxyController).Register() -} diff --git a/internal/router/submission.go b/internal/router/submission.go deleted file mode 100644 index c6572ba5..00000000 --- a/internal/router/submission.go +++ /dev/null @@ -1,44 +0,0 @@ -package router - -import ( - "github.com/elabosak233/cloudsdale/internal/controller" - "github.com/elabosak233/cloudsdale/internal/model" - "github.com/elabosak233/cloudsdale/internal/utils/convertor" - "github.com/gin-gonic/gin" -) - -type ISubmissionRouter interface { - Register() -} - -type SubmissionRouter struct { - router *gin.RouterGroup - controller controller.ISubmissionController -} - -func NewSubmissionRouter(submissionRouter *gin.RouterGroup, submissionController controller.ISubmissionController) ISubmissionRouter { - return &SubmissionRouter{ - router: submissionRouter, - controller: submissionController, - } -} - -func (s *SubmissionRouter) Register() { - s.router.GET("/", s.PreProcess(), s.controller.Find) - s.router.POST("/", s.controller.Create) - s.router.DELETE("/:id", s.controller.Delete) -} - -func (s *SubmissionRouter) PreProcess() gin.HandlerFunc { - return func(ctx *gin.Context) { - if convertor.ToBoolD(ctx.Query("is_detailed"), false) { - user := ctx.MustGet("user").(*model.User) - if user.Group == "admin" { - ctx.Set("is_detailed", true) - } - } else { - ctx.Set("is_detailed", false) - } - ctx.Next() - } -} diff --git a/internal/router/team.go b/internal/router/team.go deleted file mode 100644 index 7e9fb13d..00000000 --- a/internal/router/team.go +++ /dev/null @@ -1,56 +0,0 @@ -package router - -import ( - "github.com/elabosak233/cloudsdale/internal/controller" - "github.com/elabosak233/cloudsdale/internal/model" - "github.com/elabosak233/cloudsdale/internal/service" - "github.com/elabosak233/cloudsdale/internal/utils/convertor" - "github.com/gin-gonic/gin" - "net/http" -) - -type ITeamRouter interface { - Register() -} - -type TeamRouter struct { - router *gin.RouterGroup - controller controller.ITeamController -} - -func NewTeamRouter(teamRouter *gin.RouterGroup, teamController controller.ITeamController) ITeamRouter { - return &TeamRouter{ - router: teamRouter, - controller: teamController, - } -} - -func (t *TeamRouter) Register() { - t.router.GET("/", t.controller.Find) - t.router.POST("/", t.controller.Create) - t.router.DELETE("/:id", t.CanModifyTeam(), t.controller.Delete) - t.router.PUT("/:id", t.CanModifyTeam(), t.controller.Update) - t.router.POST("/:id/users", t.controller.CreateUser) - t.router.DELETE("/:id/users/:user_id", t.CanModifyTeam(), t.controller.DeleteUser) - t.router.GET("/:id/invite", t.CanModifyTeam(), t.controller.GetInviteToken) - t.router.PUT("/:id/invite", t.CanModifyTeam(), t.controller.UpdateInviteToken) - t.router.POST("/:id/join", t.controller.Join) - t.router.POST("/:id/leave", t.controller.Leave) - t.router.POST("/:id/avatar", t.CanModifyTeam(), t.controller.SaveAvatar) - t.router.DELETE("/:id/avatar", t.CanModifyTeam(), t.controller.DeleteAvatar) -} - -func (t *TeamRouter) CanModifyTeam() gin.HandlerFunc { - return func(ctx *gin.Context) { - user := ctx.MustGet("user").(*model.User) - - if ok := service.S().AuthService.CanModifyTeam(user, convertor.ToUintD(ctx.Param("id"), 0)); !ok { - ctx.JSON(http.StatusForbidden, gin.H{ - "code": http.StatusForbidden, - }) - ctx.Abort() - } - - ctx.Next() - } -} diff --git a/internal/router/user.go b/internal/router/user.go deleted file mode 100644 index 13f79c76..00000000 --- a/internal/router/user.go +++ /dev/null @@ -1,51 +0,0 @@ -package router - -import ( - "github.com/elabosak233/cloudsdale/internal/controller" - "github.com/elabosak233/cloudsdale/internal/model" - "github.com/elabosak233/cloudsdale/internal/service" - "github.com/elabosak233/cloudsdale/internal/utils/convertor" - "github.com/gin-gonic/gin" - "net/http" -) - -type IUserRouter interface { - Register() -} - -type UserRouter struct { - router *gin.RouterGroup - controller controller.IUserController -} - -func NewUserRouter(userRouter *gin.RouterGroup, userController controller.IUserController) IUserRouter { - return &UserRouter{ - router: userRouter, - controller: userController, - } -} - -func (u *UserRouter) Register() { - u.router.GET("/", u.controller.Find) - u.router.POST("/", u.controller.Create) - u.router.PUT("/:id", u.CanModifyUser(), u.controller.Update) - u.router.DELETE("/:id", u.CanModifyUser(), u.controller.Delete) - u.router.POST("/login", u.controller.Login) - u.router.POST("/logout", u.controller.Logout) - u.router.POST("/register", u.controller.Register) - u.router.POST("/:id/avatar", u.CanModifyUser(), u.controller.SaveAvatar) - u.router.DELETE("/:id/avatar", u.CanModifyUser(), u.controller.DeleteAvatar) -} - -func (u *UserRouter) CanModifyUser() gin.HandlerFunc { - return func(ctx *gin.Context) { - user := ctx.MustGet("user").(*model.User) - if ok := service.S().AuthService.CanModifyUser(user, convertor.ToUintD(ctx.Param("id"), 0)); !ok { - ctx.JSON(http.StatusForbidden, gin.H{ - "code": http.StatusForbidden, - }) - ctx.Abort() - } - ctx.Next() - } -} diff --git a/internal/service/auth.go b/internal/service/auth.go deleted file mode 100644 index 9de8b40a..00000000 --- a/internal/service/auth.go +++ /dev/null @@ -1,42 +0,0 @@ -package service - -import ( - "github.com/elabosak233/cloudsdale/internal/model" - "github.com/elabosak233/cloudsdale/internal/repository" -) - -type IAuthService interface { - // CanModifyUser will check if the user can modify the target user. - CanModifyUser(user *model.User, targetUserID uint) bool - - // CanModifyTeam will check if the user can modify the target team. - CanModifyTeam(user *model.User, targetTeamID uint) bool -} - -type AuthService struct { - userRepository repository.IUserRepository - teamRepository repository.ITeamRepository -} - -func NewAuthService(r *repository.Repository) IAuthService { - return &AuthService{ - userRepository: r.UserRepository, - teamRepository: r.TeamRepository, - } -} - -func (a *AuthService) CanModifyUser(user *model.User, targetUserID uint) bool { - return user.Group == "admin" || user.ID == targetUserID -} - -func (a *AuthService) CanModifyTeam(user *model.User, targetTeamID uint) bool { - isCaptain := func() bool { - for _, team := range user.Teams { - if team.ID == targetTeamID && team.CaptainID == user.ID { - return true - } - } - return false - } - return user.Group == "admin" || isCaptain() -} diff --git a/internal/service/category.go b/internal/service/category.go deleted file mode 100644 index 83737a5d..00000000 --- a/internal/service/category.go +++ /dev/null @@ -1,47 +0,0 @@ -package service - -import ( - "github.com/elabosak233/cloudsdale/internal/model" - "github.com/elabosak233/cloudsdale/internal/model/request" - "github.com/elabosak233/cloudsdale/internal/repository" -) - -type ICategoryService interface { - // Create will create a new category with the given request. - Create(req model.Category) error - - // Update will update the category with the given request. - Update(req model.Category) error - - // Find will find the category with the given request, and return the categories. - Find(req request.CategoryFindRequest) ([]model.Category, error) - - // Delete will delete the category with the given request. - Delete(req request.CategoryDeleteRequest) error -} - -type CategoryService struct { - categoryRepository repository.ICategoryRepository -} - -func NewCategoryService(r *repository.Repository) ICategoryService { - return &CategoryService{ - categoryRepository: r.CategoryRepository, - } -} - -func (c *CategoryService) Create(req model.Category) error { - return c.categoryRepository.Create(req) -} - -func (c *CategoryService) Update(req model.Category) error { - return c.categoryRepository.Update(req) -} - -func (c *CategoryService) Find(req request.CategoryFindRequest) ([]model.Category, error) { - return c.categoryRepository.Find(req) -} - -func (c *CategoryService) Delete(req request.CategoryDeleteRequest) error { - return c.categoryRepository.Delete(req.ID) -} diff --git a/internal/service/challenge.go b/internal/service/challenge.go deleted file mode 100644 index 944909d2..00000000 --- a/internal/service/challenge.go +++ /dev/null @@ -1,83 +0,0 @@ -package service - -import ( - "github.com/elabosak233/cloudsdale/internal/model" - "github.com/elabosak233/cloudsdale/internal/model/request" - "github.com/elabosak233/cloudsdale/internal/repository" - "github.com/mitchellh/mapstructure" -) - -type IChallengeService interface { - // Find will find the challenge with the given request, and return the challenges and the total number of challenges. - Find(req request.ChallengeFindRequest) ([]model.Challenge, int64, error) - - // Create will create a new challenge with the given request. - Create(req request.ChallengeCreateRequest) error - - // Update will update the challenge with the given request. - Update(req request.ChallengeUpdateRequest) error - - // Delete will delete the challenge with the given request. - Delete(id uint) error -} - -type ChallengeService struct { - challengeRepository repository.IChallengeRepository - flagRepository repository.IFlagRepository - categoryRepository repository.ICategoryRepository - gameChallengeRepository repository.IGameChallengeRepository - submissionRepository repository.ISubmissionRepository - portRepository repository.IPortRepository - envRepository repository.IEnvRepository -} - -func NewChallengeService(r *repository.Repository) IChallengeService { - return &ChallengeService{ - challengeRepository: r.ChallengeRepository, - gameChallengeRepository: r.GameChallengeRepository, - submissionRepository: r.SubmissionRepository, - categoryRepository: r.CategoryRepository, - flagRepository: r.FlagRepository, - portRepository: r.PortRepository, - envRepository: r.EnvRepository, - } -} - -func (t *ChallengeService) Create(req request.ChallengeCreateRequest) error { - challenge := model.Challenge{} - _ = mapstructure.Decode(req, &challenge) - _, err := t.challengeRepository.Create(challenge) - return err -} - -func (t *ChallengeService) Update(req request.ChallengeUpdateRequest) error { - challenge := model.Challenge{} - _ = mapstructure.Decode(req, &challenge) - challenge, err := t.challengeRepository.Update(challenge) - return err -} - -func (t *ChallengeService) Delete(id uint) error { - err := t.challengeRepository.Delete(id) - return err -} - -func (t *ChallengeService) Find(req request.ChallengeFindRequest) ([]model.Challenge, int64, error) { - challenges, total, err := t.challengeRepository.Find(req) - - for index, challenge := range challenges { - if !*(req.IsDetailed) { - challenge.Simplify() - } - - // Calculate the solved times and bloods. - challenge.SolvedTimes = len(challenge.Submissions) - if challenge.Submissions != nil { - challenge.Bloods = challenge.Submissions[:min(3, len(challenge.Submissions))] - } - - challenges[index] = challenge - } - - return challenges, total, err -} diff --git a/internal/service/config.go b/internal/service/config.go deleted file mode 100644 index daa7a1c3..00000000 --- a/internal/service/config.go +++ /dev/null @@ -1,30 +0,0 @@ -package service - -import ( - "github.com/elabosak233/cloudsdale/internal/app/config" - "github.com/elabosak233/cloudsdale/internal/model/request" -) - -type IConfigService interface { - // Update will update the config with the given request. - Update(req request.ConfigUpdateRequest) error -} - -type ConfigService struct { -} - -func NewConfigService() IConfigService { - return &ConfigService{} -} - -func (c *ConfigService) Update(req request.ConfigUpdateRequest) error { - config.PltCfg().Site.Title = req.Site.Title - config.PltCfg().Site.Description = req.Site.Description - config.PltCfg().Container.ParallelLimit = req.Container.ParallelLimit - config.PltCfg().Container.RequestLimit = req.Container.RequestLimit - config.PltCfg().User.Register.Enabled = req.User.Register.Enabled - config.PltCfg().User.Register.Captcha.Enabled = req.User.Register.Captcha.Enabled - config.PltCfg().User.Register.Email.Enabled = req.User.Register.Email.Enabled - err := config.PltCfg().Save() - return err -} diff --git a/internal/service/flag.go b/internal/service/flag.go deleted file mode 100644 index 5ae0eb97..00000000 --- a/internal/service/flag.go +++ /dev/null @@ -1,50 +0,0 @@ -package service - -import ( - "github.com/elabosak233/cloudsdale/internal/model" - "github.com/elabosak233/cloudsdale/internal/model/request" - "github.com/elabosak233/cloudsdale/internal/repository" - "github.com/mitchellh/mapstructure" -) - -type IFlagService interface { - // Create will create a new flag with the given request. - Create(req request.FlagCreateRequest) error - - // Update will update the flag with the given request. - Update(req request.FlagUpdateRequest) error - - // Delete will delete the flag with the given request. - Delete(req request.FlagDeleteRequest) error -} - -type FlagService struct { - flagRepository repository.IFlagRepository -} - -func NewFlagService(r *repository.Repository) IFlagService { - return &FlagService{ - flagRepository: r.FlagRepository, - } -} - -func (f *FlagService) Create(req request.FlagCreateRequest) error { - var flag model.Flag - _ = mapstructure.Decode(req, &flag) - _, err := f.flagRepository.Create(flag) - return err -} - -func (f *FlagService) Update(req request.FlagUpdateRequest) error { - var flag model.Flag - _ = mapstructure.Decode(req, &flag) - _, err := f.flagRepository.Update(flag) - return err -} - -func (f *FlagService) Delete(req request.FlagDeleteRequest) error { - var flag model.Flag - _ = mapstructure.Decode(req, &flag) - err := f.flagRepository.Delete(flag) - return err -} diff --git a/internal/service/game.go b/internal/service/game.go deleted file mode 100644 index 5b22aaf8..00000000 --- a/internal/service/game.go +++ /dev/null @@ -1,76 +0,0 @@ -package service - -import ( - "crypto/ed25519" - "crypto/rand" - "encoding/base64" - "github.com/elabosak233/cloudsdale/internal/model" - "github.com/elabosak233/cloudsdale/internal/model/request" - "github.com/elabosak233/cloudsdale/internal/repository" - "github.com/mitchellh/mapstructure" -) - -type IGameService interface { - // Find will find games with the given request, and return the games and total count. - Find(req request.GameFindRequest) ([]model.Game, int64, error) - - // Create will create a new game with the given request. - Create(req request.GameCreateRequest) error - - // Update will update the game with the given request. - Update(req request.GameUpdateRequest) error - - // Delete will delete the game with the given request. - Delete(req request.GameDeleteRequest) error -} - -type GameService struct { - gameRepository repository.IGameRepository - gameChallengeRepository repository.IGameChallengeRepository - gameTeamRepository repository.IGameTeamRepository - submissionRepository repository.ISubmissionRepository - challengeRepository repository.IChallengeRepository - teamRepository repository.ITeamRepository - userRepository repository.IUserRepository -} - -func NewGameService(r *repository.Repository) IGameService { - return &GameService{ - gameRepository: r.GameRepository, - gameChallengeRepository: r.GameChallengeRepository, - gameTeamRepository: r.GameTeamRepository, - submissionRepository: r.SubmissionRepository, - challengeRepository: r.ChallengeRepository, - teamRepository: r.TeamRepository, - userRepository: r.UserRepository, - } -} - -func (g *GameService) Create(req request.GameCreateRequest) error { - publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader) - game := model.Game{ - PublicKey: base64.StdEncoding.EncodeToString(publicKey), - PrivateKey: base64.StdEncoding.EncodeToString(privateKey), - } - err = mapstructure.Decode(req, &game) - _, err = g.gameRepository.Create(game) - return err -} - -func (g *GameService) Update(req request.GameUpdateRequest) error { - game := model.Game{} - err := mapstructure.Decode(req, &game) - err = g.gameRepository.Update(game) - return err -} - -func (g *GameService) Delete(req request.GameDeleteRequest) error { - return g.gameRepository.Delete(model.Game{ - ID: req.ID, - }) -} - -func (g *GameService) Find(req request.GameFindRequest) ([]model.Game, int64, error) { - games, total, err := g.gameRepository.Find(req) - return games, total, err -} diff --git a/internal/service/game_challenge.go b/internal/service/game_challenge.go deleted file mode 100644 index dd3cdc53..00000000 --- a/internal/service/game_challenge.go +++ /dev/null @@ -1,95 +0,0 @@ -package service - -import ( - "github.com/elabosak233/cloudsdale/internal/model" - "github.com/elabosak233/cloudsdale/internal/model/request" - "github.com/elabosak233/cloudsdale/internal/repository" - "github.com/elabosak233/cloudsdale/internal/utils/calculate" - "github.com/mitchellh/mapstructure" -) - -type IGameChallengeService interface { - // Find will find the challenges in game with the given request. - Find(req request.GameChallengeFindRequest) ([]model.GameChallenge, error) - - // Create will create a new game challenge with the given request. - Create(req request.GameChallengeCreateRequest) error - - // Update will update the game challenge with the given request. - Update(req request.GameChallengeUpdateRequest) error - - // Delete will delete the game challenge with the given request. - Delete(req request.GameChallengeDeleteRequest) error -} - -type GameChallengeService struct { - gameRepository repository.IGameRepository - gameChallengeRepository repository.IGameChallengeRepository - noticeRepository repository.INoticeRepository -} - -func NewGameChallengeService(r *repository.Repository) IGameChallengeService { - return &GameChallengeService{ - gameRepository: r.GameRepository, - gameChallengeRepository: r.GameChallengeRepository, - noticeRepository: r.NoticeRepository, - } -} - -func (g *GameChallengeService) Find(req request.GameChallengeFindRequest) ([]model.GameChallenge, error) { - games, _, _ := g.gameRepository.Find(request.GameFindRequest{ - ID: req.GameID, - }) - game := games[0] - gameChallenges, err := g.gameChallengeRepository.Find(req) - for i, gameChallenge := range gameChallenges { - // Calculate the challenge pts. - gameChallenge.Pts = calculate.GameChallengePts( - gameChallenge.MaxPts, - gameChallenge.MinPts, - gameChallenge.Challenge.Difficulty, - int64(len(gameChallenge.Challenge.Submissions)), - int64(len(gameChallenge.Challenge.Submissions)), - game.FirstBloodRewardRatio, - game.SecondBloodRewardRatio, - game.ThirdBloodRewardRatio, - ) - - // Calculate the solved times and bloods. - gameChallenge.Challenge.SolvedTimes = len(gameChallenge.Challenge.Submissions) - if gameChallenge.Challenge.Submissions != nil { - gameChallenge.Challenge.Bloods = gameChallenge.Challenge.Submissions[:min(3, len(gameChallenge.Challenge.Submissions))] - } - - gameChallenges[i] = gameChallenge - } - return gameChallenges, err -} - -func (g *GameChallengeService) Create(req request.GameChallengeCreateRequest) error { - var gameChallenge model.GameChallenge - err := mapstructure.Decode(req, &gameChallenge) - err = g.gameChallengeRepository.Create(gameChallenge) - return err -} - -func (g *GameChallengeService) Update(req request.GameChallengeUpdateRequest) error { - var gameChallenge model.GameChallenge - err := mapstructure.Decode(req, &gameChallenge) - err = g.gameChallengeRepository.Update(gameChallenge) - if gameChallenge.IsEnabled != nil && *(gameChallenge.IsEnabled) { - _, err = g.noticeRepository.Create(model.Notice{ - Type: "new_challenge", - ChallengeID: &gameChallenge.ChallengeID, - GameID: &gameChallenge.GameID, - }) - } - return err -} - -func (g *GameChallengeService) Delete(req request.GameChallengeDeleteRequest) error { - var gameChallenge model.GameChallenge - err := mapstructure.Decode(req, &gameChallenge) - err = g.gameChallengeRepository.Delete(gameChallenge) - return err -} diff --git a/internal/service/game_team.go b/internal/service/game_team.go deleted file mode 100644 index ea200019..00000000 --- a/internal/service/game_team.go +++ /dev/null @@ -1,128 +0,0 @@ -package service - -import ( - "errors" - "fmt" - "github.com/elabosak233/cloudsdale/internal/model" - "github.com/elabosak233/cloudsdale/internal/model/request" - "github.com/elabosak233/cloudsdale/internal/repository" - "github.com/elabosak233/cloudsdale/internal/utils" - "github.com/mitchellh/mapstructure" - "strconv" -) - -type IGameTeamService interface { - // Find will find the game team with the given request. - Find(req request.GameTeamFindRequest) ([]model.GameTeam, int64, error) - - // Create will create a new game team with the given request. - Create(req request.GameTeamCreateRequest) error - - // Update will update the game team with the given request. - Update(req request.GameTeamUpdateRequest) error - - // Delete will delete the game team with the given request. - Delete(req request.GameTeamDeleteRequest) error -} - -type GameTeamService struct { - gameTeamRepository repository.IGameTeamRepository - gameRepository repository.IGameRepository - teamRepository repository.ITeamRepository - submissionRepository repository.ISubmissionRepository - userRepository repository.IUserRepository -} - -func NewGameTeamService(r *repository.Repository) IGameTeamService { - return &GameTeamService{ - submissionRepository: r.SubmissionRepository, - gameTeamRepository: r.GameTeamRepository, - gameRepository: r.GameRepository, - teamRepository: r.TeamRepository, - userRepository: r.UserRepository, - } -} - -func (g *GameTeamService) Find(req request.GameTeamFindRequest) ([]model.GameTeam, int64, error) { - gameTeams, total, err := g.gameTeamRepository.Find(model.GameTeam{ - GameID: req.GameID, - TeamID: req.TeamID, - }) - for index, gameTeam := range gameTeams { - if req.TeamID != 0 && gameTeam.TeamID != req.TeamID { - continue - } - gameTeams[index] = gameTeam - } - return gameTeams, total, err -} - -func (g *GameTeamService) Create(req request.GameTeamCreateRequest) error { - games, _, err := g.gameRepository.Find(request.GameFindRequest{ - ID: req.ID, - }) - game := games[0] - teams, _, err := g.teamRepository.Find(request.TeamFindRequest{ - ID: req.TeamID, - }) - team := teams[0] - users, _, err := g.userRepository.Find(request.UserFindRequest{ - ID: req.UserID, - }) - user := users[0] - if req.UserID != team.Captain.ID && (user.Group != "admin") { - return errors.New("invalid team captain") - } - - if int64(len(team.Users)) < game.MemberLimitMin || int64(len(team.Users)) > game.MemberLimitMax { - return errors.New("invalid team member count") - } - - gameTeams, _, err := g.gameTeamRepository.Find(model.GameTeam{ - GameID: req.ID, - }) - for _, gameTeam := range gameTeams { - if gameTeam.TeamID == team.ID && gameTeam.GameID == game.ID { - return errors.New("team already exists") - } - for _, u := range gameTeam.Team.Users { - for _, tu := range team.Users { - if tu.ID == u.ID { - return errors.New("user already exists") - } - } - } - } - - var isAllowed bool - if game.IsPublic != nil && *game.IsPublic { - isAllowed = true - } else { - isAllowed = false - } - - gameTeam := model.GameTeam{ - TeamID: team.ID, - GameID: game.ID, - IsAllowed: &isAllowed, - } - - gameTeam.Signature = fmt.Sprintf("%s:%s", strconv.Itoa(int(team.ID)), utils.HyphenlessUUID()) - - err = g.gameTeamRepository.Create(gameTeam) - return err -} - -func (g *GameTeamService) Update(req request.GameTeamUpdateRequest) error { - var gameTeam model.GameTeam - err := mapstructure.Decode(req, &gameTeam) - err = g.gameTeamRepository.Update(gameTeam) - return err -} - -func (g *GameTeamService) Delete(req request.GameTeamDeleteRequest) error { - var gameTeam model.GameTeam - err := mapstructure.Decode(req, &gameTeam) - err = g.gameTeamRepository.Delete(gameTeam) - return err -} diff --git a/internal/service/media.go b/internal/service/media.go deleted file mode 100644 index 18d4b971..00000000 --- a/internal/service/media.go +++ /dev/null @@ -1,134 +0,0 @@ -package service - -import ( - "fmt" - "github.com/elabosak233/cloudsdale/internal/utils" - "io" - "mime/multipart" - "os" - "path" -) - -type IMediaService interface { - // SaveGamePoster will save the game poster to the media folder with the game id as the folder name. - SaveGamePoster(id uint, fileHeader *multipart.FileHeader) error - - // DeleteGamePoster will delete the game poster from the media folder with the game id as the folder name. - DeleteGamePoster(id uint) error - - // SaveUserAvatar will save the user avatar to the media folder with the user id as the folder name. - SaveUserAvatar(id uint, fileHeader *multipart.FileHeader) error - - // DeleteUserAvatar will delete the user avatar from the media folder with the user id as the folder name. - DeleteUserAvatar(id uint) error - - // SaveTeamAvatar will save the team avatar to the media folder with the team id as the folder name. - SaveTeamAvatar(id uint, fileHeader *multipart.FileHeader) error - - // DeleteTeamAvatar will delete the team avatar from the media folder with the team id as the folder name. - DeleteTeamAvatar(id uint) error - - // SaveChallengeAttachment will save the challenge attachment to the media folder with the challenge id as the folder name. - SaveChallengeAttachment(id uint, fileHeader *multipart.FileHeader) error - - // DeleteChallengeAttachment will delete the challenge attachment from the media folder with the challenge id as the folder name. - DeleteChallengeAttachment(id uint) error -} - -type MediaService struct{} - -func NewMediaService() IMediaService { - return &MediaService{} -} - -func (m *MediaService) SaveChallengeAttachment(id uint, fileHeader *multipart.FileHeader) error { - file, err := fileHeader.Open() - defer func(file multipart.File) { - _ = file.Close() - }(file) - data, err := io.ReadAll(file) - p := path.Join(utils.MediaPath, "challenges", fmt.Sprintf("%d", id), fileHeader.Filename) - err = m.DeleteChallengeAttachment(id) - dir := path.Dir(p) - if _, err = os.Stat(dir); os.IsNotExist(err) { - if err = os.MkdirAll(dir, 0755); err != nil { - return err - } - } - err = os.WriteFile(p, data, 0644) - return err -} - -func (m *MediaService) DeleteChallengeAttachment(id uint) error { - p := path.Join(utils.MediaPath, "challenges", fmt.Sprintf("%d", id)) - return os.RemoveAll(p) -} - -func (m *MediaService) SaveGamePoster(id uint, fileHeader *multipart.FileHeader) error { - file, err := fileHeader.Open() - defer func(file multipart.File) { - _ = file.Close() - }(file) - data, err := io.ReadAll(file) - p := path.Join(utils.MediaPath, "games", fmt.Sprintf("%d", id), "poster", fileHeader.Filename) - err = m.DeleteGamePoster(id) - dir := path.Dir(p) - if _, err = os.Stat(dir); os.IsNotExist(err) { - if err = os.MkdirAll(dir, 0755); err != nil { - return err - } - } - err = os.WriteFile(p, data, 0644) - return err -} - -func (m *MediaService) DeleteGamePoster(id uint) error { - p := path.Join(utils.MediaPath, "games", fmt.Sprintf("%d", id), "poster") - return os.RemoveAll(p) -} - -func (m *MediaService) SaveUserAvatar(id uint, fileHeader *multipart.FileHeader) error { - file, err := fileHeader.Open() - defer func(file multipart.File) { - _ = file.Close() - }(file) - data, err := io.ReadAll(file) - p := path.Join(utils.MediaPath, "users", fmt.Sprintf("%d", id), fileHeader.Filename) - err = m.DeleteUserAvatar(id) - dir := path.Dir(p) - if _, err = os.Stat(dir); os.IsNotExist(err) { - if err = os.MkdirAll(dir, 0755); err != nil { - return err - } - } - err = os.WriteFile(p, data, 0644) - return err -} - -func (m *MediaService) DeleteUserAvatar(id uint) error { - p := path.Join(utils.MediaPath, "users", fmt.Sprintf("%d", id)) - return os.RemoveAll(p) -} - -func (m *MediaService) SaveTeamAvatar(id uint, fileHeader *multipart.FileHeader) error { - file, err := fileHeader.Open() - defer func(file multipart.File) { - _ = file.Close() - }(file) - data, err := io.ReadAll(file) - p := path.Join(utils.MediaPath, "teams", fmt.Sprintf("%d", id), fileHeader.Filename) - err = m.DeleteTeamAvatar(id) - dir := path.Dir(p) - if _, err = os.Stat(dir); os.IsNotExist(err) { - if err = os.MkdirAll(dir, 0755); err != nil { - return err - } - } - err = os.WriteFile(p, data, 0644) - return err -} - -func (m *MediaService) DeleteTeamAvatar(id uint) error { - p := path.Join(utils.MediaPath, "teams", fmt.Sprintf("%d", id)) - return os.RemoveAll(p) -} diff --git a/internal/service/notice.go b/internal/service/notice.go deleted file mode 100644 index 6762c25c..00000000 --- a/internal/service/notice.go +++ /dev/null @@ -1,58 +0,0 @@ -package service - -import ( - "github.com/elabosak233/cloudsdale/internal/model" - "github.com/elabosak233/cloudsdale/internal/model/request" - "github.com/elabosak233/cloudsdale/internal/repository" - "github.com/mitchellh/mapstructure" -) - -type INoticeService interface { - // Find will find the notice with the given request. - Find(req request.NoticeFindRequest) ([]model.Notice, int64, error) - - // Create will create a new notice with the given request. - Create(req request.NoticeCreateRequest) error - - // Update will update the notice with the given request. - Update(req request.NoticeUpdateRequest) error - - // Delete will delete the notice with the given request. - Delete(req request.NoticeDeleteRequest) error -} - -type NoticeService struct { - noticeRepository repository.INoticeRepository -} - -func NewNoticeService(r *repository.Repository) INoticeService { - return &NoticeService{ - noticeRepository: r.NoticeRepository, - } -} - -func (n *NoticeService) Find(req request.NoticeFindRequest) ([]model.Notice, int64, error) { - notices, total, err := n.noticeRepository.Find(req) - return notices, total, err -} - -func (n *NoticeService) Create(req request.NoticeCreateRequest) error { - var notice model.Notice - _ = mapstructure.Decode(req, ¬ice) - _, err := n.noticeRepository.Create(notice) - return err -} - -func (n *NoticeService) Update(req request.NoticeUpdateRequest) error { - var notice model.Notice - _ = mapstructure.Decode(req, ¬ice) - _, err := n.noticeRepository.Update(notice) - return err -} - -func (n *NoticeService) Delete(req request.NoticeDeleteRequest) error { - var notice model.Notice - _ = mapstructure.Decode(req, ¬ice) - err := n.noticeRepository.Delete(notice) - return err -} diff --git a/internal/service/pod.go b/internal/service/pod.go deleted file mode 100644 index 841056af..00000000 --- a/internal/service/pod.go +++ /dev/null @@ -1,231 +0,0 @@ -package service - -import ( - "errors" - "fmt" - "github.com/elabosak233/cloudsdale/internal/app/config" - "github.com/elabosak233/cloudsdale/internal/extension/container/manager" - "github.com/elabosak233/cloudsdale/internal/model" - "github.com/elabosak233/cloudsdale/internal/model/request" - "github.com/elabosak233/cloudsdale/internal/repository" - "github.com/elabosak233/cloudsdale/internal/utils" - "strings" - "sync" - "time" -) - -var ( - // UserPodRequestMap 用于存储用户上次请求的时间 - UserPodRequestMap = struct { - sync.RWMutex - m map[uint]int64 - }{m: make(map[uint]int64)} - - // PodManagers is a mapping of PodID and manager pointer. - PodManagers = make(map[uint]manager.IContainerManager) -) - -// GetUserInstanceRequestMap 返回用户上次请求的时间 -func GetUserInstanceRequestMap(userID uint) int64 { - UserPodRequestMap.RLock() - defer UserPodRequestMap.RUnlock() - return UserPodRequestMap.m[userID] -} - -// SetUserInstanceRequestMap 设置用户上次请求的时间 -func SetUserInstanceRequestMap(userID uint, t int64) { - UserPodRequestMap.Lock() - defer UserPodRequestMap.Unlock() - UserPodRequestMap.m[userID] = t -} - -type IPodService interface { - Create(req request.PodCreateRequest) (model.Pod, error) - Renew(req request.PodRenewRequest) error - Remove(req request.PodRemoveRequest) error - Find(req request.PodFindRequest) ([]model.Pod, int64, error) -} - -type PodService struct { - challengeRepository repository.IChallengeRepository - podRepository repository.IPodRepository - natRepository repository.INatRepository -} - -func NewPodService(r *repository.Repository) IPodService { - return &PodService{ - challengeRepository: r.ChallengeRepository, - podRepository: r.PodRepository, - natRepository: r.NatRepository, - } -} - -func GenerateFlag(flag string) string { - flag = strings.Replace(flag, "[UUID]", utils.HyphenlessUUID(), -1) - flag = strings.Replace(flag, "[uuid]", utils.HyphenlessUUID(), -1) - return flag -} - -func (t *PodService) IsLimited(userID uint, limit int64) (remainder int64) { - if userID == 0 { - return 0 - } - ti := GetUserInstanceRequestMap(userID) - if ti != 0 { - if time.Now().Unix()-ti < limit { - return limit - (time.Now().Unix() - ti) - } - } - return 0 -} - -func (t *PodService) ParallelLimit(req request.PodCreateRequest) { - isGame := req.GameID != nil && req.TeamID != nil - if config.PltCfg().Container.ParallelLimit > 0 { - var availablePods []model.Pod - var count int64 - if !isGame { - availablePods, count, _ = t.podRepository.Find(request.PodFindRequest{ - UserID: &req.UserID, - IsAvailable: &utils.True, - }) - } else { - availablePods, count, _ = t.podRepository.Find(request.PodFindRequest{ - TeamID: req.TeamID, - GameID: req.GameID, - IsAvailable: &utils.True, - }) - } - needToBeDeactivated := count - int64(config.PltCfg().Container.ParallelLimit) + 1 - if needToBeDeactivated > 0 { - for _, pod := range availablePods { - if needToBeDeactivated == 0 { - break - } - go func() { - _ = t.Remove(request.PodRemoveRequest{ - ID: pod.ID, - }) - }() - needToBeDeactivated -= 1 - } - } - } -} - -func (t *PodService) Create(req request.PodCreateRequest) (model.Pod, error) { - remainder := t.IsLimited(req.UserID, int64(config.PltCfg().Container.RequestLimit)) - if remainder != 0 { - return model.Pod{}, errors.New(fmt.Sprintf("请等待 %d 秒后再次请求", remainder)) - } - SetUserInstanceRequestMap(req.UserID, time.Now().Unix()) - challenges, _, _ := t.challengeRepository.Find(request.ChallengeFindRequest{ - ID: req.ChallengeID, - IsDynamic: &utils.True, - }) - challenge := challenges[0] - - t.ParallelLimit(req) - - removedAt := time.Now().Add(time.Duration(challenge.Duration) * time.Second).Unix() - - // Select the first one as the target flag which will be injected into the container. - generatedFlag := model.Flag{} - for _, flag := range challenge.Flags { - generatedFlag = *flag - if flag.Type == "dynamic" { - generatedFlag.Value = GenerateFlag(flag.Value) - } - if flag.Env == "" { - generatedFlag.Env = "FLAG" - } - break - } - - ctnManager := manager.NewContainerManager( - challenge, - generatedFlag, - time.Duration(challenge.Duration)*time.Second, - ) - - nats, err := ctnManager.Setup() - - // Create Pod model, get Pod's GameID - pod, _ := t.podRepository.Create(model.Pod{ - ChallengeID: &req.ChallengeID, - UserID: &req.UserID, - GameID: req.GameID, - TeamID: req.TeamID, - RemovedAt: removedAt, - Nats: nats, - Flag: generatedFlag.Value, - }) - - ctnManager.SetPodID(pod.ID) - - go func() { - if ctnManager.RemoveAfterDuration() { - delete(PodManagers, pod.ID) - } - }() - - PodManagers[pod.ID] = ctnManager - - pod.Simplify() - - return pod, err -} - -func (t *PodService) Renew(req request.PodRenewRequest) error { - remainder := t.IsLimited(req.UserID, int64(config.PltCfg().Container.RequestLimit)) - if remainder != 0 { - return errors.New(fmt.Sprintf("请等待 %d 秒后再次请求", remainder)) - } - SetUserInstanceRequestMap(req.UserID, time.Now().Unix()) // 保存用户请求时间 - pods, total, _ := t.podRepository.Find(request.PodFindRequest{ - ID: req.ID, - }) - if total == 0 { - return errors.New("pod.not_found") - } - pod := pods[0] - ctn, ok := PodManagers[req.ID] - if !ok { - return errors.New("pod.not_found") - } - ctn.Renew(ctn.Duration()) - pod.RemovedAt = time.Now().Add(ctn.Duration()).Unix() - err := t.podRepository.Update(pod) - return err -} - -func (t *PodService) Remove(req request.PodRemoveRequest) error { - remainder := t.IsLimited(req.UserID, int64(config.PltCfg().Container.RequestLimit)) - if remainder != 0 { - return errors.New(fmt.Sprintf("请等待 %d 秒后再次请求", remainder)) - } - err := t.podRepository.Update(model.Pod{ - ID: req.ID, - RemovedAt: time.Now().Unix(), - }) - if ctn, ok := PodManagers[req.ID]; ok { - ctn.Remove() - } - go func() { - delete(PodManagers, req.ID) - }() - return err -} - -func (t *PodService) Find(req request.PodFindRequest) ([]model.Pod, int64, error) { - if req.TeamID != nil && req.GameID != nil { - req.UserID = nil - } - pods, total, err := t.podRepository.Find(req) - - for i, pod := range pods { - pod.Simplify() - pods[i] = pod - } - return pods, total, err -} diff --git a/internal/service/service.go b/internal/service/service.go deleted file mode 100644 index b77bfe07..00000000 --- a/internal/service/service.go +++ /dev/null @@ -1,62 +0,0 @@ -package service - -import ( - "github.com/elabosak233/cloudsdale/internal/repository" - "go.uber.org/zap" - "sync" -) - -var ( - s *Service = nil - onceService sync.Once -) - -type Service struct { - AuthService IAuthService - MediaService IMediaService - UserService IUserService - ChallengeService IChallengeService - PodService IPodService - ConfigService IConfigService - TeamService ITeamService - UserTeamService IUserTeamService - SubmissionService ISubmissionService - GameService IGameService - GameChallengeService IGameChallengeService - GameTeamService IGameTeamService - CategoryService ICategoryService - FlagService IFlagService - NoticeService INoticeService -} - -func S() *Service { - if s == nil { - InitService() - } - return s -} - -func InitService() { - onceService.Do(func() { - r := repository.R() - - s = &Service{ - AuthService: NewAuthService(r), - MediaService: NewMediaService(), - UserService: NewUserService(r), - ChallengeService: NewChallengeService(r), - PodService: NewPodService(r), - ConfigService: NewConfigService(), - TeamService: NewTeamService(r), - UserTeamService: NewUserTeamService(r), - SubmissionService: NewSubmissionService(r), - GameService: NewGameService(r), - GameChallengeService: NewGameChallengeService(r), - GameTeamService: NewGameTeamService(r), - CategoryService: NewCategoryService(r), - FlagService: NewFlagService(r), - NoticeService: NewNoticeService(r), - } - }) - zap.L().Info("Service layer inits successfully.") -} diff --git a/internal/service/submission.go b/internal/service/submission.go deleted file mode 100644 index 6231a510..00000000 --- a/internal/service/submission.go +++ /dev/null @@ -1,243 +0,0 @@ -package service - -import ( - "github.com/elabosak233/cloudsdale/internal/model" - "github.com/elabosak233/cloudsdale/internal/model/request" - "github.com/elabosak233/cloudsdale/internal/repository" - "github.com/elabosak233/cloudsdale/internal/utils" - "github.com/elabosak233/cloudsdale/internal/utils/calculate" - "regexp" - "time" -) - -type ISubmissionService interface { - // Create will create a new submission with the given request, and return the status and rank. - Create(req request.SubmissionCreateRequest) (status int, rank int64, err error) - - // Delete will delete the submission with the given id. - Delete(id uint) error - - // Find will find the submissions with the given request. - Find(req request.SubmissionFindRequest) ([]model.Submission, int64, error) -} - -type SubmissionService struct { - podRepository repository.IPodRepository - submissionRepository repository.ISubmissionRepository - challengeRepository repository.IChallengeRepository - teamRepository repository.ITeamRepository - userRepository repository.IUserRepository - gameChallengeRepository repository.IGameChallengeRepository - gameRepository repository.IGameRepository - noticeRepository repository.INoticeRepository -} - -func NewSubmissionService(r *repository.Repository) ISubmissionService { - return &SubmissionService{ - podRepository: r.PodRepository, - submissionRepository: r.SubmissionRepository, - challengeRepository: r.ChallengeRepository, - teamRepository: r.TeamRepository, - userRepository: r.UserRepository, - gameChallengeRepository: r.GameChallengeRepository, - gameRepository: r.GameRepository, - noticeRepository: r.NoticeRepository, - } -} - -func (t *SubmissionService) JudgeDynamicChallenge(req request.SubmissionCreateRequest) (status int, err error) { - perhapsPods, _, err := t.podRepository.Find(request.PodFindRequest{ - ChallengeID: req.ChallengeID, - GameID: req.GameID, - IsAvailable: &utils.True, - }) - status = 1 - podIDs := make([]uint, 0) - for _, pod := range perhapsPods { - podIDs = append(podIDs, pod.ID) - } - for _, pod := range perhapsPods { - if req.Flag == pod.Flag { - if (pod.UserID != nil && req.UserID == *(pod.UserID) && req.UserID != 0) || (pod.TeamID != nil && req.TeamID != nil && *(req.TeamID) == *(pod.TeamID)) { - status = 2 - } else { - status = 3 - } - break - } - } - return status, err -} - -func (t *SubmissionService) Create(req request.SubmissionCreateRequest) (status int, rank int64, err error) { - var challenge model.Challenge - if challenges, total, _ := t.challengeRepository.Find(request.ChallengeFindRequest{ - ID: req.ChallengeID, - }); total > 0 { - challenge = challenges[0] - } - - var team model.Team - if req.TeamID != nil { - if teams, total, _ := t.teamRepository.Find(request.TeamFindRequest{ - ID: *(req.TeamID), - }); total > 0 { - team = teams[0] - } - isMember := false - for _, user := range team.Users { - if req.UserID == user.ID { - isMember = true - } - } - if !isMember { - status = 4 - } - } - - status = 1 - rank = 0 - var gameChallengeID *uint = nil - for _, flag := range challenge.Flags { - switch *(flag.Banned) { - case true: - re, regexErr := regexp.Compile(flag.Value) - if regexErr != nil { - return 0, 0, regexErr - } - if re != nil && re.Match([]byte(req.Flag)) { - status = 3 - } - case false: - switch flag.Type { - case "pattern": - re, regexErr := regexp.Compile(flag.Value) - if regexErr != nil { - return 0, 0, regexErr - } - if re != nil && re.Match([]byte(req.Flag)) { - status = max(status, 2) - } - case "dynamic": - ss, _ := t.JudgeDynamicChallenge(req) - status = max(status, ss) - } - } - } - - if status == 2 { - if _, n, _ := t.submissionRepository.Find(request.SubmissionFindRequest{ - UserID: req.UserID, - Status: 2, - ChallengeID: req.ChallengeID, - TeamID: req.TeamID, - GameID: req.GameID, - }); n > 0 { - status = 4 - } - if status == 2 { - rank = int64(len(challenge.Submissions) + 1) - } - if req.GameID != nil && req.TeamID != nil { - isEnabled := true - gameChallenges, _ := t.gameChallengeRepository.Find(request.GameChallengeFindRequest{ - GameID: *(req.GameID), - ChallengeID: req.ChallengeID, - IsEnabled: &isEnabled, - }) - games, _, _ := t.gameRepository.Find(request.GameFindRequest{ - ID: *(req.GameID), - }) - if len(gameChallenges) > 0 && len(games) > 0 { - gameChallenge := gameChallenges[0] - game := games[0] - if time.Now().Unix() < game.StartedAt || time.Now().Unix() > game.EndedAt { - status = 4 - } - rank = int64(len(gameChallenge.Challenge.Submissions) + 1) - if rank <= 3 && rank != 0 && status == 2 { - var noticeType string - switch rank { - case 1: - noticeType = "first_blood" - case 2: - noticeType = "second_blood" - case 3: - noticeType = "third_blood" - } - _, err = t.noticeRepository.Create(model.Notice{ - Type: noticeType, - GameID: req.GameID, - UserID: &req.UserID, - TeamID: req.TeamID, - ChallengeID: &req.ChallengeID, - }) - } - gameChallengeID = &gameChallenge.ID - } - if len(gameChallenges) == 0 { - status = 4 - } - } - } - err = t.submissionRepository.Create(model.Submission{ - Flag: req.Flag, - UserID: req.UserID, - ChallengeID: req.ChallengeID, - GameChallengeID: gameChallengeID, - TeamID: req.TeamID, - GameID: req.GameID, - Status: status, - Rank: rank, - }) - return status, rank, err -} - -func (t *SubmissionService) Delete(id uint) error { - err := t.submissionRepository.Delete(id) - return err -} - -func (t *SubmissionService) Find(req request.SubmissionFindRequest) ([]model.Submission, int64, error) { - submissions, total, err := t.submissionRepository.Find(req) - challengeSolvesTotal := make(map[uint]int64) - - extractChallengeTotal := func(challengeID uint) int64 { - var cTotal int64 - if _, ok := challengeSolvesTotal[challengeID]; !ok { - for _, submission := range submissions { - if submission.ChallengeID == challengeID && submission.Status == 2 { - cTotal++ - } - } - challengeSolvesTotal[challengeID] = cTotal - } else { - cTotal = challengeSolvesTotal[challengeID] - } - return cTotal - } - - for index, submission := range submissions { - if submission.Status == 2 { - if submission.GameID != nil && submission.GameChallengeID != nil { - submission.Pts = calculate.GameChallengePts( - submission.GameChallenge.MaxPts, - submission.GameChallenge.MinPts, - submission.Challenge.Difficulty, - extractChallengeTotal(submission.ChallengeID), - submission.Rank-1, - submission.Game.FirstBloodRewardRatio, - submission.Game.SecondBloodRewardRatio, - submission.Game.ThirdBloodRewardRatio, - ) - } else { - submission.Pts = submission.Challenge.PracticePts - } - } - if !req.IsDetailed { - submission.Simplify() - } - submissions[index] = submission - } - return submissions, total, err -} diff --git a/internal/service/team.go b/internal/service/team.go deleted file mode 100644 index 1fd76592..00000000 --- a/internal/service/team.go +++ /dev/null @@ -1,117 +0,0 @@ -package service - -import ( - "errors" - "github.com/elabosak233/cloudsdale/internal/model" - "github.com/elabosak233/cloudsdale/internal/model/request" - "github.com/elabosak233/cloudsdale/internal/repository" - "github.com/google/uuid" -) - -type ITeamService interface { - // Create will create a team with the given request. - Create(req request.TeamCreateRequest) error - - // Update will update a team with the given id. - Update(req request.TeamUpdateRequest) error - - // Delete will delete a team with the given id. - Delete(id uint) error - - // Find will return the teams, total count and error. - Find(req request.TeamFindRequest) ([]model.Team, int64, error) - - // GetInviteToken will return the invite token of the team. - GetInviteToken(req request.TeamGetInviteTokenRequest) (string, error) - - // UpdateInviteToken will update the invite token of the team. - UpdateInviteToken(req request.TeamUpdateInviteTokenRequest) (string, error) -} - -type TeamService struct { - userRepository repository.IUserRepository - teamRepository repository.ITeamRepository - userTeamRepository repository.IUserTeamRepository -} - -func NewTeamService(r *repository.Repository) ITeamService { - return &TeamService{ - userRepository: r.UserRepository, - teamRepository: r.TeamRepository, - userTeamRepository: r.UserTeamRepository, - } -} - -func (t *TeamService) Create(req request.TeamCreateRequest) error { - user, err := t.userRepository.FindByID(req.CaptainId) - if err != nil || user.ID == 0 { - return errors.New("user.not_found") - } - isLocked := false - uid := uuid.NewString() - _, err = t.teamRepository.Create(model.Team{ - Name: req.Name, - CaptainID: req.CaptainId, - Description: req.Description, - Email: req.Email, - IsLocked: &isLocked, - InviteToken: uid[:8] + uid[9:13] + uid[14:18] + uid[19:23] + uid[24:], - }) - return err -} - -func (t *TeamService) Update(req request.TeamUpdateRequest) error { - team, err := t.teamRepository.FindByID(req.ID) - if err != nil || team.ID == 0 { - return errors.New("team.not_found") - } - err = t.teamRepository.Update(model.Team{ - ID: team.ID, - Name: req.Name, - Description: req.Description, - CaptainID: req.CaptainId, - Email: req.Email, - IsLocked: req.IsLocked, - }) - return err -} - -func (t *TeamService) Delete(id uint) error { - team, err := t.teamRepository.FindByID(id) - if err != nil || team.ID == 0 { - return errors.New("team.not_found") - } - err = t.teamRepository.Delete(id) - return err -} - -func (t *TeamService) Find(req request.TeamFindRequest) ([]model.Team, int64, error) { - teams, total, err := t.teamRepository.Find(req) - for index, team := range teams { - team.InviteToken = "" - teams[index] = team - } - return teams, total, err -} - -func (t *TeamService) GetInviteToken(req request.TeamGetInviteTokenRequest) (token string, err error) { - team, err := t.teamRepository.FindByID(req.ID) - if err != nil || team.ID == 0 { - return "", errors.New("team.not_found") - } - return team.InviteToken, err -} - -func (t *TeamService) UpdateInviteToken(req request.TeamUpdateInviteTokenRequest) (token string, err error) { - team, err := t.teamRepository.FindByID(req.ID) - if err != nil || team.ID == 0 { - return "", errors.New("team.not_found") - } - uid := uuid.NewString() - token = uid[:8] + uid[9:13] + uid[14:18] + uid[19:23] + uid[24:] - err = t.teamRepository.Update(model.Team{ - ID: req.ID, - InviteToken: token, - }) - return token, err -} diff --git a/internal/service/user.go b/internal/service/user.go deleted file mode 100644 index 787ec044..00000000 --- a/internal/service/user.go +++ /dev/null @@ -1,152 +0,0 @@ -package service - -import ( - "errors" - "github.com/elabosak233/cloudsdale/internal/app/config" - "github.com/elabosak233/cloudsdale/internal/extension/captcha" - "github.com/elabosak233/cloudsdale/internal/model" - "github.com/elabosak233/cloudsdale/internal/model/request" - "github.com/elabosak233/cloudsdale/internal/repository" - "github.com/golang-jwt/jwt/v5" - "github.com/mitchellh/mapstructure" - "golang.org/x/crypto/bcrypt" - "strings" - "time" -) - -type IUserService interface { - // Create will create a new user with the given request. - Create(req request.UserCreateRequest) error - - // Update will update the user with the given request. - Update(req request.UserUpdateRequest) error - - // Delete will delete the user with the given id. - Delete(id uint) error - - // Find will return the users, total count and error. - Find(req request.UserFindRequest) ([]model.User, int64, error) - - // Register will create a new user with the given request, but the default group is user. - Register(req request.UserRegisterRequest) error - - // Login will verify the user login request and return the user and jwt token. - Login(req request.UserLoginRequest) (model.User, string, error) - - // Logout will log out the user with the given token. - Logout(token string) (uint, error) -} - -type UserService struct { - userRepository repository.IUserRepository - teamRepository repository.ITeamRepository - userTeamRepository repository.IUserTeamRepository -} - -func NewUserService(r *repository.Repository) IUserService { - return &UserService{ - userRepository: r.UserRepository, - teamRepository: r.TeamRepository, - userTeamRepository: r.UserTeamRepository, - } -} - -func (t *UserService) GetJwtTokenByID(user model.User) (tokenString string, err error) { - jwtSecretKey := []byte(config.JwtSecretKey()) - pgsToken := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ - "user_id": user.ID, - "exp": time.Now().Add(time.Duration(config.AppCfg().Gin.Jwt.Expiration) * time.Minute).Unix(), - }) - return pgsToken.SignedString(jwtSecretKey) -} - -func (t *UserService) Logout(token string) (uint, error) { - pgsToken, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) { - return []byte(config.JwtSecretKey()), nil - }) - if err != nil { - return 0, err - } - if claims, ok := pgsToken.Claims.(jwt.MapClaims); ok && pgsToken.Valid { - return uint(claims["user_id"].(float64)), nil - } else { - return 0, errors.New("无效 Token") - } -} - -func (t *UserService) Create(req request.UserCreateRequest) error { - hashedPassword, _ := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) - userModel := model.User{ - Username: strings.ToLower(req.Username), - Email: strings.ToLower(req.Email), - Nickname: req.Nickname, - Group: req.Group, - Password: string(hashedPassword), - } - err := t.userRepository.Create(userModel) - return err -} - -func (t *UserService) Register(req request.UserRegisterRequest) error { - var err error - hashedPassword, _ := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) - success := true - if config.PltCfg().User.Register.Captcha.Enabled { - capt := captcha.NewCaptcha() - success, err = capt.Verify(req.CaptchaToken, req.RemoteIP) - } - if success { - userModel := model.User{ - Username: strings.ToLower(req.Username), - Email: strings.ToLower(req.Email), - Nickname: req.Nickname, - Group: "user", - Password: string(hashedPassword), - } - err = t.userRepository.Create(userModel) - } - return err -} - -func (t *UserService) Update(req request.UserUpdateRequest) error { - user := model.User{} - _ = mapstructure.Decode(req, &user) - if req.Password != "" { - hashedPassword, _ := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) - user.Password = string(hashedPassword) - } - if req.Username != "" { - user.Username = strings.ToLower(req.Username) - } - if req.Email != "" { - user.Email = strings.ToLower(req.Email) - } - err := t.userRepository.Update(user) - return err -} - -func (t *UserService) Delete(id uint) error { - err := t.userRepository.Delete(id) - return err -} - -func (t *UserService) Find(req request.UserFindRequest) ([]model.User, int64, error) { - users, total, err := t.userRepository.Find(req) - for index := range users { - users[index].Simplify() - } - return users, total, err -} - -func (t *UserService) Login(req request.UserLoginRequest) (model.User, string, error) { - user, err := t.userRepository.FindByUsername(strings.ToLower(req.Username)) - if err != nil { - return user, "", errors.New("user.not_found") - } - err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password)) - if err != nil { - return user, "", errors.New("user.login.invalid_password") - } - token, err := t.GetJwtTokenByID(user) - return user, token, err -} diff --git a/internal/service/user_team.go b/internal/service/user_team.go deleted file mode 100644 index 4b957479..00000000 --- a/internal/service/user_team.go +++ /dev/null @@ -1,79 +0,0 @@ -package service - -import ( - "errors" - "github.com/elabosak233/cloudsdale/internal/model" - "github.com/elabosak233/cloudsdale/internal/model/request" - "github.com/elabosak233/cloudsdale/internal/repository" -) - -type IUserTeamService interface { - Join(req request.TeamUserJoinRequest) error - Create(req request.TeamUserCreateRequest) error - Delete(req request.TeamUserDeleteRequest) error -} - -type UserTeamService struct { - userTeamRepository repository.IUserTeamRepository - teamRepository repository.ITeamRepository - userRepository repository.IUserRepository -} - -func NewUserTeamService(r *repository.Repository) IUserTeamService { - return &UserTeamService{ - userTeamRepository: r.UserTeamRepository, - teamRepository: r.TeamRepository, - userRepository: r.UserRepository, - } -} - -func (t *UserTeamService) Join(req request.TeamUserJoinRequest) error { - user, err := t.userRepository.FindByID(req.UserID) - if err != nil || user.ID == 0 { - return errors.New("user.not_found") - } - team, err := t.teamRepository.FindByID(req.TeamID) - if err != nil || team.ID == 0 { - return errors.New("team.not_found") - } - if team.InviteToken != req.InviteToken { - return errors.New("team.join.invalid_token") - } - err = t.userTeamRepository.Create(model.UserTeam{ - TeamID: team.ID, - UserID: req.UserID, - }) - return err -} - -func (t *UserTeamService) Create(req request.TeamUserCreateRequest) error { - user, err := t.userRepository.FindByID(req.UserID) - if err != nil || user.ID == 0 { - return errors.New("user.not_found") - } - team, err := t.teamRepository.FindByID(req.TeamID) - if err != nil || team.ID == 0 { - return errors.New("team.not_found") - } - err = t.userTeamRepository.Create(model.UserTeam{ - TeamID: team.ID, - UserID: req.UserID, - }) - return err -} - -func (t *UserTeamService) Delete(req request.TeamUserDeleteRequest) error { - user, err := t.userRepository.FindByID(req.UserID) - if err != nil || user.ID == 0 { - return errors.New("user.not_found") - } - team, err := t.teamRepository.FindByID(req.TeamID) - if err != nil || team.ID == 0 { - return errors.New("team.not_found") - } - err = t.userTeamRepository.Delete(model.UserTeam{ - TeamID: team.ID, - UserID: req.UserID, - }) - return err -} diff --git a/internal/service/webhook.go b/internal/service/webhook.go deleted file mode 100644 index ca746750..00000000 --- a/internal/service/webhook.go +++ /dev/null @@ -1,36 +0,0 @@ -package service - -import ( - "github.com/elabosak233/cloudsdale/internal/model" - "github.com/elabosak233/cloudsdale/internal/repository" -) - -type IWebhookService interface { - Create(webhook model.Webhook) (model.Webhook, error) - Update(webhook model.Webhook) error - Delete(webhook model.Webhook) error -} - -type WebhookService struct { - webhookRepository repository.IWebhookRepository - gameRepository repository.IGameRepository -} - -func NewWebhookService(r *repository.Repository) IWebhookService { - return &WebhookService{ - webhookRepository: r.WebhookRepository, - gameRepository: r.GameRepository, - } -} - -func (t *WebhookService) Create(webhook model.Webhook) (model.Webhook, error) { - return model.Webhook{}, nil -} - -func (t *WebhookService) Update(webhook model.Webhook) error { - return nil -} - -func (t *WebhookService) Delete(webhook model.Webhook) error { - return nil -} diff --git a/internal/utils/calculate/calculate.go b/internal/utils/calculate/calculate.go deleted file mode 100644 index ba58414c..00000000 --- a/internal/utils/calculate/calculate.go +++ /dev/null @@ -1,29 +0,0 @@ -package calculate - -import ( - "math" -) - -// ChallengePts Calculate the dynamic points of game. -// "S" is the maximum points, -// "R" is the minimum points. -// "d" is the degree of difficulty of the challenge. -// "x" is the number of submissions. -func ChallengePts(S, R, d, x int64) int64 { - ratio := float64(R) / float64(S) - result := int64(math.Floor(float64(S) * (ratio + (1-ratio)*math.Exp((1-float64(x))/float64(d))))) - return min(result, S) -} - -func GameChallengePts(S, R, d, x, rank int64, firstBloodRewardRatio, secondBloodRewardRatio, thirdBloodRewardRatio float64) int64 { - pts := ChallengePts(S, R, d, x) - switch rank { - case 0: - pts = int64(math.Floor(((firstBloodRewardRatio / 100) + 1) * float64(pts))) - case 1: - pts = int64(math.Floor(((secondBloodRewardRatio / 100) + 1) * float64(pts))) - case 2: - pts = int64(math.Floor(((thirdBloodRewardRatio / 100) + 1) * float64(pts))) - } - return pts -} diff --git a/internal/utils/const.go b/internal/utils/const.go deleted file mode 100644 index 42f627a8..00000000 --- a/internal/utils/const.go +++ /dev/null @@ -1,21 +0,0 @@ -package utils - -// Need to be injected by -ldflags -var ( - GitCommitID = "N/A" - GitBranch = "N/A" - GitTag = "N/A" -) - -const ( - ConfigsPath = "./configs" - MediaPath = "./media" - FilesPath = "./files" - CapturesPath = "./captures" - FrontendPath = "./dist" -) - -var ( - True = true - False = false -) diff --git a/internal/utils/convertor/convertor.go b/internal/utils/convertor/convertor.go deleted file mode 100644 index a01764a8..00000000 --- a/internal/utils/convertor/convertor.go +++ /dev/null @@ -1,96 +0,0 @@ -package convertor - -import ( - "github.com/duke-git/lancet/v2/convertor" - "strconv" -) - -func ToInt64D(v string, d int64) int64 { - result, err := convertor.ToInt(v) - if err != nil { - return d - } - return result -} - -func ToInt64P(v string) *int64 { - result, err := convertor.ToInt(v) - if err != nil { - return nil - } - return &result -} - -func ToIntD(v string, d int) int { - return int(ToInt64D(v, int64(d))) -} - -func ToIntP(v string) *int { - result64, err := convertor.ToInt(v) - if err != nil { - return nil - } - result := int(result64) - return &result -} - -func ToUintD(v string, d uint) uint { - return uint(ToInt64D(v, int64(d))) -} - -func ToUintE(v string) (uint, error) { - result64, err := convertor.ToInt(v) - if err != nil { - return 0, err - } - return uint(result64), nil -} - -func ToUintP(v string) *uint { - result64, err := convertor.ToInt(v) - if err != nil { - return nil - } - result := uint(result64) - return &result -} - -func ToBoolD(v string, d bool) bool { - result, err := convertor.ToBool(v) - if err != nil { - return d - } - return result -} - -func ToBoolP(v string) *bool { - result, err := convertor.ToBool(v) - if err != nil { - return nil - } - return &result -} - -func ToInt64SliceD(strSlice []string, d []int64) []int64 { - int64Slice := make([]int64, len(strSlice)) - for i, str := range strSlice { - num, err := strconv.Atoi(str) - if err != nil { - return d - } - int64Slice[i] = int64(num) - } - return int64Slice -} - -func ToUintSliceD(strSlice []string, d []uint) []uint { - uintSlice := make([]uint, len(strSlice)) - for i, str := range strSlice { - num, err := strconv.Atoi(str) - if err != nil { - return d - } - uintSlice[i] = uint(num) - } - return uintSlice -} diff --git a/internal/utils/utils.go b/internal/utils/utils.go deleted file mode 100644 index 03f97d95..00000000 --- a/internal/utils/utils.go +++ /dev/null @@ -1,25 +0,0 @@ -package utils - -import ( - "bytes" - "crypto/sha256" - "encoding/gob" - "fmt" - "github.com/google/uuid" - "strings" -) - -func HyphenlessUUID() string { - return strings.Replace(uuid.NewString(), "-", "", -1) -} - -func HashStruct(s interface{}) string { - var buf bytes.Buffer - enc := gob.NewEncoder(&buf) - err := enc.Encode(s) - if err != nil { - return "null" - } - hash := sha256.Sum256(buf.Bytes()) - return fmt.Sprintf("%x", hash) -} diff --git a/internal/utils/validator/validator.go b/internal/utils/validator/validator.go deleted file mode 100644 index 41e0d16c..00000000 --- a/internal/utils/validator/validator.go +++ /dev/null @@ -1,27 +0,0 @@ -package validator - -import ( - "errors" - "github.com/duke-git/lancet/v2/validator" - v10 "github.com/go-playground/validator/v10" - "reflect" -) - -func IsASCII(fl v10.FieldLevel) bool { - return validator.IsASCII(fl.Field().String()) -} - -// GetValidMsg Get the 'msg' tag of the field if error exists -func GetValidMsg(err error, obj any) string { - getObj := reflect.TypeOf(obj) - var errs v10.ValidationErrors - if errors.As(err, &errs) { - for _, e := range errs { - if f, exits := getObj.Elem().FieldByName(e.Field()); exits { - msg := f.Tag.Get("msg") - return msg - } - } - } - return err.Error() -} diff --git a/main.go b/main.go deleted file mode 100644 index e746d733..00000000 --- a/main.go +++ /dev/null @@ -1 +0,0 @@ -package Cloudsdale diff --git a/src/captcha/mod.rs b/src/captcha/mod.rs new file mode 100644 index 00000000..c12c2afd --- /dev/null +++ b/src/captcha/mod.rs @@ -0,0 +1,22 @@ +use traits::ICaptcha; + +pub mod recaptcha; +pub mod traits; +pub mod turnstile; + +pub enum Captcha { + Recaptcha(recaptcha::Recaptcha), + Turnstile(turnstile::Turnstile), +} + +impl Captcha { + pub fn new() -> Self { + return Captcha::Recaptcha(recaptcha::Recaptcha::new()); + } + pub async fn verify(&self, token: String, client_ip: String) -> bool { + match self { + Captcha::Recaptcha(recaptcha) => recaptcha.verify(token, client_ip).await, + Captcha::Turnstile(turnstile) => turnstile.verify(token, client_ip).await, + } + } +} diff --git a/src/captcha/recaptcha.rs b/src/captcha/recaptcha.rs new file mode 100644 index 00000000..a8cb4a7c --- /dev/null +++ b/src/captcha/recaptcha.rs @@ -0,0 +1,79 @@ +use reqwest::Client; +use serde::Serialize; + +use super::traits::ICaptcha; + +pub struct Recaptcha { + secret_key: String, + url: String, + threshold: f64, +} + +#[derive(Serialize)] +struct RecaptchaRequest { + #[serde(rename = "secret")] + secret_key: String, + #[serde(rename = "response")] + response: String, + #[serde(rename = "remoteip")] + remote_ip: String, +} + +impl ICaptcha for Recaptcha { + fn new() -> Self { + return Recaptcha { + url: crate::config::get_app_config() + .captcha + .recaptcha + .url + .clone(), + secret_key: crate::config::get_app_config() + .captcha + .recaptcha + .secret_key + .clone(), + threshold: crate::config::get_app_config().captcha.recaptcha.threshold, + }; + } + + async fn verify(&self, token: String, client_ip: String) -> bool { + let request_body = RecaptchaRequest { + secret_key: self.secret_key.clone(), + response: token, + remote_ip: client_ip, + }; + + let client = Client::new(); + let resp = client + .post(&self.url) + .json(&request_body) + .send() + .await + .unwrap(); + + let response: serde_json::Value = resp.json().await.unwrap(); + + match response.get("success") { + Some(result) => { + if let serde_json::Value::Bool(success) = result { + match response.get("score") { + Some(score) => { + if let serde_json::Value::Number(score) = score { + let score = score.as_f64().unwrap_or(0.0); + if *success && score >= self.threshold { + return true; + } else { + return false; + } + } + return false; + } + None => return false, + } + } + return false; + } + None => return false, + } + } +} diff --git a/src/captcha/traits.rs b/src/captcha/traits.rs new file mode 100644 index 00000000..0a20c28e --- /dev/null +++ b/src/captcha/traits.rs @@ -0,0 +1,8 @@ +pub trait ICaptcha { + fn new() -> Self; + fn verify( + &self, + token: String, + client_ip: String, + ) -> impl std::future::Future + Send; +} diff --git a/src/captcha/turnstile.rs b/src/captcha/turnstile.rs new file mode 100644 index 00000000..cb4a3071 --- /dev/null +++ b/src/captcha/turnstile.rs @@ -0,0 +1,59 @@ +use reqwest::Client; +use serde::Serialize; + +use super::traits::ICaptcha; + +pub struct Turnstile { + url: String, + secret_key: String, +} + +#[derive(Serialize)] +struct TurnstileRequest { + #[serde(rename = "secret")] + secret_key: String, + #[serde(rename = "response")] + response: String, + #[serde(rename = "remoteip")] + remote_ip: String, +} + +impl ICaptcha for Turnstile { + fn new() -> Self { + return Turnstile { + url: crate::config::get_app_config() + .captcha + .turnstile + .url + .clone(), + secret_key: crate::config::get_app_config() + .captcha + .turnstile + .secret_key + .clone(), + }; + } + + async fn verify(&self, token: String, client_ip: String) -> bool { + let request_body = TurnstileRequest { + secret_key: self.secret_key.clone(), + response: token, + remote_ip: client_ip, + }; + + let client = Client::new(); + let resp = client + .post(&self.url) + .json(&request_body) + .send() + .await + .unwrap(); + + let response: serde_json::Value = resp.json().await.unwrap(); + + match response.get("success") { + Some(success) => return success.as_bool().unwrap(), + None => return false, + } + } +} diff --git a/src/config/auth/jwt.rs b/src/config/auth/jwt.rs new file mode 100644 index 00000000..ed6f1220 --- /dev/null +++ b/src/config/auth/jwt.rs @@ -0,0 +1,7 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Config { + pub secret_key: String, + pub expiration: u64, +} diff --git a/src/config/auth/mod.rs b/src/config/auth/mod.rs new file mode 100644 index 00000000..236a433e --- /dev/null +++ b/src/config/auth/mod.rs @@ -0,0 +1,10 @@ +pub mod jwt; +pub mod registration; + +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Config { + pub jwt: jwt::Config, + pub registration: registration::Config, +} diff --git a/src/config/auth/registration/email.rs b/src/config/auth/registration/email.rs new file mode 100644 index 00000000..d00f8443 --- /dev/null +++ b/src/config/auth/registration/email.rs @@ -0,0 +1,7 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Config { + pub enabled: bool, + pub domains: Vec, +} diff --git a/src/config/auth/registration/mod.rs b/src/config/auth/registration/mod.rs new file mode 100644 index 00000000..8ad71da6 --- /dev/null +++ b/src/config/auth/registration/mod.rs @@ -0,0 +1,10 @@ +pub mod email; + +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Config { + pub enabled: bool, + pub captcha: bool, + pub email: email::Config, +} diff --git a/src/config/axum/cors.rs b/src/config/axum/cors.rs new file mode 100644 index 00000000..cb1f6068 --- /dev/null +++ b/src/config/axum/cors.rs @@ -0,0 +1,7 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Config { + pub allow_origins: Vec, + pub allow_methods: Vec, +} diff --git a/src/config/axum/mod.rs b/src/config/axum/mod.rs new file mode 100644 index 00000000..8de1b8be --- /dev/null +++ b/src/config/axum/mod.rs @@ -0,0 +1,10 @@ +pub mod cors; + +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Config { + pub cors: cors::Config, + pub host: String, + pub port: u16, +} diff --git a/src/config/cache/mod.rs b/src/config/cache/mod.rs new file mode 100644 index 00000000..420f5ff3 --- /dev/null +++ b/src/config/cache/mod.rs @@ -0,0 +1,9 @@ +pub mod redis; + +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Config { + pub provider: String, + pub redis: redis::Config, +} diff --git a/src/config/cache/redis.rs b/src/config/cache/redis.rs new file mode 100644 index 00000000..fb61d49d --- /dev/null +++ b/src/config/cache/redis.rs @@ -0,0 +1,9 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Config { + pub host: String, + pub port: u16, + pub password: String, + pub db: u8, +} diff --git a/src/config/captcha/mod.rs b/src/config/captcha/mod.rs new file mode 100644 index 00000000..b90c24e0 --- /dev/null +++ b/src/config/captcha/mod.rs @@ -0,0 +1,11 @@ +pub mod recaptcha; +pub mod turnstile; + +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Config { + pub provider: String, + pub turnstile: turnstile::Config, + pub recaptcha: recaptcha::Config, +} diff --git a/src/config/captcha/recaptcha.rs b/src/config/captcha/recaptcha.rs new file mode 100644 index 00000000..ab5425c4 --- /dev/null +++ b/src/config/captcha/recaptcha.rs @@ -0,0 +1,9 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Config { + pub url: String, + pub site_key: String, + pub secret_key: String, + pub threshold: f64, +} diff --git a/src/config/captcha/turnstile.rs b/src/config/captcha/turnstile.rs new file mode 100644 index 00000000..9c88a32c --- /dev/null +++ b/src/config/captcha/turnstile.rs @@ -0,0 +1,8 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Config { + pub url: String, + pub site_key: String, + pub secret_key: String, +} diff --git a/src/config/consts.rs b/src/config/consts.rs new file mode 100644 index 00000000..047103bd --- /dev/null +++ b/src/config/consts.rs @@ -0,0 +1,6 @@ +pub mod path { + pub const CONFIG: &str = "./configs"; + pub const MEDIA: &str = "./media"; + pub const FRONTEND: &str = "./dist"; + pub const CAPTURE: &str = "./captures"; +} \ No newline at end of file diff --git a/src/config/container/docker.rs b/src/config/container/docker.rs new file mode 100644 index 00000000..d0db9daf --- /dev/null +++ b/src/config/container/docker.rs @@ -0,0 +1,6 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Config { + pub uri: String, +} diff --git a/src/config/container/k8s.rs b/src/config/container/k8s.rs new file mode 100644 index 00000000..88e15f81 --- /dev/null +++ b/src/config/container/k8s.rs @@ -0,0 +1,7 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Config { + pub namespace: String, + pub path: String, +} diff --git a/src/config/container/mod.rs b/src/config/container/mod.rs new file mode 100644 index 00000000..b84c12b2 --- /dev/null +++ b/src/config/container/mod.rs @@ -0,0 +1,16 @@ +pub mod docker; +pub mod k8s; +pub mod proxy; +pub mod strategy; + +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Config { + pub provider: String, + pub entry: String, + pub docker: docker::Config, + pub k8s: k8s::Config, + pub proxy: proxy::Config, + pub strategy: strategy::Config, +} diff --git a/src/config/container/proxy.rs b/src/config/container/proxy.rs new file mode 100644 index 00000000..32c9582c --- /dev/null +++ b/src/config/container/proxy.rs @@ -0,0 +1,7 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Config { + pub enabled: bool, + pub traffic_capture: bool, +} diff --git a/src/config/container/strategy.rs b/src/config/container/strategy.rs new file mode 100644 index 00000000..376184ae --- /dev/null +++ b/src/config/container/strategy.rs @@ -0,0 +1,7 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Config { + pub parallel_limit: u64, + pub request_limit: u64, +} diff --git a/src/config/db/mod.rs b/src/config/db/mod.rs new file mode 100644 index 00000000..93d933f3 --- /dev/null +++ b/src/config/db/mod.rs @@ -0,0 +1,13 @@ +pub mod mysql; +pub mod postgres; +pub mod sqlite; + +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Config { + pub provider: String, + pub sqlite: sqlite::Config, + pub postgres: postgres::Config, + pub mysql: mysql::Config, +} diff --git a/src/config/db/mysql.rs b/src/config/db/mysql.rs new file mode 100644 index 00000000..1abfbb9a --- /dev/null +++ b/src/config/db/mysql.rs @@ -0,0 +1,10 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Config { + pub dbname: String, + pub host: String, + pub port: u16, + pub username: String, + pub password: String, +} diff --git a/src/config/db/postgres.rs b/src/config/db/postgres.rs new file mode 100644 index 00000000..63a18c1d --- /dev/null +++ b/src/config/db/postgres.rs @@ -0,0 +1,11 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Config { + pub dbname: String, + pub host: String, + pub port: u16, + pub username: String, + pub password: String, + pub sslmode: String, +} diff --git a/src/config/db/sqlite.rs b/src/config/db/sqlite.rs new file mode 100644 index 00000000..dd6ac6b6 --- /dev/null +++ b/src/config/db/sqlite.rs @@ -0,0 +1,6 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Config { + pub path: String, +} diff --git a/src/config/mod.rs b/src/config/mod.rs new file mode 100644 index 00000000..ea90164e --- /dev/null +++ b/src/config/mod.rs @@ -0,0 +1,42 @@ +pub mod auth; +pub mod axum; +pub mod cache; +pub mod captcha; +pub mod consts; +pub mod container; +pub mod db; +pub mod site; + +use serde::{Deserialize, Serialize}; +use std::{path::Path, process, sync::OnceLock}; +use tokio::fs::{self}; +use tracing::error; + +static APP_CONFIG: OnceLock = OnceLock::new(); + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Config { + pub site: site::Config, + pub auth: auth::Config, + pub axum: axum::Config, + pub container: container::Config, + pub captcha: captcha::Config, + pub db: db::Config, +} + +pub async fn init() { + let target_path = Path::new("application.yml"); + if target_path.exists() { + let content = fs::read_to_string("application.yml").await.unwrap(); + APP_CONFIG + .set(serde_yaml::from_str(&content).unwrap()) + .unwrap(); + } else { + error!("Configuration application.yml not found."); + process::exit(1); + } +} + +pub fn get_app_config() -> &'static Config { + return APP_CONFIG.get().unwrap(); +} diff --git a/src/config/site/mod.rs b/src/config/site/mod.rs new file mode 100644 index 00000000..87047a39 --- /dev/null +++ b/src/config/site/mod.rs @@ -0,0 +1,9 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Config { + pub title: String, + pub description: String, + pub color: String, + pub favicon: String, +} diff --git a/src/container/docker.rs b/src/container/docker.rs new file mode 100644 index 00000000..bf652010 --- /dev/null +++ b/src/container/docker.rs @@ -0,0 +1,162 @@ +use super::traits::Container; +use crate::{database::get_db, repository}; +use async_trait::async_trait; +use bollard::{ + container::{Config, CreateContainerOptions, StartContainerOptions}, + secret::{ContainerCreateResponse, HostConfig, PortBinding}, + Docker as DockerClient, +}; +use core::time; +use sea_orm::EntityTrait; +use std::{collections::HashMap, env, error::Error, process, sync::OnceLock}; +use tracing::{error, info}; + +static DOCKER_CLI: OnceLock = OnceLock::new(); + +fn get_docker_client() -> &'static DockerClient { + return DOCKER_CLI.get().unwrap(); +} + +async fn daemon() { + info!("Docker container daemon has been started."); + tokio::spawn(async { + let interval = time::Duration::from_secs(10); + loop { + let (pods, _) = repository::pod::find(None, None, None, None, None, None, Some(false)) + .await + .unwrap(); + for pod in pods { + let _ = get_docker_client() + .stop_container(pod.name.clone().as_str(), None) + .await; + let _ = get_docker_client() + .remove_container(pod.name.clone().as_str(), None) + .await; + crate::model::pod::Entity::delete_by_id(pod.id) + .exec(&get_db().await) + .await + .unwrap(); + info!("Cleaned up expired container: {0}", pod.name); + } + tokio::time::sleep(interval).await; + } + }); +} + +#[derive(Clone)] +pub struct Docker; + +impl Docker { + pub fn new() -> Self { + return Self {}; + } +} + +#[async_trait] +impl Container for Docker { + async fn init(&self) { + let docker_uri = &crate::config::get_app_config().container.docker.uri; + env::set_var("DOCKER_HOST", docker_uri); + let docker = DockerClient::connect_with_defaults().unwrap(); + match docker.ping().await { + Ok(_) => { + info!("Docker client initialized successfully."); + DOCKER_CLI.set(docker).unwrap(); + } + Err(e) => { + error!("Docker client initialization failed: {0:?}", e); + process::exit(1); + } + } + daemon().await; + } + + async fn create( + &self, + name: String, + challenge: crate::model::challenge::Model, + injected_flag: crate::model::challenge::Flag, + ) -> Result, Box> { + let port_bindings: HashMap>> = challenge + .ports + .into_iter() + .map(|port| { + ( + format!("{}/{}", port.value, port.protocol.to_lowercase()), + Some(vec![PortBinding { + host_ip: Some("0.0.0.0".to_string()), + host_port: None, + }]), + ) + }) + .collect(); + + let mut env_bindings: Vec = challenge + .envs + .into_iter() + .map(|env| format!("{}:{}", env.key, env.value)) + .collect(); + + env_bindings.push(format!( + "{}:{}", + injected_flag.env.unwrap_or("FLAG".to_string()), + injected_flag.value + )); + + let cfg = Config { + image: challenge.image_name.clone(), + host_config: Some(HostConfig { + memory: Some(challenge.memory_limit * 1024 * 1024), + cpu_shares: Some(challenge.cpu_limit), + port_bindings: Some(port_bindings), + ..Default::default() + }), + env: Some(env_bindings), + ..Default::default() + }; + + let _: ContainerCreateResponse = get_docker_client() + .create_container( + Some(CreateContainerOptions { + name: name.clone(), + platform: None, + }), + cfg, + ) + .await?; + + get_docker_client() + .start_container(name.clone().as_str(), None::>) + .await?; + + let container_info = get_docker_client().inspect_container(&name, None).await?; + let port_mappings = container_info.network_settings.unwrap().ports.unwrap(); + + let mut nats: Vec = Vec::new(); + for (port, bindings) in port_mappings { + if let Some(binding) = bindings { + for port_binding in binding { + if let Some((src, protocol)) = port.split_once("/") { + nats.push(crate::model::pod::Nat { + src: src.to_string(), + dst: port_binding.host_port.unwrap(), + protocol: protocol.to_string(), + ..Default::default() + }) + } + } + } + } + + return Ok(nats); + } + + async fn delete(&self, name: String) { + let _ = get_docker_client() + .stop_container(name.clone().as_str(), None) + .await; + let _ = get_docker_client() + .remove_container(name.clone().as_str(), None) + .await; + } +} diff --git a/src/container/k8s.rs b/src/container/k8s.rs new file mode 100644 index 00000000..8a518e0a --- /dev/null +++ b/src/container/k8s.rs @@ -0,0 +1,159 @@ +use super::traits::Container; +use async_trait::async_trait; +use k8s_openapi::api::core::v1::{Container as K8sContainer, ContainerPort, EnvVar, Pod, PodSpec}; +use kube::config::Kubeconfig; +use kube::runtime::wait::{await_condition, conditions}; +use kube::{ + api::{Api, DeleteParams, ListParams, PostParams, ResourceExt}, + Client as K8sClient, Config, +}; +use std::{error::Error, process, sync::OnceLock}; +use tokio::time::Duration; +use tracing::{error, info}; + +static K8S_CLIENT: OnceLock = OnceLock::new(); + +fn get_k8s_client() -> &'static K8sClient { + return K8S_CLIENT.get().unwrap(); +} + +async fn daemon() { + info!("Kubernetes container daemon has been started."); + tokio::spawn(async { + let interval = Duration::from_secs(10); + loop { + let pods: Api = Api::namespaced(get_k8s_client().clone(), "default"); + let lp = ListParams::default().labels("expired=true"); + + if let Ok(pod_list) = pods.list(&lp).await { + for pod in pod_list { + let name = pod.name_any(); + let _ = pods.delete(&name, &DeleteParams::default()).await; + info!("Cleaned up expired pod: {}", name); + } + } + + tokio::time::sleep(interval).await; + } + }); +} + +#[derive(Clone)] +pub struct K8s; + +impl K8s { + pub fn new() -> Self { + return Self {}; + } +} + +#[async_trait] +impl Container for K8s { + async fn init(&self) { + match Kubeconfig::read_from(crate::config::get_app_config().container.k8s.path.clone()) { + Ok(config) => match Config::from_custom_kubeconfig(config, &Default::default()).await { + Ok(config) => { + let client = K8sClient::try_from(config).unwrap(); + let _ = K8S_CLIENT.set(client); + info!("Kubernetes client initialized successfully."); + daemon().await; + } + Err(e) => { + error!( + "Failed to create Kubernetes client from custom config: {:?}", + e + ); + process::exit(1); + } + }, + Err(e) => { + error!("Failed to read Kubernetes config file: {:?}", e); + process::exit(1); + } + } + } + + async fn create( + &self, + name: String, + challenge: crate::model::challenge::Model, + injected_flag: crate::model::challenge::Flag, + ) -> Result, Box> { + let client = get_k8s_client().clone(); + let pods: Api = Api::namespaced(client, "default"); + + let mut env_vars: Vec = challenge + .envs + .into_iter() + .map(|env| EnvVar { + name: env.key, + value: Some(env.value), + ..Default::default() + }) + .collect(); + + env_vars.push(EnvVar { + name: injected_flag.env.unwrap_or("FLAG".to_string()), + value: Some(injected_flag.value), + ..Default::default() + }); + + let container_ports: Vec = challenge + .ports + .iter() + .map(|port| ContainerPort { + container_port: port.value as i32, + protocol: Some(port.protocol.to_uppercase()), + ..Default::default() + }) + .collect(); + + let container = K8sContainer { + name: name.clone(), + image: challenge.image_name.clone(), + env: Some(env_vars), + ports: Some(container_ports), + ..Default::default() + }; + + let pod_spec = PodSpec { + containers: vec![container], + ..Default::default() + }; + + let pod = Pod { + metadata: k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta { + name: Some(name.clone()), + ..Default::default() + }, + spec: Some(pod_spec), + ..Default::default() + }; + + pods.create(&PostParams::default(), &pod).await?; + + await_condition(pods.clone(), &name, conditions::is_pod_running()).await?; + + let pod = pods.get(&name).await?; + let mut nats: Vec = Vec::new(); + if let Some(status) = pod.status { + if let Some(pod_ip) = status.pod_ip { + for port in challenge.ports { + nats.push(crate::model::pod::Nat { + src: port.value.to_string(), + dst: pod_ip.clone(), + protocol: port.protocol.to_uppercase(), + ..Default::default() + }); + } + } + } + + return Ok(nats); + } + + async fn delete(&self, name: String) { + let pods: Api = Api::namespaced(get_k8s_client().clone(), "default"); + let _ = pods.delete(&name, &DeleteParams::default()).await; + } +} diff --git a/src/container/mod.rs b/src/container/mod.rs new file mode 100644 index 00000000..1fa86eca --- /dev/null +++ b/src/container/mod.rs @@ -0,0 +1,37 @@ +pub mod docker; +pub mod k8s; +pub mod traits; + +use std::sync::Arc; + +use once_cell::sync::Lazy; +use tokio::sync::Mutex; +use tracing::error; +use traits::Container; + +use crate::config::get_app_config; + +static PROVIDER: Lazy>>> = Lazy::new(|| Mutex::new(None)); + +pub async fn init() { + let provider: Arc = match get_app_config().container.provider.as_str() { + "docker" => Arc::new(docker::Docker::new()), + "k8s" => Arc::new(k8s::K8s::new()), + _ => { + error!("Unsupported container provider"); + return; + } + }; + + { + let mut global_container = PROVIDER.lock().await; + *global_container = Some(provider.clone()); + } + + get_container().await.init().await; +} + +pub async fn get_container() -> Arc { + let global_container = PROVIDER.lock().await; + return global_container.as_ref().unwrap().clone(); +} diff --git a/src/container/traits.rs b/src/container/traits.rs new file mode 100644 index 00000000..0a3729f7 --- /dev/null +++ b/src/container/traits.rs @@ -0,0 +1,14 @@ +use async_trait::async_trait; +use std::error::Error; + +#[async_trait] +pub trait Container: Send + Sync { + async fn init(&self); + async fn create( + &self, + name: String, + challenge: crate::model::challenge::Model, + injected_flag: crate::model::challenge::Flag, + ) -> Result, Box>; + async fn delete(&self, name: String); +} diff --git a/src/database/migration.rs b/src/database/migration.rs new file mode 100644 index 00000000..8af5309a --- /dev/null +++ b/src/database/migration.rs @@ -0,0 +1,29 @@ +use sea_orm::{ConnectionTrait, DbConn, EntityTrait, Schema}; +use tracing::error; + +async fn create_table(db: &DbConn, entity: E) +where + E: EntityTrait, +{ + let builder = db.get_database_backend(); + let schema = Schema::new(builder); + let stmt = builder.build(schema.create_table_from_entity(entity).if_not_exists()); + + match db.execute(stmt).await { + Err(e) => error!("Error: {}", e), + _ => {} + } +} + +pub async fn migrate(db: &DbConn) { + create_table(db, crate::model::user::Entity).await; + create_table(db, crate::model::team::Entity).await; + create_table(db, crate::model::user_team::Entity).await; + create_table(db, crate::model::category::Entity).await; + create_table(db, crate::model::challenge::Entity).await; + create_table(db, crate::model::submission::Entity).await; + create_table(db, crate::model::game::Entity).await; + create_table(db, crate::model::pod::Entity).await; + create_table(db, crate::model::game_challenge::Entity).await; + create_table(db, crate::model::game_team::Entity).await; +} diff --git a/src/database/mod.rs b/src/database/mod.rs new file mode 100644 index 00000000..78afafff --- /dev/null +++ b/src/database/mod.rs @@ -0,0 +1,136 @@ +mod migration; + +use bcrypt::{hash, DEFAULT_COST}; +use once_cell::sync::Lazy; +use sea_orm::{ + ActiveModelTrait, ConnectOptions, Database, DatabaseConnection, EntityTrait, PaginatorTrait, + Set, +}; +use std::{process, time::Duration}; +use tokio::sync::RwLock; +use tracing::{error, info}; + +static DB: Lazy>> = Lazy::new(|| RwLock::new(None)); + +pub async fn init() { + let url = match crate::config::get_app_config() + .db + .provider + .to_lowercase() + .as_str() + { + "sqlite" => format!( + "sqlite://{}?mode=rwc", + crate::config::get_app_config().db.sqlite.path.clone() + ), + "mysql" => format!( + "mysql://{}:{}@{}:{}/{}", + crate::config::get_app_config().db.mysql.username, + crate::config::get_app_config().db.mysql.password, + crate::config::get_app_config().db.mysql.host, + crate::config::get_app_config().db.mysql.port, + crate::config::get_app_config().db.mysql.dbname, + ), + "postgres" => format!( + "postgres://{}:{}@{}:{}/{}", + crate::config::get_app_config().db.postgres.username, + crate::config::get_app_config().db.postgres.password, + crate::config::get_app_config().db.postgres.host, + crate::config::get_app_config().db.postgres.port, + crate::config::get_app_config().db.postgres.dbname, + ), + _ => { + error!("Unsupported database provider"); + process::exit(1); + } + }; + let mut opt = ConnectOptions::new(url); + opt.max_connections(100) + .min_connections(5) + .connect_timeout(Duration::from_secs(8)) + .acquire_timeout(Duration::from_secs(8)) + .idle_timeout(Duration::from_secs(8)) + .max_lifetime(Duration::from_secs(8)) + .sqlx_logging(false) + .set_schema_search_path("cloudsdale"); + + let db: DatabaseConnection = Database::connect(opt).await.unwrap(); + { + let mut db_lock = DB.write().await; + *db_lock = Some(db); + } + migration::migrate(&get_db().await).await; + info!("Database connection established successfully."); + init_admin().await; + init_category().await; +} + +pub async fn get_db() -> DatabaseConnection { + let db_lock = DB.read().await; + return db_lock.clone().expect("Database not initialized"); +} + +pub async fn init_admin() { + let total = crate::model::user::Entity::find() + .count(&get_db().await) + .await + .unwrap(); + if total == 0 { + let hashed_password = hash("123456".to_string(), DEFAULT_COST).unwrap(); + let user = crate::model::user::ActiveModel { + username: Set("admin".to_string()), + nickname: Set("Administrator".to_string()), + email: Set(Some("admin@admin.com".to_string())), + group: Set("admin".to_string()), + password: Set(Some(hashed_password)), + ..Default::default() + }; + user.insert(&get_db().await).await.unwrap(); + info!("Admin user created successfully."); + } +} + +pub async fn init_category() { + let total = crate::model::category::Entity::find() + .count(&get_db().await) + .await + .unwrap(); + if total == 0 { + let default_categories = vec![ + crate::model::category::ActiveModel { + name: Set("web".to_string()), + color: Set("#009688".to_string()), + icon: Set("language".to_string()), + ..Default::default() + }, + crate::model::category::ActiveModel { + name: Set("pwn".to_string()), + color: Set("#673AB7".to_string()), + icon: Set("function".to_string()), + ..Default::default() + }, + crate::model::category::ActiveModel { + name: Set("crypto".to_string()), + color: Set("#607D8B".to_string()), + icon: Set("tag".to_string()), + ..Default::default() + }, + crate::model::category::ActiveModel { + name: Set("misc".to_string()), + color: Set("#3F51B5".to_string()), + icon: Set("fingerprint".to_string()), + ..Default::default() + }, + crate::model::category::ActiveModel { + name: Set("reverse".to_string()), + color: Set("#6D4C41".to_string()), + icon: Set("keyboard_double_arrow_left".to_string()), + ..Default::default() + }, + ]; + for categ0ry in default_categories { + categ0ry.insert(&get_db().await).await.unwrap(); + } + info!("Default category created successfully."); + } +} diff --git a/src/email/mod.rs b/src/email/mod.rs new file mode 100644 index 00000000..e69de29b diff --git a/src/logger/mod.rs b/src/logger/mod.rs new file mode 100644 index 00000000..c9bcd56a --- /dev/null +++ b/src/logger/mod.rs @@ -0,0 +1,27 @@ +use tracing::{info, Level}; +use tracing_subscriber::layer::SubscriberExt; +use tracing_subscriber::util::SubscriberInitExt; +use tracing_subscriber::{EnvFilter, Layer}; + +pub fn init() { + let filter = EnvFilter::from_default_env() + .add_directive(Level::TRACE.into()) + .add_directive(Level::DEBUG.into()) + .add_directive("docker_api=info".parse().unwrap()); + + let fmt_layer = tracing_subscriber::fmt::layer() + .with_target(false) + .with_filter(filter); + + // let file_layer = tracing_subscriber::fmt::layer() + // .with_ansi(false) + // .with_file(true) + // .with_line_number(true); + + tracing_subscriber::registry() + .with(fmt_layer) + // .with(file_layer) + .init(); + + info!("Logger initialized successfully."); +} diff --git a/internal/files/statics/banner.txt b/src/main.rs similarity index 50% rename from internal/files/statics/banner.txt rename to src/main.rs index eb7077fa..23e0c8c7 100644 --- a/internal/files/statics/banner.txt +++ b/src/main.rs @@ -1,3 +1,18 @@ +mod captcha; +mod config; +mod container; +mod database; +mod email; +mod logger; +mod media; +mod model; +mod proxy; +mod repository; +mod server; +mod traits; +mod util; + +const BANNER: &str = r#" _ _ _ _ ___/ | ___ _ _ __| |___ __| | __ _/ | ___ / __| |/ _ \| | | |/ _` / __|/ _` |/ _` | |/ _ \ @@ -6,4 +21,14 @@ Version {{.Version}} Commit: {{.Commit}} GitHub: https://github.com/elabosak233/cloudsdale +"#; + +#[tokio::main] +async fn main() { + println!( + "{}", + BANNER.replace("{{.Version}}", env!("CARGO_PKG_VERSION")) + ); + server::bootstrap().await; +} diff --git a/src/media/mod.rs b/src/media/mod.rs new file mode 100644 index 00000000..683262e8 --- /dev/null +++ b/src/media/mod.rs @@ -0,0 +1,65 @@ +use std::{error::Error, path::PathBuf}; + +use tokio::{ + fs::{create_dir_all, metadata, read_dir, remove_dir_all, File}, + io::{AsyncReadExt, AsyncWriteExt}, +}; + +pub async fn get(path: String, filename: String) -> Result, Box> { + let filepath = + PathBuf::from(crate::config::consts::path::MEDIA).join(format!("{}/{}", path, filename)); + + match File::open(&filepath).await { + Ok(mut file) => { + let mut buffer = Vec::new(); + if let Err(_) = file.read_to_end(&mut buffer).await { + return Err("internal_server_error".into()); + } + return Ok(buffer); + } + Err(_) => return Err("not_found".into()), + } +} + +pub async fn scan_dir(path: String) -> Result, Box> { + let filepath = PathBuf::from(crate::config::consts::path::MEDIA).join(path); + let mut files = Vec::new(); + + if metadata(&filepath).await.is_err() { + return Ok(files); + } + + let mut dir = read_dir(&filepath).await?; + + while let Some(entry) = dir.next_entry().await? { + let path = entry.path(); + let metadata = entry.metadata().await?; + if metadata.is_file() { + let file_name = path.file_name().unwrap().to_string_lossy().into_owned(); + let file_size = metadata.len(); + files.push((file_name, file_size)); + } + } + return Ok(files); +} + +pub async fn save(path: String, filename: String, data: Vec) -> Result<(), Box> { + let filepath = + PathBuf::from(crate::config::consts::path::MEDIA).join(format!("{}/{}", path, filename)); + if let Some(parent) = filepath.parent() { + if metadata(parent).await.is_err() { + create_dir_all(parent).await?; + } + } + let mut file = File::create(&filepath).await?; + file.write_all(&data).await?; + return Ok(()); +} + +pub async fn delete(path: String) -> Result<(), Box> { + let filepath = PathBuf::from(crate::config::consts::path::MEDIA).join(path); + if metadata(&filepath).await.is_ok() { + remove_dir_all(&filepath).await?; + } + return Ok(()); +} diff --git a/src/model/category/mod.rs b/src/model/category/mod.rs new file mode 100644 index 00000000..8464f91a --- /dev/null +++ b/src/model/category/mod.rs @@ -0,0 +1,39 @@ +pub mod request; + +use axum::async_trait; +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +use super::challenge; + +#[derive(Debug, Clone, PartialEq, Eq, DeriveEntityModel, Serialize, Deserialize)] +#[sea_orm(table_name = "categories")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i64, + pub name: String, + pub color: String, + pub icon: String, +} + +#[derive(Copy, Clone, Debug, EnumIter)] +pub enum Relation { + Challenge, +} + +impl RelationTrait for Relation { + fn def(&self) -> RelationDef { + match self { + Self::Challenge => Entity::has_many(challenge::Entity).into(), + } + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Challenge.def() + } +} + +#[async_trait] +impl ActiveModelBehavior for ActiveModel {} diff --git a/src/model/category/request.rs b/src/model/category/request.rs new file mode 100644 index 00000000..38b8de13 --- /dev/null +++ b/src/model/category/request.rs @@ -0,0 +1,49 @@ +use sea_orm::{ActiveValue::NotSet, Set}; +use serde::{Deserialize, Serialize}; +use validator::Validate; + +use super::ActiveModel; + +#[derive(Debug, Serialize, Deserialize)] +pub struct FindRequest { + pub id: Option, + pub name: Option, +} + +#[derive(Debug, Deserialize, Serialize, Validate)] +pub struct CreateRequest { + pub name: String, + pub color: String, + pub icon: String, +} + +impl From for super::ActiveModel { + fn from(req: CreateRequest) -> Self { + Self { + name: Set(req.name), + color: Set(req.color), + icon: Set(req.icon), + ..Default::default() + } + } +} + +#[derive(Debug, Deserialize, Serialize, Validate)] +pub struct UpdateRequest { + pub id: Option, + pub name: Option, + pub color: Option, + pub icon: Option, +} + +impl From for ActiveModel { + fn from(req: UpdateRequest) -> Self { + Self { + id: req.id.map_or(NotSet, |v| Set(v)), + name: req.name.map_or(NotSet, |v| Set(v)), + color: req.color.map_or(NotSet, |v| Set(v)), + icon: req.icon.map_or(NotSet, |v| Set(v)), + ..Default::default() + } + } +} diff --git a/src/model/challenge/mod.rs b/src/model/challenge/mod.rs new file mode 100644 index 00000000..6fce30bd --- /dev/null +++ b/src/model/challenge/mod.rs @@ -0,0 +1,124 @@ +pub mod request; +pub mod response; + +use axum::async_trait; +use sea_orm::{entity::prelude::*, FromJsonQueryResult, Set}; +use serde::{Deserialize, Serialize}; + +use super::{category, game, game_challenge, pod, submission}; + +#[derive(Debug, Clone, PartialEq, Eq, DeriveEntityModel, Serialize, Deserialize)] +#[sea_orm(table_name = "challenges")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i64, + pub title: String, + pub description: Option, + pub category_id: i64, + #[sea_orm(default_value = false)] + pub is_dynamic: bool, + #[sea_orm(default_value = false)] + pub has_attachment: bool, + #[sea_orm(default_value = false)] + pub is_practicable: bool, + pub image_name: Option, + #[sea_orm(default_value = 0)] + pub cpu_limit: i64, + #[sea_orm(default_value = 0)] + pub memory_limit: i64, + #[sea_orm(default_value = 1800)] + pub duration: i64, + #[sea_orm(column_type = "Json", default_value = "[]")] + pub ports: Vec, + #[sea_orm(column_type = "Json", default_value = "[]")] + pub envs: Vec, + #[sea_orm(column_type = "Json", default_value = "[]")] + pub flags: Vec, + pub created_at: i64, + pub updated_at: i64, +} + +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, FromJsonQueryResult)] +pub struct Env { + pub key: String, + pub value: String, +} + +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, FromJsonQueryResult)] +pub struct Port { + pub value: i64, + pub protocol: String, +} + +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, FromJsonQueryResult)] +pub struct Flag { + #[serde(rename = "type")] + pub type_: String, + pub banned: bool, + pub env: Option, + pub value: String, +} + +#[derive(Copy, Clone, Debug, EnumIter)] +pub enum Relation { + Category, + Submission, + Pod, +} + +impl RelationTrait for Relation { + fn def(&self) -> RelationDef { + match self { + Self::Category => { + return Entity::belongs_to(category::Entity) + .from(Column::CategoryId) + .to(category::Column::Id) + .on_delete(ForeignKeyAction::Cascade) + .into() + } + Self::Submission => return Entity::has_many(submission::Entity).into(), + Self::Pod => return Entity::has_many(pod::Entity).into(), + } + } +} + +impl Related for Entity { + fn to() -> RelationDef { + return Relation::Category.def(); + } +} + +impl Related for Entity { + fn to() -> RelationDef { + return Relation::Submission.def(); + } +} + +impl Related for Entity { + fn to() -> RelationDef { + return game_challenge::Relation::Game.def(); + } + + fn via() -> Option { + return Some(game_challenge::Relation::Challenge.def().rev()); + } +} + +#[async_trait] +impl ActiveModelBehavior for ActiveModel { + fn new() -> Self { + return Self { + created_at: Set(chrono::Utc::now().timestamp()), + updated_at: Set(chrono::Utc::now().timestamp()), + ..ActiveModelTrait::default() + }; + } + + async fn before_save(mut self, _db: &C, _insert: bool) -> Result + where + C: ConnectionTrait, + { + self.updated_at = Set(chrono::Utc::now().timestamp()); + return Ok(self); + } +} diff --git a/src/model/challenge/request.rs b/src/model/challenge/request.rs new file mode 100644 index 00000000..9c403f85 --- /dev/null +++ b/src/model/challenge/request.rs @@ -0,0 +1,106 @@ +use sea_orm::{ActiveValue::NotSet, Set}; +use serde::{Deserialize, Serialize}; +use validator::Validate; + +use super::ActiveModel; + +#[derive(Debug, Serialize, Deserialize)] +pub struct FindRequest { + pub id: Option, + pub title: Option, + pub category_id: Option, + pub is_practicable: Option, + pub is_dynamic: Option, + pub is_detailed: Option, + pub user_id: Option, + pub page: Option, + pub size: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct CreateRequest { + pub title: String, + pub description: String, + pub category_id: i64, + pub is_practicable: Option, + pub is_dynamic: Option, + pub has_attachment: Option, + pub difficulty: Option, + pub image_name: Option, + pub cpu_limit: Option, + pub memory_limit: Option, + pub duration: Option, + pub ports: Option>, + pub envs: Option>, + pub flags: Option>, +} + +impl From for super::ActiveModel { + fn from(req: CreateRequest) -> Self { + Self { + title: Set(req.title), + description: Set(Some(req.description)), + category_id: Set(req.category_id), + is_practicable: Set(req.is_practicable.unwrap_or(false)), + is_dynamic: Set(req.is_dynamic.unwrap_or(false)), + has_attachment: Set(req.has_attachment.unwrap_or(false)), + image_name: Set(req.image_name), + cpu_limit: Set(req.cpu_limit.unwrap_or(0)), + memory_limit: Set(req.memory_limit.unwrap_or(0)), + duration: Set(req.duration.unwrap_or(1800)), + ports: Set(req.ports.unwrap_or(vec![])), + envs: Set(req.envs.unwrap_or(vec![])), + flags: Set(req.flags.unwrap_or(vec![])), + ..Default::default() + } + } +} + +#[derive(Debug, Serialize, Deserialize, Validate)] +pub struct UpdateRequest { + pub id: Option, + pub title: Option, + pub description: Option, + pub category_id: Option, + pub is_practicable: Option, + pub is_dynamic: Option, + pub has_attachment: Option, + pub difficulty: Option, + pub image_name: Option, + pub cpu_limit: Option, + pub memory_limit: Option, + pub duration: Option, + pub ports: Option>, + pub envs: Option>, + pub flags: Option>, +} + +impl From for ActiveModel { + fn from(req: UpdateRequest) -> Self { + Self { + id: req.id.map_or(NotSet, |v| Set(v)), + title: req.title.map_or(NotSet, |v| Set(v)), + description: req.description.map_or(NotSet, |v| Set(Some(v))), + category_id: req.category_id.map_or(NotSet, |v| Set(v)), + is_practicable: req.is_practicable.map_or(NotSet, |v| Set(v)), + is_dynamic: req.is_dynamic.map_or(NotSet, |v| Set(v)), + has_attachment: req.has_attachment.map_or(NotSet, |v| Set(v)), + image_name: req.image_name.map_or(NotSet, |v| Set(Some(v))), + cpu_limit: req.cpu_limit.map_or(NotSet, |v| Set(v)), + memory_limit: req.memory_limit.map_or(NotSet, |v| Set(v)), + duration: req.duration.map_or(NotSet, |v| Set(v)), + ports: req.ports.map_or(NotSet, |v| Set(v)), + envs: req.envs.map_or(NotSet, |v| Set(v)), + flags: req.flags.map_or(NotSet, |v| Set(v)), + ..Default::default() + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct StatusRequest { + pub cids: Vec, + pub user_id: Option, + pub team_id: Option, + pub game_id: Option, +} diff --git a/src/model/challenge/response.rs b/src/model/challenge/response.rs new file mode 100644 index 00000000..f5b00241 --- /dev/null +++ b/src/model/challenge/response.rs @@ -0,0 +1,10 @@ +use serde::{Deserialize, Serialize}; + +use super::submission; + +#[derive(Debug, Serialize, Deserialize)] +pub struct StatusResponse { + pub is_solved: bool, + pub solved_times: i64, + pub bloods: Vec, +} diff --git a/src/model/game/mod.rs b/src/model/game/mod.rs new file mode 100644 index 00000000..9fadd5cb --- /dev/null +++ b/src/model/game/mod.rs @@ -0,0 +1,91 @@ +pub mod request; + +use axum::async_trait; +use sea_orm::{entity::prelude::*, Set}; +use serde::{Deserialize, Serialize}; + +use super::{challenge, game_challenge, game_team, pod, submission, team}; + +#[derive(Debug, Clone, PartialEq, Eq, DeriveEntityModel, Serialize, Deserialize)] +#[sea_orm(table_name = "games")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i64, + pub title: String, + pub bio: Option, + pub description: Option, + pub is_enabled: bool, + pub is_public: bool, + #[sea_orm(default_value = 3)] + pub member_limit_min: i64, + #[sea_orm(default_value = 3)] + pub member_limit_max: i64, + #[sea_orm(default_value = 2)] + pub parallel_container_limit: i64, + #[sea_orm(default_value = 5)] + pub first_blood_reward_ratio: i64, + #[sea_orm(default_value = 3)] + pub second_blood_reward_ratio: i64, + #[sea_orm(default_value = 1)] + pub third_blood_reward_ratio: i64, + #[sea_orm(default_value = false)] + pub is_need_write_up: bool, + pub started_at: i64, + pub ended_at: i64, + pub created_at: i64, + pub updated_at: i64, +} + +#[derive(Copy, Clone, Debug, EnumIter)] +pub enum Relation { + Submission, + Pod, +} + +impl RelationTrait for Relation { + fn def(&self) -> RelationDef { + match self { + Self::Submission => Entity::has_many(submission::Entity).into(), + Self::Pod => Entity::has_many(pod::Entity).into(), + } + } +} + +impl Related for Entity { + fn to() -> RelationDef { + game_team::Relation::Team.def() + } + + fn via() -> Option { + Some(game_team::Relation::Game.def().rev()) + } +} + +impl Related for Entity { + fn to() -> RelationDef { + game_challenge::Relation::Challenge.def() + } + + fn via() -> Option { + Some(game_challenge::Relation::Game.def().rev()) + } +} + +#[async_trait] +impl ActiveModelBehavior for ActiveModel { + fn new() -> Self { + Self { + created_at: Set(chrono::Utc::now().timestamp()), + updated_at: Set(chrono::Utc::now().timestamp()), + ..ActiveModelTrait::default() + } + } + + async fn before_save(mut self, _db: &C, _insert: bool) -> Result + where + C: ConnectionTrait, + { + self.updated_at = Set(chrono::Utc::now().timestamp()); + Ok(self) + } +} diff --git a/src/model/game/request.rs b/src/model/game/request.rs new file mode 100644 index 00000000..72778b0f --- /dev/null +++ b/src/model/game/request.rs @@ -0,0 +1,114 @@ +use sea_orm::{ActiveValue::NotSet, Set}; +use serde::{Deserialize, Serialize}; +use validator::Validate; + +use super::ActiveModel; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct FindRequest { + pub id: Option, + pub title: Option, + pub is_enabled: Option, + pub page: Option, + pub size: Option, +} + +impl Default for FindRequest { + fn default() -> Self { + FindRequest { + id: None, + title: None, + is_enabled: None, + page: None, + size: None, + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, Validate)] +pub struct CreateRequest { + pub title: String, + pub bio: Option, + pub description: Option, + pub is_enabled: Option, + pub is_public: Option, + pub member_limit_min: Option, + pub member_limit_max: Option, + pub parallel_container_limit: Option, + pub first_blood_reward_ratio: Option, + pub second_blood_reward_ratio: Option, + pub third_blood_reward_ratio: Option, + pub is_need_write_up: Option, + pub started_at: Option, + pub ended_at: Option, +} + +impl From for ActiveModel { + fn from(req: CreateRequest) -> Self { + Self { + title: Set(req.title), + bio: Set(req.bio), + description: Set(req.description), + is_enabled: Set(req.is_enabled.unwrap_or(false)), + is_public: Set(req.is_public.unwrap_or(false)), + + member_limit_min: req.member_limit_min.map_or(NotSet, |v| Set(v)), + member_limit_max: req.member_limit_max.map_or(NotSet, |v| Set(v)), + parallel_container_limit: req.parallel_container_limit.map_or(NotSet, |v| Set(v)), + + first_blood_reward_ratio: req.first_blood_reward_ratio.map_or(NotSet, |v| Set(v)), + second_blood_reward_ratio: req.second_blood_reward_ratio.map_or(NotSet, |v| Set(v)), + third_blood_reward_ratio: req.third_blood_reward_ratio.map_or(NotSet, |v| Set(v)), + + is_need_write_up: Set(req.is_need_write_up.unwrap_or(false)), + started_at: req.started_at.map_or(NotSet, |v| Set(v)), + ended_at: req.ended_at.map_or(NotSet, |v| Set(v)), + ..Default::default() + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, Validate)] +pub struct UpdateRequest { + pub id: Option, + pub title: Option, + pub bio: Option, + pub description: Option, + pub is_enabled: Option, + pub is_public: Option, + pub member_limit_min: Option, + pub member_limit_max: Option, + pub parallel_container_limit: Option, + pub first_blood_reward_ratio: Option, + pub second_blood_reward_ratio: Option, + pub third_blood_reward_ratio: Option, + pub is_need_write_up: Option, + pub started_at: Option, + pub ended_at: Option, +} + +impl From for ActiveModel { + fn from(req: UpdateRequest) -> Self { + Self { + id: req.id.map_or(NotSet, |v| Set(v)), + title: req.title.map_or(NotSet, |v| Set(v)), + bio: req.bio.map_or(NotSet, |v| Set(Some(v))), + description: req.description.map_or(NotSet, |v| Set(Some(v))), + is_enabled: req.is_enabled.map_or(NotSet, |v| Set(v)), + is_public: req.is_public.map_or(NotSet, |v| Set(v)), + + member_limit_min: req.member_limit_min.map_or(NotSet, |v| Set(v)), + member_limit_max: req.member_limit_max.map_or(NotSet, |v| Set(v)), + parallel_container_limit: req.parallel_container_limit.map_or(NotSet, |v| Set(v)), + + first_blood_reward_ratio: req.first_blood_reward_ratio.map_or(NotSet, |v| Set(v)), + second_blood_reward_ratio: req.second_blood_reward_ratio.map_or(NotSet, |v| Set(v)), + third_blood_reward_ratio: req.third_blood_reward_ratio.map_or(NotSet, |v| Set(v)), + + is_need_write_up: req.is_need_write_up.map_or(NotSet, |v| Set(v)), + started_at: req.started_at.map_or(NotSet, |v| Set(v)), + ended_at: req.ended_at.map_or(NotSet, |v| Set(v)), + ..Default::default() + } + } +} diff --git a/src/model/game_challenge/mod.rs b/src/model/game_challenge/mod.rs new file mode 100644 index 00000000..eb4dde01 --- /dev/null +++ b/src/model/game_challenge/mod.rs @@ -0,0 +1,65 @@ +pub mod request; + +use axum::async_trait; +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +use super::{challenge, game}; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize, Deserialize)] +#[sea_orm(table_name = "game_challenges")] +pub struct Model { + #[sea_orm(primary_key)] + pub game_id: i64, + #[sea_orm(primary_key)] + pub challenge_id: i64, + #[sea_orm(default_value = 1)] + pub difficulty: i64, + #[sea_orm(default_value = false)] + pub is_enabled: bool, + #[sea_orm(default_value = 2000)] + pub max_pts: i64, + #[sea_orm(default_value = 200)] + pub min_pts: i64, + + #[sea_orm(ignore)] + pub challenge: Option, +} + +#[derive(Copy, Clone, Debug, EnumIter)] +pub enum Relation { + Game, + Challenge, +} + +impl RelationTrait for Relation { + fn def(&self) -> RelationDef { + match self { + Self::Game => Entity::belongs_to(game::Entity) + .from(Column::GameId) + .to(game::Column::Id) + .on_delete(ForeignKeyAction::Cascade) + .into(), + Self::Challenge => Entity::belongs_to(challenge::Entity) + .from(Column::ChallengeId) + .to(challenge::Column::Id) + .on_delete(ForeignKeyAction::Cascade) + .into(), + } + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Challenge.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Game.def() + } +} + +#[async_trait] +impl ActiveModelBehavior for ActiveModel {} diff --git a/src/model/game_challenge/request.rs b/src/model/game_challenge/request.rs new file mode 100644 index 00000000..7940e593 --- /dev/null +++ b/src/model/game_challenge/request.rs @@ -0,0 +1,69 @@ +use sea_orm::{ActiveValue::NotSet, Set}; +use serde::{Deserialize, Serialize}; + +use super::ActiveModel; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct FindRequest { + pub game_id: Option, + pub challenge_id: Option, + pub team_id: Option, + pub is_enabled: Option, +} + +impl Default for FindRequest { + fn default() -> Self { + FindRequest { + game_id: None, + challenge_id: None, + team_id: None, + is_enabled: None, + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct CreateRequest { + pub game_id: i64, + pub challenge_id: i64, + pub is_enabled: Option, + pub difficulty: Option, + pub max_pts: Option, + pub min_pts: Option, +} + +impl From for ActiveModel { + fn from(req: CreateRequest) -> Self { + Self { + game_id: Set(req.game_id), + challenge_id: Set(req.challenge_id), + difficulty: req.difficulty.map_or(NotSet, |v| Set(v)), + is_enabled: req.is_enabled.map_or(NotSet, |v| Set(v)), + max_pts: req.max_pts.map_or(NotSet, |v| Set(v)), + min_pts: req.min_pts.map_or(NotSet, |v| Set(v)), + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct UpdateRequest { + pub game_id: Option, + pub challenge_id: Option, + pub is_enabled: Option, + pub difficulty: Option, + pub max_pts: Option, + pub min_pts: Option, +} + +impl From for ActiveModel { + fn from(req: UpdateRequest) -> Self { + Self { + game_id: req.game_id.map_or(NotSet, |v| Set(v)), + challenge_id: req.challenge_id.map_or(NotSet, |v| Set(v)), + difficulty: req.difficulty.map_or(NotSet, |v| Set(v)), + is_enabled: req.is_enabled.map_or(NotSet, |v| Set(v)), + max_pts: req.max_pts.map_or(NotSet, |v| Set(v)), + min_pts: req.min_pts.map_or(NotSet, |v| Set(v)), + } + } +} diff --git a/src/model/game_team/mod.rs b/src/model/game_team/mod.rs new file mode 100644 index 00000000..56ab9822 --- /dev/null +++ b/src/model/game_team/mod.rs @@ -0,0 +1,49 @@ +pub mod request; + +use axum::async_trait; +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +use super::{game, team}; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize, Deserialize)] +#[sea_orm(table_name = "game_teams")] +pub struct Model { + #[sea_orm(primary_key)] + pub game_id: i64, + #[sea_orm(primary_key)] + pub team_id: i64, + #[sea_orm(default_value = false)] + pub is_allowed: bool, + + #[sea_orm(ignore)] + pub game: Option, + #[sea_orm(ignore)] + pub team: Option, +} + +#[derive(Copy, Clone, Debug, EnumIter)] +pub enum Relation { + Game, + Team, +} + +impl RelationTrait for Relation { + fn def(&self) -> RelationDef { + match self { + Self::Game => Entity::belongs_to(game::Entity) + .from(Column::GameId) + .to(game::Column::Id) + .on_delete(ForeignKeyAction::Cascade) + .into(), + Self::Team => Entity::belongs_to(team::Entity) + .from(Column::TeamId) + .to(team::Column::Id) + .on_delete(ForeignKeyAction::Cascade) + .into(), + } + } +} + +#[async_trait] +impl ActiveModelBehavior for ActiveModel {} diff --git a/src/model/game_team/request.rs b/src/model/game_team/request.rs new file mode 100644 index 00000000..3af05dc4 --- /dev/null +++ b/src/model/game_team/request.rs @@ -0,0 +1,53 @@ +use sea_orm::{ActiveValue::NotSet, Set}; +use serde::{Deserialize, Serialize}; + +use super::ActiveModel; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct FindRequest { + pub game_id: Option, + pub team_id: Option, +} + +impl Default for FindRequest { + fn default() -> Self { + FindRequest { + game_id: None, + team_id: None, + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct CreateRequest { + pub game_id: i64, + pub team_id: i64, +} + +impl From for ActiveModel { + fn from(req: CreateRequest) -> Self { + Self { + game_id: Set(req.game_id), + team_id: Set(req.team_id), + ..Default::default() + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct UpdateRequest { + pub game_id: Option, + pub team_id: Option, + pub is_allowed: Option, +} + +impl From for ActiveModel { + fn from(req: UpdateRequest) -> Self { + Self { + game_id: req.game_id.map_or(NotSet, |v| Set(v)), + team_id: req.team_id.map_or(NotSet, |v| Set(v)), + is_allowed: req.is_allowed.map_or(NotSet, |v| Set(v)), + ..Default::default() + } + } +} diff --git a/src/model/mod.rs b/src/model/mod.rs new file mode 100644 index 00000000..18865f3f --- /dev/null +++ b/src/model/mod.rs @@ -0,0 +1,10 @@ +pub mod category; +pub mod challenge; +pub mod game; +pub mod game_challenge; +pub mod game_team; +pub mod pod; +pub mod submission; +pub mod team; +pub mod user; +pub mod user_team; diff --git a/src/model/pod/mod.rs b/src/model/pod/mod.rs new file mode 100644 index 00000000..9b795ba3 --- /dev/null +++ b/src/model/pod/mod.rs @@ -0,0 +1,105 @@ +pub mod request; + +use axum::async_trait; +use sea_orm::{entity::prelude::*, FromJsonQueryResult, Set}; +use serde::{Deserialize, Serialize}; + +use super::{challenge, game, team, user}; + +#[derive(Debug, Clone, PartialEq, Eq, DeriveEntityModel, Serialize, Deserialize)] +#[sea_orm(table_name = "pods")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i64, + pub name: String, + pub flag: Option, // The generated flag, which will be injected into the container. + pub user_id: i64, + pub team_id: Option, + pub game_id: Option, + pub challenge_id: i64, + #[sea_orm(column_type = "Json", default_value = "[]")] + pub nats: Vec, + pub removed_at: i64, + pub created_at: i64, + + #[sea_orm(ignore)] + pub user: Option, + #[sea_orm(ignore)] + pub team: Option, + #[sea_orm(ignore)] + pub challenge: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, FromJsonQueryResult, Default)] +pub struct Nat { + pub src: String, + pub dst: String, + pub protocol: String, + pub proxy: Option, + pub entry: Option, +} + +#[derive(Copy, Clone, Debug, EnumIter)] +pub enum Relation { + Challenge, + User, + Team, + Game, +} + +impl RelationTrait for Relation { + fn def(&self) -> RelationDef { + match self { + Self::Challenge => Entity::belongs_to(challenge::Entity) + .from(Column::ChallengeId) + .to(challenge::Column::Id) + .into(), + Self::User => Entity::belongs_to(user::Entity) + .from(Column::UserId) + .to(user::Column::Id) + .into(), + Self::Team => Entity::belongs_to(team::Entity) + .from(Column::TeamId) + .to(team::Column::Id) + .into(), + Self::Game => Entity::belongs_to(game::Entity) + .from(Column::GameId) + .to(game::Column::Id) + .into(), + } + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Challenge.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::User.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Team.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Game.def() + } +} + +#[async_trait] +impl ActiveModelBehavior for ActiveModel { + fn new() -> Self { + Self { + created_at: Set(chrono::Utc::now().timestamp()), + ..ActiveModelTrait::default() + } + } +} diff --git a/src/model/pod/request.rs b/src/model/pod/request.rs new file mode 100644 index 00000000..d1852eb8 --- /dev/null +++ b/src/model/pod/request.rs @@ -0,0 +1,49 @@ +use sea_orm::Set; +use serde::{Deserialize, Serialize}; +#[derive(Debug, Serialize, Deserialize)] +pub struct FindRequest { + pub id: Option, + pub name: Option, + pub user_id: Option, + pub team_id: Option, + pub game_id: Option, + pub challenge_id: Option, + pub is_available: Option, + pub is_detailed: Option, + pub page: Option, + pub size: Option, +} + +impl Default for FindRequest { + fn default() -> Self { + FindRequest { + id: None, + name: None, + user_id: None, + team_id: None, + game_id: None, + challenge_id: None, + is_available: None, + is_detailed: Some(false), + page: None, + size: None, + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct CreateRequest { + pub challenge_id: i64, + pub team_id: Option, + pub user_id: Option, + pub game_id: Option, +} + +impl From for super::ActiveModel { + fn from(req: CreateRequest) -> Self { + Self { + challenge_id: Set(req.challenge_id), + ..Default::default() + } + } +} diff --git a/src/model/submission/mod.rs b/src/model/submission/mod.rs new file mode 100644 index 00000000..d9b1a85c --- /dev/null +++ b/src/model/submission/mod.rs @@ -0,0 +1,125 @@ +pub mod request; + +use axum::async_trait; +use sea_orm::{entity::prelude::*, Set}; +use serde::{Deserialize, Serialize}; + +use super::{challenge, game, team, user}; + +#[derive(Debug, Clone, PartialEq, Eq, DeriveEntityModel, Serialize, Deserialize)] +#[sea_orm(table_name = "submissions")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i64, + pub flag: String, + pub status: i64, + pub rank: i64, + pub user_id: i64, + pub team_id: Option, + pub game_id: Option, + pub challenge_id: i64, + pub created_at: i64, + pub updated_at: i64, + + #[sea_orm(ignore)] + pub user: Option, + #[sea_orm(ignore)] + pub team: Option, + #[sea_orm(ignore)] + pub game: Option, + #[sea_orm(ignore)] + pub challenge: Option, +} + +impl Model { + pub fn simplify(&mut self) { + self.flag = "".to_string(); + if let Some(user) = &mut self.user { + user.simplify(); + } + if let Some(team) = &mut self.team { + team.simplify(); + } + // if let Some(game) = &mut self.game { + // game.simplify(); + // } + } +} + +#[derive(Copy, Clone, Debug, EnumIter)] +pub enum Relation { + Challenge, + User, + Team, + Game, +} + +impl RelationTrait for Relation { + fn def(&self) -> RelationDef { + match self { + Self::Challenge => Entity::belongs_to(challenge::Entity) + .from(Column::ChallengeId) + .to(challenge::Column::Id) + .on_delete(ForeignKeyAction::Cascade) + .into(), + Self::User => Entity::belongs_to(user::Entity) + .from(Column::UserId) + .to(user::Column::Id) + .on_delete(ForeignKeyAction::Cascade) + .into(), + Self::Team => Entity::belongs_to(team::Entity) + .from(Column::TeamId) + .to(team::Column::Id) + .on_delete(ForeignKeyAction::Cascade) + .into(), + Self::Game => Entity::belongs_to(game::Entity) + .from(Column::GameId) + .to(game::Column::Id) + .on_delete(ForeignKeyAction::Cascade) + .into(), + } + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Challenge.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::User.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Team.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Game.def() + } +} + +#[async_trait] +impl ActiveModelBehavior for ActiveModel { + fn new() -> Self { + Self { + created_at: Set(chrono::Utc::now().timestamp()), + updated_at: Set(chrono::Utc::now().timestamp()), + ..ActiveModelTrait::default() + } + } + + async fn before_save(mut self, _db: &C, _insert: bool) -> Result + where + C: ConnectionTrait, + { + self.updated_at = Set(chrono::Utc::now().timestamp()); + Ok(self) + } +} diff --git a/src/model/submission/request.rs b/src/model/submission/request.rs new file mode 100644 index 00000000..5f97d1ba --- /dev/null +++ b/src/model/submission/request.rs @@ -0,0 +1,70 @@ +use sea_orm::{ActiveValue::NotSet, Set}; +use serde::{Deserialize, Serialize}; +use validator::Validate; + +use super::ActiveModel; + +#[derive(Debug, Serialize, Deserialize)] +pub struct FindRequest { + pub id: Option, + pub user_id: Option, + pub team_id: Option, + pub game_id: Option, + pub challenge_id: Option, + pub status: Option, + pub is_detailed: Option, + pub page: Option, + pub size: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct CreateRequest { + pub flag: String, + pub user_id: Option, + pub team_id: Option, + pub game_id: Option, + pub challenge_id: Option, +} + +impl From for super::ActiveModel { + fn from(req: CreateRequest) -> Self { + Self { + flag: Set(req.flag), + user_id: req.user_id.map_or(NotSet, |v| Set(v)), + team_id: req.team_id.map_or(NotSet, |v| Set(Some(v))), + game_id: req.game_id.map_or(NotSet, |v| Set(Some(v))), + challenge_id: req.challenge_id.map_or(NotSet, |v| Set(v)), + status: Set(0), + rank: Set(0), + ..Default::default() + } + } +} + +#[derive(Debug, Serialize, Deserialize, Validate)] +pub struct UpdateRequest { + pub id: Option, + pub flag: Option, + pub user_id: Option, + pub team_id: Option, + pub game_id: Option, + pub challenge_id: Option, + pub rank: Option, + pub status: Option, +} + +impl From for ActiveModel { + fn from(req: UpdateRequest) -> Self { + Self { + id: req.id.map_or(NotSet, |v| Set(v)), + flag: req.flag.map_or(NotSet, |v| Set(v)), + user_id: req.user_id.map_or(NotSet, |v| Set(v)), + team_id: req.team_id.map_or(NotSet, |v| Set(Some(v))), + game_id: req.game_id.map_or(NotSet, |v| Set(Some(v))), + challenge_id: req.challenge_id.map_or(NotSet, |v| Set(v)), + rank: req.rank.map_or(NotSet, |v| Set(v)), + status: req.status.map_or(NotSet, |v| Set(v)), + ..Default::default() + } + } +} diff --git a/src/model/team/mod.rs b/src/model/team/mod.rs new file mode 100644 index 00000000..5978003d --- /dev/null +++ b/src/model/team/mod.rs @@ -0,0 +1,92 @@ +pub mod request; + +use axum::async_trait; +use sea_orm::{entity::prelude::*, Set}; +use serde::{Deserialize, Serialize}; + +use super::{game, game_team, pod, submission, user, user_team}; + +#[derive(Debug, Clone, PartialEq, Eq, DeriveEntityModel, Serialize, Deserialize)] +#[sea_orm(table_name = "teams")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i64, + pub name: String, + pub email: Option, + pub captain_id: i64, + pub description: Option, + pub invite_token: Option, + pub created_at: i64, + pub updated_at: i64, + + #[sea_orm(ignore)] + pub users: Vec, + #[sea_orm(ignore)] + pub captain: Option, +} + +impl Model { + pub fn simplify(&mut self) { + self.invite_token = None; + if let Some(captain) = self.captain.as_mut() { + captain.simplify(); + } + for user in self.users.iter_mut() { + user.simplify(); + } + } +} + +#[derive(Copy, Clone, Debug, EnumIter)] +pub enum Relation { + Submission, + Pod, +} + +impl RelationTrait for Relation { + fn def(&self) -> RelationDef { + match self { + Self::Submission => Entity::has_many(submission::Entity).into(), + Self::Pod => Entity::has_many(pod::Entity).into(), + } + } +} + +impl Related for Entity { + fn to() -> RelationDef { + user_team::Relation::User.def() + } + + fn via() -> Option { + Some(user_team::Relation::Team.def().rev()) + } +} + +impl Related for Entity { + fn to() -> RelationDef { + game_team::Relation::Game.def() + } + + fn via() -> Option { + Some(game_team::Relation::Team.def().rev()) + } +} + +#[async_trait] +impl ActiveModelBehavior for ActiveModel { + fn new() -> Self { + Self { + created_at: Set(chrono::Utc::now().timestamp()), + updated_at: Set(chrono::Utc::now().timestamp()), + ..ActiveModelTrait::default() + } + } + + async fn before_save(mut self, _db: &C, _insert: bool) -> Result + where + C: ConnectionTrait, + { + self.updated_at = Set(chrono::Utc::now().timestamp()); + Ok(self) + } +} diff --git a/src/model/team/request.rs b/src/model/team/request.rs new file mode 100644 index 00000000..31877cfb --- /dev/null +++ b/src/model/team/request.rs @@ -0,0 +1,68 @@ +use sea_orm::{ActiveValue::NotSet, Set}; +use serde::{Deserialize, Serialize}; +use validator::Validate; + +use super::ActiveModel; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct FindRequest { + pub id: Option, + pub name: Option, + pub email: Option, + pub page: Option, + pub size: Option, +} + +impl Default for FindRequest { + fn default() -> Self { + FindRequest { + id: None, + name: None, + email: None, + page: None, + size: None, + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, Validate)] +pub struct CreateRequest { + pub name: String, + pub email: String, + pub captain_id: i64, + pub description: Option, +} + +impl From for ActiveModel { + fn from(req: CreateRequest) -> Self { + Self { + name: Set(req.name), + email: Set(Some(req.email)), + description: req.description.map_or(NotSet, |v| Set(Some(v))), + captain_id: Set(req.captain_id), + ..Default::default() + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, Validate)] +pub struct UpdateRequest { + pub id: Option, + pub name: Option, + pub email: Option, + pub captain_id: Option, + pub description: Option, +} + +impl From for ActiveModel { + fn from(req: UpdateRequest) -> Self { + Self { + id: req.id.map_or(NotSet, |v| Set(v)), + name: req.name.map_or(NotSet, |v| Set(v)), + email: req.email.map_or(NotSet, |v| Set(Some(v))), + captain_id: req.captain_id.map_or(NotSet, |v| Set(v)), + description: req.description.map_or(NotSet, |v| Set(Some(v))), + ..Default::default() + } + } +} diff --git a/src/model/user/mod.rs b/src/model/user/mod.rs new file mode 100644 index 00000000..25fa8323 --- /dev/null +++ b/src/model/user/mod.rs @@ -0,0 +1,79 @@ +pub mod request; + +use axum::async_trait; +use sea_orm::{entity::prelude::*, Set}; +use serde::{Deserialize, Serialize}; + +use super::{pod, submission, team, user_team}; + +#[derive(Debug, Clone, PartialEq, Eq, DeriveEntityModel, Serialize, Deserialize)] +#[sea_orm(table_name = "users")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i64, + #[sea_orm(unique)] + pub username: String, + pub nickname: String, + #[sea_orm(unique)] + pub email: Option, + pub group: String, + pub password: Option, + pub created_at: i64, + pub updated_at: i64, + + #[sea_orm(ignore)] + pub teams: Vec, +} + +impl Model { + pub fn simplify(&mut self) { + self.password = None; + for team in self.teams.iter_mut() { + team.simplify(); + } + } +} + +#[derive(Copy, Clone, Debug, EnumIter)] +pub enum Relation { + Submission, + Pod, +} + +impl RelationTrait for Relation { + fn def(&self) -> RelationDef { + match self { + Self::Submission => Entity::has_many(submission::Entity).into(), + Self::Pod => Entity::has_many(pod::Entity).into(), + } + } +} + +impl Related for Entity { + fn to() -> RelationDef { + user_team::Relation::Team.def() + } + + fn via() -> Option { + Some(user_team::Relation::User.def().rev()) + } +} + +#[async_trait] +impl ActiveModelBehavior for ActiveModel { + fn new() -> Self { + Self { + created_at: Set(chrono::Utc::now().timestamp()), + updated_at: Set(chrono::Utc::now().timestamp()), + ..ActiveModelTrait::default() + } + } + + async fn before_save(mut self, _db: &C, _insert: bool) -> Result + where + C: ConnectionTrait, + { + self.updated_at = Set(chrono::Utc::now().timestamp()); + Ok(self) + } +} diff --git a/src/model/user/request.rs b/src/model/user/request.rs new file mode 100644 index 00000000..af28e7ea --- /dev/null +++ b/src/model/user/request.rs @@ -0,0 +1,109 @@ +use sea_orm::{ActiveValue::NotSet, Set}; +use serde::{Deserialize, Serialize}; +use validator::Validate; + +use super::ActiveModel; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct FindRequest { + pub id: Option, + pub name: Option, + pub email: Option, + pub group: Option, + pub page: Option, + pub size: Option, +} + +impl Default for FindRequest { + fn default() -> Self { + FindRequest { + id: None, + name: None, + email: None, + group: None, + page: None, + size: None, + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, Validate)] +pub struct CreateRequest { + pub username: String, + pub nickname: String, + pub email: String, + pub password: String, + pub group: String, +} + +impl From for ActiveModel { + fn from(req: CreateRequest) -> Self { + Self { + username: Set(req.username), + nickname: Set(req.nickname), + email: Set(Some(req.email)), + password: Set(Some(req.password)), + group: Set(req.group), + ..Default::default() + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, Validate)] +pub struct UpdateRequest { + pub id: Option, + #[validate(length(min = 3, max = 20))] + pub username: Option, + pub nickname: Option, + #[validate(email)] + pub email: Option, + pub password: Option, + pub group: Option, +} + +impl From for ActiveModel { + fn from(req: UpdateRequest) -> Self { + Self { + id: req.id.map_or(NotSet, |v| Set(v)), + username: req.username.map_or(NotSet, |v| Set(v)), + nickname: req.nickname.map_or(NotSet, |v| Set(v)), + email: req.email.map_or(NotSet, |v| Set(Some(v))), + password: req.password.map_or(NotSet, |v| Set(Some(v))), + group: req.group.map_or(NotSet, |v| Set(v)), + ..Default::default() + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct DeleteRequest { + pub id: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct LoginRequest { + pub username: String, + pub password: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize, Validate)] +pub struct RegisterRequest { + #[validate(length(min = 3, max = 20))] + pub username: String, + pub nickname: String, + #[validate(email)] + pub email: String, + pub password: String, +} + +impl From for ActiveModel { + fn from(req: RegisterRequest) -> Self { + Self { + username: Set(req.username), + nickname: Set(req.nickname), + email: Set(Some(req.email)), + password: Set(Some(req.password)), + ..Default::default() + } + } +} diff --git a/src/model/user_team/mod.rs b/src/model/user_team/mod.rs new file mode 100644 index 00000000..02909cd4 --- /dev/null +++ b/src/model/user_team/mod.rs @@ -0,0 +1,42 @@ +pub mod request; + +use axum::async_trait; +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +use super::{team, user}; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize, Deserialize)] +#[sea_orm(table_name = "user_teams")] +pub struct Model { + #[sea_orm(primary_key)] + pub user_id: i64, + #[sea_orm(primary_key)] + pub team_id: i64, +} + +#[derive(Copy, Clone, Debug, EnumIter)] +pub enum Relation { + User, + Team, +} + +impl RelationTrait for Relation { + fn def(&self) -> RelationDef { + match self { + Self::User => Entity::belongs_to(user::Entity) + .from(Column::UserId) + .to(user::Column::Id) + .on_delete(ForeignKeyAction::Cascade) + .into(), + Self::Team => Entity::belongs_to(team::Entity) + .from(Column::TeamId) + .to(team::Column::Id) + .on_delete(ForeignKeyAction::Cascade) + .into(), + } + } +} + +#[async_trait] +impl ActiveModelBehavior for ActiveModel {} diff --git a/src/model/user_team/request.rs b/src/model/user_team/request.rs new file mode 100644 index 00000000..a4b96e42 --- /dev/null +++ b/src/model/user_team/request.rs @@ -0,0 +1,37 @@ +use sea_orm::Set; +use serde::{Deserialize, Serialize}; + +use super::ActiveModel; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct JoinRequest { + pub user_id: i64, + pub team_id: i64, + pub invite_token: String, +} + +impl From for ActiveModel { + fn from(req: JoinRequest) -> Self { + Self { + user_id: Set(req.user_id), + team_id: Set(req.team_id), + ..Default::default() + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct CreateRequest { + pub user_id: i64, + pub team_id: i64, +} + +impl From for ActiveModel { + fn from(req: CreateRequest) -> Self { + Self { + user_id: Set(req.user_id), + team_id: Set(req.team_id), + ..Default::default() + } + } +} diff --git a/src/proxy/mod.rs b/src/proxy/mod.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/src/proxy/mod.rs @@ -0,0 +1 @@ + diff --git a/src/repository/category.rs b/src/repository/category.rs new file mode 100644 index 00000000..884064c3 --- /dev/null +++ b/src/repository/category.rs @@ -0,0 +1,45 @@ +use sea_orm::{ + ActiveModelTrait, ColumnTrait, DbErr, EntityTrait, PaginatorTrait, QueryFilter, TryIntoModel, +}; + +use crate::database::get_db; + +pub async fn find( + id: Option, + name: Option, +) -> Result<(Vec, u64), DbErr> { + let mut query = crate::model::category::Entity::find(); + if let Some(id) = id { + query = query.filter(crate::model::category::Column::Id.eq(id)); + } + if let Some(name) = name { + query = query.filter(crate::model::category::Column::Name.eq(name)); + } + let total = query.clone().count(&get_db().await).await?; + let categories = query.all(&get_db().await).await?; + Ok((categories, total)) +} + +pub async fn create( + category: crate::model::category::ActiveModel, +) -> Result { + category.insert(&get_db().await).await?.try_into_model() +} + +pub async fn update( + category: crate::model::category::ActiveModel, +) -> Result { + category.update(&get_db().await).await?.try_into_model() +} + +pub async fn delete(id: i64) -> Result<(), DbErr> { + let result = crate::model::category::Entity::delete_by_id(id) + .exec(&get_db().await) + .await?; + Ok(if result.rows_affected == 0 { + return Err(DbErr::RecordNotFound(format!( + "Category with id {} not found", + id + ))); + }) +} diff --git a/src/repository/challenge.rs b/src/repository/challenge.rs new file mode 100644 index 00000000..1989756e --- /dev/null +++ b/src/repository/challenge.rs @@ -0,0 +1,84 @@ +use sea_orm::{ + ActiveModelTrait, ColumnTrait, DbErr, EntityTrait, PaginatorTrait, QueryFilter, QuerySelect, + TryIntoModel, +}; + +use crate::database::get_db; + +pub async fn find( + id: Option, + title: Option, + category_id: Option, + is_practicable: Option, + is_dynamic: Option, + page: Option, + size: Option, +) -> Result<(Vec, u64), DbErr> { + let mut query = crate::model::challenge::Entity::find(); + + if let Some(id) = id { + query = query.filter(crate::model::challenge::Column::Id.eq(id)); + } + + if let Some(title) = title { + query = query.filter(crate::model::challenge::Column::Title.contains(title)); + } + + if let Some(category_id) = category_id { + query = query.filter(crate::model::challenge::Column::CategoryId.eq(category_id)); + } + + if let Some(is_practicable) = is_practicable { + query = query.filter(crate::model::challenge::Column::IsPracticable.eq(is_practicable)); + } + + if let Some(is_dynamic) = is_dynamic { + query = query.filter(crate::model::challenge::Column::IsDynamic.eq(is_dynamic)); + } + + let total = query.clone().count(&get_db().await).await?; + + if let Some(page) = page { + if let Some(size) = size { + let offset = (page - 1) * size; + query = query.offset(offset).limit(size); + } + } + + let challenges = query.all(&get_db().await).await?; + + return Ok((challenges, total)); +} + +pub async fn find_by_ids(ids: Vec) -> Result, DbErr> { + let challenges = crate::model::challenge::Entity::find() + .filter(crate::model::challenge::Column::Id.is_in(ids)) + .all(&get_db().await) + .await?; + + return Ok(challenges); +} + +pub async fn create( + challenge: crate::model::challenge::ActiveModel, +) -> Result { + challenge.insert(&get_db().await).await?.try_into_model() +} + +pub async fn update( + challenge: crate::model::challenge::ActiveModel, +) -> Result { + challenge.update(&get_db().await).await?.try_into_model() +} + +pub async fn delete(id: i64) -> Result<(), DbErr> { + let result = crate::model::challenge::Entity::delete_by_id(id) + .exec(&get_db().await) + .await?; + Ok(if result.rows_affected == 0 { + return Err(DbErr::RecordNotFound(format!( + "Challenge with id {} not found", + id + ))); + }) +} diff --git a/src/repository/game.rs b/src/repository/game.rs new file mode 100644 index 00000000..6074914a --- /dev/null +++ b/src/repository/game.rs @@ -0,0 +1,65 @@ +use sea_orm::{ + ActiveModelTrait, ColumnTrait, DbErr, EntityTrait, PaginatorTrait, QueryFilter, QuerySelect, + TryIntoModel, +}; + +use crate::database::get_db; + +pub async fn find( + id: Option, + title: Option, + is_enabled: Option, + page: Option, + size: Option, +) -> Result<(Vec, u64), DbErr> { + let mut query = crate::model::game::Entity::find(); + + if let Some(id) = id { + query = query.filter(crate::model::game::Column::Id.eq(id)); + } + + if let Some(title) = title { + query = query.filter(crate::model::game::Column::Title.contains(title)); + } + + if let Some(is_enabled) = is_enabled { + query = query.filter(crate::model::game::Column::IsEnabled.eq(is_enabled)); + } + + let total = query.clone().count(&get_db().await).await?; + + if let Some(page) = page { + if let Some(size) = size { + let offset = (page - 1) * size; + query = query.offset(offset).limit(size); + } + } + + let games = query.all(&get_db().await).await?; + + return Ok((games, total)); +} + +pub async fn create( + game: crate::model::game::ActiveModel, +) -> Result { + game.insert(&get_db().await).await?.try_into_model() +} + +pub async fn update( + game: crate::model::game::ActiveModel, +) -> Result { + game.update(&get_db().await).await?.try_into_model() +} + +pub async fn delete(id: i64) -> Result<(), DbErr> { + let result = crate::model::game::Entity::delete_by_id(id) + .exec(&get_db().await) + .await?; + Ok(if result.rows_affected == 0 { + return Err(DbErr::RecordNotFound(format!( + "Game with id {} not found", + id + ))); + }) +} diff --git a/src/repository/game_challenge.rs b/src/repository/game_challenge.rs new file mode 100644 index 00000000..77b029e0 --- /dev/null +++ b/src/repository/game_challenge.rs @@ -0,0 +1,71 @@ +use sea_orm::{ + ActiveModelTrait, ColumnTrait, DbErr, EntityTrait, LoaderTrait, PaginatorTrait, QueryFilter, + TryIntoModel, +}; + +use crate::database::get_db; + +async fn preload( + mut game_challenges: Vec, +) -> Result, DbErr> { + let challenges = game_challenges + .load_one(crate::model::challenge::Entity, &get_db().await) + .await?; + + for i in 0..game_challenges.len() { + game_challenges[i].challenge = challenges[i].clone(); + } + + return Ok(game_challenges); +} + +pub async fn find( + game_id: Option, + challenge_id: Option, +) -> Result<(Vec, u64), DbErr> { + let mut query = crate::model::game_challenge::Entity::find(); + + if let Some(game_id) = game_id { + query = query.filter(crate::model::game_challenge::Column::GameId.eq(game_id)); + } + + if let Some(challenge_id) = challenge_id { + query = query.filter(crate::model::game_challenge::Column::ChallengeId.eq(challenge_id)); + } + + let total = query.clone().count(&get_db().await).await?; + + let mut game_challenges = query.all(&get_db().await).await?; + + game_challenges = preload(game_challenges).await?; + + Ok((game_challenges, total)) +} + +pub async fn create( + game_challenge: crate::model::game_challenge::ActiveModel, +) -> Result { + game_challenge + .insert(&get_db().await) + .await? + .try_into_model() +} + +pub async fn update( + game_challenge: crate::model::game_challenge::ActiveModel, +) -> Result { + game_challenge + .update(&get_db().await) + .await? + .try_into_model() +} + +pub async fn delete(game_id: i64, challenge_id: i64) -> Result<(), DbErr> { + let _result = crate::model::game_challenge::Entity::delete_many() + .filter(crate::model::game_challenge::Column::GameId.eq(game_id)) + .filter(crate::model::game_challenge::Column::ChallengeId.eq(challenge_id)) + .exec(&get_db().await) + .await?; + + return Ok(()); +} diff --git a/src/repository/game_team.rs b/src/repository/game_team.rs new file mode 100644 index 00000000..be29252c --- /dev/null +++ b/src/repository/game_team.rs @@ -0,0 +1,70 @@ +use sea_orm::{ + ActiveModelTrait, ColumnTrait, DbErr, EntityTrait, PaginatorTrait, QueryFilter, TryIntoModel, +}; + +use crate::database::get_db; + +async fn preload( + mut game_teams: Vec, +) -> Result, DbErr> { + let team_ids: Vec = game_teams + .iter() + .map(|game_team| game_team.team_id) + .collect(); + + let teams = super::team::find_by_ids(team_ids).await?; + + for game_team in game_teams.iter_mut() { + game_team.team = teams + .iter() + .find(|team| team.id == game_team.team_id) + .cloned(); + } + + return Ok(game_teams); +} + +pub async fn find( + game_id: Option, + team_id: Option, +) -> Result<(Vec, u64), DbErr> { + let mut query = crate::model::game_team::Entity::find(); + + if let Some(game_id) = game_id { + query = query.filter(crate::model::game_team::Column::GameId.eq(game_id)); + } + + if let Some(team_id) = team_id { + query = query.filter(crate::model::game_team::Column::TeamId.eq(team_id)); + } + + let total = query.clone().count(&get_db().await).await?; + + let mut game_teams = query.all(&get_db().await).await?; + + game_teams = preload(game_teams).await?; + + Ok((game_teams, total)) +} + +pub async fn create( + user: crate::model::game_team::ActiveModel, +) -> Result { + user.insert(&get_db().await).await?.try_into_model() +} + +pub async fn update( + user: crate::model::game_team::ActiveModel, +) -> Result { + user.update(&get_db().await).await?.try_into_model() +} + +pub async fn delete(game_id: i64, team_id: i64) -> Result<(), DbErr> { + let _result = crate::model::game_team::Entity::delete_many() + .filter(crate::model::game_team::Column::GameId.eq(game_id)) + .filter(crate::model::game_team::Column::TeamId.eq(team_id)) + .exec(&get_db().await) + .await?; + + return Ok(()); +} diff --git a/src/repository/mod.rs b/src/repository/mod.rs new file mode 100644 index 00000000..18865f3f --- /dev/null +++ b/src/repository/mod.rs @@ -0,0 +1,10 @@ +pub mod category; +pub mod challenge; +pub mod game; +pub mod game_challenge; +pub mod game_team; +pub mod pod; +pub mod submission; +pub mod team; +pub mod user; +pub mod user_team; diff --git a/src/repository/pod.rs b/src/repository/pod.rs new file mode 100644 index 00000000..4b04d5c3 --- /dev/null +++ b/src/repository/pod.rs @@ -0,0 +1,112 @@ +use sea_orm::{ + ActiveModelTrait, ColumnTrait, DbErr, EntityTrait, LoaderTrait, PaginatorTrait, QueryFilter, + TryIntoModel, +}; + +use crate::database::get_db; + +async fn preload( + mut pods: Vec, +) -> Result, DbErr> { + let users = pods + .load_one(crate::model::user::Entity, &get_db().await) + .await?; + let teams = pods + .load_one(crate::model::team::Entity, &get_db().await) + .await?; + let challenges = pods + .load_one(crate::model::challenge::Entity, &get_db().await) + .await?; + + for i in 0..pods.len() { + let mut pod = pods[i].clone(); + pod.user = users[i].clone(); + pod.team = teams[i].clone(); + pod.challenge = challenges[i].clone(); + pods[i] = pod; + } + + return Ok(pods); +} + +pub async fn find( + id: Option, + name: Option, + user_id: Option, + team_id: Option, + game_id: Option, + challenge_id: Option, + is_available: Option, +) -> Result<(Vec, u64), DbErr> { + let mut query = crate::model::pod::Entity::find(); + if let Some(id) = id { + query = query.filter(crate::model::pod::Column::Id.eq(id)); + } + + if let Some(name) = name { + query = query.filter(crate::model::pod::Column::Name.eq(name)); + } + + if let Some(user_id) = user_id { + query = query.filter(crate::model::pod::Column::UserId.eq(user_id)); + } + + if let Some(team_id) = team_id { + query = query.filter(crate::model::pod::Column::TeamId.eq(team_id)); + } + + if let Some(game_id) = game_id { + query = query.filter(crate::model::pod::Column::GameId.eq(game_id)); + } + + if let Some(challenge_id) = challenge_id { + query = query.filter(crate::model::pod::Column::ChallengeId.eq(challenge_id)); + } + + if let Some(is_available) = is_available { + match is_available { + true => { + query = query.filter( + crate::model::pod::Column::RemovedAt.gte(chrono::Utc::now().timestamp()), + ) + } + false => { + query = query.filter( + crate::model::pod::Column::RemovedAt.lte(chrono::Utc::now().timestamp()), + ) + } + } + } + + let total = query.clone().count(&get_db().await).await?; + + let mut pods = query.all(&get_db().await).await?; + + pods = preload(pods).await?; + + return Ok((pods, total)); +} + +pub async fn create( + pod: crate::model::pod::ActiveModel, +) -> Result { + return pod.insert(&get_db().await).await?.try_into_model(); +} + +pub async fn update( + pod: crate::model::pod::ActiveModel, +) -> Result { + return pod.update(&get_db().await).await?.try_into_model(); +} + +pub async fn delete(id: i64) -> Result<(), DbErr> { + let result = crate::model::pod::Entity::delete_by_id(id) + .exec(&get_db().await) + .await?; + return Ok(if result.rows_affected == 0 { + return Err(DbErr::RecordNotFound(format!( + "Pod with id {} not found", + id + ))); + }); +} diff --git a/src/repository/submission.rs b/src/repository/submission.rs new file mode 100644 index 00000000..b7502f57 --- /dev/null +++ b/src/repository/submission.rs @@ -0,0 +1,120 @@ +use sea_orm::{ + ActiveModelTrait, ColumnTrait, DbErr, EntityTrait, LoaderTrait, PaginatorTrait, QueryFilter, + QuerySelect, TryIntoModel, +}; + +use crate::database::get_db; + +pub async fn preload( + mut submissions: Vec, +) -> Result, DbErr> { + let users = submissions + .load_one(crate::model::user::Entity, &get_db().await) + .await?; + let challenges = submissions + .load_one(crate::model::challenge::Entity, &get_db().await) + .await?; + let teams = submissions + .load_one(crate::model::team::Entity, &get_db().await) + .await?; + let games = submissions + .load_one(crate::model::game::Entity, &get_db().await) + .await?; + + for i in 0..submissions.len() { + let mut submission = submissions[i].clone(); + submission.user = users[i].clone(); + submission.challenge = challenges[i].clone(); + submission.team = teams[i].clone(); + submission.game = games[i].clone(); + submissions[i] = submission; + } + return Ok(submissions); +} + +pub async fn find( + id: Option, + user_id: Option, + team_id: Option, + game_id: Option, + challenge_id: Option, + status: Option, + page: Option, + size: Option, +) -> Result<(Vec, u64), DbErr> { + let mut query = crate::model::submission::Entity::find(); + + if let Some(id) = id { + query = query.filter(crate::model::submission::Column::Id.eq(id)); + } + + if let Some(user_id) = user_id { + query = query.filter(crate::model::submission::Column::UserId.eq(user_id)); + } + + if let Some(team_id) = team_id { + query = query.filter(crate::model::submission::Column::TeamId.eq(team_id)); + } + + if let Some(game_id) = game_id { + query = query.filter(crate::model::submission::Column::GameId.eq(game_id)); + } + + if let Some(challenge_id) = challenge_id { + query = query.filter(crate::model::submission::Column::ChallengeId.eq(challenge_id)); + } + + if let Some(status) = status { + query = query.filter(crate::model::submission::Column::Status.eq(status)); + } + + let total = query.clone().count(&get_db().await).await?; + + if let Some(page) = page { + if let Some(size) = size { + let offset = (page - 1) * size; + query = query.offset(offset).limit(size); + } + } + + let mut submissions = query.all(&get_db().await).await?; + + submissions = preload(submissions).await?; + + return Ok((submissions, total)); +} + +pub async fn find_by_challenge_ids( + challenge_ids: Vec, +) -> Result, DbErr> { + let mut submissions = crate::model::submission::Entity::find() + .filter(crate::model::submission::Column::ChallengeId.is_in(challenge_ids)) + .all(&get_db().await) + .await?; + submissions = preload(submissions).await?; + return Ok(submissions); +} + +pub async fn create( + submission: crate::model::submission::ActiveModel, +) -> Result { + return submission.insert(&get_db().await).await?.try_into_model(); +} + +pub async fn update( + submission: crate::model::submission::ActiveModel, +) -> Result { + return submission.update(&get_db().await).await?.try_into_model(); +} + +pub async fn delete(id: i64) -> Result<(), DbErr> { + let result = crate::model::submission::Entity::delete_by_id(id) + .exec(&get_db().await) + .await?; + return Ok(if result.rows_affected == 0 { + return Err(DbErr::RecordNotFound(format!( + "Submission with id {} not found", + id + ))); + }); +} diff --git a/src/repository/team.rs b/src/repository/team.rs new file mode 100644 index 00000000..44e643d3 --- /dev/null +++ b/src/repository/team.rs @@ -0,0 +1,106 @@ +use sea_orm::{ + ActiveModelTrait, ColumnTrait, DbErr, EntityTrait, LoaderTrait, PaginatorTrait, QueryFilter, + QuerySelect, TryIntoModel, +}; + +use crate::database::get_db; + +async fn preload( + mut teams: Vec, +) -> Result, DbErr> { + let users = teams + .load_many_to_many( + crate::model::user::Entity, + crate::model::user_team::Entity, + &get_db().await, + ) + .await?; + + for i in 0..teams.len() { + let mut team = teams[i].clone(); + team.users = users[i].clone(); + for j in 0..team.users.len() { + let mut user = team.users[j].clone(); + user.simplify(); + if user.id == team.captain_id { + team.captain = Some(team.users[j].clone()); + } + team.users[j] = user; + } + teams[i] = team; + } + + return Ok(teams); +} + +pub async fn find( + id: Option, + name: Option, + email: Option, + page: Option, + size: Option, +) -> Result<(Vec, u64), DbErr> { + let mut query = crate::model::team::Entity::find(); + + if let Some(id) = id { + query = query.filter(crate::model::team::Column::Id.eq(id)); + } + + if let Some(name) = name { + query = query.filter(crate::model::team::Column::Name.contains(name)); + } + + if let Some(email) = email { + query = query.filter(crate::model::team::Column::Email.eq(email)); + } + + let total = query.clone().count(&get_db().await).await?; + + if let Some(page) = page { + if let Some(size) = size { + let offset = (page - 1) * size; + query = query.offset(offset).limit(size); + } + } + + let mut teams = query.all(&get_db().await).await?; + + teams = preload(teams).await?; + + return Ok((teams, total)); +} + +pub async fn find_by_ids(ids: Vec) -> Result, DbErr> { + let mut teams = crate::model::team::Entity::find() + .filter(crate::model::team::Column::Id.is_in(ids)) + .all(&get_db().await) + .await?; + + teams = preload(teams).await?; + + return Ok(teams); +} + +pub async fn create( + team: crate::model::team::ActiveModel, +) -> Result { + return team.insert(&get_db().await).await?.try_into_model(); +} + +pub async fn update( + team: crate::model::team::ActiveModel, +) -> Result { + return team.update(&get_db().await).await?.try_into_model(); +} + +pub async fn delete(id: i64) -> Result<(), DbErr> { + let result = crate::model::team::Entity::delete_by_id(id) + .exec(&get_db().await) + .await?; + return Ok(if result.rows_affected == 0 { + return Err(DbErr::RecordNotFound(format!( + "Team with id {} not found", + id + ))); + }); +} diff --git a/src/repository/user.rs b/src/repository/user.rs new file mode 100644 index 00000000..b55b5781 --- /dev/null +++ b/src/repository/user.rs @@ -0,0 +1,101 @@ +use sea_orm::{ + ActiveModelTrait, ColumnTrait, Condition, DbErr, EntityTrait, LoaderTrait, PaginatorTrait, + QueryFilter, QuerySelect, TryIntoModel, +}; + +use crate::database::get_db; + +async fn preload( + mut users: Vec, +) -> Result, DbErr> { + let teams = users + .load_many_to_many( + crate::model::team::Entity, + crate::model::user_team::Entity, + &get_db().await, + ) + .await?; + + for i in 0..users.len() { + let mut user = users[i].clone(); + user.teams = teams[i].clone(); + users[i] = user; + } + + return Ok(users); +} + +pub async fn find( + id: Option, + name: Option, + username: Option, + group: Option, + email: Option, + page: Option, + size: Option, +) -> Result<(Vec, u64), DbErr> { + let mut query = crate::model::user::Entity::find(); + + if let Some(id) = id { + query = query.filter(crate::model::user::Column::Id.eq(id)); + } + + if let Some(name) = name { + let pattern = format!("%{}%", name); + let condition = Condition::any() + .add(crate::model::user::Column::Username.like(&pattern)) + .add(crate::model::user::Column::Nickname.like(&pattern)); + query = query.filter(condition); + } + + if let Some(username) = username { + query = query.filter(crate::model::user::Column::Username.eq(username)); + } + + if let Some(group) = group { + query = query.filter(crate::model::user::Column::Group.eq(group)); + } + + if let Some(email) = email { + query = query.filter(crate::model::user::Column::Email.eq(email)); + } + + let total = query.clone().count(&get_db().await).await?; + + if let Some(page) = page { + if let Some(size) = size { + let offset = (page - 1) * size; + query = query.offset(offset).limit(size); + } + } + + let mut users = query.all(&get_db().await).await?; + + users = preload(users).await?; + + Ok((users, total)) +} + +pub async fn create( + user: crate::model::user::ActiveModel, +) -> Result { + user.insert(&get_db().await).await?.try_into_model() +} + +pub async fn update( + user: crate::model::user::ActiveModel, +) -> Result { + user.update(&get_db().await).await?.try_into_model() +} + +pub async fn delete(id: i64) -> Result<(), DbErr> { + let result = crate::model::user::Entity::delete_by_id(id) + .exec(&get_db().await) + .await?; + Ok(if result.rows_affected == 0 { + return Err(DbErr::RecordNotFound(format!( + "User with id {} not found", + id + ))); + }) +} diff --git a/src/repository/user_team.rs b/src/repository/user_team.rs new file mode 100644 index 00000000..be1a96f5 --- /dev/null +++ b/src/repository/user_team.rs @@ -0,0 +1,48 @@ +use sea_orm::{ + ActiveModelTrait, ColumnTrait, DbErr, EntityTrait, PaginatorTrait, QueryFilter, TryIntoModel, +}; + +use crate::database::get_db; + +pub async fn find( + user_id: Option, + team_id: Option, +) -> Result<(Vec, u64), DbErr> { + let mut query = crate::model::user_team::Entity::find(); + + if let Some(user_id) = user_id { + query = query.filter(crate::model::user_team::Column::UserId.eq(user_id)); + } + + if let Some(team_id) = team_id { + query = query.filter(crate::model::user_team::Column::TeamId.eq(team_id)); + } + + let total = query.clone().count(&get_db().await).await?; + + let user_teams = query.all(&get_db().await).await?; + + Ok((user_teams, total)) +} + +pub async fn create( + user_team: crate::model::user_team::ActiveModel, +) -> Result { + user_team.insert(&get_db().await).await?.try_into_model() +} + +pub async fn update( + user_team: crate::model::user_team::ActiveModel, +) -> Result { + user_team.update(&get_db().await).await?.try_into_model() +} + +pub async fn delete(user_id: i64, team_id: i64) -> Result<(), DbErr> { + let _result: sea_orm::DeleteResult = crate::model::user_team::Entity::delete_many() + .filter(crate::model::user_team::Column::UserId.eq(user_id)) + .filter(crate::model::user_team::Column::TeamId.eq(team_id)) + .exec(&get_db().await) + .await?; + + return Ok(()); +} diff --git a/src/server/controller/category.rs b/src/server/controller/category.rs new file mode 100644 index 00000000..bf6081ab --- /dev/null +++ b/src/server/controller/category.rs @@ -0,0 +1,119 @@ +use axum::{ + extract::{Path, Query}, + http::StatusCode, + response::IntoResponse, + Json, +}; +use serde_json::json; + +use crate::model::category::request::{CreateRequest, FindRequest, UpdateRequest}; +use crate::server::service; +use crate::util::validate; + +/// **Find** can find categories. +/// +/// ## Arguments +/// - `params`: category's information. +/// +/// ## Returns +/// - `200`: find successfully. +/// - `400`: find failed. +pub async fn find(Query(params): Query) -> impl IntoResponse { + match service::category::find(params).await { + Ok((categories, total)) => ( + StatusCode::OK, + Json(json!({ + "code": StatusCode::OK.as_u16(), + "data": json!(categories), + "total": total, + })), + ), + Err(_err) => ( + StatusCode::BAD_REQUEST, + Json(json!({ + "code": StatusCode::BAD_REQUEST.as_u16(), + })), + ), + } +} + +/// **Create** can create a new category. +/// +/// ## Arguments +/// - `body`: category's information(validated). +/// +/// ## Returns +/// - `200`: create successfully. +/// - `400`: create failed. +pub async fn create(validate::Json(body): validate::Json) -> impl IntoResponse { + match service::category::create(body).await { + Ok(category) => ( + StatusCode::OK, + Json(json!({ + "code": StatusCode::OK.as_u16(), + "data": json!(category), + })), + ), + Err(_err) => ( + StatusCode::BAD_REQUEST, + Json(json!({ + "code": StatusCode::BAD_REQUEST.as_u16(), + })), + ), + } +} + +/// **Update** can update category's information. +/// +/// ## Arguments +/// - `id`: category's id. +/// - `body`: category's information(validated). +/// +/// ## Returns +/// - `200`: update successfully. +/// - `400`: update failed. +pub async fn update( + Path(id): Path, + validate::Json(mut body): validate::Json, +) -> impl IntoResponse { + body.id = Some(id); + match service::category::update(body).await { + Ok(()) => ( + StatusCode::OK, + Json(json!({ + "code": StatusCode::OK.as_u16() + })), + ), + Err(_err) => ( + StatusCode::BAD_REQUEST, + Json(json!({ + "code": StatusCode::BAD_REQUEST.as_u16(), + })), + ), + } +} + +/// **Delete** can be used to delete category. +/// +/// ## Arguments +/// - `id`: category's id. +/// +/// ## Returns +/// - `200`: delete successfully. +/// - `400`: delete failed. +pub async fn delete(Path(id): Path) -> impl IntoResponse { + match service::category::delete(id).await { + Ok(()) => ( + StatusCode::OK, + Json(json!({ + "code": StatusCode::OK.as_u16() + })), + ), + Err(_err) => ( + StatusCode::BAD_REQUEST, + Json(json!({ + "code": StatusCode::BAD_REQUEST.as_u16(), + })), + ), + } +} diff --git a/src/server/controller/challenge.rs b/src/server/controller/challenge.rs new file mode 100644 index 00000000..9b9dd8d9 --- /dev/null +++ b/src/server/controller/challenge.rs @@ -0,0 +1,243 @@ +use axum::{ + extract::{Multipart, Path, Query}, + http::{header, Response, StatusCode}, + response::IntoResponse, + Extension, Json, +}; +use serde_json::json; + +use crate::{server::service, traits::Ext}; + +use crate::util::validate; + +/// **Find** can be used to find challenges. +/// +/// ## Arguments +/// - `params`: A `FindRequest` struct containing the parameters for the find operation. +/// - `ext`: An `Ext` struct containing the current operator. +/// +/// ## Returns +/// - `200`: find successfully. +/// - `403`: operator does not have permission to find challenges. +/// - `400`: find failed. +pub async fn find( + Extension(ext): Extension, + Query(params): Query, +) -> impl IntoResponse { + let operator = ext.operator.unwrap(); + if operator.group != "admin" && params.is_detailed.unwrap_or(false) { + return ( + StatusCode::FORBIDDEN, + Json(json!({ + "code": StatusCode::FORBIDDEN.as_u16(), + })), + ); + } + + match service::challenge::find(params).await { + Ok((challenges, total)) => ( + StatusCode::OK, + Json(json!({ + "code": StatusCode::OK.as_u16(), + "data": json!(challenges), + "total": total, + })), + ), + Err(e) => ( + StatusCode::BAD_REQUEST, + Json(json!({ + "code": StatusCode::BAD_REQUEST.as_u16(), + })), + ), + } +} + +pub async fn status( + Json(body): Json, +) -> impl IntoResponse { + match service::challenge::status(body).await { + Ok(status) => ( + StatusCode::OK, + Json(json!({ + "code": StatusCode::OK.as_u16(), + "data": json!(status), + })), + ), + Err(e) => ( + StatusCode::BAD_REQUEST, + Json(json!({ + "code": StatusCode::BAD_REQUEST.as_u16(), + })), + ), + } +} + +pub async fn create( + Json(body): Json, +) -> impl IntoResponse { + match service::challenge::create(body).await { + Ok(challenge) => ( + StatusCode::OK, + Json(json!({ + "code": StatusCode::OK.as_u16(), + "data": json!(challenge), + })), + ), + Err(e) => ( + StatusCode::BAD_REQUEST, + Json(json!({ + "code": StatusCode::BAD_REQUEST.as_u16(), + "msg": format!("{:?}", e), + })), + ), + } +} + +pub async fn update( + Path(id): Path, + validate::Json(mut body): validate::Json, +) -> impl IntoResponse { + body.id = Some(id); + match service::challenge::update(body).await { + Ok(()) => ( + StatusCode::OK, + Json(json!({ + "code": StatusCode::OK.as_u16(), + })), + ), + Err(_err) => ( + StatusCode::BAD_REQUEST, + Json(json!({ + "code": StatusCode::BAD_REQUEST.as_u16(), + })), + ), + } +} + +pub async fn delete(Path(id): Path) -> impl IntoResponse { + match service::challenge::delete(id).await { + Ok(()) => ( + StatusCode::OK, + Json(json!({ + "code": StatusCode::OK.as_u16() + })), + ), + Err(e) => ( + StatusCode::BAD_REQUEST, + Json(json!({ + "code": StatusCode::BAD_REQUEST.as_u16(), + })), + ), + } +} + +pub async fn find_attachment(Path(id): Path) -> impl IntoResponse { + let path = format!("challenges/{}/attachment", id); + match crate::media::scan_dir(path.clone()).await.unwrap().first() { + Some((filename, _size)) => { + let buffer = crate::media::get(path, filename.to_string()).await.unwrap(); + return Response::builder() + .header(header::CONTENT_TYPE, "application/octet-stream") + .header( + header::CONTENT_DISPOSITION, + format!("attachment; filename=\"{}\"", filename), + ) + .body(buffer.into()) + .unwrap(); + } + None => return (StatusCode::NOT_FOUND).into_response(), + } +} + +pub async fn find_attachment_metadata(Path(id): Path) -> impl IntoResponse { + let path = format!("challenges/{}/attachment", id); + match crate::media::scan_dir(path.clone()).await.unwrap().first() { + Some((filename, size)) => { + return ( + StatusCode::OK, + Json(json!({ + "code": StatusCode::OK.as_u16(), + "data": { + "filename": filename, + "size": size, + }, + })), + ) + } + None => { + return ( + StatusCode::NOT_FOUND, + Json(json!({ + "code": StatusCode::NOT_FOUND.as_u16(), + })), + ) + } + } +} + +pub async fn save_attachment(Path(id): Path, mut multipart: Multipart) -> impl IntoResponse { + let path = format!("challenges/{}/attachment", id); + let mut filename = String::new(); + let mut data = Vec::::new(); + while let Some(field) = multipart.next_field().await.unwrap() { + if field.name() == Some("file") { + filename = field.file_name().unwrap().to_string(); + data = match field.bytes().await { + Ok(bytes) => bytes.to_vec(), + Err(_err) => { + return ( + StatusCode::BAD_REQUEST, + Json(json!({ + "code": StatusCode::BAD_REQUEST.as_u16(), + "msg": "size_too_large", + })), + ); + } + }; + } + } + + crate::media::delete(path.clone()).await.unwrap(); + + match crate::media::save(path, filename, data).await { + Ok(_) => { + return ( + StatusCode::OK, + Json(json!({ + "code": StatusCode::OK.as_u16(), + })), + ); + } + Err(_err) => { + return ( + StatusCode::BAD_REQUEST, + Json(json!({ + "code": StatusCode::BAD_REQUEST.as_u16(), + })), + ) + } + } +} + +pub async fn delete_attachment(Path(id): Path) -> impl IntoResponse { + let path = format!("challenges/{}/attachment", id); + + match crate::media::delete(path).await { + Ok(_) => { + return ( + StatusCode::OK, + Json(json!({ + "code": StatusCode::OK.as_u16(), + })), + ) + } + Err(_err) => { + return ( + StatusCode::NOT_FOUND, + Json(json!({ + "code": StatusCode::NOT_FOUND.as_u16(), + })), + ) + } + } +} diff --git a/src/server/controller/config.rs b/src/server/controller/config.rs new file mode 100644 index 00000000..9088fbed --- /dev/null +++ b/src/server/controller/config.rs @@ -0,0 +1,54 @@ +use std::path::PathBuf; + +use axum::{ + http::{Response, StatusCode}, + response::IntoResponse, + Json, +}; +use serde_json::json; +use tokio::{fs::File, io::AsyncReadExt}; + +use crate::config::get_app_config; + +pub async fn find() -> impl IntoResponse { + return ( + StatusCode::OK, + Json(json!({ + "code": StatusCode::OK.as_u16(), + "data": { + "site": get_app_config().site, + "auth": { + "registration": get_app_config().auth.registration, + }, + "container": { + "parallel_limit": get_app_config().container.strategy.parallel_limit, + "request_limit": get_app_config().container.strategy.request_limit, + }, + "captcha": { + "provider": get_app_config().captcha.provider, + "turnstile": { + "site_key": get_app_config().captcha.turnstile.site_key + }, + "recaptcha": { + "site_key": get_app_config().captcha.recaptcha.site_key + } + } + } + })), + ); +} + +pub async fn get_favicon() -> impl IntoResponse { + let path = PathBuf::from(get_app_config().site.favicon.clone()); + + match File::open(&path).await { + Ok(mut file) => { + let mut buffer = Vec::new(); + if let Err(_) = file.read_to_end(&mut buffer).await { + return (StatusCode::INTERNAL_SERVER_ERROR).into_response(); + } + return Response::builder().body(buffer.into()).unwrap(); + } + Err(_) => return (StatusCode::NOT_FOUND).into_response(), + } +} diff --git a/src/server/controller/game.rs b/src/server/controller/game.rs new file mode 100644 index 00000000..ab7f57f4 --- /dev/null +++ b/src/server/controller/game.rs @@ -0,0 +1,411 @@ +use crate::{server::service, traits::Ext}; +use axum::{ + extract::{Multipart, Path, Query}, + http::{Response, StatusCode}, + response::IntoResponse, + Extension, Json, +}; +use mime::Mime; +use serde_json::json; + +pub async fn find( + Extension(ext): Extension, + Query(params): Query, +) -> impl IntoResponse { + let operator = ext.operator.unwrap(); + if operator.group != "admin" && !params.is_enabled.unwrap_or(true) { + return ( + StatusCode::FORBIDDEN, + Json(json!({ + "code": StatusCode::FORBIDDEN.as_u16(), + })), + ); + } + + match service::game::find(params).await { + Ok((challenges, total)) => { + return ( + StatusCode::OK, + Json(json!({ + "code": StatusCode::OK.as_u16(), + "data": json!(challenges), + "total": total, + })), + ) + } + Err(_err) => { + return ( + StatusCode::BAD_REQUEST, + Json(json!({ + "code": StatusCode::BAD_REQUEST.as_u16(), + })), + ) + } + } +} + +pub async fn create( + Json(body): Json, +) -> impl IntoResponse { + match service::game::create(body).await { + Ok(challenge) => { + return ( + StatusCode::OK, + Json(json!({ + "code": StatusCode::OK.as_u16(), + "data": json!(challenge), + })), + ) + } + Err(e) => { + return ( + StatusCode::BAD_REQUEST, + Json(json!({ + "code": StatusCode::BAD_REQUEST.as_u16(), + "msg": format!("{:?}", e), + })), + ) + } + } +} + +pub async fn update( + Path(id): Path, + Json(mut body): Json, +) -> impl IntoResponse { + body.id = Some(id); + match service::game::update(body).await { + Ok(()) => { + return ( + StatusCode::OK, + Json(json!({ + "code": StatusCode::OK.as_u16(), + })), + ) + } + Err(_err) => { + return ( + StatusCode::BAD_REQUEST, + Json(json!({ + "code": StatusCode::BAD_REQUEST.as_u16(), + })), + ) + } + } +} + +pub async fn delete(Path(id): Path) -> impl IntoResponse { + match service::game::delete(id).await { + Ok(()) => { + return ( + StatusCode::OK, + Json(json!({ + "code": StatusCode::OK.as_u16() + })), + ) + } + Err(_err) => { + return ( + StatusCode::BAD_REQUEST, + Json(json!({ + "code": StatusCode::BAD_REQUEST.as_u16(), + })), + ) + } + } +} + +pub async fn find_challenge( + Query(params): Query, +) -> impl IntoResponse { + match service::game_challenge::find(params).await { + Ok((game_challenges, total)) => { + return ( + StatusCode::OK, + Json(json!({ + "code": StatusCode::OK.as_u16(), + "data": json!(game_challenges), + "total": total, + })), + ) + } + Err(_err) => { + return ( + StatusCode::BAD_REQUEST, + Json(json!({ + "code": StatusCode::BAD_REQUEST.as_u16(), + })), + ) + } + } +} + +pub async fn create_challenge( + Json(body): Json, +) -> impl IntoResponse { + match service::game_challenge::create(body).await { + Ok(()) => { + return ( + StatusCode::OK, + Json(json!({ + "code": StatusCode::OK.as_u16(), + })), + ) + } + Err(_err) => { + return ( + StatusCode::BAD_REQUEST, + Json(json!({ + "code": StatusCode::BAD_REQUEST.as_u16(), + })), + ) + } + } +} + +pub async fn update_challenge( + Path((id, challenge_id)): Path<(i64, i64)>, + Json(mut body): Json, +) -> impl IntoResponse { + body.game_id = Some(id); + body.challenge_id = Some(challenge_id); + match service::game_challenge::update(body).await { + Ok(()) => { + return ( + StatusCode::OK, + Json(json!({ + "code": StatusCode::OK.as_u16(), + })), + ) + } + Err(_err) => { + return ( + StatusCode::BAD_REQUEST, + Json(json!({ + "code": StatusCode::BAD_REQUEST.as_u16(), + })), + ) + } + } +} + +pub async fn delete_challenge(Path((id, challenge_id)): Path<(i64, i64)>) -> impl IntoResponse { + match service::game_challenge::delete(id, challenge_id).await { + Ok(()) => { + return ( + StatusCode::OK, + Json(json!({ + "code": StatusCode::OK.as_u16(), + })), + ) + } + Err(_err) => { + return ( + StatusCode::BAD_REQUEST, + Json(json!({ + "code": StatusCode::BAD_REQUEST.as_u16(), + })), + ) + } + } +} + +pub async fn find_team( + Query(params): Query, +) -> impl IntoResponse { + match service::game_team::find(params).await { + Ok((game_teams, total)) => { + return ( + StatusCode::OK, + Json(json!({ + "code": StatusCode::OK.as_u16(), + "data": json!(game_teams), + "total": total, + })), + ) + } + Err(_err) => { + return ( + StatusCode::BAD_REQUEST, + Json(json!({ + "code": StatusCode::BAD_REQUEST.as_u16(), + })), + ) + } + } +} + +pub async fn create_team( + Json(body): Json, +) -> impl IntoResponse { + match service::game_team::create(body).await { + Ok(()) => { + return ( + StatusCode::OK, + Json(json!({ + "code": StatusCode::OK.as_u16(), + })), + ) + } + Err(_err) => { + return ( + StatusCode::BAD_REQUEST, + Json(json!({ + "code": StatusCode::BAD_REQUEST.as_u16(), + })), + ) + } + } +} + +pub async fn update_team( + Path((id, team_id)): Path<(i64, i64)>, + Json(mut body): Json, +) -> impl IntoResponse { + body.game_id = Some(id); + body.team_id = Some(team_id); + match service::game_team::update(body).await { + Ok(()) => { + return ( + StatusCode::OK, + Json(json!({ + "code": StatusCode::OK.as_u16(), + })), + ) + } + Err(_err) => { + return ( + StatusCode::BAD_REQUEST, + Json(json!({ + "code": StatusCode::BAD_REQUEST.as_u16(), + })), + ) + } + } +} + +pub async fn delete_team(Path((id, team_id)): Path<(i64, i64)>) -> impl IntoResponse { + match service::game_team::delete(id, team_id).await { + Ok(()) => { + return ( + StatusCode::OK, + Json(json!({ + "code": StatusCode::OK.as_u16(), + })), + ) + } + Err(_err) => { + return ( + StatusCode::BAD_REQUEST, + Json(json!({ + "code": StatusCode::BAD_REQUEST.as_u16(), + })), + ) + } + } +} + +pub async fn find_notice() -> impl IntoResponse { + todo!() +} + +pub async fn create_notice() -> impl IntoResponse { + todo!() +} + +pub async fn update_notice() -> impl IntoResponse { + todo!() +} + +pub async fn delete_notice() -> impl IntoResponse { + todo!() +} + +pub async fn find_poster(Path(id): Path) -> impl IntoResponse { + let path = format!("games/{}/poster", id); + match crate::media::scan_dir(path.clone()).await.unwrap().first() { + Some((filename, _size)) => { + let buffer = crate::media::get(path, filename.to_string()).await.unwrap(); + return Response::builder().body(buffer.into()).unwrap(); + } + None => return (StatusCode::NOT_FOUND).into_response(), + } +} + +pub async fn save_poster(Path(id): Path, mut multipart: Multipart) -> impl IntoResponse { + let path = format!("games/{}/poster", id); + let mut filename = String::new(); + let mut data = Vec::::new(); + while let Some(field) = multipart.next_field().await.unwrap() { + if field.name() == Some("file") { + filename = field.file_name().unwrap().to_string(); + let content_type = field.content_type().unwrap().to_string(); + let mime: Mime = content_type.parse().unwrap(); + if mime.type_() != mime::IMAGE { + return ( + StatusCode::BAD_REQUEST, + Json(json!({ + "code": StatusCode::BAD_REQUEST.as_u16(), + "msg": "forbidden_file_type", + })), + ); + } + data = match field.bytes().await { + Ok(bytes) => bytes.to_vec(), + Err(_err) => { + return ( + StatusCode::BAD_REQUEST, + Json(json!({ + "code": StatusCode::BAD_REQUEST.as_u16(), + "msg": "size_too_large", + })), + ); + } + }; + } + } + + crate::media::delete(path.clone()).await.unwrap(); + + match crate::media::save(path, filename, data).await { + Ok(_) => { + return ( + StatusCode::OK, + Json(json!({ + "code": StatusCode::OK.as_u16(), + })), + ) + } + Err(_err) => { + return ( + StatusCode::BAD_REQUEST, + Json(json!({ + "code": StatusCode::BAD_REQUEST.as_u16(), + })), + ) + } + } +} + +pub async fn delete_poster(Path(id): Path) -> impl IntoResponse { + let path = format!("games/{}/poster", id); + + match crate::media::delete(path).await { + Ok(_) => { + return ( + StatusCode::OK, + Json(json!({ + "code": StatusCode::OK.as_u16(), + })), + ) + } + Err(_err) => { + return ( + StatusCode::NOT_FOUND, + Json(json!({ + "code": StatusCode::NOT_FOUND.as_u16(), + })), + ) + } + } +} diff --git a/src/server/controller/media.rs b/src/server/controller/media.rs new file mode 100644 index 00000000..72f352f3 --- /dev/null +++ b/src/server/controller/media.rs @@ -0,0 +1,22 @@ +use axum::{ + extract::Path, + http::{header, StatusCode}, + response::{IntoResponse, Response}, +}; + +pub async fn get_file(Path(path): Path) -> impl IntoResponse { + let filename = path.split("/").last().unwrap_or("attachment"); + match crate::media::get(path.clone(), filename.to_string()).await { + Ok(buffer) => { + return Response::builder() + .header(header::CONTENT_TYPE, "application/octet-stream") + .header( + header::CONTENT_DISPOSITION, + format!("attachment; filename=\"{}\"", filename), + ) + .body(buffer.into()) + .unwrap(); + } + Err(_) => return (StatusCode::NOT_FOUND).into_response(), + } +} diff --git a/src/server/controller/mod.rs b/src/server/controller/mod.rs new file mode 100644 index 00000000..0e1da993 --- /dev/null +++ b/src/server/controller/mod.rs @@ -0,0 +1,9 @@ +pub mod category; +pub mod challenge; +pub mod config; +pub mod game; +pub mod media; +pub mod pod; +pub mod submission; +pub mod team; +pub mod user; diff --git a/src/server/controller/pod.rs b/src/server/controller/pod.rs new file mode 100644 index 00000000..9c628301 --- /dev/null +++ b/src/server/controller/pod.rs @@ -0,0 +1,163 @@ +use axum::{ + extract::{Path, Query}, + http::StatusCode, + response::IntoResponse, + Extension, Json, +}; +use serde_json::json; + +use crate::{server::service, traits::Ext}; + +pub async fn find( + Query(params): Query, +) -> impl IntoResponse { + match service::pod::find(params).await { + Ok((pods, total)) => ( + StatusCode::OK, + Json(json!({ + "code": StatusCode::OK.as_u16(), + "data": json!(pods), + "total": total, + })), + ), + Err(e) => ( + StatusCode::BAD_REQUEST, + Json(json!({ + "code": StatusCode::BAD_REQUEST.as_u16(), + "msg": format!("{:?}", e), + })), + ), + } +} + +pub async fn create( + Extension(ext): Extension, + Json(mut body): Json, +) -> impl IntoResponse { + let operator = ext.operator.clone().unwrap(); + body.user_id = Some(operator.id); + + match service::pod::create(body).await { + Ok(pod) => ( + StatusCode::OK, + Json(json!({ + "code": StatusCode::OK.as_u16(), + "data": json!(pod), + })), + ), + Err(e) => ( + StatusCode::BAD_REQUEST, + Json(json!({ + "code": StatusCode::BAD_REQUEST.as_u16(), + "msg": format!("{:?}", e), + })), + ), + } +} + +pub async fn update(Extension(ext): Extension, Path(id): Path) -> impl IntoResponse { + let operator = ext.operator.clone().unwrap(); + let (pods, total) = service::pod::find(crate::model::pod::request::FindRequest { + id: Some(operator.id), + ..Default::default() + }) + .await + .unwrap(); + + let pod = pods + .get(0) + .ok_or_else(|| { + ( + StatusCode::NOT_FOUND, + Json(json!({ + "code": StatusCode::NOT_FOUND.as_u16(), + })), + ) + }) + .unwrap(); + + if operator.group == "admin" + || operator.id == pod.user_id + || operator + .teams + .iter() + .any(|team| Some(team.id) == pod.team_id) + { + match service::pod::update(id).await { + Ok(_) => ( + StatusCode::OK, + Json(json!({ + "code": StatusCode::OK.as_u16(), + })), + ), + Err(e) => ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(json!({ + "code": StatusCode::INTERNAL_SERVER_ERROR.as_u16(), + })), + ), + } + } else { + ( + StatusCode::FORBIDDEN, + Json(json!({ + "code": StatusCode::FORBIDDEN.as_u16(), + })), + ) + } +} + +pub async fn delete(Extension(ext): Extension, Path(id): Path) -> impl IntoResponse { + let operator = ext.operator.clone().unwrap(); + let (pods, total) = service::pod::find(crate::model::pod::request::FindRequest { + id: Some(id), + ..Default::default() + }) + .await + .unwrap(); + + if total == 0 { + return ( + StatusCode::NOT_FOUND, + Json(json!({ + "code": StatusCode::NOT_FOUND.as_u16(), + })), + ); + } + + let pod = pods.get(0).unwrap(); + + if operator.group == "admin" + || operator.id == pod.user_id + || operator + .teams + .iter() + .any(|team| Some(team.id) == pod.team_id) + { + match service::pod::delete(id).await { + Ok(_) => { + return ( + StatusCode::OK, + Json(json!({ + "code": StatusCode::OK.as_u16(), + })), + ) + } + Err(e) => { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(json!({ + "code": StatusCode::INTERNAL_SERVER_ERROR.as_u16(), + })), + ) + } + } + } else { + return ( + StatusCode::FORBIDDEN, + Json(json!({ + "code": StatusCode::FORBIDDEN.as_u16(), + })), + ); + } +} diff --git a/src/server/controller/submission.rs b/src/server/controller/submission.rs new file mode 100644 index 00000000..b5639211 --- /dev/null +++ b/src/server/controller/submission.rs @@ -0,0 +1,113 @@ +use axum::{ + extract::{Path, Query}, + http::StatusCode, + response::IntoResponse, + Extension, Json, +}; +use serde_json::json; + +use crate::{server::service::submission as submission_service, traits::Ext}; + +/// **Find** can be used to find submissions. +/// +/// ## Arguments +/// - `params`: A `FindRequest` struct containing the parameters for the find operation. +/// - `ext`: An `Ext` struct containing the current operator. +/// +/// ## Returns +/// - `200`: find successfully. +/// - `403`: operator does not have permission to find submissions. +/// - `400`: find failed. +pub async fn find( + Extension(ext): Extension, + Query(params): Query, +) -> impl IntoResponse { + let operator = ext.operator.unwrap(); + if operator.group != "admin" && params.is_detailed.unwrap_or(false) { + return ( + StatusCode::FORBIDDEN, + Json(json!({ + "code": StatusCode::FORBIDDEN.as_u16(), + })), + ); + } + + match submission_service::find(params).await { + Ok((submissions, total)) => ( + StatusCode::OK, + Json(json!({ + "code": StatusCode::OK.as_u16(), + "data": json!(submissions), + "total": total, + })), + ), + Err(e) => ( + StatusCode::BAD_REQUEST, + Json(json!({ + "code": StatusCode::BAD_REQUEST.as_u16(), + })), + ), + } +} + +pub async fn create( + Extension(ext): Extension, + Json(mut body): Json, +) -> impl IntoResponse { + let operator = ext.operator.unwrap(); + body.user_id = Some(operator.id); + match submission_service::create(body).await { + Ok(submission) => ( + StatusCode::OK, + Json(json!({ + "code": StatusCode::OK.as_u16(), + "data": json!(submission), + })), + ), + Err(e) => ( + StatusCode::BAD_REQUEST, + Json(json!({ + "code": StatusCode::BAD_REQUEST.as_u16(), + "msg": format!("{:?}", e), + })), + ), + } +} + +// pub async fn update( +// Path(id): Path, +// validate::Json(mut body): validate::Json, +// ) -> impl IntoResponse { +// body.id = Some(id); +// match submission_service::update(body).await { +// Ok(()) => ( +// StatusCode::OK, +// Json(json!({ +// "code": StatusCode::OK.as_u16(), +// })), +// ), +// Err(e) => ( +// StatusCode::BAD_REQUEST, +// Json(json!({ +// "code": StatusCode::BAD_REQUEST.as_u16(), +// })), +// ), +// } +// } + +pub async fn delete(Path(id): Path) -> impl IntoResponse { + match submission_service::delete(id).await { + Ok(()) => ( + StatusCode::OK, + Json(json!({ + "code": StatusCode::OK.as_u16() + })), + ), + Err(_err) => ( + StatusCode::BAD_REQUEST, + Json(json!({ + "code": StatusCode::BAD_REQUEST.as_u16(), + })), + ), + } +} diff --git a/src/server/controller/team.rs b/src/server/controller/team.rs new file mode 100644 index 00000000..3359990d --- /dev/null +++ b/src/server/controller/team.rs @@ -0,0 +1,400 @@ +use crate::{ + server::service::{team as team_service, user_team as user_team_service}, + traits::Ext, +}; +use axum::{ + extract::{Multipart, Path, Query}, + http::{Response, StatusCode}, + response::IntoResponse, + Extension, Json, +}; +use mime::Mime; +use serde_json::json; + +fn can_modify_team(user: crate::model::user::Model, team_id: i64) -> bool { + return user.group == "admin" + || user + .teams + .iter() + .any(|team| team.id == team_id && team.captain_id == user.id); +} + +pub async fn find( + Query(params): Query, +) -> impl IntoResponse { + match team_service::find(params).await { + Ok((teams, total)) => ( + StatusCode::OK, + Json(json!({ + "code": StatusCode::OK.as_u16(), + "data": json!(teams), + "total": total, + })), + ), + Err(e) => ( + StatusCode::BAD_REQUEST, + Json(json!({ + "code": StatusCode::BAD_REQUEST.as_u16(), + "msg": format!("{:?}", e), + })), + ), + } +} + +pub async fn create( + Json(body): Json, +) -> impl IntoResponse { + match team_service::create(body).await { + Ok(team) => ( + StatusCode::OK, + Json(json!({ + "code": StatusCode::OK.as_u16(), + "data": json!(team), + })), + ), + Err(e) => ( + StatusCode::BAD_REQUEST, + Json(json!({ + "code": StatusCode::BAD_REQUEST.as_u16(), + "msg": format!("{:?}", e), + })), + ), + } +} + +pub async fn update( + Extension(ext): Extension, + Path(id): Path, + Json(mut body): Json, +) -> impl IntoResponse { + if !can_modify_team(ext.operator.unwrap(), id) { + return ( + StatusCode::FORBIDDEN, + Json(json!({ + "code": StatusCode::FORBIDDEN.as_u16(), + "msg": "forbidden", + })), + ); + } + body.id = Some(id); + match team_service::update(body).await { + Ok(()) => ( + StatusCode::OK, + Json(json!({ + "code": StatusCode::OK.as_u16(), + })), + ), + Err(e) => ( + StatusCode::BAD_REQUEST, + Json(json!({ + "code": StatusCode::BAD_REQUEST.as_u16(), + "msg": format!("{:?}", e), + })), + ), + } +} + +pub async fn delete(Extension(ext): Extension, Path(id): Path) -> impl IntoResponse { + if !can_modify_team(ext.operator.unwrap(), id) { + return ( + StatusCode::FORBIDDEN, + Json(json!({ + "code": StatusCode::FORBIDDEN.as_u16(), + "msg": "forbidden", + })), + ); + } + match team_service::delete(id).await { + Ok(()) => ( + StatusCode::OK, + Json(json!({ + "code": StatusCode::OK.as_u16(), + })), + ), + Err(e) => ( + StatusCode::BAD_REQUEST, + Json(json!({ + "code": StatusCode::BAD_REQUEST.as_u16(), + "msg": format!("{:?}", e), + })), + ), + } +} + +pub async fn create_user( + Json(body): Json, +) -> impl IntoResponse { + match user_team_service::create(body).await { + Ok(()) => ( + StatusCode::OK, + Json(json!({ + "code": StatusCode::OK.as_u16(), + })), + ), + Err(e) => ( + StatusCode::BAD_REQUEST, + Json(json!({ + "code": StatusCode::BAD_REQUEST.as_u16(), + "msg": format!("{:?}", e), + })), + ), + } +} + +pub async fn delete_user(Path((id, user_id)): Path<(i64, i64)>) -> impl IntoResponse { + match user_team_service::delete(user_id, id).await { + Ok(()) => ( + StatusCode::OK, + Json(json!({ + "code": StatusCode::OK.as_u16(), + })), + ), + Err(e) => ( + StatusCode::BAD_REQUEST, + Json(json!({ + "code": StatusCode::BAD_REQUEST.as_u16(), + "msg": format!("{:?}", e), + })), + ), + } +} + +pub async fn get_invite_token( + Extension(ext): Extension, + Path(id): Path, +) -> impl IntoResponse { + if !can_modify_team(ext.operator.unwrap(), id) { + return ( + StatusCode::FORBIDDEN, + Json(json!({ + "code": StatusCode::FORBIDDEN.as_u16(), + "msg": "forbidden", + })), + ); + } + match team_service::get_invite_token(id).await { + Ok(token) => { + return ( + StatusCode::OK, + Json(json!({ + "code": StatusCode::OK.as_u16(), + "token": token, + })), + ) + } + Err(e) => { + return ( + StatusCode::BAD_REQUEST, + Json(json!({ + "code": StatusCode::BAD_REQUEST.as_u16(), + "msg": format!("{:?}", e), + })), + ) + } + } +} + +pub async fn update_invite_token( + Extension(ext): Extension, + Path(id): Path, +) -> impl IntoResponse { + if !can_modify_team(ext.operator.unwrap(), id) { + return ( + StatusCode::FORBIDDEN, + Json(json!({ + "code": StatusCode::FORBIDDEN.as_u16(), + "msg": "forbidden", + })), + ); + } + match team_service::update_invite_token(id).await { + Ok(token) => { + return ( + StatusCode::OK, + Json(json!({ + "code": StatusCode::OK.as_u16(), + "token": token, + })), + ) + } + Err(e) => { + return ( + StatusCode::BAD_REQUEST, + Json(json!({ + "code": StatusCode::BAD_REQUEST.as_u16(), + "msg": format!("{:?}", e), + })), + ) + } + } +} + +pub async fn join( + Json(body): Json, +) -> impl IntoResponse { + match user_team_service::join(body).await { + Ok(()) => { + return ( + StatusCode::OK, + Json(json!({ + "code": StatusCode::OK.as_u16(), + })), + ) + } + Err(e) => { + return ( + StatusCode::BAD_REQUEST, + Json(json!({ + "code": StatusCode::BAD_REQUEST.as_u16(), + "msg": format!("{:?}", e), + })), + ) + } + } +} + +pub async fn leave() -> impl IntoResponse { + todo!() +} + +pub async fn find_avatar_metadata(Path(id): Path) -> impl IntoResponse { + let path = format!("teams/{}/avatar", id); + match crate::media::scan_dir(path.clone()).await.unwrap().first() { + Some((filename, size)) => { + return ( + StatusCode::OK, + Json(json!({ + "code": StatusCode::OK.as_u16(), + "data": { + "filename": filename, + "size": size, + }, + })), + ) + } + None => { + return ( + StatusCode::NOT_FOUND, + Json(json!({ + "code": StatusCode::NOT_FOUND.as_u16(), + })), + ) + } + } +} + +pub async fn find_avatar(Path(id): Path) -> impl IntoResponse { + let path = format!("teams/{}/avatar", id); + match crate::media::scan_dir(path.clone()).await.unwrap().first() { + Some((filename, _size)) => { + let buffer = crate::media::get(path, filename.to_string()).await.unwrap(); + return Response::builder().body(buffer.into()).unwrap(); + } + None => return (StatusCode::NOT_FOUND).into_response(), + } +} + +pub async fn save_avatar( + Extension(ext): Extension, + Path(id): Path, + mut multipart: Multipart, +) -> impl IntoResponse { + if !can_modify_team(ext.operator.unwrap(), id) { + return ( + StatusCode::FORBIDDEN, + Json(json!({ + "code": StatusCode::FORBIDDEN.as_u16(), + "msg": "forbidden", + })), + ); + } + + let path = format!("teams/{}/avatar", id); + let mut filename = String::new(); + let mut data = Vec::::new(); + while let Some(field) = multipart.next_field().await.unwrap() { + if field.name() == Some("file") { + filename = field.file_name().unwrap().to_string(); + let content_type = field.content_type().unwrap().to_string(); + let mime: Mime = content_type.parse().unwrap(); + if mime.type_() != mime::IMAGE { + return ( + StatusCode::BAD_REQUEST, + Json(json!({ + "code": StatusCode::BAD_REQUEST.as_u16(), + "msg": "forbidden_file_type", + })), + ); + } + data = match field.bytes().await { + Ok(bytes) => bytes.to_vec(), + Err(_err) => { + return ( + StatusCode::BAD_REQUEST, + Json(json!({ + "code": StatusCode::BAD_REQUEST.as_u16(), + "msg": "size_too_large", + })), + ); + } + }; + } + } + + crate::media::delete(path.clone()).await.unwrap(); + + match crate::media::save(path, filename, data).await { + Ok(_) => { + return ( + StatusCode::OK, + Json(json!({ + "code": StatusCode::OK.as_u16(), + })), + ) + } + Err(_err) => { + return ( + StatusCode::BAD_REQUEST, + Json(json!({ + "code": StatusCode::BAD_REQUEST.as_u16(), + })), + ) + } + } +} + +pub async fn delete_avatar( + Extension(ext): Extension, + Path(id): Path, +) -> impl IntoResponse { + if !can_modify_team(ext.operator.unwrap(), id) { + return ( + StatusCode::FORBIDDEN, + Json(json!({ + "code": StatusCode::FORBIDDEN.as_u16(), + "msg": "forbidden", + })), + ); + } + + let path = format!("teams/{}/avatar", id); + + match crate::media::delete(path).await { + Ok(_) => { + return ( + StatusCode::OK, + Json(json!({ + "code": StatusCode::OK.as_u16(), + })), + ) + } + Err(_err) => { + return ( + StatusCode::NOT_FOUND, + Json(json!({ + "code": StatusCode::NOT_FOUND.as_u16(), + })), + ) + } + } +} diff --git a/src/server/controller/user.rs b/src/server/controller/user.rs new file mode 100644 index 00000000..d9066ae0 --- /dev/null +++ b/src/server/controller/user.rs @@ -0,0 +1,383 @@ +use axum::{ + extract::{Multipart, Path, Query}, + http::{Response, StatusCode}, + response::IntoResponse, + Extension, Json, +}; +use mime::Mime; +use serde_json::json; + +use crate::{server::service::user as user_service, traits::Ext, util::validate}; + +/// **Find** can find user's information. +/// +/// ## Arguments +/// - `params`: user's information. +/// +/// ## Returns +/// - `200`: find successfully. +/// - `400`: find failed. +pub async fn find( + Query(params): Query, +) -> impl IntoResponse { + match user_service::find(params).await { + Ok((users, total)) => ( + StatusCode::OK, + Json(json!({ + "code": StatusCode::OK.as_u16(), + "data": json!(users), + "total": total, + })), + ), + Err(e) => ( + StatusCode::BAD_REQUEST, + Json(json!({ + "code": StatusCode::BAD_REQUEST.as_u16(), + "msg": format!("{:?}", e), + })), + ), + } +} + +/// **Create** can create a new user. +/// +/// ## Arguments +/// - `body`: user's information(validated). +/// +/// ## Returns +/// - `200`: create successfully. +/// - `400`: create failed. +pub async fn create( + validate::Json(body): validate::Json, +) -> impl IntoResponse { + match user_service::create(body).await { + Ok(user) => ( + StatusCode::OK, + Json(json!({ + "code": StatusCode::OK.as_u16(), + "data": json!(user), + })), + ), + Err(e) => ( + StatusCode::BAD_REQUEST, + Json(json!({ + "code": StatusCode::BAD_REQUEST.as_u16(), + "msg": format!("{:?}", e), + })), + ), + } +} + +/// **Update** can update user's information. +/// +/// ## Arguments +/// - `id`: user's id. +/// - `body`: user's information(validated). +/// - `ext`: extension from middleware. +/// +/// ## Returns +/// - `200`: update successfully. +/// - `400`: update failed. +/// +/// There are some restrictions: +/// - If operator's group is "admin", operator can update any user's information. +/// - If operator's group is not "admin", operator can update his own information, but can not update group. +pub async fn update( + Extension(ext): Extension, + Path(id): Path, + validate::Json(mut body): validate::Json, +) -> impl IntoResponse { + let operator = ext.clone().operator.unwrap(); + body.id = Some(id); + if operator.group == "admin" + || (operator.id == body.id.unwrap_or(0) + && (body.group.clone().is_none() + || operator.group == body.group.clone().unwrap_or("".to_string()))) + { + match user_service::update(body).await { + Ok(()) => { + return ( + StatusCode::OK, + Json(json!({ + "code": StatusCode::OK.as_u16() + })), + ) + } + Err(e) => { + return ( + StatusCode::BAD_REQUEST, + Json(json!({ + "code": StatusCode::BAD_REQUEST.as_u16(), + "msg": format!("{:?}", e), + })), + ) + } + } + } else { + return ( + StatusCode::FORBIDDEN, + Json(json!({ + "code": StatusCode::FORBIDDEN.as_u16(), + "msg": format!("{:?}", "forbidden"), + })), + ); + } +} + +/// **Delete** can be used to delete user. +/// +/// ## Arguments +/// - `id`: user's id. +/// +/// ## Returns +/// - `200`: delete successfully. +/// - `400`: delete failed. +pub async fn delete(Path(id): Path) -> impl IntoResponse { + match user_service::delete(id).await { + Ok(()) => { + return ( + StatusCode::OK, + Json(json!({ + "code": StatusCode::OK.as_u16() + })), + ) + } + Err(e) => { + return ( + StatusCode::BAD_REQUEST, + Json(json!({ + "code": StatusCode::BAD_REQUEST.as_u16(), + "msg": format!("{:?}", e), + })), + ) + } + } +} + +/// **Login** can be used to login with username and password. +/// +/// ## Arguments +/// - `body`: username and password. +/// +/// ## Returns +/// - `200`: login successfully, with token and user information. +/// - `400`: login failed. +pub async fn login( + Json(body): Json, +) -> impl IntoResponse { + match user_service::login(body).await { + Ok((user, token)) => { + return ( + StatusCode::OK, + Json(json!({ + "code": StatusCode::OK.as_u16(), + "data": json!(user), + "token": token, + })), + ) + } + Err(e) => { + return ( + StatusCode::BAD_REQUEST, + Json(json!({ + "code": StatusCode::BAD_REQUEST.as_u16(), + "msg": format!("{:?}", e), + })), + ) + } + } +} + +/// **Register** can be used to register with username, nickname, email and password. +/// +/// ## Arguments +/// - `body`: username, nickname, email and password. +/// +/// ## Returns +/// - `200`: register successfully, with user information. +/// - `400`: register failed. +/// - `409`: username or email has been registered. +/// - `500`: internal server error(most likely database error). +pub async fn register( + validate::Json(body): validate::Json, +) -> impl IntoResponse { + match user_service::register(body).await { + Ok(user) => { + return ( + StatusCode::OK, + Json(json!({ + "code": StatusCode::OK.as_u16(), + "data": json!(user), + })), + ) + } + Err(err) => match err { + StatusCode::CONFLICT => { + return ( + StatusCode::CONFLICT, + Json(json!({ + "code": StatusCode::CONFLICT.as_u16(), + })), + ) + } + StatusCode::INTERNAL_SERVER_ERROR => { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(json!({ + "code": StatusCode::INTERNAL_SERVER_ERROR.as_u16(), + })), + ) + } + _ => { + return ( + StatusCode::BAD_REQUEST, + Json(json!({ + "code": StatusCode::BAD_REQUEST.as_u16(), + })), + ) + } + }, + } +} + +pub async fn find_avatar(Path(id): Path) -> impl IntoResponse { + let path = format!("users/{}/avatar", id); + match crate::media::scan_dir(path.clone()).await.unwrap().first() { + Some((filename, _size)) => { + let buffer = crate::media::get(path, filename.to_string()).await.unwrap(); + return Response::builder().body(buffer.into()).unwrap(); + } + None => return (StatusCode::NOT_FOUND).into_response(), + } +} + +pub async fn find_avatar_metadata(Path(id): Path) -> impl IntoResponse { + let path = format!("users/{}/avatar", id); + match crate::media::scan_dir(path.clone()).await.unwrap().first() { + Some((filename, size)) => { + return ( + StatusCode::OK, + Json(json!({ + "code": StatusCode::OK.as_u16(), + "data": { + "filename": filename, + "size": size, + }, + })), + ) + } + None => { + return ( + StatusCode::NOT_FOUND, + Json(json!({ + "code": StatusCode::NOT_FOUND.as_u16(), + })), + ) + } + } +} + +pub async fn save_avatar( + Extension(ext): Extension, + Path(id): Path, + mut multipart: Multipart, +) -> impl IntoResponse { + let operator = ext.operator.unwrap(); + if operator.group != "admin" && operator.id != id { + return ( + StatusCode::FORBIDDEN, + Json(json!({ + "code": StatusCode::FORBIDDEN.as_u16(), + })), + ); + } + + let path = format!("users/{}/avatar", id); + let mut filename = String::new(); + let mut data = Vec::::new(); + while let Some(field) = multipart.next_field().await.unwrap() { + if field.name() == Some("file") { + filename = field.file_name().unwrap().to_string(); + let content_type = field.content_type().unwrap().to_string(); + let mime: Mime = content_type.parse().unwrap(); + if mime.type_() != mime::IMAGE { + return ( + StatusCode::BAD_REQUEST, + Json(json!({ + "code": StatusCode::BAD_REQUEST.as_u16(), + "msg": "forbidden_file_type", + })), + ); + } + data = match field.bytes().await { + Ok(bytes) => bytes.to_vec(), + Err(_err) => { + return ( + StatusCode::BAD_REQUEST, + Json(json!({ + "code": StatusCode::BAD_REQUEST.as_u16(), + "msg": "size_too_large", + })), + ); + } + }; + } + } + + crate::media::delete(path.clone()).await.unwrap(); + + match crate::media::save(path, filename, data).await { + Ok(_) => { + return ( + StatusCode::OK, + Json(json!({ + "code": StatusCode::OK.as_u16(), + })), + ) + } + Err(_err) => { + return ( + StatusCode::BAD_REQUEST, + Json(json!({ + "code": StatusCode::BAD_REQUEST.as_u16(), + })), + ) + } + } +} + +pub async fn delete_avatar( + Extension(ext): Extension, + Path(id): Path, +) -> impl IntoResponse { + let operator = ext.operator.unwrap(); + if operator.group != "admin" && operator.id != id { + return ( + StatusCode::FORBIDDEN, + Json(json!({ + "code": StatusCode::FORBIDDEN.as_u16(), + })), + ); + } + + let path = format!("users/{}/avatar", id); + + match crate::media::delete(path).await { + Ok(_) => { + return ( + StatusCode::OK, + Json(json!({ + "code": StatusCode::OK.as_u16(), + })), + ) + } + Err(_err) => { + return ( + StatusCode::NOT_FOUND, + Json(json!({ + "code": StatusCode::NOT_FOUND.as_u16(), + })), + ) + } + } +} diff --git a/src/server/middleware/auth.rs b/src/server/middleware/auth.rs new file mode 100644 index 00000000..c15acd68 --- /dev/null +++ b/src/server/middleware/auth.rs @@ -0,0 +1,95 @@ +use crate::model::user::request::FindRequest; +use axum::{ + extract::Request, + http::StatusCode, + middleware::Next, + response::{IntoResponse, Response}, + Json, +}; +use jsonwebtoken::{decode, DecodingKey, Validation}; +use serde_json::json; +use std::future::Future; +use std::pin::Pin; + +use crate::server::service::user as user_service; +use crate::{traits::Ext, util}; + +pub fn jwt( + group: util::jwt::Group, +) -> impl Fn( + Request, + Next, +) -> Pin> + Send>> + + Clone { + move |mut req: Request, next: Next| { + Box::pin({ + let value = group.clone(); + async move { + let token = req + .headers() + .get("Authorization") + .and_then(|header| header.to_str().ok()) + // .and_then(|header| header.strip_prefix("Bearer ")) + .unwrap_or(""); + + let decoding_key = + DecodingKey::from_secret(util::jwt::get_secret().await.as_bytes()); + let validation = Validation::default(); + + match decode::(token, &decoding_key, &validation) { + Ok(token_data) => { + let (users, total) = user_service::find(FindRequest { + id: Some(token_data.claims.id), + ..Default::default() + }) + .await + .unwrap(); + + if total == 0 { + return Ok(( + StatusCode::UNAUTHORIZED, + Json(json!({ + "code": StatusCode::UNAUTHORIZED.as_u16(), + "msg": "unauthorized" + })), + ) + .into_response()); + } + + let user = users.get(0).unwrap(); + req.extensions_mut().insert(Ext { + operator: Some(user.clone()), + }); + + if (value as u8) + <= (util::jwt::Group::from_str(user.group.clone()) + .unwrap_or(util::jwt::Group::Banned) + as u8) + { + return Ok(next.run(req).await); + } else { + return Ok(( + StatusCode::FORBIDDEN, + Json(json!({ + "code": StatusCode::FORBIDDEN.as_u16(), + "msg": "forbidden" + })), + ) + .into_response()); + } + } + Err(_) => { + return Ok(( + StatusCode::UNAUTHORIZED, + Json(json!({ + "code": StatusCode::UNAUTHORIZED.as_u16(), + "msg": "unauthorized" + })), + ) + .into_response()); + } + } + } + }) + } +} diff --git a/src/server/middleware/mod.rs b/src/server/middleware/mod.rs new file mode 100644 index 00000000..0e4a05d5 --- /dev/null +++ b/src/server/middleware/mod.rs @@ -0,0 +1 @@ +pub mod auth; diff --git a/src/server/mod.rs b/src/server/mod.rs new file mode 100644 index 00000000..a063b9f3 --- /dev/null +++ b/src/server/mod.rs @@ -0,0 +1,59 @@ +pub mod controller; +pub mod middleware; +pub mod router; +pub mod service; + +use axum::Router; +use reqwest::Method; +use tower_http::{ + cors::{Any, CorsLayer}, + services::{ServeDir, ServeFile}, + trace::TraceLayer, +}; +use tracing::info; + +use crate::{config, container, database, logger, util}; + +pub async fn bootstrap() { + logger::init(); + config::init().await; + database::init().await; + container::init().await; + + info!("{:?}", util::jwt::get_secret().await); + + let cors = CorsLayer::new() + .allow_methods([ + Method::GET, + Method::POST, + Method::PUT, + Method::DELETE, + Method::OPTIONS, + ]) + .allow_headers(Any) + .allow_origin(Any); + + let app: Router = Router::new() + .merge( + Router::new() + .nest("/api", router::router()) + .layer(TraceLayer::new_for_http()), + ) + .merge(Router::new().fallback_service( + ServeDir::new("dist").not_found_service(ServeFile::new("dist/index.html")), + )) + .layer(cors); + + let addr = format!( + "{}:{}", + config::get_app_config().axum.host, + config::get_app_config().axum.port + ); + + let listener = tokio::net::TcpListener::bind(&addr).await; + info!( + "Cloudsdale service has been started at {}. Enjoy your hacking challenges!", + &addr + ); + axum::serve(listener.unwrap(), app).await.unwrap(); +} diff --git a/src/server/router/category.rs b/src/server/router/category.rs new file mode 100644 index 00000000..ff270a6f --- /dev/null +++ b/src/server/router/category.rs @@ -0,0 +1,25 @@ +use axum::{ + middleware::from_fn, + routing::{delete, get, post, put}, + Router, +}; + +use crate::server::{controller, middleware::auth}; +use crate::util::jwt::Group; + +pub fn router() -> Router { + return Router::new() + .route("/", get(controller::category::find)) + .route( + "/", + post(controller::category::create).layer(from_fn(auth::jwt(Group::Admin))), + ) + .route( + "/:id", + put(controller::category::update).layer(from_fn(auth::jwt(Group::Admin))), + ) + .route( + "/:id", + delete(controller::category::delete).layer(from_fn(auth::jwt(Group::Admin))), + ); +} diff --git a/src/server/router/challenge.rs b/src/server/router/challenge.rs new file mode 100644 index 00000000..5b800cf0 --- /dev/null +++ b/src/server/router/challenge.rs @@ -0,0 +1,52 @@ +use axum::{ + extract::DefaultBodyLimit, + middleware::from_fn, + routing::{delete, get, post, put}, + Router, +}; + +use crate::server::{controller, middleware::auth}; +use crate::util::jwt::Group; + +pub fn router() -> Router { + return Router::new() + .route( + "/", + get(controller::challenge::find).layer(from_fn(auth::jwt(Group::User))), + ) + .route( + "/", + post(controller::challenge::create).layer(from_fn(auth::jwt(Group::Admin))), + ) + .route( + "/status", + post(controller::challenge::status).layer(from_fn(auth::jwt(Group::User))), + ) + .route( + "/:id", + put(controller::challenge::update).layer(from_fn(auth::jwt(Group::Admin))), + ) + .route( + "/:id", + delete(controller::challenge::delete).layer(from_fn(auth::jwt(Group::Admin))), + ) + .route( + "/:id/attachment", + get(controller::challenge::find_attachment), + ) + .route( + "/:id/attachment/metadata", + get(controller::challenge::find_attachment_metadata), + ) + .route( + "/:id/attachment", + post(controller::challenge::save_attachment) + .layer(DefaultBodyLimit::max(512 * 1024 * 1024 /* MB */)) + .layer(from_fn(auth::jwt(Group::Admin))), + ) + .route( + "/:id/attachment", + delete(controller::challenge::delete_attachment) + .layer(from_fn(auth::jwt(Group::Admin))), + ); +} diff --git a/src/server/router/config.rs b/src/server/router/config.rs new file mode 100644 index 00000000..486a2608 --- /dev/null +++ b/src/server/router/config.rs @@ -0,0 +1,9 @@ +use axum::{routing::get, Router}; + +use crate::server::controller; + +pub fn router() -> Router { + return Router::new() + .route("/", get(controller::config::find)) + .route("/favicon", get(controller::config::get_favicon)); +} diff --git a/src/server/router/game.rs b/src/server/router/game.rs new file mode 100644 index 00000000..1697f253 --- /dev/null +++ b/src/server/router/game.rs @@ -0,0 +1,88 @@ +use axum::{ + extract::DefaultBodyLimit, + middleware::from_fn, + routing::{delete, get, post, put}, + Router, +}; + +use crate::server::{controller, middleware::auth}; +use crate::util::jwt::Group; + +pub fn router() -> Router { + return Router::new() + .route( + "/", + get(controller::game::find).layer(from_fn(auth::jwt(Group::User))), + ) + .route( + "/", + post(controller::game::create).layer(from_fn(auth::jwt(Group::Admin))), + ) + .route( + "/:id", + put(controller::game::update).layer(from_fn(auth::jwt(Group::Admin))), + ) + .route( + "/:id", + delete(controller::game::delete).layer(from_fn(auth::jwt(Group::Admin))), + ) + .route( + "/:id/challenges", + get(controller::game::find_challenge).layer(from_fn(auth::jwt(Group::User))), + ) + .route( + "/:id/challenges", + post(controller::game::create_challenge).layer(from_fn(auth::jwt(Group::Admin))), + ) + .route( + "/:id/challenges/:challenge_id", + put(controller::game::update_challenge).layer(from_fn(auth::jwt(Group::Admin))), + ) + .route( + "/:id/challenges/:challenge_id", + delete(controller::game::delete_challenge).layer(from_fn(auth::jwt(Group::Admin))), + ) + .route( + "/:id/teams", + get(controller::game::find_team).layer(from_fn(auth::jwt(Group::User))), + ) + .route( + "/:id/teams", + post(controller::game::create_team).layer(from_fn(auth::jwt(Group::User))), + ) + .route( + "/:id/teams/:team_id", + put(controller::game::update_team).layer(from_fn(auth::jwt(Group::Admin))), + ) + .route( + "/:id/teams/:team_id", + delete(controller::game::delete_team).layer(from_fn(auth::jwt(Group::Admin))), + ) + .route( + "/:id/notices", + get(controller::game::find_notice).layer(from_fn(auth::jwt(Group::User))), + ) + .route( + "/:id/notices", + post(controller::game::create_notice).layer(from_fn(auth::jwt(Group::Admin))), + ) + .route( + "/:id/notices/:notice_id", + put(controller::game::update_notice).layer(from_fn(auth::jwt(Group::Admin))), + ) + .route( + "/:id/notices/:notice_id", + delete(controller::game::delete_notice).layer(from_fn(auth::jwt(Group::Admin))), + ) + .route("/:id/poster", get(controller::game::find_poster)) + .route( + "/:id/poster", + post(controller::game::save_poster) + .layer(DefaultBodyLimit::max(3 * 1024 * 1024 /* MB */)) + .layer(from_fn(auth::jwt(Group::Admin))), + ) + .route( + "/:id/poster", + delete(controller::game::delete_poster).layer(from_fn(auth::jwt(Group::Admin))), + ); +} diff --git a/src/server/router/media.rs b/src/server/router/media.rs new file mode 100644 index 00000000..6d1ce8c1 --- /dev/null +++ b/src/server/router/media.rs @@ -0,0 +1,7 @@ +use axum::{routing::get, Router}; + +use crate::server::controller; + +pub fn router() -> Router { + return Router::new().route("/*path", get(controller::media::get_file)); +} diff --git a/src/server/router/mod.rs b/src/server/router/mod.rs new file mode 100644 index 00000000..4e412912 --- /dev/null +++ b/src/server/router/mod.rs @@ -0,0 +1,36 @@ +pub mod category; +pub mod challenge; +pub mod config; +pub mod game; +pub mod media; +pub mod pod; +pub mod proxy; +pub mod submission; +pub mod team; +pub mod user; + +use axum::http::StatusCode; +use axum::{Json, Router}; +use serde_json::json; + +pub fn router() -> Router { + return Router::new() + .route( + "/", + axum::routing::any(|| async { + Json(json!({ + "code": StatusCode::OK.as_u16(), + "msg": format!("{:?}", "This is the heart of Cloudsdale!") + })) + }), + ) + .nest("/configs", config::router()) + .nest("/media", media::router()) + .nest("/categories", category::router()) + .nest("/users", user::router()) + .nest("/teams", team::router()) + .nest("/challenges", challenge::router()) + .nest("/games", game::router()) + .nest("/pods", pod::router()) + .nest("/submissions", submission::router()); +} diff --git a/src/server/router/pod.rs b/src/server/router/pod.rs new file mode 100644 index 00000000..8dbadbb8 --- /dev/null +++ b/src/server/router/pod.rs @@ -0,0 +1,28 @@ +use axum::{ + middleware::from_fn, + routing::{delete, get, post, put}, + Router, +}; + +use crate::server::{controller, middleware::auth}; +use crate::util::jwt::Group; + +pub fn router() -> Router { + return Router::new() + .route( + "/", + get(controller::pod::find).layer(from_fn(auth::jwt(Group::User))), + ) + .route( + "/", + post(controller::pod::create).layer(from_fn(auth::jwt(Group::User))), + ) + .route( + "/:id", + put(controller::pod::update).layer(from_fn(auth::jwt(Group::User))), + ) + .route( + "/:id", + delete(controller::pod::delete).layer(from_fn(auth::jwt(Group::User))), + ); +} diff --git a/src/server/router/proxy.rs b/src/server/router/proxy.rs new file mode 100644 index 00000000..e69de29b diff --git a/src/server/router/submission.rs b/src/server/router/submission.rs new file mode 100644 index 00000000..9b3af86e --- /dev/null +++ b/src/server/router/submission.rs @@ -0,0 +1,24 @@ +use axum::{ + middleware::from_fn, + routing::{delete, get, post}, + Router, +}; + +use crate::server::{controller, middleware::auth}; +use crate::util::jwt::Group; + +pub fn router() -> Router { + return Router::new() + .route( + "/", + get(controller::submission::find).layer(from_fn(auth::jwt(Group::User))), + ) + .route( + "/", + post(controller::submission::create).layer(from_fn(auth::jwt(Group::User))), + ) + .route( + "/:id", + delete(controller::submission::delete).layer(from_fn(auth::jwt(Group::Admin))), + ); +} diff --git a/src/server/router/team.rs b/src/server/router/team.rs new file mode 100644 index 00000000..a055fee3 --- /dev/null +++ b/src/server/router/team.rs @@ -0,0 +1,68 @@ +use axum::{ + extract::DefaultBodyLimit, + middleware::from_fn, + routing::{delete, get, post, put}, + Router, +}; + +use crate::server::{controller, middleware::auth}; +use crate::util::jwt::Group; + +pub fn router() -> Router { + return Router::new() + .route( + "/", + get(controller::team::find).layer(from_fn(auth::jwt(Group::Guest))), + ) + .route( + "/", + post(controller::team::create).layer(from_fn(auth::jwt(Group::User))), + ) + .route( + "/:id", + put(controller::team::update).layer(from_fn(auth::jwt(Group::User))), + ) + .route( + "/:id", + delete(controller::team::delete).layer(from_fn(auth::jwt(Group::User))), + ) + .route( + "/:id/users", + post(controller::team::create_user).layer(from_fn(auth::jwt(Group::User))), + ) + .route( + "/:id/users/:user_id", + delete(controller::team::delete_user).layer(from_fn(auth::jwt(Group::User))), + ) + .route( + "/:id/invite", + get(controller::team::get_invite_token).layer(from_fn(auth::jwt(Group::User))), + ) + .route( + "/:id/invite", + put(controller::team::update_invite_token).layer(from_fn(auth::jwt(Group::User))), + ) + .route( + "/:id/join", + post(controller::team::join).layer(from_fn(auth::jwt(Group::User))), + ) + .route( + "/:id/leave", + delete(controller::team::leave).layer(from_fn(auth::jwt(Group::User))), + ) + .route("/:id/avatar", get(controller::team::find_avatar)) + .route( + "/:id/avatar/metadata", + get(controller::team::find_avatar_metadata), + ) + .route( + "/:id/avatar", + post(controller::team::save_avatar) + .layer(DefaultBodyLimit::max(3 * 1024 * 1024 /* MB */)) + .layer(from_fn(auth::jwt(Group::User))), + ) + .route( + "/:id/avatar", + delete(controller::team::delete_avatar).layer(from_fn(auth::jwt(Group::User))), + ); +} diff --git a/src/server/router/user.rs b/src/server/router/user.rs new file mode 100644 index 00000000..ba755786 --- /dev/null +++ b/src/server/router/user.rs @@ -0,0 +1,46 @@ +use axum::{ + extract::DefaultBodyLimit, + middleware::from_fn, + routing::{delete, get, post, put}, + Router, +}; + +use crate::server::{controller, middleware::auth}; +use crate::util::jwt::Group; + +pub fn router() -> Router { + return Router::new() + .route( + "/", + get(controller::user::find).layer(from_fn(auth::jwt(Group::Guest))), + ) + .route( + "/", + post(controller::user::create).layer(from_fn(auth::jwt(Group::Admin))), + ) + .route( + "/:id", + put(controller::user::update).layer(from_fn(auth::jwt(Group::User))), + ) + .route( + "/:id", + delete(controller::user::delete).layer(from_fn(auth::jwt(Group::Admin))), + ) + .route("/login", post(controller::user::login)) + .route("/register", post(controller::user::register)) + .route("/:id/avatar", get(controller::user::find_avatar)) + .route( + "/:id/avatar/metadata", + get(controller::user::find_avatar_metadata), + ) + .route( + "/:id/avatar", + post(controller::user::save_avatar) + .layer(DefaultBodyLimit::max(3 * 1024 * 1024 /* MB */)) + .layer(from_fn(auth::jwt(Group::User))), + ) + .route( + "/:id/avatar", + delete(controller::user::delete_avatar).layer(from_fn(auth::jwt(Group::User))), + ); +} diff --git a/src/server/service/category.rs b/src/server/service/category.rs new file mode 100644 index 00000000..e0b6958a --- /dev/null +++ b/src/server/service/category.rs @@ -0,0 +1,37 @@ +use std::error::Error; + +use sea_orm::TryIntoModel; + +pub async fn find( + req: crate::model::category::request::FindRequest, +) -> Result<(Vec, u64), Box> { + let (categories, total) = crate::repository::category::find(req.id, req.name) + .await + .unwrap(); + return Ok((categories, total)); +} + +pub async fn create( + req: crate::model::category::request::CreateRequest, +) -> Result> { + match crate::repository::category::create(req.into()).await { + Ok(port) => return Ok(port.try_into_model().unwrap()), + Err(err) => return Err(Box::new(err)), + } +} + +pub async fn update( + req: crate::model::category::request::UpdateRequest, +) -> Result<(), Box> { + match crate::repository::category::update(req.into()).await { + Ok(_) => return Ok(()), + Err(err) => return Err(Box::new(err)), + } +} + +pub async fn delete(id: i64) -> Result<(), Box> { + match crate::repository::category::delete(id).await { + Ok(_) => return Ok(()), + Err(err) => return Err(Box::new(err)), + } +} diff --git a/src/server/service/challenge.rs b/src/server/service/challenge.rs new file mode 100644 index 00000000..c894fc00 --- /dev/null +++ b/src/server/service/challenge.rs @@ -0,0 +1,119 @@ +use std::collections::HashMap; +use std::error::Error; + +use sea_orm::TryIntoModel; + +pub async fn find( + req: crate::model::challenge::request::FindRequest, +) -> Result<(Vec, u64), ()> { + let (mut challenges, total) = crate::repository::challenge::find( + req.id, + req.title, + req.category_id, + req.is_practicable, + req.is_dynamic, + req.page, + req.size, + ) + .await + .unwrap(); + + for challenge in challenges.iter_mut() { + let is_detailed = req.is_detailed.unwrap_or(false); + if !is_detailed { + challenge.flags.clear(); + } + } + + return Ok((challenges, total)); +} + +pub async fn status( + req: crate::model::challenge::request::StatusRequest, +) -> Result, Box> { + let mut submissions = crate::repository::submission::find_by_challenge_ids(req.cids.clone()) + .await + .unwrap(); + + let mut result: HashMap = + HashMap::new(); + + for cid in req.cids { + result + .entry(cid) + .or_insert_with(|| crate::model::challenge::response::StatusResponse { + is_solved: false, + solved_times: 0, + bloods: Vec::new(), + }); + } + + for submission in submissions.iter_mut() { + submission.simplify(); + submission.challenge = None; + + if submission.status != 2 { + continue; + } + + let status_response = result.get_mut(&submission.challenge_id).unwrap(); + + if let Some(user_id) = req.user_id { + if submission.user_id == user_id { + status_response.is_solved = true; + } + } + + if let Some(team_id) = req.team_id { + if let Some(game_id) = req.game_id { + if submission.team_id == Some(team_id) && submission.game_id == Some(game_id) { + status_response.is_solved = true; + } + } + } + + status_response.solved_times += 1; + if status_response.bloods.len() < 3 { + status_response.bloods.push(submission.clone()); + status_response + .bloods + .sort_by(|a, b| a.created_at.cmp(&b.created_at)); + } else { + let last_submission = status_response.bloods.last().unwrap(); + if submission.created_at < last_submission.created_at { + status_response.bloods.pop(); + status_response.bloods.push(submission.clone()); + status_response + .bloods + .sort_by(|a, b| a.created_at.cmp(&b.created_at)); + } + } + } + + return Ok(result); +} + +pub async fn create( + req: crate::model::challenge::request::CreateRequest, +) -> Result> { + match crate::repository::challenge::create(req.into()).await { + Ok(challenge) => return Ok(challenge.try_into_model().unwrap()), + Err(err) => return Err(Box::new(err)), + } +} + +pub async fn update( + req: crate::model::challenge::request::UpdateRequest, +) -> Result<(), Box> { + match crate::repository::challenge::update(req.into()).await { + Ok(_) => return Ok(()), + Err(err) => return Err(Box::new(err)), + } +} + +pub async fn delete(id: i64) -> Result<(), Box> { + match crate::repository::challenge::delete(id).await { + Ok(_) => return Ok(()), + Err(err) => return Err(Box::new(err)), + } +} diff --git a/src/server/service/game.rs b/src/server/service/game.rs new file mode 100644 index 00000000..722a98df --- /dev/null +++ b/src/server/service/game.rs @@ -0,0 +1,37 @@ +use std::error::Error; + +use sea_orm::TryIntoModel; + +pub async fn find( + req: crate::model::game::request::FindRequest, +) -> Result<(Vec, u64), Box> { + let (games, total) = + crate::repository::game::find(req.id, req.title, req.is_enabled, req.page, req.size) + .await + .unwrap(); + + return Ok((games, total)); +} + +pub async fn create( + req: crate::model::game::request::CreateRequest, +) -> Result> { + match crate::repository::game::create(req.into()).await { + Ok(game) => return Ok(game.try_into_model().unwrap()), + Err(err) => return Err(Box::new(err)), + } +} + +pub async fn update(req: crate::model::game::request::UpdateRequest) -> Result<(), Box> { + match crate::repository::game::update(req.into()).await { + Ok(_game) => return Ok(()), + Err(err) => return Err(Box::new(err)), + } +} + +pub async fn delete(id: i64) -> Result<(), Box> { + match crate::repository::game::delete(id).await { + Ok(_) => return Ok(()), + Err(err) => return Err(Box::new(err)), + } +} diff --git a/src/server/service/game_challenge.rs b/src/server/service/game_challenge.rs new file mode 100644 index 00000000..89e354dd --- /dev/null +++ b/src/server/service/game_challenge.rs @@ -0,0 +1,49 @@ +use std::error::Error; + +pub async fn find( + req: crate::model::game_challenge::request::FindRequest, +) -> Result<(Vec, u64), Box> { + let (game_challenges, total) = + crate::repository::game_challenge::find(req.game_id, req.challenge_id) + .await + .unwrap(); + + return Ok((game_challenges, total)); +} + +pub async fn create( + req: crate::model::game_challenge::request::CreateRequest, +) -> Result<(), Box> { + match crate::repository::game_challenge::create(req.into()).await { + Ok(_) => { + return Ok(()); + } + Err(e) => { + return Err(Box::new(e)); + } + }; +} + +pub async fn update( + req: crate::model::game_challenge::request::UpdateRequest, +) -> Result<(), Box> { + match crate::repository::game_challenge::update(req.into()).await { + Ok(_) => { + return Ok(()); + } + Err(e) => { + return Err(Box::new(e)); + } + }; +} + +pub async fn delete(id: i64, challenge_id: i64) -> Result<(), Box> { + match crate::repository::game_challenge::delete(id, challenge_id).await { + Ok(_) => { + return Ok(()); + } + Err(e) => { + return Err(Box::new(e)); + } + } +} diff --git a/src/server/service/game_team.rs b/src/server/service/game_team.rs new file mode 100644 index 00000000..1acf1188 --- /dev/null +++ b/src/server/service/game_team.rs @@ -0,0 +1,54 @@ +use std::error::Error; + +pub async fn find( + req: crate::model::game_team::request::FindRequest, +) -> Result<(Vec, u64), Box> { + let (mut game_teams, total) = crate::repository::game_team::find(req.game_id, req.team_id) + .await + .unwrap(); + + for game_team in game_teams.iter_mut() { + if let Some(team) = game_team.team.as_mut() { + team.simplify(); + } + } + + return Ok((game_teams, total)); +} + +pub async fn create( + req: crate::model::game_team::request::CreateRequest, +) -> Result<(), Box> { + match crate::repository::game_team::create(req.into()).await { + Ok(_) => { + return Ok(()); + } + Err(e) => { + return Err(Box::new(e)); + } + }; +} + +pub async fn update( + req: crate::model::game_team::request::UpdateRequest, +) -> Result<(), Box> { + match crate::repository::game_team::update(req.into()).await { + Ok(_) => { + return Ok(()); + } + Err(e) => { + return Err(Box::new(e)); + } + } +} + +pub async fn delete(id: i64, challenge_id: i64) -> Result<(), Box> { + match crate::repository::game_team::delete(id, challenge_id).await { + Ok(_) => { + return Ok(()); + } + Err(e) => { + return Err(Box::new(e)); + } + } +} diff --git a/src/server/service/mod.rs b/src/server/service/mod.rs new file mode 100644 index 00000000..18865f3f --- /dev/null +++ b/src/server/service/mod.rs @@ -0,0 +1,10 @@ +pub mod category; +pub mod challenge; +pub mod game; +pub mod game_challenge; +pub mod game_team; +pub mod pod; +pub mod submission; +pub mod team; +pub mod user; +pub mod user_team; diff --git a/src/server/service/pod.rs b/src/server/service/pod.rs new file mode 100644 index 00000000..1f410f31 --- /dev/null +++ b/src/server/service/pod.rs @@ -0,0 +1,134 @@ +use std::error::Error; + +use regex::Regex; +use sea_orm::{IntoActiveModel, Set}; +use uuid::Uuid; + +use crate::container::traits::Container; + +pub async fn find( + req: crate::model::pod::request::FindRequest, +) -> Result<(Vec, u64), ()> { + let (mut pods, total) = crate::repository::pod::find( + req.id, + req.name, + req.user_id, + req.team_id, + req.game_id, + req.challenge_id, + req.is_available, + ) + .await + .unwrap(); + + if let Some(is_detailed) = req.is_detailed { + if !is_detailed { + for pod in pods.iter_mut() { + pod.flag = None; + } + } + } + return Ok((pods, total)); +} + +pub async fn create( + req: crate::model::pod::request::CreateRequest, +) -> Result> { + let (challenges, _) = crate::repository::challenge::find( + Some(req.challenge_id.clone()), + None, + None, + None, + None, + None, + None, + ) + .await + .unwrap(); + + let challenge = challenges.get(0).unwrap(); + + let ctn_name = format!("cds-{}", Uuid::new_v4().simple().to_string()); + + if challenge.flags.clone().into_iter().next().is_none() { + return Err("No flags found".into()); + } + + let mut injected_flag = challenge.flags.clone().into_iter().next().unwrap(); + + let re = Regex::new(r"\[([Uu][Ii][Dd])\]").unwrap(); + if injected_flag.type_.to_ascii_lowercase() == "dynamic" { + injected_flag.value = re + .replace_all( + &injected_flag.value, + uuid::Uuid::new_v4().simple().to_string(), + ) + .to_string(); + } + + let nats = crate::container::get_container() + .await + .create(ctn_name.clone(), challenge.clone(), injected_flag.clone()) + .await?; + + let mut pod = crate::repository::pod::create(crate::model::pod::ActiveModel { + name: Set(ctn_name), + user_id: Set(req.user_id.clone().unwrap()), + team_id: Set(req.team_id.clone()), + game_id: Set(req.game_id.clone()), + challenge_id: Set(req.challenge_id.clone()), + flag: Set(Some(injected_flag.value)), + removed_at: Set(chrono::Utc::now().timestamp() + challenge.duration), + nats: Set(nats), + ..Default::default() + }) + .await?; + + pod.flag = None; + + return Ok(pod); +} + +pub async fn update(id: i64) -> Result<(), Box> { + let (pods, total) = + crate::repository::pod::find(Some(id), None, None, None, None, None, None).await?; + if total == 0 { + return Err("No pod found".into()); + } + let pod = pods.get(0).unwrap(); + let (challenges, _) = crate::repository::challenge::find( + Some(pod.challenge_id.clone()), + None, + None, + None, + None, + None, + None, + ) + .await?; + let challenge = challenges.get(0).unwrap(); + + let mut pod = pod.clone().into_active_model(); + pod.removed_at = Set(chrono::Utc::now().timestamp() + challenge.duration); + let _ = crate::repository::pod::update(pod).await; + return Ok(()); +} + +pub async fn delete(id: i64) -> Result<(), Box> { + let (pods, total) = + crate::repository::pod::find(Some(id), None, None, None, None, None, None).await?; + if total == 0 { + return Err("No pod found".into()); + } + let pod = pods.get(0).unwrap(); + crate::container::get_container() + .await + .delete(pod.name.clone()) + .await; + + let mut pod = pod.clone().into_active_model(); + pod.removed_at = Set(chrono::Utc::now().timestamp()); + + let _ = crate::repository::pod::update(pod).await; + return Ok(()); +} diff --git a/src/server/service/submission.rs b/src/server/service/submission.rs new file mode 100644 index 00000000..982b2d20 --- /dev/null +++ b/src/server/service/submission.rs @@ -0,0 +1,131 @@ +use std::error::Error; + +use sea_orm::{IntoActiveModel, Set}; + +pub async fn find( + req: crate::model::submission::request::FindRequest, +) -> Result<(Vec, u64), Box> { + let (mut submissions, total) = crate::repository::submission::find( + req.id, + req.user_id, + req.team_id, + req.game_id, + req.challenge_id, + req.status, + req.page, + req.size, + ) + .await + .unwrap(); + + let is_detailed = req.is_detailed.unwrap_or(false); + if !is_detailed { + for submission in submissions.iter_mut() { + submission.flag.clear(); + } + } + + return Ok((submissions, total)); +} + +pub async fn create( + req: crate::model::submission::request::CreateRequest, +) -> Result> { + // Get related challenge + let (challenges, _) = + crate::repository::challenge::find(req.challenge_id, None, None, None, None, None, None) + .await + .unwrap(); + + let challenge = challenges.first().unwrap(); + + // Default submission record + let mut submission = crate::repository::submission::create(req.clone().into()) + .await + .unwrap() + .into_active_model(); + + let (exist_submissions, total) = crate::repository::submission::find( + None, + None, + None, + req.game_id, + req.challenge_id, + Some(2), + None, + None, + ) + .await + .unwrap(); + + let mut status: i64 = 1; // Wrong answer + + match challenge.is_dynamic { + true => { + // Dynamic challenge, verify flag correctness from pods + let (pods, _) = crate::repository::pod::find( + None, + None, + None, + None, + req.game_id, + Some(challenge.id), + Some(true), + ) + .await + .unwrap(); + + for pod in pods { + if pod.flag == Some(req.clone().flag) { + if Some(pod.user_id) == req.user_id || req.team_id == pod.team_id { + status = 2; // Accept + break; + } else { + status = 3; // Cheat + break; + } + } + } + } + false => { + // Static challenge + for flag in challenge.flags.clone() { + if flag.value == req.flag { + if flag.banned { + status = 3; // Cheat + break; + } else { + status = 2; // Accept + } + } + } + } + } + + for exist_submission in exist_submissions { + if Some(exist_submission.user_id) == req.user_id + || (req.game_id.is_some() && exist_submission.team_id == req.team_id) + { + status = 4; // Invalid + break; + } + } + + submission.status = Set(status); + if status == 1 { + submission.rank = Set((total + 1).try_into().unwrap()); + } + + let submission = crate::repository::submission::update(submission) + .await + .unwrap(); + + return Ok(submission); +} + +pub async fn delete(id: i64) -> Result<(), Box> { + match crate::repository::submission::delete(id).await { + Ok(_) => return Ok(()), + Err(err) => return Err(Box::new(err)), + } +} diff --git a/src/server/service/team.rs b/src/server/service/team.rs new file mode 100644 index 00000000..193589af --- /dev/null +++ b/src/server/service/team.rs @@ -0,0 +1,79 @@ +use std::error::Error; + +use sea_orm::{IntoActiveModel, Set}; + +pub async fn find( + req: crate::model::team::request::FindRequest, +) -> Result<(Vec, u64), ()> { + let (teams, total) = + crate::repository::team::find(req.id, req.name, req.email, req.page, req.size) + .await + .unwrap(); + return Ok((teams, total)); +} + +pub async fn create(req: crate::model::team::request::CreateRequest) -> Result<(), Box> { + match crate::repository::team::create(req.clone().into()).await { + Ok(team) => { + match crate::repository::user_team::create(crate::model::user_team::ActiveModel { + team_id: Set(team.id), + user_id: Set(req.captain_id), + }) + .await + { + Ok(_) => { + return Ok(()); + } + Err(err) => return Err(Box::new(err)), + } + } + Err(err) => return Err(Box::new(err)), + } +} + +pub async fn update(req: crate::model::team::request::UpdateRequest) -> Result<(), Box> { + match crate::repository::team::update(req.into()).await { + Ok(_) => return Ok(()), + Err(err) => return Err(Box::new(err)), + } +} + +pub async fn delete(id: i64) -> Result<(), Box> { + match crate::repository::team::delete(id).await { + Ok(()) => return Ok(()), + Err(err) => return Err(Box::new(err)), + } +} + +pub async fn get_invite_token(id: i64) -> Result> { + let (teams, total) = crate::repository::team::find(Some(id), None, None, None, None) + .await + .unwrap(); + + if total == 0 { + return Err("team_not_found".into()); + } + + let team = teams.get(0).unwrap(); + + return Ok(team.invite_token.clone().unwrap_or("".to_string())); +} + +pub async fn update_invite_token(id: i64) -> Result> { + let (teams, total) = crate::repository::team::find(Some(id), None, None, None, None) + .await + .unwrap(); + + if total == 0 { + return Err("team_not_found".into()); + } + + let mut team = teams.get(0).unwrap().clone().into_active_model(); + let token = uuid::Uuid::new_v4().simple().to_string(); + team.invite_token = Set(Some(token.clone())); + + match crate::repository::team::update(team).await { + Ok(_) => return Ok(token), + Err(err) => return Err(Box::new(err)), + } +} diff --git a/src/server/service/user.rs b/src/server/service/user.rs new file mode 100644 index 00000000..c03f1033 --- /dev/null +++ b/src/server/service/user.rs @@ -0,0 +1,113 @@ +use std::error::Error; + +use crate::util::jwt; +use axum::http::StatusCode; +use bcrypt::{hash, verify, DEFAULT_COST}; +use sea_orm::Set; + +pub async fn find( + req: crate::model::user::request::FindRequest, +) -> Result<(Vec, u64), ()> { + let (mut users, total) = crate::repository::user::find( + req.id, req.name, None, req.group, req.email, req.page, req.size, + ) + .await + .unwrap(); + for user in users.iter_mut() { + user.simplify(); + } + return Ok((users, total)); +} + +pub async fn create( + mut req: crate::model::user::request::CreateRequest, +) -> Result> { + let hashed_password = hash(req.password, DEFAULT_COST); + req.password = hashed_password.unwrap(); + match crate::repository::user::create(req.into()).await { + Ok(mut user) => { + user.simplify(); + return Ok(user); + } + Err(err) => return Err(Box::new(err)), + } +} + +pub async fn update( + mut req: crate::model::user::request::UpdateRequest, +) -> Result<(), Box> { + if let Some(password) = req.password { + let hashed_password = hash(password, DEFAULT_COST); + req.password = Some(hashed_password.unwrap()); + } + match crate::repository::user::update(req.into()).await { + Ok(_) => return Ok(()), + Err(err) => return Err(Box::new(err)), + } +} + +pub async fn delete(id: i64) -> Result<(), Box> { + match crate::repository::user::delete(id).await { + Ok(()) => return Ok(()), + Err(err) => return Err(Box::new(err)), + } +} + +pub async fn login( + req: crate::model::user::request::LoginRequest, +) -> Result<(crate::model::user::Model, String), Box> { + let (users, total) = + crate::repository::user::find(None, None, Some(req.username), None, None, None, None) + .await + .unwrap(); + + if total == 0 { + return Err("user_not_found".into()); + } + + let mut user = users.get(0).unwrap().clone(); + let hashed_password = user.password.clone().unwrap(); + let is_match = verify(&req.password, &hashed_password).unwrap(); + + if !is_match { + return Err("password_incorrect".into()); + } + + let token = jwt::generate_jwt_token(user.id.clone()).await; + user.simplify(); + + return Ok((user, token)); +} + +pub async fn register( + req: crate::model::user::request::RegisterRequest, +) -> Result { + match crate::repository::user::find( + None, + None, + Some(req.clone().username), + None, + None, + None, + None, + ) + .await + { + Ok((_, total)) => { + if total != 0 { + return Err(StatusCode::CONFLICT); + } + } + Err(_err) => return Err(StatusCode::INTERNAL_SERVER_ERROR), + } + + let hashed_password = hash(req.password.clone(), DEFAULT_COST).unwrap(); + let mut user: crate::model::user::ActiveModel = req.into(); + user.password = Set(Some(hashed_password)); + user.group = Set("user".to_string()); + + match crate::repository::user::create(user).await { + Ok(user) => return Ok(user), + Err(_err) => return Err(StatusCode::INTERNAL_SERVER_ERROR), + } +} diff --git a/src/server/service/user_team.rs b/src/server/service/user_team.rs new file mode 100644 index 00000000..371106a2 --- /dev/null +++ b/src/server/service/user_team.rs @@ -0,0 +1,54 @@ +use std::error::Error; + +pub async fn join( + req: crate::model::user_team::request::JoinRequest, +) -> Result<(), Box> { + let (_, user_total) = + crate::repository::user::find(Some(req.user_id), None, None, None, None, None, None) + .await + .unwrap(); + let (teams, team_total) = + crate::repository::team::find(Some(req.team_id), None, None, None, None) + .await + .unwrap(); + + if user_total == 0 || team_total == 0 { + return Err("invalid_user_or_team".into()); + } + + let team = teams.get(0).unwrap().clone(); + + if Some(req.invite_token.clone()) != team.invite_token { + return Err("invalid_invite_token".into()); + } + + crate::repository::user_team::create(req.into()) + .await + .unwrap(); + + return Ok(()); +} + +pub async fn create( + req: crate::model::user_team::request::CreateRequest, +) -> Result<(), Box> { + match crate::repository::user_team::create(req.into()).await { + Ok(_) => { + return Ok(()); + } + Err(e) => { + return Err(e.into()); + } + } +} + +pub async fn delete(user_id: i64, team_id: i64) -> Result<(), Box> { + match crate::repository::user_team::delete(user_id, team_id).await { + Ok(_) => { + return Ok(()); + } + Err(e) => { + return Err(e.into()); + } + } +} diff --git a/src/traits.rs b/src/traits.rs new file mode 100644 index 00000000..44d5e910 --- /dev/null +++ b/src/traits.rs @@ -0,0 +1,9 @@ +use crate::model::user; + +#[derive(Clone, Debug)] +pub struct Ext { + pub operator: Option, +} + +#[derive(Debug, thiserror::Error)] +pub enum Error {} diff --git a/src/util/jwt.rs b/src/util/jwt.rs new file mode 100644 index 00000000..fcd146dd --- /dev/null +++ b/src/util/jwt.rs @@ -0,0 +1,64 @@ +use jsonwebtoken::{encode, EncodingKey, Header}; +use once_cell::sync::Lazy; +use regex::Regex; +use serde::{Deserialize, Serialize}; +use tokio::sync::Mutex; + +use crate::config; + +static SECRET: Lazy> = Lazy::new(|| { + let mut secret_key = config::get_app_config().auth.jwt.secret_key.clone(); + let re = Regex::new(r"\[([Uu][Ii][Dd])\]").unwrap(); + secret_key = re + .replace_all(&secret_key, uuid::Uuid::new_v4().simple().to_string()) + .to_string(); + return Mutex::new(secret_key); +}); + +#[derive(Debug, Deserialize, Serialize)] +pub struct Claims { + pub id: i64, + pub exp: usize, +} + +#[derive(Debug, Clone)] +#[repr(u8)] +pub enum Group { + Admin = 3, + User = 2, + Guest = 1, + Banned = 0, +} + +impl Group { + pub fn from_str(s: String) -> Result { + match s.as_str() { + "admin" => Ok(Group::Admin), + "user" => Ok(Group::User), + "guest" => Ok(Group::Guest), + "banned" => Ok(Group::Banned), + _ => Err("Invalid group"), + } + } +} + +pub async fn get_secret() -> String { + return SECRET.lock().await.clone(); +} + +pub async fn generate_jwt_token(user_id: i64) -> String { + let secret = get_secret().await; + let claims = Claims { + id: user_id, + exp: (chrono::Utc::now() + chrono::Duration::seconds(3600)).timestamp() as usize, + }; + + let token = encode( + &Header::default(), + &claims, + &EncodingKey::from_secret(secret.as_bytes()), + ) + .unwrap(); + + return token; +} diff --git a/src/util/math.rs b/src/util/math.rs new file mode 100644 index 00000000..f9964cef --- /dev/null +++ b/src/util/math.rs @@ -0,0 +1,14 @@ +use std::f64::consts::E; + +/// curve is a function that calculates the value of a curve given the parameters s, r, d, and x. +/// +/// - "s" is the maximum value. +/// - "r" is the maximum value. +/// - "d" is the degree of difficulty of the challenge. +/// - "x" is the quantity of correct submissions. +pub fn curve(s: i64, r: i64, d: i64, x: i64) -> i64 { + let ratio = r as f64 / s as f64; + let result = + (s as f64 * (ratio + (1.0 - ratio) * E.powf((1.0 - x as f64) / d as f64))).floor() as i64; + return result.min(s); +} diff --git a/src/util/mod.rs b/src/util/mod.rs new file mode 100644 index 00000000..69e1595b --- /dev/null +++ b/src/util/mod.rs @@ -0,0 +1,3 @@ +pub mod jwt; +pub mod math; +pub mod validate; diff --git a/src/util/validate.rs b/src/util/validate.rs new file mode 100644 index 00000000..5d65decc --- /dev/null +++ b/src/util/validate.rs @@ -0,0 +1,62 @@ +use axum::{ + async_trait, + extract::{rejection::JsonRejection, FromRequest, MatchedPath, Request}, + http::StatusCode, + RequestPartsExt, +}; +use serde_json::{json, Value}; +use validator::Validate; + +pub struct Json(pub T); + +#[async_trait] +impl FromRequest for Json +where + axum::Json: FromRequest, + T: Validate + Send + Sync, + S: Send + Sync, +{ + type Rejection = (StatusCode, axum::Json); + + async fn from_request(req: Request, state: &S) -> Result { + let (mut parts, body) = req.into_parts(); + + // We can use other extractors to provide better rejection messages. + // For example, here we are using `axum::extract::MatchedPath` to + // provide a better error message. + // + // Have to run that first since `Json` extraction consumes the request. + let path = parts + .extract::() + .await + .map(|path| path.as_str().to_owned()) + .ok(); + + let req = Request::from_parts(parts, body); + + match axum::Json::::from_request(req, state).await { + Ok(value) => { + if let Err(validation_errors) = value.0.validate() { + let payload = json!({ + "msg": "Validation failed", + "errors": validation_errors, + "origin": "custom_extractor", + "path": path, + }); + + return Err((StatusCode::UNPROCESSABLE_ENTITY, axum::Json(payload))); + } + Ok(Self(value.0)) + } + Err(rejection) => { + let payload = json!({ + "message": rejection.body_text(), + "origin": "custom_extractor", + "path": path, + }); + + Err((rejection.status(), axum::Json(payload))) + } + } + } +} diff --git a/web/src/App.tsx b/web/src/App.tsx index f7b1488f..586274ea 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -1,22 +1,14 @@ import { useRoutes } from "react-router"; -import Navbar, { - NavItems, - AdminNavItems, -} from "@/components/navigations/Navbar"; +import Navbar, { NavItems } from "@/components/navigations/Navbar"; import routes from "~react-pages"; -import { - AppShell, - Button, - LoadingOverlay, - UnstyledButton, -} from "@mantine/core"; +import { AppShell, Button, LoadingOverlay } from "@mantine/core"; import { Suspense, useEffect, useState } from "react"; import { useCategoryApi } from "@/api/category"; import { useCategoryStore } from "@/stores/category"; import { useConfigApi } from "@/api/config"; import { useConfigStore } from "@/stores/config"; import "dayjs/locale/zh-cn"; -import { useDisclosure } from "@mantine/hooks"; +import { useDisclosure, useFavicon } from "@mantine/hooks"; import { Link, useLocation } from "react-router-dom"; import MDIcon from "./components/ui/MDIcon"; @@ -26,6 +18,9 @@ function App() { const configApi = useConfigApi(); const configStore = useConfigStore(); + const [favicon, setFavicon] = useState("./favicon.ico"); + useFavicon(favicon); + const [opened, { toggle }] = useDisclosure(); const [adminMode, setAdminMode] = useState(false); const location = useLocation(); @@ -43,16 +38,6 @@ function App() { }); }, [configStore.refresh]); - // Get captcha config - useEffect(() => { - if (configStore?.pltCfg?.user?.register?.captcha?.enabled) { - configApi.getCaptchaCfg().then((res) => { - const r = res.data; - configStore.setCaptchaCfg(r.data); - }); - } - }, [configStore?.pltCfg]); - // Get exists categories useEffect(() => { categoryApi.getCategories().then((res) => { @@ -61,6 +46,12 @@ function App() { }); }, [categoryStore.refresh]); + useEffect(() => { + if (configStore.pltCfg?.site?.favicon) { + setFavicon(`${import.meta.env.VITE_BASE_API}/configs/favicon`); + } + }, [configStore.pltCfg]); + useEffect(() => { setAdminMode(false); if (location.pathname.startsWith("/admin")) { diff --git a/web/src/api/category.ts b/web/src/api/category.ts index 5932add7..532a9068 100644 --- a/web/src/api/category.ts +++ b/web/src/api/category.ts @@ -9,11 +9,11 @@ export function useCategoryApi() { const auth = useAuth(); const getCategories = () => { - return auth.get("/categories/"); + return auth.get("/categories"); }; const createCategory = (request: CategoryCreateRequest) => { - return auth.post("/categories/", request); + return auth.post("/categories", request); }; const updateCategory = (request: CategoryUpdateRequest) => { diff --git a/web/src/api/challenge.ts b/web/src/api/challenge.ts index 320f7f2c..40de496e 100644 --- a/web/src/api/challenge.ts +++ b/web/src/api/challenge.ts @@ -2,13 +2,9 @@ import { ChallengeCreateRequest, ChallengeDeleteRequest, ChallengeFindRequest, + ChallengeStatusRequest, ChallengeUpdateRequest, } from "@/types/challenge"; -import { - FlagCreateRequest, - FlagDeleteRequest, - FlagUpdateRequest, -} from "@/types/flag"; import { useAuth } from "@/utils/axios"; import { AxiosRequestConfig } from "axios"; @@ -16,11 +12,11 @@ export function useChallengeApi() { const auth = useAuth(); const getChallenges = (request: ChallengeFindRequest) => { - return auth.get("/challenges/", { params: request }); + return auth.get("/challenges", { params: request }); }; const createChallenge = (request: ChallengeCreateRequest) => { - return auth.post("/challenges/", request); + return auth.post("/challenges", request); }; const updateChallenge = (request: ChallengeUpdateRequest) => { @@ -31,21 +27,12 @@ export function useChallengeApi() { return auth.delete(`/challenges/${request.id}`); }; - const updateChallengeFlag = (request: FlagUpdateRequest) => { - return auth.put( - `/challenges/${request.challenge_id}/flags/${request.id}`, - request - ); - }; - - const createChallengeFlag = (request: FlagCreateRequest) => { - return auth.post(`/challenges/${request.challenge_id}/flags`, request); + const getChallengeStatus = (request: ChallengeStatusRequest) => { + return auth.post(`/challenges/status`, request); }; - const deleteChallengeFlag = (request: FlagDeleteRequest) => { - return auth.delete( - `/challenges/${request.challenge_id}/flags/${request.id}` - ); + const getChallengeAttachmentMetadata = (id: number) => { + return auth.get(`/challenges/${id}/attachment/metadata`); }; const saveChallengeAttachment = ( @@ -67,9 +54,8 @@ export function useChallengeApi() { createChallenge, updateChallenge, deleteChallenge, - updateChallengeFlag, - createChallengeFlag, - deleteChallengeFlag, + getChallengeStatus, + getChallengeAttachmentMetadata, saveChallengeAttachment, deleteChallengeAttachment, }; diff --git a/web/src/api/config.ts b/web/src/api/config.ts index 4fb4649e..89752dc4 100644 --- a/web/src/api/config.ts +++ b/web/src/api/config.ts @@ -1,24 +1,13 @@ -import { ConfigUpdateRequest } from "@/types/config"; import { useAuth } from "@/utils/axios"; export function useConfigApi() { const auth = useAuth(); const getPltCfg = () => { - return auth.get("/configs/"); - }; - - const updatePltCfg = (request: ConfigUpdateRequest) => { - return auth.put("/configs/", request); - }; - - const getCaptchaCfg = () => { - return auth.get("/configs/captcha"); + return auth.get("/configs"); }; return { getPltCfg, - updatePltCfg, - getCaptchaCfg, }; } diff --git a/web/src/api/game.ts b/web/src/api/game.ts index 42a61678..02b21cb0 100644 --- a/web/src/api/game.ts +++ b/web/src/api/game.ts @@ -24,11 +24,11 @@ export function useGameApi() { const auth = useAuth(); const getGames = (request: GameFindRequest) => { - return auth.get("/games/", { params: request }); + return auth.get("/games", { params: request }); }; const createGame = (request: GameCreateRequest) => { - return auth.post("/games/", request); + return auth.post("/games", request); }; const updateGame = (request: GameUpdateRequest) => { @@ -106,6 +106,10 @@ export function useGameApi() { return auth.delete(`/games/${request?.game_id}/notices/${request?.id}`); }; + const getGamePosterMetadata = (id: number) => { + return auth.get(`/games/${id}/poster/metadata`); + }; + const saveGamePoster = ( id: number, file: File, @@ -122,10 +126,10 @@ export function useGameApi() { return { getGames, - getGameChallenges, createGame, updateGame, deleteGame, + getGameChallenges, updateGameChallenge, createGameChallenge, deleteGameChallenge, @@ -137,6 +141,7 @@ export function useGameApi() { createGameNotice, updateGameNotice, deleteGameNotice, + getGamePosterMetadata, saveGamePoster, deleteGamePoster, }; diff --git a/web/src/api/pod.ts b/web/src/api/pod.ts index 17625ed2..c8438de5 100644 --- a/web/src/api/pod.ts +++ b/web/src/api/pod.ts @@ -9,11 +9,11 @@ export function usePodApi() { const auth = useAuth(); const getPods = (request: PodFindRequest) => { - return auth.get("/pods/", { params: { ...request } }); + return auth.get("/pods", { params: { ...request } }); }; const createPod = (request: PodCreateRequest) => { - return auth.post("/pods/", { ...request }); + return auth.post("/pods", { ...request }); }; const removePod = (request: PodRemoveRequest) => { diff --git a/web/src/api/submission.ts b/web/src/api/submission.ts index 3cb21cee..f8942437 100644 --- a/web/src/api/submission.ts +++ b/web/src/api/submission.ts @@ -9,11 +9,11 @@ export function useSubmissionApi() { const auth = useAuth(); const createSubmission = (request: SubmissionCreateRequest) => { - return auth.post("/submissions/", { ...request }); + return auth.post("/submissions", { ...request }); }; const getSubmissions = (request: SubmissionFindRequest) => { - return auth.get("/submissions/", { params: request }); + return auth.get("/submissions", { params: request }); }; const deleteSubmission = (request: SubmissionDeleteRequest) => { diff --git a/web/src/api/team.ts b/web/src/api/team.ts index cebfab92..62da0f24 100644 --- a/web/src/api/team.ts +++ b/web/src/api/team.ts @@ -16,11 +16,11 @@ export function useTeamApi() { const auth = useAuth(); const getTeams = (request?: TeamFindRequest) => { - return auth.get("/teams/", { params: request }); + return auth.get("/teams", { params: request }); }; const createTeam = (request?: TeamCreateRequest) => { - return auth.post("/teams/", request); + return auth.post("/teams", request); }; const updateTeam = (request: TeamUpdateRequest) => { @@ -51,6 +51,10 @@ export function useTeamApi() { return auth.delete(`/teams/${request?.id}/leave`); }; + const getTeamAvatarMetadata = (id: number) => { + return auth.get(`/teams/${id}/avatar/metadata`); + }; + const saveTeamAvatar = ( id: number, file: File, @@ -75,6 +79,7 @@ export function useTeamApi() { leaveTeam, getTeamInviteToken, updateTeamInviteToken, + getTeamAvatarMetadata, saveTeamAvatar, deleteTeamAvatar, }; diff --git a/web/src/api/user.ts b/web/src/api/user.ts index 03c29ee5..115633e8 100644 --- a/web/src/api/user.ts +++ b/web/src/api/user.ts @@ -22,11 +22,7 @@ export function useUserApi() { }; const getUsers = (request: UserFindRequest) => { - return auth.get("/users/", { params: request }); - }; - - const getUserByID = (id: number) => { - return auth.get(`/users/${id}`); + return auth.get("/users", { params: request }); }; const updateUser = (request: UserUpdateRequest) => { @@ -34,13 +30,17 @@ export function useUserApi() { }; const createUser = (request: UserCreateRequest) => { - return auth.post(`/users/`, request); + return auth.post(`/users`, request); }; const deleteUser = (request: UserDeleteRequest) => { return auth.delete(`/users/${request?.id}`); }; + const getUserAvatarMetadata = (id: number) => { + return auth.get(`/users/${id}/avatar/metadata`); + }; + const saveUserAvatar = ( id: number, file: File, @@ -59,10 +59,10 @@ export function useUserApi() { login, register, getUsers, - getUserByID, updateUser, createUser, deleteUser, + getUserAvatarMetadata, saveUserAvatar, deleteUserAvatar, }; diff --git a/web/src/components/modals/ChallengeModal.tsx b/web/src/components/modals/ChallengeModal.tsx index aa052cc6..be97e37f 100644 --- a/web/src/components/modals/ChallengeModal.tsx +++ b/web/src/components/modals/ChallengeModal.tsx @@ -34,6 +34,9 @@ import { import { useForm } from "@mantine/form"; import { useTeamStore } from "@/stores/team"; import { useClipboard, useInterval } from "@mantine/hooks"; +import { Metadata } from "@/types/media"; +import { useChallengeApi } from "@/api/challenge"; +import { useCategoryStore } from "@/stores/category"; interface ChallengeModalProps extends ModalProps { challenge?: Challenge; @@ -47,9 +50,13 @@ export default function ChallengeModal(props: ChallengeModalProps) { const clipboard = useClipboard({ timeout: 500 }); const podApi = usePodApi(); + const challengeApi = useChallengeApi(); const submissionApi = useSubmissionApi(); const authStore = useAuthStore(); const teamStore = useTeamStore(); + const categoryStore = useCategoryStore(); + + const [attachmentMetadata, setAttachmentMetadata] = useState(); const [pod, setPod] = useState(); const [podTime, setPodTime] = useState(0); @@ -58,6 +65,8 @@ export default function ChallengeModal(props: ChallengeModalProps) { const [podRemoveLoading, setPodRemoveLoading] = useState(false); const [podRenewLoading, setPodRenewLoading] = useState(false); + const category = categoryStore.getCategory(Number(challenge?.category_id)); + const form = useForm({ mode: "uncontrolled", initialValues: { @@ -65,6 +74,15 @@ export default function ChallengeModal(props: ChallengeModalProps) { }, }); + function getAttachmentMetadata() { + challengeApi + .getChallengeAttachmentMetadata(Number(challenge?.id)) + .then((res) => { + const r = res.data; + setAttachmentMetadata(r?.data); + }); + } + function getPod() { podApi .getPods({ @@ -157,7 +175,7 @@ export default function ChallengeModal(props: ChallengeModalProps) { }) .then((res) => { const r = res.data; - switch (r?.status) { + switch (r?.data?.status) { case 1: showWarnNotification({ title: "错误", @@ -216,12 +234,16 @@ export default function ChallengeModal(props: ChallengeModalProps) { if (challenge?.is_dynamic) { getPod(); } + if (challenge?.has_attachment) { + getAttachmentMetadata(); + } }, [challenge, modalProps.opened]); useEffect(() => { form.reset(); setPodTime(0); setPod(undefined); + setAttachmentMetadata(undefined); }, [modalProps.opened]); return ( @@ -250,8 +272,8 @@ export default function ChallengeModal(props: ChallengeModalProps) { - - {challenge?.category?.icon} + + {category?.icon} {challenge?.title} @@ -307,20 +329,20 @@ export default function ChallengeModal(props: ChallengeModalProps) { - {challenge?.attachment?.name && ( + {attachmentMetadata?.filename && ( { window.open( - `${import.meta.env.VITE_BASE_API}/media/challenges/${challenge?.id}/${challenge?.attachment?.name}` + `${import.meta.env.VITE_BASE_API}/challenges/${challenge?.id}/attachment` ); }} > - + download @@ -334,15 +356,13 @@ export default function ChallengeModal(props: ChallengeModalProps) { {pod?.nats?.map((nat) => ( - - {nat.src_port} - + {nat.src} arrow_right_alt @@ -457,9 +475,7 @@ export default function ChallengeModal(props: ChallengeModalProps) { {!pod?.id && (