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