diff --git a/api/doc.go b/api/doc.go index 6299a0a..3507d5a 100644 --- a/api/doc.go +++ b/api/doc.go @@ -1,4 +1,5 @@ package api +//go:generate merger -i parts/common.yaml -i parts/game.yaml -i parts/admin.yaml -i parts/user.yaml -i parts/schemas.yaml -i parts/responses.yaml -o openapi.yaml //go:generate oapi-codegen -generate server,spec -package api -o ./server.go ./openapi.yaml //go:generate oapi-codegen -generate types -package api -o ./types.go ./openapi.yaml diff --git a/api/openapi.yaml b/api/openapi.yaml index 3b0dcb0..d91fe5a 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -1,237 +1,245 @@ -openapi: "3.1.0" - -info: - version: 1.0.0 - title: nQuest - -servers: - - url: /api - -paths: - # User routes - /user: - get: - responses: - 200: - $ref: "#/components/responses/userResponse" - 403: - $ref: "#/components/responses/errorResponse" - /user/login: - post: - security: [] - requestBody: - content: - application/json: - schema: - type: object - properties: - email: - type: string - password: - type: string - required: [email, password] - responses: - 200: - $ref: "#/components/responses/userResponse" - 400: - $ref: "#/components/responses/errorResponse" - /user/register: - post: - security: [] - requestBody: - 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" - 400: - $ref: "#/components/responses/errorResponse" - /user/logout: - post: - responses: - 204: - description: "success logout" - 400: - $ref: "#/components/responses/errorResponse" - - # Game routes - /games: - get: - responses: - 200: - $ref: "#/components/responses/gameListResponse" - /engine/{uid}: - get: - operationId: gameEngine - parameters: - - name: uid - in: path - required: true - schema: - type: string - format: uuid - responses: - 200: - $ref: "#/components/responses/taskResponse" - /engine/{uid}/code: - post: - operationId: enterCode - parameters: - - name: uid - in: path - required: true - schema: - type: string - format: uuid - requestBody: - content: - application/json: - schema: - type: object - properties: - code: - type: string - required: - - code - responses: - 200: - $ref: "#/components/responses/taskResponse" - /file/{uid}: - get: - operationId: getFile - parameters: - - name: uid - in: path - required: true - schema: - type: string - format: uuid - responses: - 200: - description: file - content: - "application/octet-stream": - schema: - type: string - format: binary - - # Admin routes - /admin/file/{quest}/upload: - post: - operationId: adminUploadFile - security: - - cookieAuth: [creator, admin] - parameters: - - name: quest - in: path - required: true - schema: - type: string - format: uuid - requestBody: - content: - multipart/form-data: - schema: - type: object - properties: - file: - type: string - format: binary - responses: - 200: - $ref: "#/components/responses/uploadResponse" - /admin/file/{quest}: - get: - operationId: adminListFiles - security: - - cookieAuth: [creator, admin] - parameters: - - name: quest - in: path - required: true - schema: - type: string - format: uuid - responses: - 200: - $ref: "#/components/responses/filesListResponse" - - /admin/games: - get: - operationId: adminListGames - responses: - 200: - $ref: "#/components/responses/gameListResponse" - post: - operationId: adminEditGame - security: - - cookieAuth: [creator, admin] - requestBody: - content: - application/json: - schema: - $ref: "#/components/schemas/gameEdit" - responses: - 200: - $ref: "#/components/responses/gameAdminResponse" - /admin/games/{uid}: - get: - operationId: adminGetGame - parameters: - - name: uid - in: path - required: true - schema: - type: string - format: uuid - security: - - cookieAuth: [creator, admin] - responses: - 200: - $ref: "#/components/responses/gameAdminResponse" - components: + responses: + errorResponse: + content: + application/json: + schema: + properties: + code: + type: integer + message: + type: string + required: + - code + - message + type: object + description: "" + filesListResponse: + content: + application/json: + schema: + items: + $ref: '#/components/schemas/fileItem' + type: array + description: "" + gameAdminResponse: + content: + application/json: + schema: + $ref: '#/components/schemas/gameEdit' + description: "" + gameListResponse: + content: + application/json: + schema: + items: + $ref: '#/components/schemas/gameView' + type: array + description: "" + gameResponse: + content: + application/json: + schema: + $ref: '#/components/schemas/gameView' + description: "" + taskResponse: + content: + application/json: + schema: + $ref: '#/components/schemas/taskView' + description: "" + uploadResponse: + content: + application/json: + schema: + properties: + uuid: + format: uuid + type: string + required: + - uuid + type: object + description: "" + userResponse: + content: + application/json: + schema: + $ref: '#/components/schemas/userView' + description: "" schemas: - userView: + codeEdit: + properties: + code: + type: string + description: + type: string + id: + format: uuid + type: string + required: + - code type: object + codeView: + properties: + code: + type: string + description: + type: string + type: object + fileItem: properties: id: - type: string format: uuid - username: type: string + originalName: + type: string + size: + type: integer + required: + - id + - originalName + - size + type: object + gameEdit: + properties: + description: + type: string + icon: + format: uuid + type: string + id: + format: uuid + type: string + points: + type: integer + tasks: + items: + $ref: '#/components/schemas/taskEdit' + type: array + title: + type: string + type: + $ref: '#/components/schemas/gameType' + visible: + type: boolean + required: + - visible + - title + - description + - type + - tasks + - points + type: object + gameType: + enum: + - virtual + - city + type: string + gameView: + properties: + authors: + items: + $ref: '#/components/schemas/userView' + type: array + createdAt: + type: string + description: + type: string + icon: + format: uuid + type: string + id: + format: uuid + type: string + points: + type: integer + taskCount: + type: integer + title: + type: string + type: + $ref: '#/components/schemas/gameType' + visible: + type: boolean + required: + - id + - title + - description + - type + - points + - taskCount + - createdAt + - authors + type: object + taskEdit: + properties: + codes: + items: + $ref: '#/components/schemas/codeEdit' + type: array + id: + format: uuid + type: string + text: + type: string + title: + type: string + required: + - id + - title + - text + - codes + type: object + taskView: + properties: + codes: + items: + $ref: '#/components/schemas/codeView' + type: array + message: + enum: + - ok_code + - invalid_code + - old_code + - next_level + - game_complete + type: string + text: + type: string + title: + type: string + required: + - title + - text + - codes + type: object + userView: + properties: email: type: string - experience: - type: integer - level: - type: integer expToCurrentLevel: type: integer expToNextLevel: type: integer + experience: + type: integer games: - type: array items: - $ref: "#/components/schemas/gameView" - role: + $ref: '#/components/schemas/gameView' + type: array + id: + format: uuid type: string + level: + type: integer + role: enum: - user - creator - admin + type: string + username: + type: string required: - id - username @@ -242,221 +250,201 @@ components: - expToNextLevel - games - role - gameView: type: object - properties: - id: - type: string - format: uuid - visible: - type: boolean - title: - type: string - description: - type: string - type: - $ref: "#/components/schemas/gameType" - points: - type: integer - taskCount: - type: integer - createdAt: - type: string - authors: - type: array - items: - $ref: "#/components/schemas/userView" - icon: - type: string - format: uuid - required: - - id - - title - - description - - type - - points - - taskCount - - createdAt - - authors - taskView: - type: object - properties: - message: - type: string - enum: - - ok_code - - invalid_code - - old_code - - next_level - - game_complete - title: - type: string - text: - type: string - codes: - type: array - items: - $ref: "#/components/schemas/codeView" - required: - - title - - text - - codes - codeView: - type: object - properties: - description: - type: string - code: - type: string - gameEdit: - type: object - properties: - id: - type: string - format: uuid - visible: - type: boolean - title: - type: string - description: - type: string - type: - $ref: "#/components/schemas/gameType" - tasks: - type: array - items: - $ref: "#/components/schemas/taskEdit" - points: - type: integer - icon: - type: string - format: uuid - required: - - visible - - title - - description - - type - - tasks - - points - taskEdit: - type: object - properties: - id: - type: string - format: uuid - title: - type: string - text: - type: string - codes: - type: array - items: - $ref: "#/components/schemas/codeEdit" - required: - - title - - text - - codes - codeEdit: - type: object - properties: - id: - type: string - format: uuid - description: - type: string - code: - type: string - required: - - code - gameType: - type: string - enum: - - virtual - - city - fileItem: - type: object - properties: - id: - type: string - format: uuid - originalName: - type: string - size: - type: integer - required: - - id - - originalName - - size - responses: - userResponse: - description: "" - content: - application/json: - schema: - $ref: "#/components/schemas/userView" - errorResponse: - description: "" - content: - application/json: - schema: - type: object - properties: - code: - type: integer - message: - type: string - required: [code, message] - gameListResponse: - description: "" - content: - application/json: - schema: - type: array - items: - $ref: "#/components/schemas/gameView" - gameResponse: - description: "" - content: - application/json: - schema: - $ref: "#/components/schemas/gameView" - gameAdminResponse: - description: "" - content: - application/json: - schema: - $ref: "#/components/schemas/gameEdit" - taskResponse: - description: "" - content: - application/json: - schema: - $ref: "#/components/schemas/taskView" - uploadResponse: - description: "" - content: - application/json: - schema: - type: object - properties: - uuid: - type: string - format: uuid - required: - - uuid - filesListResponse: - description: "" - content: - application/json: - schema: - type: array - items: - $ref: "#/components/schemas/fileItem" securitySchemes: cookieAuth: - type: apiKey in: cookie name: session - + type: apiKey +info: + title: nQuest + version: 1.0.0 +openapi: 3.1.0 +paths: + /admin/file/{quest}: + get: + operationId: adminListFiles + parameters: + - in: path + name: quest + required: true + schema: + format: uuid + type: string + responses: + 200: + $ref: '#/components/responses/filesListResponse' + security: + - cookieAuth: + - creator + - admin + /admin/file/{quest}/upload: + post: + operationId: adminUploadFile + parameters: + - in: path + name: quest + required: true + schema: + format: uuid + type: string + requestBody: + content: + multipart/form-data: + properties: + file: + format: binary + type: string + schema: null + type: object + responses: + 200: + $ref: '#/components/responses/uploadResponse' + security: + - cookieAuth: + - creator + - admin + /admin/games: + get: + operationId: adminListGames + responses: + 200: + $ref: '#/components/responses/gameListResponse' + post: + operationId: adminEditGame + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/gameEdit' + responses: + 200: + $ref: '#/components/responses/gameAdminResponse' + security: + - cookieAuth: + - creator + - admin + /admin/games/{uid}: + get: + operationId: adminGetGame + parameters: + - in: path + name: uid + required: true + schema: + format: uuid + type: string + responses: + 200: + $ref: '#/components/responses/gameAdminResponse' + security: + - cookieAuth: + - creator + - admin + /engine/{uid}: + get: + operationId: gameEngine + parameters: + - in: path + name: uid + required: true + schema: + format: uuid + type: string + responses: + 200: + $ref: '#/components/responses/taskResponse' + /engine/{uid}/code: + post: + operationId: enterCode + parameters: + - in: path + name: uid + required: true + schema: + format: uuid + type: string + requestBody: + content: + application/json: + schema: + properties: + code: + type: string + required: + - code + type: object + responses: + 200: + $ref: '#/components/responses/taskResponse' + /games: + get: + responses: + 200: + $ref: '#/components/responses/gameListResponse' + /user: + get: + responses: + 200: + $ref: '#/components/responses/userResponse' + 403: + $ref: '#/components/responses/errorResponse' + /user/login: + post: + requestBody: + content: + application/json: + schema: + properties: + email: + type: string + password: + type: string + required: + - email + - password + type: object + responses: + 200: + $ref: '#/components/responses/userResponse' + 400: + $ref: '#/components/responses/errorResponse' + security: [] + /user/logout: + post: + responses: + 204: + description: success logout + 400: + $ref: '#/components/responses/errorResponse' + /user/register: + post: + requestBody: + content: + application/json: + schema: + properties: + email: + type: string + password: + type: string + password2: + type: string + username: + type: string + required: + - username + - email + - password + - password2 + type: object + responses: + 200: + $ref: '#/components/responses/userResponse' + 400: + $ref: '#/components/responses/errorResponse' + security: [] security: - cookieAuth: [] +servers: + - url: /api diff --git a/api/parts/admin.yaml b/api/parts/admin.yaml new file mode 100644 index 0000000..cb82133 --- /dev/null +++ b/api/parts/admin.yaml @@ -0,0 +1,74 @@ +paths: + /admin/file/{quest}/upload: + post: + operationId: adminUploadFile + security: + - cookieAuth: [creator, admin] + parameters: + - name: quest + in: path + required: true + schema: + type: string + format: uuid + requestBody: + content: + multipart/form-data: + schema: + type: object + properties: + file: + type: string + format: binary + responses: + 200: + $ref: "#/components/responses/uploadResponse" + /admin/file/{quest}: + get: + operationId: adminListFiles + security: + - cookieAuth: [creator, admin] + parameters: + - name: quest + in: path + required: true + schema: + type: string + format: uuid + responses: + 200: + $ref: "#/components/responses/filesListResponse" + + /admin/games: + get: + operationId: adminListGames + responses: + 200: + $ref: "#/components/responses/gameListResponse" + post: + operationId: adminEditGame + security: + - cookieAuth: [creator, admin] + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/gameEdit" + responses: + 200: + $ref: "#/components/responses/gameAdminResponse" + /admin/games/{uid}: + get: + operationId: adminGetGame + parameters: + - name: uid + in: path + required: true + schema: + type: string + format: uuid + security: + - cookieAuth: [creator, admin] + responses: + 200: + $ref: "#/components/responses/gameAdminResponse" diff --git a/api/parts/common.yaml b/api/parts/common.yaml new file mode 100644 index 0000000..be981f2 --- /dev/null +++ b/api/parts/common.yaml @@ -0,0 +1,19 @@ +openapi: "3.1.0" + +info: + version: 1.0.0 + title: nQuest + +servers: + - url: /api + + +components: + securitySchemes: + cookieAuth: + type: apiKey + in: cookie + name: session + +security: + - cookieAuth: [] diff --git a/api/parts/game.yaml b/api/parts/game.yaml new file mode 100644 index 0000000..9629fa7 --- /dev/null +++ b/api/parts/game.yaml @@ -0,0 +1,43 @@ +paths: + /games: + get: + responses: + 200: + $ref: "#/components/responses/gameListResponse" + /engine/{uid}: + get: + operationId: gameEngine + parameters: + - name: uid + in: path + required: true + schema: + type: string + format: uuid + responses: + 200: + $ref: "#/components/responses/taskResponse" + /engine/{uid}/code: + post: + operationId: enterCode + parameters: + - name: uid + in: path + required: true + schema: + type: string + format: uuid + requestBody: + content: + application/json: + schema: + type: object + properties: + code: + type: string + required: + - code + responses: + 200: + $ref: "#/components/responses/taskResponse" + diff --git a/api/parts/responses.yaml b/api/parts/responses.yaml new file mode 100644 index 0000000..1da6894 --- /dev/null +++ b/api/parts/responses.yaml @@ -0,0 +1,66 @@ +components: + responses: + userResponse: + description: "" + content: + application/json: + schema: + $ref: "#/components/schemas/userView" + errorResponse: + description: "" + content: + application/json: + schema: + type: object + properties: + code: + type: integer + message: + type: string + required: [code, message] + gameListResponse: + description: "" + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/gameView" + gameResponse: + description: "" + content: + application/json: + schema: + $ref: "#/components/schemas/gameView" + gameAdminResponse: + description: "" + content: + application/json: + schema: + $ref: "#/components/schemas/gameEdit" + taskResponse: + description: "" + content: + application/json: + schema: + $ref: "#/components/schemas/taskView" + uploadResponse: + description: "" + content: + application/json: + schema: + type: object + properties: + uuid: + type: string + format: uuid + required: + - uuid + filesListResponse: + description: "" + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/fileItem" \ No newline at end of file diff --git a/api/parts/schemas.yaml b/api/parts/schemas.yaml new file mode 100644 index 0000000..ec63f25 --- /dev/null +++ b/api/parts/schemas.yaml @@ -0,0 +1,186 @@ +components: + schemas: + userView: + type: object + properties: + id: + type: string + format: uuid + username: + type: string + email: + type: string + experience: + type: integer + level: + type: integer + expToCurrentLevel: + type: integer + expToNextLevel: + type: integer + games: + type: array + items: + $ref: "#/components/schemas/gameView" + role: + type: string + enum: + - user + - creator + - admin + required: + - id + - username + - email + - experience + - level + - expToCurrentLevel + - expToNextLevel + - games + - role + gameView: + type: object + properties: + id: + type: string + format: uuid + visible: + type: boolean + title: + type: string + description: + type: string + type: + $ref: "#/components/schemas/gameType" + points: + type: integer + taskCount: + type: integer + createdAt: + type: string + authors: + type: array + items: + $ref: "#/components/schemas/userView" + icon: + type: string + format: uuid + required: + - id + - title + - description + - type + - points + - taskCount + - createdAt + - authors + taskView: + type: object + properties: + message: + type: string + enum: + - ok_code + - invalid_code + - old_code + - next_level + - game_complete + title: + type: string + text: + type: string + codes: + type: array + items: + $ref: "#/components/schemas/codeView" + required: + - title + - text + - codes + codeView: + type: object + properties: + description: + type: string + code: + type: string + gameEdit: + type: object + properties: + id: + type: string + format: uuid + visible: + type: boolean + title: + type: string + description: + type: string + type: + $ref: "#/components/schemas/gameType" + tasks: + type: array + items: + $ref: "#/components/schemas/taskEdit" + points: + type: integer + icon: + type: string + format: uuid + required: + - visible + - title + - description + - type + - tasks + - points + taskEdit: + type: object + properties: + id: + type: string + format: uuid + title: + type: string + text: + type: string + codes: + type: array + items: + $ref: "#/components/schemas/codeEdit" + required: + - id + - title + - text + - codes + codeEdit: + type: object + properties: + id: + type: string + format: uuid + description: + type: string + code: + type: string + required: + - code + gameType: + type: string + enum: + - virtual + - city + fileItem: + type: object + properties: + id: + type: string + format: uuid + originalName: + type: string + size: + type: integer + required: + - id + - originalName + - size \ No newline at end of file diff --git a/api/parts/user.yaml b/api/parts/user.yaml new file mode 100644 index 0000000..25947dd --- /dev/null +++ b/api/parts/user.yaml @@ -0,0 +1,57 @@ +paths: + /user: + get: + responses: + 200: + $ref: "#/components/responses/userResponse" + 403: + $ref: "#/components/responses/errorResponse" + /user/login: + post: + security: [] + requestBody: + content: + application/json: + schema: + type: object + properties: + email: + type: string + password: + type: string + required: [email, password] + responses: + 200: + $ref: "#/components/responses/userResponse" + 400: + $ref: "#/components/responses/errorResponse" + /user/register: + post: + security: [] + requestBody: + 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" + 400: + $ref: "#/components/responses/errorResponse" + /user/logout: + post: + responses: + 204: + description: "success logout" + 400: + $ref: "#/components/responses/errorResponse" diff --git a/api/server.go b/api/server.go index 421aafd..3b1054e 100644 --- a/api/server.go +++ b/api/server.go @@ -43,9 +43,6 @@ type ServerInterface interface { // (POST /engine/{uid}/code) EnterCode(ctx echo.Context, uid openapi_types.UUID) error - // (GET /file/{uid}) - GetFile(ctx echo.Context, uid openapi_types.UUID) error - // (GET /games) GetGames(ctx echo.Context) error @@ -179,24 +176,6 @@ func (w *ServerInterfaceWrapper) EnterCode(ctx echo.Context) error { return err } -// GetFile converts echo context to params. -func (w *ServerInterfaceWrapper) GetFile(ctx echo.Context) error { - var err error - // ------------- Path parameter "uid" ------------- - var uid openapi_types.UUID - - err = runtime.BindStyledParameterWithOptions("simple", "uid", ctx.Param("uid"), &uid, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter uid: %s", err)) - } - - ctx.Set(CookieAuthScopes, []string{}) - - // Invoke the callback with all the unmarshaled arguments - err = w.Handler.GetFile(ctx, uid) - return err -} - // GetGames converts echo context to params. func (w *ServerInterfaceWrapper) GetGames(ctx echo.Context) error { var err error @@ -283,7 +262,6 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL router.GET(baseURL+"/admin/games/:uid", wrapper.AdminGetGame) router.GET(baseURL+"/engine/:uid", wrapper.GameEngine) router.POST(baseURL+"/engine/:uid/code", wrapper.EnterCode) - router.GET(baseURL+"/file/:uid", wrapper.GetFile) router.GET(baseURL+"/games", wrapper.GetGames) router.GET(baseURL+"/user", wrapper.GetUser) router.POST(baseURL+"/user/login", wrapper.PostUserLogin) @@ -295,27 +273,26 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "H4sIAAAAAAAC/8xZS2/cNhD+KwXbo2JtHifdUsM1ghpBmzq9GAuDlsZrxhIpkyPHW0P/vRhS0kor6uGN", - "ssnJWnI4j28enKGfWayyXEmQaFj0zDSYXEkD9gdorfSnaoUWYiURJNInz/NUxByFkuEXoyStmfgOMk5f", - "uVY5aBSOT6wSexy3ObCICYmwAc3KgGVgDN+0Nw1qITesLAOm4aEQGhIWXTkWO/p1UNOrmy8QIyvpQAIm", - "1iInnVjEiP+tSMFcCIMHWSEQMmvAbxpuWcR+DXdghY7MhCTiA0JG4iqduNZ8O6TShmfwPsmEPEilMU2I", - "81kicEzy98WCJPwr4OtcLJCb+8VhIKZOCb/QIk8VTxYI66IQCf29VTrjyCK3EExEsiWaG7+FAb04QMR0", - "GKAyqNg0qWtjaiqpa2v3GHr2DwHNCuuDFtgNa8tS+pUeIU2K94TMsiVgSouNkDz9yDO/Tkb8562QezBY", - "7h1m1VEfNE056Gk96aHYbUzaNdP8XInqeunfAJStZnaBIWpX4vYLTMBQYOpH1y1Ml65LoisD9iiMuOnw", - "ulEqBS57Dqkpa+nd6Kok10Y2QAx567LSE2SROe4aC56ygMUCt61jO9OaittzMi/wTun50O6qQh/aWANH", - "SN7jYQl/5HA6VYXEge0fECPOiLHwqAxqa9/GPGh86QucJiW8FXC+/5tK7/H/TMcgPPkjZAj2PaRqkCyf", - "oNJ/yObhqv8ym4divtWa1vmo7q+rPlTIR56KpP6p0uZTwhNep/AIlLYUMNckNwUEb/4eDbImvXuQQcZF", - "6tUBnvJLdVpoDRIvrEnepLJkH+Fpgga0ABkPDAKElFmgz5wdq+mwrlqlHbcTdHVCKvri1L573Umk0n/J", - "+6pCQx5UXugAVSvpc0QP9RrCSn1PgxkwA3GhBW7/IRjrfFH3At4XeGfBpxbQLVEoW0OYAWNa5SpiPBd/", - "QtXTC3mrrLEuXJn8uwBDkfgI2riW8vXJ6mRlu6AcJM8Fi9jbk9cnKyp8HO+sGqGF1M5S4fMDsShpeQM2", - "OShabaP7IWERs8MTzTF/0HBnuWieAQJddleVEcR5Z8JDpdTOAagLCFr98lQ7ug66o/Gb1WooSBu6sD99", - "tr1glW3jf9UPsXJNJzzohG6KsdmszBBKny0R4XRMmCyb31Wy3ZtYsiJFkXONIbF5lXD0DFVkZEfSjZBc", - "b70TgmeEermT9sbBb/JQU8XGA/e8ztQXK9sb4a3RIxFAN/q5qzDDjlnmyaE81KDua8i3OyB8LkQyUT/O", - "oYZlOi1cqB+5diwGDMiNkDCBCWFxZgl/XkQ6j0Uu3jrGhfXE70+HM4mgT12TdiQLD8u3Wc8Y895HDkxK", - "H9Lu8hkPIsDZV813iqABkFWMgK8MauBZF+zpa6b3RGZvqAqV8YJfFZnlSj2JtN3oiMTPrls94CJsvzWW", - "AXu3ejt9qPsvgpaKYao2Qg6n41/KWFUvLNlS2TI8y+TcmK9KJ9O5VHfizYnF8qqP8OrlCHcugXUHb1Xg", - "LMCJrqf/u97LIDNFHIMxv1SsD9V4p6OGjTDo4ndcy0815Q+NjN3mG+/u/HHPM+k1cttSfu5YG+1A1lSU", - "DejHuuwXOmURC2nkK9fl/wEAAP//MtUIV2ocAAA=", + "H4sIAAAAAAAC/8xYS3OkNhD+KyklR2JmHyduG5fj2oprK9l4c3FNuWRoj7UGCUvNrCcu/nuqJWBgEA+P", + "iXdPHkPTj6+/bnXricUqy5UEiYZFT0yDyZU0YP8BrZX+XD2hB7GSCBLpJ8/zVMQchZLhV6MkPTPxHWSc", + "fuVa5aBROD2xSuznuMuBRUxIhA1oVgYsA2P4pv3SoBZyw8oyYBoeCqEhYdGVU7GXXwe1vLr5CjGykj5I", + "wMRa5OQTixjpvxUpmAth8KgoBEJmA/hFwy2L2M/hHqzQiZmQTHxEyMhc5RPXmu+GXNrwDD4kmZBHuTTm", + "CWk+SwSOWf5/sSAL/wj4NhcL5OZ+cRhIqXPCb7TIU8WTBWhdFCKhv7dKZxxZ5B4EE0y2QnP5WxjQiwNE", + "SocBKoNKTVO6llNTRV1He6DQ8/4Y0KyxPmiBfWFjWcq/0mOkKfGekVmxBExpsRGSp5945vfJiH+9HfIA", + "Bqu9o6z61AdN0w56Xk9mKHYvJuOaGX6uRHW89E8AqlYzu8GQtGtxhw0mYCgw9aPrHky3rkuSKwO2FUbc", + "dHTdKJUCl72E1JK19S67Kst1kA0QQ9m6rPwEWWROu8aCpyxgscBd67N9aE3H7SWZF3in9Hxo912hD22s", + "gSMkH/C4gn9lOp2qQuLA6+/AERfEGD2qgNretzEPmlz6iNOUhLcDzs9/0+k9+Z+ZGIRHP0OGYB9DyioL", + "qiCGAh9u/c8LfIj4rfm0Lkp1f10No0JueSqS+l+VNj8lPOJ1Clug2iXWXJPdFBC8RfxS3GZD1tR4DzLI", + "uEi9PsBjfqlOC61B4oUNyVtZVuwTPE7IgBYg44FtgJAyCwybswmbDvuqVdpJO0FXV6WiX5xmeG86SVT6", + "T3of4RvxoMpCB6jaSV8ieqjXEFbue6bMgBmICy1w9zfBWNeLuhfwocA7Cz7Nge4RUdkGwgwY0+pZEeO5", + "+AOqwV7IW2WDdXRl8q8CDDFxC9q4ufLNyepkZUehHCTPBYvYu5M3JyvqfhzvrBuhhdQuVOHTA6ko6fEG", + "bHEQW+20+zFhEbMbFC0zv9OGZ7VongECnXhXVRCkeR/CQ+XUPgGoCwhaQ/PUTLoOuvvx29VqiKSNXNhf", + "QdtZsM628b/qU6xc0xcedEK3ythqVmYIpS9WiHB6TZismt9UsjtYW7IiRZFzjSGp+TXh6NmsKMiOpRsh", + "ud551wTPHvX8JB3shC/KUNPFxol7Xlfqs53t7fE26BEG0LF+7jrMcGKWuXcojw2oeyXy8gSET4VIJvrH", + "OdSwTJeFo/or947FgAG5ERImMCEszqzgj4tI58bI8a0TXFiv/f5yOJMI+tQNaa8U4XH1NusuY94lyZFF", + "6UN6vLVV5bRcUyOTdu4asfjFzWVHtPz21VoZsPerd9MfdW/EWy6GqdoIOUy8P5Wxrl5YsaV4MTy159yY", + "b0on06ypZ87mi8UY1Ed49XyEO+1u3cFbFTgLcJLr+f++dxHGTBHHYMxPlepjPd77qGEjDDr+jnv5uZb8", + "rszYv3zrfTt/sfHsNI3dtpUfm2ujZ+2a2rsBva0PkEKnLGIhLTfluvwvAAD//8w97C1ZGwAA", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/api/types.go b/api/types.go index e02bb79..8c88a5d 100644 --- a/api/types.go +++ b/api/types.go @@ -84,10 +84,10 @@ type GameView struct { // TaskEdit defines model for taskEdit. type TaskEdit struct { - Codes []CodeEdit `json:"codes"` - Id *openapi_types.UUID `json:"id,omitempty"` - Text string `json:"text"` - Title string `json:"title"` + Codes []CodeEdit `json:"codes"` + Id openapi_types.UUID `json:"id"` + Text string `json:"text"` + Title string `json:"title"` } // TaskView defines model for taskView. diff --git a/frontend/.eslintrc.cjs b/frontend/.eslintrc.cjs index d19fd89..f6e8ac6 100644 --- a/frontend/.eslintrc.cjs +++ b/frontend/.eslintrc.cjs @@ -1,33 +1,34 @@ module.exports = { - env: { - browser: true, - es2021: true - }, - extends: [ - 'standard', - 'plugin:react/recommended', - 'plugin:react/jsx-runtime' - ], - overrides: [ - { - env: { - node: true - }, - files: [ - '.eslintrc.{js,cjs}' - ], - parserOptions: { - sourceType: 'script' - } + env: { + browser: true, + es2021: true + }, + extends: [ + 'standard', + 'plugin:react/recommended', + 'plugin:react/jsx-runtime' + ], + overrides: [ + { + env: { + node: true + }, + files: [ + '.eslintrc.{js,cjs}' + ], + parserOptions: { + sourceType: 'script' + } + } + ], + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module' + }, + plugins: [ + 'react' + ], + rules: { + indent: ['error', 4] } - ], - parserOptions: { - ecmaVersion: 'latest', - sourceType: 'module' - }, - plugins: [ - 'react' - ], - rules: { - } } diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 693be2b..e37044d 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -18,81 +18,81 @@ import EditQuest from './pages/admin/Quest' import AdminQuest from './pages/admin/Quests' const router = createBrowserRouter( - createRoutesFromElements( - } - loader={async () => ajax('/api/user').catch(x => { console.log(x); return null })} - > - } - /> - } - loader={() => ajax('/api/games').catch(x => { console.log(x); return null })} - /> - } /> - } /> - } /> - } - loader={({ params }) => ajax(`/api/engine/${params.gameId}`).catch(x => { console.log(x); return null })} - /> - } - loader={() => ({ quest: null, files: [] })} - /> - } - loader={async ({ params }) => { - const quest = await ajax(`/api/admin/games/${params.gameId}`) - const files = await ajax(`/api/admin/file/${params.gameId}`) + createRoutesFromElements( + } + loader={async () => ajax('/api/user').catch(x => { console.log(x); return null })} + > + } + /> + } + loader={() => ajax('/api/games').catch(x => { console.log(x); return null })} + /> + } /> + } /> + } /> + } + loader={({ params }) => ajax(`/api/engine/${params.gameId}`).catch(x => { console.log(x); return null })} + /> + } + loader={() => ({ quest: null, files: [] })} + /> + } + loader={async ({ params }) => { + const quest = await ajax(`/api/admin/games/${params.gameId}`) + const files = await ajax(`/api/admin/file/${params.gameId}`) - return { quest, files } - } - } - /> - } - loader={() => ajax('/api/admin/games')} - /> + return { quest, files } + } + } + /> + } + loader={() => ajax('/api/admin/games')} + /> - } /> - - ) + } /> + + ) ) function App () { - return ( - - ) + return ( + + ) } function Auth (props) { - const baseUser = useRouteLoaderData('root') - const { user } = UserProvider.useContainer() - const { hasRole } = useRole() - const location = useLocation() - if (!user && !baseUser) { - return - } - if (props.role && !hasRole(props.role)) { - return null - } + const baseUser = useRouteLoaderData('root') + const { user } = UserProvider.useContainer() + const { hasRole } = useRole() + const location = useLocation() + if (!user && !baseUser) { + return + } + if (props.role && !hasRole(props.role)) { + return null + } - return props.children + return props.children } Auth.propTypes = { - children: PropTypes.any, - role: PropTypes.string + children: PropTypes.any, + role: PropTypes.string } export default App diff --git a/frontend/src/components/Layout.jsx b/frontend/src/components/Layout.jsx index 2f67639..4d4f60e 100644 --- a/frontend/src/components/Layout.jsx +++ b/frontend/src/components/Layout.jsx @@ -8,82 +8,82 @@ import { Content, Header } from 'antd/es/layout/layout' import { useRole } from '../utils/roles' const AppLayout = () => { - const params = useLoaderData() - const navigate = useNavigate() - const location = useLocation() - useEffect(() => { - setUser(params) - }, [params]) - const { user, setUser } = UserProvider.useContainer() - const { hasRole } = useRole() + const params = useLoaderData() + const navigate = useNavigate() + const location = useLocation() + useEffect(() => { + setUser(params) + }, [params]) + const { user, setUser } = UserProvider.useContainer() + const { hasRole } = useRole() - const logout = () => { - ajax('/api/user/logout', { - method: 'POST' - }) - .then(() => { setUser(null); navigate('/login') }) - .catch(() => { setUser(null); navigate('/login') }) - } - let items = [ - { key: 'login', label: 'Вход', link: '/login' }, - { key: 'register', label: 'Регистрация', link: '/register' } - ] - if (user != null) { - items = [ - { - key: 'quests', - label: 'Квесты', - link: '/quests' - }, - { - key: 'me', - label: `${user.username} [${user.level}]`, - link: '/me' - }, - { - key: 'logout', - label: 'Выход', - onClick: logout - } + const logout = () => { + ajax('/api/user/logout', { + method: 'POST' + }) + .then(() => { setUser(null); navigate('/login') }) + .catch(() => { setUser(null); navigate('/login') }) + } + let items = [ + { key: 'login', label: 'Вход', link: '/login' }, + { key: 'register', label: 'Регистрация', link: '/register' } ] + if (user != null) { + items = [ + { + key: 'quests', + label: 'Квесты', + link: '/quests' + }, + { + key: 'me', + label: `${user.username} [${user.level}]`, + link: '/me' + }, + { + key: 'logout', + label: 'Выход', + onClick: logout + } + ] - if (hasRole('creator')) { - items.push({ - key: 'admin/quests', - label: 'Управление квестами', - link: '/admin/quests' - }) + if (hasRole('creator')) { + items.push({ + key: 'admin/quests', + label: 'Управление квестами', + link: '/admin/quests' + }) + } + + if (hasRole('admin')) { + items.push({ + key: 'admin', + label: 'Админка', + link: '/admin' + }) + } + } + const menuHandler = (x) => { + const item = items.find(y => y.key === x.key) + if (item.link) { navigate(item.link) } } - if (hasRole('admin')) { - items.push({ - key: 'admin', - label: 'Админка', - link: '/admin' - }) - } - } - const menuHandler = (x) => { - const item = items.find(y => y.key === x.key) - if (item.link) { navigate(item.link) } - } - - return ( -
- - } - style={{ width: '100%' }} - /> -
- - - -
) + return ( +
+ + } + style={{ width: '100%' }} + /> +
+ + + +
) } export default AppLayout diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx index 91b0efe..6607107 100644 --- a/frontend/src/main.jsx +++ b/frontend/src/main.jsx @@ -12,27 +12,27 @@ const { darkAlgorithm } = theme console.log(import.meta.env.VITE_VERSION) ReactDOM.createRoot(document.getElementById('root')).render( - - - - - - - - - + + + + + + + + + ) diff --git a/frontend/src/pages/Engine.jsx b/frontend/src/pages/Engine.jsx index cc5c62e..5655920 100644 --- a/frontend/src/pages/Engine.jsx +++ b/frontend/src/pages/Engine.jsx @@ -6,65 +6,65 @@ import Title from 'antd/es/typography/Title' import { Alert, App, Button, Card, Col, Form, Input, List, Row } from 'antd' const Engine = () => { - const params = useParams() - const loadedTask = useLoaderData() - const [task, setTask] = useState(loadedTask) - const { message } = App.useApp() + const params = useParams() + const loadedTask = useLoaderData() + const [task, setTask] = useState(loadedTask) + const { message } = App.useApp() - useEffect(() => { - if (!task) { - return - } - switch (task.message) { - case 'invalid_code': - message.error('Неверный код') - break - case 'old_code': - message.error('Этот код уже вводился') - break - case 'next_level': - message.success('Переход на новый уровень') - break - case 'ok_code': - message.success('Код принят, ищите оставшиеся') - break - } - }, [task]) - - const [form] = Form.useForm() - const onFinish = ({ code }) => { - ajax(`/api/engine/${params.gameId}/code`, { - method: 'POST', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ code }) - }) - .then((x) => { - if (x != null) { - setTask(x) - form.setFieldsValue({ code: '' }) + useEffect(() => { + if (!task) { + return } - }).catch(e => { - console.warn(e) - }) - } + switch (task.message) { + case 'invalid_code': + message.error('Неверный код') + break + case 'old_code': + message.error('Этот код уже вводился') + break + case 'next_level': + message.success('Переход на новый уровень') + break + case 'ok_code': + message.success('Код принят, ищите оставшиеся') + break + } + }, [task]) - if (task && task.message === 'game_complete') { - return (
+ const [form] = Form.useForm() + const onFinish = ({ code }) => { + ajax(`/api/engine/${params.gameId}/code`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ code }) + }) + .then((x) => { + if (x != null) { + setTask(x) + form.setFieldsValue({ code: '' }) + } + }).catch(e => { + console.warn(e) + }) + } + + if (task && task.message === 'game_complete') { + return (
К списку игр
) - } - if (!task) { - return (
+ } + if (!task) { + return (
К списку игр
) - } + } - return (<> + return (<> {task.title} diff --git a/frontend/src/pages/Index.jsx b/frontend/src/pages/Index.jsx index b55c674..d671bbc 100644 --- a/frontend/src/pages/Index.jsx +++ b/frontend/src/pages/Index.jsx @@ -5,9 +5,9 @@ import { UserProvider } from '../store/user' const { Title, Paragraph } = Typography const Index = () => { - const { user } = UserProvider.useContainer() - const navigate = useNavigate() - return (<> + const { user } = UserProvider.useContainer() + const navigate = useNavigate() + return (<> NQuest Привет! Это платформа для ARG игр. @@ -15,11 +15,11 @@ const Index = () => { А если ты знаешь зачем пришёл, то добро пожаловать! {!user - ? ( + ? ( ) - : ()} + : ()} ) } diff --git a/frontend/src/pages/Login.jsx b/frontend/src/pages/Login.jsx index f53ae92..6a640f4 100644 --- a/frontend/src/pages/Login.jsx +++ b/frontend/src/pages/Login.jsx @@ -5,45 +5,45 @@ import { ajax } from '../utils/fetch' import { Alert, Button, Form, Input } from 'antd' const Login = () => { - const { user, setUser } = UserProvider.useContainer() - const { state } = useLocation() - const [error, setError] = useState(null) - const navigate = useNavigate() - const [form] = Form.useForm() + const { user, setUser } = UserProvider.useContainer() + const { state } = useLocation() + const [error, setError] = useState(null) + const navigate = useNavigate() + const [form] = Form.useForm() - useEffect(() => { - if (user) { - navigate(state && state.from ? state.from : '/') + useEffect(() => { + if (user) { + navigate(state && state.from ? state.from : '/') + } + }, [user]) + + const onFinish = (values) => { + ajax('/api/user/login', { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json' + }, + body: JSON.stringify(values) + }) + .then(setUser) + .catch(({ message }) => setError('Проверьте e-mail и пароль')) } - }, [user]) - const onFinish = (values) => { - ajax('/api/user/login', { - method: 'POST', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json' - }, - body: JSON.stringify(values) - }) - .then(setUser) - .catch(({ message }) => setError('Проверьте e-mail и пароль')) - } - - return (<> + return (<>

Вход

{error ? : null}
@@ -51,14 +51,14 @@ const Login = () => { label='E-mail' name='email' rules={[ - { - required: true, - message: 'Обязательное поле' - }, - { - type: 'email', - message: 'E-mail некорректный' - } + { + required: true, + message: 'Обязательное поле' + }, + { + type: 'email', + message: 'E-mail некорректный' + } ]} > { label='Пароль' name='password' rules={[ - { - required: true, - message: 'Обязательное поле' - } + { + required: true, + message: 'Обязательное поле' + } ]} > - ) - } + if (user) { + questAction = (user.games.find(x => x.id === item.id) + ? Вы уже прошли этот квест + : + ) + } - return ( + return ( { {questAction} - ) + ) } export default Quests diff --git a/frontend/src/pages/Register.jsx b/frontend/src/pages/Register.jsx index 2ae952a..4bb9d80 100644 --- a/frontend/src/pages/Register.jsx +++ b/frontend/src/pages/Register.jsx @@ -5,42 +5,42 @@ import { ajax } from '../utils/fetch' import { Alert, Button, Form, Input } from 'antd' const Register = () => { - const { user, setUser } = UserProvider.useContainer() - const [error, setError] = useState(null) - const navigate = useNavigate() + const { user, setUser } = UserProvider.useContainer() + const [error, setError] = useState(null) + const navigate = useNavigate() - useEffect(() => { - if (user) { - navigate('/') + useEffect(() => { + if (user) { + navigate('/') + } + }, [user]) + + const onFinish = (values) => { + ajax('/api/user/register', { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json' + }, + body: JSON.stringify(values) + }) + .then(setUser) + .catch(({ message }) => setError('Ошибка регистрации')) } - }, [user]) - const onFinish = (values) => { - ajax('/api/user/register', { - method: 'POST', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json' - }, - body: JSON.stringify(values) - }) - .then(setUser) - .catch(({ message }) => setError('Ошибка регистрации')) - } - - return (<> + return (<>

Регистрация

{error ? : null} @@ -48,10 +48,10 @@ const Register = () => { label="Имя пользователя" name="username" rules={[ - { - required: true, - message: 'Обязательное поле' - } + { + required: true, + message: 'Обязательное поле' + } ]} > @@ -60,14 +60,14 @@ const Register = () => { label="E-mail" name="email" rules={[ - { - required: true, - message: 'Обязательное поле' - }, - { - type: 'email', - message: 'E-mail некорректный' - } + { + required: true, + message: 'Обязательное поле' + }, + { + type: 'email', + message: 'E-mail некорректный' + } ]} help="Не видно другим пользователям" > @@ -80,10 +80,10 @@ const Register = () => { label="Пароль" name="password" rules={[ - { - required: true, - message: 'Обязательное поле' - } + { + required: true, + message: 'Обязательное поле' + } ]} > @@ -94,26 +94,26 @@ const Register = () => { dependencies={['password']} hasFeedback rules={[ - { - required: true, - message: 'Обязательное поле' - }, - ({ getFieldValue }) => ({ - validator (_, value) { - if (!value || getFieldValue('password') === value) { - return Promise.resolve() - } - return Promise.reject(new Error('Пароли отличаются!')) - } - }) + { + required: true, + message: 'Обязательное поле' + }, + ({ getFieldValue }) => ({ + validator (_, value) { + if (!value || getFieldValue('password') === value) { + return Promise.resolve() + } + return Promise.reject(new Error('Пароли отличаются!')) + } + }) ]} >
- - - - - - - - - - - - - - - - {quest.icon ? : null} - - - - - - - Полевой - Виртуальный - - - - - - - {(tasks, { add, remove }) => ( - <> - {tasks.map(renderTaskForm(remove))} - - - - - )} - - + + +
+ + + + + + + + + + + + + {quest.icon ? : null} + + + + + + + Полевой + Виртуальный + + + + + + + {(tasks, { add, remove }) => ( + <> + {tasks.map(renderTaskForm(remove))} + + + + + )} + + Файлы @@ -132,41 +132,40 @@ const Quest = () => { action={`/api/admin/file/${quest.id}/upload`} listType='picture' maxCount={10} - itemRender={renderFile} + itemRender={(e, file) => renderFile(e, file, quest)} > Ранее загруженные файлы: - + renderFileItem(x, quest)} /> -
- setPreview(false)}>Закрыть} - width={'80%'} - centered - > - ( - - - {task.text} - + + setPreview(false)}>Закрыть} + width={'80%'} + centered + > + ( + + + {task.text} + Коды: -
    +
      {task.codes.map(c =>
    • {c.code}
    • )} -
    - - } - /> - - )} /> - - - ) +
+ + } + /> +
+ )} /> +
+ ) } // eslint-disable-next-line react/display-name @@ -184,11 +183,11 @@ const renderTaskForm = remove => task => ( cancelText='Нет' > ]} - > + > @@ -203,9 +202,9 @@ const renderTaskForm = remove => task => ( <> {codes.map(renderCodeForm(codesOpts.remove))} - + )} @@ -219,19 +218,19 @@ const renderCodeForm = remove => code => ( key={code.key} style={{ marginBottom: 8 }} actions={[ - // remove(code.name)} - // okText='Да' - // cancelText='Нет' - // > + // remove(code.name)} + // okText='Да' + // cancelText='Нет' + // > - // + // ]} - > + > @@ -244,21 +243,24 @@ const renderCodeForm = remove => code => ( ) -const renderFile = (e, file) => ( -
- {e} - {file && file.response && file.response.uuid - ? <>Код для вставки:
![](/api/file/{file.response.uuid})
- : null} -
-) +const renderFile = (e, file, quest) => { + console.log(file) + return ( +
+ {e} + {file && file.response && file.response.uuid + ? <>Код для вставки:
![](/file/{quest.id}/{file.originFileObj.name})
+ : null} +
+ ) +} -const renderFileItem = (file) => ( +const renderFileItem = (file, quest) => ( } + avatar={} title={file.originalName} - description={<>Код для вставки:
![](/api/file/{file.id})
} + description={<>Код для вставки:
![](/file/{quest.id}/{file.originalName})
} />
diff --git a/frontend/src/pages/admin/Quests.jsx b/frontend/src/pages/admin/Quests.jsx index 0c310a3..8084a13 100644 --- a/frontend/src/pages/admin/Quests.jsx +++ b/frontend/src/pages/admin/Quests.jsx @@ -4,9 +4,9 @@ import { Link, useLoaderData } from 'react-router-dom' const { Title } = Typography const Quests = () => { - const quests = useLoaderData() + const quests = useLoaderData() - return ( + return ( <> Управление своими квестами Создать новый квест @@ -14,29 +14,29 @@ const Quests = () => { dataSource={quests} rowKey={'id'} columns={[ - { - title: 'Опубликован?', - dataIndex: 'visible', - key: 'visible', - render: visible => visible ? 'Да' : 'Нет' - }, - { - title: 'Название', - dataIndex: 'title', - key: 'title', - render: (title, q) => {title} - }, - { - title: 'Тип', - dataIndex: 'type', - key: 'type', - render: type => type === 'virtual' ? 'Виртуальный' : 'Полевой' - } + { + title: 'Опубликован?', + dataIndex: 'visible', + key: 'visible', + render: visible => visible ? 'Да' : 'Нет' + }, + { + title: 'Название', + dataIndex: 'title', + key: 'title', + render: (title, q) => {title} + }, + { + title: 'Тип', + dataIndex: 'type', + key: 'type', + render: type => type === 'virtual' ? 'Виртуальный' : 'Полевой' + } ]} /> - ) + ) } export default Quests diff --git a/frontend/src/store/provider.js b/frontend/src/store/provider.js index d957bb9..09d707c 100644 --- a/frontend/src/store/provider.js +++ b/frontend/src/store/provider.js @@ -1,5 +1,5 @@ import { UserProvider } from './user' export const store = [ - UserProvider.Provider + UserProvider.Provider ] diff --git a/frontend/src/store/user.js b/frontend/src/store/user.js index 2b32467..8868967 100644 --- a/frontend/src/store/user.js +++ b/frontend/src/store/user.js @@ -2,8 +2,8 @@ import { useState } from 'react' import { createContainer } from 'unstated-next' const useUser = () => { - const [user, setUser] = useState(null) - return { user, setUser } + const [user, setUser] = useState(null) + return { user, setUser } } export const UserProvider = createContainer(useUser) diff --git a/frontend/src/utils/fetch.js b/frontend/src/utils/fetch.js index 52fad33..910aa26 100644 --- a/frontend/src/utils/fetch.js +++ b/frontend/src/utils/fetch.js @@ -1,10 +1,10 @@ export const ajax = async (path, params) => { - return fetch(path, params) - .then(r => { - if (r.status < 200 || r.status >= 300) { - throw Error(r.statusText) - } - return r - }) - .then(r => r.json()) + return fetch(path, params) + .then(r => { + if (r.status < 200 || r.status >= 300) { + throw Error(r.statusText) + } + return r + }) + .then(r => r.json()) } diff --git a/frontend/src/utils/roles.js b/frontend/src/utils/roles.js index 9a34442..db711c5 100644 --- a/frontend/src/utils/roles.js +++ b/frontend/src/utils/roles.js @@ -1,24 +1,24 @@ import { UserProvider } from '../store/user' const roleHierarchy = { - user: { - user: true - }, - creator: { - user: true, - creator: true - }, - admin: { - user: true, - creator: true, - admin: true - } + user: { + user: true + }, + creator: { + user: true, + creator: true + }, + admin: { + user: true, + creator: true, + admin: true + } } export const useRole = () => { - const { user } = UserProvider.useContainer() + const { user } = UserProvider.useContainer() - return { - hasRole: (role) => user && user.role && !!roleHierarchy[user.role][role] - } + return { + hasRole: (role) => user && user.role && !!roleHierarchy[user.role][role] + } } diff --git a/frontend/src/utils/uuid.js b/frontend/src/utils/uuid.js index d876381..f3f0120 100644 --- a/frontend/src/utils/uuid.js +++ b/frontend/src/utils/uuid.js @@ -1,5 +1,5 @@ export function uuidv4 () { - return '10000000-1000-4000-8000-100000000000'.replace(/[018]/g, c => - (+c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> +c / 4).toString(16) - ) + return '10000000-1000-4000-8000-100000000000'.replace(/[018]/g, c => + (+c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> +c / 4).toString(16) + ) }; diff --git a/frontend/vite.config.js b/frontend/vite.config.js index c1d78c6..cf71160 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -3,88 +3,95 @@ import react from '@vitejs/plugin-react' import { VitePWA } from 'vite-plugin-pwa' const manifest = { - registerType: 'autoUpdate', - includeAssets: ['assets/icon.png', 'assets/logo.png'], - workbox: { - cleanupOutdatedCaches: true - }, - manifest: { - name: 'NQuest', - short_name: 'NQuest', - description: 'NQuest - платформа для ARG игр.', - icons: [ - { - src: 'assets/icons/icon-72x72.png', - sizes: '72x72', - type: 'image/png', - purpose: 'maskable any' - }, - { - src: 'assets/icons/icon-96x96.png', - sizes: '96x96', - type: 'image/png', - purpose: 'maskable any' - }, - { - src: 'assets/icons/icon-128x128.png', - sizes: '128x128', - type: 'image/png', - purpose: 'maskable any' - }, - { - src: 'assets/icons/icon-144x144.png', - sizes: '144x144', - type: 'image/png', - purpose: 'maskable any' - }, - { - src: 'assets/icons/icon-152x152.png', - sizes: '152x152', - type: 'image/png', - purpose: 'maskable any' - }, - { - src: 'assets/icons/icon-192x192.png', - sizes: '192x192', - type: 'image/png', - purpose: 'maskable any' - }, - { - src: 'assets/icons/icon-384x384.png', - sizes: '384x384', - type: 'image/png', - purpose: 'maskable any' - }, - { - src: 'assets/icons/icon-512x512.png', - sizes: '512x512', - type: 'image/png', - purpose: 'maskable any' - } - ], - theme_color: '#59FBEA', - background_color: '#171e26', - display: 'standalone', - scope: '/', - start_url: '/quests' - } + registerType: 'autoUpdate', + includeAssets: ['assets/icon.png', 'assets/logo.png'], + workbox: { + cleanupOutdatedCaches: true + }, + base: 'assets', + manifest: { + name: 'NQuest', + short_name: 'NQuest', + description: 'NQuest - платформа для ARG игр.', + icons: [ + { + src: 'assets/icons/icon-72x72.png', + sizes: '72x72', + type: 'image/png', + purpose: 'maskable any' + }, + { + src: 'assets/icons/icon-96x96.png', + sizes: '96x96', + type: 'image/png', + purpose: 'maskable any' + }, + { + src: 'assets/icons/icon-128x128.png', + sizes: '128x128', + type: 'image/png', + purpose: 'maskable any' + }, + { + src: 'assets/icons/icon-144x144.png', + sizes: '144x144', + type: 'image/png', + purpose: 'maskable any' + }, + { + src: 'assets/icons/icon-152x152.png', + sizes: '152x152', + type: 'image/png', + purpose: 'maskable any' + }, + { + src: 'assets/icons/icon-192x192.png', + sizes: '192x192', + type: 'image/png', + purpose: 'maskable any' + }, + { + src: 'assets/icons/icon-384x384.png', + sizes: '384x384', + type: 'image/png', + purpose: 'maskable any' + }, + { + src: 'assets/icons/icon-512x512.png', + sizes: '512x512', + type: 'image/png', + purpose: 'maskable any' + } + ], + theme_color: '#59FBEA', + background_color: '#171e26', + display: 'standalone', + scope: '/', + start_url: '/quests' + } } // https://vitejs.dev/config/ export default defineConfig({ - plugins: [react(), splitVendorChunkPlugin(), VitePWA(manifest)], - server: { - proxy: { - '/api': { - target: 'http://localhost:8000', - changeOrigin: true, - secure: false, - ws: false - } - } - }, - build: { + plugins: [react(), splitVendorChunkPlugin(), VitePWA(manifest)], + server: { + proxy: { + '/api': { + target: 'http://localhost:8000', + changeOrigin: true, + secure: false, + ws: false + }, + '/file': { + target: 'http://localhost:8000', + changeOrigin: true, + secure: false, + ws: false + } + } + }, + build: { // generate .vite/manifest.json in outDir - manifest: true - } + manifest: true + } }) diff --git a/main.go b/main.go index 40c246d..36954b2 100644 --- a/main.go +++ b/main.go @@ -127,12 +127,12 @@ func main() { api.RegisterHandlersWithBaseURL(codegen, handler, "/api") - e.FileFS("/", "index.html", distIndexHtml) - e.FileFS("/*", "index.html", distIndexHtml) - e.StaticFS("/", distDirFS) - // --[ System ]-- e.GET("/metrics", echoprometheus.NewHandler()) + + e.StaticFS("/file", afero.NewIOFS(storage)) + e.StaticFS("/*", distDirFS) + e.Logger.Debugf("backend version %s", Version) e.Logger.Fatal(e.Start(cfg.Listen)) } diff --git a/pkg/controller/admin.go b/pkg/controller/admin.go index 1167a72..7a03661 100644 --- a/pkg/controller/admin.go +++ b/pkg/controller/admin.go @@ -45,7 +45,7 @@ func (a *Admin) AdminEditGame(ctx echo.Context) error { }) } tasks = append(tasks, api.TaskEdit{ - Id: &t.ID, + Id: t.ID, Codes: codes, Text: t.Text, Title: t.Title, @@ -98,7 +98,7 @@ func (a *Admin) AdminGetGame(ctx echo.Context, uid uuid.UUID) error { }) } tasks = append(tasks, api.TaskEdit{ - Id: &t.ID, + Id: t.ID, Codes: codes, Text: t.Text, Title: t.Title, @@ -163,12 +163,8 @@ func (*Admin) mapCreateGameRequest(req *api.GameEdit, user *models.User) *models IconID: req.Icon, } for order, te := range req.Tasks { - id := uuid.New() - if te.Id != nil { - id = *te.Id - } task := &models.Task{ - ID: id, + ID: te.Id, Title: te.Title, Text: te.Text, Codes: make([]*models.Code, 0, len(te.Codes)), diff --git a/pkg/controller/file.go b/pkg/controller/file.go index 531ae12..cb77e2c 100644 --- a/pkg/controller/file.go +++ b/pkg/controller/file.go @@ -40,16 +40,6 @@ func (u *File) AdminUploadFile(c echo.Context, quest uuid.UUID) error { }) } -// (GET /file/{uid}) -func (u *File) GetFile(c echo.Context, uid uuid.UUID) error { - f, rdr, err := u.FileService.GetFile(c.Request().Context(), uid) - if err != nil { - return err - } - - return c.Stream(200, f.ContentType, rdr) -} - func (u *File) AdminListFiles(c echo.Context, quest uuid.UUID) error { fl, err := u.FileService.GetFilesByQuest(c.Request().Context(), quest) if err != nil { diff --git a/pkg/models/task.go b/pkg/models/task.go index 448c592..5d6e97d 100644 --- a/pkg/models/task.go +++ b/pkg/models/task.go @@ -10,6 +10,7 @@ type Task struct { Title string Text string MaxTime int + Game *Game GameID uuid.UUID Codes []*Code `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"` TaskOrder uint diff --git a/pkg/service/file.go b/pkg/service/file.go index ef976ca..b851f4d 100644 --- a/pkg/service/file.go +++ b/pkg/service/file.go @@ -3,7 +3,6 @@ package service import ( "context" "fmt" - "io" "mime/multipart" "github.com/google/uuid" @@ -53,21 +52,6 @@ func (u *File) Upload( return file.ID, u.DB.WithContext(ctx).Create(file).Error } -func (u *File) GetFile(ctx context.Context, uid uuid.UUID) (*models.File, io.ReadCloser, error) { - f := new(models.File) - if err := u.DB.WithContext(ctx).First(f, uid).Error; err != nil { - return nil, nil, err - } - - filePath := fmt.Sprintf("%s/%s", f.QuestID.String(), f.Filename) - file, err := u.store.Open(filePath) - if err != nil { - return nil, nil, err - } - - return f, file, nil -} - func (u *File) GetFilesByQuest(ctx context.Context, quest uuid.UUID) ([]*models.File, error) { list := make([]*models.File, 0) diff --git a/pkg/service/game.go b/pkg/service/game.go index 6dda806..52b3441 100644 --- a/pkg/service/game.go +++ b/pkg/service/game.go @@ -68,7 +68,14 @@ func (gs *Game) ListByAuthor(ctx context.Context, author *models.User) ([]*model } func (gs *Game) UpsertGame(ctx context.Context, game *models.Game) (*models.Game, error) { - return game, gs.DB.Debug(). + + ids := []uuid.UUID{} + for _, t := range game.Tasks { + ids = append(ids, t.ID) + } + gs.DB.Delete([]models.Task{}, `game_id = ? and id not in (?)`, game.ID, ids) + + err := gs.DB. Session(&gorm.Session{FullSaveAssociations: true}). Clauses(clause.OnConflict{ Columns: []clause.Column{{Name: "id"}}, @@ -76,6 +83,11 @@ func (gs *Game) UpsertGame(ctx context.Context, game *models.Game) (*models.Game "title", "description", }), }). - Create(&game). + Create(game). Error + if err != nil { + return nil, err + } + + return game, gs.DB.Save(game).Error }