From 7618d8526481dc633cf284cc47dc28c8ff6db6b4 Mon Sep 17 00:00:00 2001 From: NeonXP Date: Sun, 28 Jan 2024 19:19:41 +0000 Subject: [PATCH] devcontainer :) --- .env => .devcontainer/.env | 0 .devcontainer/Dockerfile | 1 + .devcontainer/devcontainer.json | 26 ++++ .devcontainer/docker-compose.yml | 24 ++++ .github/dependabot.yml | 12 ++ .vscode/launch.json | 2 +- api/openapi.yaml | 190 ++++++++++------------------ api/server.go | 123 ++++++++---------- api/types.go | 37 +----- auth.go | 41 ++++++ frontend/src/App.jsx | 3 +- frontend/src/assets/styles.css | 12 +- frontend/src/pages/admin/Quest.jsx | 11 +- frontend/src/pages/admin/Quests.jsx | 1 + main.go | 54 +------- pkg/controller/admin.go | 104 +++------------ pkg/controller/file.go | 2 +- pkg/models/file.go | 15 ++- pkg/models/game.go | 15 ++- pkg/models/model.go | 15 --- pkg/models/task.go | 19 +-- pkg/models/user.go | 15 ++- pkg/service/engine.go | 1 - pkg/service/file.go | 2 +- pkg/service/game.go | 16 +-- pkg/service/user.go | 4 +- requests.http | 2 +- 27 files changed, 313 insertions(+), 434 deletions(-) rename .env => .devcontainer/.env (100%) create mode 100644 .devcontainer/Dockerfile create mode 100644 .devcontainer/devcontainer.json create mode 100644 .devcontainer/docker-compose.yml create mode 100644 .github/dependabot.yml create mode 100644 auth.go delete mode 100644 pkg/models/model.go diff --git a/.env b/.devcontainer/.env similarity index 100% rename from .env rename to .devcontainer/.env diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000..a02e4da --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1 @@ +FROM gitrepo.ru/neonxp/devcontainer:latest diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..e1e7f8d --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,26 @@ +{ + "name": "Go", + "dockerComposeFile": "./docker-compose.yml", + "service": "app", + "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", + "forwardPorts": [5432, 5173, 8000], + "remoteUser": "vscode", + "customizations": { + "vscode": { + "extensions": [ + "ms-vscode.makefile-tools", + "redhat.vscode-yaml", + "humao.rest-client", + "mtxr.sqltools", + "mtxr.sqltools-driver-pg", + "codezombiech.gitignore", + "ms-azuretools.vscode-docker" + ] + } + }, + "postCreateCommand": "go mod download", + "features": { + "https://gitrepo.ru/api/packages/NeonXP/generic/features/latest/devcontainer-feature-common-utils.tgz": {}, + "https://gitrepo.ru/api/packages/NeonXP/generic/features/latest/devcontainer-feature-go.tgz": {} + } +} diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml new file mode 100644 index 0000000..4a959ae --- /dev/null +++ b/.devcontainer/docker-compose.yml @@ -0,0 +1,24 @@ +version: '3.8' + +volumes: + postgres-data: + +services: + app: + build: + context: . + dockerfile: Dockerfile + env_file: + - .env + volumes: + - ../..:/workspaces:cached + command: sleep infinity + network_mode: service:db + + db: + image: postgres:15-alpine3.17 + restart: unless-stopped + volumes: + - postgres-data:/var/lib/postgresql/data + env_file: + - .env diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..f33a02c --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,12 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for more information: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates +# https://containers.dev/guide/dependabot + +version: 2 +updates: + - package-ecosystem: "devcontainers" + directory: "/" + schedule: + interval: weekly diff --git a/.vscode/launch.json b/.vscode/launch.json index eac9d30..aaded20 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -10,7 +10,7 @@ "request": "launch", "mode": "auto", "program": "${workspaceFolder}/", - "envFile": "${workspaceFolder}/.env" + "envFile": "${workspaceFolder}/.devcontainer/.env" } ] } \ No newline at end of file diff --git a/api/openapi.yaml b/api/openapi.yaml index b912361..acdf281 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -20,7 +20,16 @@ paths: post: security: [] requestBody: - $ref: "#/components/requestBodies/login" + content: + application/json: + schema: + type: object + properties: + email: + type: string + password: + type: string + required: [email, password] responses: 200: $ref: "#/components/responses/userResponse" @@ -30,7 +39,20 @@ paths: post: security: [] requestBody: - $ref: "#/components/requestBodies/register" + content: + application/json: + schema: + type: object + properties: + username: + type: string + email: + type: string + password: + type: string + password2: + type: string + required: [username, email, password, password2] responses: 200: $ref: "#/components/responses/userResponse" @@ -50,7 +72,6 @@ paths: responses: 200: $ref: "#/components/responses/gameListResponse" - /engine/{uid}: get: operationId: gameEngine @@ -75,27 +96,18 @@ paths: type: string format: uuid requestBody: - $ref: "#/components/requestBodies/enterCodeRequest" + content: + application/json: + schema: + type: object + properties: + code: + type: string + required: + - code responses: 200: $ref: "#/components/responses/taskResponse" - /file/upload: - post: - operationId: uploadFile - security: - - cookieAuth: [creator, admin] - requestBody: - content: - multipart/form-data: - schema: - type: object - properties: - file: - type: string - format: binary - responses: - 200: - $ref: "#/components/responses/uploadResponse" /file/{uid}: get: operationId: getFile @@ -114,24 +126,46 @@ paths: schema: type: string format: binary + + # Admin routes + /admin/file/upload: + post: + operationId: adminUploadFile + security: + - cookieAuth: [creator, admin] + requestBody: + content: + multipart/form-data: + schema: + type: object + properties: + file: + type: string + format: binary + responses: + 200: + $ref: "#/components/responses/uploadResponse" /admin/games: get: - operationId: listGamesByAdmin + operationId: adminListGames responses: 200: $ref: "#/components/responses/gameListResponse" post: - operationId: createGame + operationId: adminEditGame security: - cookieAuth: [creator, admin] requestBody: - $ref: "#/components/requestBodies/gameEditRequest" + content: + application/json: + schema: + $ref: "#/components/schemas/gameEdit" responses: 200: $ref: "#/components/responses/gameAdminResponse" /admin/games/{uid}: get: - operationId: getGameByAdmin + operationId: adminGetGame parameters: - name: uid in: path @@ -144,22 +178,7 @@ paths: responses: 200: $ref: "#/components/responses/gameAdminResponse" - post: - operationId: editGame - parameters: - - name: uid - in: path - required: true - schema: - type: string - format: uuid - security: - - cookieAuth: [creator, admin] - requestBody: - $ref: "#/components/requestBodies/gameEditRequest" - responses: - 200: - $ref: "#/components/responses/gameAdminResponse" + components: schemas: userView: @@ -254,15 +273,10 @@ components: type: array items: $ref: "#/components/schemas/codeView" - # solutions: - # type: array - # items: - # $ref: '#/components/schemas/solutionView' required: - title - text - codes - # - solutions codeView: type: object properties: @@ -272,15 +286,6 @@ components: type: string required: - description - solutionView: - type: object - properties: - text: - type: string - after: - type: integer - required: - - after gameEdit: type: object properties: @@ -326,15 +331,10 @@ components: type: array items: $ref: "#/components/schemas/codeEdit" - # solutions: - # type: array - # items: - # $ref: '#/components/schemas/solutionEdit' required: - title - text - codes - # - solutions codeEdit: type: object properties: @@ -348,78 +348,22 @@ components: required: - description - code - # solutionEdit: - # type: object - # properties: - # text: - # type: string - # after: - # type: integer - # required: - # - after - # - text gameType: type: string enum: - virtual - city - requestBodies: - login: - required: true - content: - "application/json": - schema: - type: object - properties: - email: - type: string - password: - type: string - required: [email, password] - register: - required: true - content: - "application/json": - schema: - type: object - properties: - username: - type: string - email: - type: string - password: - type: string - password2: - type: string - required: [username, email, password, password2] - gameEditRequest: - required: true - content: - "application/json": - schema: - $ref: "#/components/schemas/gameEdit" - enterCodeRequest: - required: true - content: - "application/json": - schema: - type: object - properties: - code: - type: string - required: - - code responses: userResponse: description: "" content: - "application/json": + application/json: schema: $ref: "#/components/schemas/userView" errorResponse: description: "" content: - "application/json": + application/json: schema: type: object properties: @@ -431,7 +375,7 @@ components: gameListResponse: description: "" content: - "application/json": + application/json: schema: type: array items: @@ -439,25 +383,25 @@ components: gameResponse: description: "" content: - "application/json": + application/json: schema: $ref: "#/components/schemas/gameView" gameAdminResponse: description: "" content: - "application/json": + application/json: schema: $ref: "#/components/schemas/gameEdit" taskResponse: description: "" content: - "application/json": + application/json: schema: $ref: "#/components/schemas/taskView" uploadResponse: description: "" content: - "application/json": + application/json: schema: type: object properties: diff --git a/api/server.go b/api/server.go index a7b818e..64948a3 100644 --- a/api/server.go +++ b/api/server.go @@ -22,17 +22,17 @@ import ( // ServerInterface represents all server handlers. type ServerInterface interface { + // (POST /admin/file/upload) + AdminUploadFile(ctx echo.Context) error + // (GET /admin/games) - ListGamesByAdmin(ctx echo.Context) error + AdminListGames(ctx echo.Context) error // (POST /admin/games) - CreateGame(ctx echo.Context) error + AdminEditGame(ctx echo.Context) error // (GET /admin/games/{uid}) - GetGameByAdmin(ctx echo.Context, uid openapi_types.UUID) error - - // (POST /admin/games/{uid}) - EditGame(ctx echo.Context, uid openapi_types.UUID) error + AdminGetGame(ctx echo.Context, uid openapi_types.UUID) error // (GET /engine/{uid}) GameEngine(ctx echo.Context, uid openapi_types.UUID) error @@ -40,9 +40,6 @@ type ServerInterface interface { // (POST /engine/{uid}/code) EnterCode(ctx echo.Context, uid openapi_types.UUID) error - // (POST /file/upload) - UploadFile(ctx echo.Context) error - // (GET /file/{uid}) GetFile(ctx echo.Context, uid openapi_types.UUID) error @@ -67,30 +64,41 @@ type ServerInterfaceWrapper struct { Handler ServerInterface } -// ListGamesByAdmin converts echo context to params. -func (w *ServerInterfaceWrapper) ListGamesByAdmin(ctx echo.Context) error { +// AdminUploadFile converts echo context to params. +func (w *ServerInterfaceWrapper) AdminUploadFile(ctx echo.Context) error { + var err error + + ctx.Set(CookieAuthScopes, []string{"creator", "admin"}) + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.AdminUploadFile(ctx) + return err +} + +// AdminListGames converts echo context to params. +func (w *ServerInterfaceWrapper) AdminListGames(ctx echo.Context) error { var err error ctx.Set(CookieAuthScopes, []string{}) // Invoke the callback with all the unmarshaled arguments - err = w.Handler.ListGamesByAdmin(ctx) + err = w.Handler.AdminListGames(ctx) return err } -// CreateGame converts echo context to params. -func (w *ServerInterfaceWrapper) CreateGame(ctx echo.Context) error { +// AdminEditGame converts echo context to params. +func (w *ServerInterfaceWrapper) AdminEditGame(ctx echo.Context) error { var err error ctx.Set(CookieAuthScopes, []string{"creator", "admin"}) // Invoke the callback with all the unmarshaled arguments - err = w.Handler.CreateGame(ctx) + err = w.Handler.AdminEditGame(ctx) return err } -// GetGameByAdmin converts echo context to params. -func (w *ServerInterfaceWrapper) GetGameByAdmin(ctx echo.Context) error { +// AdminGetGame converts echo context to params. +func (w *ServerInterfaceWrapper) AdminGetGame(ctx echo.Context) error { var err error // ------------- Path parameter "uid" ------------- var uid openapi_types.UUID @@ -103,25 +111,7 @@ func (w *ServerInterfaceWrapper) GetGameByAdmin(ctx echo.Context) error { ctx.Set(CookieAuthScopes, []string{"creator", "admin"}) // Invoke the callback with all the unmarshaled arguments - err = w.Handler.GetGameByAdmin(ctx, uid) - return err -} - -// EditGame converts echo context to params. -func (w *ServerInterfaceWrapper) EditGame(ctx echo.Context) error { - var err error - // ------------- Path parameter "uid" ------------- - var uid openapi_types.UUID - - err = runtime.BindStyledParameterWithLocation("simple", false, "uid", runtime.ParamLocationPath, ctx.Param("uid"), &uid) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter uid: %s", err)) - } - - ctx.Set(CookieAuthScopes, []string{"creator", "admin"}) - - // Invoke the callback with all the unmarshaled arguments - err = w.Handler.EditGame(ctx, uid) + err = w.Handler.AdminGetGame(ctx, uid) return err } @@ -161,17 +151,6 @@ func (w *ServerInterfaceWrapper) EnterCode(ctx echo.Context) error { return err } -// UploadFile converts echo context to params. -func (w *ServerInterfaceWrapper) UploadFile(ctx echo.Context) error { - var err error - - ctx.Set(CookieAuthScopes, []string{"creator", "admin"}) - - // Invoke the callback with all the unmarshaled arguments - err = w.Handler.UploadFile(ctx) - return err -} - // GetFile converts echo context to params. func (w *ServerInterfaceWrapper) GetFile(ctx echo.Context) error { var err error @@ -269,13 +248,12 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL Handler: si, } - router.GET(baseURL+"/admin/games", wrapper.ListGamesByAdmin) - router.POST(baseURL+"/admin/games", wrapper.CreateGame) - router.GET(baseURL+"/admin/games/:uid", wrapper.GetGameByAdmin) - router.POST(baseURL+"/admin/games/:uid", wrapper.EditGame) + router.POST(baseURL+"/admin/file/upload", wrapper.AdminUploadFile) + router.GET(baseURL+"/admin/games", wrapper.AdminListGames) + router.POST(baseURL+"/admin/games", wrapper.AdminEditGame) + router.GET(baseURL+"/admin/games/:uid", wrapper.AdminGetGame) router.GET(baseURL+"/engine/:uid", wrapper.GameEngine) router.POST(baseURL+"/engine/:uid/code", wrapper.EnterCode) - router.POST(baseURL+"/file/upload", wrapper.UploadFile) router.GET(baseURL+"/file/:uid", wrapper.GetFile) router.GET(baseURL+"/games", wrapper.GetGames) router.GET(baseURL+"/user", wrapper.GetUser) @@ -288,27 +266,26 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "H4sIAAAAAAAC/9RZzW7kNgx+lULt0RvP/px8ywZtUDQo2jTbSzAIFJuZaGNLrkRnMwj87gUl/47lseMa", - "2fQUR6JF8uNHmuI8s1hluZIg0bDomWn4pwCDn1UiwC6ARNBnKoFLt0NrsZII0j7yPE9FzFEoGX41StKa", - "ie8h4/SUa5WDxuqoWCVAf3GfA4uYQS3kjpVlYLUKDQmLrp3UNqil1O1XiJGVfTHUBZQB2/EMfk4ELrHt", - "Jw13LGI/hi0Aods1YX3uiNpU7YT8D0BAxkXqQSJgOTfmm9LJNEzujM4bMyHTsBMGQb+2+e3mB+9uYUBL", - "ns0gSCMZDEHoapkFiF0xuZKm8k1rpS+rlfW4LiTCDjQ5moExfDc3EVp5vzsJmFiLnGxiEWNVTpwmmZCL", - "vJifFX7NF8LgIsUCITNzLPhbwDfSVoHBteb7MYuQm4fVYaBDnRF+pUWeKp6swKGiEDaT7pTOOLLILQRT", - "6UFCc8lCubQ6QHToOEBlUB3T5Inl1NyvxcGBnv0loHXPDMY+QW7DeraWtUfM8OlvMnCgfxKU2G1MwDIT", - "vYDlSlQdw7DCUYKY2TlN0q6qHOZ0wFBg6ofVLUxXiyuSKwP2KIy47Z11q1QKXA5iUEvW2oMDctjXaycb", - "ICqEx4J2VZkLssicEo0Fp09XLHDfea31sKl1g1jzAu+Vno9wm49DhGMNHCE5xWWp9sqsOlOFxJHtlahy", - "wAZn5zEiNARoDezCGjThOkKRJge8dWV+pJtq6on0zBAgPPm5MAbwAWA1VvacoLJ/zOfxWvoyn8fY3em1", - "6sxTDzdVYyXkI09FUv+r0uZRwhPepPAIlKBEjRvSmwKCN1NfDbImkV/QksNTfqXOCq1B4oV1yZs+Vux3", - "eJqQAS1AxiOdLSFlVujlZnM1HbdVq7QXdoKuzktFT5xaZG84599ErFWe60gHqNpIXyAGqNcQVuZ7mriA", - "GYgLLXD/F8FY54t6EHBa4L0Fn9ost0RUto4wA8Z0qlbEeC5+g6pvFvJOWWcdXZn8016mA/YI2ri27f3J", - "5mRD4KgcJM8Fi9jHk/cnG3vhwntrRmghDRsW7MBmBdHUdpG/JixidD84J4nPe3tJYQf3rw+bzRhxGrlw", - "cNGwbuTKeBSe2Up87gLUTjf241o6A5DwcMBQLjW3fyPrBpJF1/0QXg9ZWm7pjS7A4XMhknIU5nOwKLcg", - "51zzDBCobbiuSEKRaynicqx/PQ46Hf9UQ739btCMhZ4CVwX+ldz/X7ML5E5ImCIWmWwF3y6perf+shw4", - "F9aXtRHa1BPPN8ubwUy2XAumO5FC6AYY4wB9sfu/iNRTUjsDhKxIUeRcY0gYvEs4emYcpLAH062QXO+9", - "F3bvNO+lXh9MZxbmisVpsgRXEH23NBmZ5qgYAd8Z1MCz/lRnOgqDgY4NYMWe45/+6ptkVvvkk0rb1x3R", - "+MX1fQt40p2MlQH7tPk4/VJ/etwxMWx+MvCn1B/KWFMvrNiCkuDOL9fxdPNyT3tZtO35rQqc5TjJDez/", - "NBhuMVPEMRjzQ3X0UotbG7s/iRy38rKWXBChRsvbCdLR2relqmJAP9Z1q9Api1hI3X+5Lf8NAAD//0cV", - "3sY5HAAA", + "H4sIAAAAAAAC/8xY32+kNhD+Vyq3j1zY+/HEWxqlUdWoaq+5vkSryIHJxhewqT3OZRXxv1djAwuLWcge", + "Su4pGzx4Zr75vmHsZ5aqolQSJBqWPDMNplTSgPsHtFb6c/2EHqRKIkikn7wsc5FyFErGX42S9Myk91Bw", + "+lVqVYJG4fdJVeZex20JLGFCImxAsypiBRjDN91Fg1rIDauqiGn4zwoNGUuu/RY7+3XU2Kvbr5Aiq+iF", + "DEyqRUkxsYTR/htewGlWCHlUFr9ouGMJ+zneYRT7VRPTzueZOOj5Uhg8yrFAKMycCP4V8I281WBwrfl2", + "LCLk5mFxGGhTH0TYqS1zxbMFOGStyOjvndIFR5b4B9EEbZzRXLJYA3pxgGjTcYCqqN6m1Ynj1JSCmmz3", + "NgysHwNad8/Iux5C6BdcZktFeyCMkP9WgQP/k6CkfmEClpnoRaxUom6fww5HAjGzNU3WvqvsazpiKDAP", + "w+ofTHeLK7KrIvYojLjt7XWrVA5cDmrQWDbeoz1yuNebJFsgaoTHinZVhwvSFt6JRstz4prAbee1XYZt", + "rxvUmlu8V3o+wjs9DhFONXCE7BSPk9ors+pMWYkjywtRZY8NPs5DRGgJsAuwC2vUlusARVoNBPvK/Eq3", + "3TRQ6ZklQHgKc2EM4D3AGqzcPlEd/1jO4730ZTmPsbszazXKUw839WAl5CPPRdb8q/L2p4QnvMnhEUig", + "RI0b8psDQlCprwZZK+QBZFBwkQdjgKfySp1ZrUHipUspKB9n9ic8TdiAFiDTkcmWkDILzHKzuZqPx6pV", + "3is7QdfoUtEvTiNysJxkKnkxo3AuqtY8qqvQA6oJMlSIAeoNhHX4gSEuYgZSqwVu/yEYG72oBwGnFu8d", + "+DRm+UdEZZcIM2BMp2sljJfiD6jnZiHvlEvW05XJvy0YYuIjaOPHtvcnq5MVgaNKkLwULGEfT96frKj/", + "cbx3YcQO0vhO5BD7GdjxVBknDmKrGyZ/z1jC3AHlizP6TTjyE65g8FeVbfcG0cLmKEquMSZCvMs4BmZl", + "8trjzK2QXG+Dg19gMu6dAT+sVmPkbe3ivSm/WxqWXPeLcj3kXbWmN2rIWuFsYAwrOlddNOR4cbCDk5lL", + "+kBp6CNy4Uk9XphlTpLVsQn1D7nfX4D42YqsOlyGC2hgKbnmBSDQHHZdq46ksNOcb1q7foHaQtQBaOqE", + "sn5TYEBuhIQJTAiLc2f44yLSuwPwfOslFzdHt7AcziWCPvNzwStleJzeZp1HQ1dMobuCaimk3ddggkSA", + "9TfgzRg0ArJKEfCdQQ286IM9/ZkZ3Hy4L1SNyuGGXzeZ5Vo9uXQD0AGPX/yAdMSHsHuFVEXs0+rj9Ev9", + "a9ZOiHGuNkKOy/EvZVyol85sKbWMj88lN+ab0tm0lprhr31jMV0NEV69HOHeR2Ddw1tZnAU42Q3i/zS4", + "fWLGpikY81O99bER72LUsBEGPX8PR/m5sXxTZuwWPwRX558wAoeL1m/Xy4/NtYMTyJqasgH92LR9q3OW", + "sJhOGdW6+j8AAP//fdZwRK4ZAAA=", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/api/types.go b/api/types.go index 901ffec..03ef2b4 100644 --- a/api/types.go +++ b/api/types.go @@ -132,36 +132,14 @@ type UploadResponse struct { // UserResponse defines model for userResponse. type UserResponse = UserView -// EnterCodeRequest defines model for enterCodeRequest. -type EnterCodeRequest struct { - Code string `json:"code"` -} - -// GameEditRequest defines model for gameEditRequest. -type GameEditRequest = GameEdit - -// Login defines model for login. -type Login struct { - Email string `json:"email"` - Password string `json:"password"` -} - -// Register defines model for register. -type Register struct { - Email string `json:"email"` - Password string `json:"password"` - Password2 string `json:"password2"` - Username string `json:"username"` -} +// AdminUploadFileMultipartBody defines parameters for AdminUploadFile. +type AdminUploadFileMultipartBody interface{} // EnterCodeJSONBody defines parameters for EnterCode. type EnterCodeJSONBody struct { Code string `json:"code"` } -// UploadFileMultipartBody defines parameters for UploadFile. -type UploadFileMultipartBody interface{} - // PostUserLoginJSONBody defines parameters for PostUserLogin. type PostUserLoginJSONBody struct { Email string `json:"email"` @@ -176,18 +154,15 @@ type PostUserRegisterJSONBody struct { Username string `json:"username"` } -// CreateGameJSONRequestBody defines body for CreateGame for application/json ContentType. -type CreateGameJSONRequestBody = GameEdit +// AdminUploadFileMultipartRequestBody defines body for AdminUploadFile for multipart/form-data ContentType. +type AdminUploadFileMultipartRequestBody AdminUploadFileMultipartBody -// EditGameJSONRequestBody defines body for EditGame for application/json ContentType. -type EditGameJSONRequestBody = GameEdit +// AdminEditGameJSONRequestBody defines body for AdminEditGame for application/json ContentType. +type AdminEditGameJSONRequestBody = GameEdit // EnterCodeJSONRequestBody defines body for EnterCode for application/json ContentType. type EnterCodeJSONRequestBody EnterCodeJSONBody -// UploadFileMultipartRequestBody defines body for UploadFile for multipart/form-data ContentType. -type UploadFileMultipartRequestBody UploadFileMultipartBody - // PostUserLoginJSONRequestBody defines body for PostUserLogin for application/json ContentType. type PostUserLoginJSONRequestBody PostUserLoginJSONBody diff --git a/auth.go b/auth.go new file mode 100644 index 0000000..5492b2a --- /dev/null +++ b/auth.go @@ -0,0 +1,41 @@ +package main + +import ( + "context" + + "github.com/getkin/kin-openapi/openapi3filter" + "github.com/labstack/echo/v4" + oapiMiddleware "github.com/oapi-codegen/echo-middleware" + appmiddleware "gitrepo.ru/neonxp/nquest/pkg/contextlib" + + "gitrepo.ru/neonxp/nquest/pkg/models" +) + +var authFunc = func(ctx context.Context, ai *openapi3filter.AuthenticationInput) error { + echoCtx := ctx.Value(oapiMiddleware.EchoContextKey).(echo.Context) + user := appmiddleware.GetUser(echoCtx) + if user != nil { + if len(ai.Scopes) > 0 { + for _, v := range ai.Scopes { + switch v { + case "user": + return nil + case "creator": + if user.HasRole(models.RoleCreator) { + return nil + } + case "admin": + if user.HasRole(models.RoleAdmin) { + return nil + } + } + } + + return echo.ErrForbidden + } + + return nil + } + + return echo.ErrForbidden +} diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index adf8c12..5f3bf13 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -77,9 +77,8 @@ function Auth (props) { if (!user && !baseUser) { return } - if (props.role && !hasRole(props.role)) { - return + return null } return props.children diff --git a/frontend/src/assets/styles.css b/frontend/src/assets/styles.css index 30d7829..35cf452 100644 --- a/frontend/src/assets/styles.css +++ b/frontend/src/assets/styles.css @@ -1,3 +1,11 @@ +:root { + --accent-color: #fb923c; + --primary-bg: #171E26; + --secondary-bg: #26323f; + + accent-color: var(--accent-color); +} + .navbar-brand { padding-top: 0 !important; padding-bottom: 0 !important; @@ -21,11 +29,11 @@ body, #container { height: 100%; margin: 0; - background-color: #171E26; + background-color: var(--primary-bg); } .ant-layout-header { - background-color: #26323f; + background-color: var(--secondary-bg); border: 0; border-bottom: 1px solid rgba(154, 197, 247, 0.19); } diff --git a/frontend/src/pages/admin/Quest.jsx b/frontend/src/pages/admin/Quest.jsx index 18dba26..315fa3a 100644 --- a/frontend/src/pages/admin/Quest.jsx +++ b/frontend/src/pages/admin/Quest.jsx @@ -30,11 +30,7 @@ const Quest = () => { } const onFinish = (values) => { - let url = '/api/admin/games' - if (quest.id) { - url = `/api/admin/games/${quest.id}` - } - ajax(url, { + ajax('/api/admin/games', { method: 'POST', headers: { Accept: 'application/json', @@ -68,6 +64,9 @@ const Quest = () => { Сохранить квест + @@ -83,7 +82,7 @@ const Quest = () => { getValueFromEvent={normFile} > {quest.icon ? : null} - + diff --git a/frontend/src/pages/admin/Quests.jsx b/frontend/src/pages/admin/Quests.jsx index 98dfc13..37ef9f6 100644 --- a/frontend/src/pages/admin/Quests.jsx +++ b/frontend/src/pages/admin/Quests.jsx @@ -12,6 +12,7 @@ const Quests = () => { Создать новый квест 0 { - for _, v := range ai.Scopes { - switch v { - case "user": - return nil - case "creator": - if user.HasRole(models.RoleCreator) { - return nil - } - case "admin": - if user.HasRole(models.RoleAdmin) { - return nil - } - } - } - - return echo.ErrForbidden - } - - return nil - } - - return echo.ErrForbidden - } - swagger, err := api.GetSwagger() if err != nil { fmt.Fprintf(os.Stderr, "Error loading swagger spec\n: %s", err) @@ -127,14 +83,6 @@ func main() { session.Middleware(store), middleware.Logger(), middleware.RateLimiter(middleware.NewRateLimiterMemoryStore(20)), - // middleware.CSRFWithConfig(middleware.CSRFConfig{ - // TokenLookup: "cookie:_csrf", - // CookiePath: "/", - // // CookieDomain: "nquest.ru", - // // CookieSecure: true, - // CookieHTTPOnly: true, - // CookieSameSite: http.SameSiteStrictMode, - // }), middleware.Gzip(), echoprometheus.NewMiddleware("nquest"), appmiddleware.User(userService), diff --git a/pkg/controller/admin.go b/pkg/controller/admin.go index 7c8a22b..b849ae0 100644 --- a/pkg/controller/admin.go +++ b/pkg/controller/admin.go @@ -16,78 +16,18 @@ type Admin struct { GameService *service.Game } -// (POST /admin/games) -func (a *Admin) CreateGame(ctx echo.Context) error { +// (POST /games/{uid}) +func (a *Admin) AdminEditGame(ctx echo.Context) error { user := contextlib.GetUser(ctx) - req := &api.GameEditRequest{} + req := &api.AdminEditGameJSONRequestBody{} + if err := ctx.Bind(req); err != nil { return err } game := a.mapCreateGameRequest(req, user) - var err error - game, err = a.GameService.CreateGame(ctx.Request().Context(), game) - if err != nil { - return err - } - - tasks := make([]api.TaskEdit, 0, len(game.Tasks)) - - for _, t := range game.Tasks { - codes := make([]api.CodeEdit, 0, len(t.Codes)) - for _, c := range t.Codes { - codes = append(codes, api.CodeEdit{ - Code: c.Code, - Description: c.Description, - }) - } - tasks = append(tasks, api.TaskEdit{ - Codes: codes, - Text: t.Text, - Title: t.Title, - }) - } - - return ctx.JSON(http.StatusOK, api.GameAdminResponse{ - Description: game.Description, - Icon: game.IconID, - Id: &game.ID, - Points: game.Points, - Tasks: tasks, - Title: game.Title, - Type: api.MapGameTypeReverse(game.Type), - Visible: game.Visible, - }) -} - -// (POST /games/{uid}) -func (a *Admin) EditGame(ctx echo.Context, uid uuid.UUID) error { - user := contextlib.GetUser(ctx) - req := &api.GameEditRequest{} - - game, err := a.GameService.GetByID(ctx.Request().Context(), uid) - if err != nil { - return echo.ErrNotFound - } - if user.Role != models.RoleAdmin { - isAuthor := false - for _, u := range game.Authors { - if u.ID == user.ID { - isAuthor = true - break - } - } - if !isAuthor { - return echo.ErrForbidden - } - } - if err = ctx.Bind(req); err != nil { - return err - } - game = a.mapCreateGameRequest(req, user) - - game, err = a.GameService.UpdateGame(ctx.Request().Context(), uid, game) + game, err := a.GameService.UpsertGame(ctx.Request().Context(), game) if err != nil { return err } @@ -125,7 +65,7 @@ func (a *Admin) EditGame(ctx echo.Context, uid uuid.UUID) error { } // (GET /games/{uid}) -func (a *Admin) GetGameByAdmin(ctx echo.Context, uid uuid.UUID) error { +func (a *Admin) AdminGetGame(ctx echo.Context, uid uuid.UUID) error { user := contextlib.GetUser(ctx) game, err := a.GameService.GetByID(ctx.Request().Context(), uid) @@ -177,7 +117,7 @@ func (a *Admin) GetGameByAdmin(ctx echo.Context, uid uuid.UUID) error { }) } -func (a *Admin) ListGamesByAdmin(ctx echo.Context) error { +func (a *Admin) AdminListGames(ctx echo.Context) error { user := contextlib.GetUser(ctx) games, err := a.GameService.ListByAuthor(ctx.Request().Context(), user) @@ -202,11 +142,13 @@ func (a *Admin) ListGamesByAdmin(ctx echo.Context) error { return ctx.JSON(http.StatusOK, resp) } -func (*Admin) mapCreateGameRequest(req *api.GameEditRequest, user *models.User) *models.Game { +func (*Admin) mapCreateGameRequest(req *api.GameEdit, user *models.User) *models.Game { + id := uuid.New() + if req.Id != nil { + id = *req.Id + } game := &models.Game{ - Model: models.Model{ - ID: uuid.New(), - }, + ID: id, Visible: req.Visible, Title: req.Title, Description: req.Description, @@ -219,14 +161,12 @@ func (*Admin) mapCreateGameRequest(req *api.GameEditRequest, user *models.User) IconID: req.Icon, } for order, te := range req.Tasks { - if te.Id == nil { - u := uuid.New() - te.Id = &u + id := uuid.New() + if te.Id != nil { + id = *te.Id } task := &models.Task{ - Model: models.Model{ - ID: *te.Id, - }, + ID: id, Title: te.Title, Text: te.Text, Codes: make([]*models.Code, 0, len(te.Codes)), @@ -234,14 +174,12 @@ func (*Admin) mapCreateGameRequest(req *api.GameEditRequest, user *models.User) } for _, ce := range te.Codes { - if ce.Id == nil { - u := uuid.New() - ce.Id = &u + id := uuid.New() + if ce.Id != nil { + id = *ce.Id } task.Codes = append(task.Codes, &models.Code{ - Model: models.Model{ - ID: *ce.Id, - }, + ID: id, Code: ce.Code, Description: ce.Description, }) diff --git a/pkg/controller/file.go b/pkg/controller/file.go index 0e296c9..939e88c 100644 --- a/pkg/controller/file.go +++ b/pkg/controller/file.go @@ -12,7 +12,7 @@ type File struct { } // (POST /file/upload) -func (u *File) UploadFile(c echo.Context) error { +func (u *File) AdminUploadFile(c echo.Context) error { // user := contextlib.GetUser(c) fh, err := c.FormFile("file") if err != nil { diff --git a/pkg/models/file.go b/pkg/models/file.go index cf4138e..b413ee5 100644 --- a/pkg/models/file.go +++ b/pkg/models/file.go @@ -1,10 +1,19 @@ package models -type File struct { - Model +import ( + "time" + "github.com/google/uuid" + "gorm.io/gorm" +) + +type File struct { + ID uuid.UUID `gorm:"primarykey" json:"id"` Filename string ContentType string Size int - Body []byte `gorm:"type:bytea"` + Body []byte `gorm:"type:bytea"` + CreatedAt time.Time `json:"-"` + UpdatedAt time.Time `json:"-"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` } diff --git a/pkg/models/game.go b/pkg/models/game.go index 7b60bc9..51f1b08 100644 --- a/pkg/models/game.go +++ b/pkg/models/game.go @@ -1,11 +1,15 @@ package models -import "github.com/google/uuid" +import ( + "time" + + "github.com/google/uuid" + "gorm.io/gorm" +) type Game struct { - Model - - Visible bool `gorm:"index"` + ID uuid.UUID `gorm:"primarykey" json:"id"` + Visible bool `gorm:"index"` Title string Description string Tasks []*Task `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"` @@ -14,6 +18,9 @@ type Game struct { Points int Icon *File IconID uuid.UUID + CreatedAt time.Time `json:"-"` + UpdatedAt time.Time `json:"-"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` } type GameType int diff --git a/pkg/models/model.go b/pkg/models/model.go deleted file mode 100644 index cb8b301..0000000 --- a/pkg/models/model.go +++ /dev/null @@ -1,15 +0,0 @@ -package models - -import ( - "time" - - "github.com/google/uuid" - "gorm.io/gorm" -) - -type Model struct { - ID uuid.UUID `gorm:"primarykey" json:"id"` - CreatedAt time.Time `json:"-"` - UpdatedAt time.Time `json:"-"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` -} diff --git a/pkg/models/task.go b/pkg/models/task.go index 5a0df22..448c592 100644 --- a/pkg/models/task.go +++ b/pkg/models/task.go @@ -1,29 +1,22 @@ package models -import "github.com/google/uuid" +import ( + "github.com/google/uuid" +) type Task struct { - Model + ID uuid.UUID `gorm:"primarykey" json:"id"` Title string Text string MaxTime int GameID uuid.UUID - Solutions []*Solution `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"` - Codes []*Code `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"` + Codes []*Code `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"` TaskOrder uint } -type Solution struct { - Model - - TaskID uuid.UUID - After int - Text string -} - type Code struct { - Model + ID uuid.UUID `gorm:"primarykey" json:"id"` TaskID uuid.UUID Code string `gorm:"index"` diff --git a/pkg/models/user.go b/pkg/models/user.go index 2d4d28c..d322886 100644 --- a/pkg/models/user.go +++ b/pkg/models/user.go @@ -2,15 +2,22 @@ package models import ( "errors" + "time" + + "github.com/google/uuid" + "gorm.io/gorm" ) var ErrEmptyPassword = errors.New("empty password") type User struct { - Model - Username string `gorm:"unique" json:"username"` - Email string `gorm:"unique" json:"email"` - Password string `json:"-"` + ID uuid.UUID `gorm:"primarykey" json:"id"` + CreatedAt time.Time `json:"-"` + UpdatedAt time.Time `json:"-"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` + Username string `gorm:"unique" json:"username"` + Email string `gorm:"unique" json:"email"` + Password string `json:"-"` Experience int Games []*GameCursor Role UserRole diff --git a/pkg/service/engine.go b/pkg/service/engine.go index c0086b4..866dd8b 100644 --- a/pkg/service/engine.go +++ b/pkg/service/engine.go @@ -137,7 +137,6 @@ func (e *Engine) GetNext(ctx context.Context, gameID uuid.UUID, currentOrder uin var t models.Task err := e.DB.WithContext(ctx). Preload("Codes"). - Preload("Solutions"). Order("task_order ASC"). First(&t, `game_id = ? AND task_order > ?`, gameID, currentOrder). Error diff --git a/pkg/service/file.go b/pkg/service/file.go index 9c48085..833ad5b 100644 --- a/pkg/service/file.go +++ b/pkg/service/file.go @@ -32,7 +32,7 @@ func (u *File) Upload( } file := &models.File{ - Model: models.Model{ID: uuid.New()}, + ID: uuid.New(), Filename: filename, ContentType: contentType, Size: size, diff --git a/pkg/service/game.go b/pkg/service/game.go index 373aa48..214555e 100644 --- a/pkg/service/game.go +++ b/pkg/service/game.go @@ -28,7 +28,6 @@ func (gs *Game) GetByID(ctx context.Context, id uuid.UUID) (*models.Game, error) return db.Order("tasks.task_order ASC") }). Preload("Tasks.Codes"). - Preload("Tasks.Solutions"). First(g, id). Error } @@ -67,20 +66,9 @@ func (gs *Game) ListByAuthor(ctx context.Context, author *models.User) ([]*model Error } -func (gs *Game) CreateGame(ctx context.Context, game *models.Game) (*models.Game, error) { - return game, gs.DB. +func (gs *Game) UpsertGame(ctx context.Context, game *models.Game) (*models.Game, error) { + return game, gs.DB.Debug(). Session(&gorm.Session{FullSaveAssociations: true}). - Create(game). - Error -} - -func (gs *Game) UpdateGame(ctx context.Context, uid uuid.UUID, game *models.Game) (*models.Game, error) { - game.ID = uid - - db := gs.DB - - return game, db.Debug(). - Session(&gorm.Session{FullSaveAssociations: true}).Omit("created_at"). Save(game). Error } diff --git a/pkg/service/user.go b/pkg/service/user.go index 794d2c2..3121586 100644 --- a/pkg/service/user.go +++ b/pkg/service/user.go @@ -59,9 +59,7 @@ func (s *User) Register(ctx context.Context, username, email, password, password } u := &models.User{ - Model: models.Model{ - ID: uuid.New(), - }, + ID: uuid.New(), Username: username, Email: normalizer.NewNormalizer().Normalize(email), Password: hex.EncodeToString(hashed), diff --git a/requests.http b/requests.http index 3696404..09dd72f 100644 --- a/requests.http +++ b/requests.http @@ -252,4 +252,4 @@ Content-Type: image/jpg ### -GET http://localhost:8000/api/file/f343919a-b068-4d72-ade5-bbb84db0bec9 \ No newline at end of file +GET http://localhost:8000/api/file/f343919a-b068-4d72-ade5-bbb84db0bec9