diff --git a/api/openapi.yaml b/api/openapi.yaml index a3a6038..b912361 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -8,57 +8,49 @@ servers: - url: /api paths: -# User routes + # User routes /user: get: responses: 200: - $ref: '#/components/responses/userResponse' + $ref: "#/components/responses/userResponse" 403: - $ref: '#/components/responses/errorResponse' + $ref: "#/components/responses/errorResponse" /user/login: post: security: [] requestBody: - $ref: '#/components/requestBodies/login' + $ref: "#/components/requestBodies/login" responses: 200: - $ref: '#/components/responses/userResponse' + $ref: "#/components/responses/userResponse" 400: - $ref: '#/components/responses/errorResponse' + $ref: "#/components/responses/errorResponse" /user/register: post: security: [] requestBody: - $ref: '#/components/requestBodies/register' + $ref: "#/components/requestBodies/register" responses: 200: - $ref: '#/components/responses/userResponse' + $ref: "#/components/responses/userResponse" 400: - $ref: '#/components/responses/errorResponse' + $ref: "#/components/responses/errorResponse" /user/logout: post: responses: 204: description: "success logout" 400: - $ref: '#/components/responses/errorResponse' + $ref: "#/components/responses/errorResponse" -# Game routes + # Game routes /games: get: responses: 200: - $ref: '#/components/responses/gameListResponse' - post: - operationId: createGame - security: - - cookieAuth: [creator, admin] - requestBody: - $ref: "#/components/requestBodies/gameEditRequest" - responses: - 200: - $ref: "#/components/responses/gameResponse" + $ref: "#/components/responses/gameListResponse" + /engine/{uid}: get: operationId: gameEngine @@ -71,7 +63,7 @@ paths: format: uuid responses: 200: - $ref: '#/components/responses/taskResponse' + $ref: "#/components/responses/taskResponse" /engine/{uid}/code: post: operationId: enterCode @@ -86,12 +78,12 @@ paths: $ref: "#/components/requestBodies/enterCodeRequest" responses: 200: - $ref: '#/components/responses/taskResponse' + $ref: "#/components/responses/taskResponse" /file/upload: post: operationId: uploadFile security: - - cookieAuth: [creator, admin] + - cookieAuth: [creator, admin] requestBody: content: multipart/form-data: @@ -103,7 +95,7 @@ paths: format: binary responses: 200: - $ref: '#/components/responses/uploadResponse' + $ref: "#/components/responses/uploadResponse" /file/{uid}: get: operationId: getFile @@ -118,11 +110,40 @@ paths: 200: description: file content: - 'application/octet-stream': + "application/octet-stream": schema: type: string format: binary - /games/{uid}: + /admin/games: + get: + operationId: listGamesByAdmin + responses: + 200: + $ref: "#/components/responses/gameListResponse" + post: + operationId: createGame + security: + - cookieAuth: [creator, admin] + requestBody: + $ref: "#/components/requestBodies/gameEditRequest" + responses: + 200: + $ref: "#/components/responses/gameAdminResponse" + /admin/games/{uid}: + get: + operationId: getGameByAdmin + parameters: + - name: uid + in: path + required: true + schema: + type: string + format: uuid + security: + - cookieAuth: [creator, admin] + responses: + 200: + $ref: "#/components/responses/gameAdminResponse" post: operationId: editGame parameters: @@ -133,12 +154,12 @@ paths: type: string format: uuid security: - - cookieAuth: [creator, admin] + - cookieAuth: [creator, admin] requestBody: $ref: "#/components/requestBodies/gameEditRequest" responses: 200: - $ref: "#/components/responses/gameResponse" + $ref: "#/components/responses/gameAdminResponse" components: schemas: userView: @@ -230,9 +251,9 @@ components: text: type: string codes: - type: array - items: - $ref: '#/components/schemas/codeView' + type: array + items: + $ref: "#/components/schemas/codeView" # solutions: # type: array # items: @@ -263,6 +284,11 @@ components: gameEdit: type: object properties: + id: + type: string + format: uuid + visible: + type: boolean title: type: string description: @@ -279,6 +305,7 @@ components: type: string format: uuid required: + - visible - title - description - type @@ -288,14 +315,17 @@ components: taskEdit: type: object properties: + id: + type: string + format: uuid title: type: string text: type: string codes: - type: array - items: - $ref: '#/components/schemas/codeEdit' + type: array + items: + $ref: "#/components/schemas/codeEdit" # solutions: # type: array # items: @@ -308,6 +338,9 @@ components: codeEdit: type: object properties: + id: + type: string + format: uuid description: type: string code: @@ -328,13 +361,13 @@ components: gameType: type: string enum: - - virtual - - city + - virtual + - city requestBodies: login: required: true content: - 'application/json': + "application/json": schema: type: object properties: @@ -342,11 +375,11 @@ components: type: string password: type: string - required: [ email, password ] + required: [email, password] register: required: true content: - 'application/json': + "application/json": schema: type: object properties: @@ -358,17 +391,17 @@ components: type: string password2: type: string - required: [ username, email, password, password2 ] + required: [username, email, password, password2] gameEditRequest: required: true content: - 'application/json': + "application/json": schema: - $ref: '#/components/schemas/gameEdit' + $ref: "#/components/schemas/gameEdit" enterCodeRequest: required: true content: - 'application/json': + "application/json": schema: type: object properties: @@ -378,15 +411,15 @@ components: - code responses: userResponse: - description: '' + description: "" content: - 'application/json': + "application/json": schema: $ref: "#/components/schemas/userView" errorResponse: - description: '' + description: "" content: - 'application/json': + "application/json": schema: type: object properties: @@ -394,31 +427,37 @@ components: type: integer message: type: string - required: [ code, message ] + required: [code, message] gameListResponse: - description: '' + description: "" content: - 'application/json': + "application/json": schema: type: array items: $ref: "#/components/schemas/gameView" gameResponse: - description: '' + description: "" content: - 'application/json': + "application/json": schema: $ref: "#/components/schemas/gameView" - taskResponse: - description: '' + gameAdminResponse: + description: "" content: - 'application/json': + "application/json": + schema: + $ref: "#/components/schemas/gameEdit" + taskResponse: + description: "" + content: + "application/json": schema: $ref: "#/components/schemas/taskView" uploadResponse: - description: '' + description: "" content: - 'application/json': + "application/json": schema: type: object properties: @@ -431,7 +470,7 @@ components: cookieAuth: type: apiKey in: cookie - name: session + name: session security: - cookieAuth: [] diff --git a/api/server.go b/api/server.go index f0f287e..a7b818e 100644 --- a/api/server.go +++ b/api/server.go @@ -22,6 +22,18 @@ import ( // ServerInterface represents all server handlers. type ServerInterface interface { + // (GET /admin/games) + ListGamesByAdmin(ctx echo.Context) error + + // (POST /admin/games) + CreateGame(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 + // (GET /engine/{uid}) GameEngine(ctx echo.Context, uid openapi_types.UUID) error @@ -37,12 +49,6 @@ type ServerInterface interface { // (GET /games) GetGames(ctx echo.Context) error - // (POST /games) - CreateGame(ctx echo.Context) error - - // (POST /games/{uid}) - EditGame(ctx echo.Context, uid openapi_types.UUID) error - // (GET /user) GetUser(ctx echo.Context) error @@ -61,6 +67,64 @@ type ServerInterfaceWrapper struct { Handler ServerInterface } +// ListGamesByAdmin converts echo context to params. +func (w *ServerInterfaceWrapper) ListGamesByAdmin(ctx echo.Context) error { + var err error + + ctx.Set(CookieAuthScopes, []string{}) + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.ListGamesByAdmin(ctx) + return err +} + +// CreateGame converts echo context to params. +func (w *ServerInterfaceWrapper) CreateGame(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) + return err +} + +// GetGameByAdmin converts echo context to params. +func (w *ServerInterfaceWrapper) GetGameByAdmin(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.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) + return err +} + // GameEngine converts echo context to params. func (w *ServerInterfaceWrapper) GameEngine(ctx echo.Context) error { var err error @@ -137,35 +201,6 @@ func (w *ServerInterfaceWrapper) GetGames(ctx echo.Context) error { return err } -// CreateGame converts echo context to params. -func (w *ServerInterfaceWrapper) CreateGame(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) - 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) - return err -} - // GetUser converts echo context to params. func (w *ServerInterfaceWrapper) GetUser(ctx echo.Context) error { var err error @@ -234,13 +269,15 @@ 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.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.POST(baseURL+"/games", wrapper.CreateGame) - router.POST(baseURL+"/games/:uid", wrapper.EditGame) router.GET(baseURL+"/user", wrapper.GetUser) router.POST(baseURL+"/user/login", wrapper.PostUserLogin) router.POST(baseURL+"/user/logout", wrapper.PostUserLogout) @@ -251,26 +288,27 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "H4sIAAAAAAAC/8xYTW/jNhD9KwXbozbyfpx02wbboGhQtGm2l8AIuNLE4UYiVXKUjRHovxdDUl8WZcle", - "N80pDjki37yZNxzymaWqKJUEiYYlz0zDPxUY/FllAuwASAR9rjK4cjM0liqJIO1PXpa5SDkKJeOvRkka", - "M+k9FJx+lVqVoNEvlaoM6C9uS2AJM6iF3LC6juyuQkPGkhtntY4aK/XlK6TI6qEZ6grqiG14AZ8ygcdg", - "+0nDHUvYj3FHQOxmTdysO7FtrjZCfgcRUHCRB5iIWMmN+aZ0Nk+TW6P3xULKNGyEQdAvDb+bfBecrQxo", - "yYsFCdJaRmMS+rssIsSOmFJJ433TWukrP3K6XBcSYQOaHC3AGL5ZKoTOPuxOBibVoiRMLGHMa+JSGDzK", - "CYFQmCXq+FvAN9rNQ+Ja8+0+REehWQYivCly83DyTWnRfZtWZa54doL0qSphRXSndMGRJW4gmlMGGS3N", - "E5LRyQmiRacJqiO/TCsRW2SXHhQ7C87pp28cTR0rbsJCfgEYof3bs2a0//5tIiZSNzGTJBErlfDH+7gc", - "UUqbxdIna3cw7ko/YigwD/PlBub1fE12u/y5ZaOdcNolG/Sth56TKZqvPQ6QVUFLPwqNFacDJBW47X3W", - "QW/LzCg6vMJ7pZdT10ljTF2qgSNkH/GIdDsgDxbVlPl0OVeVxInp/yYHHM59idAmQAewT2vUhmtPirTJ", - "HawEyyPdFraQSOApHOQp5ibUYNeJPLApZ6bL2mHOTKVtr5VpJKUebn3fIuQjz0XW/Kvy9qeEJ7zN4RFI", - "eRTzW9o3B4SgBF+MslahB3S88FReq/NKa5B4aV0K6sKa/Q5PMzagBch0onEkpswJmrTFdSCfxqpVPgg7", - "UdcITtEvnhVCBsO5vNG3qALdfo+oBmQoECPWGwo9/ECjFDEDaaUFbv8iGhu9qAcBHyu8t+RTK+OGKJWt", - "I8yAMb1ylDBeit/AN8RC3inrrEtXJv+0d9WIPYI2rjV6e7Y6WxE5qgTJS8ES9v7s7dnK3mfw3sKIQW6E", - "hPi5EllNAxuwsqA8ta3arxlL2AV1E9bQfqt5AQh0St146LReB9xFfngninq93lz/ud65Pr1braYSs7WL", - "B615bTkaOBc3jVepTMDFT82LxMt52LyJbKed6z2bxKM3k/pUNN2JHGJ3y5gm6LOd/0XYqrcDvtflF1WO", - "ouQaY+LgTcYxcBGhDQc0fRGS623wKhK8bR/q9c4Vqq9JG+K+Gm/GBadedzzNKAXQU/S/yWTiyqVSBHxj", - "UAMvhlev+SiMbl02gD572hNkipCLpj4eHLbRq4PdMZyf57Ytu3BF/WBx7b751cfC/d4cs2x2STZRrDKB", - "3tPXWateDZ22hdiTm59di3FERek/dNQR+7B6P//R8B3Q64dWitvH33DA/1DGQr20ZkcExK1fn8bT1eGe", - "DoK3HvitKlzkONmN8H8YPWkwU6UpGPODX/pYxB3G/uP2fpRXjeUREWp3eT1B2iu5NRUGA/qxKT2VzlnC", - "Ymo063X9bwAAAP//ZDhhHgMaAAA=", + "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", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/api/types.go b/api/types.go index 5e2e658..901ffec 100644 --- a/api/types.go +++ b/api/types.go @@ -35,8 +35,9 @@ const ( // CodeEdit defines model for codeEdit. type CodeEdit struct { - Code string `json:"code"` - Description string `json:"description"` + Code string `json:"code"` + Description string `json:"description"` + Id *openapi_types.UUID `json:"id,omitempty"` } // CodeView defines model for codeView. @@ -47,12 +48,14 @@ type CodeView struct { // GameEdit defines model for gameEdit. type GameEdit struct { - Description string `json:"description"` - Icon openapi_types.UUID `json:"icon"` - Points int `json:"points"` - Tasks []TaskEdit `json:"tasks"` - Title string `json:"title"` - Type GameType `json:"type"` + Description string `json:"description"` + Icon openapi_types.UUID `json:"icon"` + Id *openapi_types.UUID `json:"id,omitempty"` + Points int `json:"points"` + Tasks []TaskEdit `json:"tasks"` + Title string `json:"title"` + Type GameType `json:"type"` + Visible bool `json:"visible"` } // GameType defines model for gameType. @@ -73,9 +76,10 @@ type GameView struct { // TaskEdit defines model for taskEdit. type TaskEdit struct { - Codes []CodeEdit `json:"codes"` - Text string `json:"text"` - Title string `json:"title"` + Codes []CodeEdit `json:"codes"` + Id *openapi_types.UUID `json:"id,omitempty"` + Text string `json:"text"` + Title string `json:"title"` } // TaskView defines model for taskView. @@ -111,12 +115,12 @@ type ErrorResponse struct { Message string `json:"message"` } +// GameAdminResponse defines model for gameAdminResponse. +type GameAdminResponse = GameEdit + // GameListResponse defines model for gameListResponse. type GameListResponse = []GameView -// GameResponse defines model for gameResponse. -type GameResponse = GameView - // TaskResponse defines model for taskResponse. type TaskResponse = TaskView @@ -172,18 +176,18 @@ type PostUserRegisterJSONBody struct { Username string `json:"username"` } -// 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 - // CreateGameJSONRequestBody defines body for CreateGame for application/json ContentType. type CreateGameJSONRequestBody = GameEdit // EditGameJSONRequestBody defines body for EditGame for application/json ContentType. type EditGameJSONRequestBody = 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/frontend/src/App.jsx b/frontend/src/App.jsx index 2f8c31a..adf8c12 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -14,7 +14,8 @@ import Engine from './pages/Engine' import Quests from './pages/Quests' import User from './pages/User' import { useRole } from './utils/roles' -import Quest from './pages/admin/Quest' +import EditQuest from './pages/admin/Quest' +import AdminQuest from './pages/admin/Quests' const router = createBrowserRouter( createRoutesFromElements( @@ -43,14 +44,19 @@ const router = createBrowserRouter( loader={({ params }) => ajax(`/api/engine/${params.gameId}`).catch(x => { console.log(x); return null })} /> } + path="/admin/quests/new" + element={} // loader={() => ajax(`/api/admin/games`)} /> } - // loader={() => ajax(`/api/admin/games`)} + path="/admin/quests/:gameId" + element={} + loader={({ params }) => ajax(`/api/admin/games/${params.gameId}`)} + /> + } + loader={() => ajax('/api/admin/games')} /> } /> diff --git a/frontend/src/components/Layout.jsx b/frontend/src/components/Layout.jsx index 93c8edb..2f67639 100644 --- a/frontend/src/components/Layout.jsx +++ b/frontend/src/components/Layout.jsx @@ -49,9 +49,9 @@ const AppLayout = () => { if (hasRole('creator')) { items.push({ - key: 'quest/new', - label: 'Создать квест', - link: '/quest/new' + key: 'admin/quests', + label: 'Управление квестами', + link: '/admin/quests' }) } diff --git a/frontend/src/pages/Engine.jsx b/frontend/src/pages/Engine.jsx index 0d71c62..cc5c62e 100644 --- a/frontend/src/pages/Engine.jsx +++ b/frontend/src/pages/Engine.jsx @@ -12,6 +12,9 @@ const Engine = () => { const { message } = App.useApp() useEffect(() => { + if (!task) { + return + } switch (task.message) { case 'invalid_code': message.error('Неверный код') @@ -26,7 +29,7 @@ const Engine = () => { message.success('Код принят, ищите оставшиеся') break } - }, [task.message]) + }, [task]) const [form] = Form.useForm() const onFinish = ({ code }) => { diff --git a/frontend/src/pages/Quests.jsx b/frontend/src/pages/Quests.jsx index 8c5e509..b67c3b5 100644 --- a/frontend/src/pages/Quests.jsx +++ b/frontend/src/pages/Quests.jsx @@ -16,7 +16,7 @@ const Quests = () => { return (<> Квесты {games.map(item => renderItem(user, navigate, item))} - {!games ? (Квестов пока не анонсировано) : null} + {games.length === 0 ? (Квестов пока не анонсировано) : null} ) } diff --git a/frontend/src/pages/admin/Quest.jsx b/frontend/src/pages/admin/Quest.jsx index 8c0b3e0..18dba26 100644 --- a/frontend/src/pages/admin/Quest.jsx +++ b/frontend/src/pages/admin/Quest.jsx @@ -1,5 +1,5 @@ import { useLoaderData, useNavigate } from 'react-router-dom' -import { Alert, Button, Card, Form, Input, InputNumber, Popconfirm, Radio, Typography, Upload } from 'antd' +import { Alert, Avatar, Button, Card, Form, Input, InputNumber, Popconfirm, Radio, Switch, Typography, Upload } from 'antd' import { UploadOutlined, PlusOutlined, CloseOutlined } from '@ant-design/icons' import { ajax } from '../../utils/fetch' import { useState } from 'react' @@ -30,7 +30,11 @@ const Quest = () => { } const onFinish = (values) => { - ajax('/api/games', { + let url = '/api/admin/games' + if (quest.id) { + url = `/api/admin/games/${quest.id}` + } + ajax(url, { method: 'POST', headers: { Accept: 'application/json', @@ -38,7 +42,7 @@ const Quest = () => { }, body: JSON.stringify(values) }) - .then(g => navigate(`/quest/${g.id}/edit`)) + .then(g => navigate(`/admin/quests/${g.id}/`)) .catch(({ message }) => setError('Ошибка создания')) } @@ -64,6 +68,9 @@ const Quest = () => { Сохранить квест + + + @@ -75,6 +82,7 @@ const Quest = () => { label='Иконка' getValueFromEvent={normFile} > + {quest.icon ? : null} @@ -108,7 +116,7 @@ const Quest = () => { // eslint-disable-next-line react/display-name const renderTaskForm = remove => task => ( task => ( ]} > + @@ -152,19 +163,22 @@ const renderCodeForm = remove => code => ( key={code.key} style={{ marginBottom: 8 }} actions={[ - remove(code.name)} - okText='Да' - cancelText='Нет' - > - - + // remove(code.name)} + // okText='Да' + // cancelText='Нет' + // > + + // ]} > + diff --git a/frontend/src/pages/admin/Quests.jsx b/frontend/src/pages/admin/Quests.jsx new file mode 100644 index 0000000..98dfc13 --- /dev/null +++ b/frontend/src/pages/admin/Quests.jsx @@ -0,0 +1,40 @@ +import { Table, Typography } from 'antd' +import { Link, useLoaderData } from 'react-router-dom' + +const { Title } = Typography + +const Quests = () => { + const quests = useLoaderData() + + return ( + <> + Управление своими квестами + Создать новый квест + ${uid} + }, + { + title: 'Название', + dataIndex: 'title', + key: 'title' + }, + { + title: 'Тип', + dataIndex: 'type', + key: 'type', + render: type => type === 'virtual' ? 'Виртуальный' : 'Полевой' + } + ]} + + /> + + ) +} + +export default Quests diff --git a/pkg/controller/admin.go b/pkg/controller/admin.go index 605a471..7c8a22b 100644 --- a/pkg/controller/admin.go +++ b/pkg/controller/admin.go @@ -32,16 +32,32 @@ func (a *Admin) CreateGame(ctx echo.Context) error { return err } - return ctx.JSON(http.StatusCreated, api.GameResponse{ - Authors: make([]api.UserView, 0, len(game.Authors)), - CreatedAt: game.CreatedAt.Format(time.RFC3339), + 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, + Id: &game.ID, Points: game.Points, - TaskCount: len(game.Tasks), + Tasks: tasks, Title: game.Title, Type: api.MapGameTypeReverse(game.Type), + Visible: game.Visible, }) } @@ -49,37 +65,149 @@ func (a *Admin) CreateGame(ctx echo.Context) error { func (a *Admin) EditGame(ctx echo.Context, uid uuid.UUID) error { user := contextlib.GetUser(ctx) req := &api.GameEditRequest{} - if err := ctx.Bind(req); err != nil { + + 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 := a.mapCreateGameRequest(req, user) - - var err error game, err = a.GameService.UpdateGame(ctx.Request().Context(), uid, game) if err != nil { return err } - return ctx.JSON(http.StatusCreated, api.GameResponse{ - Authors: make([]api.UserView, 0, len(game.Authors)), - CreatedAt: game.CreatedAt.Format(time.RFC3339), + tasks := make([]api.TaskEdit, 0, len(game.Tasks)) + + for _, t := range game.Tasks { + t := t + codes := make([]api.CodeEdit, 0, len(t.Codes)) + for _, c := range t.Codes { + codes = append(codes, api.CodeEdit{ + Id: &c.ID, + Code: c.Code, + Description: c.Description, + }) + } + tasks = append(tasks, api.TaskEdit{ + Id: &t.ID, + Codes: codes, + Text: t.Text, + Title: t.Title, + }) + } + + return ctx.JSON(http.StatusOK, api.GameAdminResponse{ Description: game.Description, Icon: game.IconID, - Id: game.ID, + Id: &game.ID, Points: game.Points, - TaskCount: len(game.Tasks), + Tasks: tasks, Title: game.Title, Type: api.MapGameTypeReverse(game.Type), + Visible: game.Visible, }) } +// (GET /games/{uid}) +func (a *Admin) GetGameByAdmin(ctx echo.Context, uid uuid.UUID) error { + user := contextlib.GetUser(ctx) + + 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 + } + } + + tasks := make([]api.TaskEdit, 0, len(game.Tasks)) + + for _, t := range game.Tasks { + t := t + codes := make([]api.CodeEdit, 0, len(t.Codes)) + for _, c := range t.Codes { + codes = append(codes, api.CodeEdit{ + Id: &c.ID, + Code: c.Code, + Description: c.Description, + }) + } + tasks = append(tasks, api.TaskEdit{ + Id: &t.ID, + 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, + }) +} + +func (a *Admin) ListGamesByAdmin(ctx echo.Context) error { + user := contextlib.GetUser(ctx) + + games, err := a.GameService.ListByAuthor(ctx.Request().Context(), user) + if err != nil { + return echo.ErrNotFound + } + + resp := make(api.GameListResponse, 0, len(games)) + for _, game := range games { + gv := api.GameView{ + Id: game.ID, + Title: game.Title, + Type: api.MapGameTypeReverse(game.Type), + Points: game.Points, + TaskCount: len(game.Tasks), + CreatedAt: game.CreatedAt.Format(time.RFC3339), + Icon: game.IconID, + } + resp = append(resp, gv) + } + + return ctx.JSON(http.StatusOK, resp) +} + func (*Admin) mapCreateGameRequest(req *api.GameEditRequest, user *models.User) *models.Game { game := &models.Game{ Model: models.Model{ ID: uuid.New(), }, - Visible: false, + Visible: req.Visible, Title: req.Title, Description: req.Description, Authors: []*models.User{ @@ -91,30 +219,28 @@ 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 + } task := &models.Task{ Model: models.Model{ - ID: uuid.New(), + ID: *te.Id, }, - Title: te.Title, - Text: te.Text, - MaxTime: 0, - // Solutions: make([]*models.Solution, 0, len(te.Solutions)), + Title: te.Title, + Text: te.Text, Codes: make([]*models.Code, 0, len(te.Codes)), TaskOrder: uint(order), } - // for _, s := range te.Solutions { - // task.Solutions = append(task.Solutions, &models.Solution{ - // Model: models.Model{ - // ID: uuid.New(), - // }, - // After: s.After, - // Text: s.Text, - // }) - // } + for _, ce := range te.Codes { + if ce.Id == nil { + u := uuid.New() + ce.Id = &u + } task.Codes = append(task.Codes, &models.Code{ Model: models.Model{ - ID: uuid.New(), + ID: *ce.Id, }, Code: ce.Code, Description: ce.Description, diff --git a/pkg/service/game.go b/pkg/service/game.go index 42ee0ca..373aa48 100644 --- a/pkg/service/game.go +++ b/pkg/service/game.go @@ -41,8 +41,7 @@ func (gs *Game) List(ctx context.Context) ([]*models.Game, error) { Order("created_at DESC"). Preload("Tasks"). Preload("Authors"). - Find(&games). - Limit(20). + Find(&games, "visible = true"). Error } @@ -54,15 +53,18 @@ func (gs *Game) GetTaskID(ctx context.Context, id uuid.UUID) (*models.Task, erro func (gs *Game) ListByAuthor(ctx context.Context, author *models.User) ([]*models.Game, error) { games := make([]*models.Game, 0) - - return games, gs.DB. + model := gs.DB. WithContext(ctx). - Model(&models.Game{}). - Preload("Authors", gs.DB.Where("id = ?", author.ID)). - Order("created_at DESC"). - Find(&games). - Limit(20). - Error + Model(&models.Game{}) + if author.Role == models.RoleCreator { + model.Preload("Authors", gs.DB.Where("id = ?", author.ID)) + } + + return games, + model. + Order("created_at DESC"). + Find(&games). + Error } func (gs *Game) CreateGame(ctx context.Context, game *models.Game) (*models.Game, error) { @@ -75,8 +77,10 @@ func (gs *Game) CreateGame(ctx context.Context, game *models.Game) (*models.Game func (gs *Game) UpdateGame(ctx context.Context, uid uuid.UUID, game *models.Game) (*models.Game, error) { game.ID = uid - return game, gs.DB. - Session(&gorm.Session{FullSaveAssociations: true}). + db := gs.DB + + return game, db.Debug(). + Session(&gorm.Session{FullSaveAssociations: true}).Omit("created_at"). Save(game). Error }