Почти рабочая админка
This commit is contained in:
parent
10a355d0f1
commit
fac2df0bc1
11 changed files with 478 additions and 204 deletions
129
api/openapi.yaml
129
api/openapi.yaml
|
@ -13,52 +13,44 @@ paths:
|
|||
get:
|
||||
responses:
|
||||
200:
|
||||
$ref: '#/components/responses/userResponse'
|
||||
$ref: "#/components/responses/userResponse"
|
||||
403:
|
||||
$ref: '#/components/responses/errorResponse'
|
||||
$ref: "#/components/responses/errorResponse"
|
||||
/user/login:
|
||||
post:
|
||||
security: []
|
||||
requestBody:
|
||||
$ref: '#/components/requestBodies/login'
|
||||
$ref: "#/components/requestBodies/login"
|
||||
responses:
|
||||
200:
|
||||
$ref: '#/components/responses/userResponse'
|
||||
$ref: "#/components/responses/userResponse"
|
||||
400:
|
||||
$ref: '#/components/responses/errorResponse'
|
||||
$ref: "#/components/responses/errorResponse"
|
||||
/user/register:
|
||||
post:
|
||||
security: []
|
||||
requestBody:
|
||||
$ref: '#/components/requestBodies/register'
|
||||
$ref: "#/components/requestBodies/register"
|
||||
responses:
|
||||
200:
|
||||
$ref: '#/components/responses/userResponse'
|
||||
$ref: "#/components/responses/userResponse"
|
||||
400:
|
||||
$ref: '#/components/responses/errorResponse'
|
||||
$ref: "#/components/responses/errorResponse"
|
||||
/user/logout:
|
||||
post:
|
||||
responses:
|
||||
204:
|
||||
description: "success logout"
|
||||
400:
|
||||
$ref: '#/components/responses/errorResponse'
|
||||
$ref: "#/components/responses/errorResponse"
|
||||
|
||||
# Game routes
|
||||
/games:
|
||||
get:
|
||||
responses:
|
||||
200:
|
||||
$ref: '#/components/responses/gameListResponse'
|
||||
post:
|
||||
operationId: createGame
|
||||
security:
|
||||
- cookieAuth: [creator, admin]
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/gameEditRequest"
|
||||
responses:
|
||||
200:
|
||||
$ref: "#/components/responses/gameResponse"
|
||||
$ref: "#/components/responses/gameListResponse"
|
||||
|
||||
/engine/{uid}:
|
||||
get:
|
||||
operationId: gameEngine
|
||||
|
@ -71,7 +63,7 @@ paths:
|
|||
format: uuid
|
||||
responses:
|
||||
200:
|
||||
$ref: '#/components/responses/taskResponse'
|
||||
$ref: "#/components/responses/taskResponse"
|
||||
/engine/{uid}/code:
|
||||
post:
|
||||
operationId: enterCode
|
||||
|
@ -86,7 +78,7 @@ paths:
|
|||
$ref: "#/components/requestBodies/enterCodeRequest"
|
||||
responses:
|
||||
200:
|
||||
$ref: '#/components/responses/taskResponse'
|
||||
$ref: "#/components/responses/taskResponse"
|
||||
/file/upload:
|
||||
post:
|
||||
operationId: uploadFile
|
||||
|
@ -103,7 +95,7 @@ paths:
|
|||
format: binary
|
||||
responses:
|
||||
200:
|
||||
$ref: '#/components/responses/uploadResponse'
|
||||
$ref: "#/components/responses/uploadResponse"
|
||||
/file/{uid}:
|
||||
get:
|
||||
operationId: getFile
|
||||
|
@ -118,11 +110,40 @@ paths:
|
|||
200:
|
||||
description: file
|
||||
content:
|
||||
'application/octet-stream':
|
||||
"application/octet-stream":
|
||||
schema:
|
||||
type: string
|
||||
format: binary
|
||||
/games/{uid}:
|
||||
/admin/games:
|
||||
get:
|
||||
operationId: listGamesByAdmin
|
||||
responses:
|
||||
200:
|
||||
$ref: "#/components/responses/gameListResponse"
|
||||
post:
|
||||
operationId: createGame
|
||||
security:
|
||||
- cookieAuth: [creator, admin]
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/gameEditRequest"
|
||||
responses:
|
||||
200:
|
||||
$ref: "#/components/responses/gameAdminResponse"
|
||||
/admin/games/{uid}:
|
||||
get:
|
||||
operationId: getGameByAdmin
|
||||
parameters:
|
||||
- name: uid
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
security:
|
||||
- cookieAuth: [creator, admin]
|
||||
responses:
|
||||
200:
|
||||
$ref: "#/components/responses/gameAdminResponse"
|
||||
post:
|
||||
operationId: editGame
|
||||
parameters:
|
||||
|
@ -138,7 +159,7 @@ paths:
|
|||
$ref: "#/components/requestBodies/gameEditRequest"
|
||||
responses:
|
||||
200:
|
||||
$ref: "#/components/responses/gameResponse"
|
||||
$ref: "#/components/responses/gameAdminResponse"
|
||||
components:
|
||||
schemas:
|
||||
userView:
|
||||
|
@ -232,7 +253,7 @@ components:
|
|||
codes:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/codeView'
|
||||
$ref: "#/components/schemas/codeView"
|
||||
# solutions:
|
||||
# type: array
|
||||
# items:
|
||||
|
@ -263,6 +284,11 @@ components:
|
|||
gameEdit:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
format: uuid
|
||||
visible:
|
||||
type: boolean
|
||||
title:
|
||||
type: string
|
||||
description:
|
||||
|
@ -279,6 +305,7 @@ components:
|
|||
type: string
|
||||
format: uuid
|
||||
required:
|
||||
- visible
|
||||
- title
|
||||
- description
|
||||
- type
|
||||
|
@ -288,6 +315,9 @@ components:
|
|||
taskEdit:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
format: uuid
|
||||
title:
|
||||
type: string
|
||||
text:
|
||||
|
@ -295,7 +325,7 @@ components:
|
|||
codes:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/codeEdit'
|
||||
$ref: "#/components/schemas/codeEdit"
|
||||
# solutions:
|
||||
# type: array
|
||||
# items:
|
||||
|
@ -308,6 +338,9 @@ components:
|
|||
codeEdit:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
format: uuid
|
||||
description:
|
||||
type: string
|
||||
code:
|
||||
|
@ -334,7 +367,7 @@ components:
|
|||
login:
|
||||
required: true
|
||||
content:
|
||||
'application/json':
|
||||
"application/json":
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
|
@ -346,7 +379,7 @@ components:
|
|||
register:
|
||||
required: true
|
||||
content:
|
||||
'application/json':
|
||||
"application/json":
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
|
@ -362,13 +395,13 @@ components:
|
|||
gameEditRequest:
|
||||
required: true
|
||||
content:
|
||||
'application/json':
|
||||
"application/json":
|
||||
schema:
|
||||
$ref: '#/components/schemas/gameEdit'
|
||||
$ref: "#/components/schemas/gameEdit"
|
||||
enterCodeRequest:
|
||||
required: true
|
||||
content:
|
||||
'application/json':
|
||||
"application/json":
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
|
@ -378,15 +411,15 @@ components:
|
|||
- code
|
||||
responses:
|
||||
userResponse:
|
||||
description: ''
|
||||
description: ""
|
||||
content:
|
||||
'application/json':
|
||||
"application/json":
|
||||
schema:
|
||||
$ref: "#/components/schemas/userView"
|
||||
errorResponse:
|
||||
description: ''
|
||||
description: ""
|
||||
content:
|
||||
'application/json':
|
||||
"application/json":
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
|
@ -396,29 +429,35 @@ components:
|
|||
type: string
|
||||
required: [code, message]
|
||||
gameListResponse:
|
||||
description: ''
|
||||
description: ""
|
||||
content:
|
||||
'application/json':
|
||||
"application/json":
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/gameView"
|
||||
gameResponse:
|
||||
description: ''
|
||||
description: ""
|
||||
content:
|
||||
'application/json':
|
||||
"application/json":
|
||||
schema:
|
||||
$ref: "#/components/schemas/gameView"
|
||||
taskResponse:
|
||||
description: ''
|
||||
gameAdminResponse:
|
||||
description: ""
|
||||
content:
|
||||
'application/json':
|
||||
"application/json":
|
||||
schema:
|
||||
$ref: "#/components/schemas/gameEdit"
|
||||
taskResponse:
|
||||
description: ""
|
||||
content:
|
||||
"application/json":
|
||||
schema:
|
||||
$ref: "#/components/schemas/taskView"
|
||||
uploadResponse:
|
||||
description: ''
|
||||
description: ""
|
||||
content:
|
||||
'application/json':
|
||||
"application/json":
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
|
|
152
api/server.go
152
api/server.go
|
@ -22,6 +22,18 @@ import (
|
|||
// ServerInterface represents all server handlers.
|
||||
type ServerInterface interface {
|
||||
|
||||
// (GET /admin/games)
|
||||
ListGamesByAdmin(ctx echo.Context) error
|
||||
|
||||
// (POST /admin/games)
|
||||
CreateGame(ctx echo.Context) error
|
||||
|
||||
// (GET /admin/games/{uid})
|
||||
GetGameByAdmin(ctx echo.Context, uid openapi_types.UUID) error
|
||||
|
||||
// (POST /admin/games/{uid})
|
||||
EditGame(ctx echo.Context, uid openapi_types.UUID) error
|
||||
|
||||
// (GET /engine/{uid})
|
||||
GameEngine(ctx echo.Context, uid openapi_types.UUID) error
|
||||
|
||||
|
@ -37,12 +49,6 @@ type ServerInterface interface {
|
|||
// (GET /games)
|
||||
GetGames(ctx echo.Context) error
|
||||
|
||||
// (POST /games)
|
||||
CreateGame(ctx echo.Context) error
|
||||
|
||||
// (POST /games/{uid})
|
||||
EditGame(ctx echo.Context, uid openapi_types.UUID) error
|
||||
|
||||
// (GET /user)
|
||||
GetUser(ctx echo.Context) error
|
||||
|
||||
|
@ -61,6 +67,64 @@ type ServerInterfaceWrapper struct {
|
|||
Handler ServerInterface
|
||||
}
|
||||
|
||||
// ListGamesByAdmin converts echo context to params.
|
||||
func (w *ServerInterfaceWrapper) ListGamesByAdmin(ctx echo.Context) error {
|
||||
var err error
|
||||
|
||||
ctx.Set(CookieAuthScopes, []string{})
|
||||
|
||||
// Invoke the callback with all the unmarshaled arguments
|
||||
err = w.Handler.ListGamesByAdmin(ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
// CreateGame converts echo context to params.
|
||||
func (w *ServerInterfaceWrapper) CreateGame(ctx echo.Context) error {
|
||||
var err error
|
||||
|
||||
ctx.Set(CookieAuthScopes, []string{"creator", "admin"})
|
||||
|
||||
// Invoke the callback with all the unmarshaled arguments
|
||||
err = w.Handler.CreateGame(ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetGameByAdmin converts echo context to params.
|
||||
func (w *ServerInterfaceWrapper) GetGameByAdmin(ctx echo.Context) error {
|
||||
var err error
|
||||
// ------------- Path parameter "uid" -------------
|
||||
var uid openapi_types.UUID
|
||||
|
||||
err = runtime.BindStyledParameterWithLocation("simple", false, "uid", runtime.ParamLocationPath, ctx.Param("uid"), &uid)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter uid: %s", err))
|
||||
}
|
||||
|
||||
ctx.Set(CookieAuthScopes, []string{"creator", "admin"})
|
||||
|
||||
// Invoke the callback with all the unmarshaled arguments
|
||||
err = w.Handler.GetGameByAdmin(ctx, uid)
|
||||
return err
|
||||
}
|
||||
|
||||
// EditGame converts echo context to params.
|
||||
func (w *ServerInterfaceWrapper) EditGame(ctx echo.Context) error {
|
||||
var err error
|
||||
// ------------- Path parameter "uid" -------------
|
||||
var uid openapi_types.UUID
|
||||
|
||||
err = runtime.BindStyledParameterWithLocation("simple", false, "uid", runtime.ParamLocationPath, ctx.Param("uid"), &uid)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter uid: %s", err))
|
||||
}
|
||||
|
||||
ctx.Set(CookieAuthScopes, []string{"creator", "admin"})
|
||||
|
||||
// Invoke the callback with all the unmarshaled arguments
|
||||
err = w.Handler.EditGame(ctx, uid)
|
||||
return err
|
||||
}
|
||||
|
||||
// GameEngine converts echo context to params.
|
||||
func (w *ServerInterfaceWrapper) GameEngine(ctx echo.Context) error {
|
||||
var err error
|
||||
|
@ -137,35 +201,6 @@ func (w *ServerInterfaceWrapper) GetGames(ctx echo.Context) error {
|
|||
return err
|
||||
}
|
||||
|
||||
// CreateGame converts echo context to params.
|
||||
func (w *ServerInterfaceWrapper) CreateGame(ctx echo.Context) error {
|
||||
var err error
|
||||
|
||||
ctx.Set(CookieAuthScopes, []string{"creator", "admin"})
|
||||
|
||||
// Invoke the callback with all the unmarshaled arguments
|
||||
err = w.Handler.CreateGame(ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
// EditGame converts echo context to params.
|
||||
func (w *ServerInterfaceWrapper) EditGame(ctx echo.Context) error {
|
||||
var err error
|
||||
// ------------- Path parameter "uid" -------------
|
||||
var uid openapi_types.UUID
|
||||
|
||||
err = runtime.BindStyledParameterWithLocation("simple", false, "uid", runtime.ParamLocationPath, ctx.Param("uid"), &uid)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter uid: %s", err))
|
||||
}
|
||||
|
||||
ctx.Set(CookieAuthScopes, []string{"creator", "admin"})
|
||||
|
||||
// Invoke the callback with all the unmarshaled arguments
|
||||
err = w.Handler.EditGame(ctx, uid)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetUser converts echo context to params.
|
||||
func (w *ServerInterfaceWrapper) GetUser(ctx echo.Context) error {
|
||||
var err error
|
||||
|
@ -234,13 +269,15 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL
|
|||
Handler: si,
|
||||
}
|
||||
|
||||
router.GET(baseURL+"/admin/games", wrapper.ListGamesByAdmin)
|
||||
router.POST(baseURL+"/admin/games", wrapper.CreateGame)
|
||||
router.GET(baseURL+"/admin/games/:uid", wrapper.GetGameByAdmin)
|
||||
router.POST(baseURL+"/admin/games/:uid", wrapper.EditGame)
|
||||
router.GET(baseURL+"/engine/:uid", wrapper.GameEngine)
|
||||
router.POST(baseURL+"/engine/:uid/code", wrapper.EnterCode)
|
||||
router.POST(baseURL+"/file/upload", wrapper.UploadFile)
|
||||
router.GET(baseURL+"/file/:uid", wrapper.GetFile)
|
||||
router.GET(baseURL+"/games", wrapper.GetGames)
|
||||
router.POST(baseURL+"/games", wrapper.CreateGame)
|
||||
router.POST(baseURL+"/games/:uid", wrapper.EditGame)
|
||||
router.GET(baseURL+"/user", wrapper.GetUser)
|
||||
router.POST(baseURL+"/user/login", wrapper.PostUserLogin)
|
||||
router.POST(baseURL+"/user/logout", wrapper.PostUserLogout)
|
||||
|
@ -251,26 +288,27 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL
|
|||
// Base64 encoded, gzipped, json marshaled Swagger object
|
||||
var swaggerSpec = []string{
|
||||
|
||||
"H4sIAAAAAAAC/8xYTW/jNhD9KwXbozbyfpx02wbboGhQtGm2l8AIuNLE4UYiVXKUjRHovxdDUl8WZcle",
|
||||
"N80pDjki37yZNxzymaWqKJUEiYYlz0zDPxUY/FllAuwASAR9rjK4cjM0liqJIO1PXpa5SDkKJeOvRkka",
|
||||
"M+k9FJx+lVqVoNEvlaoM6C9uS2AJM6iF3LC6juyuQkPGkhtntY4aK/XlK6TI6qEZ6grqiG14AZ8ygcdg",
|
||||
"+0nDHUvYj3FHQOxmTdysO7FtrjZCfgcRUHCRB5iIWMmN+aZ0Nk+TW6P3xULKNGyEQdAvDb+bfBecrQxo",
|
||||
"yYsFCdJaRmMS+rssIsSOmFJJ433TWukrP3K6XBcSYQOaHC3AGL5ZKoTOPuxOBibVoiRMLGHMa+JSGDzK",
|
||||
"CYFQmCXq+FvAN9rNQ+Ja8+0+REehWQYivCly83DyTWnRfZtWZa54doL0qSphRXSndMGRJW4gmlMGGS3N",
|
||||
"E5LRyQmiRacJqiO/TCsRW2SXHhQ7C87pp28cTR0rbsJCfgEYof3bs2a0//5tIiZSNzGTJBErlfDH+7gc",
|
||||
"UUqbxdIna3cw7ko/YigwD/PlBub1fE12u/y5ZaOdcNolG/Sth56TKZqvPQ6QVUFLPwqNFacDJBW47X3W",
|
||||
"QW/LzCg6vMJ7pZdT10ljTF2qgSNkH/GIdDsgDxbVlPl0OVeVxInp/yYHHM59idAmQAewT2vUhmtPirTJ",
|
||||
"HawEyyPdFraQSOApHOQp5ibUYNeJPLApZ6bL2mHOTKVtr5VpJKUebn3fIuQjz0XW/Kvy9qeEJ7zN4RFI",
|
||||
"eRTzW9o3B4SgBF+MslahB3S88FReq/NKa5B4aV0K6sKa/Q5PMzagBch0onEkpswJmrTFdSCfxqpVPgg7",
|
||||
"UdcITtEvnhVCBsO5vNG3qALdfo+oBmQoECPWGwo9/ECjFDEDaaUFbv8iGhu9qAcBHyu8t+RTK+OGKJWt",
|
||||
"I8yAMb1ylDBeit/AN8RC3inrrEtXJv+0d9WIPYI2rjV6e7Y6WxE5qgTJS8ES9v7s7dnK3mfw3sKIQW6E",
|
||||
"hPi5EllNAxuwsqA8ta3arxlL2AV1E9bQfqt5AQh0St146LReB9xFfngninq93lz/ud65Pr1braYSs7WL",
|
||||
"B615bTkaOBc3jVepTMDFT82LxMt52LyJbKed6z2bxKM3k/pUNN2JHGJ3y5gm6LOd/0XYqrcDvtflF1WO",
|
||||
"ouQaY+LgTcYxcBGhDQc0fRGS623wKhK8bR/q9c4Vqq9JG+K+Gm/GBadedzzNKAXQU/S/yWTiyqVSBHxj",
|
||||
"UAMvhlev+SiMbl02gD572hNkipCLpj4eHLbRq4PdMZyf57Ytu3BF/WBx7b751cfC/d4cs2x2STZRrDKB",
|
||||
"3tPXWateDZ22hdiTm59di3FERek/dNQR+7B6P//R8B3Q64dWitvH33DA/1DGQr20ZkcExK1fn8bT1eGe",
|
||||
"DoK3HvitKlzkONmN8H8YPWkwU6UpGPODX/pYxB3G/uP2fpRXjeUREWp3eT1B2iu5NRUGA/qxKT2VzlnC",
|
||||
"Ymo063X9bwAAAP//ZDhhHgMaAAA=",
|
||||
"H4sIAAAAAAAC/9RZzW7kNgx+lULt0RvP/px8ywZtUDQo2jTbSzAIFJuZaGNLrkRnMwj87gUl/47lseMa",
|
||||
"2fQUR6JF8uNHmuI8s1hluZIg0bDomWn4pwCDn1UiwC6ARNBnKoFLt0NrsZII0j7yPE9FzFEoGX41StKa",
|
||||
"ie8h4/SUa5WDxuqoWCVAf3GfA4uYQS3kjpVlYLUKDQmLrp3UNqil1O1XiJGVfTHUBZQB2/EMfk4ELrHt",
|
||||
"Jw13LGI/hi0Aods1YX3uiNpU7YT8D0BAxkXqQSJgOTfmm9LJNEzujM4bMyHTsBMGQb+2+e3mB+9uYUBL",
|
||||
"ns0gSCMZDEHoapkFiF0xuZKm8k1rpS+rlfW4LiTCDjQ5moExfDc3EVp5vzsJmFiLnGxiEWNVTpwmmZCL",
|
||||
"vJifFX7NF8LgIsUCITNzLPhbwDfSVoHBteb7MYuQm4fVYaBDnRF+pUWeKp6swKGiEDaT7pTOOLLILQRT",
|
||||
"6UFCc8lCubQ6QHToOEBlUB3T5Inl1NyvxcGBnv0loHXPDMY+QW7DeraWtUfM8OlvMnCgfxKU2G1MwDIT",
|
||||
"vYDlSlQdw7DCUYKY2TlN0q6qHOZ0wFBg6ofVLUxXiyuSKwP2KIy47Z11q1QKXA5iUEvW2oMDctjXaycb",
|
||||
"ICqEx4J2VZkLssicEo0Fp09XLHDfea31sKl1g1jzAu+Vno9wm49DhGMNHCE5xWWp9sqsOlOFxJHtlahy",
|
||||
"wAZn5zEiNARoDezCGjThOkKRJge8dWV+pJtq6on0zBAgPPm5MAbwAWA1VvacoLJ/zOfxWvoyn8fY3em1",
|
||||
"6sxTDzdVYyXkI09FUv+r0uZRwhPepPAIlKBEjRvSmwKCN1NfDbImkV/QksNTfqXOCq1B4oV1yZs+Vux3",
|
||||
"eJqQAS1AxiOdLSFlVujlZnM1HbdVq7QXdoKuzktFT5xaZG84599ErFWe60gHqNpIXyAGqNcQVuZ7mriA",
|
||||
"GYgLLXD/F8FY54t6EHBa4L0Fn9ost0RUto4wA8Z0qlbEeC5+g6pvFvJOWWcdXZn8016mA/YI2ri27f3J",
|
||||
"5mRD4KgcJM8Fi9jHk/cnG3vhwntrRmghDRsW7MBmBdHUdpG/JixidD84J4nPe3tJYQf3rw+bzRhxGrlw",
|
||||
"cNGwbuTKeBSe2Up87gLUTjf241o6A5DwcMBQLjW3fyPrBpJF1/0QXg9ZWm7pjS7A4XMhknIU5nOwKLcg",
|
||||
"51zzDBCobbiuSEKRaynicqx/PQ46Hf9UQ739btCMhZ4CVwX+ldz/X7ML5E5ImCIWmWwF3y6perf+shw4",
|
||||
"F9aXtRHa1BPPN8ubwUy2XAumO5FC6AYY4wB9sfu/iNRTUjsDhKxIUeRcY0gYvEs4emYcpLAH062QXO+9",
|
||||
"F3bvNO+lXh9MZxbmisVpsgRXEH23NBmZ5qgYAd8Z1MCz/lRnOgqDgY4NYMWe45/+6ptkVvvkk0rb1x3R",
|
||||
"+MX1fQt40p2MlQH7tPk4/VJ/etwxMWx+MvCn1B/KWFMvrNiCkuDOL9fxdPNyT3tZtO35rQqc5TjJDez/",
|
||||
"NBhuMVPEMRjzQ3X0UotbG7s/iRy38rKWXBChRsvbCdLR2relqmJAP9Z1q9Api1hI3X+5Lf8NAAD//0cV",
|
||||
"3sY5HAAA",
|
||||
}
|
||||
|
||||
// GetSwagger returns the content of the embedded swagger specification file
|
||||
|
|
22
api/types.go
22
api/types.go
|
@ -37,6 +37,7 @@ const (
|
|||
type CodeEdit struct {
|
||||
Code string `json:"code"`
|
||||
Description string `json:"description"`
|
||||
Id *openapi_types.UUID `json:"id,omitempty"`
|
||||
}
|
||||
|
||||
// CodeView defines model for codeView.
|
||||
|
@ -49,10 +50,12 @@ type CodeView struct {
|
|||
type GameEdit struct {
|
||||
Description string `json:"description"`
|
||||
Icon openapi_types.UUID `json:"icon"`
|
||||
Id *openapi_types.UUID `json:"id,omitempty"`
|
||||
Points int `json:"points"`
|
||||
Tasks []TaskEdit `json:"tasks"`
|
||||
Title string `json:"title"`
|
||||
Type GameType `json:"type"`
|
||||
Visible bool `json:"visible"`
|
||||
}
|
||||
|
||||
// GameType defines model for gameType.
|
||||
|
@ -74,6 +77,7 @@ 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"`
|
||||
}
|
||||
|
@ -111,12 +115,12 @@ type ErrorResponse struct {
|
|||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// GameAdminResponse defines model for gameAdminResponse.
|
||||
type GameAdminResponse = GameEdit
|
||||
|
||||
// GameListResponse defines model for gameListResponse.
|
||||
type GameListResponse = []GameView
|
||||
|
||||
// GameResponse defines model for gameResponse.
|
||||
type GameResponse = GameView
|
||||
|
||||
// TaskResponse defines model for taskResponse.
|
||||
type TaskResponse = TaskView
|
||||
|
||||
|
@ -172,18 +176,18 @@ type PostUserRegisterJSONBody struct {
|
|||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
// EnterCodeJSONRequestBody defines body for EnterCode for application/json ContentType.
|
||||
type EnterCodeJSONRequestBody EnterCodeJSONBody
|
||||
|
||||
// UploadFileMultipartRequestBody defines body for UploadFile for multipart/form-data ContentType.
|
||||
type UploadFileMultipartRequestBody UploadFileMultipartBody
|
||||
|
||||
// CreateGameJSONRequestBody defines body for CreateGame for application/json ContentType.
|
||||
type CreateGameJSONRequestBody = GameEdit
|
||||
|
||||
// EditGameJSONRequestBody defines body for EditGame for application/json ContentType.
|
||||
type EditGameJSONRequestBody = GameEdit
|
||||
|
||||
// EnterCodeJSONRequestBody defines body for EnterCode for application/json ContentType.
|
||||
type EnterCodeJSONRequestBody EnterCodeJSONBody
|
||||
|
||||
// UploadFileMultipartRequestBody defines body for UploadFile for multipart/form-data ContentType.
|
||||
type UploadFileMultipartRequestBody UploadFileMultipartBody
|
||||
|
||||
// PostUserLoginJSONRequestBody defines body for PostUserLogin for application/json ContentType.
|
||||
type PostUserLoginJSONRequestBody PostUserLoginJSONBody
|
||||
|
||||
|
|
|
@ -14,7 +14,8 @@ import Engine from './pages/Engine'
|
|||
import Quests from './pages/Quests'
|
||||
import User from './pages/User'
|
||||
import { useRole } from './utils/roles'
|
||||
import Quest from './pages/admin/Quest'
|
||||
import EditQuest from './pages/admin/Quest'
|
||||
import AdminQuest from './pages/admin/Quests'
|
||||
|
||||
const router = createBrowserRouter(
|
||||
createRoutesFromElements(
|
||||
|
@ -43,14 +44,19 @@ const router = createBrowserRouter(
|
|||
loader={({ params }) => ajax(`/api/engine/${params.gameId}`).catch(x => { console.log(x); return null })}
|
||||
/>
|
||||
<Route
|
||||
path="admin"
|
||||
element={<Auth role="admin"><NoMatch /></Auth>}
|
||||
path="/admin/quests/new"
|
||||
element={<Auth role="creator"><EditQuest /></Auth>}
|
||||
// loader={() => ajax(`/api/admin/games`)}
|
||||
/>
|
||||
<Route
|
||||
path="quest/new"
|
||||
element={<Auth role="creator"><Quest /></Auth>}
|
||||
// loader={() => ajax(`/api/admin/games`)}
|
||||
path="/admin/quests/:gameId"
|
||||
element={<Auth role="creator"><EditQuest /></Auth>}
|
||||
loader={({ params }) => ajax(`/api/admin/games/${params.gameId}`)}
|
||||
/>
|
||||
<Route
|
||||
path="/admin/quests"
|
||||
element={<Auth role="creator"><AdminQuest /></Auth>}
|
||||
loader={() => ajax('/api/admin/games')}
|
||||
/>
|
||||
|
||||
<Route path="*" element={<NoMatch />} />
|
||||
|
|
|
@ -49,9 +49,9 @@ const AppLayout = () => {
|
|||
|
||||
if (hasRole('creator')) {
|
||||
items.push({
|
||||
key: 'quest/new',
|
||||
label: 'Создать квест',
|
||||
link: '/quest/new'
|
||||
key: 'admin/quests',
|
||||
label: 'Управление квестами',
|
||||
link: '/admin/quests'
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -12,6 +12,9 @@ const Engine = () => {
|
|||
const { message } = App.useApp()
|
||||
|
||||
useEffect(() => {
|
||||
if (!task) {
|
||||
return
|
||||
}
|
||||
switch (task.message) {
|
||||
case 'invalid_code':
|
||||
message.error('Неверный код')
|
||||
|
@ -26,7 +29,7 @@ const Engine = () => {
|
|||
message.success('Код принят, ищите оставшиеся')
|
||||
break
|
||||
}
|
||||
}, [task.message])
|
||||
}, [task])
|
||||
|
||||
const [form] = Form.useForm()
|
||||
const onFinish = ({ code }) => {
|
||||
|
|
|
@ -16,7 +16,7 @@ const Quests = () => {
|
|||
return (<>
|
||||
<Title>Квесты</Title>
|
||||
{games.map(item => renderItem(user, navigate, item))}
|
||||
{!games ? (<strong>Квестов пока не анонсировано</strong>) : null}
|
||||
{games.length === 0 ? (<strong>Квестов пока не анонсировано</strong>) : null}
|
||||
</>)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { useLoaderData, useNavigate } from 'react-router-dom'
|
||||
import { Alert, Button, Card, Form, Input, InputNumber, Popconfirm, Radio, Typography, Upload } from 'antd'
|
||||
import { Alert, Avatar, Button, Card, Form, Input, InputNumber, Popconfirm, Radio, Switch, Typography, Upload } from 'antd'
|
||||
import { UploadOutlined, PlusOutlined, CloseOutlined } from '@ant-design/icons'
|
||||
import { ajax } from '../../utils/fetch'
|
||||
import { useState } from 'react'
|
||||
|
@ -30,7 +30,11 @@ const Quest = () => {
|
|||
}
|
||||
|
||||
const onFinish = (values) => {
|
||||
ajax('/api/games', {
|
||||
let url = '/api/admin/games'
|
||||
if (quest.id) {
|
||||
url = `/api/admin/games/${quest.id}`
|
||||
}
|
||||
ajax(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
|
@ -38,7 +42,7 @@ const Quest = () => {
|
|||
},
|
||||
body: JSON.stringify(values)
|
||||
})
|
||||
.then(g => navigate(`/quest/${g.id}/edit`))
|
||||
.then(g => navigate(`/admin/quests/${g.id}/`))
|
||||
.catch(({ message }) => setError('Ошибка создания'))
|
||||
}
|
||||
|
||||
|
@ -64,6 +68,9 @@ const Quest = () => {
|
|||
Сохранить квест
|
||||
</Button>
|
||||
</Form.Item>
|
||||
<Form.Item label='Опубликован?' name='visible'>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item label='Название' name='title'>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
|
@ -75,6 +82,7 @@ const Quest = () => {
|
|||
label='Иконка'
|
||||
getValueFromEvent={normFile}
|
||||
>
|
||||
{quest.icon ? <Avatar src={`/api/file/${quest.icon}`} /> : null}
|
||||
<Upload name='file' action='/api/file/upload' listType='picture' maxCount={1}>
|
||||
<Button icon={<UploadOutlined />}>Загрузка</Button>
|
||||
</Upload>
|
||||
|
@ -108,7 +116,7 @@ const Quest = () => {
|
|||
// eslint-disable-next-line react/display-name
|
||||
const renderTaskForm = remove => task => (
|
||||
<Card
|
||||
key={task.key}
|
||||
key={task.id}
|
||||
title={`Уровень ${task.key}`}
|
||||
style={{ marginBottom: 8 }}
|
||||
actions={[
|
||||
|
@ -125,6 +133,9 @@ const renderTaskForm = remove => task => (
|
|||
</Popconfirm>
|
||||
]}
|
||||
>
|
||||
<Form.Item name={[task.name, 'id']} hidden>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name={[task.name, 'title']} label='Название уровня' help='ВИДНО игрокам'>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
|
@ -152,19 +163,22 @@ const renderCodeForm = remove => code => (
|
|||
key={code.key}
|
||||
style={{ marginBottom: 8 }}
|
||||
actions={[
|
||||
<Popconfirm
|
||||
key='delete'
|
||||
title='Удалить код?'
|
||||
onConfirm={() => remove(code.name)}
|
||||
okText='Да'
|
||||
cancelText='Нет'
|
||||
>
|
||||
<Button danger>
|
||||
// <Popconfirm
|
||||
// key='delete'
|
||||
// title='Удалить код?'
|
||||
// onConfirm={() => remove(code.name)}
|
||||
// okText='Да'
|
||||
// cancelText='Нет'
|
||||
// >
|
||||
<Button key="delete" danger onClick={() => remove(code.name)}>
|
||||
<CloseOutlined/> Удалить код
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
// </Popconfirm>
|
||||
]}
|
||||
>
|
||||
<Form.Item name={[code.name, 'id']} hidden>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name={[code.name, 'code']} label='Код'>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
|
|
40
frontend/src/pages/admin/Quests.jsx
Normal file
40
frontend/src/pages/admin/Quests.jsx
Normal file
|
@ -0,0 +1,40 @@
|
|||
import { Table, Typography } from 'antd'
|
||||
import { Link, useLoaderData } from 'react-router-dom'
|
||||
|
||||
const { Title } = Typography
|
||||
|
||||
const Quests = () => {
|
||||
const quests = useLoaderData()
|
||||
|
||||
return (
|
||||
<>
|
||||
<Title>Управление своими квестами</Title>
|
||||
<Link to="/admin/quests/new">Создать новый квест</Link>
|
||||
<Table
|
||||
dataSource={quests}
|
||||
columns={[
|
||||
{
|
||||
title: 'UUID',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
render: uid => <Link to={`/admin/quests/${uid}`}>${uid}</Link>
|
||||
},
|
||||
{
|
||||
title: 'Название',
|
||||
dataIndex: 'title',
|
||||
key: 'title'
|
||||
},
|
||||
{
|
||||
title: 'Тип',
|
||||
dataIndex: 'type',
|
||||
key: 'type',
|
||||
render: type => type === 'virtual' ? 'Виртуальный' : 'Полевой'
|
||||
}
|
||||
]}
|
||||
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Quests
|
|
@ -32,16 +32,32 @@ func (a *Admin) CreateGame(ctx echo.Context) error {
|
|||
return err
|
||||
}
|
||||
|
||||
return ctx.JSON(http.StatusCreated, api.GameResponse{
|
||||
Authors: make([]api.UserView, 0, len(game.Authors)),
|
||||
CreatedAt: game.CreatedAt.Format(time.RFC3339),
|
||||
tasks := make([]api.TaskEdit, 0, len(game.Tasks))
|
||||
|
||||
for _, t := range game.Tasks {
|
||||
codes := make([]api.CodeEdit, 0, len(t.Codes))
|
||||
for _, c := range t.Codes {
|
||||
codes = append(codes, api.CodeEdit{
|
||||
Code: c.Code,
|
||||
Description: c.Description,
|
||||
})
|
||||
}
|
||||
tasks = append(tasks, api.TaskEdit{
|
||||
Codes: codes,
|
||||
Text: t.Text,
|
||||
Title: t.Title,
|
||||
})
|
||||
}
|
||||
|
||||
return ctx.JSON(http.StatusOK, api.GameAdminResponse{
|
||||
Description: game.Description,
|
||||
Icon: game.IconID,
|
||||
Id: game.ID,
|
||||
Id: &game.ID,
|
||||
Points: game.Points,
|
||||
TaskCount: len(game.Tasks),
|
||||
Tasks: tasks,
|
||||
Title: game.Title,
|
||||
Type: api.MapGameTypeReverse(game.Type),
|
||||
Visible: game.Visible,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -49,37 +65,149 @@ func (a *Admin) CreateGame(ctx echo.Context) error {
|
|||
func (a *Admin) EditGame(ctx echo.Context, uid uuid.UUID) error {
|
||||
user := contextlib.GetUser(ctx)
|
||||
req := &api.GameEditRequest{}
|
||||
if err := ctx.Bind(req); err != nil {
|
||||
|
||||
game, err := a.GameService.GetByID(ctx.Request().Context(), uid)
|
||||
if err != nil {
|
||||
return echo.ErrNotFound
|
||||
}
|
||||
if user.Role != models.RoleAdmin {
|
||||
isAuthor := false
|
||||
for _, u := range game.Authors {
|
||||
if u.ID == user.ID {
|
||||
isAuthor = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !isAuthor {
|
||||
return echo.ErrForbidden
|
||||
}
|
||||
}
|
||||
if err = ctx.Bind(req); err != nil {
|
||||
return err
|
||||
}
|
||||
game = a.mapCreateGameRequest(req, user)
|
||||
|
||||
game := a.mapCreateGameRequest(req, user)
|
||||
|
||||
var err error
|
||||
game, err = a.GameService.UpdateGame(ctx.Request().Context(), uid, game)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return ctx.JSON(http.StatusCreated, api.GameResponse{
|
||||
Authors: make([]api.UserView, 0, len(game.Authors)),
|
||||
CreatedAt: game.CreatedAt.Format(time.RFC3339),
|
||||
tasks := make([]api.TaskEdit, 0, len(game.Tasks))
|
||||
|
||||
for _, t := range game.Tasks {
|
||||
t := t
|
||||
codes := make([]api.CodeEdit, 0, len(t.Codes))
|
||||
for _, c := range t.Codes {
|
||||
codes = append(codes, api.CodeEdit{
|
||||
Id: &c.ID,
|
||||
Code: c.Code,
|
||||
Description: c.Description,
|
||||
})
|
||||
}
|
||||
tasks = append(tasks, api.TaskEdit{
|
||||
Id: &t.ID,
|
||||
Codes: codes,
|
||||
Text: t.Text,
|
||||
Title: t.Title,
|
||||
})
|
||||
}
|
||||
|
||||
return ctx.JSON(http.StatusOK, api.GameAdminResponse{
|
||||
Description: game.Description,
|
||||
Icon: game.IconID,
|
||||
Id: game.ID,
|
||||
Id: &game.ID,
|
||||
Points: game.Points,
|
||||
TaskCount: len(game.Tasks),
|
||||
Tasks: tasks,
|
||||
Title: game.Title,
|
||||
Type: api.MapGameTypeReverse(game.Type),
|
||||
Visible: game.Visible,
|
||||
})
|
||||
}
|
||||
|
||||
// (GET /games/{uid})
|
||||
func (a *Admin) GetGameByAdmin(ctx echo.Context, uid uuid.UUID) error {
|
||||
user := contextlib.GetUser(ctx)
|
||||
|
||||
game, err := a.GameService.GetByID(ctx.Request().Context(), uid)
|
||||
if err != nil {
|
||||
return echo.ErrNotFound
|
||||
}
|
||||
if user.Role != models.RoleAdmin {
|
||||
isAuthor := false
|
||||
for _, u := range game.Authors {
|
||||
if u.ID == user.ID {
|
||||
isAuthor = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !isAuthor {
|
||||
return echo.ErrForbidden
|
||||
}
|
||||
}
|
||||
|
||||
tasks := make([]api.TaskEdit, 0, len(game.Tasks))
|
||||
|
||||
for _, t := range game.Tasks {
|
||||
t := t
|
||||
codes := make([]api.CodeEdit, 0, len(t.Codes))
|
||||
for _, c := range t.Codes {
|
||||
codes = append(codes, api.CodeEdit{
|
||||
Id: &c.ID,
|
||||
Code: c.Code,
|
||||
Description: c.Description,
|
||||
})
|
||||
}
|
||||
tasks = append(tasks, api.TaskEdit{
|
||||
Id: &t.ID,
|
||||
Codes: codes,
|
||||
Text: t.Text,
|
||||
Title: t.Title,
|
||||
})
|
||||
}
|
||||
|
||||
return ctx.JSON(http.StatusOK, api.GameAdminResponse{
|
||||
Description: game.Description,
|
||||
Icon: game.IconID,
|
||||
Id: &game.ID,
|
||||
Points: game.Points,
|
||||
Tasks: tasks,
|
||||
Title: game.Title,
|
||||
Type: api.MapGameTypeReverse(game.Type),
|
||||
Visible: game.Visible,
|
||||
})
|
||||
}
|
||||
|
||||
func (a *Admin) ListGamesByAdmin(ctx echo.Context) error {
|
||||
user := contextlib.GetUser(ctx)
|
||||
|
||||
games, err := a.GameService.ListByAuthor(ctx.Request().Context(), user)
|
||||
if err != nil {
|
||||
return echo.ErrNotFound
|
||||
}
|
||||
|
||||
resp := make(api.GameListResponse, 0, len(games))
|
||||
for _, game := range games {
|
||||
gv := api.GameView{
|
||||
Id: game.ID,
|
||||
Title: game.Title,
|
||||
Type: api.MapGameTypeReverse(game.Type),
|
||||
Points: game.Points,
|
||||
TaskCount: len(game.Tasks),
|
||||
CreatedAt: game.CreatedAt.Format(time.RFC3339),
|
||||
Icon: game.IconID,
|
||||
}
|
||||
resp = append(resp, gv)
|
||||
}
|
||||
|
||||
return ctx.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
func (*Admin) mapCreateGameRequest(req *api.GameEditRequest, user *models.User) *models.Game {
|
||||
game := &models.Game{
|
||||
Model: models.Model{
|
||||
ID: uuid.New(),
|
||||
},
|
||||
Visible: false,
|
||||
Visible: req.Visible,
|
||||
Title: req.Title,
|
||||
Description: req.Description,
|
||||
Authors: []*models.User{
|
||||
|
@ -91,30 +219,28 @@ func (*Admin) mapCreateGameRequest(req *api.GameEditRequest, user *models.User)
|
|||
IconID: req.Icon,
|
||||
}
|
||||
for order, te := range req.Tasks {
|
||||
if te.Id == nil {
|
||||
u := uuid.New()
|
||||
te.Id = &u
|
||||
}
|
||||
task := &models.Task{
|
||||
Model: models.Model{
|
||||
ID: uuid.New(),
|
||||
ID: *te.Id,
|
||||
},
|
||||
Title: te.Title,
|
||||
Text: te.Text,
|
||||
MaxTime: 0,
|
||||
// Solutions: make([]*models.Solution, 0, len(te.Solutions)),
|
||||
Codes: make([]*models.Code, 0, len(te.Codes)),
|
||||
TaskOrder: uint(order),
|
||||
}
|
||||
// for _, s := range te.Solutions {
|
||||
// task.Solutions = append(task.Solutions, &models.Solution{
|
||||
// Model: models.Model{
|
||||
// ID: uuid.New(),
|
||||
// },
|
||||
// After: s.After,
|
||||
// Text: s.Text,
|
||||
// })
|
||||
// }
|
||||
|
||||
for _, ce := range te.Codes {
|
||||
if ce.Id == nil {
|
||||
u := uuid.New()
|
||||
ce.Id = &u
|
||||
}
|
||||
task.Codes = append(task.Codes, &models.Code{
|
||||
Model: models.Model{
|
||||
ID: uuid.New(),
|
||||
ID: *ce.Id,
|
||||
},
|
||||
Code: ce.Code,
|
||||
Description: ce.Description,
|
||||
|
|
|
@ -41,8 +41,7 @@ func (gs *Game) List(ctx context.Context) ([]*models.Game, error) {
|
|||
Order("created_at DESC").
|
||||
Preload("Tasks").
|
||||
Preload("Authors").
|
||||
Find(&games).
|
||||
Limit(20).
|
||||
Find(&games, "visible = true").
|
||||
Error
|
||||
}
|
||||
|
||||
|
@ -54,14 +53,17 @@ func (gs *Game) GetTaskID(ctx context.Context, id uuid.UUID) (*models.Task, erro
|
|||
|
||||
func (gs *Game) ListByAuthor(ctx context.Context, author *models.User) ([]*models.Game, error) {
|
||||
games := make([]*models.Game, 0)
|
||||
|
||||
return games, gs.DB.
|
||||
model := gs.DB.
|
||||
WithContext(ctx).
|
||||
Model(&models.Game{}).
|
||||
Preload("Authors", gs.DB.Where("id = ?", author.ID)).
|
||||
Model(&models.Game{})
|
||||
if author.Role == models.RoleCreator {
|
||||
model.Preload("Authors", gs.DB.Where("id = ?", author.ID))
|
||||
}
|
||||
|
||||
return games,
|
||||
model.
|
||||
Order("created_at DESC").
|
||||
Find(&games).
|
||||
Limit(20).
|
||||
Error
|
||||
}
|
||||
|
||||
|
@ -75,8 +77,10 @@ func (gs *Game) CreateGame(ctx context.Context, game *models.Game) (*models.Game
|
|||
func (gs *Game) UpdateGame(ctx context.Context, uid uuid.UUID, game *models.Game) (*models.Game, error) {
|
||||
game.ID = uid
|
||||
|
||||
return game, gs.DB.
|
||||
Session(&gorm.Session{FullSaveAssociations: true}).
|
||||
db := gs.DB
|
||||
|
||||
return game, db.Debug().
|
||||
Session(&gorm.Session{FullSaveAssociations: true}).Omit("created_at").
|
||||
Save(game).
|
||||
Error
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue