Фикс ошибок
This commit is contained in:
parent
7e706a3981
commit
a1dc96088c
34 changed files with 1584 additions and 1180 deletions
|
@ -1,4 +1,5 @@
|
||||||
package api
|
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 server,spec -package api -o ./server.go ./openapi.yaml
|
||||||
//go:generate oapi-codegen -generate types -package api -o ./types.go ./openapi.yaml
|
//go:generate oapi-codegen -generate types -package api -o ./types.go ./openapi.yaml
|
||||||
|
|
838
api/openapi.yaml
838
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:
|
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:
|
schemas:
|
||||||
userView:
|
codeEdit:
|
||||||
|
properties:
|
||||||
|
code:
|
||||||
|
type: string
|
||||||
|
description:
|
||||||
|
type: string
|
||||||
|
id:
|
||||||
|
format: uuid
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- code
|
||||||
type: object
|
type: object
|
||||||
|
codeView:
|
||||||
|
properties:
|
||||||
|
code:
|
||||||
|
type: string
|
||||||
|
description:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
fileItem:
|
||||||
properties:
|
properties:
|
||||||
id:
|
id:
|
||||||
type: string
|
|
||||||
format: uuid
|
format: uuid
|
||||||
username:
|
|
||||||
type: string
|
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:
|
email:
|
||||||
type: string
|
type: string
|
||||||
experience:
|
|
||||||
type: integer
|
|
||||||
level:
|
|
||||||
type: integer
|
|
||||||
expToCurrentLevel:
|
expToCurrentLevel:
|
||||||
type: integer
|
type: integer
|
||||||
expToNextLevel:
|
expToNextLevel:
|
||||||
type: integer
|
type: integer
|
||||||
|
experience:
|
||||||
|
type: integer
|
||||||
games:
|
games:
|
||||||
type: array
|
|
||||||
items:
|
items:
|
||||||
$ref: "#/components/schemas/gameView"
|
$ref: '#/components/schemas/gameView'
|
||||||
role:
|
type: array
|
||||||
|
id:
|
||||||
|
format: uuid
|
||||||
type: string
|
type: string
|
||||||
|
level:
|
||||||
|
type: integer
|
||||||
|
role:
|
||||||
enum:
|
enum:
|
||||||
- user
|
- user
|
||||||
- creator
|
- creator
|
||||||
- admin
|
- admin
|
||||||
|
type: string
|
||||||
|
username:
|
||||||
|
type: string
|
||||||
required:
|
required:
|
||||||
- id
|
- id
|
||||||
- username
|
- username
|
||||||
|
@ -242,221 +250,201 @@ components:
|
||||||
- expToNextLevel
|
- expToNextLevel
|
||||||
- games
|
- games
|
||||||
- role
|
- role
|
||||||
gameView:
|
|
||||||
type: object
|
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:
|
securitySchemes:
|
||||||
cookieAuth:
|
cookieAuth:
|
||||||
type: apiKey
|
|
||||||
in: cookie
|
in: cookie
|
||||||
name: session
|
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:
|
security:
|
||||||
- cookieAuth: []
|
- cookieAuth: []
|
||||||
|
servers:
|
||||||
|
- url: /api
|
||||||
|
|
74
api/parts/admin.yaml
Normal file
74
api/parts/admin.yaml
Normal file
|
@ -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"
|
19
api/parts/common.yaml
Normal file
19
api/parts/common.yaml
Normal file
|
@ -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: []
|
43
api/parts/game.yaml
Normal file
43
api/parts/game.yaml
Normal file
|
@ -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"
|
||||||
|
|
66
api/parts/responses.yaml
Normal file
66
api/parts/responses.yaml
Normal file
|
@ -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"
|
186
api/parts/schemas.yaml
Normal file
186
api/parts/schemas.yaml
Normal file
|
@ -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
|
57
api/parts/user.yaml
Normal file
57
api/parts/user.yaml
Normal file
|
@ -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"
|
|
@ -43,9 +43,6 @@ type ServerInterface interface {
|
||||||
// (POST /engine/{uid}/code)
|
// (POST /engine/{uid}/code)
|
||||||
EnterCode(ctx echo.Context, uid openapi_types.UUID) error
|
EnterCode(ctx echo.Context, uid openapi_types.UUID) error
|
||||||
|
|
||||||
// (GET /file/{uid})
|
|
||||||
GetFile(ctx echo.Context, uid openapi_types.UUID) error
|
|
||||||
|
|
||||||
// (GET /games)
|
// (GET /games)
|
||||||
GetGames(ctx echo.Context) error
|
GetGames(ctx echo.Context) error
|
||||||
|
|
||||||
|
@ -179,24 +176,6 @@ func (w *ServerInterfaceWrapper) EnterCode(ctx echo.Context) error {
|
||||||
return err
|
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.
|
// GetGames converts echo context to params.
|
||||||
func (w *ServerInterfaceWrapper) GetGames(ctx echo.Context) error {
|
func (w *ServerInterfaceWrapper) GetGames(ctx echo.Context) error {
|
||||||
var err 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+"/admin/games/:uid", wrapper.AdminGetGame)
|
||||||
router.GET(baseURL+"/engine/:uid", wrapper.GameEngine)
|
router.GET(baseURL+"/engine/:uid", wrapper.GameEngine)
|
||||||
router.POST(baseURL+"/engine/:uid/code", wrapper.EnterCode)
|
router.POST(baseURL+"/engine/:uid/code", wrapper.EnterCode)
|
||||||
router.GET(baseURL+"/file/:uid", wrapper.GetFile)
|
|
||||||
router.GET(baseURL+"/games", wrapper.GetGames)
|
router.GET(baseURL+"/games", wrapper.GetGames)
|
||||||
router.GET(baseURL+"/user", wrapper.GetUser)
|
router.GET(baseURL+"/user", wrapper.GetUser)
|
||||||
router.POST(baseURL+"/user/login", wrapper.PostUserLogin)
|
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
|
// Base64 encoded, gzipped, json marshaled Swagger object
|
||||||
var swaggerSpec = []string{
|
var swaggerSpec = []string{
|
||||||
|
|
||||||
"H4sIAAAAAAAC/8xZS2/cNhD+KwXbo2JtHifdUsM1ghpBmzq9GAuDlsZrxhIpkyPHW0P/vRhS0kor6uGN",
|
"H4sIAAAAAAAC/8xYS3OkNhD+KyklR2JmHyduG5fj2oprK9l4c3FNuWRoj7UGCUvNrCcu/nuqJWBgEA+P",
|
||||||
"ssnJWnI4j28enKGfWayyXEmQaFj0zDSYXEkD9gdorfSnaoUWYiURJNInz/NUxByFkuEXoyStmfgOMk5f",
|
"iXdPHkPTj6+/bnXricUqy5UEiYZFT0yDyZU0YP8BrZX+XD2hB7GSCBLpJ8/zVMQchZLhV6MkPTPxHWSc",
|
||||||
"uVY5aBSOT6wSexy3ObCICYmwAc3KgGVgDN+0Nw1qITesLAOm4aEQGhIWXTkWO/p1UNOrmy8QIyvpQAIm",
|
"fuVa5aBROD2xSuznuMuBRUxIhA1oVgYsA2P4pv3SoBZyw8oyYBoeCqEhYdGVU7GXXwe1vLr5CjGykj5I",
|
||||||
"1iInnVjEiP+tSMFcCIMHWSEQMmvAbxpuWcR+DXdghY7MhCTiA0JG4iqduNZ8O6TShmfwPsmEPEilMU2I",
|
"wMRa5OQTixjpvxUpmAth8KgoBEJmA/hFwy2L2M/hHqzQiZmQTHxEyMhc5RPXmu+GXNrwDD4kmZBHuTTm",
|
||||||
"81kicEzy98WCJPwr4OtcLJCb+8VhIKZOCb/QIk8VTxYI66IQCf29VTrjyCK3EExEsiWaG7+FAb04QMR0",
|
"CWk+SwSOWf5/sSAL/wj4NhcL5OZ+cRhIqXPCb7TIU8WTBWhdFCKhv7dKZxxZ5B4EE0y2QnP5WxjQiwNE",
|
||||||
"GKAyqNg0qWtjaiqpa2v3GHr2DwHNCuuDFtgNa8tS+pUeIU2K94TMsiVgSouNkDz9yDO/Tkb8562QezBY",
|
"SocBKoNKTVO6llNTRV1He6DQ8/4Y0KyxPmiBfWFjWcq/0mOkKfGekVmxBExpsRGSp5945vfJiH+9HfIA",
|
||||||
"7h1m1VEfNE056Gk96aHYbUzaNdP8XInqeunfAJStZnaBIWpX4vYLTMBQYOpH1y1Ml65LoisD9iiMuOnw",
|
"Bqu9o6z61AdN0w56Xk9mKHYvJuOaGX6uRHW89E8AqlYzu8GQtGtxhw0mYCgw9aPrHky3rkuSKwO2FUbc",
|
||||||
"ulEqBS57Dqkpa+nd6Kok10Y2QAx567LSE2SROe4aC56ygMUCt61jO9OaittzMi/wTun50O6qQh/aWANH",
|
"dHTdKJUCl72E1JK19S67Kst1kA0QQ9m6rPwEWWROu8aCpyxgscBd67N9aE3H7SWZF3in9Hxo912hD22s",
|
||||||
"SN7jYQl/5HA6VYXEge0fECPOiLHwqAxqa9/GPGh86QucJiW8FXC+/5tK7/H/TMcgPPkjZAj2PaRqkCyf",
|
"gSMkH/C4gn9lOp2qQuLA6+/AERfEGD2qgNretzEPmlz6iNOUhLcDzs9/0+k9+Z+ZGIRHP0OGYB9DyioL",
|
||||||
"oNJ/yObhqv8ym4divtWa1vmo7q+rPlTIR56KpP6p0uZTwhNep/AIlLYUMNckNwUEb/4eDbImvXuQQcZF",
|
"qiCGAh9u/c8LfIj4rfm0Lkp1f10No0JueSqS+l+VNj8lPOJ1Clug2iXWXJPdFBC8RfxS3GZD1tR4DzLI",
|
||||||
"6tUBnvJLdVpoDRIvrEnepLJkH+Fpgga0ABkPDAKElFmgz5wdq+mwrlqlHbcTdHVCKvri1L573Umk0n/J",
|
"uEi9PsBjfqlOC61B4oUNyVtZVuwTPE7IgBYg44FtgJAyCwybswmbDvuqVdpJO0FXV6WiX5xmeG86SVT6",
|
||||||
"+6pCQx5UXugAVSvpc0QP9RrCSn1PgxkwA3GhBW7/IRjrfFH3At4XeGfBpxbQLVEoW0OYAWNa5SpiPBd/",
|
"T3of4RvxoMpCB6jaSV8ieqjXEFbue6bMgBmICy1w9zfBWNeLuhfwocA7Cz7Nge4RUdkGwgwY0+pZEeO5",
|
||||||
"QtXTC3mrrLEuXJn8uwBDkfgI2riW8vXJ6mRlu6AcJM8Fi9jbk9cnKyp8HO+sGqGF1M5S4fMDsShpeQM2",
|
"+AOqwV7IW2WDdXRl8q8CDDFxC9q4ufLNyepkZUehHCTPBYvYu5M3JyvqfhzvrBuhhdQuVOHTA6ko6fEG",
|
||||||
"OShabaP7IWERs8MTzTF/0HBnuWieAQJddleVEcR5Z8JDpdTOAagLCFr98lQ7ug66o/Gb1WooSBu6sD99",
|
"bHEQW+20+zFhEbMbFC0zv9OGZ7VongECnXhXVRCkeR/CQ+XUPgGoCwhaQ/PUTLoOuvvx29VqiKSNXNhf",
|
||||||
"tr1glW3jf9UPsXJNJzzohG6KsdmszBBKny0R4XRMmCyb31Wy3ZtYsiJFkXONIbF5lXD0DFVkZEfSjZBc",
|
"QdtZsM628b/qU6xc0xcedEK3ythqVmYIpS9WiHB6TZismt9UsjtYW7IiRZFzjSGp+TXh6NmsKMiOpRsh",
|
||||||
"b70TgmeEermT9sbBb/JQU8XGA/e8ztQXK9sb4a3RIxFAN/q5qzDDjlnmyaE81KDua8i3OyB8LkQyUT/O",
|
"ud551wTPHvX8JB3shC/KUNPFxol7Xlfqs53t7fE26BEG0LF+7jrMcGKWuXcojw2oeyXy8gSET4VIJvrH",
|
||||||
"oYZlOi1cqB+5diwGDMiNkDCBCWFxZgl/XkQ6j0Uu3jrGhfXE70+HM4mgT12TdiQLD8u3Wc8Y895HDkxK",
|
"OdSwTJeFo/or947FgAG5ERImMCEszqzgj4tI58bI8a0TXFiv/f5yOJMI+tQNaa8U4XH1NusuY94lyZFF",
|
||||||
"H9Lu8hkPIsDZV813iqABkFWMgK8MauBZF+zpa6b3RGZvqAqV8YJfFZnlSj2JtN3oiMTPrls94CJsvzWW",
|
"6UN6vLVV5bRcUyOTdu4asfjFzWVHtPz21VoZsPerd9MfdW/EWy6GqdoIOUy8P5Wxrl5YsaV4MTy159yY",
|
||||||
"AXu3ejt9qPsvgpaKYao2Qg6n41/KWFUvLNlS2TI8y+TcmK9KJ9O5VHfizYnF8qqP8OrlCHcugXUHb1Xg",
|
"b0on06ypZ87mi8UY1Ed49XyEO+1u3cFbFTgLcJLr+f++dxHGTBHHYMxPlepjPd77qGEjDDr+jnv5uZb8",
|
||||||
"LMCJrqf/u97LIDNFHIMxv1SsD9V4p6OGjTDo4ndcy0815Q+NjN3mG+/u/HHPM+k1cttSfu5YG+1A1lSU",
|
"rszYv3zrfTt/sfHsNI3dtpUfm2ujZ+2a2rsBva0PkEKnLGIhLTfluvwvAAD//8w97C1ZGwAA",
|
||||||
"DejHuuwXOmURC2nkK9fl/wEAAP//MtUIV2ocAAA=",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetSwagger returns the content of the embedded swagger specification file
|
// GetSwagger returns the content of the embedded swagger specification file
|
||||||
|
|
|
@ -84,10 +84,10 @@ type GameView struct {
|
||||||
|
|
||||||
// TaskEdit defines model for taskEdit.
|
// TaskEdit defines model for taskEdit.
|
||||||
type TaskEdit struct {
|
type TaskEdit struct {
|
||||||
Codes []CodeEdit `json:"codes"`
|
Codes []CodeEdit `json:"codes"`
|
||||||
Id *openapi_types.UUID `json:"id,omitempty"`
|
Id openapi_types.UUID `json:"id"`
|
||||||
Text string `json:"text"`
|
Text string `json:"text"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TaskView defines model for taskView.
|
// TaskView defines model for taskView.
|
||||||
|
|
|
@ -1,33 +1,34 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
env: {
|
env: {
|
||||||
browser: true,
|
browser: true,
|
||||||
es2021: true
|
es2021: true
|
||||||
},
|
},
|
||||||
extends: [
|
extends: [
|
||||||
'standard',
|
'standard',
|
||||||
'plugin:react/recommended',
|
'plugin:react/recommended',
|
||||||
'plugin:react/jsx-runtime'
|
'plugin:react/jsx-runtime'
|
||||||
],
|
],
|
||||||
overrides: [
|
overrides: [
|
||||||
{
|
{
|
||||||
env: {
|
env: {
|
||||||
node: true
|
node: true
|
||||||
},
|
},
|
||||||
files: [
|
files: [
|
||||||
'.eslintrc.{js,cjs}'
|
'.eslintrc.{js,cjs}'
|
||||||
],
|
],
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
sourceType: 'script'
|
sourceType: 'script'
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: 'latest',
|
||||||
|
sourceType: 'module'
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
'react'
|
||||||
|
],
|
||||||
|
rules: {
|
||||||
|
indent: ['error', 4]
|
||||||
}
|
}
|
||||||
],
|
|
||||||
parserOptions: {
|
|
||||||
ecmaVersion: 'latest',
|
|
||||||
sourceType: 'module'
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
'react'
|
|
||||||
],
|
|
||||||
rules: {
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,81 +18,81 @@ import EditQuest from './pages/admin/Quest'
|
||||||
import AdminQuest from './pages/admin/Quests'
|
import AdminQuest from './pages/admin/Quests'
|
||||||
|
|
||||||
const router = createBrowserRouter(
|
const router = createBrowserRouter(
|
||||||
createRoutesFromElements(
|
createRoutesFromElements(
|
||||||
<Route
|
<Route
|
||||||
path="/"
|
path="/"
|
||||||
id="root"
|
id="root"
|
||||||
element={<Layout />}
|
element={<Layout />}
|
||||||
loader={async () => ajax('/api/user').catch(x => { console.log(x); return null })}
|
loader={async () => ajax('/api/user').catch(x => { console.log(x); return null })}
|
||||||
>
|
>
|
||||||
<Route
|
<Route
|
||||||
index
|
index
|
||||||
element={<Index />}
|
element={<Index />}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
id="quests"
|
id="quests"
|
||||||
path="/quests"
|
path="/quests"
|
||||||
element={<Auth><Quests /></Auth>}
|
element={<Auth><Quests /></Auth>}
|
||||||
loader={() => ajax('/api/games').catch(x => { console.log(x); return null })}
|
loader={() => ajax('/api/games').catch(x => { console.log(x); return null })}
|
||||||
/>
|
/>
|
||||||
<Route path="me" element={<User />} />
|
<Route path="me" element={<User />} />
|
||||||
<Route path="login" element={<Login />} />
|
<Route path="login" element={<Login />} />
|
||||||
<Route path="register" element={<Register />} />
|
<Route path="register" element={<Register />} />
|
||||||
<Route
|
<Route
|
||||||
path="go/:gameId"
|
path="go/:gameId"
|
||||||
element={<Auth><Engine /></Auth>}
|
element={<Auth><Engine /></Auth>}
|
||||||
loader={({ params }) => ajax(`/api/engine/${params.gameId}`).catch(x => { console.log(x); return null })}
|
loader={({ params }) => ajax(`/api/engine/${params.gameId}`).catch(x => { console.log(x); return null })}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="/admin/quests/new"
|
path="/admin/quests/new"
|
||||||
element={<Auth role="creator"><EditQuest /></Auth>}
|
element={<Auth role="creator"><EditQuest /></Auth>}
|
||||||
loader={() => ({ quest: null, files: [] })}
|
loader={() => ({ quest: null, files: [] })}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="/admin/quests/:gameId"
|
path="/admin/quests/:gameId"
|
||||||
element={<Auth role="creator"><EditQuest /></Auth>}
|
element={<Auth role="creator"><EditQuest /></Auth>}
|
||||||
loader={async ({ params }) => {
|
loader={async ({ params }) => {
|
||||||
const quest = await ajax(`/api/admin/games/${params.gameId}`)
|
const quest = await ajax(`/api/admin/games/${params.gameId}`)
|
||||||
const files = await ajax(`/api/admin/file/${params.gameId}`)
|
const files = await ajax(`/api/admin/file/${params.gameId}`)
|
||||||
|
|
||||||
return { quest, files }
|
return { quest, files }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="/admin/quests"
|
path="/admin/quests"
|
||||||
element={<Auth role="creator"><AdminQuest /></Auth>}
|
element={<Auth role="creator"><AdminQuest /></Auth>}
|
||||||
loader={() => ajax('/api/admin/games')}
|
loader={() => ajax('/api/admin/games')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Route path="*" element={<NoMatch />} />
|
<Route path="*" element={<NoMatch />} />
|
||||||
</Route>
|
</Route>
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
function App () {
|
function App () {
|
||||||
return (
|
return (
|
||||||
<RouterProvider router={router} />
|
<RouterProvider router={router} />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function Auth (props) {
|
function Auth (props) {
|
||||||
const baseUser = useRouteLoaderData('root')
|
const baseUser = useRouteLoaderData('root')
|
||||||
const { user } = UserProvider.useContainer()
|
const { user } = UserProvider.useContainer()
|
||||||
const { hasRole } = useRole()
|
const { hasRole } = useRole()
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
if (!user && !baseUser) {
|
if (!user && !baseUser) {
|
||||||
return <Navigate to="/login" state={{ from: location }} replace />
|
return <Navigate to="/login" state={{ from: location }} replace />
|
||||||
}
|
}
|
||||||
if (props.role && !hasRole(props.role)) {
|
if (props.role && !hasRole(props.role)) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return props.children
|
return props.children
|
||||||
}
|
}
|
||||||
|
|
||||||
Auth.propTypes = {
|
Auth.propTypes = {
|
||||||
children: PropTypes.any,
|
children: PropTypes.any,
|
||||||
role: PropTypes.string
|
role: PropTypes.string
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App
|
export default App
|
||||||
|
|
|
@ -8,82 +8,82 @@ import { Content, Header } from 'antd/es/layout/layout'
|
||||||
import { useRole } from '../utils/roles'
|
import { useRole } from '../utils/roles'
|
||||||
|
|
||||||
const AppLayout = () => {
|
const AppLayout = () => {
|
||||||
const params = useLoaderData()
|
const params = useLoaderData()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setUser(params)
|
setUser(params)
|
||||||
}, [params])
|
}, [params])
|
||||||
const { user, setUser } = UserProvider.useContainer()
|
const { user, setUser } = UserProvider.useContainer()
|
||||||
const { hasRole } = useRole()
|
const { hasRole } = useRole()
|
||||||
|
|
||||||
const logout = () => {
|
const logout = () => {
|
||||||
ajax('/api/user/logout', {
|
ajax('/api/user/logout', {
|
||||||
method: 'POST'
|
method: 'POST'
|
||||||
})
|
})
|
||||||
.then(() => { setUser(null); navigate('/login') })
|
.then(() => { setUser(null); navigate('/login') })
|
||||||
.catch(() => { setUser(null); navigate('/login') })
|
.catch(() => { setUser(null); navigate('/login') })
|
||||||
}
|
}
|
||||||
let items = [
|
let items = [
|
||||||
{ key: 'login', label: 'Вход', link: '/login' },
|
{ key: 'login', label: 'Вход', link: '/login' },
|
||||||
{ key: 'register', label: 'Регистрация', link: '/register' }
|
{ 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 (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')) {
|
if (hasRole('creator')) {
|
||||||
items.push({
|
items.push({
|
||||||
key: 'admin/quests',
|
key: 'admin/quests',
|
||||||
label: 'Управление квестами',
|
label: 'Управление квестами',
|
||||||
link: '/admin/quests'
|
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')) {
|
return (<Layout>
|
||||||
items.push({
|
<Header style={{ display: 'flex', alignItems: 'center', width: '100%', justifyContent: 'space-between' }}>
|
||||||
key: 'admin',
|
<Link to="/" className='navbar-brand'><img src="/assets/logo.png" /></Link>
|
||||||
label: 'Админка',
|
<Menu
|
||||||
link: '/admin'
|
mode="horizontal"
|
||||||
})
|
items={items}
|
||||||
}
|
selectedKeys={location.pathname.replace('/', '')}
|
||||||
}
|
onClick={menuHandler}
|
||||||
const menuHandler = (x) => {
|
overflowedIndicator={<MenuOutlined />}
|
||||||
const item = items.find(y => y.key === x.key)
|
style={{ width: '100%' }}
|
||||||
if (item.link) { navigate(item.link) }
|
/>
|
||||||
}
|
</Header>
|
||||||
|
<Content style={{ padding: '0 24px' }}>
|
||||||
return (<Layout>
|
<Outlet />
|
||||||
<Header style={{ display: 'flex', alignItems: 'center', width: '100%', justifyContent: 'space-between' }}>
|
</Content>
|
||||||
<Link to="/" className='navbar-brand'><img src="/assets/logo.png" /></Link>
|
</Layout>)
|
||||||
<Menu
|
|
||||||
mode="horizontal"
|
|
||||||
items={items}
|
|
||||||
selectedKeys={location.pathname.replace('/', '')}
|
|
||||||
onClick={menuHandler}
|
|
||||||
overflowedIndicator={<MenuOutlined />}
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
/>
|
|
||||||
</Header>
|
|
||||||
<Content style={{ padding: '0 24px' }}>
|
|
||||||
<Outlet />
|
|
||||||
</Content>
|
|
||||||
</Layout>)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AppLayout
|
export default AppLayout
|
||||||
|
|
|
@ -12,27 +12,27 @@ const { darkAlgorithm } = theme
|
||||||
console.log(import.meta.env.VITE_VERSION)
|
console.log(import.meta.env.VITE_VERSION)
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<Compose providers={store}>
|
<Compose providers={store}>
|
||||||
<ConfigProvider
|
<ConfigProvider
|
||||||
locale={ruRU}
|
locale={ruRU}
|
||||||
theme={{
|
theme={{
|
||||||
token: {
|
token: {
|
||||||
colorTextBase: '#fff',
|
colorTextBase: '#fff',
|
||||||
colorPrimary: '#59FBEA',
|
colorPrimary: '#59FBEA',
|
||||||
colorButtonText: '#000',
|
colorButtonText: '#000',
|
||||||
colorInfo: '#59FBEA',
|
colorInfo: '#59FBEA',
|
||||||
colorSuccess: '#15803d',
|
colorSuccess: '#15803d',
|
||||||
colorBgBase: '#171e26',
|
colorBgBase: '#171e26',
|
||||||
borderRadius: 2,
|
borderRadius: 2,
|
||||||
wireframe: false
|
wireframe: false
|
||||||
},
|
},
|
||||||
algorithm: darkAlgorithm
|
algorithm: darkAlgorithm
|
||||||
}}>
|
}}>
|
||||||
<AntdApp>
|
<AntdApp>
|
||||||
<App />
|
<App />
|
||||||
</AntdApp>
|
</AntdApp>
|
||||||
</ConfigProvider>
|
</ConfigProvider>
|
||||||
</Compose>
|
</Compose>
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
)
|
)
|
||||||
|
|
|
@ -6,65 +6,65 @@ import Title from 'antd/es/typography/Title'
|
||||||
import { Alert, App, Button, Card, Col, Form, Input, List, Row } from 'antd'
|
import { Alert, App, Button, Card, Col, Form, Input, List, Row } from 'antd'
|
||||||
|
|
||||||
const Engine = () => {
|
const Engine = () => {
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const loadedTask = useLoaderData()
|
const loadedTask = useLoaderData()
|
||||||
const [task, setTask] = useState(loadedTask)
|
const [task, setTask] = useState(loadedTask)
|
||||||
const { message } = App.useApp()
|
const { message } = App.useApp()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!task) {
|
if (!task) {
|
||||||
return
|
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: '' })
|
|
||||||
}
|
}
|
||||||
}).catch(e => {
|
switch (task.message) {
|
||||||
console.warn(e)
|
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') {
|
const [form] = Form.useForm()
|
||||||
return (<div style={{ padding: 8 }}>
|
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 (<div style={{ padding: 8 }}>
|
||||||
<Alert type="success" message="Вы прошли все уровни!" />
|
<Alert type="success" message="Вы прошли все уровни!" />
|
||||||
<Link to={'/'}>К списку игр</Link>
|
<Link to={'/'}>К списку игр</Link>
|
||||||
</div>)
|
</div>)
|
||||||
}
|
}
|
||||||
if (!task) {
|
if (!task) {
|
||||||
return (<div style={{ padding: 8 }}>
|
return (<div style={{ padding: 8 }}>
|
||||||
<Alert type="warning" message="Для вас не предусмотренно уровней" />
|
<Alert type="warning" message="Для вас не предусмотренно уровней" />
|
||||||
<Link to={'/'}>К списку игр</Link>
|
<Link to={'/'}>К списку игр</Link>
|
||||||
</div>)
|
</div>)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (<>
|
return (<>
|
||||||
<Title>{task.title}</Title>
|
<Title>{task.title}</Title>
|
||||||
<Row gutter={8}>
|
<Row gutter={8}>
|
||||||
<Col xs={24} sm={24} md={18}>
|
<Col xs={24} sm={24} md={18}>
|
||||||
|
|
|
@ -5,9 +5,9 @@ import { UserProvider } from '../store/user'
|
||||||
const { Title, Paragraph } = Typography
|
const { Title, Paragraph } = Typography
|
||||||
|
|
||||||
const Index = () => {
|
const Index = () => {
|
||||||
const { user } = UserProvider.useContainer()
|
const { user } = UserProvider.useContainer()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
return (<>
|
return (<>
|
||||||
<Title>NQuest</Title>
|
<Title>NQuest</Title>
|
||||||
<Paragraph>Привет! Это платформа для ARG игр.</Paragraph>
|
<Paragraph>Привет! Это платформа для ARG игр.</Paragraph>
|
||||||
<Paragraph>
|
<Paragraph>
|
||||||
|
@ -15,11 +15,11 @@ const Index = () => {
|
||||||
А если ты знаешь зачем пришёл, то добро пожаловать!
|
А если ты знаешь зачем пришёл, то добро пожаловать!
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
{!user
|
{!user
|
||||||
? (<Button.Group>
|
? (<Button.Group>
|
||||||
<Button type="primary" onClick={() => navigate('/login')}>Вход</Button>
|
<Button type="primary" onClick={() => navigate('/login')}>Вход</Button>
|
||||||
<Button onClick={() => navigate('/register')}>Регистрация</Button>
|
<Button onClick={() => navigate('/register')}>Регистрация</Button>
|
||||||
</Button.Group>)
|
</Button.Group>)
|
||||||
: (<Button type="primary" onClick={() => navigate('/quests')}>К квестам</Button>)}
|
: (<Button type="primary" onClick={() => navigate('/quests')}>К квестам</Button>)}
|
||||||
</>)
|
</>)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,45 +5,45 @@ import { ajax } from '../utils/fetch'
|
||||||
import { Alert, Button, Form, Input } from 'antd'
|
import { Alert, Button, Form, Input } from 'antd'
|
||||||
|
|
||||||
const Login = () => {
|
const Login = () => {
|
||||||
const { user, setUser } = UserProvider.useContainer()
|
const { user, setUser } = UserProvider.useContainer()
|
||||||
const { state } = useLocation()
|
const { state } = useLocation()
|
||||||
const [error, setError] = useState(null)
|
const [error, setError] = useState(null)
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const [form] = Form.useForm()
|
const [form] = Form.useForm()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (user) {
|
if (user) {
|
||||||
navigate(state && state.from ? state.from : '/')
|
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) => {
|
return (<>
|
||||||
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 (<>
|
|
||||||
<h1>Вход</h1>
|
<h1>Вход</h1>
|
||||||
{error ? <Alert type='error' message={error} /> : null}
|
{error ? <Alert type='error' message={error} /> : null}
|
||||||
<Form
|
<Form
|
||||||
form={form}
|
form={form}
|
||||||
name='login'
|
name='login'
|
||||||
labelCol={{
|
labelCol={{
|
||||||
span: 8
|
span: 8
|
||||||
}}
|
}}
|
||||||
wrapperCol={{
|
wrapperCol={{
|
||||||
span: 16
|
span: 16
|
||||||
}}
|
}}
|
||||||
style={{
|
style={{
|
||||||
maxWidth: 600
|
maxWidth: 600
|
||||||
}}
|
}}
|
||||||
|
|
||||||
onFinish={onFinish}>
|
onFinish={onFinish}>
|
||||||
|
@ -51,14 +51,14 @@ const Login = () => {
|
||||||
label='E-mail'
|
label='E-mail'
|
||||||
name='email'
|
name='email'
|
||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
message: 'Обязательное поле'
|
message: 'Обязательное поле'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'email',
|
type: 'email',
|
||||||
message: 'E-mail некорректный'
|
message: 'E-mail некорректный'
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
|
@ -70,18 +70,18 @@ const Login = () => {
|
||||||
label='Пароль'
|
label='Пароль'
|
||||||
name='password'
|
name='password'
|
||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
message: 'Обязательное поле'
|
message: 'Обязательное поле'
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Input.Password />
|
<Input.Password />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
wrapperCol={{
|
wrapperCol={{
|
||||||
offset: 8,
|
offset: 8,
|
||||||
span: 16
|
span: 16
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button type='primary' htmlType='submit'>
|
<Button type='primary' htmlType='submit'>
|
||||||
|
|
|
@ -8,12 +8,12 @@ import { UserProvider } from '../store/user'
|
||||||
const { Title, Paragraph } = Typography
|
const { Title, Paragraph } = Typography
|
||||||
|
|
||||||
const Quests = () => {
|
const Quests = () => {
|
||||||
moment.locale('ru')
|
moment.locale('ru')
|
||||||
const games = useLoaderData()
|
const games = useLoaderData()
|
||||||
const { user } = UserProvider.useContainer()
|
const { user } = UserProvider.useContainer()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
return (<>
|
return (<>
|
||||||
<Title>Квесты</Title>
|
<Title>Квесты</Title>
|
||||||
{games.map(item => renderItem(user, navigate, item))}
|
{games.map(item => renderItem(user, navigate, item))}
|
||||||
{games.length === 0 ? (<strong>Квестов пока не анонсировано</strong>) : null}
|
{games.length === 0 ? (<strong>Квестов пока не анонсировано</strong>) : null}
|
||||||
|
@ -21,38 +21,38 @@ const Quests = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderItem = (user, navigate, item) => {
|
const renderItem = (user, navigate, item) => {
|
||||||
const actions = [
|
const actions = [
|
||||||
<Popover
|
<Popover
|
||||||
key='type'
|
key='type'
|
||||||
content={<Paragraph>Квесты бывают двух типов: <ul><li><b>Виртуальные</b> - проходимые из дома</li><li><b>Полевые</b> - проходимые в городе Казани</li></ul></Paragraph>}
|
content={<Paragraph>Квесты бывают двух типов: <ul><li><b>Виртуальные</b> - проходимые из дома</li><li><b>Полевые</b> - проходимые в городе Казани</li></ul></Paragraph>}
|
||||||
title="Тип квеста">
|
title="Тип квеста">
|
||||||
<Space>
|
<Space>
|
||||||
{item.type === 'city' ? 'Полевой' : 'Виртуальный'}
|
{item.type === 'city' ? 'Полевой' : 'Виртуальный'}
|
||||||
</Space>
|
</Space>
|
||||||
</Popover>,
|
</Popover>,
|
||||||
<Popover
|
<Popover
|
||||||
key='exp'
|
key='exp'
|
||||||
content={<Paragraph>Вы получите {item.points} очков опыта за выполнение этого квеста.<br />Чем сложнее квест - тем больше за него опыта!</Paragraph>}
|
content={<Paragraph>Вы получите {item.points} очков опыта за выполнение этого квеста.<br />Чем сложнее квест - тем больше за него опыта!</Paragraph>}
|
||||||
title='Опыт за выполнения квеста'>
|
title='Опыт за выполнения квеста'>
|
||||||
<Space>{item.points} ОО</Space>
|
<Space>{item.points} ОО</Space>
|
||||||
</Popover>,
|
</Popover>,
|
||||||
<Popover key='taskCount' content={`Этот квест состоит из ${item.taskCount} уровней`} title='Количество уровней в квесте'>
|
<Popover key='taskCount' content={`Этот квест состоит из ${item.taskCount} уровней`} title='Количество уровней в квесте'>
|
||||||
<Space>{item.taskCount} ур</Space>
|
<Space>{item.taskCount} ур</Space>
|
||||||
</Popover>,
|
</Popover>,
|
||||||
<>{moment(item.createdAt).fromNow()}</>,
|
<>{moment(item.createdAt).fromNow()}</>,
|
||||||
<>Автор {item.authors.map(a => a.username)}</>
|
<>Автор {item.authors.map(a => a.username)}</>
|
||||||
]
|
]
|
||||||
|
|
||||||
let questAction = (<span>Необходимо войти</span>)
|
let questAction = (<span>Необходимо войти</span>)
|
||||||
|
|
||||||
if (user) {
|
if (user) {
|
||||||
questAction = (user.games.find(x => x.id === item.id)
|
questAction = (user.games.find(x => x.id === item.id)
|
||||||
? <span>Вы уже прошли этот квест</span>
|
? <span>Вы уже прошли этот квест</span>
|
||||||
: <Button onClick={() => navigate(`/go/${item.id}`)} type="primary">Начать квест</Button>
|
: <Button onClick={() => navigate(`/go/${item.id}`)} type="primary">Начать квест</Button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
key={item.id}
|
key={item.id}
|
||||||
actions={actions}
|
actions={actions}
|
||||||
|
@ -65,7 +65,7 @@ const renderItem = (user, navigate, item) => {
|
||||||
</Markdown>
|
</Markdown>
|
||||||
<Space>{questAction}</Space>
|
<Space>{questAction}</Space>
|
||||||
</Card>
|
</Card>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Quests
|
export default Quests
|
||||||
|
|
|
@ -5,42 +5,42 @@ import { ajax } from '../utils/fetch'
|
||||||
import { Alert, Button, Form, Input } from 'antd'
|
import { Alert, Button, Form, Input } from 'antd'
|
||||||
|
|
||||||
const Register = () => {
|
const Register = () => {
|
||||||
const { user, setUser } = UserProvider.useContainer()
|
const { user, setUser } = UserProvider.useContainer()
|
||||||
const [error, setError] = useState(null)
|
const [error, setError] = useState(null)
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (user) {
|
if (user) {
|
||||||
navigate('/')
|
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) => {
|
return (<>
|
||||||
ajax('/api/user/register', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
Accept: 'application/json',
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify(values)
|
|
||||||
})
|
|
||||||
.then(setUser)
|
|
||||||
.catch(({ message }) => setError('Ошибка регистрации'))
|
|
||||||
}
|
|
||||||
|
|
||||||
return (<>
|
|
||||||
<h1>Регистрация</h1>
|
<h1>Регистрация</h1>
|
||||||
{error ? <Alert type="error" message={error} /> : null}
|
{error ? <Alert type="error" message={error} /> : null}
|
||||||
<Form
|
<Form
|
||||||
name="register"
|
name="register"
|
||||||
labelCol={{
|
labelCol={{
|
||||||
span: 8
|
span: 8
|
||||||
}}
|
}}
|
||||||
wrapperCol={{
|
wrapperCol={{
|
||||||
span: 16
|
span: 16
|
||||||
}}
|
}}
|
||||||
style={{
|
style={{
|
||||||
maxWidth: 600
|
maxWidth: 600
|
||||||
}}
|
}}
|
||||||
|
|
||||||
onFinish={onFinish}>
|
onFinish={onFinish}>
|
||||||
|
@ -48,10 +48,10 @@ const Register = () => {
|
||||||
label="Имя пользователя"
|
label="Имя пользователя"
|
||||||
name="username"
|
name="username"
|
||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
message: 'Обязательное поле'
|
message: 'Обязательное поле'
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Input />
|
<Input />
|
||||||
|
@ -60,14 +60,14 @@ const Register = () => {
|
||||||
label="E-mail"
|
label="E-mail"
|
||||||
name="email"
|
name="email"
|
||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
message: 'Обязательное поле'
|
message: 'Обязательное поле'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'email',
|
type: 'email',
|
||||||
message: 'E-mail некорректный'
|
message: 'E-mail некорректный'
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
help="Не видно другим пользователям"
|
help="Не видно другим пользователям"
|
||||||
>
|
>
|
||||||
|
@ -80,10 +80,10 @@ const Register = () => {
|
||||||
label="Пароль"
|
label="Пароль"
|
||||||
name="password"
|
name="password"
|
||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
message: 'Обязательное поле'
|
message: 'Обязательное поле'
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Input.Password />
|
<Input.Password />
|
||||||
|
@ -94,26 +94,26 @@ const Register = () => {
|
||||||
dependencies={['password']}
|
dependencies={['password']}
|
||||||
hasFeedback
|
hasFeedback
|
||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
message: 'Обязательное поле'
|
message: 'Обязательное поле'
|
||||||
},
|
},
|
||||||
({ getFieldValue }) => ({
|
({ getFieldValue }) => ({
|
||||||
validator (_, value) {
|
validator (_, value) {
|
||||||
if (!value || getFieldValue('password') === value) {
|
if (!value || getFieldValue('password') === value) {
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
}
|
}
|
||||||
return Promise.reject(new Error('Пароли отличаются!'))
|
return Promise.reject(new Error('Пароли отличаются!'))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Input.Password />
|
<Input.Password />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
wrapperCol={{
|
wrapperCol={{
|
||||||
offset: 8,
|
offset: 8,
|
||||||
span: 16
|
span: 16
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button type="primary" htmlType="submit">
|
<Button type="primary" htmlType="submit">
|
||||||
|
|
|
@ -5,29 +5,29 @@ import Markdown from 'react-markdown'
|
||||||
const { Paragraph } = Typography
|
const { Paragraph } = Typography
|
||||||
|
|
||||||
const User = () => {
|
const User = () => {
|
||||||
const { user } = UserProvider.useContainer()
|
const { user } = UserProvider.useContainer()
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
return (<Space>Загрузка...</Space>)
|
return (<Space>Загрузка...</Space>)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (<>
|
return (<>
|
||||||
<h1>{user.username}</h1>
|
<h1>{user.username}</h1>
|
||||||
<Paragraph>Уровень: {user.level} ур</Paragraph>
|
<Paragraph>Уровень: {user.level} ур</Paragraph>
|
||||||
<Paragraph>Очков опыта: {user.experience} ОО</Paragraph>
|
<Paragraph>Очков опыта: {user.experience} ОО</Paragraph>
|
||||||
<Paragraph>Следующий уровень: {user.expToNextLevel} ОО</Paragraph>
|
<Paragraph>Следующий уровень: {user.expToNextLevel} ОО</Paragraph>
|
||||||
<Progress
|
<Progress
|
||||||
value={user.experience}
|
value={user.experience}
|
||||||
percent={((user.experience - user.expToCurrentLevel) / (user.expToNextLevel - user.expToCurrentLevel) * 100)}
|
percent={((user.experience - user.expToCurrentLevel) / (user.expToNextLevel - user.expToCurrentLevel) * 100)}
|
||||||
size="small"
|
size="small"
|
||||||
showInfo={false}
|
showInfo={false}
|
||||||
/>
|
/>
|
||||||
{user.games.map(item => <Popover
|
{user.games.map(item => <Popover
|
||||||
key={item.id}
|
key={item.id}
|
||||||
title={item.title}
|
title={item.title}
|
||||||
content={<Markdown>{item.description}</Markdown>}
|
content={<Markdown>{item.description}</Markdown>}
|
||||||
>
|
>
|
||||||
<Avatar size={64} style={{ marginRight: 4, marginBottom: 4 }} key={item.id} src={`/api/file/${item.icon}`} />
|
<Avatar size={64} style={{ marginRight: 4, marginBottom: 4 }} key={item.id} src={`/api/file/${item.icon}`} />
|
||||||
</Popover>)}
|
</Popover>)}
|
||||||
</>)
|
</>)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,121 +9,121 @@ import Markdown from 'react-markdown'
|
||||||
const { Title } = Typography
|
const { Title } = Typography
|
||||||
|
|
||||||
const Quest = () => {
|
const Quest = () => {
|
||||||
let { quest, files } = useLoaderData()
|
let { quest, files } = useLoaderData()
|
||||||
const [error, setError] = useState()
|
const [error, setError] = useState()
|
||||||
if (!quest) {
|
if (!quest) {
|
||||||
quest = {
|
quest = {
|
||||||
type: 'city',
|
type: 'city',
|
||||||
points: 10,
|
points: 10,
|
||||||
tasks: [],
|
tasks: [],
|
||||||
id: uuidv4(),
|
id: uuidv4(),
|
||||||
visible: false,
|
visible: false,
|
||||||
title: '',
|
title: '',
|
||||||
description: ''
|
description: ''
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
const [fields, setFields] = useState(quest)
|
||||||
const [fields, setFields] = useState(quest)
|
const [preview, setPreview] = useState(false)
|
||||||
const [preview, setPreview] = useState(false)
|
const navigate = useNavigate()
|
||||||
const navigate = useNavigate()
|
const normFile = (e) => {
|
||||||
const normFile = (e) => {
|
if (Array.isArray(e)) {
|
||||||
if (Array.isArray(e)) {
|
return e
|
||||||
return e
|
}
|
||||||
|
if (e.file.response) {
|
||||||
|
return e.file.response.uuid
|
||||||
|
}
|
||||||
|
|
||||||
|
return e
|
||||||
}
|
}
|
||||||
if (e.file.response) {
|
const formItemLayout = {
|
||||||
return e.file.response.uuid
|
labelCol: { span: 6 },
|
||||||
|
wrapperCol: { span: 14 }
|
||||||
|
}
|
||||||
|
const buttonLayout = {
|
||||||
|
offset: 6,
|
||||||
|
span: 14
|
||||||
}
|
}
|
||||||
|
|
||||||
return e
|
const onFinish = (values) => {
|
||||||
}
|
ajax('/api/admin/games', {
|
||||||
const formItemLayout = {
|
method: 'POST',
|
||||||
labelCol: { span: 6 },
|
headers: {
|
||||||
wrapperCol: { span: 14 }
|
Accept: 'application/json',
|
||||||
}
|
'Content-Type': 'application/json'
|
||||||
const buttonLayout = {
|
},
|
||||||
offset: 6,
|
body: JSON.stringify(values)
|
||||||
span: 14
|
})
|
||||||
}
|
.then(g => navigate(`/admin/quests/${g.id}/`))
|
||||||
|
.catch(({ message }) => setError('Ошибка создания'))
|
||||||
|
}
|
||||||
|
|
||||||
const onFinish = (values) => {
|
return (
|
||||||
ajax('/api/admin/games', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
Accept: 'application/json',
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify(values)
|
|
||||||
})
|
|
||||||
.then(g => navigate(`/admin/quests/${g.id}/`))
|
|
||||||
.catch(({ message }) => setError('Ошибка создания'))
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
<>
|
||||||
<Title>{quest.title ? (quest.title) : ('Новый квест')}</Title>
|
<Title>{quest.title ? (quest.title) : ('Новый квест')}</Title>
|
||||||
{error ? <Alert type="error" message={error} /> : null}
|
{error ? <Alert type="error" message={error} /> : null}
|
||||||
<Row gutter={8}>
|
<Row gutter={8}>
|
||||||
<Col xs={24} sm={16} md={16}>
|
<Col xs={24} sm={16} md={16}>
|
||||||
<Form
|
<Form
|
||||||
initialValues={quest}
|
initialValues={quest}
|
||||||
onFinish={onFinish}
|
onFinish={onFinish}
|
||||||
{...formItemLayout}
|
{...formItemLayout}
|
||||||
onValuesChange={(_, allFields) => setFields(allFields)}
|
onValuesChange={(_, allFields) => setFields(allFields)}
|
||||||
>
|
>
|
||||||
<Form.Item wrapperCol={buttonLayout}>
|
<Form.Item wrapperCol={buttonLayout}>
|
||||||
<Button.Group block>
|
<Button.Group block>
|
||||||
<Button type='primary' htmlType='submit' block>
|
<Button type='primary' htmlType='submit' block>
|
||||||
Сохранить квест
|
Сохранить квест
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button type='default' block onClick={() => setPreview(true)}>
|
|
||||||
Предпросмотр
|
|
||||||
</Button>
|
|
||||||
</Button.Group>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item name='id' hidden>
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item label='Опубликован?' name='visible'>
|
|
||||||
<Switch />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item label='Название' name='title'>
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item label='Описание' name='description' help='Поддерживается Markdown'>
|
|
||||||
<Input.TextArea />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
name='icon'
|
|
||||||
label='Иконка'
|
|
||||||
getValueFromEvent={normFile}
|
|
||||||
>
|
|
||||||
{quest.icon ? <Avatar src={`/api/file/${quest.icon}`} /> : null}
|
|
||||||
<Upload name='file' action={`/api/admin/file/${quest.id}/upload`} listType='picture' maxCount={1}>
|
|
||||||
<Button icon={<UploadOutlined />}>Загрузка</Button>
|
|
||||||
</Upload>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item label='Тип квеста' name='type'>
|
|
||||||
<Radio.Group>
|
|
||||||
<Radio.Button value='city'>Полевой</Radio.Button>
|
|
||||||
<Radio.Button value='virtual'>Виртуальный</Radio.Button>
|
|
||||||
</Radio.Group>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item label='Очков опыта за квест' name='points'>
|
|
||||||
<InputNumber />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.List name='tasks'>
|
|
||||||
{(tasks, { add, remove }) => (
|
|
||||||
<>
|
|
||||||
{tasks.map(renderTaskForm(remove))}
|
|
||||||
<Form.Item wrapperCol={buttonLayout}>
|
|
||||||
<Button type='primary' onClick={() => add()} block>
|
|
||||||
<PlusOutlined/> Добавить уровень
|
|
||||||
</Button>
|
</Button>
|
||||||
</Form.Item>
|
|
||||||
</>
|
<Button type='default' block onClick={() => setPreview(true)}>
|
||||||
)}
|
Предпросмотр
|
||||||
</Form.List>
|
</Button>
|
||||||
</Form>
|
</Button.Group>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name='id' hidden>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label='Опубликован?' name='visible'>
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label='Название' name='title'>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label='Описание' name='description' help='Поддерживается Markdown'>
|
||||||
|
<Input.TextArea />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name='icon'
|
||||||
|
label='Иконка'
|
||||||
|
getValueFromEvent={normFile}
|
||||||
|
>
|
||||||
|
{quest.icon ? <Avatar src={`/api/file/${quest.icon}`} /> : null}
|
||||||
|
<Upload name='file' action={`/api/admin/file/${quest.id}/upload`} listType='picture' maxCount={1}>
|
||||||
|
<Button icon={<UploadOutlined />}>Загрузка</Button>
|
||||||
|
</Upload>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label='Тип квеста' name='type'>
|
||||||
|
<Radio.Group>
|
||||||
|
<Radio.Button value='city'>Полевой</Radio.Button>
|
||||||
|
<Radio.Button value='virtual'>Виртуальный</Radio.Button>
|
||||||
|
</Radio.Group>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label='Очков опыта за квест' name='points'>
|
||||||
|
<InputNumber />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.List name='tasks'>
|
||||||
|
{(tasks, { add, remove }) => (
|
||||||
|
<>
|
||||||
|
{tasks.map(renderTaskForm(remove))}
|
||||||
|
<Form.Item wrapperCol={buttonLayout}>
|
||||||
|
<Button type='primary' onClick={() => add()} block>
|
||||||
|
<PlusOutlined /> Добавить уровень
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Form.List>
|
||||||
|
</Form>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={24} sm={8} md={8}>
|
<Col xs={24} sm={8} md={8}>
|
||||||
<Title>Файлы</Title>
|
<Title>Файлы</Title>
|
||||||
|
@ -132,41 +132,40 @@ const Quest = () => {
|
||||||
action={`/api/admin/file/${quest.id}/upload`}
|
action={`/api/admin/file/${quest.id}/upload`}
|
||||||
listType='picture'
|
listType='picture'
|
||||||
maxCount={10}
|
maxCount={10}
|
||||||
itemRender={renderFile}
|
itemRender={(e, file) => renderFile(e, file, quest)}
|
||||||
>
|
>
|
||||||
<Button icon={<UploadOutlined />}>Загрузка</Button>
|
<Button icon={<UploadOutlined />}>Загрузка</Button>
|
||||||
</Upload>
|
</Upload>
|
||||||
Ранее загруженные файлы:
|
Ранее загруженные файлы:
|
||||||
<List dataSource={files} renderItem={renderFileItem} />
|
<List dataSource={files} renderItem={x => renderFileItem(x, quest)} />
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
<Modal
|
<Modal
|
||||||
title="Предпросмотр квеста"
|
title="Предпросмотр квеста"
|
||||||
open={preview}
|
open={preview}
|
||||||
footer={<Button onClick={() => setPreview(false)}>Закрыть</Button>}
|
footer={<Button onClick={() => setPreview(false)}>Закрыть</Button>}
|
||||||
width={'80%'}
|
width={'80%'}
|
||||||
centered
|
centered
|
||||||
>
|
>
|
||||||
<List dataSource={fields.tasks} renderItem={(task) => (
|
<List dataSource={fields.tasks} renderItem={(task) => (
|
||||||
<List.Item key={task.id}>
|
<List.Item key={task.id}>
|
||||||
<List.Item.Meta
|
<List.Item.Meta
|
||||||
title={task.title}
|
title={task.title}
|
||||||
description={
|
description={
|
||||||
<><Card>
|
<><Card>
|
||||||
<Markdown>{task.text}</Markdown>
|
<Markdown>{task.text}</Markdown>
|
||||||
</Card>
|
</Card>
|
||||||
Коды:
|
Коды:
|
||||||
<ul>
|
<ul>
|
||||||
{task.codes.map(c => <li key={c.key}>{c.code}</li>)}
|
{task.codes.map(c => <li key={c.key}>{c.code}</li>)}
|
||||||
</ul>
|
</ul>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</List.Item>
|
</List.Item>
|
||||||
)} />
|
)} />
|
||||||
</Modal>
|
</Modal>
|
||||||
</>
|
</>)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line react/display-name
|
// eslint-disable-next-line react/display-name
|
||||||
|
@ -184,11 +183,11 @@ const renderTaskForm = remove => task => (
|
||||||
cancelText='Нет'
|
cancelText='Нет'
|
||||||
>
|
>
|
||||||
<Button danger>
|
<Button danger>
|
||||||
<CloseOutlined/> Удалить уровень
|
<CloseOutlined /> Удалить уровень
|
||||||
</Button>
|
</Button>
|
||||||
</Popconfirm>
|
</Popconfirm>
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Form.Item name={[task.name, 'id']} hidden>
|
<Form.Item name={[task.name, 'id']} hidden>
|
||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
@ -203,9 +202,9 @@ const renderTaskForm = remove => task => (
|
||||||
<>
|
<>
|
||||||
{codes.map(renderCodeForm(codesOpts.remove))}
|
{codes.map(renderCodeForm(codesOpts.remove))}
|
||||||
<Form.Item wrapperCol={{ offset: 6, span: 14 }}>
|
<Form.Item wrapperCol={{ offset: 6, span: 14 }}>
|
||||||
<Button key='addCode' type='primary' onClick={() => codesOpts.add()} block>
|
<Button key='addCode' type='primary' onClick={() => codesOpts.add()} block>
|
||||||
<PlusOutlined/> Добавить код
|
<PlusOutlined /> Добавить код
|
||||||
</Button>
|
</Button>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
@ -219,19 +218,19 @@ const renderCodeForm = remove => code => (
|
||||||
key={code.key}
|
key={code.key}
|
||||||
style={{ marginBottom: 8 }}
|
style={{ marginBottom: 8 }}
|
||||||
actions={[
|
actions={[
|
||||||
// <Popconfirm
|
// <Popconfirm
|
||||||
// key='delete'
|
// key='delete'
|
||||||
// title='Удалить код?'
|
// title='Удалить код?'
|
||||||
// onConfirm={() => remove(code.name)}
|
// onConfirm={() => remove(code.name)}
|
||||||
// okText='Да'
|
// okText='Да'
|
||||||
// cancelText='Нет'
|
// cancelText='Нет'
|
||||||
// >
|
// >
|
||||||
<Button key="delete" danger onClick={() => remove(code.name)}>
|
<Button key="delete" danger onClick={() => remove(code.name)}>
|
||||||
<CloseOutlined/> Удалить код
|
<CloseOutlined /> Удалить код
|
||||||
</Button>
|
</Button>
|
||||||
// </Popconfirm>
|
// </Popconfirm>
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Form.Item name={[code.name, 'id']} hidden>
|
<Form.Item name={[code.name, 'id']} hidden>
|
||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
@ -244,21 +243,24 @@ const renderCodeForm = remove => code => (
|
||||||
</Card>
|
</Card>
|
||||||
)
|
)
|
||||||
|
|
||||||
const renderFile = (e, file) => (
|
const renderFile = (e, file, quest) => {
|
||||||
<div key={file ? file.uid : null}>
|
console.log(file)
|
||||||
{e}
|
return (
|
||||||
{file && file.response && file.response.uuid
|
<div key={file ? file.uid : null}>
|
||||||
? <>Код для вставки: <pre>![](/api/file/{file.response.uuid})</pre></>
|
{e}
|
||||||
: null}
|
{file && file.response && file.response.uuid
|
||||||
</div>
|
? <>Код для вставки: <pre>![](/file/{quest.id}/{file.originFileObj.name})</pre></>
|
||||||
)
|
: null}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const renderFileItem = (file) => (
|
const renderFileItem = (file, quest) => (
|
||||||
<List.Item>
|
<List.Item>
|
||||||
<List.Item.Meta
|
<List.Item.Meta
|
||||||
avatar={<Avatar src={`/api/file/${file.id}`} />}
|
avatar={<Avatar src={`/file/${quest.id})/${file.originalName}`} />}
|
||||||
title={file.originalName}
|
title={file.originalName}
|
||||||
description={<>Код для вставки: <pre>![](/api/file/{file.id})</pre></>}
|
description={<>Код для вставки: <pre>![](/file/{quest.id}/{file.originalName})</pre></>}
|
||||||
/>
|
/>
|
||||||
</List.Item>
|
</List.Item>
|
||||||
|
|
||||||
|
|
|
@ -4,9 +4,9 @@ import { Link, useLoaderData } from 'react-router-dom'
|
||||||
const { Title } = Typography
|
const { Title } = Typography
|
||||||
|
|
||||||
const Quests = () => {
|
const Quests = () => {
|
||||||
const quests = useLoaderData()
|
const quests = useLoaderData()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Title>Управление своими квестами</Title>
|
<Title>Управление своими квестами</Title>
|
||||||
<Link to="/admin/quests/new">Создать новый квест</Link>
|
<Link to="/admin/quests/new">Создать новый квест</Link>
|
||||||
|
@ -14,29 +14,29 @@ const Quests = () => {
|
||||||
dataSource={quests}
|
dataSource={quests}
|
||||||
rowKey={'id'}
|
rowKey={'id'}
|
||||||
columns={[
|
columns={[
|
||||||
{
|
{
|
||||||
title: 'Опубликован?',
|
title: 'Опубликован?',
|
||||||
dataIndex: 'visible',
|
dataIndex: 'visible',
|
||||||
key: 'visible',
|
key: 'visible',
|
||||||
render: visible => visible ? 'Да' : 'Нет'
|
render: visible => visible ? 'Да' : 'Нет'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Название',
|
title: 'Название',
|
||||||
dataIndex: 'title',
|
dataIndex: 'title',
|
||||||
key: 'title',
|
key: 'title',
|
||||||
render: (title, q) => <Link to={`/admin/quests/${q.id}`}>{title}</Link>
|
render: (title, q) => <Link to={`/admin/quests/${q.id}`}>{title}</Link>
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Тип',
|
title: 'Тип',
|
||||||
dataIndex: 'type',
|
dataIndex: 'type',
|
||||||
key: 'type',
|
key: 'type',
|
||||||
render: type => type === 'virtual' ? 'Виртуальный' : 'Полевой'
|
render: type => type === 'virtual' ? 'Виртуальный' : 'Полевой'
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
|
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Quests
|
export default Quests
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { UserProvider } from './user'
|
import { UserProvider } from './user'
|
||||||
|
|
||||||
export const store = [
|
export const store = [
|
||||||
UserProvider.Provider
|
UserProvider.Provider
|
||||||
]
|
]
|
||||||
|
|
|
@ -2,8 +2,8 @@ import { useState } from 'react'
|
||||||
import { createContainer } from 'unstated-next'
|
import { createContainer } from 'unstated-next'
|
||||||
|
|
||||||
const useUser = () => {
|
const useUser = () => {
|
||||||
const [user, setUser] = useState(null)
|
const [user, setUser] = useState(null)
|
||||||
return { user, setUser }
|
return { user, setUser }
|
||||||
}
|
}
|
||||||
|
|
||||||
export const UserProvider = createContainer(useUser)
|
export const UserProvider = createContainer(useUser)
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
export const ajax = async (path, params) => {
|
export const ajax = async (path, params) => {
|
||||||
return fetch(path, params)
|
return fetch(path, params)
|
||||||
.then(r => {
|
.then(r => {
|
||||||
if (r.status < 200 || r.status >= 300) {
|
if (r.status < 200 || r.status >= 300) {
|
||||||
throw Error(r.statusText)
|
throw Error(r.statusText)
|
||||||
}
|
}
|
||||||
return r
|
return r
|
||||||
})
|
})
|
||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,24 +1,24 @@
|
||||||
import { UserProvider } from '../store/user'
|
import { UserProvider } from '../store/user'
|
||||||
|
|
||||||
const roleHierarchy = {
|
const roleHierarchy = {
|
||||||
user: {
|
user: {
|
||||||
user: true
|
user: true
|
||||||
},
|
},
|
||||||
creator: {
|
creator: {
|
||||||
user: true,
|
user: true,
|
||||||
creator: true
|
creator: true
|
||||||
},
|
},
|
||||||
admin: {
|
admin: {
|
||||||
user: true,
|
user: true,
|
||||||
creator: true,
|
creator: true,
|
||||||
admin: true
|
admin: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useRole = () => {
|
export const useRole = () => {
|
||||||
const { user } = UserProvider.useContainer()
|
const { user } = UserProvider.useContainer()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
hasRole: (role) => user && user.role && !!roleHierarchy[user.role][role]
|
hasRole: (role) => user && user.role && !!roleHierarchy[user.role][role]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
export function uuidv4 () {
|
export function uuidv4 () {
|
||||||
return '10000000-1000-4000-8000-100000000000'.replace(/[018]/g, c =>
|
return '10000000-1000-4000-8000-100000000000'.replace(/[018]/g, c =>
|
||||||
(+c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> +c / 4).toString(16)
|
(+c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> +c / 4).toString(16)
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,88 +3,95 @@ import react from '@vitejs/plugin-react'
|
||||||
import { VitePWA } from 'vite-plugin-pwa'
|
import { VitePWA } from 'vite-plugin-pwa'
|
||||||
|
|
||||||
const manifest = {
|
const manifest = {
|
||||||
registerType: 'autoUpdate',
|
registerType: 'autoUpdate',
|
||||||
includeAssets: ['assets/icon.png', 'assets/logo.png'],
|
includeAssets: ['assets/icon.png', 'assets/logo.png'],
|
||||||
workbox: {
|
workbox: {
|
||||||
cleanupOutdatedCaches: true
|
cleanupOutdatedCaches: true
|
||||||
},
|
},
|
||||||
manifest: {
|
base: 'assets',
|
||||||
name: 'NQuest',
|
manifest: {
|
||||||
short_name: 'NQuest',
|
name: 'NQuest',
|
||||||
description: 'NQuest - платформа для ARG игр.',
|
short_name: 'NQuest',
|
||||||
icons: [
|
description: 'NQuest - платформа для ARG игр.',
|
||||||
{
|
icons: [
|
||||||
src: 'assets/icons/icon-72x72.png',
|
{
|
||||||
sizes: '72x72',
|
src: 'assets/icons/icon-72x72.png',
|
||||||
type: 'image/png',
|
sizes: '72x72',
|
||||||
purpose: 'maskable any'
|
type: 'image/png',
|
||||||
},
|
purpose: 'maskable any'
|
||||||
{
|
},
|
||||||
src: 'assets/icons/icon-96x96.png',
|
{
|
||||||
sizes: '96x96',
|
src: 'assets/icons/icon-96x96.png',
|
||||||
type: 'image/png',
|
sizes: '96x96',
|
||||||
purpose: 'maskable any'
|
type: 'image/png',
|
||||||
},
|
purpose: 'maskable any'
|
||||||
{
|
},
|
||||||
src: 'assets/icons/icon-128x128.png',
|
{
|
||||||
sizes: '128x128',
|
src: 'assets/icons/icon-128x128.png',
|
||||||
type: 'image/png',
|
sizes: '128x128',
|
||||||
purpose: 'maskable any'
|
type: 'image/png',
|
||||||
},
|
purpose: 'maskable any'
|
||||||
{
|
},
|
||||||
src: 'assets/icons/icon-144x144.png',
|
{
|
||||||
sizes: '144x144',
|
src: 'assets/icons/icon-144x144.png',
|
||||||
type: 'image/png',
|
sizes: '144x144',
|
||||||
purpose: 'maskable any'
|
type: 'image/png',
|
||||||
},
|
purpose: 'maskable any'
|
||||||
{
|
},
|
||||||
src: 'assets/icons/icon-152x152.png',
|
{
|
||||||
sizes: '152x152',
|
src: 'assets/icons/icon-152x152.png',
|
||||||
type: 'image/png',
|
sizes: '152x152',
|
||||||
purpose: 'maskable any'
|
type: 'image/png',
|
||||||
},
|
purpose: 'maskable any'
|
||||||
{
|
},
|
||||||
src: 'assets/icons/icon-192x192.png',
|
{
|
||||||
sizes: '192x192',
|
src: 'assets/icons/icon-192x192.png',
|
||||||
type: 'image/png',
|
sizes: '192x192',
|
||||||
purpose: 'maskable any'
|
type: 'image/png',
|
||||||
},
|
purpose: 'maskable any'
|
||||||
{
|
},
|
||||||
src: 'assets/icons/icon-384x384.png',
|
{
|
||||||
sizes: '384x384',
|
src: 'assets/icons/icon-384x384.png',
|
||||||
type: 'image/png',
|
sizes: '384x384',
|
||||||
purpose: 'maskable any'
|
type: 'image/png',
|
||||||
},
|
purpose: 'maskable any'
|
||||||
{
|
},
|
||||||
src: 'assets/icons/icon-512x512.png',
|
{
|
||||||
sizes: '512x512',
|
src: 'assets/icons/icon-512x512.png',
|
||||||
type: 'image/png',
|
sizes: '512x512',
|
||||||
purpose: 'maskable any'
|
type: 'image/png',
|
||||||
}
|
purpose: 'maskable any'
|
||||||
],
|
}
|
||||||
theme_color: '#59FBEA',
|
],
|
||||||
background_color: '#171e26',
|
theme_color: '#59FBEA',
|
||||||
display: 'standalone',
|
background_color: '#171e26',
|
||||||
scope: '/',
|
display: 'standalone',
|
||||||
start_url: '/quests'
|
scope: '/',
|
||||||
}
|
start_url: '/quests'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react(), splitVendorChunkPlugin(), VitePWA(manifest)],
|
plugins: [react(), splitVendorChunkPlugin(), VitePWA(manifest)],
|
||||||
server: {
|
server: {
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
'/api': {
|
||||||
target: 'http://localhost:8000',
|
target: 'http://localhost:8000',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
secure: false,
|
secure: false,
|
||||||
ws: false
|
ws: false
|
||||||
}
|
},
|
||||||
}
|
'/file': {
|
||||||
},
|
target: 'http://localhost:8000',
|
||||||
build: {
|
changeOrigin: true,
|
||||||
|
secure: false,
|
||||||
|
ws: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
build: {
|
||||||
// generate .vite/manifest.json in outDir
|
// generate .vite/manifest.json in outDir
|
||||||
manifest: true
|
manifest: true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
8
main.go
8
main.go
|
@ -127,12 +127,12 @@ func main() {
|
||||||
|
|
||||||
api.RegisterHandlersWithBaseURL(codegen, handler, "/api")
|
api.RegisterHandlersWithBaseURL(codegen, handler, "/api")
|
||||||
|
|
||||||
e.FileFS("/", "index.html", distIndexHtml)
|
|
||||||
e.FileFS("/*", "index.html", distIndexHtml)
|
|
||||||
e.StaticFS("/", distDirFS)
|
|
||||||
|
|
||||||
// --[ System ]--
|
// --[ System ]--
|
||||||
e.GET("/metrics", echoprometheus.NewHandler())
|
e.GET("/metrics", echoprometheus.NewHandler())
|
||||||
|
|
||||||
|
e.StaticFS("/file", afero.NewIOFS(storage))
|
||||||
|
e.StaticFS("/*", distDirFS)
|
||||||
|
|
||||||
e.Logger.Debugf("backend version %s", Version)
|
e.Logger.Debugf("backend version %s", Version)
|
||||||
e.Logger.Fatal(e.Start(cfg.Listen))
|
e.Logger.Fatal(e.Start(cfg.Listen))
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,7 +45,7 @@ func (a *Admin) AdminEditGame(ctx echo.Context) error {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
tasks = append(tasks, api.TaskEdit{
|
tasks = append(tasks, api.TaskEdit{
|
||||||
Id: &t.ID,
|
Id: t.ID,
|
||||||
Codes: codes,
|
Codes: codes,
|
||||||
Text: t.Text,
|
Text: t.Text,
|
||||||
Title: t.Title,
|
Title: t.Title,
|
||||||
|
@ -98,7 +98,7 @@ func (a *Admin) AdminGetGame(ctx echo.Context, uid uuid.UUID) error {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
tasks = append(tasks, api.TaskEdit{
|
tasks = append(tasks, api.TaskEdit{
|
||||||
Id: &t.ID,
|
Id: t.ID,
|
||||||
Codes: codes,
|
Codes: codes,
|
||||||
Text: t.Text,
|
Text: t.Text,
|
||||||
Title: t.Title,
|
Title: t.Title,
|
||||||
|
@ -163,12 +163,8 @@ func (*Admin) mapCreateGameRequest(req *api.GameEdit, user *models.User) *models
|
||||||
IconID: req.Icon,
|
IconID: req.Icon,
|
||||||
}
|
}
|
||||||
for order, te := range req.Tasks {
|
for order, te := range req.Tasks {
|
||||||
id := uuid.New()
|
|
||||||
if te.Id != nil {
|
|
||||||
id = *te.Id
|
|
||||||
}
|
|
||||||
task := &models.Task{
|
task := &models.Task{
|
||||||
ID: id,
|
ID: te.Id,
|
||||||
Title: te.Title,
|
Title: te.Title,
|
||||||
Text: te.Text,
|
Text: te.Text,
|
||||||
Codes: make([]*models.Code, 0, len(te.Codes)),
|
Codes: make([]*models.Code, 0, len(te.Codes)),
|
||||||
|
|
|
@ -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 {
|
func (u *File) AdminListFiles(c echo.Context, quest uuid.UUID) error {
|
||||||
fl, err := u.FileService.GetFilesByQuest(c.Request().Context(), quest)
|
fl, err := u.FileService.GetFilesByQuest(c.Request().Context(), quest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -10,6 +10,7 @@ type Task struct {
|
||||||
Title string
|
Title string
|
||||||
Text string
|
Text string
|
||||||
MaxTime int
|
MaxTime int
|
||||||
|
Game *Game
|
||||||
GameID uuid.UUID
|
GameID uuid.UUID
|
||||||
Codes []*Code `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"`
|
Codes []*Code `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"`
|
||||||
TaskOrder uint
|
TaskOrder uint
|
||||||
|
|
|
@ -3,7 +3,6 @@ package service
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
@ -53,21 +52,6 @@ func (u *File) Upload(
|
||||||
return file.ID, u.DB.WithContext(ctx).Create(file).Error
|
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) {
|
func (u *File) GetFilesByQuest(ctx context.Context, quest uuid.UUID) ([]*models.File, error) {
|
||||||
list := make([]*models.File, 0)
|
list := make([]*models.File, 0)
|
||||||
|
|
||||||
|
|
|
@ -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) {
|
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}).
|
Session(&gorm.Session{FullSaveAssociations: true}).
|
||||||
Clauses(clause.OnConflict{
|
Clauses(clause.OnConflict{
|
||||||
Columns: []clause.Column{{Name: "id"}},
|
Columns: []clause.Column{{Name: "id"}},
|
||||||
|
@ -76,6 +83,11 @@ func (gs *Game) UpsertGame(ctx context.Context, game *models.Game) (*models.Game
|
||||||
"title", "description",
|
"title", "description",
|
||||||
}),
|
}),
|
||||||
}).
|
}).
|
||||||
Create(&game).
|
Create(game).
|
||||||
Error
|
Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return game, gs.DB.Save(game).Error
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue